Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
429a4f1330 | ||
|
|
97fca76108 | ||
|
|
cbccfb600a | ||
|
|
f48e04ed36 | ||
|
|
05e6ced31d | ||
|
|
ea1f836bf9 | ||
|
|
2ac0843cb2 | ||
|
|
4cb6f6882a | ||
|
|
0e838e9dd1 | ||
|
|
8780b40bb7 | ||
|
|
de1eb7e406 | ||
|
|
76aaf06c0b | ||
|
|
0d0326769c | ||
|
|
2dba90bd0f | ||
|
|
b295c8b97a | ||
|
|
c2b8bf83de | ||
|
|
77ed7a24c0 | ||
|
|
f8aa0c5cdf | ||
|
|
b7b8d09e40 | ||
|
|
1096c3167a | ||
|
|
445287c95c | ||
|
|
750217631d | ||
|
|
9b4cc329ce | ||
|
|
02048eab77 | ||
|
|
d8d51221ee |
No files matched your search
@@ -25,6 +25,12 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"]
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["core", "macro", "rig-input"]
|
members = ["core", "macro", "rig-input"]
|
||||||
|
|
||||||
|
# Full debug info was the bulk of what the linker wrote here and almost none of
|
||||||
|
# what anything read. `dev` keeps line tables and scopes, which is what stepping
|
||||||
|
# through an example wants; the tests keep the line tables alone, which is what
|
||||||
|
# a backtrace reads. Measured when the tests became one target: relinking them
|
||||||
|
# went from 9.8 s to 7.7 s with these, and target/ from 45 GB to 13 GB with the
|
||||||
|
# two changes together.
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug = 1
|
debug = 1
|
||||||
|
|
||||||
|
|||||||
+9
-65
@@ -1,7 +1,7 @@
|
|||||||
use crate::{UiNum, util::Vec2};
|
use crate::{UiNum, util::Vec2};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{Debug, Display, Formatter},
|
fmt::{Debug, Display, Formatter},
|
||||||
ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign},
|
ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A number held as a whole count of `1 / 2^SHIFT`.
|
/// A number held as a whole count of `1 / 2^SHIFT`.
|
||||||
@@ -115,7 +115,10 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
|||||||
/// that leaves out the thing it was measured from.
|
/// that leaves out the thing it was measured from.
|
||||||
pub const fn ceil_from_f32(v: f32) -> Self {
|
pub const fn ceil_from_f32(v: f32) -> Self {
|
||||||
let nearest = Self::from_f32(v);
|
let nearest = Self::from_f32(v);
|
||||||
match nearest.to_f32() < v {
|
// The top of the grid has no step above it, and stepping past it
|
||||||
|
// wraps to the bottom. A value out there is a caller's mistake, and
|
||||||
|
// the clamp `from_f32` already made is the answer to it.
|
||||||
|
match nearest.to_f32() < v && nearest.0 != Self::MAX.0 {
|
||||||
true => nearest.next_up(),
|
true => nearest.next_up(),
|
||||||
false => nearest,
|
false => nearest,
|
||||||
}
|
}
|
||||||
@@ -131,14 +134,6 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
|||||||
self.0 as f32 / Self::one().0 as f32
|
self.0 as f32 / Self::one().0 as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The same value on another grid, rounded where the new one is coarser.
|
|
||||||
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
|
|
||||||
Fixed(match TO >= SHIFT {
|
|
||||||
true => self.0 << (TO - SHIFT),
|
|
||||||
false => shift_round(self.0 as i64, SHIFT - TO) as i32,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn add(self, rhs: Self) -> Self {
|
pub const fn add(self, rhs: Self) -> Self {
|
||||||
Self(self.0.wrapping_add(rhs.0))
|
Self(self.0.wrapping_add(rhs.0))
|
||||||
}
|
}
|
||||||
@@ -180,21 +175,6 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
|||||||
Self(div_round(self.0 as i64, by as i64) as i32)
|
Self(div_round(self.0 as i64, by as i64) as i32)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 answers with the end
|
|
||||||
/// of the range so that a release build lays out something absurd rather
|
|
||||||
/// than dying.
|
|
||||||
pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self {
|
|
||||||
debug_assert!(by.0 != 0, "dividing by a length of zero");
|
|
||||||
if by.0 == 0 {
|
|
||||||
return match self.0 < 0 {
|
|
||||||
true => Self::MIN,
|
|
||||||
false => Self::MAX,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Self(div_round((self.0 as i64) << BY, by.0 as i64) as i32)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `num / den` on *this* grid rather than on theirs, for weights coarser
|
/// `num / den` on *this* grid rather than on theirs, for weights coarser
|
||||||
/// than the share they divide.
|
/// than the share they divide.
|
||||||
pub const fn ratio<const OF: u32>(num: Fixed<OF>, den: Fixed<OF>) -> Self {
|
pub const fn ratio<const OF: u32>(num: Fixed<OF>, den: Fixed<OF>) -> Self {
|
||||||
@@ -246,16 +226,6 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Back to a single step, rounding halves away from zero so that a value and
|
|
||||||
/// its negation round to the same distance.
|
|
||||||
const fn shift_round(v: i64, bits: u32) -> i64 {
|
|
||||||
let half = (1i64 << bits) >> 1;
|
|
||||||
match v < 0 {
|
|
||||||
true => -((-v + half) >> bits),
|
|
||||||
false => (v + half) >> bits,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn div_round(num: i64, den: i64) -> i64 {
|
const fn div_round(num: i64, den: i64) -> i64 {
|
||||||
let (q, rem) = (num / den, num % den);
|
let (q, rem) = (num / den, num % den);
|
||||||
match rem.unsigned_abs() * 2 >= den.unsigned_abs() {
|
match rem.unsigned_abs() * 2 >= den.unsigned_abs() {
|
||||||
@@ -335,14 +305,6 @@ const impl<const SHIFT: u32, const BY: u32> Mul<Fixed<BY>> for Fixed<SHIFT> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const impl<const SHIFT: u32, const BY: u32> Div<Fixed<BY>> for Fixed<SHIFT> {
|
|
||||||
type Output = Self;
|
|
||||||
|
|
||||||
fn div(self, rhs: Fixed<BY>) -> Self {
|
|
||||||
Fixed::div(self, rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const SHIFT: u32> Display for Fixed<SHIFT> {
|
impl<const SHIFT: u32> Display for Fixed<SHIFT> {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
Display::fmt(&self.to_f32(), f)
|
Display::fmt(&self.to_f32(), f)
|
||||||
@@ -493,19 +455,6 @@ mod tests {
|
|||||||
assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2));
|
assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A division rounds to the nearest step, so it cannot put back the
|
|
||||||
/// steps a truncating multiply dropped: a round trip comes back short,
|
|
||||||
/// never long, and by the few steps the two operations gave up.
|
|
||||||
#[test]
|
|
||||||
fn dividing_by_a_fraction_cannot_undo_a_truncating_multiply() {
|
|
||||||
let third = Rel::ONE / Rel::from_int(3);
|
|
||||||
let len = Px::from_int(300);
|
|
||||||
let back = len * third / third;
|
|
||||||
assert!(back <= len, "{back:?} is longer than {len:?}");
|
|
||||||
assert!(len - back <= Px::from_raw(3), "{back:?} against {len:?}");
|
|
||||||
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The bound a greedy line break needs: the width it was measured at is
|
/// The bound a greedy line break needs: the width it was measured at is
|
||||||
/// not on the grid, and the narrowest box the break still holds for is
|
/// not on the grid, and the narrowest box the break still holds for is
|
||||||
/// the step at or above it, never the one below.
|
/// the step at or above it, never the one below.
|
||||||
@@ -529,15 +478,10 @@ mod tests {
|
|||||||
fn a_number_from_outside_is_clamped_to_the_grid() {
|
fn a_number_from_outside_is_clamped_to_the_grid() {
|
||||||
assert_eq!(Px::from_f32(1e12), Px::MAX);
|
assert_eq!(Px::from_f32(1e12), Px::MAX);
|
||||||
assert_eq!(Px::from_f32(-1e12), Px::MIN);
|
assert_eq!(Px::from_f32(-1e12), Px::MIN);
|
||||||
}
|
// The ceiling is the other way in from a float, and there is no step
|
||||||
|
// above the top of the grid for it to take.
|
||||||
#[test]
|
assert_eq!(Px::ceil_from_f32(1e12), Px::MAX);
|
||||||
fn a_coarser_grid_rounds_and_a_finer_one_does_not() {
|
assert_eq!(Px::ceil_from_f32(-1e12), Px::MIN);
|
||||||
// A third, which neither grid holds exactly.
|
|
||||||
let third = Rel::ONE / Rel::from_int(3);
|
|
||||||
assert_eq!(third.to_scale::<6>(), Fixed::<6>::from_raw(21));
|
|
||||||
let coarse = Fixed::<6>::from_raw(21);
|
|
||||||
assert_eq!(coarse.to_scale::<24>().to_scale::<6>(), coarse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ labelled! {
|
|||||||
TextBreaks = "text line breaks",
|
TextBreaks = "text line breaks",
|
||||||
GlyphPlacements = "glyph placements",
|
GlyphPlacements = "glyph placements",
|
||||||
OutsidePinnedLen = "reuse outside: the length it was pinned to",
|
OutsidePinnedLen = "reuse outside: the length it was pinned to",
|
||||||
|
OutsideWindow = "reuse outside: this window",
|
||||||
OutsideRelBase = "reuse outside: a rel base",
|
OutsideRelBase = "reuse outside: a rel base",
|
||||||
OutsideRegion = "reuse outside: a region length",
|
OutsideRegion = "reuse outside: a region length",
|
||||||
}
|
}
|
||||||
@@ -352,7 +353,7 @@ pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A drawing that cannot be reused because the box on offer is outside what
|
/// A drawing that cannot be reused because the box on offer is outside what
|
||||||
/// it holds for, and which of the three contracts said so. They overlap: a
|
/// it holds for, and which of the four contracts said so. They overlap: a
|
||||||
/// drawing can be outside two of them at once, and counting each is what
|
/// drawing can be outside two of them at once, and counting each is what
|
||||||
/// says where a rel base redrawing more than it should is coming from.
|
/// says where a rel base redrawing more than it should is coming from.
|
||||||
pub(crate) fn outside(
|
pub(crate) fn outside(
|
||||||
@@ -362,24 +363,35 @@ pub(crate) fn outside(
|
|||||||
rel_base: UiVec2,
|
rel_base: UiVec2,
|
||||||
window: PxVec2,
|
window: PxVec2,
|
||||||
) {
|
) {
|
||||||
|
let mut reasons = 0;
|
||||||
|
let mut why = |counter| {
|
||||||
|
bump(counter);
|
||||||
|
reasons += 1;
|
||||||
|
};
|
||||||
for axis in Axis::BOTH {
|
for axis in Axis::BOTH {
|
||||||
let holds = holds[axis];
|
let holds = holds[axis];
|
||||||
let len = region[axis].len();
|
let len = region[axis].len();
|
||||||
let window = window[axis];
|
let window = window[axis];
|
||||||
if holds.region_len.is_some_and(|pinned| pinned != len) {
|
if holds.region_len.is_some_and(|pinned| pinned != len) {
|
||||||
bump(Counter::OutsidePinnedLen);
|
why(Counter::OutsidePinnedLen);
|
||||||
}
|
}
|
||||||
if !holds.window.contains(window)
|
if !holds.window.contains(window) {
|
||||||
|| holds
|
why(Counter::OutsideWindow);
|
||||||
|
}
|
||||||
|
if holds
|
||||||
.rel_base
|
.rel_base
|
||||||
.is_some_and(|pinned| pinned != rel_base[axis])
|
.is_some_and(|pinned| pinned != rel_base[axis])
|
||||||
{
|
{
|
||||||
bump(Counter::OutsideRelBase);
|
why(Counter::OutsideRelBase);
|
||||||
}
|
}
|
||||||
if !holds.region.contains(len.to_px(window)) {
|
if !holds.region.contains(len.to_px(window)) {
|
||||||
bump(Counter::OutsideRegion);
|
why(Counter::OutsideRegion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// These four are `AxisHolds::contains`'s four clauses written out again,
|
||||||
|
// because the report wants which one refused rather than that one did. A
|
||||||
|
// clause added there and not here would leave a refusal unexplained.
|
||||||
|
debug_assert!(reasons > 0, "a reuse was refused for no reason counted");
|
||||||
bump(Counter::ReuseOutside);
|
bump(Counter::ReuseOutside);
|
||||||
reuse(id, ReuseOutcome::Outside);
|
reuse(id, ReuseOutcome::Outside);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{Px, Rel};
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Align {
|
pub struct Align {
|
||||||
pub x: Option<AxisAlign>,
|
pub x: Option<AxisAlign>,
|
||||||
pub y: Option<AxisAlign>,
|
pub y: Option<AxisAlign>,
|
||||||
@@ -84,14 +84,6 @@ pub struct RegionAlign {
|
|||||||
pub y: AxisAlign,
|
pub y: AxisAlign,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegionAlign {
|
|
||||||
/// Both axes at the near edge: the start of a box in its own orientation.
|
|
||||||
pub const NEAR: Self = Self {
|
|
||||||
x: AxisAlign::NEG,
|
|
||||||
y: AxisAlign::NEG,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RegionAlign {
|
impl RegionAlign {
|
||||||
pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG);
|
pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG);
|
||||||
pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG);
|
pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG);
|
||||||
@@ -159,13 +151,15 @@ impl Vec2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Len {
|
impl Len {
|
||||||
|
/// This length placed in the box it is measured in: the alignment names a
|
||||||
|
/// point along that box, and the two ends are that point less the part of
|
||||||
|
/// the length falling before it and plus the part falling after.
|
||||||
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
||||||
let rel = align.rel();
|
let rel = align.rel();
|
||||||
let rest = Rel::ONE.sub(rel);
|
|
||||||
let at = Len::from_parts(rel, Px::ZERO);
|
let at = Len::from_parts(rel, Px::ZERO);
|
||||||
UiSpan {
|
UiSpan {
|
||||||
start: Len::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))),
|
start: at - self.scale(rel),
|
||||||
end: Len::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))),
|
end: at + self.scale(Rel::ONE.sub(rel)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,3 +214,4 @@ impl RegionAlign {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl_axis_index!(RegionAlign => AxisAlign);
|
impl_axis_index!(RegionAlign => AxisAlign);
|
||||||
|
impl_axis_index!(Align => Option<AxisAlign>);
|
||||||
@@ -11,15 +11,6 @@ pub enum Axis {
|
|||||||
impl Axis {
|
impl Axis {
|
||||||
/// Both of them, for the layout code that asks the same question of each.
|
/// Both of them, for the layout code that asks the same question of each.
|
||||||
pub const BOTH: [Self; 2] = [Self::X, Self::Y];
|
pub const BOTH: [Self; 2] = [Self::X, Self::Y];
|
||||||
|
|
||||||
/// A per-axis pair with `aligned` on this axis and `ortho` on the other,
|
|
||||||
/// which is what `from_axis` does for a vector.
|
|
||||||
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] {
|
|
||||||
match self {
|
|
||||||
Self::X => [aligned, ortho],
|
|
||||||
Self::Y => [ortho, aligned],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Not for Axis {
|
impl std::ops::Not for Axis {
|
||||||
@@ -80,50 +71,5 @@ impl Vec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const trait AxisT {
|
|
||||||
fn get() -> Axis;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct XAxis;
|
|
||||||
const impl AxisT for XAxis {
|
|
||||||
fn get() -> Axis {
|
|
||||||
Axis::X
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct YAxis;
|
|
||||||
const impl AxisT for YAxis {
|
|
||||||
fn get() -> Axis {
|
|
||||||
Axis::Y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
|
||||||
pub struct BothAxis<T> {
|
|
||||||
pub x: T,
|
|
||||||
pub y: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> BothAxis<T> {
|
|
||||||
pub const fn axis<A: const AxisT>(&mut self) -> &mut T {
|
|
||||||
match A::get() {
|
|
||||||
Axis::X => &mut self.x,
|
|
||||||
Axis::Y => &mut self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn take_axis<A: const AxisT>(self) -> T {
|
|
||||||
match A::get() {
|
|
||||||
Axis::X => self.x,
|
|
||||||
Axis::Y => self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn axis_dyn(&mut self, axis: Axis) -> &mut T {
|
|
||||||
match axis {
|
|
||||||
Axis::X => &mut self.x,
|
|
||||||
Axis::Y => &mut self.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>);
|
impl_axis_index!({const SHIFT: u32} FixedVec2<SHIFT> => Fixed<SHIFT>);
|
||||||
impl_axis_index!(Vec2 => f32);
|
impl_axis_index!(Vec2 => f32);
|
||||||
@@ -23,6 +23,14 @@ pub struct LayoutLen {
|
|||||||
pub leftover: Weight,
|
pub leftover: Weight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A bare number is pixels, which is the one length that needs no box to be
|
||||||
|
/// read in.
|
||||||
|
impl<N: UiNum> From<N> for Len {
|
||||||
|
fn from(value: N) -> Self {
|
||||||
|
Len::px(value.to_f32())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<N: UiNum> From<N> for LayoutLen {
|
impl<N: UiNum> From<N> for LayoutLen {
|
||||||
fn from(value: N) -> Self {
|
fn from(value: N) -> Self {
|
||||||
LayoutLen::px(value.to_f32())
|
LayoutLen::px(value.to_f32())
|
||||||
@@ -148,20 +156,20 @@ impl LayoutLen {
|
|||||||
/// Only pixels: the same number of them whatever box it lands in, and
|
/// Only pixels: the same number of them whatever box it lands in, and
|
||||||
/// whatever anyone else in the row asks for. A length that is any part
|
/// whatever anyone else in the row asks for. A length that is any part
|
||||||
/// of a box or of what is left over is not one.
|
/// of a box or of what is left over is not one.
|
||||||
pub fn is_px(self) -> bool {
|
pub fn is_px(&self) -> bool {
|
||||||
self.rel == Rel::ZERO && self.leftover == Weight::ZERO
|
self.rel == Rel::ZERO && self.leftover == Weight::ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nothing but a claim on what is left over, so there is no length here
|
/// Nothing but a claim on what is left over, so there is no length here
|
||||||
/// at all where nothing is.
|
/// at all where nothing is.
|
||||||
pub fn is_only_leftover(self) -> bool {
|
pub fn is_only_leftover(&self) -> bool {
|
||||||
self.leftover > Weight::ZERO && self.without_leftover() == Len::ZERO
|
self.leftover > Weight::ZERO && self.without_leftover() == Len::ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This as a length of a box, where it is one. `leftover` is not: a
|
/// This as a length of a box, where it is one. `leftover` is not: a
|
||||||
/// share of what is left over is a length only to whoever divides one,
|
/// share of what is left over is a length only to whoever divides one,
|
||||||
/// so it passes up in the reported size instead and is resolved there.
|
/// so it passes up in the reported size instead and is resolved there.
|
||||||
pub fn declared(self) -> Option<Len> {
|
pub fn declared(&self) -> Option<Len> {
|
||||||
(self.leftover == Weight::ZERO).then(|| self.without_leftover())
|
(self.leftover == Weight::ZERO).then(|| self.without_leftover())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +177,7 @@ impl LayoutLen {
|
|||||||
/// anyone not dividing a box between siblings, where a share is a claim
|
/// anyone not dividing a box between siblings, where a share is a claim
|
||||||
/// on someone else's room rather than a length of its own.
|
/// on someone else's room rather than a length of its own.
|
||||||
/// [`Self::apply_leftover`] is the opposite reading of the same value.
|
/// [`Self::apply_leftover`] is the opposite reading of the same value.
|
||||||
pub const fn without_leftover(self) -> Len {
|
pub const fn without_leftover(&self) -> Len {
|
||||||
Len::from_parts(self.rel, self.px)
|
Len::from_parts(self.rel, self.px)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +247,12 @@ impl std::fmt::Display for Size {
|
|||||||
|
|
||||||
impl std::fmt::Display for LayoutLen {
|
impl std::fmt::Display for LayoutLen {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
// A part that is zero is left out, so a length that is zero all
|
||||||
|
// through would print as nothing -- which reads as no length at all
|
||||||
|
// wherever one is printed beside something that has none.
|
||||||
|
if *self == Self::ZERO {
|
||||||
|
return write!(f, "0 px;");
|
||||||
|
}
|
||||||
if self.px != Px::ZERO {
|
if self.px != Px::ZERO {
|
||||||
write!(f, "{} px;", self.px)?;
|
write!(f, "{} px;", self.px)?;
|
||||||
}
|
}
|
||||||
@@ -253,3 +267,24 @@ impl std::fmt::Display for LayoutLen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl_axis_index!(Size => LayoutLen);
|
impl_axis_index!(Size => LayoutLen);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// What a request prints as is how a failing case is read and written out
|
||||||
|
/// again, and a length that printed as nothing could not be told from a
|
||||||
|
/// widget that has no rule at all.
|
||||||
|
#[test]
|
||||||
|
fn every_length_prints_as_something() {
|
||||||
|
for len in [
|
||||||
|
LayoutLen::ZERO,
|
||||||
|
LayoutLen::px(8),
|
||||||
|
LayoutLen::rel(0.5),
|
||||||
|
LayoutLen::LEFTOVER,
|
||||||
|
] {
|
||||||
|
assert!(!len.to_string().is_empty(), "{len:?} printed as nothing");
|
||||||
|
}
|
||||||
|
assert_eq!(LayoutLen::ZERO.to_string(), "0 px;");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -281,15 +281,6 @@ impl UiSpan {
|
|||||||
pub const fn len(&self) -> Len {
|
pub const fn len(&self) -> Len {
|
||||||
self.end - self.start
|
self.end - self.start
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Both ends by the same amount, which is what moving a box without
|
|
||||||
/// changing its length does to every part of it.
|
|
||||||
pub const fn translated(self, by: Len) -> Self {
|
|
||||||
Self {
|
|
||||||
start: self.start + by,
|
|
||||||
end: self.end + by,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -300,17 +291,6 @@ pub struct UiRegion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UiRegion {
|
impl UiRegion {
|
||||||
/// Every part of the box by the same amount on each axis. Done to the
|
|
||||||
/// whole region rather than an end at a time, because that is what it is
|
|
||||||
/// -- and because four adds in a row are four adds, where four asked for
|
|
||||||
/// separately are four sequences.
|
|
||||||
pub const fn translated(self, x: Len, y: Len) -> Self {
|
|
||||||
Self {
|
|
||||||
x: self.x.translated(x),
|
|
||||||
y: self.y.translated(y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const FULL: Self = Self {
|
pub const FULL: Self = Self {
|
||||||
x: UiSpan::FULL,
|
x: UiSpan::FULL,
|
||||||
y: UiSpan::FULL,
|
y: UiSpan::FULL,
|
||||||
|
|||||||
+36
-34
@@ -133,10 +133,6 @@ impl TextBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_empty() -> Self {
|
|
||||||
Self::new("")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn text(&self) -> &str {
|
pub fn text(&self) -> &str {
|
||||||
&self.text
|
&self.text
|
||||||
}
|
}
|
||||||
@@ -198,41 +194,25 @@ impl TextBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
|
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
|
||||||
let layout_key = LayoutKey {
|
// Asked of the attrs it was given rather than of a copy: copying one
|
||||||
attrs: attrs.clone(),
|
// allocates wherever its family is named, and the hit below is what
|
||||||
max_width: width,
|
// this cache is for.
|
||||||
};
|
let same_shaping = self
|
||||||
if self.layout_key.as_ref() == Some(&layout_key) {
|
.layout_key
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
.as_ref()
|
||||||
diag::bump(Counter::TextShapeHits);
|
.is_some_and(|key| key.attrs == *attrs);
|
||||||
return;
|
if same_shaping
|
||||||
}
|
&& let Some(key) = &self.layout_key
|
||||||
// A greedy break at one width is the same break at every width down
|
&& self.breaks_the_same(key.max_width, width)
|
||||||
// to the longest line it produced: each line still fits, and none can
|
|
||||||
// take a word that would not fit in the wider box. So the layout in
|
|
||||||
// hand already answers, and re-breaking would only be work.
|
|
||||||
//
|
|
||||||
// At the longest line exactly, with no margin below it. A narrower
|
|
||||||
// width really does break differently, so answering one from the
|
|
||||||
// break in hand is how a warm tree keeps lines a cold tree would
|
|
||||||
// never produce. The margin was here because a text reports the
|
|
||||||
// width it used and a parent hands that back; the report is the step
|
|
||||||
// at or above its longest line now, so what comes back fits.
|
|
||||||
if let Some(key) = &self.layout_key
|
|
||||||
&& key.attrs == *attrs
|
|
||||||
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
|
||||||
&& want <= broke_at
|
|
||||||
&& want >= self.layout.width()
|
|
||||||
{
|
{
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::TextShapeHits);
|
diag::bump(Counter::TextShapeHits);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let same_shaping = self
|
let old_key = self.layout_key.replace(LayoutKey {
|
||||||
.layout_key
|
attrs: attrs.clone(),
|
||||||
.as_ref()
|
max_width: width,
|
||||||
.is_some_and(|key| key.attrs == *attrs);
|
});
|
||||||
let old_key = self.layout_key.replace(layout_key);
|
|
||||||
// The glyphs it holds are of the width it held, which the layout may
|
// The glyphs it holds are of the width it held, which the layout may
|
||||||
// well come back to.
|
// well come back to.
|
||||||
if let Some(key) = old_key
|
if let Some(key) = old_key
|
||||||
@@ -272,6 +252,28 @@ impl TextBuffer {
|
|||||||
self.break_lines(width);
|
self.break_lines(width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the break in hand is the break `want` would make. The attrs
|
||||||
|
/// are the caller's to compare; this is about the width alone.
|
||||||
|
///
|
||||||
|
/// A greedy break at one width is the same break at every width down to
|
||||||
|
/// the longest line it produced: each line still fits, and none can take a
|
||||||
|
/// word that would not fit in the wider box. So the layout in hand already
|
||||||
|
/// answers, and re-breaking would only be work.
|
||||||
|
///
|
||||||
|
/// At the longest line exactly, with no margin below it. A narrower width
|
||||||
|
/// really does break differently, so answering one from the break in hand
|
||||||
|
/// is how a warm tree keeps lines a cold tree would never produce. The
|
||||||
|
/// margin was here because a text reports the width it used and a parent
|
||||||
|
/// hands that back; the report is the step at or above its longest line
|
||||||
|
/// now, so what comes back fits.
|
||||||
|
fn breaks_the_same(&self, broke_at: Option<f32>, want: Option<f32>) -> bool {
|
||||||
|
match (broke_at, want) {
|
||||||
|
(broke_at, want) if broke_at == want => true,
|
||||||
|
(Some(broke_at), Some(want)) => want <= broke_at && want >= self.layout.width(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn break_lines(&mut self, width: Option<f32>) {
|
fn break_lines(&mut self, width: Option<f32>) {
|
||||||
self.layout.break_all_lines(width);
|
self.layout.break_all_lines(width);
|
||||||
self.layout
|
self.layout
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ impl Textures {
|
|||||||
TextureHandle {
|
TextureHandle {
|
||||||
slot: self.push(image),
|
slot: self.push(image),
|
||||||
size,
|
size,
|
||||||
counter: RefCounter::new(),
|
counter: RefCounter::default(),
|
||||||
send: self.send.clone(),
|
send: self.send.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,10 +167,6 @@ impl GlyphAtlas {
|
|||||||
pub fn page_count(&self) -> u32 {
|
pub fn page_count(&self) -> u32 {
|
||||||
self.pages.len() as u32
|
self.pages.len() as u32
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn glyph_count(&self) -> usize {
|
|
||||||
self.entries.len()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
|
|||||||
+18
-7
@@ -23,13 +23,22 @@ pub use primitive::*;
|
|||||||
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||||
|
|
||||||
fn module_source(wgsl: &str) -> String {
|
fn module_source(wgsl: &str) -> String {
|
||||||
// The steps come from the same constants the CPU counts in, rather than
|
// Every number both sides count in, written once here rather than a
|
||||||
// a second copy of them written into the shader: a grid the two disagree
|
// second time in the shader: a grid the two disagree about puts every
|
||||||
// about puts every coordinate somewhere else.
|
// coordinate somewhere else, and a sentinel they disagree about makes one
|
||||||
|
// of them walk a chain from a slot the other says is not there.
|
||||||
format!(
|
format!(
|
||||||
"const PX_STEP: f32 = 1.0 / {}.0;\nconst REL_STEP: f32 = 1.0 / {}.0;\n{PRELUDE}\n{wgsl}",
|
"const PX_STEP: f32 = 1.0 / {}.0;\n\
|
||||||
|
const REL_STEP: f32 = 1.0 / {}.0;\n\
|
||||||
|
const MASK_NONE: u32 = {}u;\n\
|
||||||
|
const MOVE_NONE: u32 = {}u;\n\
|
||||||
|
const CHAIN_LIMIT: u32 = {}u;\n\
|
||||||
|
{PRELUDE}\n{wgsl}",
|
||||||
1u32 << crate::PX_SHIFT,
|
1u32 << crate::PX_SHIFT,
|
||||||
1u32 << crate::REL_SHIFT,
|
1u32 << crate::REL_SHIFT,
|
||||||
|
MaskIdx::NONE.idx(),
|
||||||
|
MoveIdx::NONE.idx(),
|
||||||
|
crate::CHAIN_LIMIT,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +285,9 @@ impl UiRenderNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// What every draw in the ui is given: the window, the masks and the
|
/// What every draw in the ui is given: the window, the masks and the
|
||||||
/// move chain every position is resolved through.
|
/// move chain every position is resolved through. The last two are the
|
||||||
|
/// vertex stage's alone -- a mask's rectangle is the same for every
|
||||||
|
/// fragment of one instance, so it is resolved once and handed on.
|
||||||
fn shared_layout(device: &Device) -> BindGroupLayout {
|
fn shared_layout(device: &Device) -> BindGroupLayout {
|
||||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
entries: &[
|
entries: &[
|
||||||
@@ -292,7 +303,7 @@ impl UiRenderNode {
|
|||||||
},
|
},
|
||||||
BindGroupLayoutEntry {
|
BindGroupLayoutEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
visibility: ShaderStages::FRAGMENT,
|
visibility: ShaderStages::VERTEX,
|
||||||
ty: BindingType::Buffer {
|
ty: BindingType::Buffer {
|
||||||
ty: BufferBindingType::Storage { read_only: true },
|
ty: BufferBindingType::Storage { read_only: true },
|
||||||
has_dynamic_offset: false,
|
has_dynamic_offset: false,
|
||||||
@@ -302,7 +313,7 @@ impl UiRenderNode {
|
|||||||
},
|
},
|
||||||
BindGroupLayoutEntry {
|
BindGroupLayoutEntry {
|
||||||
binding: 2,
|
binding: 2,
|
||||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
visibility: ShaderStages::VERTEX,
|
||||||
ty: BindingType::Buffer {
|
ty: BindingType::Buffer {
|
||||||
ty: BufferBindingType::Storage { read_only: true },
|
ty: BufferBindingType::Storage { read_only: true },
|
||||||
has_dynamic_offset: false,
|
has_dynamic_offset: false,
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ struct MoveOffset {
|
|||||||
parent: u32,
|
parent: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// `PX_STEP` and `REL_STEP` are prepended from `iris_core`'s own constants:
|
// `PX_STEP`, `REL_STEP`, `MASK_NONE`, `MOVE_NONE` and `CHAIN_LIMIT` are
|
||||||
// what it stores is a whole count of each, both powers of two, so decoding
|
// prepended from `iris_core`'s own constants, so none of them is written
|
||||||
// is exact and the number here is the number the CPU decided.
|
// twice. What the CPU stores is a whole count of each step, and both steps
|
||||||
|
// are 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
|
// Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that
|
||||||
// composes to within half a step of a pixel boundary is on that boundary and
|
// composes to within half a step of a pixel boundary is on that boundary and
|
||||||
@@ -70,12 +72,6 @@ struct Region {
|
|||||||
y: UiSpan,
|
y: UiSpan,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MOVE_NONE: u32 = 4294967295u;
|
|
||||||
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
|
|
||||||
// rather than any real tree, and the CPU walk uses the same number so both
|
|
||||||
// resolve a deep one the same way.
|
|
||||||
const CHAIN_LIMIT: u32 = 64u;
|
|
||||||
|
|
||||||
// The same expression `Len::within` uses, in floats rather than on the
|
// The same expression `Len::within` uses, in floats rather than on the
|
||||||
// CPU's grid: a move is resolved here so that scrolling a subtree writes one
|
// 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
|
// entry instead of walking it. What has to hold is that this agrees with
|
||||||
@@ -130,9 +126,25 @@ struct VertexOutput {
|
|||||||
@location(2) uv: vec2<f32>,
|
@location(2) uv: vec2<f32>,
|
||||||
@location(3) @interpolate(flat) mask_idx: u32,
|
@location(3) @interpolate(flat) mask_idx: u32,
|
||||||
@location(4) @interpolate(flat) idx: u32,
|
@location(4) @interpolate(flat) idx: u32,
|
||||||
|
// The mask's rectangle in output pixels, resolved here because it is the
|
||||||
|
// same rectangle for every fragment of one instance and resolving it is a
|
||||||
|
// walk up a chain. Its own chain, not the drawn primitive's, so a
|
||||||
|
// stationary viewport clips content that moves inside it.
|
||||||
|
@location(5) @interpolate(flat) mask_top_left: vec2<f32>,
|
||||||
|
@location(6) @interpolate(flat) mask_bot_right: vec2<f32>,
|
||||||
@builtin(position) clip_position: vec4<f32>,
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The pixel corners of a region, which is where every coordinate the CPU
|
||||||
|
// decided becomes one.
|
||||||
|
fn corners(r: Region) -> mat2x2<f32> {
|
||||||
|
let top_left = snap_floor(vec2(r.x.start.rel, r.y.start.rel) * window.dim
|
||||||
|
+ vec2(r.x.start.px, r.y.start.px));
|
||||||
|
let bot_right = snap_floor(vec2(r.x.end.rel, r.y.end.rel) * window.dim
|
||||||
|
+ vec2(r.x.end.px, r.y.end.px));
|
||||||
|
return mat2x2<f32>(top_left, bot_right);
|
||||||
|
}
|
||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(
|
fn vs_main(
|
||||||
@builtin(vertex_index) vi: u32,
|
@builtin(vertex_index) vi: u32,
|
||||||
@@ -145,16 +157,17 @@ fn vs_main(
|
|||||||
UiSpan(scalar_of_pair(in.x_start), scalar_of_pair(in.x_end)),
|
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)),
|
UiSpan(scalar_of_pair(in.y_start), scalar_of_pair(in.y_end)),
|
||||||
);
|
);
|
||||||
let r = resolve_move(in.move_idx, local);
|
let own = corners(resolve_move(in.move_idx, local));
|
||||||
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
|
let top_left = own[0];
|
||||||
let top_left_px = vec2(r.x.start.px, r.y.start.px);
|
let bot_right = own[1];
|
||||||
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 = snap_floor(top_left_rel * window.dim + top_left_px);
|
|
||||||
let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
|
|
||||||
let size = bot_right - top_left;
|
let size = bot_right - top_left;
|
||||||
|
|
||||||
|
var mask = mat2x2<f32>(vec2<f32>(0.0), vec2<f32>(0.0));
|
||||||
|
if in.mask_idx != MASK_NONE {
|
||||||
|
let m = masks[in.mask_idx];
|
||||||
|
mask = corners(resolve_move(m.move_idx, Region(span_of(m.x), span_of(m.y))));
|
||||||
|
}
|
||||||
|
|
||||||
let uv = vec2<f32>(
|
let uv = vec2<f32>(
|
||||||
f32(vi % 2u),
|
f32(vi % 2u),
|
||||||
f32(vi / 2u)
|
f32(vi / 2u)
|
||||||
@@ -165,26 +178,19 @@ fn vs_main(
|
|||||||
out.top_left = top_left;
|
out.top_left = top_left;
|
||||||
out.bot_right = bot_right;
|
out.bot_right = bot_right;
|
||||||
out.mask_idx = in.mask_idx;
|
out.mask_idx = in.mask_idx;
|
||||||
|
out.mask_top_left = mask[0];
|
||||||
|
out.mask_bot_right = mask[1];
|
||||||
out.idx = ii;
|
out.idx = ii;
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
||||||
if in.mask_idx == 4294967295u {
|
if in.mask_idx == MASK_NONE {
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
let mask = masks[in.mask_idx];
|
let top_left = in.mask_top_left;
|
||||||
// Its own chain, not the drawn primitive's, so a stationary viewport
|
let bot_right = in.mask_bot_right;
|
||||||
// clips content that moves inside it.
|
|
||||||
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 = snap_floor(tl * window.dim + tl_px);
|
|
||||||
let bot_right = snap_floor(br * window.dim + br_px);
|
|
||||||
let pos = in.clip_position.xy;
|
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 {
|
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;
|
return color * 0.0;
|
||||||
|
|||||||
+40
-2
@@ -1,8 +1,25 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive,
|
Bounds, Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign,
|
||||||
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
|
RetainedPrimitive, Size, TextureHandle, UiRegion, UiVec2, WidgetId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// One draw of one widget, so that a widget it asked about can carry which
|
||||||
|
/// draw that was. Its own type beside the indices here because it names an
|
||||||
|
/// occasion rather than a slot: nothing is stored per draw.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct DrawId(u64);
|
||||||
|
|
||||||
|
impl DrawId {
|
||||||
|
/// No draw at all, which is what a widget nothing has asked about carries.
|
||||||
|
pub const NONE: Self = Self(0);
|
||||||
|
|
||||||
|
/// The next one after this. Handed out in order and never reused, so a
|
||||||
|
/// note left by an earlier draw can never be read as this one's.
|
||||||
|
pub(crate) fn next(self) -> Self {
|
||||||
|
Self(self.0 + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// What is kept of a widget its parent has asked about. `drawn` says whether
|
/// What is kept of a widget its parent has asked about. `drawn` says whether
|
||||||
/// it currently draws; one that does not is kept so that a change to it, or
|
/// it currently draws; one that does not is kept so that a change to it, or
|
||||||
/// under it, still reaches whoever asked.
|
/// under it, still reaches whoever asked.
|
||||||
@@ -28,6 +45,12 @@ pub struct ActiveData {
|
|||||||
/// The measured answer and its dependencies. A hint-only dependency or
|
/// The measured answer and its dependencies. A hint-only dependency or
|
||||||
/// a widget first encountered during placement has no measurement yet.
|
/// a widget first encountered during placement has no measurement yet.
|
||||||
pub answer: Option<Answer>,
|
pub answer: Option<Answer>,
|
||||||
|
/// The draw that last asked about this widget, which is what says whether
|
||||||
|
/// the draw now running has already asked -- a question the child list can
|
||||||
|
/// only answer by a search, and so in the children a container has rather
|
||||||
|
/// than in one read. Written by whoever asked, so a draw of this widget
|
||||||
|
/// itself carries it across rather than setting it.
|
||||||
|
pub(crate) asked_by: DrawId,
|
||||||
/// Asked more than once in its parent's last draw -- measured in one box
|
/// Asked more than once in its parent's last draw -- measured in one box
|
||||||
/// and then asked in the one the parent decided. The parent's layout
|
/// and then asked in the one the parent decided. The parent's layout
|
||||||
/// rests on the first answer and its drawing on the last, so only the
|
/// rests on the first answer and its drawing on the last, so only the
|
||||||
@@ -51,6 +74,8 @@ pub struct ActiveData {
|
|||||||
/// An owned mask holds one reference independently of its primitives.
|
/// An owned mask holds one reference independently of its primitives.
|
||||||
pub mask_region: Option<UiRegion>,
|
pub mask_region: Option<UiRegion>,
|
||||||
pub children: Vec<WidgetId>,
|
pub children: Vec<WidgetId>,
|
||||||
|
pub request_deps: Vec<WidgetId>,
|
||||||
|
pub(crate) scratch: DrawScratch,
|
||||||
/// The movable region its primitives are positioned through: its own when
|
/// The movable region its primitives are positioned through: its own when
|
||||||
/// opted in, otherwise the nearest ancestor's.
|
/// opted in, otherwise the nearest ancestor's.
|
||||||
pub move_idx: MoveIdx,
|
pub move_idx: MoveIdx,
|
||||||
@@ -58,6 +83,10 @@ pub struct ActiveData {
|
|||||||
/// A change to one moves a box this widget cannot fix by drawing again,
|
/// A change to one moves a box this widget cannot fix by drawing again,
|
||||||
/// and comparing them is what says so.
|
/// and comparing them is what says so.
|
||||||
pub declared: Declared,
|
pub declared: Declared,
|
||||||
|
/// Its bounds, resolved the same way. The answer is held to these where
|
||||||
|
/// the box was not, so a change to one changes what it answers even
|
||||||
|
/// where its declared lengths stand.
|
||||||
|
pub bounds: Bounds,
|
||||||
/// Its alignment when it was last drawn, which a change to the property
|
/// Its alignment when it was last drawn, which a change to the property
|
||||||
/// is found against.
|
/// is found against.
|
||||||
pub own_align: RegionAlign,
|
pub own_align: RegionAlign,
|
||||||
@@ -97,3 +126,12 @@ pub struct Answer {
|
|||||||
pub size: Size,
|
pub size: Size,
|
||||||
pub holds: LayoutHolds,
|
pub holds: LayoutHolds,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct DrawScratch {
|
||||||
|
pub children: Vec<WidgetId>,
|
||||||
|
pub size_deps: Vec<WidgetId>,
|
||||||
|
pub under: Vec<(WidgetId, LayoutHolds)>,
|
||||||
|
pub requests: Vec<crate::RequestedLen>,
|
||||||
|
pub lengths: Vec<crate::Px>,
|
||||||
|
}
|
||||||
+61
-4
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
|
use crate::{Bound, Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
/// The lengths of a box, in pixels, that one drawing of a widget holds for:
|
/// The lengths of a box, in pixels, that one drawing of a widget holds for:
|
||||||
@@ -19,6 +19,63 @@ pub struct Holds {
|
|||||||
pub hi: Px,
|
pub hi: Px,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Len {
|
||||||
|
/// Whether this is longer than `than` in a window this wide, and the
|
||||||
|
/// windows that answer holds for.
|
||||||
|
///
|
||||||
|
/// Which is longer is a question in pixels -- `rel(0.5)` is longer than
|
||||||
|
/// 300 px at a box of 600 and shorter at 400 -- and it is asked of the
|
||||||
|
/// difference and answered back through that same difference, so the
|
||||||
|
/// boundary is the comparison's own rather than a second way of finding
|
||||||
|
/// it.
|
||||||
|
pub fn longer_than(&self, than: Len, window: Px) -> (bool, Holds) {
|
||||||
|
let over = *self - than;
|
||||||
|
let longer = over.to_px(window) > Px::ZERO;
|
||||||
|
let side = match longer {
|
||||||
|
true => Px::STEP..=Px::MAX,
|
||||||
|
false => Px::MIN..=Px::ZERO,
|
||||||
|
};
|
||||||
|
(longer, Holds::from(side).through(over))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bound {
|
||||||
|
/// The end of this bound `len` falls outside, which is the length it
|
||||||
|
/// gets instead of its own, and the windows that answer holds for.
|
||||||
|
/// Nothing where it is inside, which is the answer wherever there is no
|
||||||
|
/// bound at all.
|
||||||
|
///
|
||||||
|
/// `len` and this bound are lengths of the same thing, whichever that
|
||||||
|
/// is: a box in window lengths wants the bound resolved, and a length a
|
||||||
|
/// widget declares of its rel base wants it as the rule wrote it. Both
|
||||||
|
/// comparisons are in pixels, so each is a question about this window,
|
||||||
|
/// and the box is decided again on the other side of a crossing.
|
||||||
|
pub fn outside(&self, len: Len, window: Px) -> (Option<Len>, Holds) {
|
||||||
|
let mut held = None;
|
||||||
|
let mut holds = Holds::ANY;
|
||||||
|
if let Some(min) = self.min {
|
||||||
|
let (shorter, kept) = min.longer_than(len, window);
|
||||||
|
holds = holds.and(kept);
|
||||||
|
if shorter {
|
||||||
|
held = Some(min);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(max) = self.max {
|
||||||
|
let (longer, kept) = held.unwrap_or(len).longer_than(max, window);
|
||||||
|
holds = holds.and(kept);
|
||||||
|
if longer {
|
||||||
|
debug_assert!(
|
||||||
|
held.is_none(),
|
||||||
|
"a floor of {:?} over a cap of {max:?} bounds nothing",
|
||||||
|
self.min,
|
||||||
|
);
|
||||||
|
held = Some(max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(held, holds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Holds {
|
impl Holds {
|
||||||
pub const ANY: Self = Self {
|
pub const ANY: Self = Self {
|
||||||
lo: Px::MIN,
|
lo: Px::MIN,
|
||||||
@@ -35,11 +92,11 @@ impl Holds {
|
|||||||
|
|
||||||
/// Every length `other` holds for is one this holds for, so a drawing
|
/// Every length `other` holds for is one this holds for, so a drawing
|
||||||
/// made under this range is still good wherever `other` is.
|
/// made under this range is still good wherever `other` is.
|
||||||
pub const fn covers(self, other: Self) -> bool {
|
pub const fn covers(&self, other: Self) -> bool {
|
||||||
self.lo.raw() <= other.lo.raw() && self.hi.raw() >= other.hi.raw()
|
self.lo.raw() <= other.lo.raw() && self.hi.raw() >= other.hi.raw()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn and(self, other: Self) -> Self {
|
pub const fn and(&self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
lo: self.lo.max(other.lo),
|
lo: self.lo.max(other.lo),
|
||||||
hi: self.hi.min(other.hi),
|
hi: self.hi.min(other.hi),
|
||||||
@@ -57,7 +114,7 @@ impl Holds {
|
|||||||
/// boxes therefore give one length. That is a floor rather than an
|
/// boxes therefore give one length. That is a floor rather than an
|
||||||
/// allowance: inverting it is two divisions and nothing else, and the
|
/// allowance: inverting it is two divisions and nothing else, and the
|
||||||
/// whole of a box maps back to itself.
|
/// whole of a box maps back to itself.
|
||||||
pub const fn through(self, len: Len) -> Self {
|
pub const fn through(&self, len: Len) -> Self {
|
||||||
if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
|
if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
|
||||||
return Self::ANY;
|
return Self::ANY;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ impl AxisHolds {
|
|||||||
region_len: None,
|
region_len: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn and(self, other: Self) -> Self {
|
pub fn and(&self, other: Self) -> Self {
|
||||||
// Two pins of the same length disagreeing would mean one drawing was
|
// Two pins of the same length disagreeing would mean one drawing was
|
||||||
// a fraction of two different lengths at once.
|
// a fraction of two different lengths at once.
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
@@ -53,7 +53,7 @@ impl AxisHolds {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn covers(self, other: Self) -> bool {
|
pub fn covers(&self, other: Self) -> bool {
|
||||||
self.window.covers(other.window)
|
self.window.covers(other.window)
|
||||||
&& self.region.covers(other.region)
|
&& self.region.covers(other.region)
|
||||||
&& self
|
&& self
|
||||||
@@ -64,7 +64,7 @@ impl AxisHolds {
|
|||||||
|
|
||||||
/// Whether a widget in a box `len` long, with that rel base, in that
|
/// Whether a widget in a box `len` long, with that rel base, in that
|
||||||
/// window, is one this drawing holds for.
|
/// window, is one this drawing holds for.
|
||||||
pub fn contains(self, window: Px, rel_base: Len, len: Len) -> bool {
|
pub fn contains(&self, window: Px, rel_base: Len, len: Len) -> bool {
|
||||||
self.window.contains(window)
|
self.window.contains(window)
|
||||||
&& self.rel_base.is_none_or(|pinned| pinned == rel_base)
|
&& self.rel_base.is_none_or(|pinned| pinned == rel_base)
|
||||||
&& self.region.contains(len.to_px(window))
|
&& self.region.contains(len.to_px(window))
|
||||||
@@ -87,18 +87,18 @@ impl LayoutHolds {
|
|||||||
y: AxisHolds::ANY,
|
y: AxisHolds::ANY,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn and(self, other: Self) -> Self {
|
pub fn and(&self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: self.x.and(other.x),
|
x: self.x.and(other.x),
|
||||||
y: self.y.and(other.y),
|
y: self.y.and(other.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn covers(self, other: Self) -> bool {
|
pub fn covers(&self, other: Self) -> bool {
|
||||||
self.x.covers(other.x) && self.y.covers(other.y)
|
self.x.covers(other.x) && self.y.covers(other.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains(self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
|
pub fn contains(&self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
|
||||||
Axis::BOTH
|
Axis::BOTH
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.all(|axis| self[axis].contains(window[axis], rel_base[axis], region[axis].len()))
|
.all(|axis| self[axis].contains(window[axis], rel_base[axis], region[axis].len()))
|
||||||
|
|||||||
+6
-7
@@ -20,7 +20,7 @@ pub use active::*;
|
|||||||
pub use holds::*;
|
pub use holds::*;
|
||||||
pub use layout_holds::*;
|
pub use layout_holds::*;
|
||||||
pub use painter::{Painter, PrimitiveLike};
|
pub use painter::{Painter, PrimitiveLike};
|
||||||
pub use place::{PlaceDesc, PlaceDescAxis, RetainedPrimitive};
|
pub use place::{PlaceDesc, PlaceDescAxis, PlaceFit, RetainedPrimitive};
|
||||||
pub use render_state::*;
|
pub use render_state::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -57,8 +57,11 @@ impl Moves {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Frees a slot. Not a change to the entries: the slot keeps the bytes it
|
||||||
|
/// had, nothing names it until it is handed out again, and whoever is
|
||||||
|
/// handed it writes it then -- so re-uploading the array here would send
|
||||||
|
/// the GPU what it already has.
|
||||||
pub fn remove(&mut self, idx: MoveIdx) {
|
pub fn remove(&mut self, idx: MoveIdx) {
|
||||||
self.changed = true;
|
|
||||||
self.arena.remove(Id::preset(idx.idx() as u32));
|
self.arena.remove(Id::preset(idx.idx() as u32));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,11 +106,7 @@ impl Moves {
|
|||||||
/// the shader's walk costs per primitive.
|
/// the shader's walk costs per primitive.
|
||||||
pub fn depth(&self, idx: MoveIdx) -> usize {
|
pub fn depth(&self, idx: MoveIdx) -> usize {
|
||||||
let mut depth = 0;
|
let mut depth = 0;
|
||||||
let mut at = idx;
|
self.walk(idx, |_| depth += 1);
|
||||||
while at != MoveIdx::NONE && depth < CHAIN_LIMIT as usize {
|
|
||||||
at = self.arena[at.idx()].parent;
|
|
||||||
depth += 1;
|
|
||||||
}
|
|
||||||
depth
|
depth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+422
-80
@@ -1,9 +1,10 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
use crate::layout_diagnostics::{self as diag, Counter};
|
use crate::layout_diagnostics::{self as diag, Counter};
|
||||||
use crate::{
|
use crate::{
|
||||||
Axis, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel,
|
Axis, Bound, Bounds, Declared, DrawId, DrawScratch, Holds, LayoutHolds, LayoutLen, Len,
|
||||||
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
|
PlaceDesc, PlaceFit, Px, PxVec2, RegionAlign, Rel, RenderedText, RequestArena, RequestedLen,
|
||||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
|
RetainedPrimitive, Size, SizeRequests, StrongWidget, TextAttrs, TextBuffer, TextureHandle,
|
||||||
|
UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
|
||||||
render::{
|
render::{
|
||||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
|
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
|
||||||
TexturePrimitive,
|
TexturePrimitive,
|
||||||
@@ -41,6 +42,8 @@ pub struct Painter<'a> {
|
|||||||
pub(super) children: Vec<WidgetId>,
|
pub(super) children: Vec<WidgetId>,
|
||||||
/// The children whose size this widget read while drawing.
|
/// The children whose size this widget read while drawing.
|
||||||
pub(super) size_deps: Vec<WidgetId>,
|
pub(super) size_deps: Vec<WidgetId>,
|
||||||
|
pub(super) request_deps: Vec<WidgetId>,
|
||||||
|
pub(super) scratch: DrawScratch,
|
||||||
/// What this draw itself reads, as against what its children's drawings
|
/// What this draw itself reads, as against what its children's drawings
|
||||||
/// hold for: every window and every length of its own region until it
|
/// hold for: every window and every length of its own region until it
|
||||||
/// reads one, then that one unless it says otherwise, and the rel base or
|
/// reads one, then that one unless it says otherwise, and the rel base or
|
||||||
@@ -58,10 +61,137 @@ pub struct Painter<'a> {
|
|||||||
/// counted from however far `layer` has walked.
|
/// counted from however far `layer` has walked.
|
||||||
pub(super) own_layer: usize,
|
pub(super) own_layer: usize,
|
||||||
pub(super) depth: usize,
|
pub(super) depth: usize,
|
||||||
|
/// This draw's own id, which it leaves on every widget it asks about.
|
||||||
|
pub(super) draw: DrawId,
|
||||||
pub(super) id: WidgetId,
|
pub(super) id: WidgetId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Painter<'a> {
|
impl<'a> Painter<'a> {
|
||||||
|
/// Reuses this widget's allocation buffers across draws. A child drawn
|
||||||
|
/// part way through gets a painter of its own, with buffers of its own,
|
||||||
|
/// so nothing it does while this one is mid-row can reach these.
|
||||||
|
pub fn with_requests<T>(
|
||||||
|
&mut self,
|
||||||
|
f: impl FnOnce(&mut Self, &mut Vec<RequestedLen>, &mut Vec<Px>) -> T,
|
||||||
|
) -> T {
|
||||||
|
let mut requests = std::mem::take(&mut self.scratch.requests);
|
||||||
|
let mut lengths = std::mem::take(&mut self.scratch.lengths);
|
||||||
|
requests.clear();
|
||||||
|
lengths.clear();
|
||||||
|
let result = f(self, &mut requests, &mut lengths);
|
||||||
|
self.scratch.requests = requests;
|
||||||
|
self.scratch.lengths = lengths;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Discovers a composable request without painting a provisional box.
|
||||||
|
pub fn size_request<W: ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
child: &StrongWidget<W>,
|
||||||
|
axis: Axis,
|
||||||
|
) -> Option<RequestedLen> {
|
||||||
|
if let Some(len) = self.size_hint(child, axis) {
|
||||||
|
self.request_deps.push(child.id());
|
||||||
|
return Some(len.into());
|
||||||
|
}
|
||||||
|
let start = self.request_deps.len();
|
||||||
|
let mut requests = SizeRequests {
|
||||||
|
arena: &mut self.state.requests,
|
||||||
|
measured: None,
|
||||||
|
widgets: self.rsc.widgets(),
|
||||||
|
dependencies: &mut self.request_deps,
|
||||||
|
rel_base: self.rel_base[axis],
|
||||||
|
};
|
||||||
|
// Intrinsic fixed content must keep its offered box for wrapping;
|
||||||
|
// only a declaration or a share chooses the box it is drawn in.
|
||||||
|
let request = requests.widget(child, axis).filter(|request| {
|
||||||
|
request.has_leftover()
|
||||||
|
|| self.rsc.widgets().size_rules(child.id())[axis]
|
||||||
|
.request
|
||||||
|
.is_some()
|
||||||
|
});
|
||||||
|
if request.is_some() {
|
||||||
|
self.rel_base(axis);
|
||||||
|
} else {
|
||||||
|
// A discarded request contributes no dependency: the measured
|
||||||
|
// draw below records the size and box it actually used instead.
|
||||||
|
self.request_deps.truncate(start);
|
||||||
|
}
|
||||||
|
request
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Completes discovery after a child was measured. Only this call may use
|
||||||
|
/// drawn answers: before the ask they could belong to an obsolete box.
|
||||||
|
pub fn measured_request<W: ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
child: &StrongWidget<W>,
|
||||||
|
axis: Axis,
|
||||||
|
len: LayoutLen,
|
||||||
|
) -> RequestedLen {
|
||||||
|
let start = self.request_deps.len();
|
||||||
|
let bound = self.rsc.widgets().size_rules(child.id())[axis].bound;
|
||||||
|
let mut requests = SizeRequests {
|
||||||
|
arena: &mut self.state.requests,
|
||||||
|
measured: Some(&self.state.active),
|
||||||
|
widgets: self.rsc.widgets(),
|
||||||
|
dependencies: &mut self.request_deps,
|
||||||
|
rel_base: self.rel_base[axis],
|
||||||
|
};
|
||||||
|
if let Some(request) = requests.widget(child, axis)
|
||||||
|
&& request.linear().is_none()
|
||||||
|
&& request.has_leftover()
|
||||||
|
{
|
||||||
|
self.rel_base(axis);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
let shares = len.leftover > Weight::ZERO;
|
||||||
|
let request = match shares {
|
||||||
|
true => requests.bounded(len.into(), bound),
|
||||||
|
false => len.into(),
|
||||||
|
};
|
||||||
|
self.request_deps.truncate(start);
|
||||||
|
// Only a bound that is a fraction was read against the rel base; one
|
||||||
|
// in pixels binds at the same length under any of them.
|
||||||
|
if shares && bound.has_fraction() {
|
||||||
|
self.rel_base(axis);
|
||||||
|
}
|
||||||
|
request
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Divides `room` between `requests`, one length per request in
|
||||||
|
/// `output`. A deferred comparison is decided here, against this window:
|
||||||
|
/// which side of a crossing the solution falls is a question in pixels,
|
||||||
|
/// so the drawing holds only for the window that answered it.
|
||||||
|
pub fn allocate(
|
||||||
|
&mut self,
|
||||||
|
requests: &[RequestedLen],
|
||||||
|
room: Len,
|
||||||
|
axis: Axis,
|
||||||
|
output: &mut Vec<Px>,
|
||||||
|
) {
|
||||||
|
let window = self.window[axis];
|
||||||
|
self.own[axis].window = self.own[axis].window.and(Holds::at(window));
|
||||||
|
output.clear();
|
||||||
|
output.extend(
|
||||||
|
self.state
|
||||||
|
.requests
|
||||||
|
.allocate(requests, room.to_px(window), window),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The least a request can come to, which is what it takes of the row
|
||||||
|
/// before anything is divided. A comparison is read at no share at all.
|
||||||
|
pub fn minimum_request(&mut self, request: &RequestedLen, axis: Axis) -> Len {
|
||||||
|
match request.linear() {
|
||||||
|
Some(len) => len.without_leftover(),
|
||||||
|
None => {
|
||||||
|
let window = self.window[axis];
|
||||||
|
self.own[axis].window = self.own[axis].window.and(Holds::at(window));
|
||||||
|
Len::from_parts(Rel::ZERO, self.state.requests.minimum(*request, window))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||||
let kind = self.rsc.ui_mut().primitives.kind::<P>();
|
let kind = self.rsc.ui_mut().primitives.kind::<P>();
|
||||||
self.write(kind, primitive, region);
|
self.write(kind, primitive, region);
|
||||||
@@ -187,19 +317,36 @@ impl<'a> Painter<'a> {
|
|||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
place: impl Into<PlaceDesc>,
|
place: impl Into<PlaceDesc>,
|
||||||
) -> DrawResult<'s, 'a, W> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
let place = self.resolve_rel_base(place.into());
|
let offer = self.resolve_rel_base(place.into());
|
||||||
|
let Ask {
|
||||||
|
rel_base,
|
||||||
|
region,
|
||||||
|
place,
|
||||||
|
declared,
|
||||||
|
bounds,
|
||||||
|
holds: ask_holds,
|
||||||
|
inputs,
|
||||||
|
} = self.placing().ask(
|
||||||
|
self.rsc.widgets(),
|
||||||
|
&mut self.state.requests,
|
||||||
|
self.window,
|
||||||
|
id.id(),
|
||||||
|
offer,
|
||||||
|
);
|
||||||
|
self.own = self.own.and(inputs);
|
||||||
let region_node = self.rsc.widgets().is_region_node(id.id());
|
let region_node = self.rsc.widgets().is_region_node(id.id());
|
||||||
let declared = self.declared_lens(id);
|
|
||||||
let align = self.rsc.widgets().alignment(id.id());
|
|
||||||
let (rel_base, region) =
|
|
||||||
place.rel_base_and_region(self.region, self.rel_base, declared, align);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
if region_node {
|
if region_node {
|
||||||
diag::bump(Counter::RegionNodeDraws);
|
diag::bump(Counter::RegionNodeDraws);
|
||||||
diag::region_node(id.id(), self.id, region);
|
diag::region_node(id.id(), self.id, region);
|
||||||
}
|
}
|
||||||
// A child listed twice would be moved twice.
|
// A child listed twice would be moved twice.
|
||||||
let re_asked = self.children.contains(&id.id());
|
let re_asked = self.state.asked_in(id.id(), self.draw);
|
||||||
|
debug_assert_eq!(
|
||||||
|
re_asked,
|
||||||
|
self.children.contains(&id.id()),
|
||||||
|
"the note on a child disagrees with the list it says it is in",
|
||||||
|
);
|
||||||
if !re_asked {
|
if !re_asked {
|
||||||
self.children.push(id.id());
|
self.children.push(id.id());
|
||||||
}
|
}
|
||||||
@@ -215,17 +362,36 @@ impl<'a> Painter<'a> {
|
|||||||
rel_base,
|
rel_base,
|
||||||
region,
|
region,
|
||||||
placed: place,
|
placed: place,
|
||||||
asked: place,
|
asked: offer,
|
||||||
|
declared,
|
||||||
|
bounds,
|
||||||
|
ask_holds,
|
||||||
re_asked,
|
re_asked,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
self.rsc,
|
self.rsc,
|
||||||
);
|
);
|
||||||
|
// Written now the draw has happened, since a widget drawn here has no
|
||||||
|
// record of its own until it has.
|
||||||
|
self.state
|
||||||
|
.active
|
||||||
|
.get_mut(&id.id())
|
||||||
|
.expect("a widget that was drawn has a record")
|
||||||
|
.asked_by = self.draw;
|
||||||
let holds = self.in_parent(drawn.drawing_holds, region, place, declared);
|
let holds = self.in_parent(drawn.drawing_holds, region, place, declared);
|
||||||
let answer_holds = self.in_parent(drawn.answer.holds, region, place, declared);
|
let answer_holds = self.in_parent(drawn.answer.holds, region, place, declared);
|
||||||
match self.under.iter_mut().find(|(child, _)| *child == id.id()) {
|
// Added in step with the child list, so the one search here is the
|
||||||
Some((_, kept)) => *kept = holds,
|
// rare case of a child asked about twice.
|
||||||
None => self.under.push((id.id(), holds)),
|
match re_asked {
|
||||||
|
true => {
|
||||||
|
let kept = self
|
||||||
|
.under
|
||||||
|
.iter_mut()
|
||||||
|
.find(|(child, _)| *child == id.id())
|
||||||
|
.expect("a child asked about twice was added the first time");
|
||||||
|
kept.1 = holds;
|
||||||
|
}
|
||||||
|
false => self.under.push((id.id(), holds)),
|
||||||
}
|
}
|
||||||
DrawResult {
|
DrawResult {
|
||||||
child: id,
|
child: id,
|
||||||
@@ -242,6 +408,11 @@ impl<'a> Painter<'a> {
|
|||||||
self.children.retain(|child| *child != id.id());
|
self.children.retain(|child| *child != id.id());
|
||||||
self.under.retain(|(child, _)| *child != id.id());
|
self.under.retain(|(child, _)| *child != id.id());
|
||||||
self.state.undraw_rec(id.id(), self.rsc);
|
self.state.undraw_rec(id.id(), self.rsc);
|
||||||
|
// Taken out of the child list, so the note saying it is in there goes
|
||||||
|
// with it: placing it again is asking again.
|
||||||
|
if let Some(active) = self.state.active.get_mut(&id.id()) {
|
||||||
|
active.asked_by = DrawId::NONE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puts a child in `place` of this widget's box, where that box is the
|
/// Puts a child in `place` of this widget's box, where that box is the
|
||||||
@@ -263,7 +434,7 @@ impl<'a> Painter<'a> {
|
|||||||
let states_rel_base = Axis::BOTH
|
let states_rel_base = Axis::BOTH
|
||||||
.iter()
|
.iter()
|
||||||
.any(|&axis| matches!(place[axis].rel_base, RelBase::Len(_)));
|
.any(|&axis| matches!(place[axis].rel_base, RelBase::Len(_)));
|
||||||
if states_rel_base || !self.children.contains(&id.id()) {
|
if states_rel_base || !self.state.asked_in(id.id(), self.draw) {
|
||||||
return self.widget_at(id, place);
|
return self.widget_at(id, place);
|
||||||
}
|
}
|
||||||
let at = self.placing();
|
let at = self.placing();
|
||||||
@@ -282,7 +453,7 @@ impl<'a> Painter<'a> {
|
|||||||
/// This widget as the thing its children are placed within.
|
/// This widget as the thing its children are placed within.
|
||||||
fn placing(&self) -> Placing {
|
fn placing(&self) -> Placing {
|
||||||
Placing {
|
Placing {
|
||||||
id: self.id,
|
id: Some(self.id),
|
||||||
region: self.region,
|
region: self.region,
|
||||||
rel_base: self.rel_base,
|
rel_base: self.rel_base,
|
||||||
depth: self.depth,
|
depth: self.depth,
|
||||||
@@ -291,27 +462,19 @@ impl<'a> Painter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a widget's rules declare its lengths to be, which whoever draws
|
|
||||||
/// it resolves into its rel base. Reading them depends on nothing -- the box
|
|
||||||
/// that comes of them is kept on the child, and `redraw` compares it
|
|
||||||
/// there.
|
|
||||||
fn declared_lens<W: ?Sized>(&self, id: &StrongWidget<W>) -> Declared {
|
|
||||||
self.rsc.widgets().declared_lens(id.id())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What a child says its length is without being drawn, if it can say,
|
/// What a child says its length is without being drawn, if it can say,
|
||||||
/// as the length its draw would report: a fraction in it is resolved
|
/// as the length its draw would report: a fraction in it is resolved
|
||||||
/// against this widget's rel base, which is the rel base a child asked with
|
/// against this widget's rel base, which is the rel base a child asked with
|
||||||
/// nothing narrowed gets. Asking counts as reading its size.
|
/// nothing narrowed gets. Asking counts as reading its size.
|
||||||
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
|
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
|
||||||
let widgets = self.rsc.widgets();
|
// A bound is composed into a request rather than applied to a hint,
|
||||||
// A rule is the answer where there is one: it wins over whatever the
|
// so a bounded widget cannot say its length without being asked: what
|
||||||
// widget would draw, so it has to win over what the widget says too.
|
// it comes to is a comparison only the ask or the allocator makes.
|
||||||
let hint = widgets.size_rules(id.id())[axis].exact().or_else(|| {
|
// A miss rather than no read at all, so the counters see it.
|
||||||
widgets
|
let bounded = self.rsc.widgets().size_rules(id.id())[axis].bound != Bound::ANY;
|
||||||
.get_dyn(id.id())
|
let hint = (!bounded)
|
||||||
.and_then(|widget| widget.size_hint(axis))
|
.then(|| self.rsc.widgets().exact_len(id.id(), axis))
|
||||||
});
|
.flatten();
|
||||||
let rel_base = self.rel_base[axis];
|
let rel_base = self.rel_base[axis];
|
||||||
let resolved = hint.map(|hint| hint.within_len(rel_base));
|
let resolved = hint.map(|hint| hint.within_len(rel_base));
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
@@ -335,11 +498,13 @@ impl<'a> Painter<'a> {
|
|||||||
resolved
|
resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Records that this draw read a child's length. Listed once per read
|
||||||
|
/// rather than once per child: a child that answered with a hint may have
|
||||||
|
/// no record to note it on until this draw ends, and what the list drives
|
||||||
|
/// asks the same of a widget twice as of it once.
|
||||||
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||||
if !self.size_deps.contains(&child.id()) {
|
|
||||||
self.size_deps.push(child.id());
|
self.size_deps.push(child.id());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_text<'b>(
|
pub fn render_text<'b>(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -429,7 +594,7 @@ impl<'a> Painter<'a> {
|
|||||||
/// depend on it.
|
/// depend on it.
|
||||||
pub fn has_exact_size(&self, axis: Axis) -> bool {
|
pub fn has_exact_size(&self, axis: Axis) -> bool {
|
||||||
self.rsc.widgets().size_rules(self.id)[axis]
|
self.rsc.widgets().size_rules(self.id)[axis]
|
||||||
.exact()
|
.request
|
||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,6 +647,26 @@ impl<'a> Painter<'a> {
|
|||||||
len.to_px(window)
|
len.to_px(window)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`Len::longer_than`], asked on this widget's behalf: the windows the
|
||||||
|
/// comparison comes out the same way on are windows its drawing holds
|
||||||
|
/// for, and nowhere else does it. What a container has left for the
|
||||||
|
/// shares it divides is the one thing that asks.
|
||||||
|
///
|
||||||
|
/// Narrowed rather than stated, because whatever else this widget read
|
||||||
|
/// about the window is a reason its drawing holds where it does too.
|
||||||
|
pub fn longer_than(&mut self, len: Len, than: Len, axis: Axis) -> bool {
|
||||||
|
let window = self.window[axis];
|
||||||
|
let (longer, holds) = len.longer_than(than, window);
|
||||||
|
debug_assert!(
|
||||||
|
holds.contains(window),
|
||||||
|
"'{}' ({:?}) compared two lengths and kept a range without this window",
|
||||||
|
self.label(),
|
||||||
|
self.id
|
||||||
|
);
|
||||||
|
self.own[axis].window = self.own[axis].window.and(holds);
|
||||||
|
longer
|
||||||
|
}
|
||||||
|
|
||||||
/// The windows this drawing holds for, stated rather than taken: a
|
/// The windows this drawing holds for, stated rather than taken: a
|
||||||
/// container that branched on a length in pixels says which side of the
|
/// container that branched on a length in pixels says which side of the
|
||||||
/// boundary it was on, which is wider than the one window reading that
|
/// boundary it was on, which is wider than the one window reading that
|
||||||
@@ -497,10 +682,6 @@ impl<'a> Painter<'a> {
|
|||||||
self.own[axis].window = holds;
|
self.own[axis].window = holds;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_data(&mut self) -> &mut TextData {
|
|
||||||
&mut self.rsc.ui_mut().text
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn child_layer(&mut self) {
|
pub fn child_layer(&mut self) {
|
||||||
self.layer = self.state.layers.child(self.layer);
|
self.layer = self.state.layers.child(self.layer);
|
||||||
}
|
}
|
||||||
@@ -651,36 +832,211 @@ impl Painter<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widgets {
|
impl Widgets {
|
||||||
/// What a widget's box is where a rule or its own hint says so outright.
|
/// What says a widget's length on one axis without drawing it, if anything
|
||||||
pub(super) fn declared_lens(&self, id: WidgetId) -> Declared {
|
/// does. A rule is the answer where there is one: it wins over whatever the
|
||||||
let rules = self.size_rules(id);
|
/// widget would draw, so it has to win over what the widget says too -- a
|
||||||
let widget = self.get_dyn(id);
|
/// share included, since a share is a length only to whoever divides one,
|
||||||
Declared::from_axes(|axis| {
|
/// and that is the parent rather than this widget.
|
||||||
rules[axis].declared().or_else(|| {
|
fn exact_len(&self, id: WidgetId, axis: Axis) -> Option<LayoutLen> {
|
||||||
// A hint still narrows the box where no rule does, which is
|
// A request is a length the rule gives, and the hint below must not
|
||||||
// how a widget with a natural pixel size -- an image, a gap
|
// narrow the box in its place: what the request comes to is not known
|
||||||
// -- gets that size rather than the whole offer. That is the
|
// until the parent allocates, and it is the parent's answer, not this
|
||||||
// offer's business rather than a declaration's, and this
|
// widget's.
|
||||||
// falls away once a widget occupies its reported size inside
|
let rule = &self.size_rules(id)[axis];
|
||||||
// the box it was offered.
|
if rule.deferred().is_some() {
|
||||||
widget
|
return None;
|
||||||
.and_then(|widget| widget.size_hint(axis))
|
}
|
||||||
.and_then(LayoutLen::declared)
|
rule.exact().or_else(|| {
|
||||||
})
|
// A hint still narrows the box where no rule does, which is how a
|
||||||
|
// widget with a natural pixel size -- an image, a gap -- gets that
|
||||||
|
// size rather than the whole offer. That is the offer's business
|
||||||
|
// rather than a declaration's, and this falls away once a widget
|
||||||
|
// occupies its reported size inside the box it was offered.
|
||||||
|
self.get_dyn(id)?.size_hint(axis)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One ask of a widget: the box it draws in, what its fractions are of, and
|
||||||
|
/// what deciding those read.
|
||||||
|
pub(super) struct Ask {
|
||||||
|
pub rel_base: UiVec2,
|
||||||
|
pub region: UiRegion,
|
||||||
|
/// The place the ask came to, which a rule of the widget's own can take
|
||||||
|
/// past the box its parent offered.
|
||||||
|
pub place: PlaceDesc,
|
||||||
|
/// What the widget's box is on each axis where something says so
|
||||||
|
/// outright: its rule, its hint, or a bound the offer fell outside.
|
||||||
|
/// An intrinsic answer can still occupy less than a capped box.
|
||||||
|
pub declared: Declared,
|
||||||
|
/// Its bounds, resolved against the rel base its rules were resolved
|
||||||
|
/// against, for the answer to be held to where the box was not.
|
||||||
|
pub bounds: Bounds,
|
||||||
|
/// What the ask itself holds for, kept on the widget asked about: a rule
|
||||||
|
/// compared against the offer in pixels holds only for the windows on its
|
||||||
|
/// side of the crossing, and that range reaches whoever asked through the
|
||||||
|
/// drawing it is part of. Kept on the widget asked about rather than on
|
||||||
|
/// the asker because the root has no asker.
|
||||||
|
pub holds: LayoutHolds,
|
||||||
|
/// Inputs read against the parent before declarations choose a new base.
|
||||||
|
/// These belong to the asker; the widget's own holds describe its output box.
|
||||||
|
pub inputs: LayoutHolds,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Placing {
|
||||||
|
/// Asks about a widget at `place` of this box, with the widget's own
|
||||||
|
/// rules applied to what the place offers it. `place` is resolved: what
|
||||||
|
/// a rel base of the asker's is a fraction of, the asker worked out.
|
||||||
|
///
|
||||||
|
/// Every ask is this one, the root's included -- there the box is the
|
||||||
|
/// window and nothing above narrowed it, which is what [`Self::WINDOW`]
|
||||||
|
/// says.
|
||||||
|
pub(super) fn ask(
|
||||||
|
&self,
|
||||||
|
widgets: &Widgets,
|
||||||
|
requests: &mut RequestArena,
|
||||||
|
window: PxVec2,
|
||||||
|
id: WidgetId,
|
||||||
|
mut place: PlaceDesc,
|
||||||
|
) -> Ask {
|
||||||
|
let align = widgets.alignment(id);
|
||||||
|
let rules = widgets.size_rules(id);
|
||||||
|
let mut holds = LayoutHolds::ANY;
|
||||||
|
let mut inputs = LayoutHolds::ANY;
|
||||||
|
let mut declared = Declared::NONE;
|
||||||
|
let mut bounds = Bounds::ANY;
|
||||||
|
for axis in Axis::BOTH {
|
||||||
|
let base = place.base(axis, self.rel_base);
|
||||||
|
let stated = widgets.exact_len(id, axis);
|
||||||
|
if let Some(len) = stated.and_then(|len| len.declared()) {
|
||||||
|
if len.rel != Rel::ZERO {
|
||||||
|
inputs[axis].rel_base = Some(self.rel_base[axis]);
|
||||||
|
}
|
||||||
|
declared[axis] = Some(len.within_len(base));
|
||||||
|
}
|
||||||
|
if let Some(request) = rules[axis].deferred() {
|
||||||
|
inputs[axis].rel_base = Some(self.rel_base[axis]);
|
||||||
|
inputs[axis].region_len = Some(self.region[axis].len());
|
||||||
|
inputs[axis].window = Holds::at(window[axis]);
|
||||||
|
let offer = place.of(self.region, align)[axis].len();
|
||||||
|
let px = if place[axis].fit == PlaceFit::Allocated {
|
||||||
|
offer.to_px(window[axis])
|
||||||
|
} else {
|
||||||
|
let request = requests.import(request, base);
|
||||||
|
let request = requests.bounded(request, rules[axis].bound.within_len(base));
|
||||||
|
holds[axis].window = Holds::at(window[axis]);
|
||||||
|
requests
|
||||||
|
.allocate(&[request], offer.to_px(window[axis]), window[axis])
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
let len = Len::from_parts(Rel::ZERO, px);
|
||||||
|
place[axis].rel_base = RelBase::Len(len);
|
||||||
|
declared[axis] = Some(len);
|
||||||
|
holds[axis].rel_base = Some(len);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// A share fills what the pixels and fraction beside it leave of
|
||||||
|
// the box and overflows where they are longer, which is the rule
|
||||||
|
// a span follows with one child. Only the overflow is a box of
|
||||||
|
// the widget's own: a share that fits is the box it was given,
|
||||||
|
// which is what this place already says.
|
||||||
|
let (share, kept) = self.share_past_the_offer(stated, window[axis], place, align, axis);
|
||||||
|
holds[axis].window = holds[axis].window.and(kept);
|
||||||
|
if let Some(len) = share {
|
||||||
|
place[axis] = len.as_desc().fills();
|
||||||
|
}
|
||||||
|
let bound = rules[axis].bound;
|
||||||
|
if bound != Bound::ANY {
|
||||||
|
if bound.has_fraction() {
|
||||||
|
inputs[axis].rel_base = Some(self.rel_base[axis]);
|
||||||
|
}
|
||||||
|
// A solved slot may narrow the widget's rel base, but the
|
||||||
|
// allocator evaluated its bounds against this parent's base.
|
||||||
|
let bound_base = if place[axis].fit == PlaceFit::Allocated {
|
||||||
|
self.rel_base[axis]
|
||||||
|
} else {
|
||||||
|
base
|
||||||
|
};
|
||||||
|
bounds[axis] = bound.within_len(bound_base);
|
||||||
|
let offer =
|
||||||
|
declared[axis].unwrap_or_else(|| place.of(self.region, align)[axis].len());
|
||||||
|
let (held, kept) = bounds[axis].outside(offer, window[axis]);
|
||||||
|
holds[axis].window = holds[axis].window.and(kept);
|
||||||
|
// The comparison reads the incoming box, before a bound
|
||||||
|
// replaces it. Placement keeps that decision; only an ask
|
||||||
|
// may compare a new offer.
|
||||||
|
if declared[axis].is_none() {
|
||||||
|
inputs[axis].region_len = Some(self.region[axis].len());
|
||||||
|
}
|
||||||
|
if let Some(len) = held {
|
||||||
|
declared[axis] = Some(len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (rel_base, region) =
|
||||||
|
place.rel_base_and_region(self.region, self.rel_base, declared, align);
|
||||||
|
Ask {
|
||||||
|
rel_base,
|
||||||
|
region,
|
||||||
|
place,
|
||||||
|
declared,
|
||||||
|
bounds,
|
||||||
|
holds,
|
||||||
|
inputs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The box a widget's own share asks for where that is longer than the
|
||||||
|
/// box `place` gives it, and nothing where the share fits -- with the
|
||||||
|
/// windows that answer holds for, which is a range either way.
|
||||||
|
///
|
||||||
|
/// A share is a length only to whoever divides one, and nothing divides a
|
||||||
|
/// box handed to one child: what is left of it after the pixels and the
|
||||||
|
/// fraction beside the share is what the share takes, so the length comes
|
||||||
|
/// to the whole box until those are longer than it and to them once they
|
||||||
|
/// are. Only that second case is a box its parent did not give, and the
|
||||||
|
/// crossing between them is a question in pixels.
|
||||||
|
fn share_past_the_offer(
|
||||||
|
&self,
|
||||||
|
stated: Option<LayoutLen>,
|
||||||
|
window: Px,
|
||||||
|
place: PlaceDesc,
|
||||||
|
align: RegionAlign,
|
||||||
|
axis: Axis,
|
||||||
|
) -> (Option<Len>, Holds) {
|
||||||
|
// A place that is the widget's placement outright is a box its parent
|
||||||
|
// decided, and a parent that divides one has already given the share
|
||||||
|
// whatever it was owed. Only an offer -- a box with the answer still
|
||||||
|
// to be placed inside it -- is a box a share reads.
|
||||||
|
if place[axis].fit.fills() {
|
||||||
|
return (None, Holds::ANY);
|
||||||
|
}
|
||||||
|
// A share with nothing beside it is the box whatever the box is, so
|
||||||
|
// there is no comparison to make and no range to keep for one.
|
||||||
|
let Some(stated) = stated else {
|
||||||
|
return (None, Holds::ANY);
|
||||||
|
};
|
||||||
|
if stated.leftover == Weight::ZERO || stated.is_only_leftover() {
|
||||||
|
return (None, Holds::ANY);
|
||||||
|
}
|
||||||
|
let fixed = stated
|
||||||
|
.without_leftover()
|
||||||
|
.within_len(place.base(axis, self.rel_base));
|
||||||
|
let offer = place.of(self.region, align)[axis].len();
|
||||||
|
let (longer, holds) = fixed.longer_than(offer, window);
|
||||||
|
(longer.then_some(fixed), holds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl LayoutLen {
|
impl LayoutLen {
|
||||||
/// Whether what a widget reported along an axis is the whole of the box
|
/// Whether what a widget reported along an axis is the whole of the box
|
||||||
/// it is in rather than a part to be placed inside it. A share fills,
|
/// it is in rather than a part to be placed inside it. A share fills,
|
||||||
/// because a share is a length only to whoever divides one, and whoever
|
/// because a share is a length only to whoever divides one, and whoever
|
||||||
/// did is the one that handed down this box. A declared axis does too:
|
/// did is the one that handed down this box. An axis the parent decided
|
||||||
/// the rule already gave the region its length, and the rule's length is
|
/// from the answer is the answer already.
|
||||||
/// what the widget reports there. And an axis the parent decided from
|
pub(super) fn fills(&self, decided: bool) -> bool {
|
||||||
/// the answer is the answer already.
|
self.leftover != Weight::ZERO || decided
|
||||||
pub(super) fn fills(self, declared: Option<Len>, decided: bool) -> bool {
|
|
||||||
self.leftover != Weight::ZERO || declared.is_some() || decided
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,17 +1050,11 @@ impl PlaceDesc {
|
|||||||
/// part. That is what makes a fraction the same fraction wherever the part
|
/// part. That is what makes a fraction the same fraction wherever the part
|
||||||
/// it is placed in sits and however long it is -- the fraction is resolved
|
/// it is placed in sits and however long it is -- the fraction is resolved
|
||||||
/// once, here, against the rel base it was reported of.
|
/// once, here, against the rel base it was reported of.
|
||||||
pub(super) fn placement(
|
pub(super) fn placement(self, region: UiRegion, size: Size, align: RegionAlign) -> UiRegion {
|
||||||
self,
|
|
||||||
region: UiRegion,
|
|
||||||
size: Size,
|
|
||||||
declared: Declared,
|
|
||||||
align: RegionAlign,
|
|
||||||
) -> UiRegion {
|
|
||||||
let mut placed = region;
|
let mut placed = region;
|
||||||
for axis in Axis::BOTH {
|
for axis in Axis::BOTH {
|
||||||
let reported = size[axis];
|
let reported = size[axis];
|
||||||
if reported.fills(declared[axis], self[axis].fills) {
|
if reported.fills(self[axis].fit.fills()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
|
placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
|
||||||
@@ -717,11 +1067,8 @@ impl PlaceDesc {
|
|||||||
///
|
///
|
||||||
/// `own` is that widget's own box, and `place` what of it the child is
|
/// `own` is that widget's own box, and `place` what of it the child is
|
||||||
/// given, including any rel base it states -- a row's slot, or padding's rel
|
/// given, including any rel base it states -- a row's slot, or padding's rel
|
||||||
/// base less its pixels. That is a window length, like every other length
|
/// base less its pixels. The ask has already resolved declarations into
|
||||||
/// here, since a slot of a row is not a fraction of anything the row can
|
/// window lengths, so placement only aligns them inside the given box.
|
||||||
/// name. The child's declaration is a fraction of whichever reached it, and
|
|
||||||
/// is the only one that also places the box: a box the caller decided is
|
|
||||||
/// what `place` names.
|
|
||||||
pub(super) fn rel_base_and_region(
|
pub(super) fn rel_base_and_region(
|
||||||
self,
|
self,
|
||||||
own: UiRegion,
|
own: UiRegion,
|
||||||
@@ -733,13 +1080,8 @@ impl PlaceDesc {
|
|||||||
let mut rel_base = parent_rel_base;
|
let mut rel_base = parent_rel_base;
|
||||||
let mut region = given;
|
let mut region = given;
|
||||||
for axis in Axis::BOTH {
|
for axis in Axis::BOTH {
|
||||||
let base = match self[axis].rel_base {
|
let base = self.base(axis, parent_rel_base);
|
||||||
RelBase::Len(len) => len,
|
let len = declared[axis].unwrap_or(base);
|
||||||
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
|
|
||||||
};
|
|
||||||
let len = declared[axis]
|
|
||||||
.map(|len| len.within_len(base))
|
|
||||||
.unwrap_or(base);
|
|
||||||
rel_base[axis] = len;
|
rel_base[axis] = len;
|
||||||
if declared[axis].is_some() {
|
if declared[axis].is_some() {
|
||||||
region[axis] = given[axis].place(len, align[axis]);
|
region[axis] = given[axis].place(len, align[axis]);
|
||||||
|
|||||||
+36
-6
@@ -1,5 +1,5 @@
|
|||||||
use crate::util::impl_axis_index;
|
use crate::util::impl_axis_index;
|
||||||
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan};
|
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan, UiVec2};
|
||||||
|
|
||||||
/// How a child's region along one axis comes from the region of the widget
|
/// How a child's region along one axis comes from the region of the widget
|
||||||
/// asking, and what its fractions are of.
|
/// asking, and what its fractions are of.
|
||||||
@@ -12,10 +12,24 @@ use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan
|
|||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct PlaceDescAxis {
|
pub struct PlaceDescAxis {
|
||||||
pub span: PlaceSpan,
|
pub span: PlaceSpan,
|
||||||
pub fills: bool,
|
pub fit: PlaceFit,
|
||||||
pub rel_base: RelBase,
|
pub rel_base: RelBase,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum PlaceFit {
|
||||||
|
Align,
|
||||||
|
Fill,
|
||||||
|
/// The parent has already evaluated the child's size request.
|
||||||
|
Allocated,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaceFit {
|
||||||
|
pub fn fills(&self) -> bool {
|
||||||
|
!matches!(self, Self::Align)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum PlaceSpan {
|
pub enum PlaceSpan {
|
||||||
Within(UiSpan),
|
Within(UiSpan),
|
||||||
@@ -44,7 +58,13 @@ impl PlaceDescAxis {
|
|||||||
/// it again. A container uses it where it hands back exactly what the
|
/// it again. A container uses it where it hands back exactly what the
|
||||||
/// child asked for -- a row placing a child at the length it reported.
|
/// child asked for -- a row placing a child at the length it reported.
|
||||||
pub const fn fills(mut self) -> Self {
|
pub const fn fills(mut self) -> Self {
|
||||||
self.fills = true;
|
self.fit = PlaceFit::Fill;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A final allocation, including any comparisons in the child's request.
|
||||||
|
pub const fn allocated(mut self) -> Self {
|
||||||
|
self.fit = PlaceFit::Allocated;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +143,16 @@ impl PlaceDesc {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What a child's fractions on one axis are of, as a length of the
|
||||||
|
/// window: a length this place names, or the rel base of the widget
|
||||||
|
/// giving it, which is `parent_rel_base`.
|
||||||
|
pub(super) fn base(&self, axis: Axis, parent_rel_base: UiVec2) -> Len {
|
||||||
|
match self[axis].rel_base {
|
||||||
|
RelBase::Len(len) => len,
|
||||||
|
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The box each axis names, in the coordinates `own` is in.
|
/// The box each axis names, in the coordinates `own` is in.
|
||||||
pub fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion {
|
pub fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion {
|
||||||
UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y))
|
UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y))
|
||||||
@@ -143,7 +173,7 @@ impl UiSpan {
|
|||||||
pub const fn within_desc(self) -> PlaceDescAxis {
|
pub const fn within_desc(self) -> PlaceDescAxis {
|
||||||
PlaceDescAxis {
|
PlaceDescAxis {
|
||||||
span: PlaceSpan::Within(self),
|
span: PlaceSpan::Within(self),
|
||||||
fills: false,
|
fit: PlaceFit::Align,
|
||||||
rel_base: RelBase::WithRegion,
|
rel_base: RelBase::WithRegion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,7 +191,7 @@ impl UiSpan {
|
|||||||
pub const fn shifted_desc(self) -> PlaceDescAxis {
|
pub const fn shifted_desc(self) -> PlaceDescAxis {
|
||||||
PlaceDescAxis {
|
PlaceDescAxis {
|
||||||
span: PlaceSpan::Shifted(self),
|
span: PlaceSpan::Shifted(self),
|
||||||
fills: false,
|
fit: PlaceFit::Align,
|
||||||
rel_base: RelBase::Inherit,
|
rel_base: RelBase::Inherit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +205,7 @@ impl Len {
|
|||||||
pub const fn as_desc(self) -> PlaceDescAxis {
|
pub const fn as_desc(self) -> PlaceDescAxis {
|
||||||
PlaceDescAxis {
|
PlaceDescAxis {
|
||||||
span: PlaceSpan::Sized(self),
|
span: PlaceSpan::Sized(self),
|
||||||
fills: false,
|
fit: PlaceFit::Align,
|
||||||
rel_base: RelBase::Len(self),
|
rel_base: RelBase::Len(self),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+298
-122
@@ -1,9 +1,10 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, Answer, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx,
|
ActiveData, Answer, Axis, Bounds, Declared, DrawId, DrawLayers, IdLike, LayoutHolds, LayoutLen,
|
||||||
MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion,
|
Len, MaskIdx, MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Size, StrongWidget,
|
||||||
UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
|
UiRegion, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
|
||||||
|
ui::painter::Ask,
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,11 +24,18 @@ pub(super) struct DrawInfo {
|
|||||||
/// The box the widget is asked in, in its parent region node's
|
/// The box the widget is asked in, in its parent region node's
|
||||||
/// coordinates.
|
/// coordinates.
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
/// Where the widget is put, and where it was asked, as parts of the
|
/// Where the widget is put, and what its parent offered it, as parts of
|
||||||
/// parent's box. See [`PlaceDesc`]. The two are one ask's place until the
|
/// the parent's box. See [`PlaceDesc`]. The two are one place until a
|
||||||
/// parent puts the answer somewhere else.
|
/// rule of the widget's own takes it past the offer, or the parent puts
|
||||||
|
/// the answer somewhere else.
|
||||||
pub placed: PlaceDesc,
|
pub placed: PlaceDesc,
|
||||||
pub asked: PlaceDesc,
|
pub asked: PlaceDesc,
|
||||||
|
/// What the ask made of the widget's own rules. See [`Ask::declared`]
|
||||||
|
/// and [`Ask::bounds`].
|
||||||
|
pub declared: Declared,
|
||||||
|
pub bounds: Bounds,
|
||||||
|
/// What the ask that gave it those two holds for. See [`Ask::holds`].
|
||||||
|
pub ask_holds: LayoutHolds,
|
||||||
/// Whether the parent already asked about this widget in this draw.
|
/// Whether the parent already asked about this widget in this draw.
|
||||||
pub re_asked: bool,
|
pub re_asked: bool,
|
||||||
}
|
}
|
||||||
@@ -43,7 +51,8 @@ pub(super) struct Drawn {
|
|||||||
/// What a widget's children are placed in: its own box, the coordinates its
|
/// What a widget's children are placed in: its own box, the coordinates its
|
||||||
/// drawing is in, and what else one ask of a child is decided from.
|
/// drawing is in, and what else one ask of a child is decided from.
|
||||||
pub(super) struct Placing {
|
pub(super) struct Placing {
|
||||||
pub id: WidgetId,
|
/// The widget whose box this is, and nothing for the window.
|
||||||
|
pub id: Option<WidgetId>,
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
pub rel_base: UiVec2,
|
pub rel_base: UiVec2,
|
||||||
pub depth: usize,
|
pub depth: usize,
|
||||||
@@ -51,6 +60,21 @@ pub(super) struct Placing {
|
|||||||
pub mask: MaskIdx,
|
pub mask: MaskIdx,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Placing {
|
||||||
|
/// The window, which is what the root is placed within. Nothing above the
|
||||||
|
/// root narrowed a box or chose where it goes, so it is asked in the whole
|
||||||
|
/// output and its fractions are of the whole output -- an ordinary ask,
|
||||||
|
/// from the one box nobody drew.
|
||||||
|
pub const WINDOW: Self = Self {
|
||||||
|
id: None,
|
||||||
|
region: UiRegion::FULL,
|
||||||
|
rel_base: UiVec2::FULL_SIZE,
|
||||||
|
depth: 0,
|
||||||
|
move_idx: MoveIdx::NONE,
|
||||||
|
mask: MaskIdx::NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub struct UiRenderState {
|
pub struct UiRenderState {
|
||||||
pub active: HashMap<WidgetId, ActiveData>,
|
pub active: HashMap<WidgetId, ActiveData>,
|
||||||
pub layers: DrawLayers,
|
pub layers: DrawLayers,
|
||||||
@@ -70,7 +94,14 @@ pub struct UiRenderState {
|
|||||||
deferred: crate::util::HashSet<WidgetId>,
|
deferred: crate::util::HashSet<WidgetId>,
|
||||||
/// What the walk has left to settle, deepest last. Ordered rather than
|
/// What the walk has left to settle, deepest last. Ordered rather than
|
||||||
/// searched for, so finding the next one is not a pass over the marks.
|
/// searched for, so finding the next one is not a pass over the marks.
|
||||||
pending: std::collections::BTreeSet<(usize, WidgetId)>,
|
pending: std::collections::BinaryHeap<(usize, WidgetId)>,
|
||||||
|
pub(super) requests: crate::RequestArena,
|
||||||
|
changed: Vec<WidgetId>,
|
||||||
|
/// The last draw id handed out. Each draw takes a fresh one and leaves it
|
||||||
|
/// on every widget it asks about, which is how it knows in one read
|
||||||
|
/// whether it has asked already.
|
||||||
|
last_draw: DrawId,
|
||||||
|
request_readers: HashMap<WidgetId, crate::util::HashSet<WidgetId>>,
|
||||||
pub moves: Moves,
|
pub moves: Moves,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +115,10 @@ impl UiRenderState {
|
|||||||
slots: Default::default(),
|
slots: Default::default(),
|
||||||
deferred: Default::default(),
|
deferred: Default::default(),
|
||||||
pending: Default::default(),
|
pending: Default::default(),
|
||||||
|
requests: Default::default(),
|
||||||
|
changed: Vec::new(),
|
||||||
|
last_draw: DrawId::NONE,
|
||||||
|
request_readers: Default::default(),
|
||||||
moves: Default::default(),
|
moves: Default::default(),
|
||||||
resized: false,
|
resized: false,
|
||||||
}
|
}
|
||||||
@@ -124,20 +159,23 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The root is asked about in the output. Its own rules narrow both its
|
/// The root's first draw: the ask [`Placing::WINDOW`] answered, with the
|
||||||
/// rel base and box; nothing above it chose a different one.
|
/// bookkeeping a widget with no parent carries.
|
||||||
fn root_info(&self, rel_base: UiVec2, region: UiRegion) -> DrawInfo {
|
fn root_info(&self, ask: &Ask, region_node: bool) -> DrawInfo {
|
||||||
DrawInfo {
|
DrawInfo {
|
||||||
layer: 0,
|
layer: 0,
|
||||||
parent: None,
|
parent: None,
|
||||||
depth: 1,
|
depth: Placing::WINDOW.depth + 1,
|
||||||
parent_move: MoveIdx::NONE,
|
parent_move: MoveIdx::NONE,
|
||||||
region_node: false,
|
region_node,
|
||||||
mask: MaskIdx::NONE,
|
mask: MaskIdx::NONE,
|
||||||
rel_base,
|
rel_base: ask.rel_base,
|
||||||
region,
|
region: ask.region,
|
||||||
placed: PlaceDesc::WHOLE,
|
placed: ask.place,
|
||||||
asked: PlaceDesc::WHOLE,
|
asked: PlaceDesc::WHOLE,
|
||||||
|
declared: ask.declared,
|
||||||
|
bounds: ask.bounds,
|
||||||
|
ask_holds: ask.holds,
|
||||||
re_asked: false,
|
re_asked: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +205,7 @@ impl UiRenderState {
|
|||||||
weak widgets: {all:#?}"
|
weak widgets: {all:#?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
self.requests.reset();
|
||||||
let root = root.into();
|
let root = root.into();
|
||||||
if self.root_changed(root) {
|
if self.root_changed(root) {
|
||||||
self.redraw_all(root, rsc);
|
self.redraw_all(root, rsc);
|
||||||
@@ -184,24 +223,18 @@ impl UiRenderState {
|
|||||||
let _layout = diag::timer(TimerKind::FullLayout);
|
let _layout = diag::timer(TimerKind::FullLayout);
|
||||||
self.clear(rsc);
|
self.clear(rsc);
|
||||||
if let Some(id) = root {
|
if let Some(id) = root {
|
||||||
let (rel_base, region) = Self::root_layout(id.id(), rsc.widgets());
|
let ask = Placing::WINDOW.ask(
|
||||||
let info = self.root_info(rel_base, region);
|
rsc.widgets(),
|
||||||
|
&mut self.requests,
|
||||||
|
self.output_size,
|
||||||
|
id.id(),
|
||||||
|
PlaceDesc::WHOLE,
|
||||||
|
);
|
||||||
|
let info = self.root_info(&ask, rsc.widgets().is_region_node(id.id()));
|
||||||
self.draw_inner(id.id(), info, None, rsc);
|
self.draw_inner(id.id(), info, None, rsc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The root's rel base and box: the window, taken in by the root's own
|
|
||||||
/// rules. Nothing above it narrowed anything or chose where it goes, so
|
|
||||||
/// its declaration is the whole of what decides either.
|
|
||||||
fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) {
|
|
||||||
PlaceDesc::WHOLE.rel_base_and_region(
|
|
||||||
UiRegion::FULL,
|
|
||||||
UiVec2::FULL_SIZE,
|
|
||||||
widgets.declared_lens(id),
|
|
||||||
widgets.alignment(id),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn draw_inner(
|
pub(super) fn draw_inner(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
@@ -226,7 +259,6 @@ impl UiRenderState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let align = rsc.widgets().alignment(id);
|
let align = rsc.widgets().alignment(id);
|
||||||
let declared = rsc.widgets().declared_lens(id);
|
|
||||||
// Nothing this widget measured can be dirty while it draws: layout is
|
// Nothing this widget measured can be dirty while it draws: layout is
|
||||||
// one bottom-up walk, so anything deeper has settled or deferred to
|
// one bottom-up walk, so anything deeper has settled or deferred to
|
||||||
// its own parent, and a deferred one leaves that parent marked.
|
// its own parent, and a deferred one leaves that parent marked.
|
||||||
@@ -240,7 +272,7 @@ impl UiRenderState {
|
|||||||
.then(|| self.retained_answer(id, region, info))
|
.then(|| self.retained_answer(id, region, info))
|
||||||
.flatten()
|
.flatten()
|
||||||
.and_then(|answer| {
|
.and_then(|answer| {
|
||||||
let placed = info.placed.placement(region, answer.size, declared, align);
|
let placed = info.placed.placement(region, answer.size, align);
|
||||||
self.try_reuse(id, region, placed, info, rsc)
|
self.try_reuse(id, region, placed, info, rsc)
|
||||||
.then_some(answer)
|
.then_some(answer)
|
||||||
});
|
});
|
||||||
@@ -252,7 +284,7 @@ impl UiRenderState {
|
|||||||
// Where the drawing goes: the part its parent gave it, with the
|
// Where the drawing goes: the part its parent gave it, with the
|
||||||
// answer placed inside that part on any axis the parent left
|
// answer placed inside that part on any axis the parent left
|
||||||
// open.
|
// open.
|
||||||
let placed = info.placed.placement(region, answer.size, declared, align);
|
let placed = info.placed.placement(region, answer.size, align);
|
||||||
if placed != region {
|
if placed != region {
|
||||||
self.relocate(id, placed, info, rsc);
|
self.relocate(id, placed, info, rsc);
|
||||||
}
|
}
|
||||||
@@ -295,6 +327,14 @@ impl UiRenderState {
|
|||||||
old: Option<ActiveData>,
|
old: Option<ActiveData>,
|
||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) -> Answer {
|
) -> Answer {
|
||||||
|
let draw = self.next_draw();
|
||||||
|
// Whoever asked about this widget wrote this, and a draw of the widget
|
||||||
|
// itself is not that: the record is rebuilt below, so it is carried
|
||||||
|
// across rather than reset.
|
||||||
|
let asked_by = old
|
||||||
|
.as_ref()
|
||||||
|
.or_else(|| self.active.get(&id))
|
||||||
|
.map_or(DrawId::NONE, |active| active.asked_by);
|
||||||
let rel_base = info.rel_base;
|
let rel_base = info.rel_base;
|
||||||
let (move_idx, region, retired_move) = match info.region_node {
|
let (move_idx, region, retired_move) = match info.region_node {
|
||||||
// A node entry is only a translation. Its local box keeps the
|
// A node entry is only a translation. Its local box keeps the
|
||||||
@@ -311,7 +351,27 @@ impl UiRenderState {
|
|||||||
let mask_slot = old
|
let mask_slot = old
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|old| old.mask_region.map(|_| old.mask));
|
.and_then(|old| old.mask_region.map(|_| old.mask));
|
||||||
let old_children = old.map_or_else(Vec::new, |old| old.children);
|
let (mut old_children, textures, primitives, request_deps, mut scratch) = match old {
|
||||||
|
Some(old) => (
|
||||||
|
old.children,
|
||||||
|
old.textures,
|
||||||
|
old.primitives,
|
||||||
|
old.request_deps,
|
||||||
|
old.scratch,
|
||||||
|
),
|
||||||
|
None => Default::default(),
|
||||||
|
};
|
||||||
|
// Every one of these is a buffer this widget's last draw filled and
|
||||||
|
// `remove` emptied, kept for its capacity alone. A drawing whose
|
||||||
|
// primitives were still in it would record them twice.
|
||||||
|
debug_assert!(
|
||||||
|
textures.is_empty() && primitives.is_empty() && request_deps.is_empty(),
|
||||||
|
"'{}' ({id:?}) was drawn again over what its last draw left",
|
||||||
|
rsc.widgets().label(id)
|
||||||
|
);
|
||||||
|
let children = std::mem::take(&mut scratch.children);
|
||||||
|
let size_deps = std::mem::take(&mut scratch.size_deps);
|
||||||
|
let under = std::mem::take(&mut scratch.under);
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
let window = self.output_size;
|
let window = self.output_size;
|
||||||
let mut painter = Painter {
|
let mut painter = Painter {
|
||||||
@@ -323,17 +383,20 @@ impl UiRenderState {
|
|||||||
layer: info.layer,
|
layer: info.layer,
|
||||||
own_layer: info.layer,
|
own_layer: info.layer,
|
||||||
id,
|
id,
|
||||||
textures: Vec::new(),
|
textures,
|
||||||
primitives: Vec::new(),
|
primitives,
|
||||||
mask_region: None,
|
mask_region: None,
|
||||||
mask_slot,
|
mask_slot,
|
||||||
children: Vec::new(),
|
children,
|
||||||
size_deps: Vec::new(),
|
size_deps,
|
||||||
|
request_deps,
|
||||||
|
scratch,
|
||||||
own: LayoutHolds::ANY,
|
own: LayoutHolds::ANY,
|
||||||
under: Vec::new(),
|
under,
|
||||||
answer_under: LayoutHolds::ANY,
|
answer_under: LayoutHolds::ANY,
|
||||||
depth: info.depth,
|
depth: info.depth,
|
||||||
move_idx,
|
move_idx,
|
||||||
|
draw,
|
||||||
rsc,
|
rsc,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -362,12 +425,15 @@ impl UiRenderState {
|
|||||||
own,
|
own,
|
||||||
answer_under,
|
answer_under,
|
||||||
children,
|
children,
|
||||||
size_deps,
|
mut size_deps,
|
||||||
under,
|
request_deps,
|
||||||
|
mut scratch,
|
||||||
|
mut under,
|
||||||
move_idx,
|
move_idx,
|
||||||
layer,
|
layer,
|
||||||
own_layer: _,
|
own_layer: _,
|
||||||
depth: _,
|
depth: _,
|
||||||
|
draw: _,
|
||||||
id,
|
id,
|
||||||
} = painter;
|
} = painter;
|
||||||
|
|
||||||
@@ -379,11 +445,15 @@ impl UiRenderState {
|
|||||||
// A rule wins on the axis it names, and the draw answers the rest.
|
// A rule wins on the axis it names, and the draw answers the rest.
|
||||||
// Applied here so it is one place rather than every widget that could
|
// Applied here so it is one place rather than every widget that could
|
||||||
// carry one, and so the widget under a rule never learns of it. The
|
// carry one, and so the widget under a rule never learns of it. The
|
||||||
// rel base is the answer where the rule gave a length outright: it was
|
// rel base is the answer wherever the ask declared a length: it was
|
||||||
// resolved into the rel base when the child was asked, and resolving it
|
// resolved into the rel base when the widget was asked, and resolving
|
||||||
// again here would take the fraction of a fraction.
|
// it again here would take the fraction of a fraction.
|
||||||
let rules = rsc.widgets().size_rules(id);
|
let rules = rsc.widgets().size_rules(id);
|
||||||
let ruled = |axis: Axis, reported: LayoutLen| match rules[axis].exact() {
|
let ruled = |axis: Axis, reported: LayoutLen| {
|
||||||
|
if rules[axis].deferred().is_some() {
|
||||||
|
return info.rel_base[axis].into();
|
||||||
|
}
|
||||||
|
match rules[axis].exact() {
|
||||||
None => reported,
|
None => reported,
|
||||||
Some(len) if len.leftover == Weight::ZERO => LayoutLen {
|
Some(len) if len.leftover == Weight::ZERO => LayoutLen {
|
||||||
rel: info.rel_base[axis].rel,
|
rel: info.rel_base[axis].rel,
|
||||||
@@ -391,11 +461,50 @@ impl UiRenderState {
|
|||||||
leftover: Weight::ZERO,
|
leftover: Weight::ZERO,
|
||||||
},
|
},
|
||||||
Some(len) => len.within_len(info.rel_base[axis]),
|
Some(len) => len.within_len(info.rel_base[axis]),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let size = Size {
|
let mut size = Size {
|
||||||
x: ruled(Axis::X, size.x),
|
x: ruled(Axis::X, size.x),
|
||||||
y: ruled(Axis::Y, size.y),
|
y: ruled(Axis::Y, size.y),
|
||||||
};
|
};
|
||||||
|
// What the drawing read is combined with what the ask decided rather
|
||||||
|
// than replacing it: a widget may widen its own ranges, and cannot
|
||||||
|
// widen the ask's.
|
||||||
|
let mut own_holds = own.and(info.ask_holds);
|
||||||
|
for axis in Axis::BOTH {
|
||||||
|
// A rule that is a fraction of the rel base is answered with the
|
||||||
|
// rel base's own length, so the answer is that rel base's and not
|
||||||
|
// just that many pixels of this window -- the same pin a widget
|
||||||
|
// that read its rel base took for its drawing. A bound counts:
|
||||||
|
// which side of it the box fell was decided against this rel
|
||||||
|
// base, and the same box of a different one can fall on the other.
|
||||||
|
if rules[axis].has_fraction() {
|
||||||
|
own_holds[axis].rel_base = Some(info.rel_base[axis]);
|
||||||
|
}
|
||||||
|
// A bound is a promise about the length as well as about the box:
|
||||||
|
// a widget that drew past the box it was given -- a text too tall
|
||||||
|
// for it, an image at its own size under a cap -- is still held to
|
||||||
|
// what its rule allows.
|
||||||
|
//
|
||||||
|
// Held here rather than taken from the box, even where the bound
|
||||||
|
// decided that box. What a widget answers is its own, and a bound
|
||||||
|
// that replaced the answer would make a share into a fixed length
|
||||||
|
// the moment a box was long enough -- which is a length the span
|
||||||
|
// dividing that box decided from this answer, so the two would
|
||||||
|
// choose each other. A share is left alone here for the same
|
||||||
|
// reason: it is a length only to whoever divides one, and the box
|
||||||
|
// that divider gives is a box this widget is asked in, where the
|
||||||
|
// bound is applied to it.
|
||||||
|
let answer = size[axis];
|
||||||
|
if answer.leftover != Weight::ZERO {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (held, kept) = info.bounds[axis].outside(answer.without_leftover(), window[axis]);
|
||||||
|
own_holds[axis].window = own_holds[axis].window.and(kept);
|
||||||
|
if let Some(held) = held {
|
||||||
|
size[axis] = held.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
// A widget that clipped its contents to its box drew nothing outside
|
// A widget that clipped its contents to its box drew nothing outside
|
||||||
// it, so reporting more than the box asks to be placed at a length it
|
// it, so reporting more than the box asks to be placed at a length it
|
||||||
// does not occupy -- and its parent would place the part it cut off.
|
// does not occupy -- and its parent would place the part it cut off.
|
||||||
@@ -413,7 +522,7 @@ impl UiRenderState {
|
|||||||
region.to_px(window),
|
region.to_px(window),
|
||||||
);
|
);
|
||||||
for c in &old_children {
|
for c in &old_children {
|
||||||
if !children.contains(c) {
|
if !self.asked_in(*c, draw) {
|
||||||
self.undraw_rec(*c, rsc);
|
self.undraw_rec(*c, rsc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,21 +532,10 @@ impl UiRenderState {
|
|||||||
if let Some(idx) = retired_move {
|
if let Some(idx) = retired_move {
|
||||||
self.moves.remove(idx);
|
self.moves.remove(idx);
|
||||||
}
|
}
|
||||||
// A rule that is a fraction of the rel base is answered with the
|
|
||||||
// rel base's own length, so the answer is that rel base's and not just
|
|
||||||
// that many pixels of this window -- the same pin a widget that read
|
|
||||||
// its rel base took for its drawing.
|
|
||||||
let mut own_holds = own;
|
|
||||||
for axis in Axis::BOTH {
|
|
||||||
let fraction = rules[axis].exact().is_some_and(|len| len.rel != Rel::ZERO);
|
|
||||||
if fraction {
|
|
||||||
own_holds[axis].rel_base = Some(info.rel_base[axis]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let answer_holds = own_holds.and(answer_under);
|
let answer_holds = own_holds.and(answer_under);
|
||||||
let holds = under
|
let holds = under
|
||||||
.into_iter()
|
.iter()
|
||||||
.fold(answer_holds, |holds, (_, child)| holds.and(child));
|
.fold(answer_holds, |holds, (_, child)| holds.and(*child));
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
holds.contains(self.output_size, info.rel_base, region),
|
holds.contains(self.output_size, info.rel_base, region),
|
||||||
"'{}' ({id:?}) drew in {}, outside the ranges it reported: {holds:?}",
|
"'{}' ({id:?}) drew in {}, outside the ranges it reported: {holds:?}",
|
||||||
@@ -448,7 +546,7 @@ impl UiRenderState {
|
|||||||
// and a change there has to reach it. Asking answered whatever mark
|
// and a change there has to reach it. Asking answered whatever mark
|
||||||
// it had: a hint is read live, and a drawing is not kept past one.
|
// it had: a hint is read live, and a drawing is not kept past one.
|
||||||
for &dep in &size_deps {
|
for &dep in &size_deps {
|
||||||
if !children.contains(&dep) {
|
if !self.asked_in(dep, draw) {
|
||||||
self.asked(
|
self.asked(
|
||||||
dep,
|
dep,
|
||||||
DrawInfo {
|
DrawInfo {
|
||||||
@@ -462,6 +560,9 @@ impl UiRenderState {
|
|||||||
region: UiRegion::FULL,
|
region: UiRegion::FULL,
|
||||||
placed: PlaceDesc::WHOLE,
|
placed: PlaceDesc::WHOLE,
|
||||||
asked: PlaceDesc::WHOLE,
|
asked: PlaceDesc::WHOLE,
|
||||||
|
declared: Declared::NONE,
|
||||||
|
bounds: Bounds::ANY,
|
||||||
|
ask_holds: LayoutHolds::ANY,
|
||||||
re_asked: false,
|
re_asked: false,
|
||||||
},
|
},
|
||||||
rsc,
|
rsc,
|
||||||
@@ -470,6 +571,15 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for &dep in &request_deps {
|
||||||
|
self.request_readers.entry(dep).or_default().insert(id);
|
||||||
|
}
|
||||||
|
old_children.clear();
|
||||||
|
size_deps.clear();
|
||||||
|
under.clear();
|
||||||
|
scratch.children = old_children;
|
||||||
|
scratch.size_deps = size_deps;
|
||||||
|
scratch.under = under;
|
||||||
let active = ActiveData {
|
let active = ActiveData {
|
||||||
id,
|
id,
|
||||||
placement: region,
|
placement: region,
|
||||||
@@ -479,6 +589,7 @@ impl UiRenderState {
|
|||||||
region,
|
region,
|
||||||
// Whoever asked writes the answer.
|
// Whoever asked writes the answer.
|
||||||
answer: None,
|
answer: None,
|
||||||
|
asked_by,
|
||||||
re_asked: info.re_asked,
|
re_asked: info.re_asked,
|
||||||
size,
|
size,
|
||||||
holds,
|
holds,
|
||||||
@@ -489,7 +600,10 @@ impl UiRenderState {
|
|||||||
primitives,
|
primitives,
|
||||||
mask_region,
|
mask_region,
|
||||||
children,
|
children,
|
||||||
declared: rsc.widgets().declared_lens(id),
|
request_deps,
|
||||||
|
scratch,
|
||||||
|
declared: info.declared,
|
||||||
|
bounds: info.bounds,
|
||||||
own_align: rsc.widgets().alignment(id),
|
own_align: rsc.widgets().alignment(id),
|
||||||
move_idx,
|
move_idx,
|
||||||
parent_move: info.parent_move,
|
parent_move: info.parent_move,
|
||||||
@@ -505,6 +619,21 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn next_draw(&mut self) -> DrawId {
|
||||||
|
self.last_draw = self.last_draw.next();
|
||||||
|
self.last_draw
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `draw` has asked about this widget, which is what it left on
|
||||||
|
/// the widget's own record when it did. One widget is asked about by one
|
||||||
|
/// container, since the handle a container holds a child by cannot be
|
||||||
|
/// cloned, so one note per widget is enough to answer this.
|
||||||
|
pub(super) fn asked_in(&self, id: WidgetId, draw: DrawId) -> bool {
|
||||||
|
self.active
|
||||||
|
.get(&id)
|
||||||
|
.is_some_and(|active| active.asked_by == draw)
|
||||||
|
}
|
||||||
|
|
||||||
/// Keeps a region node's entry across redraws because descendants retain
|
/// Keeps a region node's entry across redraws because descendants retain
|
||||||
/// its index.
|
/// its index.
|
||||||
fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx {
|
fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx {
|
||||||
@@ -534,6 +663,7 @@ impl UiRenderState {
|
|||||||
if !active.drawn
|
if !active.drawn
|
||||||
|| active.is_region_node() != info.region_node
|
|| active.is_region_node() != info.region_node
|
||||||
|| active.parent_move != info.parent_move
|
|| active.parent_move != info.parent_move
|
||||||
|
|| active.bounds != info.bounds
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -659,6 +789,9 @@ impl UiRenderState {
|
|||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
active.rel_base = info.rel_base;
|
active.rel_base = info.rel_base;
|
||||||
active.placed = info.placed;
|
active.placed = info.placed;
|
||||||
|
// What the ask made of its rules, which a re-place decides again.
|
||||||
|
active.declared = info.declared;
|
||||||
|
active.bounds = info.bounds;
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
{
|
{
|
||||||
let (counter, outcome) = match (moved, is_region_node) {
|
let (counter, outcome) = match (moved, is_region_node) {
|
||||||
@@ -692,12 +825,11 @@ impl UiRenderState {
|
|||||||
let placed = place.placement(
|
let placed = place.placement(
|
||||||
region,
|
region,
|
||||||
active.measured().unwrap_or(active.size),
|
active.measured().unwrap_or(active.size),
|
||||||
active.declared,
|
|
||||||
active.own_align,
|
active.own_align,
|
||||||
);
|
);
|
||||||
let info = DrawInfo {
|
let info = DrawInfo {
|
||||||
layer: active.layer,
|
layer: active.layer,
|
||||||
parent: Some(at.id),
|
parent: at.id,
|
||||||
depth: at.depth + 1,
|
depth: at.depth + 1,
|
||||||
parent_move: at.move_idx,
|
parent_move: at.move_idx,
|
||||||
region_node: active.is_region_node(),
|
region_node: active.is_region_node(),
|
||||||
@@ -706,6 +838,10 @@ impl UiRenderState {
|
|||||||
region,
|
region,
|
||||||
placed: place,
|
placed: place,
|
||||||
asked: active.asked,
|
asked: active.asked,
|
||||||
|
declared: active.declared,
|
||||||
|
bounds: active.bounds,
|
||||||
|
// Placing decides no box: this is the one the ask already gave.
|
||||||
|
ask_holds: LayoutHolds::ANY,
|
||||||
re_asked: active.re_asked,
|
re_asked: active.re_asked,
|
||||||
};
|
};
|
||||||
self.relocate(child, placed, info, rsc);
|
self.relocate(child, placed, info, rsc);
|
||||||
@@ -713,8 +849,8 @@ impl UiRenderState {
|
|||||||
|
|
||||||
/// The rel base and the box a widget already drawn is given at `place` of
|
/// The rel base and the box a widget already drawn is given at `place` of
|
||||||
/// the box its parent is being taken as. What narrowed its rel base and what
|
/// the box its parent is being taken as. What narrowed its rel base and what
|
||||||
/// it declared are its own record's, so both are resolved against that
|
/// it declared are its own record's. Declared lengths stay as the ask
|
||||||
/// parent's rel base again exactly as the first ask resolved them.
|
/// resolved them; the destination only decides where they sit.
|
||||||
fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) {
|
fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) {
|
||||||
place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align)
|
place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align)
|
||||||
}
|
}
|
||||||
@@ -734,7 +870,7 @@ impl UiRenderState {
|
|||||||
rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&placed);
|
rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&placed);
|
||||||
}
|
}
|
||||||
let at = Placing {
|
let at = Placing {
|
||||||
id,
|
id: Some(id),
|
||||||
region: placed,
|
region: placed,
|
||||||
rel_base: info.rel_base,
|
rel_base: info.rel_base,
|
||||||
depth: info.depth,
|
depth: info.depth,
|
||||||
@@ -783,6 +919,11 @@ impl UiRenderState {
|
|||||||
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
|
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
|
||||||
let mut active = self.active.remove(&id);
|
let mut active = self.active.remove(&id);
|
||||||
if let Some(active) = &mut active {
|
if let Some(active) = &mut active {
|
||||||
|
for dep in active.request_deps.drain(..) {
|
||||||
|
if let Some(readers) = self.request_readers.get_mut(&dep) {
|
||||||
|
readers.remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
for primitive in &active.primitives {
|
for primitive in &active.primitives {
|
||||||
let mask = self.layers.free(&primitive.handle);
|
let mask = self.layers.free(&primitive.handle);
|
||||||
if mask != MaskIdx::NONE {
|
if mask != MaskIdx::NONE {
|
||||||
@@ -846,6 +987,7 @@ impl UiRenderState {
|
|||||||
asked: PlaceDesc::WHOLE,
|
asked: PlaceDesc::WHOLE,
|
||||||
region: UiRegion::FULL,
|
region: UiRegion::FULL,
|
||||||
answer: None,
|
answer: None,
|
||||||
|
asked_by: DrawId::NONE,
|
||||||
re_asked: false,
|
re_asked: false,
|
||||||
size,
|
size,
|
||||||
holds: LayoutHolds::ANY,
|
holds: LayoutHolds::ANY,
|
||||||
@@ -856,8 +998,11 @@ impl UiRenderState {
|
|||||||
primitives: Vec::new(),
|
primitives: Vec::new(),
|
||||||
mask_region: None,
|
mask_region: None,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
|
request_deps: Vec::new(),
|
||||||
|
scratch: Default::default(),
|
||||||
move_idx: info.parent_move,
|
move_idx: info.parent_move,
|
||||||
declared: Declared::NONE,
|
declared: Declared::NONE,
|
||||||
|
bounds: Bounds::ANY,
|
||||||
own_align: rsc.widgets().alignment(id),
|
own_align: rsc.widgets().alignment(id),
|
||||||
parent_move: info.parent_move,
|
parent_move: info.parent_move,
|
||||||
mask: info.mask,
|
mask: info.mask,
|
||||||
@@ -868,6 +1013,7 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clear(&mut self, rsc: &mut dyn UiRsc) {
|
fn clear(&mut self, rsc: &mut dyn UiRsc) {
|
||||||
|
self.request_readers.clear();
|
||||||
for (_, active) in self.active.drain() {
|
for (_, active) in self.active.drain() {
|
||||||
if active.drawn {
|
if active.drawn {
|
||||||
rsc.on_undraw(&active);
|
rsc.on_undraw(&active);
|
||||||
@@ -888,6 +1034,7 @@ impl UiRenderState {
|
|||||||
rsc.on_remove(id);
|
rsc.on_remove(id);
|
||||||
self.remove(id, true, rsc);
|
self.remove(id, true, rsc);
|
||||||
self.drop_slot(id);
|
self.drop_slot(id);
|
||||||
|
self.request_readers.remove(&id);
|
||||||
}
|
}
|
||||||
rsc.ui_mut().textures.free();
|
rsc.ui_mut().textures.free();
|
||||||
}
|
}
|
||||||
@@ -895,6 +1042,18 @@ impl UiRenderState {
|
|||||||
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
let _layout = diag::timer(TimerKind::IncrementalLayout);
|
let _layout = diag::timer(TimerKind::IncrementalLayout);
|
||||||
|
self.changed.clear();
|
||||||
|
self.changed
|
||||||
|
.extend(rsc.widgets().needs_redraw.iter().copied());
|
||||||
|
while let Some(id) = self.changed.pop() {
|
||||||
|
if let Some(readers) = self.request_readers.get(&id) {
|
||||||
|
for &reader in readers {
|
||||||
|
if rsc.widgets_mut().needs_redraw.insert(reader) {
|
||||||
|
self.changed.push(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Deepest first, and strictly: a widget that cannot settle where it
|
// Deepest first, and strictly: a widget that cannot settle where it
|
||||||
// is defers to its parent rather than drawing the parent from
|
// is defers to its parent rather than drawing the parent from
|
||||||
// inside itself. It marks the parent, stays marked, and waits here
|
// inside itself. It marks the parent, stays marked, and waits here
|
||||||
@@ -906,21 +1065,21 @@ impl UiRenderState {
|
|||||||
// something below is about to change it -- which is the whole class
|
// something below is about to change it -- which is the whole class
|
||||||
// of defect where a widget settles inside its parent's draw, clears
|
// of defect where a widget settles inside its parent's draw, clears
|
||||||
// its mark there, and tells nobody its answer moved.
|
// its mark there, and tells nobody its answer moved.
|
||||||
// The queue is that set, ordered: a mark made while the walk runs
|
// A mark made while the walk runs queues itself through `mark`. What
|
||||||
// queues itself through `mark`. What ends the walk is still the set
|
// ends the walk is the marks being spent rather than the queue being
|
||||||
// being spent, not the queue, so a mark that reached it another way
|
// empty, so a mark that reached the queue twice, or that was settled
|
||||||
// cannot be left for the next frame.
|
// another way, costs a pop and nothing else.
|
||||||
loop {
|
loop {
|
||||||
for &id in rsc.widgets().needs_redraw.iter() {
|
for &id in rsc.widgets().needs_redraw.iter() {
|
||||||
if !self.deferred.contains(&id) {
|
if !self.deferred.contains(&id) {
|
||||||
let depth = self.depth(id);
|
let depth = self.depth(id);
|
||||||
self.pending.insert((depth, id));
|
self.pending.push((depth, id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.pending.is_empty() {
|
if self.pending.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
while let Some((depth, id)) = self.pending.pop_last() {
|
while let Some((depth, id)) = self.pending.pop() {
|
||||||
// Settled inside an ancestor's draw, or deferred to one,
|
// Settled inside an ancestor's draw, or deferred to one,
|
||||||
// since the mark that queued it.
|
// since the mark that queued it.
|
||||||
if self.deferred.contains(&id) || !rsc.widgets().needs_redraw.contains(&id) {
|
if self.deferred.contains(&id) || !rsc.widgets().needs_redraw.contains(&id) {
|
||||||
@@ -931,7 +1090,7 @@ impl UiRenderState {
|
|||||||
// depth it had under the parent it left.
|
// depth it had under the parent it left.
|
||||||
let now = self.depth(id);
|
let now = self.depth(id);
|
||||||
if now != depth {
|
if now != depth {
|
||||||
self.pending.insert((now, id));
|
self.pending.push((now, id));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
@@ -948,7 +1107,7 @@ impl UiRenderState {
|
|||||||
fn mark(&mut self, id: WidgetId, widgets: &mut Widgets) {
|
fn mark(&mut self, id: WidgetId, widgets: &mut Widgets) {
|
||||||
if widgets.needs_redraw.insert(id) && !self.deferred.contains(&id) {
|
if widgets.needs_redraw.insert(id) && !self.deferred.contains(&id) {
|
||||||
let depth = self.depth(id);
|
let depth = self.depth(id);
|
||||||
self.pending.insert((depth, id));
|
self.pending.push((depth, id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1042,15 +1201,32 @@ impl UiRenderState {
|
|||||||
let Some(active) = self.active.get(&id) else {
|
let Some(active) = self.active.get(&id) else {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
// Its parent resolved its declared lengths into its box and decided
|
// Asked where its parent asked it, which is what says whether the
|
||||||
// whether to draw it at all, so a change to either is the parent's
|
// question is still this widget's own: its parent resolved its
|
||||||
// to draw -- with the mark left on, so the parent draws it rather
|
// declared lengths into its box -- a bound of its own that the box
|
||||||
// than keeping it. So is a widget the parent asked twice: its
|
// falls outside is one of them -- and decided whether to draw it at
|
||||||
// layout rests on an answer this widget cannot give again alone.
|
// all, so a change to either is the parent's to draw, with the mark
|
||||||
let declared_changed = rsc.widgets().declared_lens(id) != active.declared;
|
// left on so the parent draws it rather than keeping it. So is a
|
||||||
|
// widget the parent asked twice: its layout rests on an answer this
|
||||||
|
// widget cannot give again alone. The root's parent is the window,
|
||||||
|
// which no draw made and no answer can move.
|
||||||
|
let at = match active.parent {
|
||||||
|
Some(parent) => self.placing_of(parent, self.active[&parent].region),
|
||||||
|
None => Placing::WINDOW,
|
||||||
|
};
|
||||||
|
let ask = at.ask(
|
||||||
|
rsc.widgets(),
|
||||||
|
&mut self.requests,
|
||||||
|
self.output_size,
|
||||||
|
id,
|
||||||
|
active.asked,
|
||||||
|
);
|
||||||
|
let active = &self.active[&id];
|
||||||
|
// Even an inactive bound changes what the parent must track about its offer.
|
||||||
|
let constraints_changed = ask.declared != active.declared || ask.bounds != active.bounds;
|
||||||
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
|
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
|
||||||
if let Some(parent) = active.parent
|
if let Some(parent) = active.parent
|
||||||
&& (declared_changed
|
&& (constraints_changed
|
||||||
|| alignment_changed
|
|| alignment_changed
|
||||||
|| active.re_asked
|
|| active.re_asked
|
||||||
|| !active.drawn
|
|| !active.drawn
|
||||||
@@ -1066,29 +1242,13 @@ impl UiRenderState {
|
|||||||
if !active.drawn {
|
if !active.drawn {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Nothing above the root resolved its rules or its alignment, so its
|
|
||||||
// box is its own to work out again against the output. Every other
|
|
||||||
// widget was given one.
|
|
||||||
let Some(parent) = active.parent else {
|
|
||||||
let (rel_base, region) = Self::root_layout(id, rsc.widgets());
|
|
||||||
let info = DrawInfo {
|
|
||||||
mask: active.parent_mask,
|
|
||||||
..self.root_info(rel_base, region)
|
|
||||||
};
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::LocalRedraws);
|
|
||||||
let old = self.remove(id, false, rsc);
|
|
||||||
self.draw_inner(id, info, old, rsc);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.placed);
|
let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.placed);
|
||||||
// The question its parent asked, asked again: the same place of the
|
// The place the ask above came to: the same place of the box the
|
||||||
// box the parent was asked in, which is the box the parent's own
|
// parent was asked in, which is the box the parent's own draw ran in
|
||||||
// draw ran in and what its children's parts are of. Where the
|
// and what its children's parts are of. Where the parent's answer put
|
||||||
// parent's answer put its own drawing is not a question anybody
|
// its own drawing is not a question anybody asked, and nothing is
|
||||||
// asked, and nothing is asked in it here either.
|
// asked in it here either.
|
||||||
let parent_at = self.placing_of(parent, self.active[&parent].region);
|
let (rel_base, region) = (ask.rel_base, ask.region);
|
||||||
let (rel_base, region) = Self::ask_again(active, &parent_at, active.asked);
|
|
||||||
let info = DrawInfo {
|
let info = DrawInfo {
|
||||||
layer: active.layer,
|
layer: active.layer,
|
||||||
parent: active.parent,
|
parent: active.parent,
|
||||||
@@ -1098,8 +1258,11 @@ impl UiRenderState {
|
|||||||
mask: active.parent_mask,
|
mask: active.parent_mask,
|
||||||
rel_base,
|
rel_base,
|
||||||
region,
|
region,
|
||||||
placed: active.asked,
|
placed: ask.place,
|
||||||
asked: active.asked,
|
asked: active.asked,
|
||||||
|
declared: ask.declared,
|
||||||
|
bounds: ask.bounds,
|
||||||
|
ask_holds: ask.holds,
|
||||||
re_asked: false,
|
re_asked: false,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
@@ -1108,38 +1271,51 @@ impl UiRenderState {
|
|||||||
let old = self.remove(id, false, rsc);
|
let old = self.remove(id, false, rsc);
|
||||||
let drawn = self.draw_inner(id, info, old, rsc);
|
let drawn = self.draw_inner(id, info, old, rsc);
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
// A wider contract does not invalidate the guarantee the parent kept.
|
|
||||||
// Retain that guarantee so widening and narrowing back do not churn it.
|
|
||||||
if let Some(was) = was_answer
|
|
||||||
&& drawn.answer.size == was.size
|
|
||||||
&& drawn.answer.holds.covers(was.holds)
|
|
||||||
{
|
|
||||||
active.answer = was_answer;
|
|
||||||
}
|
|
||||||
// Against the box it was asked in, which is what both contracts are
|
// Against the box it was asked in, which is what both contracts are
|
||||||
// about. Where the answer put the drawing is shorter than that
|
// about. Where the answer put the drawing is shorter than that
|
||||||
// wherever the widget reported less than it was offered.
|
// wherever the widget reported less than it was offered.
|
||||||
if active.holds.covers(was_holds)
|
let (window, rel_base, region) = (self.output_size, active.rel_base, active.region);
|
||||||
&& was_holds.contains(self.output_size, active.rel_base, active.region)
|
// A wider contract does not invalidate the guarantee the parent kept.
|
||||||
|
// Retain that guarantee so widening and narrowing back do not churn
|
||||||
|
// it -- but only where the narrower range still holds here: one this
|
||||||
|
// window is outside is refused by the parent's next ask, and refusing
|
||||||
|
// it throws away the drawing this one just made.
|
||||||
|
if let Some(was) = was_answer
|
||||||
|
&& drawn.answer.size == was.size
|
||||||
|
&& drawn.answer.holds.covers(was.holds)
|
||||||
|
&& was.holds.contains(window, rel_base, region)
|
||||||
{
|
{
|
||||||
|
active.answer = was_answer;
|
||||||
|
}
|
||||||
|
if active.holds.covers(was_holds) && was_holds.contains(window, rel_base, region) {
|
||||||
active.holds = was_holds;
|
active.holds = was_holds;
|
||||||
}
|
}
|
||||||
if active.answer != was_answer || active.holds != was_holds {
|
let changed = active.answer != was_answer || active.holds != was_holds;
|
||||||
// The parent retains both the answer and the drawing's validity;
|
// Nothing above the root retained either, so there is nobody to tell
|
||||||
// even an unchanged size can narrow the range safe for a resize.
|
// and nowhere else the drawing has to go back to.
|
||||||
|
if let Some(parent) = active.parent {
|
||||||
|
match changed {
|
||||||
|
// The parent retains both the answer and the drawing's
|
||||||
|
// validity; even an unchanged size can narrow the range safe
|
||||||
|
// for a resize.
|
||||||
|
true => {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
{
|
{
|
||||||
diag::bump(Counter::SizeChanges);
|
diag::bump(Counter::SizeChanges);
|
||||||
diag::bump(Counter::ReaderEdges);
|
diag::bump(Counter::ReaderEdges);
|
||||||
}
|
}
|
||||||
self.mark(parent, rsc.widgets_mut());
|
self.mark(parent, rsc.widgets_mut());
|
||||||
} else {
|
}
|
||||||
// The answer stands, so where the parent put it stands: the
|
// The answer stands, so where the parent put it stands: the
|
||||||
// fresh drawing goes back there -- the same place, of the box
|
// fresh drawing goes back there -- the same place, of the box
|
||||||
// the parent's answer chose rather than the one it was asked in.
|
// the parent's answer chose rather than the one it was asked
|
||||||
|
// in.
|
||||||
|
false => {
|
||||||
let at = self.placing_of(parent, self.active[&parent].placement);
|
let at = self.placing_of(parent, self.active[&parent].placement);
|
||||||
self.place_in(id, &at, was_place, rsc);
|
self.place_in(id, &at, was_place, rsc);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1149,7 +1325,7 @@ impl UiRenderState {
|
|||||||
fn placing_of(&self, id: WidgetId, region: UiRegion) -> Placing {
|
fn placing_of(&self, id: WidgetId, region: UiRegion) -> Placing {
|
||||||
let active = &self.active[&id];
|
let active = &self.active[&id];
|
||||||
Placing {
|
Placing {
|
||||||
id,
|
id: Some(id),
|
||||||
region,
|
region,
|
||||||
rel_base: active.rel_base,
|
rel_base: active.rel_base,
|
||||||
depth: active.depth,
|
depth: active.depth,
|
||||||
@@ -1164,7 +1340,7 @@ impl Size {
|
|||||||
/// in. Both are lengths of the window, so the comparison is in its
|
/// in. Both are lengths of the window, so the comparison is in its
|
||||||
/// pixels. A share is a length only to whoever divides one, so it is not
|
/// pixels. A share is a length only to whoever divides one, so it is not
|
||||||
/// a claim about this box and cannot exceed it.
|
/// a claim about this box and cannot exceed it.
|
||||||
fn within_box(self, region: UiRegion, window: PxVec2, axis: Axis) -> bool {
|
fn within_box(&self, region: UiRegion, window: PxVec2, axis: Axis) -> bool {
|
||||||
let len = self[axis];
|
let len = self[axis];
|
||||||
let window = window[axis];
|
let window = window[axis];
|
||||||
len.leftover != Weight::ZERO
|
len.leftover != Weight::ZERO
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ impl<T, I: IdNum> Arena<T, I> {
|
|||||||
self.data[i]
|
self.data[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||||
&mut self.data[id.idx()]
|
&mut self.data[id.idx()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,29 +3,18 @@ use std::sync::{
|
|||||||
atomic::{AtomicU32, Ordering},
|
atomic::{AtomicU32, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
/// How many handles to one thing there are, less one: a fresh counter is a
|
||||||
|
/// single handle, which is what the derived default gives. Only
|
||||||
|
/// [`crate::TextureHandle`] has any, a texture slot being shared by every
|
||||||
|
/// widget drawing that picture.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
pub struct RefCounter(Arc<AtomicU32>);
|
pub struct RefCounter(Arc<AtomicU32>);
|
||||||
|
|
||||||
impl RefCounter {
|
impl RefCounter {
|
||||||
pub fn new() -> Self {
|
|
||||||
Self(Arc::new(0.into()))
|
|
||||||
}
|
|
||||||
pub fn refs(&self) -> u32 {
|
|
||||||
self.0.load(Ordering::Acquire)
|
|
||||||
}
|
|
||||||
pub fn drop(&mut self) -> bool {
|
pub fn drop(&mut self) -> bool {
|
||||||
let refs = self.0.fetch_sub(1, Ordering::Release);
|
let refs = self.0.fetch_sub(1, Ordering::Release);
|
||||||
refs == 0
|
refs == 0
|
||||||
}
|
}
|
||||||
pub fn quiet_clone(&self) -> Self {
|
|
||||||
Self(self.0.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RefCounter {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for RefCounter {
|
impl Clone for RefCounter {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use std::{marker::Unsize, ops::CoerceUnsized, sync::mpsc::Sender};
|
use std::{marker::Unsize, ops::CoerceUnsized, sync::mpsc::Sender};
|
||||||
|
|
||||||
use crate::{
|
use crate::{UiRsc, Widget, util::SlotId};
|
||||||
UiRsc, Widget,
|
|
||||||
util::{RefCounter, SlotId},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub type WidgetId = SlotId;
|
pub type WidgetId = SlotId;
|
||||||
|
|
||||||
@@ -11,10 +8,12 @@ pub type WidgetId = SlotId;
|
|||||||
/// This is a strong handle that does not impl Clone, and when it is dropped,
|
/// This is a strong handle that does not impl Clone, and when it is dropped,
|
||||||
/// a signal is sent to the owning UI to clean up the resources.
|
/// a signal is sent to the owning UI to clean up the resources.
|
||||||
///
|
///
|
||||||
|
/// There is nothing to count: not being `Clone` is what makes one handle the
|
||||||
|
/// only one, so the drop is the last one and the widget is one container's.
|
||||||
|
///
|
||||||
/// TODO: ergonomic clones when they get put in rust-analyzer & don't cause ICEs?
|
/// TODO: ergonomic clones when they get put in rust-analyzer & don't cause ICEs?
|
||||||
pub struct StrongWidget<W: ?Sized = dyn Widget> {
|
pub struct StrongWidget<W: ?Sized = dyn Widget> {
|
||||||
pub(super) id: WidgetId,
|
pub(super) id: WidgetId,
|
||||||
counter: RefCounter,
|
|
||||||
send: Sender<WidgetId>,
|
send: Sender<WidgetId>,
|
||||||
ty: *const W,
|
ty: *const W,
|
||||||
}
|
}
|
||||||
@@ -37,7 +36,6 @@ impl<W: ?Sized> StrongWidget<W> {
|
|||||||
pub(crate) fn new(id: WidgetId, send: Sender<WidgetId>) -> Self {
|
pub(crate) fn new(id: WidgetId, send: Sender<WidgetId>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
counter: RefCounter::new(),
|
|
||||||
send,
|
send,
|
||||||
ty: null_ptr(),
|
ty: null_ptr(),
|
||||||
}
|
}
|
||||||
@@ -47,10 +45,6 @@ impl<W: ?Sized> StrongWidget<W> {
|
|||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refs(&self) -> u32 {
|
|
||||||
self.counter.refs()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn weak(&self) -> WeakWidget<W> {
|
pub fn weak(&self) -> WeakWidget<W> {
|
||||||
let Self { ty, id, .. } = *self;
|
let Self { ty, id, .. } = *self;
|
||||||
WeakWidget { ty, id }
|
WeakWidget { ty, id }
|
||||||
@@ -74,11 +68,9 @@ impl<W: ?Sized> WeakWidget<W> {
|
|||||||
|
|
||||||
impl<W: ?Sized> Drop for StrongWidget<W> {
|
impl<W: ?Sized> Drop for StrongWidget<W> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if self.counter.drop() {
|
|
||||||
let _ = self.send.send(self.id);
|
let _ = self.send.send(self.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub trait WidgetIdFn<Rsc, W: ?Sized = dyn Widget>: FnOnce(&mut Rsc) -> WeakWidget<W> {}
|
pub trait WidgetIdFn<Rsc, W: ?Sized = dyn Widget>: FnOnce(&mut Rsc) -> WeakWidget<W> {}
|
||||||
impl<Rsc, W: ?Sized, F: FnOnce(&mut Rsc) -> WeakWidget<W>> WidgetIdFn<Rsc, W> for F {}
|
impl<Rsc, W: ?Sized, F: FnOnce(&mut Rsc) -> WeakWidget<W>> WidgetIdFn<Rsc, W> for F {}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::any::Any;
|
|||||||
mod data;
|
mod data;
|
||||||
mod handle;
|
mod handle;
|
||||||
mod like;
|
mod like;
|
||||||
|
mod request;
|
||||||
mod size_rule;
|
mod size_rule;
|
||||||
mod tag;
|
mod tag;
|
||||||
mod view;
|
mod view;
|
||||||
@@ -12,6 +13,7 @@ mod widgets;
|
|||||||
pub use data::*;
|
pub use data::*;
|
||||||
pub use handle::*;
|
pub use handle::*;
|
||||||
pub use like::*;
|
pub use like::*;
|
||||||
|
pub use request::*;
|
||||||
pub use size_rule::*;
|
pub use size_rule::*;
|
||||||
pub use tag::*;
|
pub use tag::*;
|
||||||
pub use view::*;
|
pub use view::*;
|
||||||
@@ -21,6 +23,12 @@ pub trait Widget: Any {
|
|||||||
/// Draws the widget, and returns what it used of the box it was given.
|
/// Draws the widget, and returns what it used of the box it was given.
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size;
|
fn draw(&mut self, painter: &mut Painter) -> Size;
|
||||||
|
|
||||||
|
/// Describes an axis before painting. Return `None` when discovering it
|
||||||
|
/// needs a concrete box or work performed by `draw`.
|
||||||
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
|
self.size_hint(axis).map(|len| requests.length(len))
|
||||||
|
}
|
||||||
|
|
||||||
/// An exact length the widget can give without a painter or its children.
|
/// An exact length the widget can give without a painter or its children.
|
||||||
/// Optional, and saves a draw rather than changing one: a hint that
|
/// Optional, and saves a draw rather than changing one: a hint that
|
||||||
/// disagrees with the eventual draw fails a debug assertion.
|
/// disagrees with the eventual draw fails a debug assertion.
|
||||||
|
|||||||
@@ -0,0 +1,609 @@
|
|||||||
|
use crate::{
|
||||||
|
ActiveData, Axis, Bound, LayoutLen, Len, Px, Rel, StrongWidget, UiNum, Weight, WidgetId,
|
||||||
|
Widgets, util::HashMap,
|
||||||
|
};
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
impl<N: UiNum> From<N> for SizeRequest {
|
||||||
|
fn from(value: N) -> Self {
|
||||||
|
LayoutLen::px(value).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutLen {
|
||||||
|
pub fn min(self, other: impl Into<SizeRequest>) -> SizeRequest {
|
||||||
|
SizeRequest::from(self).min(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max(self, other: impl Into<SizeRequest>) -> SizeRequest {
|
||||||
|
SizeRequest::from(self).max(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clamp(self, min: impl Into<SizeRequest>, max: impl Into<SizeRequest>) -> SizeRequest {
|
||||||
|
SizeRequest::from(self).clamp(min, max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
enum Op {
|
||||||
|
Sum,
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One operand of a [`Node`]: a length, or another node. Node numbers are an
|
||||||
|
/// arena's own, so an operand says nothing about which arena it came from --
|
||||||
|
/// [`RequestedLen`] is the form that does, and the only one that leaves one.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
enum Operand {
|
||||||
|
Linear(LayoutLen),
|
||||||
|
Node(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operand {
|
||||||
|
/// The length itself, where no comparison is waiting on an allocation.
|
||||||
|
fn linear(&self) -> Option<LayoutLen> {
|
||||||
|
match *self {
|
||||||
|
Self::Linear(len) => Some(len),
|
||||||
|
Self::Node(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One sum or comparison, with its two operands. Whether anything under it
|
||||||
|
/// divides leftover space is carried on the node rather than walked for,
|
||||||
|
/// because every caller of one asks.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
struct Node {
|
||||||
|
op: Op,
|
||||||
|
a: Operand,
|
||||||
|
b: Operand,
|
||||||
|
leftover: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The nodes of one expression, numbered from zero, and the folding that
|
||||||
|
/// happens as each is added. There are two kinds of owner and one kind of
|
||||||
|
/// arena: a rule's [`SizeRequest`] holds a small one for as long as the rule
|
||||||
|
/// lasts, and [`RequestArena`] holds the layout pass's.
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
struct Nodes(Vec<Node>);
|
||||||
|
|
||||||
|
impl Nodes {
|
||||||
|
fn node(&self, index: u32) -> Node {
|
||||||
|
self.0[index as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leftover(&self, at: Operand) -> bool {
|
||||||
|
match at {
|
||||||
|
Operand::Linear(len) => len.leftover > Weight::ZERO,
|
||||||
|
Operand::Node(index) => self.node(index).leftover,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `a op b`, which is a node only where the answer needs one. Two
|
||||||
|
/// lengths that keep their order whatever the room comes to are already
|
||||||
|
/// decided, and so are two operands that are the same thing.
|
||||||
|
fn combine(&mut self, op: Op, a: Operand, b: Operand) -> Operand {
|
||||||
|
if let (Some(x), Some(y)) = (a.linear(), b.linear()) {
|
||||||
|
if matches!(op, Op::Sum) {
|
||||||
|
return Operand::Linear(x + y);
|
||||||
|
}
|
||||||
|
if let Some(order) = independent_order(x, y) {
|
||||||
|
let take_a = match op {
|
||||||
|
Op::Min => !order.is_gt(),
|
||||||
|
_ => !order.is_lt(),
|
||||||
|
};
|
||||||
|
return if take_a { a } else { b };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a == b && !matches!(op, Op::Sum) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
let index = u32::try_from(self.0.len()).expect("more nodes than one arena can number");
|
||||||
|
let leftover = self.leftover(a) || self.leftover(b);
|
||||||
|
self.0.push(Node { op, a, b, leftover });
|
||||||
|
Operand::Node(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies `at` and everything under it out of `from`, with every length
|
||||||
|
/// it holds passed through `resolve`. Folded again on the way in, since
|
||||||
|
/// resolving a fraction can settle a comparison that was open before it.
|
||||||
|
fn graft(
|
||||||
|
&mut self,
|
||||||
|
from: &Self,
|
||||||
|
at: Operand,
|
||||||
|
resolve: impl Copy + Fn(LayoutLen) -> LayoutLen,
|
||||||
|
) -> Operand {
|
||||||
|
let index = match at {
|
||||||
|
Operand::Linear(len) => return Operand::Linear(resolve(len)),
|
||||||
|
Operand::Node(index) => index,
|
||||||
|
};
|
||||||
|
let Node { op, a, b, .. } = from.node(index);
|
||||||
|
let a = self.graft(from, a, resolve);
|
||||||
|
let b = self.graft(from, b, resolve);
|
||||||
|
self.combine(op, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&self, f: &mut std::fmt::Formatter<'_>, at: Operand) -> std::fmt::Result {
|
||||||
|
let index = match at {
|
||||||
|
Operand::Linear(len) => return write!(f, "{len}"),
|
||||||
|
Operand::Node(index) => index,
|
||||||
|
};
|
||||||
|
let Node { op, a, b, .. } = self.node(index);
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}(",
|
||||||
|
match op {
|
||||||
|
Op::Sum => "sum",
|
||||||
|
Op::Min => "min",
|
||||||
|
Op::Max => "max",
|
||||||
|
}
|
||||||
|
)?;
|
||||||
|
self.write(f, a)?;
|
||||||
|
write!(f, ", ")?;
|
||||||
|
self.write(f, b)?;
|
||||||
|
write!(f, ")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A size request before a container has divided its leftover space.
|
||||||
|
/// Comparisons keep both operands until the share is known.
|
||||||
|
///
|
||||||
|
/// An expression is the same nodes the layout pass allocates, in an arena of
|
||||||
|
/// its own: importing one copies those nodes into the pass's arena, so there
|
||||||
|
/// is no second shape to keep in step and one place where folding is decided.
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum SizeRequest {
|
||||||
|
Linear(LayoutLen),
|
||||||
|
/// Behind a pointer, because a plain length is what nearly every rule
|
||||||
|
/// holds and an expression should cost those rules nothing.
|
||||||
|
Expr(Box<Expr>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An expression's own arena, and which of its nodes is the whole of it.
|
||||||
|
/// `root` is a node number rather than an operand, so an expression that
|
||||||
|
/// folded all the way down to a length cannot be written as one: that is a
|
||||||
|
/// [`SizeRequest::Linear`].
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Expr {
|
||||||
|
nodes: Nodes,
|
||||||
|
root: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeRequest {
|
||||||
|
pub fn min(self, other: impl Into<Self>) -> Self {
|
||||||
|
self.join(Op::Min, other.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max(self, other: impl Into<Self>) -> Self {
|
||||||
|
self.join(Op::Max, other.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clamp(self, min: impl Into<Self>, max: impl Into<Self>) -> Self {
|
||||||
|
self.max(min).min(max)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `self op other`, keeping `self`'s arena and copying `other`'s into
|
||||||
|
/// it, so the two sets of node numbers become one. Two requests that are
|
||||||
|
/// the same request compare to the same thing whatever the room is, which
|
||||||
|
/// is a comparison worth not building -- but adding something to itself
|
||||||
|
/// is twice it.
|
||||||
|
fn join(self, op: Op, other: Self) -> Self {
|
||||||
|
if self == other && !matches!(op, Op::Sum) {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
let (mut nodes, a) = match self {
|
||||||
|
Self::Linear(len) => (Nodes::default(), Operand::Linear(len)),
|
||||||
|
Self::Expr(expr) => {
|
||||||
|
let Expr { nodes, root } = *expr;
|
||||||
|
(nodes, Operand::Node(root))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let b = match other {
|
||||||
|
Self::Linear(len) => Operand::Linear(len),
|
||||||
|
Self::Expr(expr) => nodes.graft(&expr.nodes, Operand::Node(expr.root), |len| len),
|
||||||
|
};
|
||||||
|
match nodes.combine(op, a, b) {
|
||||||
|
Operand::Linear(len) => Self::Linear(len),
|
||||||
|
Operand::Node(root) => Self::Expr(Box::new(Expr { nodes, root })),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LayoutLen> for SizeRequest {
|
||||||
|
fn from(len: LayoutLen) -> Self {
|
||||||
|
Self::Linear(len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Len> for SizeRequest {
|
||||||
|
fn from(len: Len) -> Self {
|
||||||
|
LayoutLen::from(len).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add for SizeRequest {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, other: Self) -> Self {
|
||||||
|
self.join(Op::Sum, other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SizeRequest {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Linear(len) => write!(f, "{len}"),
|
||||||
|
Self::Expr(expr) => expr.nodes.write(f, Operand::Node(expr.root)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same, since an arena printed as a struct is not a tree anyone can
|
||||||
|
/// write out again, which is what a request is printed for.
|
||||||
|
impl std::fmt::Debug for SizeRequest {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
std::fmt::Display::fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A discovered length. Deferred values belong to the current layout pass;
|
||||||
|
/// widgets must not retain them. Ordinary requests remain inline lengths.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct RequestedLen(RequestValue);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
enum RequestValue {
|
||||||
|
Linear(LayoutLen),
|
||||||
|
Deferred {
|
||||||
|
index: u32,
|
||||||
|
epoch: u64,
|
||||||
|
leftover: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LayoutLen> for RequestedLen {
|
||||||
|
fn from(len: LayoutLen) -> Self {
|
||||||
|
Self(RequestValue::Linear(len))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Len> for RequestedLen {
|
||||||
|
fn from(len: Len) -> Self {
|
||||||
|
LayoutLen::from(len).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl RequestedLen {
|
||||||
|
/// The length itself, where no comparison is waiting on an allocation.
|
||||||
|
pub fn linear(&self) -> Option<LayoutLen> {
|
||||||
|
match self.0 {
|
||||||
|
RequestValue::Linear(len) => Some(len),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn has_leftover(&self) -> bool {
|
||||||
|
match self.0 {
|
||||||
|
RequestValue::Linear(len) => len.leftover > Weight::ZERO,
|
||||||
|
RequestValue::Deferred { leftover, .. } => leftover,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct RequestArena {
|
||||||
|
nodes: Nodes,
|
||||||
|
epoch: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestArena {
|
||||||
|
pub(crate) fn reset(&mut self) {
|
||||||
|
self.nodes.0.clear();
|
||||||
|
self.epoch = self
|
||||||
|
.epoch
|
||||||
|
.checked_add(1)
|
||||||
|
.expect("layout generation exhausted");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An operand of this pass's arena as the handle that leaves it. The
|
||||||
|
/// epoch says which pass numbered the node, so a handle a widget kept
|
||||||
|
/// past its pass is caught rather than answering about whatever node
|
||||||
|
/// took its place.
|
||||||
|
fn handle(&self, at: Operand) -> RequestedLen {
|
||||||
|
RequestedLen(match at {
|
||||||
|
Operand::Linear(len) => RequestValue::Linear(len),
|
||||||
|
Operand::Node(index) => RequestValue::Deferred {
|
||||||
|
index,
|
||||||
|
epoch: self.epoch,
|
||||||
|
leftover: self.nodes.leftover(at),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The other direction, checked once where a handle comes back in rather
|
||||||
|
/// than again at every level of the walk it starts.
|
||||||
|
fn operand(&self, request: RequestedLen) -> Operand {
|
||||||
|
match request.0 {
|
||||||
|
RequestValue::Linear(len) => Operand::Linear(len),
|
||||||
|
RequestValue::Deferred { index, epoch, .. } => {
|
||||||
|
assert_eq!(epoch, self.epoch, "request retained beyond its layout pass");
|
||||||
|
Operand::Node(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rule's expression in this pass's arena, with its fractions resolved
|
||||||
|
/// against the rel base the widget is being asked with.
|
||||||
|
pub(crate) fn import(&mut self, request: &SizeRequest, base: Len) -> RequestedLen {
|
||||||
|
let at = match request {
|
||||||
|
SizeRequest::Linear(len) => Operand::Linear(len.within_len(base)),
|
||||||
|
SizeRequest::Expr(expr) => {
|
||||||
|
self.nodes
|
||||||
|
.graft(&expr.nodes, Operand::Node(expr.root), |len| {
|
||||||
|
len.within_len(base)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.handle(at)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen {
|
||||||
|
let request = match bound.min {
|
||||||
|
Some(min) => self.combine(Op::Max, request, min.into()),
|
||||||
|
None => request,
|
||||||
|
};
|
||||||
|
match bound.max {
|
||||||
|
Some(max) => self.combine(Op::Min, request, max.into()),
|
||||||
|
None => request,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn combine(&mut self, op: Op, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||||
|
let (a, b) = (self.operand(a), self.operand(b));
|
||||||
|
let at = self.nodes.combine(op, a, b);
|
||||||
|
self.handle(at)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn minimum(&self, request: RequestedLen, window: Px) -> Px {
|
||||||
|
let at = self.operand(request);
|
||||||
|
Px::from_raw(self.segment(at, Ratio::ZERO, window).fixed as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segment(&self, of: Operand, at: Ratio, window: Px) -> Segment {
|
||||||
|
let index = match of {
|
||||||
|
Operand::Linear(len) => {
|
||||||
|
debug_assert!(
|
||||||
|
len.leftover >= Weight::ZERO,
|
||||||
|
"a leftover weight cannot be negative"
|
||||||
|
);
|
||||||
|
return Segment {
|
||||||
|
fixed: i64::from(len.without_leftover().to_px(window).raw()),
|
||||||
|
weight: i64::from(len.leftover.raw()),
|
||||||
|
end: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Operand::Node(index) => index,
|
||||||
|
};
|
||||||
|
let Node { op, a, b, .. } = self.nodes.node(index);
|
||||||
|
let a = self.segment(a, at, window);
|
||||||
|
let b = self.segment(b, at, window);
|
||||||
|
if matches!(op, Op::Sum) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
// At a crossing choose the branch to its right, so the next
|
||||||
|
// iteration advances rather than selecting that crossing again.
|
||||||
|
let order = a.value(at).cmp(&b.value(at)).then(a.weight.cmp(&b.weight));
|
||||||
|
let take_a = match op {
|
||||||
|
Op::Min => !order.is_gt(),
|
||||||
|
_ => !order.is_lt(),
|
||||||
|
};
|
||||||
|
let mut selected = if take_a { a } else { b };
|
||||||
|
selected.end = first(a.end, b.end);
|
||||||
|
if a.weight != b.weight {
|
||||||
|
let crossing = Ratio::new(b.fixed - a.fixed, a.weight - b.weight);
|
||||||
|
if crossing > at {
|
||||||
|
selected.end = first(selected.end, Some(crossing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selected
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Divides `room` between requests whose weights are nonnegative, one
|
||||||
|
/// length per request. A floor can overflow the room and a cap can leave
|
||||||
|
/// part of it unused, so the lengths need not come to `room`. Each edge
|
||||||
|
/// is rounded from the running total rather than from the length before
|
||||||
|
/// it, so two neighbouring slots meet exactly.
|
||||||
|
pub(crate) fn allocate<'a>(
|
||||||
|
&'a self,
|
||||||
|
requests: &'a [RequestedLen],
|
||||||
|
room: Px,
|
||||||
|
window: Px,
|
||||||
|
) -> impl Iterator<Item = Px> + 'a {
|
||||||
|
let mut at = Ratio::ZERO;
|
||||||
|
loop {
|
||||||
|
let total = requests.iter().fold(Segment::ZERO, |total, request| {
|
||||||
|
total + self.segment(self.operand(*request), at, window)
|
||||||
|
});
|
||||||
|
if total.value(at) >= i128::from(room.raw()) * i128::from(at.den) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if total.weight != 0 {
|
||||||
|
let solution = Ratio::new(i64::from(room.raw()) - total.fixed, total.weight);
|
||||||
|
if total.end.is_none_or(|end| solution <= end) {
|
||||||
|
at = solution;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match total.end {
|
||||||
|
Some(end) => at = end,
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut prefix = 0_i128;
|
||||||
|
let mut previous = 0_i128;
|
||||||
|
requests.iter().map(move |request| {
|
||||||
|
prefix += self.segment(self.operand(*request), at, window).value(at);
|
||||||
|
let den = i128::from(at.den);
|
||||||
|
// Half away from zero, which is what `Fixed` rounds a division
|
||||||
|
// to: the two decide the same edge, and a change to one of them
|
||||||
|
// is a change to the other.
|
||||||
|
let edge = prefix.signum() * ((prefix.abs() + den / 2) / den);
|
||||||
|
let len = Px::from_raw((edge - previous) as i32);
|
||||||
|
previous = edge;
|
||||||
|
len
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq)]
|
||||||
|
struct Ratio {
|
||||||
|
num: i64,
|
||||||
|
den: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Ratio {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.cmp(other).is_eq()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ratio {
|
||||||
|
const ZERO: Self = Self { num: 0, den: 1 };
|
||||||
|
|
||||||
|
fn new(num: i64, den: i64) -> Self {
|
||||||
|
debug_assert_ne!(den, 0, "a ratio of nothing");
|
||||||
|
if den < 0 {
|
||||||
|
Self {
|
||||||
|
num: -num,
|
||||||
|
den: -den,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self { num, den }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Ratio {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
(i128::from(self.num) * i128::from(other.den))
|
||||||
|
.cmp(&(i128::from(other.num) * i128::from(self.den)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Ratio {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Segment {
|
||||||
|
fixed: i64,
|
||||||
|
weight: i64,
|
||||||
|
end: Option<Ratio>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Segment {
|
||||||
|
const ZERO: Self = Self {
|
||||||
|
fixed: 0,
|
||||||
|
weight: 0,
|
||||||
|
end: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn value(self, at: Ratio) -> i128 {
|
||||||
|
i128::from(self.fixed) * i128::from(at.den) + i128::from(self.weight) * i128::from(at.num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add for Segment {
|
||||||
|
type Output = Self;
|
||||||
|
fn add(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
fixed: self.fixed + other.fixed,
|
||||||
|
weight: self.weight + other.weight,
|
||||||
|
end: first(self.end, other.end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first(a: Option<Ratio>, b: Option<Ratio>) -> Option<Ratio> {
|
||||||
|
match (a, b) {
|
||||||
|
(Some(a), Some(b)) => Some(a.min(b)),
|
||||||
|
(a, b) => a.or(b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only discovery of requests through a widget's children. A request is
|
||||||
|
/// expressed in window lengths; `rel_base` supplies the base for declarations.
|
||||||
|
pub struct SizeRequests<'a> {
|
||||||
|
pub(crate) arena: &'a mut RequestArena,
|
||||||
|
pub(crate) measured: Option<&'a HashMap<WidgetId, ActiveData>>,
|
||||||
|
pub(crate) widgets: &'a Widgets,
|
||||||
|
pub(crate) dependencies: &'a mut Vec<WidgetId>,
|
||||||
|
pub(crate) rel_base: Len,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeRequests<'_> {
|
||||||
|
pub fn sum(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||||
|
self.arena.combine(Op::Sum, a, b)
|
||||||
|
}
|
||||||
|
pub fn min(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||||
|
self.arena.combine(Op::Min, a, b)
|
||||||
|
}
|
||||||
|
pub fn max(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
|
||||||
|
self.arena.combine(Op::Max, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn widget<W: ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
child: &StrongWidget<W>,
|
||||||
|
axis: Axis,
|
||||||
|
) -> Option<RequestedLen> {
|
||||||
|
self.dependencies.push(child.id());
|
||||||
|
let rules = self.widgets.size_rules(child.id());
|
||||||
|
let rule = &rules[axis];
|
||||||
|
let request = match &rule.request {
|
||||||
|
Some(request) => self.arena.import(request, self.rel_base),
|
||||||
|
None => {
|
||||||
|
let widget = self.widgets.get_dyn(child.id())?;
|
||||||
|
widget.size_request(self, axis).or_else(|| {
|
||||||
|
self.measured?
|
||||||
|
.get(&child.id())?
|
||||||
|
.measured()
|
||||||
|
.map(|size| size[axis].into())
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(self.bounded(request, rule.bound))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen {
|
||||||
|
self.arena.bounded(request, bound.within_len(self.rel_base))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn length(&self, len: LayoutLen) -> RequestedLen {
|
||||||
|
len.within_len(self.rel_base).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inset<W: ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
child: &StrongWidget<W>,
|
||||||
|
axis: Axis,
|
||||||
|
padding: Px,
|
||||||
|
) -> Option<RequestedLen> {
|
||||||
|
let base = self.rel_base;
|
||||||
|
self.rel_base.px -= padding;
|
||||||
|
let request = self.widget(child, axis);
|
||||||
|
self.rel_base = base;
|
||||||
|
request.map(|request| self.sum(request, Len::from_parts(Rel::ZERO, padding).into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal fractions keep this valid even when padding makes a rel base negative.
|
||||||
|
fn independent_order(a: LayoutLen, b: LayoutLen) -> Option<Ordering> {
|
||||||
|
if a.rel == b.rel && a.leftover == b.leftover {
|
||||||
|
Some(a.px.cmp(&b.px))
|
||||||
|
} else if a.px == b.px && a.rel == b.rel {
|
||||||
|
Some(a.leftover.cmp(&b.leftover))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
+131
-44
@@ -1,58 +1,154 @@
|
|||||||
use crate::util::impl_axis_index;
|
use crate::util::impl_axis_index;
|
||||||
use crate::{Axis, LayoutLen, Len};
|
use crate::{LayoutLen, Len, Rel, SizeRequest, Weight};
|
||||||
|
|
||||||
/// What a widget's length on one axis is, as a rule its parent applies where
|
/// A preferred length and independent bounds on one axis. Without a
|
||||||
/// it draws it rather than an answer the widget gives about itself.
|
/// request, the widget's drawing supplies the preferred length.
|
||||||
///
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
/// A rule and a drawn size are not two opinions to reconcile: a rule wins on
|
pub struct SizeRule {
|
||||||
/// the axis it names, and the `Size` returned by `draw` answers only the axes
|
pub request: Option<SizeRequest>,
|
||||||
/// with no rule. That is what lets a span divide its space around a length
|
pub bound: Bound,
|
||||||
/// nobody has drawn yet, and it is why a rule lives beside the widget rather
|
|
||||||
/// than inside it -- the widget under the rule never has to know about it.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
|
||||||
pub enum SizeRule {
|
|
||||||
/// Whatever the widget reports from drawing.
|
|
||||||
#[default]
|
|
||||||
Free,
|
|
||||||
/// This length, whatever the widget reports.
|
|
||||||
Exact(LayoutLen),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SizeRule {
|
impl SizeRule {
|
||||||
/// The length this rule gives without the widget being drawn, if it can
|
pub const FREE: Self = Self {
|
||||||
/// give one.
|
request: None,
|
||||||
pub fn declared(&self) -> Option<Len> {
|
bound: Bound::ANY,
|
||||||
self.exact().and_then(LayoutLen::declared)
|
};
|
||||||
|
|
||||||
|
pub fn min(min: Len) -> Self {
|
||||||
|
Self::bounded(Bound {
|
||||||
|
min: Some(min),
|
||||||
|
max: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The length this rule gives outright, whatever the widget reports --
|
pub fn max(max: Len) -> Self {
|
||||||
/// which makes the widget's answer on that axis moot. A share counts: it
|
Self::bounded(Bound {
|
||||||
/// is a length the widget's parent still has to divide, so it is exact
|
min: None,
|
||||||
/// here and resolved there, unlike `declared`, which is only the ones
|
max: Some(max),
|
||||||
/// that give a box directly.
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clamp(min: Len, max: Len) -> Self {
|
||||||
|
Self::bounded(Bound {
|
||||||
|
min: Some(min),
|
||||||
|
max: Some(max),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bound and no preferred length, so whatever the widget draws is held
|
||||||
|
/// to it.
|
||||||
|
fn bounded(bound: Bound) -> Self {
|
||||||
|
Self {
|
||||||
|
request: None,
|
||||||
|
bound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_fraction(&self) -> bool {
|
||||||
|
self.exact().is_some_and(|len| len.rel != Rel::ZERO) || self.bound.has_fraction()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A linear preferred length, before applying the independent bounds.
|
||||||
pub fn exact(&self) -> Option<LayoutLen> {
|
pub fn exact(&self) -> Option<LayoutLen> {
|
||||||
match self {
|
match self.request {
|
||||||
Self::Free => None,
|
Some(SizeRequest::Linear(len)) => Some(len),
|
||||||
Self::Exact(len) => Some(*len),
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requests whose final length needs allocation, including a linear
|
||||||
|
/// share constrained by an independent bound.
|
||||||
|
pub(crate) fn deferred(&self) -> Option<&SizeRequest> {
|
||||||
|
let request = self.request.as_ref()?;
|
||||||
|
match request {
|
||||||
|
SizeRequest::Linear(len)
|
||||||
|
if len.leftover == Weight::ZERO || self.bound == Bound::ANY =>
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => Some(request),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A floor, a cap, or both, independent of the preferred length.
|
||||||
|
/// Fractions use the incoming rel base. An allocated slot keeps the
|
||||||
|
/// allocator's base for bounds, even when the slot narrows the widget's own.
|
||||||
|
///
|
||||||
|
/// A bound is a [`Len`]. Comparisons involving shares are [`SizeRequest`]s.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct Bound {
|
||||||
|
pub min: Option<Len>,
|
||||||
|
pub max: Option<Len>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bound {
|
||||||
|
/// Every length.
|
||||||
|
pub const ANY: Self = Self {
|
||||||
|
min: None,
|
||||||
|
max: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Whether either end is a fraction of the rel base, so that the same
|
||||||
|
/// bound against a different one binds at a different length.
|
||||||
|
pub fn has_fraction(&self) -> bool {
|
||||||
|
[self.min, self.max]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.any(|len| len.rel != Rel::ZERO)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This bound as lengths of the window, from lengths of a rel base that
|
||||||
|
/// long.
|
||||||
|
pub fn within_len(&self, len: Len) -> Self {
|
||||||
|
Self {
|
||||||
|
min: self.min.map(|min| min.within_len(len)),
|
||||||
|
max: self.max.map(|max| max.within_len(len)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One bound per axis, as [`SizeRules`] is one rule per axis.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct Bounds {
|
||||||
|
pub x: Bound,
|
||||||
|
pub y: Bound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bounds {
|
||||||
|
pub const ANY: Self = Self {
|
||||||
|
x: Bound::ANY,
|
||||||
|
y: Bound::ANY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_axis_index!(Bounds => Bound);
|
||||||
|
|
||||||
impl From<LayoutLen> for SizeRule {
|
impl From<LayoutLen> for SizeRule {
|
||||||
fn from(len: LayoutLen) -> Self {
|
fn from(len: LayoutLen) -> Self {
|
||||||
Self::Exact(len)
|
SizeRequest::from(len).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SizeRequest> for SizeRule {
|
||||||
|
fn from(request: SizeRequest) -> Self {
|
||||||
|
Self {
|
||||||
|
request: Some(request),
|
||||||
|
bound: Bound::ANY,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Option<LayoutLen>> for SizeRule {
|
impl From<Option<LayoutLen>> for SizeRule {
|
||||||
fn from(len: Option<LayoutLen>) -> Self {
|
fn from(len: Option<LayoutLen>) -> Self {
|
||||||
len.map_or(Self::Free, Self::Exact)
|
len.map_or(Self::FREE, Self::from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One rule per axis, which is how a widget carries a length on one axis and
|
/// One rule per axis, which is how a widget carries a length on one axis and
|
||||||
/// leaves the other to whatever it draws.
|
/// leaves the other to whatever it draws.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
pub struct SizeRules {
|
pub struct SizeRules {
|
||||||
pub x: SizeRule,
|
pub x: SizeRule,
|
||||||
pub y: SizeRule,
|
pub y: SizeRule,
|
||||||
@@ -60,12 +156,10 @@ pub struct SizeRules {
|
|||||||
|
|
||||||
impl_axis_index!(SizeRules => SizeRule);
|
impl_axis_index!(SizeRules => SizeRule);
|
||||||
|
|
||||||
/// What a widget's box is on each axis where something says so outright,
|
/// Box lengths chosen by the ask: a declaration, a natural-size hint, or
|
||||||
/// before it is drawn: a rule beside it, or a hint it gives about itself.
|
/// a binding bound. These are window lengths, already resolved against the
|
||||||
/// Whoever draws the widget resolves these against its rel base.
|
/// incoming rel base. Moving a drawing preserves them instead of resolving
|
||||||
///
|
/// a fraction against its destination a second time.
|
||||||
/// A [`Len`] rather than a [`LayoutLen`], because a share can never be one
|
|
||||||
/// -- see [`LayoutLen::declared`].
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Declared {
|
pub struct Declared {
|
||||||
pub x: Option<Len>,
|
pub x: Option<Len>,
|
||||||
@@ -74,13 +168,6 @@ pub struct Declared {
|
|||||||
|
|
||||||
impl Declared {
|
impl Declared {
|
||||||
pub const NONE: Self = Self { x: None, y: None };
|
pub const NONE: Self = Self { x: None, y: None };
|
||||||
|
|
||||||
pub fn from_axes(f: impl Fn(Axis) -> Option<Len>) -> Self {
|
|
||||||
Self {
|
|
||||||
x: f(Axis::X),
|
|
||||||
y: f(Axis::Y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_axis_index!(Declared => Option<Len>);
|
impl_axis_index!(Declared => Option<Len>);
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Axis, AxisAlign, IdLike, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget,
|
Axis, AxisAlign, Bound, IdLike, Len, RegionAlign, SizeRequest, SizeRule, SizeRules,
|
||||||
WidgetData, WidgetId,
|
StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
|
||||||
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,6 +30,14 @@ impl Widgets {
|
|||||||
!self.needs_redraw.is_empty()
|
!self.needs_redraw.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Marks this widget for the next frame to draw again, with nothing about
|
||||||
|
/// it changed. Taking a widget mutably marks it too, which is the ordinary
|
||||||
|
/// content-change signal; this is for a change the borrow cannot express,
|
||||||
|
/// and for asking for the same tree over again.
|
||||||
|
pub fn mark_for_redraw(&mut self, id: impl IdLike) {
|
||||||
|
self.needs_redraw.insert(id.id());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
|
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
|
||||||
Some(self.vec.get(id)?.widget.as_ref())
|
Some(self.vec.get(id)?.widget.as_ref())
|
||||||
}
|
}
|
||||||
@@ -41,14 +49,14 @@ impl Widgets {
|
|||||||
|
|
||||||
/// get_dyn but dynamic borrow checking of widgets
|
/// get_dyn but dynamic borrow checking of widgets
|
||||||
/// lets you do recursive (tree) operations, like the painter does
|
/// lets you do recursive (tree) operations, like the painter does
|
||||||
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
|
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> DynBorrower<'a, dyn Widget> {
|
||||||
// SAFETY: must guarantee no other mutable references to this widget exist
|
// SAFETY: must guarantee no other mutable references to this widget exist
|
||||||
// done through the borrow variable
|
// done through the borrow variable
|
||||||
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
|
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
|
||||||
if data.borrowed {
|
if data.borrowed {
|
||||||
panic!("tried to mutably borrow the same widget twice");
|
panic!("tried to mutably borrow the same widget twice");
|
||||||
}
|
}
|
||||||
WidgetWrapper::new(data.widget.as_mut(), &mut data.borrowed)
|
DynBorrower::new(data.widget.as_mut(), &mut data.borrowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
|
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
|
||||||
@@ -120,8 +128,8 @@ impl Widgets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The length rules whoever draws this widget applies to its box.
|
/// The length rules whoever draws this widget applies to its box.
|
||||||
pub fn size_rules(&self, id: impl IdLike) -> SizeRules {
|
pub fn size_rules(&self, id: impl IdLike) -> &SizeRules {
|
||||||
self.data(id).unwrap().size
|
&self.data(id).unwrap().size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets one axis's rule. The widget is marked rather than its parent
|
/// Sets one axis's rule. The widget is marked rather than its parent
|
||||||
@@ -137,6 +145,42 @@ impl Widgets {
|
|||||||
self.needs_redraw.insert(id);
|
self.needs_redraw.insert(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Changes the preferred length, leaving the bounds beside it alone.
|
||||||
|
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<SizeRequest>) {
|
||||||
|
let id = id.id();
|
||||||
|
let request = Some(len.into());
|
||||||
|
let rule = &mut self.data_mut(id).unwrap().size[axis];
|
||||||
|
if rule.request == request {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rule.request = request;
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts a floor under this widget's length on one axis, keeping a cap it
|
||||||
|
/// already had and the preferred length beside it.
|
||||||
|
pub fn set_min_len(&mut self, id: impl IdLike, axis: Axis, min: Len) {
|
||||||
|
self.edit_bound(id.id(), axis, |bound| bound.min = Some(min));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts a cap over it, keeping a floor it already had.
|
||||||
|
pub fn set_max_len(&mut self, id: impl IdLike, axis: Axis, max: Len) {
|
||||||
|
self.edit_bound(id.id(), axis, |bound| bound.max = Some(max));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Edits one axis's bound where it sits, rather than reading the whole
|
||||||
|
/// rule out and writing it back: an expression beside the bound is not
|
||||||
|
/// this edit's business, and copying it to move one end would be the
|
||||||
|
/// only thing here that ever copies one.
|
||||||
|
fn edit_bound(&mut self, id: WidgetId, axis: Axis, edit: impl FnOnce(&mut Bound)) {
|
||||||
|
let bound = &mut self.data_mut(id).unwrap().size[axis].bound;
|
||||||
|
let before = *bound;
|
||||||
|
edit(bound);
|
||||||
|
if *bound != before {
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Where this widget sits in a box longer than the length it takes.
|
/// Where this widget sits in a box longer than the length it takes.
|
||||||
pub fn alignment(&self, id: impl IdLike) -> RegionAlign {
|
pub fn alignment(&self, id: impl IdLike) -> RegionAlign {
|
||||||
self.data(id).unwrap().align
|
self.data(id).unwrap().align
|
||||||
@@ -154,7 +198,7 @@ impl Widgets {
|
|||||||
self.needs_redraw.insert(id);
|
self.needs_redraw.insert(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Both axes at once, for a caller holding a pair.
|
/// Both axes at once.
|
||||||
pub fn set_size_rules(
|
pub fn set_size_rules(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: impl IdLike,
|
id: impl IdLike,
|
||||||
@@ -188,8 +232,6 @@ impl Default for Widgets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>;
|
|
||||||
|
|
||||||
impl<I: IdLike> std::ops::Index<I> for Widgets
|
impl<I: IdLike> std::ops::Index<I> for Widgets
|
||||||
where
|
where
|
||||||
I::Widget: Sized + Widget,
|
I::Widget: Sized + Widget,
|
||||||
|
|||||||
+10
-3
@@ -18,6 +18,7 @@ struct Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct InputFn {
|
struct InputFn {
|
||||||
|
attrs: Vec<Attribute>,
|
||||||
sig: Signature,
|
sig: Signature,
|
||||||
body: Block,
|
body: Block,
|
||||||
}
|
}
|
||||||
@@ -32,9 +33,10 @@ impl Parse for Input {
|
|||||||
input.parse::<Token![;]>()?;
|
input.parse::<Token![;]>()?;
|
||||||
let mut fns = Vec::new();
|
let mut fns = Vec::new();
|
||||||
while !input.is_empty() {
|
while !input.is_empty() {
|
||||||
|
let attrs = input.call(Attribute::parse_outer)?;
|
||||||
let sig = input.parse()?;
|
let sig = input.parse()?;
|
||||||
let body = input.parse()?;
|
let body = input.parse()?;
|
||||||
fns.push(InputFn { sig, body })
|
fns.push(InputFn { attrs, sig, body })
|
||||||
}
|
}
|
||||||
if !input.is_empty() {
|
if !input.is_empty() {
|
||||||
input.error("function expected");
|
input.error("function expected");
|
||||||
@@ -59,10 +61,15 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
|
|||||||
fns,
|
fns,
|
||||||
} = parse_macro_input!(input as Input);
|
} = parse_macro_input!(input as Input);
|
||||||
|
|
||||||
let sigs: Vec<_> = fns.iter().map(|f| f.sig.clone()).collect();
|
// What a method says about itself belongs on the trait, where a reader
|
||||||
|
// looks it up; the implementation is the same text and says it again.
|
||||||
|
let sigs: Vec<_> = fns
|
||||||
|
.iter()
|
||||||
|
.map(|InputFn { attrs, sig, .. }| quote! { #(#attrs)* #sig })
|
||||||
|
.collect();
|
||||||
let impls: Vec<_> = fns
|
let impls: Vec<_> = fns
|
||||||
.iter()
|
.iter()
|
||||||
.map(|InputFn { sig, body }| quote! { #sig #body })
|
.map(|InputFn { attrs, sig, body }| quote! { #(#attrs)* #sig #body })
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let Some(GenericParam::Type(state)) = generics.params.first() else {
|
let Some(GenericParam::Type(state)) = generics.params.first() else {
|
||||||
|
|||||||
+11
-6
@@ -106,11 +106,16 @@ export WAYLAND_DISPLAY
|
|||||||
|
|
||||||
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
|
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
|
||||||
|
|
||||||
swaymsg output HEADLESS-1 mode "$mode" >/dev/null
|
# The extent `replay-touch` positions against, so a script's coordinates are
|
||||||
# The extent `replay-touch` positions against, so a script's coordinates
|
# the output's own pixels. Set beside every mode change, since a gesture
|
||||||
# are the output's own pixels.
|
# scaled against a mode the output no longer has lands somewhere else and
|
||||||
out_w=${mode%x*}
|
# still looks like a run that worked.
|
||||||
out_h=${mode#*x}; out_h=${out_h%@*}
|
set_mode() {
|
||||||
|
swaymsg output HEADLESS-1 mode "$1" >/dev/null
|
||||||
|
out_w=${1%x*}
|
||||||
|
out_h=${1#*x}; out_h=${out_h%@*}
|
||||||
|
}
|
||||||
|
set_mode "$mode"
|
||||||
|
|
||||||
# Built before the app starts, so a compile error is not reported as a
|
# Built before the app starts, so a compile error is not reported as a
|
||||||
# window that failed to move.
|
# window that failed to move.
|
||||||
@@ -149,7 +154,7 @@ while [ $i -lt "$((seconds * 2))" ]; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
|
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
|
||||||
swaymsg output HEADLESS-1 mode "$resize" >/dev/null
|
set_mode "$resize"
|
||||||
echo "run-headless: resized to $resize" >&2
|
echo "run-headless: resized to $resize" >&2
|
||||||
sleep 2
|
sleep 2
|
||||||
fi
|
fi
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 191 B |
+4
-3
@@ -15,9 +15,10 @@ where
|
|||||||
let region = ctx.data.render.window_region(&id).unwrap();
|
let region = ctx.data.render.window_region(&id).unwrap();
|
||||||
let id_pos = region.top_left;
|
let id_pos = region.top_left;
|
||||||
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
|
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
|
||||||
// The pointer arrives from the platform in floats; everything
|
// The two regions are on the grid and the pointer is not, so the
|
||||||
// it is compared against is on the grid.
|
// step between them is taken there and the pointer keeps the
|
||||||
let pos = (PxVec2::from_f32(ctx.data.pos) + container_pos - id_pos).to_f32();
|
// precision the platform gave it.
|
||||||
|
let pos = ctx.data.pos + (container_pos - id_pos).to_f32();
|
||||||
let size = region.size().to_f32();
|
let size = region.size().to_f32();
|
||||||
select(
|
select(
|
||||||
rsc,
|
rsc,
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ impl UiRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self) {
|
pub fn draw(&mut self) {
|
||||||
let output = match self.surface.get_current_texture() {
|
let (output, suboptimal) = match self.surface.get_current_texture() {
|
||||||
CurrentSurfaceTexture::Success(texture) => texture,
|
CurrentSurfaceTexture::Success(texture) => (texture, false),
|
||||||
CurrentSurfaceTexture::Suboptimal(texture) => {
|
// Used for this frame, and the swapchain rebuilt after it has
|
||||||
self.surface.configure(&self.device, &self.config);
|
// been presented: configuring the surface while a texture it
|
||||||
texture
|
// handed out is still alive panics.
|
||||||
}
|
CurrentSurfaceTexture::Suboptimal(texture) => (texture, true),
|
||||||
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
|
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
|
||||||
self.surface.configure(&self.device, &self.config);
|
self.surface.configure(&self.device, &self.config);
|
||||||
return;
|
return;
|
||||||
@@ -60,6 +60,9 @@ impl UiRenderer {
|
|||||||
self.queue.submit(std::iter::once(encoder.finish()));
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
self.window.pre_present_notify();
|
self.window.pre_present_notify();
|
||||||
self.queue.present(output);
|
self.queue.present(output);
|
||||||
|
if suboptimal {
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
||||||
|
|||||||
+4
-4
@@ -164,11 +164,11 @@ impl Harness {
|
|||||||
self.render.resize(size, self.rsc.widgets_mut());
|
self.render.resize(size, self.rsc.widgets_mut());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Changes a length rule after the fact, the way `.width()` sets one.
|
/// Changes a length rule after the fact, the way `.width()` sets one --
|
||||||
|
/// which leaves a bound beside it alone, where writing the whole rule
|
||||||
|
/// would drop it and pass the case for the wrong reason.
|
||||||
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) {
|
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) {
|
||||||
self.rsc
|
self.rsc.widgets_mut().set_len(id, axis, len.into());
|
||||||
.widgets_mut()
|
|
||||||
.set_size_rule(id, axis, SizeRule::Exact(len.into()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the root and lays it out, so a pointer event has something to hit.
|
/// Sets the root and lays it out, so a pointer event has something to hit.
|
||||||
|
|||||||
+108
-38
@@ -8,23 +8,16 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// The declared lengths of one widget carrying a size rule, by axis.
|
|
||||||
pub type Lens = [Option<LayoutLen>; 2];
|
|
||||||
|
|
||||||
/// Where one widget carrying an alignment sits, by axis. `None` uses the
|
|
||||||
/// centered default.
|
|
||||||
pub type Aligns = [Option<AxisAlign>; 2];
|
|
||||||
|
|
||||||
/// What a test changes between two trees grown from the same seed, so the
|
/// What a test changes between two trees grown from the same seed, so the
|
||||||
/// warm one can be mutated and the cold one grown that way to begin with.
|
/// warm one can be mutated and the cold one grown that way to begin with.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Edits {
|
pub struct Edits {
|
||||||
/// Declared sizes, by the order the rules were put on.
|
/// Declared sizes, by the order the rules were put on.
|
||||||
pub sizes: HashMap<usize, Lens>,
|
pub sizes: HashMap<usize, SizeRules>,
|
||||||
/// Which children a span has, by the order the spans were made.
|
/// Which children a span has, by the order the spans were made.
|
||||||
pub spans: HashMap<usize, SpanEdit>,
|
pub spans: HashMap<usize, SpanEdit>,
|
||||||
/// Alignments, by the order they were put on.
|
/// Alignments, by the order they were put on.
|
||||||
pub aligns: HashMap<usize, Aligns>,
|
pub aligns: HashMap<usize, Align>,
|
||||||
/// Which widgets own a movable region, by the order they were offered
|
/// Which widgets own a movable region, by the order they were offered
|
||||||
/// one. Region nodes change what a move writes and how deep a primitive's
|
/// one. Region nodes change what a move writes and how deep a primitive's
|
||||||
/// chain is, so a tree that never grows one leaves both untested.
|
/// chain is, so a tree that never grows one leaves both untested.
|
||||||
@@ -176,9 +169,11 @@ pub struct Plan {
|
|||||||
/// it one and the offer is taken or declined; a second offer to the same
|
/// it one and the offer is taken or declined; a second offer to the same
|
||||||
/// widget is dropped, because two rules on one widget would settle in the
|
/// widget is dropped, because two rules on one widget would settle in the
|
||||||
/// order they were applied rather than in grow order.
|
/// order they were applied rather than in grow order.
|
||||||
pub size: Option<Lens>,
|
pub size: Option<SizeRules>,
|
||||||
/// The alignment it carries, under the same one-offer rule.
|
/// The alignment it carries, under the same one-offer rule. An axis left
|
||||||
pub align: Option<Aligns>,
|
/// out takes the centered default, which is what [`RegionAlign`] reads it
|
||||||
|
/// as.
|
||||||
|
pub align: Option<Align>,
|
||||||
/// Whether it was offered a movable region of its own and what it
|
/// Whether it was offered a movable region of its own and what it
|
||||||
/// answered. `Some(false)` is an offer declined, which still uses up the
|
/// answered. `Some(false)` is an offer declined, which still uses up the
|
||||||
/// one offer, where `None` is an offer never made.
|
/// one offer, where `None` is an offer never made.
|
||||||
@@ -195,6 +190,9 @@ pub enum Kind {
|
|||||||
color: usize,
|
color: usize,
|
||||||
alpha: u8,
|
alpha: u8,
|
||||||
},
|
},
|
||||||
|
/// The one leaf whose own length is a number of pixels it knows before it
|
||||||
|
/// is drawn, which is the hint a rule beside it has to win over.
|
||||||
|
Image,
|
||||||
/// Scrolling reads the pixel length of its box, which nothing else here
|
/// Scrolling reads the pixel length of its box, which nothing else here
|
||||||
/// does, and gives its child a box longer than its own.
|
/// does, and gives its child a box longer than its own.
|
||||||
Scroll {
|
Scroll {
|
||||||
@@ -294,7 +292,7 @@ impl Plan {
|
|||||||
align: None,
|
align: None,
|
||||||
..self.clone()
|
..self.clone()
|
||||||
}),
|
}),
|
||||||
self.size.map(|_| Plan {
|
self.size.as_ref().map(|_| Plan {
|
||||||
size: None,
|
size: None,
|
||||||
..self.clone()
|
..self.clone()
|
||||||
}),
|
}),
|
||||||
@@ -344,6 +342,19 @@ impl Plan {
|
|||||||
at(self);
|
at(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drops every intrinsic bound from this tree, leaving the rest of it
|
||||||
|
/// -- and the generator's draws -- exactly as they were. That isolates
|
||||||
|
/// the ordinary path from the deferred one over the same shapes, which
|
||||||
|
/// is what says whether a difference is the bounds or the trees.
|
||||||
|
pub fn drop_bounds(&mut self) {
|
||||||
|
self.walk_mut(&mut |node| {
|
||||||
|
let Some(rules) = &mut node.size else { return };
|
||||||
|
for axis in Axis::BOTH {
|
||||||
|
rules[axis].bound = Bound::ANY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// The same tree with `edits` applied, by the indices the generator would
|
/// The same tree with `edits` applied, by the indices the generator would
|
||||||
/// have used for them.
|
/// have used for them.
|
||||||
///
|
///
|
||||||
@@ -375,7 +386,7 @@ impl Plan {
|
|||||||
}
|
}
|
||||||
if plan.size.is_some() {
|
if plan.size.is_some() {
|
||||||
if let Some(lens) = edits.sizes.get(&sized) {
|
if let Some(lens) = edits.sizes.get(&sized) {
|
||||||
plan.size = Some(*lens);
|
plan.size = Some(lens.clone());
|
||||||
}
|
}
|
||||||
sized += 1;
|
sized += 1;
|
||||||
}
|
}
|
||||||
@@ -448,9 +459,11 @@ impl Kind {
|
|||||||
}
|
}
|
||||||
match self {
|
match self {
|
||||||
// The one leaf that reads the width it is given, then the one
|
// The one leaf that reads the width it is given, then the one
|
||||||
// that does not, then the one that measures nothing at all.
|
// that does not, then the one that measures nothing at all. A
|
||||||
|
// picture measures nothing either, but its length is its own, so
|
||||||
|
// it steps to the leaf that takes whatever it is given.
|
||||||
Kind::Wrapped => out.push(Kind::OneLine),
|
Kind::Wrapped => out.push(Kind::OneLine),
|
||||||
Kind::OneLine => out.push(Kind::Rect {
|
Kind::OneLine | Kind::Image => out.push(Kind::Rect {
|
||||||
color: 0,
|
color: 0,
|
||||||
alpha: 255,
|
alpha: 255,
|
||||||
}),
|
}),
|
||||||
@@ -632,9 +645,10 @@ struct Sow<'a> {
|
|||||||
|
|
||||||
impl Sow<'_> {
|
impl Sow<'_> {
|
||||||
fn leaf(&mut self) -> Plan {
|
fn leaf(&mut self) -> Plan {
|
||||||
Plan::bare(match self.rng.below(4) {
|
Plan::bare(match self.rng.below(5) {
|
||||||
0 => Kind::Wrapped,
|
0 => Kind::Wrapped,
|
||||||
1 => Kind::OneLine,
|
1 => Kind::OneLine,
|
||||||
|
2 => Kind::Image,
|
||||||
_ => {
|
_ => {
|
||||||
let color = self.rng.below(COLORS.len());
|
let color = self.rng.below(COLORS.len());
|
||||||
let alpha = (self.rng.below(5) * 63) as u8;
|
let alpha = (self.rng.below(5) * 63) as u8;
|
||||||
@@ -643,15 +657,47 @@ impl Sow<'_> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn len(&mut self) -> Option<LayoutLen> {
|
fn len(&mut self) -> LayoutLen {
|
||||||
match self.rng.below(4) {
|
LayoutLen::px(20.0 + self.rng.below(180) as f32)
|
||||||
0 => Some(LayoutLen::px(20.0 + self.rng.below(180) as f32)),
|
}
|
||||||
1 => Some(LayoutLen::LEFTOVER),
|
|
||||||
_ => None,
|
/// A length of a box rather than a length of the window, which is what a
|
||||||
|
/// bound is.
|
||||||
|
///
|
||||||
|
/// Pixels, because a fraction is resolved against the rel base the widget
|
||||||
|
/// was asked with and `deferred_generated.rs` is where that is varied:
|
||||||
|
/// its relative-bound corpus rewrites these as fractions, so growing them
|
||||||
|
/// that way here would buy overlap and move every box in the cold dump.
|
||||||
|
/// Two depth-5 trees once disagreed warm against cold with fractions in
|
||||||
|
/// them, which is why this was written as pixels; they were named by
|
||||||
|
/// seed, and seeds stopped naming those trees when the leaves grew
|
||||||
|
/// images. Re-measured 2026-09-20: 600 depth-5 trees over all sixteen
|
||||||
|
/// cases agree with every bound here a fraction.
|
||||||
|
fn bound(&mut self) -> Len {
|
||||||
|
Len::px(20.0 + self.rng.below(180) as f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rule(&mut self) -> SizeRule {
|
||||||
|
match self.rng.below(8) {
|
||||||
|
0 | 1 => self.len().into(),
|
||||||
|
2 => LayoutLen::LEFTOVER.into(),
|
||||||
|
3 => SizeRule::min(self.bound()),
|
||||||
|
4 => SizeRule::max(self.bound()),
|
||||||
|
// Both in pixels, so one can be put under the other: a floor and
|
||||||
|
// a cap that change sides with the window bound nothing, which
|
||||||
|
// is a caller's bug rather than a tree to grow.
|
||||||
|
5 => {
|
||||||
|
let (a, b) = (
|
||||||
|
Px::from_f32(20.0 + self.rng.below(180) as f32),
|
||||||
|
Px::from_f32(20.0 + self.rng.below(180) as f32),
|
||||||
|
);
|
||||||
|
SizeRule::clamp(Len::px(a.min(b).to_f32()), Len::px(a.max(b).to_f32()))
|
||||||
|
}
|
||||||
|
_ => SizeRule::FREE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn align(&mut self) -> Aligns {
|
fn align(&mut self) -> Align {
|
||||||
let axis = |s: &mut Self| match s.rng.below(4) {
|
let axis = |s: &mut Self| match s.rng.below(4) {
|
||||||
0 => None,
|
0 => None,
|
||||||
1 => Some(AxisAlign::NEG),
|
1 => Some(AxisAlign::NEG),
|
||||||
@@ -661,21 +707,27 @@ impl Sow<'_> {
|
|||||||
let (x, y) = (axis(self), axis(self));
|
let (x, y) = (axis(self), axis(self));
|
||||||
// Aligning on neither axis leaves the branch unexercised.
|
// Aligning on neither axis leaves the branch unexercised.
|
||||||
match x.is_none() && y.is_none() {
|
match x.is_none() && y.is_none() {
|
||||||
true => [Some(AxisAlign::CENTER), y],
|
true => Align {
|
||||||
false => [x, y],
|
x: Some(AxisAlign::CENTER),
|
||||||
|
y,
|
||||||
|
},
|
||||||
|
false => Align { x, y },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A declared size over half the tree, kept where a test can change it.
|
/// A declared size over half the tree, kept where a test can change it.
|
||||||
fn sized(&mut self, inner: &mut Plan) {
|
fn sized(&mut self, inner: &mut Plan) {
|
||||||
let take = self.rng.chance();
|
let take = self.rng.chance();
|
||||||
let lens = [self.len(), self.len()];
|
let lens = SizeRules {
|
||||||
|
x: self.rule(),
|
||||||
|
y: self.rule(),
|
||||||
|
};
|
||||||
if !take || inner.size.is_some() {
|
if !take || inner.size.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let idx = self.sized;
|
let idx = self.sized;
|
||||||
self.sized += 1;
|
self.sized += 1;
|
||||||
inner.size = Some(self.edits.sizes.get(&idx).copied().unwrap_or(lens));
|
inner.size = Some(self.edits.sizes.get(&idx).cloned().unwrap_or(lens));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An alignment over some of the tree, kept where a test can change it.
|
/// An alignment over some of the tree, kept where a test can change it.
|
||||||
@@ -773,10 +825,6 @@ impl Sow<'_> {
|
|||||||
self.spans += 1;
|
self.spans += 1;
|
||||||
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
|
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
|
||||||
let dir = self.rng.below(4);
|
let dir = self.rng.below(4);
|
||||||
// A row takes the height it is given rather than its tallest child,
|
|
||||||
// which is a rule beside it. Derived from an existing choice and
|
|
||||||
// consuming no randomness: a seed must keep growing the same tree
|
|
||||||
// when the generator gains another configuration.
|
|
||||||
let gap = self.rng.below(3) as i32 * 4;
|
let gap = self.rng.below(3) as i32 * 4;
|
||||||
let grown: Vec<usize> = (0..children.len()).collect();
|
let grown: Vec<usize> = (0..children.len()).collect();
|
||||||
let order = span_edited(&grown, children.len(), spares.len(), &edit);
|
let order = span_edited(&grown, children.len(), spares.len(), &edit);
|
||||||
@@ -796,6 +844,7 @@ pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget,
|
|||||||
let mut build = Build {
|
let mut build = Build {
|
||||||
rsc,
|
rsc,
|
||||||
tree: Tree::default(),
|
tree: Tree::default(),
|
||||||
|
checkerboard: None,
|
||||||
};
|
};
|
||||||
let root = build.node(plan);
|
let root = build.node(plan);
|
||||||
(root, build.tree)
|
(root, build.tree)
|
||||||
@@ -804,23 +853,25 @@ pub fn build<Rsc: UiRsc + 'static>(rsc: &mut Rsc, plan: &Plan) -> (StrongWidget,
|
|||||||
struct Build<'a, Rsc> {
|
struct Build<'a, Rsc> {
|
||||||
rsc: &'a mut Rsc,
|
rsc: &'a mut Rsc,
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
|
/// The checkerboard, uploaded when the first image in this tree is built.
|
||||||
|
/// A handle is a reference to the texture, so every image after that one
|
||||||
|
/// clones this rather than uploading the same picture again.
|
||||||
|
checkerboard: Option<TextureHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
||||||
fn node(&mut self, plan: &Plan) -> StrongWidget {
|
fn node(&mut self, plan: &Plan) -> StrongWidget {
|
||||||
let built = self.kind(&plan.kind);
|
let built = self.kind(&plan.kind);
|
||||||
let id = built.id();
|
let id = built.id();
|
||||||
if let Some(lens) = plan.size {
|
if let Some(lens) = plan.size.clone() {
|
||||||
self.rsc
|
self.rsc.ui_mut().widgets.set_size_rules(id, lens.x, lens.y);
|
||||||
.ui_mut()
|
|
||||||
.widgets
|
|
||||||
.set_size_rules(id, lens[0], lens[1]);
|
|
||||||
self.tree.sized.push(id);
|
self.tree.sized.push(id);
|
||||||
}
|
}
|
||||||
if let Some(align) = plan.align {
|
if let Some(align) = plan.align {
|
||||||
|
let resolved = RegionAlign::from(align);
|
||||||
let widgets = &mut self.rsc.ui_mut().widgets;
|
let widgets = &mut self.rsc.ui_mut().widgets;
|
||||||
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
|
for axis in Axis::BOTH {
|
||||||
widgets.set_alignment(id, axis, align.unwrap_or_default());
|
widgets.set_alignment(id, axis, resolved[axis]);
|
||||||
}
|
}
|
||||||
self.tree.aligned.push(id);
|
self.tree.aligned.push(id);
|
||||||
}
|
}
|
||||||
@@ -831,6 +882,20 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
|||||||
built
|
built
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The one picture the generated trees draw: a 64x64 checkerboard of purple
|
||||||
|
/// and black in 8 px cells. Committed rather than drawn here, so that one
|
||||||
|
/// seed is one tree whatever anything else does, and included rather than
|
||||||
|
/// opened, so that growing a tree does not depend on a working directory.
|
||||||
|
fn checkerboard(&mut self) -> TextureHandle {
|
||||||
|
if self.checkerboard.is_none() {
|
||||||
|
let image = include_bytes!("assets/checkerboard.png")
|
||||||
|
.get_image()
|
||||||
|
.expect("the checkerboard is committed beside this file");
|
||||||
|
self.checkerboard = Some(self.rsc.ui_mut().textures.add(image));
|
||||||
|
}
|
||||||
|
self.checkerboard.clone().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn kind(&mut self, kind: &Kind) -> StrongWidget {
|
fn kind(&mut self, kind: &Kind) -> StrongWidget {
|
||||||
let id: StrongWidget = match kind {
|
let id: StrongWidget = match kind {
|
||||||
Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
Kind::Wrapped => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
||||||
@@ -839,6 +904,7 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
|||||||
.wrap(false)
|
.wrap(false)
|
||||||
.add_strong(self.rsc),
|
.add_strong(self.rsc),
|
||||||
Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc),
|
Kind::Rect { color, alpha } => rect(COLORS[*color].alpha(*alpha)).add_strong(self.rsc),
|
||||||
|
Kind::Image => Image::new(self.checkerboard()).add_strong(self.rsc),
|
||||||
Kind::Scroll { axis, inner } => {
|
Kind::Scroll { axis, inner } => {
|
||||||
let inner = self.node(inner);
|
let inner = self.node(inner);
|
||||||
let id = Scroll::new(inner, *axis).add(self.rsc);
|
let id = Scroll::new(inner, *axis).add(self.rsc);
|
||||||
@@ -915,6 +981,10 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
|
|||||||
gap: Px::from_int(*gap),
|
gap: Px::from_int(*gap),
|
||||||
}
|
}
|
||||||
.add(self.rsc);
|
.add(self.rsc);
|
||||||
|
// A row takes the height it is given rather than its tallest
|
||||||
|
// child, which is a rule beside the span rather than anything
|
||||||
|
// it draws. Derived from `dir` rather than stored, so a plan
|
||||||
|
// that says the direction says this too.
|
||||||
if dir.axis == Axis::X {
|
if dir.axis == Axis::X {
|
||||||
self.rsc
|
self.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ impl Widget for Image {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Image {
|
||||||
|
/// One texture already uploaded, for a caller holding its handle: [`image()`]
|
||||||
|
/// uploads what it is given, and several widgets showing one picture want
|
||||||
|
/// one upload and one slot between them.
|
||||||
|
pub fn new(handle: TextureHandle) -> Self {
|
||||||
|
Self { handle }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn image<State: UiRsc>(image: impl LoadableImage) -> impl WidgetFn<State, Image> {
|
pub fn image<State: UiRsc>(image: impl LoadableImage) -> impl WidgetFn<State, Image> {
|
||||||
let image = image.get_image().expect("Failed to load image");
|
let image = image.get_image().expect("Failed to load image");
|
||||||
move |state| Image {
|
move |state| Image {
|
||||||
|
|||||||
+8
-5
@@ -8,11 +8,14 @@ impl Widget for Masked {
|
|||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter.set_mask(UiRegion::FULL);
|
painter.set_mask(UiRegion::FULL);
|
||||||
painter.widget(&self.inner);
|
painter.widget(&self.inner);
|
||||||
// What it occupies is its box, on both axes, for the reason `Scroll`
|
// What it occupies is its box, on both axes, because it clips what is
|
||||||
// reports the same: it clips what is inside to that box, so it can
|
// inside to that box: it can neither take less of one nor honestly ask
|
||||||
// neither take less of one nor honestly ask for more. Passing the
|
// for more. Passing the inner size up instead asks to be placed at a
|
||||||
// inner size up instead asks to be placed at a length it does not
|
// length it does not draw, and the framework would place the drawing it
|
||||||
// draw, and the framework would place the drawing it clipped away.
|
// clipped away. `Scroll` reports its box too, for a reason of its own:
|
||||||
|
// it is a viewport whose content is positioned by a move rather than
|
||||||
|
// clipped, since masking is a capability a caller opts into by putting
|
||||||
|
// one of these around it.
|
||||||
Size::LEFTOVER
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ pub struct LayerOffset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for LayerOffset {
|
impl Widget for LayerOffset {
|
||||||
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
|
requests.widget(&self.inner, axis)
|
||||||
|
}
|
||||||
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
for _ in 0..self.offset {
|
for _ in 0..self.offset {
|
||||||
painter.next_layer();
|
painter.next_layer();
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ pub struct Offset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Offset {
|
impl Widget for Offset {
|
||||||
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
|
requests.widget(&self.inner, axis)
|
||||||
|
}
|
||||||
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter
|
painter
|
||||||
.widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
|
.widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ pub struct Pad {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Pad {
|
impl Widget for Pad {
|
||||||
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
|
requests.inset(&self.inner, axis, self.padding.along(axis))
|
||||||
|
}
|
||||||
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
// The inner's own alignment, not the near edge. This reports the
|
// The inner's own alignment, not the near edge. This reports the
|
||||||
// inner's size plus the padding, so where the box is that answer the
|
// inner's size plus the padding, so where the box is that answer the
|
||||||
@@ -22,11 +26,11 @@ impl Widget for Pad {
|
|||||||
let inner = painter.widget_at(&self.inner, self.padding.region()).size();
|
let inner = painter.widget_at(&self.inner, self.padding.region()).size();
|
||||||
Size {
|
Size {
|
||||||
x: LayoutLen {
|
x: LayoutLen {
|
||||||
px: inner.x.px + self.padding.left + self.padding.right,
|
px: inner.x.px + self.padding.along(Axis::X),
|
||||||
..inner.x
|
..inner.x
|
||||||
},
|
},
|
||||||
y: LayoutLen {
|
y: LayoutLen {
|
||||||
px: inner.y.px + self.padding.top + self.padding.bottom,
|
px: inner.y.px + self.padding.along(Axis::Y),
|
||||||
..inner.y
|
..inner.y
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -57,6 +61,16 @@ impl Padding {
|
|||||||
bottom: amt,
|
bottom: amt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Both sides of one axis together, which is what this padding takes
|
||||||
|
/// of a length along it.
|
||||||
|
pub fn along(&self, axis: Axis) -> Px {
|
||||||
|
match axis {
|
||||||
|
Axis::X => self.left + self.right,
|
||||||
|
Axis::Y => self.top + self.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `region` less this padding on each side.
|
/// `region` less this padding on each side.
|
||||||
pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
|
pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
|
||||||
region.x.start.px += self.left;
|
region.x.start.px += self.left;
|
||||||
|
|||||||
@@ -24,36 +24,37 @@ impl Widget for Scroll {
|
|||||||
self.amt = self.content_len - self.container_len;
|
self.amt = self.content_len - self.container_len;
|
||||||
}
|
}
|
||||||
self.update_amt();
|
self.update_amt();
|
||||||
let align = painter.alignment()[self.axis];
|
// Reading the box in pixels above holds this drawing to that one
|
||||||
// Content of a fixed length that fits sits at the start of any box it
|
// length, so these two say where it holds more widely.
|
||||||
// fits in -- but only anchored there. Anywhere else it is a part of
|
//
|
||||||
// the room left over, so it moves with every length the box takes and
|
// Content of a fixed length that fits is handed the whole box below,
|
||||||
// the drawing holds for that length alone. One scrolled part way sits
|
// and nothing here reads the box again, so every longer box gives the
|
||||||
// where it is until the box shrinks past what is left of it. Kept to
|
// same drawing: it holds from the length the content needs upwards,
|
||||||
// the end, it moves with every length.
|
// and shrinking past that is what changes it. Where it sits in a box
|
||||||
|
// longer than itself is not this widget's to say -- placing its
|
||||||
|
// answer in the whole box is its own alignment, and that placement is
|
||||||
|
// a fraction of the box, so it holds at every length too.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
let answer_is_px = answer_len.is_px();
|
let answer_is_px = answer_len.is_px();
|
||||||
if answer_is_px && self.content_len <= self.container_len && align == AxisAlign::NEG {
|
if answer_is_px && self.content_len <= self.container_len {
|
||||||
painter.holds(self.axis, answer_px..=Px::MAX);
|
painter.holds(self.axis, answer_px..=Px::MAX);
|
||||||
} else if answer_is_px && !self.snap_end {
|
} else if answer_is_px && !self.snap_end {
|
||||||
let left = self.content_len - self.amt;
|
let left = self.content_len - self.amt;
|
||||||
painter.holds(self.axis, Px::MIN..=left);
|
painter.holds(self.axis, Px::MIN..=left);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content shorter than the viewport has room to sit in, and where it
|
// Content that fills the viewport is the viewport, and is handed back
|
||||||
// sits is this widget's own alignment -- the same property that would
|
// as it came -- it has nothing to scroll through, so the clamp above
|
||||||
// have placed the whole scroll in a box longer than it.
|
// has already put `amt` at zero. Writing the same box as its own
|
||||||
let slack = (self.container_len - self.content_len).max(Px::ZERO);
|
// length in pixels is the same box in another form, and the two do
|
||||||
let anchor = slack.mul(align.rel());
|
// not round alike: a part centred in `rel 1` lands a step from one
|
||||||
// Content that fills the viewport and has not been scrolled is the
|
// centred in `px 900`, since halving a difference is not halving each
|
||||||
// viewport, and is handed back as it came. Writing the same box as
|
// part of it.
|
||||||
// its own length in pixels is the same box in another form, and the
|
let content = match self.content_len > self.container_len {
|
||||||
// two do not round alike: a part centred in `rel 1` lands a step from
|
|
||||||
// one centred in `px 900`, since halving a difference is not halving
|
|
||||||
// each part of it.
|
|
||||||
let moved = anchor != Px::ZERO || self.amt != Px::ZERO;
|
|
||||||
let content = match moved || self.content_len != self.container_len {
|
|
||||||
true => {
|
true => {
|
||||||
let start = Len::from_parts(Rel::ZERO, anchor - self.amt);
|
let start = Len::from_parts(Rel::ZERO, -self.amt);
|
||||||
UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
|
UiSpan::new(start, start.offset(self.content_len)).shifted_desc()
|
||||||
}
|
}
|
||||||
false => PlaceDescAxis::WHOLE,
|
false => PlaceDescAxis::WHOLE,
|
||||||
|
|||||||
+127
-65
@@ -8,69 +8,75 @@ pub struct Span {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Span {
|
impl Widget for Span {
|
||||||
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
|
if axis != self.dir.axis {
|
||||||
|
// A share can be hidden when the other axis has no room. Its
|
||||||
|
// cross-axis length then contributes nothing to the drawn answer.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut total = RequestedLen::from(Len::from_parts(Rel::ZERO, self.gaps()));
|
||||||
|
for child in &self.children {
|
||||||
|
let child = requests.widget(child, axis)?;
|
||||||
|
total = requests.sum(total, child);
|
||||||
|
}
|
||||||
|
Some(total)
|
||||||
|
}
|
||||||
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.with_requests(|painter, lens, values| self.layout(painter, lens, values))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Span {
|
||||||
|
fn layout(
|
||||||
|
&self,
|
||||||
|
painter: &mut Painter,
|
||||||
|
lens: &mut Vec<RequestedLen>,
|
||||||
|
values: &mut Vec<Px>,
|
||||||
|
) -> Size {
|
||||||
let axis = self.dir.axis;
|
let axis = self.dir.axis;
|
||||||
// The row this span lays its children out along, as a length of the
|
// The row this span lays its children out along, as a length of the
|
||||||
// rel base they are laid out against. Where it starts is nothing's
|
// rel base they are laid out against. Where it starts is nothing's
|
||||||
// business -- a slot is a length from there -- so what this reads is
|
// business -- a slot is a length from there -- so what this reads is
|
||||||
// the length alone.
|
// the length alone.
|
||||||
let row = painter.region_len(axis);
|
let row = painter.region_len(axis);
|
||||||
// A length for every child before their final slots are chosen: from
|
self.collect(painter, row, lens, true);
|
||||||
// a hint where one says, and from drawing otherwise. The rel base passes
|
let gaps = self.gaps();
|
||||||
// through unchanged, so `rel(0.5)` is half the area this span was
|
let fixed = lens
|
||||||
// given whatever else is in it and wherever this child sits among
|
.iter()
|
||||||
// them; what a drawn child is asked in is the room left from the
|
.try_fold(Len::from_parts(Rel::ZERO, gaps), |sum, len| {
|
||||||
// cursor, because a text has to wrap at the width actually there.
|
Some(sum + len.linear()?.without_leftover())
|
||||||
let mut cursor = Len::ZERO;
|
});
|
||||||
let mut lens = Vec::with_capacity(self.children.len());
|
if let Some(fixed) = fixed
|
||||||
for child in &self.children {
|
&& lens.iter().any(|len| len.has_leftover())
|
||||||
let len = match painter.size_hint(child, axis) {
|
&& !painter.longer_than(row, fixed, axis)
|
||||||
Some(len) => len,
|
{
|
||||||
None => {
|
// With no share to assign, intrinsic drawings keep the remaining
|
||||||
// Across itself the child sits where its own alignment
|
// offer, including overflow. Their answer is only moved into a slot.
|
||||||
// says, in the whole of the row: a span is what contains
|
self.collect(painter, row, lens, false);
|
||||||
// its children there, and nothing divides that axis.
|
|
||||||
let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
|
|
||||||
painter.widget_at(child, room).len(axis)
|
|
||||||
}
|
}
|
||||||
};
|
let nonlinear = lens.iter().any(|len| len.linear().is_none());
|
||||||
cursor += len.without_leftover();
|
if nonlinear {
|
||||||
cursor.px += self.gap;
|
painter.allocate(lens, row - Len::from_parts(Rel::ZERO, gaps), axis, values);
|
||||||
lens.push(len);
|
|
||||||
}
|
}
|
||||||
|
let allocated = nonlinear.then(|| &values[..]);
|
||||||
let gaps = self
|
let total = match allocated {
|
||||||
.gap
|
Some(allocated) => LayoutLen {
|
||||||
.mul_int(self.children.len().saturating_sub(1) as i32);
|
px: allocated.iter().fold(gaps, |sum, len| sum + *len),
|
||||||
let total = lens.iter().fold(
|
..LayoutLen::ZERO
|
||||||
|
},
|
||||||
|
None => lens.iter().fold(
|
||||||
LayoutLen {
|
LayoutLen {
|
||||||
px: gaps,
|
px: gaps,
|
||||||
..LayoutLen::ZERO
|
..LayoutLen::ZERO
|
||||||
},
|
},
|
||||||
|sum, len| sum + *len,
|
|sum, len| sum + len.linear().unwrap(),
|
||||||
);
|
),
|
||||||
|
|
||||||
// What is left for the shares to divide: the row less everything
|
|
||||||
// fixed, as a length of the rel base rather than a number of pixels.
|
|
||||||
let room = row - total.without_leftover();
|
|
||||||
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
|
||||||
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
|
|
||||||
// itself, and answered back through the same expression, so the
|
|
||||||
// boundary is the drawing's own and not a second way of finding it:
|
|
||||||
// the three cases a rounded division needed -- the fixed parts
|
|
||||||
// growing slower than the box, faster, or exactly with it -- are the
|
|
||||||
// sign of `room.rel`, which `through` already reads. What the
|
|
||||||
// generated oracle checks is the consequence, since which children
|
|
||||||
// exist at all turns on this.
|
|
||||||
let any_leftover = total.leftover > Weight::ZERO;
|
|
||||||
let has_room = any_leftover && painter.to_px(room, axis) > Px::ZERO;
|
|
||||||
if any_leftover {
|
|
||||||
let holds = match has_room {
|
|
||||||
true => Holds::from(Px::STEP..=Px::MAX),
|
|
||||||
false => Holds::from(Px::MIN..=Px::ZERO),
|
|
||||||
};
|
};
|
||||||
painter.window_holds(axis, holds.through(room));
|
let all_fixed = total.without_leftover();
|
||||||
}
|
let room = row - all_fixed;
|
||||||
|
let any_leftover = total.leftover > Weight::ZERO;
|
||||||
|
let has_room = any_leftover && painter.longer_than(row, all_fixed, axis);
|
||||||
|
|
||||||
// Across itself a span is as long as its longest child -- unless a
|
// Across itself a span is as long as its longest child -- unless a
|
||||||
// rule beside it gives that length outright, and then reading them
|
// rule beside it gives that length outright, and then reading them
|
||||||
@@ -93,17 +99,34 @@ impl Widget for Span {
|
|||||||
false => fixed,
|
false => fixed,
|
||||||
true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
|
true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
|
||||||
};
|
};
|
||||||
for (child, &len) in self.children.iter().zip(&lens) {
|
for (index, (child, request)) in self.children.iter().zip(lens.iter()).enumerate() {
|
||||||
// A child asking for nothing but a part of what is left over,
|
// An allocated row already has a length for every child; without
|
||||||
// when nothing is, is not drawn at all. One that also asked for
|
// one the request is the length and the room is divided here.
|
||||||
// pixels or a fraction keeps those and overflows.
|
// Either way a child asking for nothing but a part of what is
|
||||||
if len.is_only_leftover() && !has_room {
|
// left over, when nothing is, is not drawn at all -- one that
|
||||||
|
// also asked for pixels or a fraction keeps those and overflows.
|
||||||
|
let (len, shares, nothing_left) = match allocated {
|
||||||
|
Some(allocated) => {
|
||||||
|
let len = LayoutLen {
|
||||||
|
px: allocated[index],
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
};
|
||||||
|
let shares = request.has_leftover();
|
||||||
|
(len, shares, shares && len.px == Px::ZERO)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let len = request.linear().unwrap();
|
||||||
|
let shares = len.leftover > Weight::ZERO && has_room;
|
||||||
|
(len, shares, len.is_only_leftover() && !has_room)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if nothing_left {
|
||||||
painter.undraw(child);
|
painter.undraw(child);
|
||||||
fixed.px += self.gap;
|
fixed.px += self.gap;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let from = reached(fixed, taken);
|
let from = reached(fixed, taken);
|
||||||
if len.leftover > Weight::ZERO && has_room {
|
if shares {
|
||||||
taken += len.leftover;
|
taken += len.leftover;
|
||||||
}
|
}
|
||||||
fixed += len.without_leftover();
|
fixed += len.without_leftover();
|
||||||
@@ -116,15 +139,15 @@ impl Widget for Span {
|
|||||||
// fixed child's slot is its own answer, so a drawing made in the
|
// fixed child's slot is its own answer, so a drawing made in the
|
||||||
// room is put there as it is, and one not made yet is made here.
|
// room is put there as it is, and one not made yet is made here.
|
||||||
let slot = self.slot(row, from, to);
|
let slot = self.slot(row, from, to);
|
||||||
let mut place = slot.shifted_desc().fills().on_axis(axis);
|
let mut place = slot.shifted_desc().allocated().on_axis(axis);
|
||||||
if len.leftover > Weight::ZERO && has_room {
|
if shares {
|
||||||
place = place.rel_base(axis, slot.len());
|
place = place.rel_base(axis, slot.len());
|
||||||
}
|
}
|
||||||
let used = painter.place_at(child, place).len(!axis);
|
let used = painter.place_at(child, place).len(!axis);
|
||||||
if shrinks {
|
if shrinks {
|
||||||
// Choosing between a fixed and a relative length from the
|
// Choosing between a fixed and a relative length from the
|
||||||
// span's own eventual width admits multiple fixed points.
|
// span's own eventual width admits multiple fixed points.
|
||||||
// A scalable child therefore makes Children scalable too;
|
// A scalable child therefore makes the span scalable too;
|
||||||
// only fixed children are compared with one another.
|
// only fixed children are compared with one another.
|
||||||
if !used.is_px() {
|
if !used.is_px() {
|
||||||
ortho = LayoutLen::LEFTOVER;
|
ortho = LayoutLen::LEFTOVER;
|
||||||
@@ -135,13 +158,15 @@ impl Widget for Span {
|
|||||||
fixed.px += self.gap;
|
fixed.px += self.gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carried whole rather than collapsed to one share: a span that sizes
|
// Where nothing was allocated the weight is carried whole rather
|
||||||
// from its children does not resolve `leftover`, it passes the weight up,
|
// than collapsed to one share, so nesting spans divides the same
|
||||||
// so nesting spans divides the same space rather than re-dividing a
|
// space rather than re-dividing a share of it: four `leftover(1)`
|
||||||
// share of it. Four `leftover(1)` children under two spans under one span
|
// children under two spans under one span get a quarter each, which
|
||||||
// get a quarter each, which collapsing to `leftover(1)` per level does
|
// one share per level does not give. Resolution happens at the
|
||||||
// not give. Resolution happens at the nearest ancestor with a length,
|
// nearest ancestor with a length, and the root always has one --
|
||||||
// and the root always has one.
|
// or, where a comparison deferred the row, at the ancestor whose
|
||||||
|
// allocation discovery carried these requests to, and `total` is
|
||||||
|
// pixels by the time it gets here.
|
||||||
let ortho = match shrinks {
|
let ortho = match shrinks {
|
||||||
true => ortho,
|
true => ortho,
|
||||||
false => LayoutLen::rel(1.0),
|
false => LayoutLen::rel(1.0),
|
||||||
@@ -151,6 +176,43 @@ impl Widget for Span {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Span {
|
impl Span {
|
||||||
|
/// What the gaps between this span's children take, which is a length of
|
||||||
|
/// the row before anything is divided.
|
||||||
|
fn gaps(&self) -> Px {
|
||||||
|
self.gap
|
||||||
|
.mul_int(self.children.len().saturating_sub(1) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect(
|
||||||
|
&self,
|
||||||
|
painter: &mut Painter,
|
||||||
|
row: Len,
|
||||||
|
lens: &mut Vec<RequestedLen>,
|
||||||
|
discover: bool,
|
||||||
|
) {
|
||||||
|
let axis = self.dir.axis;
|
||||||
|
let mut cursor = Len::ZERO;
|
||||||
|
lens.clear();
|
||||||
|
for child in &self.children {
|
||||||
|
let request = if discover {
|
||||||
|
painter.size_request(child, axis)
|
||||||
|
} else {
|
||||||
|
painter.size_hint(child, axis).map(Into::into)
|
||||||
|
};
|
||||||
|
let len = match request {
|
||||||
|
Some(len) => len,
|
||||||
|
None => {
|
||||||
|
let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
|
||||||
|
let len = painter.widget_at(child, room).len(axis);
|
||||||
|
painter.measured_request(child, axis, len)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
cursor += painter.minimum_request(&len, axis);
|
||||||
|
cursor.px += self.gap;
|
||||||
|
lens.push(len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The stretch of the row between two distances from where this span
|
/// The stretch of the row between two distances from where this span
|
||||||
/// starts laying children out, as a span of its own box. A negative
|
/// starts laying children out, as a span of its own box. A negative
|
||||||
/// direction lays out from the far end, so the same two distances mirror
|
/// direction lays out from the far end, so the same two distances mirror
|
||||||
|
|||||||
@@ -8,6 +8,19 @@ pub struct Stack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Stack {
|
impl Widget for Stack {
|
||||||
|
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||||
|
let sizing = match self.size {
|
||||||
|
StackSize::Default => None,
|
||||||
|
StackSize::Child(i) => self.children.get(i),
|
||||||
|
};
|
||||||
|
// With nothing sizing it a stack is a share of the box it is given,
|
||||||
|
// which is what its draw answers too.
|
||||||
|
match sizing {
|
||||||
|
Some(child) => requests.widget(child, axis),
|
||||||
|
None => Some(LayoutLen::LEFTOVER.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let sizing = match self.size {
|
let sizing = match self.size {
|
||||||
StackSize::Default => None,
|
StackSize::Default => None,
|
||||||
|
|||||||
@@ -321,12 +321,10 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
let old = (self.text.view.buf.text().to_string(), self.text.selection);
|
let old = (self.text.view.buf.text().to_string(), self.text.selection);
|
||||||
let mut undo = false;
|
let mut undo = false;
|
||||||
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
||||||
if undo {
|
if undo && let Some((old, selection)) = self.text.history.pop() {
|
||||||
if let Some((old, selection)) = self.text.history.pop() {
|
|
||||||
self.set(&old);
|
self.set(&old);
|
||||||
self.text.selection = selection;
|
self.text.selection = selection;
|
||||||
self.clamp_selection_to_layout();
|
self.clamp_selection_to_layout();
|
||||||
}
|
|
||||||
} else if self.text.view.buf.text() != old.0 {
|
} else if self.text.view.buf.text() != old.0 {
|
||||||
self.text.history.push(old);
|
self.text.history.push(old);
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-17
@@ -19,8 +19,8 @@ widget_trait! {
|
|||||||
move |state| {
|
move |state| {
|
||||||
let id = self.add(state);
|
let id = self.add(state);
|
||||||
let widgets = &mut state.ui_mut().widgets;
|
let widgets = &mut state.ui_mut().widgets;
|
||||||
for (axis, align) in [(Axis::X, align.x), (Axis::Y, align.y)] {
|
for axis in Axis::BOTH {
|
||||||
if let Some(align) = align {
|
if let Some(align) = align[axis] {
|
||||||
widgets.set_alignment(id, axis, align);
|
widgets.set_alignment(id, axis, align);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,32 +53,64 @@ widget_trait! {
|
|||||||
move |state| {
|
move |state| {
|
||||||
let id = self.add(state);
|
let id = self.add(state);
|
||||||
let widgets = &mut state.ui_mut().widgets;
|
let widgets = &mut state.ui_mut().widgets;
|
||||||
widgets.set_size_rule(id, Axis::X, SizeRule::Exact(size.x));
|
widgets.set_len(id, Axis::X, size.x);
|
||||||
widgets.set_size_rule(id, Axis::Y, SizeRule::Exact(size.y));
|
widgets.set_len(id, Axis::Y, size.y);
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn width(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
fn width(self, len: impl Into<SizeRequest>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
let len = len.into();
|
let len = len.into();
|
||||||
move |state| {
|
move |state| {
|
||||||
let id = self.add(state);
|
let id = self.add(state);
|
||||||
state
|
state.ui_mut().widgets.set_len(id, Axis::X, len);
|
||||||
.ui_mut()
|
|
||||||
.widgets
|
|
||||||
.set_size_rule(id, Axis::X, SizeRule::Exact(len));
|
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn height(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
/// Sets a floor on this widget's offered width and reported width.
|
||||||
|
fn min_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
let len = len.into();
|
let len = len.into();
|
||||||
move |state| {
|
move |state| {
|
||||||
let id = self.add(state);
|
let id = self.add(state);
|
||||||
state
|
state.ui_mut().widgets.set_min_len(id, Axis::X, len);
|
||||||
.ui_mut()
|
id
|
||||||
.widgets
|
}
|
||||||
.set_size_rule(id, Axis::Y, SizeRule::Exact(len));
|
}
|
||||||
|
|
||||||
|
fn min_height(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
|
let len = len.into();
|
||||||
|
move |state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
state.ui_mut().widgets.set_min_len(id, Axis::Y, len);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Caps this widget's offered width and reported width.
|
||||||
|
fn max_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
|
let len = len.into();
|
||||||
|
move |state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
state.ui_mut().widgets.set_max_len(id, Axis::X, len);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_height(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
|
let len = len.into();
|
||||||
|
move |state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
state.ui_mut().widgets.set_max_len(id, Axis::Y, len);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn height(self, len: impl Into<SizeRequest>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
|
let len = len.into();
|
||||||
|
move |state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
state.ui_mut().widgets.set_len(id, Axis::Y, len);
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,9 +166,9 @@ widget_trait! {
|
|||||||
|state| self.add(state)
|
|state| self.add(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Named for the type it makes rather than as `wrapped`, which would read
|
/// This widget in a [`Wrapper`], which is how it gets a second length or
|
||||||
// as the text setting. `widget_trait!` takes no attributes, so what it is
|
/// alignment beside the one it already carries. Named for the type it
|
||||||
// for is on `Wrapper` itself.
|
/// makes rather than as `wrapped`, which would read as the text setting.
|
||||||
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|
||||||
|state| Wrapper {
|
|state| Wrapper {
|
||||||
inner: Some(self.add_strong(state)),
|
inner: Some(self.add_strong(state)),
|
||||||
|
|||||||
+2
-11
@@ -9,6 +9,7 @@ use std::marker::Unsize;
|
|||||||
///
|
///
|
||||||
/// Its child is optional so it can also be the swappable slot a tab bar
|
/// Its child is optional so it can also be the swappable slot a tab bar
|
||||||
/// needs, which is what it was written for.
|
/// needs, which is what it was written for.
|
||||||
|
#[derive(Default)]
|
||||||
pub struct Wrapper {
|
pub struct Wrapper {
|
||||||
pub inner: Option<StrongWidget>,
|
pub inner: Option<StrongWidget>,
|
||||||
}
|
}
|
||||||
@@ -26,11 +27,7 @@ impl Wrapper {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
pub fn empty() -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
|
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
|
||||||
self.inner = Some(to)
|
self.inner = Some(to)
|
||||||
}
|
}
|
||||||
@@ -42,9 +39,3 @@ impl Wrapper {
|
|||||||
self.inner.replace(to)
|
self.inner.replace(to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Wrapper {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
use iris::{harness::Harness, prelude::*};
|
||||||
|
use std::{
|
||||||
|
alloc::{GlobalAlloc, Layout, System},
|
||||||
|
cell::Cell,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Counting;
|
||||||
|
thread_local! {
|
||||||
|
static COUNT: Cell<Option<usize>> = const { Cell::new(None) };
|
||||||
|
}
|
||||||
|
fn count() {
|
||||||
|
COUNT.with(|count| {
|
||||||
|
if let Some(n) = count.get() {
|
||||||
|
count.set(Some(n + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// The wrapper preserves System's allocation and deallocation contracts;
|
||||||
|
// observing calls here also counts allocations hidden inside layout helpers.
|
||||||
|
unsafe impl GlobalAlloc for Counting {
|
||||||
|
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||||
|
count();
|
||||||
|
unsafe { System.alloc(layout) }
|
||||||
|
}
|
||||||
|
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||||
|
unsafe { System.dealloc(ptr, layout) }
|
||||||
|
}
|
||||||
|
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
|
||||||
|
count();
|
||||||
|
unsafe { System.realloc(ptr, layout, size) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOCATOR: Counting = Counting;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unchanged_tree_reuses_layout_storage() {
|
||||||
|
for rule in [
|
||||||
|
SizeRule::FREE,
|
||||||
|
leftover(1).clamp(20, 80).into(),
|
||||||
|
SizeRule::clamp(20.into(), 80.into()),
|
||||||
|
] {
|
||||||
|
let mut h = Harness::new((600, 200));
|
||||||
|
let mut children: Vec<StrongWidget> = Vec::new();
|
||||||
|
for _ in 0..8 {
|
||||||
|
let a = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_size_rule(a, Axis::X, rule.clone());
|
||||||
|
let row = (a, rect(Color::BLUE))
|
||||||
|
.span(Dir::RIGHT)
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
children.push(row);
|
||||||
|
}
|
||||||
|
let root = h.rsc.widgets_mut().add_strong(Span {
|
||||||
|
children,
|
||||||
|
dir: Dir::DOWN,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
});
|
||||||
|
h.state.root = Some(root);
|
||||||
|
h.frame();
|
||||||
|
let ids: Vec<_> = h.render.active.keys().copied().collect();
|
||||||
|
for frame in 0..8 {
|
||||||
|
for &id in &ids {
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
|
}
|
||||||
|
h.resize((600 + frame % 2, 200));
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
COUNT.set(Some(0));
|
||||||
|
for frame in 0..100 {
|
||||||
|
for &id in &ids {
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
|
}
|
||||||
|
h.resize((600 + frame % 2, 200));
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
let allocations = COUNT.replace(None).unwrap();
|
||||||
|
println!("rule={rule:?}: {allocations} allocations over 100 resize frames");
|
||||||
|
assert_eq!(allocations, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text drawn again at the width it already has places no glyphs and shapes
|
||||||
|
/// nothing, so the frame costs nothing at all -- which is what the shaping
|
||||||
|
/// cache is for, and a copy of the attrs made to ask it undid for any text
|
||||||
|
/// naming its font family.
|
||||||
|
///
|
||||||
|
/// Only that case: a text drawn at a width it has not seen places its glyphs,
|
||||||
|
/// and placing them allocates a list to hold them.
|
||||||
|
#[test]
|
||||||
|
fn redrawing_a_text_at_one_width_allocates_nothing() {
|
||||||
|
let mut h = Harness::new((600, 200));
|
||||||
|
let mut col = Span::empty(Dir::DOWN);
|
||||||
|
let mut texts = Vec::new();
|
||||||
|
for _ in 0..8 {
|
||||||
|
let text =
|
||||||
|
wtext("wrapping shapes one source into as many lines as the box leaves room for")
|
||||||
|
.size(16)
|
||||||
|
// Named rather than generic, because a named one is the family
|
||||||
|
// that costs an allocation to copy.
|
||||||
|
.family(Family::Named("sans-serif".into()))
|
||||||
|
.wrap(true)
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
texts.push(text.id());
|
||||||
|
col.push(text);
|
||||||
|
}
|
||||||
|
let root = col.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
let redraw = |h: &mut Harness| {
|
||||||
|
for &id in &texts {
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
|
}
|
||||||
|
h.frame();
|
||||||
|
};
|
||||||
|
for _ in 0..8 {
|
||||||
|
redraw(&mut h);
|
||||||
|
}
|
||||||
|
COUNT.set(Some(0));
|
||||||
|
for _ in 0..100 {
|
||||||
|
redraw(&mut h);
|
||||||
|
}
|
||||||
|
let allocations = COUNT.replace(None).unwrap();
|
||||||
|
println!("text: {allocations} allocations over 100 redraws of 8 texts");
|
||||||
|
assert_eq!(allocations, 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
//! CPU comparison of a bounds attribute against the `MaxSize` wrapper it
|
||||||
|
//! replaced, using the same builder calls and the same geometry. The wrapper
|
||||||
|
//! is gone from this tree, so its side of the comparison is run by checking
|
||||||
|
//! out a commit that still has it: the fixture is written to build the same
|
||||||
|
//! way at both.
|
||||||
|
//!
|
||||||
|
//! MODE=cap FRAMES=2000 cargo test --release --test bounds_cost \
|
||||||
|
//! -- --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! `MODE` is `plain`, `exact` or `cap`; `REDRAW=1` marks every widget for
|
||||||
|
//! redraw each frame; `FRAMES` is how many resize frames to measure. Use
|
||||||
|
//! repeated `perf stat -e instructions:u` runs on the executable directly.
|
||||||
|
//! Process totals include the cold frame, so compare identical modes and
|
||||||
|
//! frame counts. Wall time on this machine is not a stable comparison.
|
||||||
|
|
||||||
|
mod rig;
|
||||||
|
|
||||||
|
use iris::{harness::Harness, prelude::*};
|
||||||
|
use rig::env;
|
||||||
|
|
||||||
|
/// The two widths the loop alternates. The cap of 80 binds at 300 and does
|
||||||
|
/// not at 100, so the measured frames cross it in both directions.
|
||||||
|
const WIDTHS: [i32; 2] = [100, 300];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "instruction-count measurement"]
|
||||||
|
fn bounds_cost() {
|
||||||
|
let mode = env("MODE", "cap".to_string());
|
||||||
|
let redraw = env("REDRAW", 0_u8) != 0;
|
||||||
|
let frames = env("FRAMES", 2000_usize);
|
||||||
|
let mut h = Harness::new((300, 512));
|
||||||
|
let mut column = Span::empty(Dir::DOWN);
|
||||||
|
let mut leaves = Vec::new();
|
||||||
|
for _ in 0..128 {
|
||||||
|
let first = match mode.as_str() {
|
||||||
|
"plain" => rect(Color::RED).add_strong(&mut h.rsc).any(),
|
||||||
|
"exact" => rect(Color::RED).width(40).add_strong(&mut h.rsc).any(),
|
||||||
|
"cap" => rect(Color::RED).max_width(80).add_strong(&mut h.rsc).any(),
|
||||||
|
_ => panic!("unknown MODE {mode}"),
|
||||||
|
};
|
||||||
|
leaves.push(first.id());
|
||||||
|
let second = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let row = Span {
|
||||||
|
children: vec![first, second],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
};
|
||||||
|
column.push(row.height(4).add_strong(&mut h.rsc));
|
||||||
|
}
|
||||||
|
h.set_root(column);
|
||||||
|
let ids: Vec<_> = h.render.active.keys().copied().collect();
|
||||||
|
println!(
|
||||||
|
"mode={mode}, widgets={}, rule_bytes={}",
|
||||||
|
ids.len(),
|
||||||
|
std::mem::size_of::<SizeRule>()
|
||||||
|
);
|
||||||
|
// That the fixture measures what it says is checked on both sides of the
|
||||||
|
// crossing here rather than inside the measured loop, which is what the
|
||||||
|
// other rigs do. Per frame it measured only 0.65% of the total (5.780B
|
||||||
|
// against 5.743B instructions at MODE=cap, FRAMES=2000), but none of it
|
||||||
|
// is the layout the number is about.
|
||||||
|
for width in WIDTHS {
|
||||||
|
h.resize((width, 512));
|
||||||
|
h.frame();
|
||||||
|
let expected = match mode.as_str() {
|
||||||
|
"plain" => width / 2,
|
||||||
|
"exact" => 40,
|
||||||
|
"cap" => (width / 2).min(80),
|
||||||
|
_ => unreachable!("the mode was checked while building"),
|
||||||
|
};
|
||||||
|
for id in &leaves {
|
||||||
|
assert_eq!(h.region(id).unwrap().size().x, Px::from_int(expected));
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"width {width}: first leaf {}",
|
||||||
|
h.region(&leaves[0]).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for frame in 0..frames {
|
||||||
|
if redraw {
|
||||||
|
for &id in &ids {
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.resize((WIDTHS[frame % WIDTHS.len()], 512));
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
use std::{cell::Cell, rc::Rc};
|
||||||
|
|
||||||
|
use iris::harness::{Harness, assert_corners};
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
struct Counted {
|
||||||
|
draws: Rc<Cell<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Counted {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.draws.set(self.draws.get() + 1);
|
||||||
|
painter.px_size();
|
||||||
|
painter.primitive(RectPrimitive::color(Color::RED));
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
|
Some(LayoutLen::LEFTOVER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_capped_share_returns_room_to_its_sibling() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let first = rect(Color::RED).max_width(80).add(&mut h.rsc);
|
||||||
|
let second = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((first, second).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, first, (0, 0), (80, 100));
|
||||||
|
assert_corners!(h, second, (80, 0), (300, 100));
|
||||||
|
h.resize((100, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, first, (0, 0), (50, 100));
|
||||||
|
assert_corners!(h, second, (50, 0), (100, 100));
|
||||||
|
h.resize((300, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, first, (0, 0), (80, 100));
|
||||||
|
assert_corners!(h, second, (80, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_shares_are_discovered_without_provisional_paint() {
|
||||||
|
let mut h = Harness::new((400, 100));
|
||||||
|
let draws = Rc::new(Cell::new(0));
|
||||||
|
let leaf = h.rsc.ui_mut().widgets.add_strong(Counted {
|
||||||
|
draws: draws.clone(),
|
||||||
|
});
|
||||||
|
let leaf_id = leaf.id();
|
||||||
|
let mut inner: StrongWidget = leaf;
|
||||||
|
for _ in 0..8 {
|
||||||
|
let sibling = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
inner = h.rsc.ui_mut().widgets.add_strong(Span {
|
||||||
|
children: vec![inner, sibling],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
h.state.root = Some(inner);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(draws.get(), 1);
|
||||||
|
assert!(h.region(&leaf_id).is_some());
|
||||||
|
h.resize((800, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(draws.get(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_bounds_are_resolved_in_the_outer_allocation() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = rect(Color::RED).max_width(40).add(&mut h.rsc);
|
||||||
|
let b = rect(Color::GREEN).max_width(60).add(&mut h.rsc);
|
||||||
|
let inner = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((inner, tail).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, a, (0, 0), (40, 100));
|
||||||
|
assert_corners!(h, b, (40, 0), (100, 100));
|
||||||
|
assert_corners!(h, tail, (100, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_edits_in_a_nested_child_reach_the_allocator() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = rect(Color::RED).max_width(80).add(&mut h.rsc);
|
||||||
|
let inner = (a,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((inner, tail).span(Dir::RIGHT));
|
||||||
|
h.rsc.widgets_mut().set_max_len(a, Axis::X, Len::px(40.0));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, a, (0, 0), (40, 100));
|
||||||
|
assert_corners!(h, tail, (40, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adding_a_bound_to_a_previously_unbounded_share_reallocates_the_row() {
|
||||||
|
for hinted in [true, false] {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = if hinted {
|
||||||
|
rect(Color::RED).add_strong(&mut h.rsc).any()
|
||||||
|
} else {
|
||||||
|
h.rsc.widgets_mut().add_strong(Unhinted).any()
|
||||||
|
};
|
||||||
|
let id = a.id();
|
||||||
|
let b = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let mut row = Span::empty(Dir::RIGHT);
|
||||||
|
row.push(a);
|
||||||
|
row.push(b.add_strong(&mut h.rsc));
|
||||||
|
h.set_root(row);
|
||||||
|
h.resize((400, 100));
|
||||||
|
h.frame();
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(id, Axis::X, SizeRule::max(Len::px(80.0)));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, id, (0, 0), (80, 100));
|
||||||
|
assert_corners!(h, b, (80, 0), (400, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_deferred_comparison_can_compare_two_different_weights() {
|
||||||
|
let a = SizeRequest::from(leftover(1.0) + px(30.0)).min(leftover(2.0));
|
||||||
|
let b = SizeRequest::from(leftover(1.0)).clamp(px(20.0), px(100.0));
|
||||||
|
let mut h = Harness::new((60, 100));
|
||||||
|
let a = rect(Color::RED).width(a).add(&mut h.rsc);
|
||||||
|
let b = rect(Color::BLUE).width(b).add(&mut h.rsc);
|
||||||
|
h.set_root((a, b).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, a, (0, 0), (40, 100));
|
||||||
|
assert_corners!(h, b, (40, 0), (60, 100));
|
||||||
|
h.resize((300, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, a, (0, 0), (200, 100));
|
||||||
|
assert_corners!(h, b, (200, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_comparison_between_two_comparisons_keeps_both_of_them() {
|
||||||
|
// Joining two expressions is the one path that copies a request's nodes
|
||||||
|
// into another's arena; the floor puts a node under the copied one, so
|
||||||
|
// its operands have to be renumbered as they land.
|
||||||
|
let capped = leftover(1).min(px(40));
|
||||||
|
let floored = leftover(2).min(px(70)).max(px(10));
|
||||||
|
let mut h = Harness::new((90, 100));
|
||||||
|
let both = rect(Color::RED).width(capped.max(floored)).add(&mut h.rsc);
|
||||||
|
let rest = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((both, rest).span(Dir::RIGHT));
|
||||||
|
// Under either cap, so the doubled share is the longer of the two.
|
||||||
|
assert_corners!(h, both, (0, 0), (60, 100));
|
||||||
|
assert_corners!(h, rest, (60, 0), (90, 100));
|
||||||
|
h.resize((300, 100));
|
||||||
|
h.frame();
|
||||||
|
// Over both caps, so the comparison is between 40 and 70.
|
||||||
|
assert_corners!(h, both, (0, 0), (70, 100));
|
||||||
|
assert_corners!(h, rest, (70, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_length_expression_is_resolved_before_wrapping_text() {
|
||||||
|
let mut h = Harness::new((300, 500));
|
||||||
|
let text = wtext("one two three four five six seven eight nine ten")
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.width(leftover(1).clamp(40, 80))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let other = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((text, other).span(Dir::RIGHT));
|
||||||
|
let box_ = h.region(&text).unwrap();
|
||||||
|
assert_eq!(box_.top_left.x, Px::ZERO);
|
||||||
|
assert_eq!(box_.bot_right.x, Px::from_int(80));
|
||||||
|
assert!(box_.bot_right.y - box_.top_left.y > Px::from_int(30));
|
||||||
|
assert_corners!(h, other, (80, 0), (300, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_bounds_keep_the_allocators_base() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let head = rect(Color::BLUE).width(30).add(&mut h.rsc);
|
||||||
|
let bounded = rect(Color::RED)
|
||||||
|
.width(leftover(1).min(rel(0.25)))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
h.set_root((head, bounded, tail).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, bounded, (30, 0), (105, 100));
|
||||||
|
assert_corners!(h, tail, (105, 0), (300, 100));
|
||||||
|
h.resize((400, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, bounded, (30, 0), (130, 100));
|
||||||
|
assert_corners!(h, tail, (130, 0), (400, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_root_resolves_a_deferred_request_again_after_resize() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let bounded = rect(Color::RED)
|
||||||
|
.width(leftover(1).min(rel(0.25)))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(bounded);
|
||||||
|
assert_corners!(h, bounded, (112.5, 0), (187.5, 100));
|
||||||
|
h.resize((400, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, bounded, (150, 0), (250, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filling_a_stack_does_not_mean_its_sizing_child_was_already_allocated() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let child = rect(Color::RED).width(leftover(1).min(80)).add(&mut h.rsc);
|
||||||
|
let overlay = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let children: Vec<StrongWidget> =
|
||||||
|
vec![child.add_strong(&mut h.rsc), overlay.add_strong(&mut h.rsc)];
|
||||||
|
h.set_root(Stack {
|
||||||
|
children,
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
});
|
||||||
|
assert_corners!(h, child, (110, 0), (190, 100));
|
||||||
|
assert_corners!(h, overlay, (110, 0), (190, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Unhinted;
|
||||||
|
impl Widget for Unhinted {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.px_size();
|
||||||
|
painter.primitive(RectPrimitive::color(Color::RED));
|
||||||
|
Size::LEFTOVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bounds_also_apply_to_shares_discovered_by_drawing() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = h.rsc.widgets_mut().add_strong(Unhinted);
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(a.id(), Axis::X, SizeRule::max(Len::px(80.0)));
|
||||||
|
let id = a.id();
|
||||||
|
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let b_id = b.id();
|
||||||
|
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
|
||||||
|
children: vec![a, b],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, id, (0, 0), (80, 100));
|
||||||
|
assert_corners!(h, b_id, (80, 0), (300, 100));
|
||||||
|
h.resize((100, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, id, (0, 0), (50, 100));
|
||||||
|
assert_corners!(h, b_id, (50, 0), (100, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comparisons_with_a_known_order_remain_plain_lengths() {
|
||||||
|
assert_eq!(leftover(2).max(leftover(5)), SizeRequest::from(leftover(5)));
|
||||||
|
assert_eq!(leftover(2).min(leftover(5)), SizeRequest::from(leftover(2)));
|
||||||
|
assert_eq!(
|
||||||
|
(px(10) + rel(0.5)).max(px(30) + rel(0.5)),
|
||||||
|
SizeRequest::from(px(30) + rel(0.5))
|
||||||
|
);
|
||||||
|
assert_eq!(LayoutLen::px(10).clamp(20, 80), SizeRequest::from(20));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_measured_nested_share_keeps_its_comparison_for_the_outer_span() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = h.rsc.widgets_mut().add_strong(Unhinted);
|
||||||
|
let a_id = a.id();
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(a_id, Axis::X, SizeRule::max(Len::px(80.0)));
|
||||||
|
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let b_id = b.id();
|
||||||
|
let inner = h.rsc.widgets_mut().add_strong(Span {
|
||||||
|
children: vec![a, b],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
});
|
||||||
|
let c = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||||
|
let c_id = c.id();
|
||||||
|
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
|
||||||
|
children: vec![inner, c],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, a_id, (0, 0), (80, 100));
|
||||||
|
assert_corners!(h, b_id, (80, 0), (190, 100));
|
||||||
|
assert_corners!(h, c_id, (190, 0), (300, 100));
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.mark_for_redraw(h.state.root.as_ref().unwrap().id());
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, c_id, (190, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Natural;
|
||||||
|
impl Widget for Natural {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.primitive(RectPrimitive::color(Color::RED));
|
||||||
|
Size::from_axis(Axis::X, LayoutLen::px(64), LayoutLen::px(64))
|
||||||
|
}
|
||||||
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
|
Some(LayoutLen::px(64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_bounds_on_a_hinted_child_track_the_offer_before_its_declared_size() {
|
||||||
|
fn tree(h: &mut Harness) -> WidgetId {
|
||||||
|
let natural = h.rsc.widgets_mut().add_strong(Natural);
|
||||||
|
let id = natural.id();
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(id, Axis::X, SizeRule::max(Len::rel(0.75)));
|
||||||
|
h.rsc.widgets_mut().set_size_rule(
|
||||||
|
id,
|
||||||
|
Axis::Y,
|
||||||
|
SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)),
|
||||||
|
);
|
||||||
|
let inner = h.rsc.widgets_mut().add_strong(Stack {
|
||||||
|
children: vec![natural],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
});
|
||||||
|
let inner_id = inner.id();
|
||||||
|
let fill = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let overlay = h.rsc.widgets_mut().add_strong(Stack {
|
||||||
|
children: vec![fill, inner],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
});
|
||||||
|
let share = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||||
|
let bounded = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_size_rule(
|
||||||
|
bounded.id(),
|
||||||
|
Axis::X,
|
||||||
|
SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)),
|
||||||
|
);
|
||||||
|
let fixed = wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
|
||||||
|
children: vec![share, bounded, fixed, overlay],
|
||||||
|
dir: Dir::LEFT,
|
||||||
|
gap: Px::from_int(8),
|
||||||
|
}));
|
||||||
|
h.rsc.widgets_mut().set_size_rule(
|
||||||
|
h.state.root.as_ref().unwrap().id(),
|
||||||
|
Axis::Y,
|
||||||
|
LayoutLen::rel(1).into(),
|
||||||
|
);
|
||||||
|
h.frame();
|
||||||
|
inner_id
|
||||||
|
}
|
||||||
|
let mut warm = Harness::new((1920, 1200));
|
||||||
|
let a = tree(&mut warm);
|
||||||
|
warm.resize((640, 900));
|
||||||
|
warm.frame();
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let b = tree(&mut cold);
|
||||||
|
assert_eq!(warm.region(&a), cold.region(&b));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bound_can_extend_an_explicit_share_request() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let a = rect(Color::RED)
|
||||||
|
.width(leftover(1))
|
||||||
|
.min_width(100)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let b = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((a, b).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, a, (0, 0), (150, 100));
|
||||||
|
h.resize((120, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, a, (0, 0), (100, 100));
|
||||||
|
assert_corners!(h, b, (100, 0), (120, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn moving_scroll_content_preserves_its_resolved_expression_size() {
|
||||||
|
let mut h = Harness::new((300, 300));
|
||||||
|
let leaf = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let leaf_id = leaf.id();
|
||||||
|
let content = h.rsc.widgets_mut().add_strong(Stack {
|
||||||
|
children: vec![leaf],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
});
|
||||||
|
let content_id = content.id();
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(content_id, Axis::Y, leftover(1).min(120).into());
|
||||||
|
let scroll = h
|
||||||
|
.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.add_strong(Scroll::new(content, Axis::Y));
|
||||||
|
let scroll_id = scroll.id();
|
||||||
|
h.state.root = Some(scroll);
|
||||||
|
for _ in 0..3 {
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(scroll_id);
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, content_id, (0, 90), (300, 210));
|
||||||
|
assert_corners!(h, leaf_id, (0, 90), (300, 210));
|
||||||
|
}
|
||||||
|
h.resize((300, 600));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, leaf_id, (0, 240), (300, 360));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_intrinsic_share_cap_uses_the_allocators_fractional_base() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let head = rect(Color::BLUE).width(30).add(&mut h.rsc);
|
||||||
|
let bounded = rect(Color::RED).max_width(Len::rel(0.25)).add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
h.set_root((head, bounded, tail).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, bounded, (30, 0), (105, 100));
|
||||||
|
assert_corners!(h, tail, (105, 0), (300, 100));
|
||||||
|
h.resize((400, 100));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, bounded, (30, 0), (130, 100));
|
||||||
|
assert_corners!(h, tail, (130, 0), (400, 100));
|
||||||
|
}
|
||||||
@@ -69,8 +69,8 @@ fn a_branch_taken_on_a_measurement_holds_across_repaints() {
|
|||||||
assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
|
assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
|
||||||
|
|
||||||
for frame in 0..4 {
|
for frame in 0..4 {
|
||||||
h.rsc.widgets_mut().get_dyn_mut(wide);
|
h.rsc.widgets_mut().mark_for_redraw(wide);
|
||||||
h.rsc.widgets_mut().get_dyn_mut(narrow);
|
h.rsc.widgets_mut().mark_for_redraw(narrow);
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
taken(&h, wide, narrow),
|
taken(&h, wide, narrow),
|
||||||
@@ -88,7 +88,7 @@ fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
|
|||||||
let (wide, narrow) = plant(&mut warm, threshold);
|
let (wide, narrow) = plant(&mut warm, threshold);
|
||||||
warm.resize((640, 480));
|
warm.resize((640, 480));
|
||||||
warm.frame();
|
warm.frame();
|
||||||
warm.rsc.widgets_mut().get_dyn_mut(wide);
|
warm.rsc.widgets_mut().mark_for_redraw(wide);
|
||||||
warm.frame();
|
warm.frame();
|
||||||
|
|
||||||
let mut cold = Harness::new((640, 480));
|
let mut cold = Harness::new((640, 480));
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ fn a_wrapping_text_in_a_span_settles_on_one_width() {
|
|||||||
let r = h.region(&t.id()).unwrap();
|
let r = h.region(&t.id()).unwrap();
|
||||||
widths.push(r.bot_right.x - r.top_left.x);
|
widths.push(r.bot_right.x - r.top_left.x);
|
||||||
// Redrawing it changes nothing about the state, so nothing may move.
|
// Redrawing it changes nothing about the state, so nothing may move.
|
||||||
h.rsc.widgets_mut().get_dyn_mut(t.id());
|
h.rsc.widgets_mut().mark_for_redraw(t.id());
|
||||||
h.frame();
|
h.frame();
|
||||||
}
|
}
|
||||||
println!("widths over six frames: {widths:?}");
|
println!("widths over six frames: {widths:?}");
|
||||||
|
|||||||
+470
-2
@@ -1,5 +1,7 @@
|
|||||||
//! Where a frame puts things, with no window to put them in.
|
//! Where a frame puts things, with no window to put them in.
|
||||||
|
|
||||||
|
use std::{cell::Cell, rc::Rc};
|
||||||
|
|
||||||
use iris::harness::{Harness, assert_corners};
|
use iris::harness::{Harness, assert_corners};
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
|
|
||||||
@@ -219,6 +221,134 @@ fn an_empty_widget_takes_a_share_of_a_span() {
|
|||||||
assert_corners!(h, right, (300, 0), (400, 200));
|
assert_corners!(h, right, (300, 0), (400, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A widget with a natural pixel size, like an image, which records the box
|
||||||
|
/// it was asked in so a test can see which length decided it.
|
||||||
|
struct NaturalSize {
|
||||||
|
len: f32,
|
||||||
|
asked: Rc<Cell<f32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for NaturalSize {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.asked.set(painter.px_len(Axis::X).to_f32());
|
||||||
|
Size::px(Vec2::new(self.len, self.len))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
|
||||||
|
Some(LayoutLen::px(self.len))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rule wins over what the widget says about itself, and a share is a rule:
|
||||||
|
/// it is a length only to whoever divides one, and nobody here does, so the
|
||||||
|
/// widget is asked in the whole box rather than in the size it asked for.
|
||||||
|
#[test]
|
||||||
|
fn a_share_rule_beats_the_widgets_own_pixel_size() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let asked = Rc::new(Cell::new(0.0));
|
||||||
|
let natural = NaturalSize {
|
||||||
|
len: 50.0,
|
||||||
|
asked: asked.clone(),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(natural.wrapper());
|
||||||
|
assert_eq!(asked.get(), 50.0, "its hint gives it its own size");
|
||||||
|
|
||||||
|
h.set_len(natural, Axis::X, LayoutLen::LEFTOVER);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(asked.get(), 400.0, "the share is all of the box");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every box a widget is given comes of one ask, and the window is one of
|
||||||
|
/// them: the root is asked in it exactly as a child is asked in its parent's
|
||||||
|
/// box, so a rule of its own reads the same way at either place.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum Asked {
|
||||||
|
Root,
|
||||||
|
Wrapped,
|
||||||
|
InASpan,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Asked {
|
||||||
|
const ALL: [Self; 3] = [Self::Root, Self::Wrapped, Self::InASpan];
|
||||||
|
|
||||||
|
/// The width the probe is given under this parent, in a 400 px window.
|
||||||
|
fn width(&self, rule: LayoutLen) -> Px {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
h.set_len(probe, Axis::X, rule);
|
||||||
|
match self {
|
||||||
|
Self::Root => h.set_root(probe),
|
||||||
|
Self::Wrapped => h.set_root(probe.wrapper()),
|
||||||
|
Self::InASpan => h.set_root((probe,).span(Dir::RIGHT)),
|
||||||
|
}
|
||||||
|
h.region(&probe).unwrap().size().x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A share with pixels or a fraction beside it is the longer of the two: it
|
||||||
|
/// fills what they leave of the box and overflows the box where they are
|
||||||
|
/// longer than it. A parent that divides nothing gives the same length as a
|
||||||
|
/// span with one child, because in both there is nobody else to divide with --
|
||||||
|
/// and so does the window, which divides nothing either.
|
||||||
|
#[test]
|
||||||
|
fn a_share_is_a_minimum_wherever_nothing_divides_it() {
|
||||||
|
for (rule, want) in [
|
||||||
|
(LayoutLen::LEFTOVER, 400),
|
||||||
|
(LayoutLen::px(50.0) + LayoutLen::LEFTOVER, 400),
|
||||||
|
(LayoutLen::px(500.0) + LayoutLen::LEFTOVER, 500),
|
||||||
|
(LayoutLen::rel(0.5) + LayoutLen::LEFTOVER, 400),
|
||||||
|
(LayoutLen::rel(2.0) + LayoutLen::LEFTOVER, 800),
|
||||||
|
(LayoutLen::px(500.0), 500),
|
||||||
|
] {
|
||||||
|
let want = Px::from_int(want);
|
||||||
|
for asked in Asked::ALL {
|
||||||
|
assert_eq!(asked.width(rule), want, "{rule:?} asked {asked:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which of the two is longer is a question in pixels, so the box is decided
|
||||||
|
/// again wherever the answer can change: a window that crosses the length the
|
||||||
|
/// pixels ask for, and the rule itself crossing it while the window holds
|
||||||
|
/// still. The first is a range the drawing holds for; the second cannot be
|
||||||
|
/// seen in what the widget declares, since a share declares nothing either
|
||||||
|
/// way, so it reaches the parent as a length only the parent can resolve.
|
||||||
|
#[test]
|
||||||
|
fn a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing() {
|
||||||
|
// At the root as well as under a parent: the comparison is the same one,
|
||||||
|
// and nothing above the root will make it again on its behalf, so the
|
||||||
|
// range it holds for is the root's own.
|
||||||
|
for wrapped in [false, true] {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
h.set_len(probe, Axis::X, LayoutLen::px(500.0) + LayoutLen::LEFTOVER);
|
||||||
|
match wrapped {
|
||||||
|
true => h.set_root(probe.wrapper()),
|
||||||
|
false => h.set_root(probe),
|
||||||
|
}
|
||||||
|
let width = |h: &Harness| h.region(&probe).unwrap().size().x;
|
||||||
|
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
|
||||||
|
|
||||||
|
h.resize((900, 200));
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(width(&h), Px::from_int(900), "wrapped: {wrapped}");
|
||||||
|
|
||||||
|
h.resize((400, 200));
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
|
||||||
|
|
||||||
|
h.set_len(probe, Axis::X, LayoutLen::px(50.0) + LayoutLen::LEFTOVER);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(width(&h), Px::from_int(400), "wrapped: {wrapped}");
|
||||||
|
|
||||||
|
h.set_len(probe, Axis::X, LayoutLen::px(500.0) + LayoutLen::LEFTOVER);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_child_drawn_twice_moves_once() {
|
fn a_child_drawn_twice_moves_once() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
@@ -626,7 +756,7 @@ fn only_a_pure_leftover_child_disappears_when_nothing_is_left() {
|
|||||||
let mut h = Harness::new((100, 20));
|
let mut h = Harness::new((100, 20));
|
||||||
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
|
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
let mixed = rect(Color::BLUE)
|
let mixed = rect(Color::BLUE)
|
||||||
.width(LayoutLen::px(20) + LayoutLen::LEFTOVER)
|
.width(LayoutLen::px(20.0) + LayoutLen::LEFTOVER)
|
||||||
.add(&mut h.rsc);
|
.add(&mut h.rsc);
|
||||||
h.set_root((fixed, mixed).span(Dir::RIGHT));
|
h.set_root((fixed, mixed).span(Dir::RIGHT));
|
||||||
|
|
||||||
@@ -694,7 +824,7 @@ fn row_under_fractions(kid: Option<LayoutLen>, gap: f32, box_w: f32) -> (Vec<Px>
|
|||||||
if let Some(len) = kid {
|
if let Some(len) = kid {
|
||||||
h.rsc
|
h.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
.set_size_rule(r.id(), Axis::X, SizeRule::Exact(len));
|
.set_size_rule(r.id(), Axis::X, SizeRule::from(len));
|
||||||
}
|
}
|
||||||
ids.push(r.id());
|
ids.push(r.id());
|
||||||
kids.push(r.add_strong(&mut h.rsc));
|
kids.push(r.add_strong(&mut h.rsc));
|
||||||
@@ -858,3 +988,341 @@ fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The root is asked the way any child is, so what it says about itself is
|
||||||
|
/// read there too: a root that opted into a region node gets one, where the
|
||||||
|
/// path it used to have ignored the flag.
|
||||||
|
#[test]
|
||||||
|
fn a_region_node_root_is_a_region_node() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let root = (probe,).span(Dir::RIGHT).region_node().add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(400));
|
||||||
|
|
||||||
|
h.resize((900, 200));
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bound holds the length a widget reports as well as narrowing the box it
|
||||||
|
/// is offered, and the two are not the same question. Here two 200-wide rects
|
||||||
|
/// fill a row in a 250 window, so a cap of 300 leaves the box alone and only
|
||||||
|
/// cuts what the row reports -- which the window then centres, past both its
|
||||||
|
/// edges -- while a floor raises the report and the rects stay where the 250
|
||||||
|
/// box put them.
|
||||||
|
#[test]
|
||||||
|
fn a_bound_holds_what_a_widget_answers() {
|
||||||
|
let bounded_row = |rule: SizeRule| {
|
||||||
|
let mut h = Harness::new((250, 200));
|
||||||
|
let left = rect(Color::RED).width(200).add(&mut h.rsc);
|
||||||
|
let right = rect(Color::BLUE).width(200).add(&mut h.rsc);
|
||||||
|
let row = (left, right).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_size_rule(row, Axis::X, rule);
|
||||||
|
h.set_root(row);
|
||||||
|
(h, row, left)
|
||||||
|
};
|
||||||
|
let (h, row, left) = bounded_row(SizeRule::max(Len::px(300.0)));
|
||||||
|
assert_eq!(
|
||||||
|
h.region(&row).unwrap().size().x,
|
||||||
|
Px::from_int(300),
|
||||||
|
"the cap, not the 400 drawn"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
h.region(&left).unwrap().size().x,
|
||||||
|
Px::from_int(200),
|
||||||
|
"the box the children were given"
|
||||||
|
);
|
||||||
|
assert_corners!(h, row, (-25, 0), (275, 200));
|
||||||
|
|
||||||
|
let (h, row, _) = bounded_row(SizeRule::min(Len::px(600.0)));
|
||||||
|
assert_eq!(
|
||||||
|
h.region(&row).unwrap().size().x,
|
||||||
|
Px::from_int(600),
|
||||||
|
"the floor, not the 400 drawn"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (h, row, _) = bounded_row(SizeRule::FREE);
|
||||||
|
assert_eq!(
|
||||||
|
h.region(&row).unwrap().size().x,
|
||||||
|
Px::from_int(400),
|
||||||
|
"what it drew"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cap narrows the box the widget is asked in, whether a declaration of its
|
||||||
|
/// own decides that box or the allocator divides a share into it. The cap is
|
||||||
|
/// an attribute of the widget rather than something wrapped around it, which
|
||||||
|
/// is what the id assertions say.
|
||||||
|
#[test]
|
||||||
|
fn a_cap_attribute_narrows_the_widgets_box() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// A fraction of its box, so it says what box it was asked in.
|
||||||
|
let fills = rect(Color::RED).width(rel(1.0)).add(&mut h.rsc);
|
||||||
|
let capped = fills.max_width(300).add(&mut h.rsc);
|
||||||
|
assert_eq!(fills.id(), capped.id());
|
||||||
|
h.set_root(capped);
|
||||||
|
assert_eq!(h.region(&fills).unwrap().size().x, Px::from_int(300));
|
||||||
|
|
||||||
|
// A share with nothing beside it: the cap is composed into the request
|
||||||
|
// and the allocator answers with it rather than the whole 400.
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let share = rect(Color::RED).width(leftover(1)).add(&mut h.rsc);
|
||||||
|
let capped = share.max_width(300).add(&mut h.rsc);
|
||||||
|
assert_eq!(share.id(), capped.id());
|
||||||
|
h.set_root(capped);
|
||||||
|
assert_eq!(h.region(&share).unwrap().size().x, Px::from_int(300));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_cap_attribute_is_decided_again_on_either_side_of_the_crossing() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
h.set_root(probe.max_width(300));
|
||||||
|
let width = |h: &Harness| h.region(&probe).unwrap().size().x;
|
||||||
|
assert_eq!(width(&h), Px::from_int(300));
|
||||||
|
|
||||||
|
h.resize((250, 200));
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(
|
||||||
|
width(&h),
|
||||||
|
Px::from_int(250),
|
||||||
|
"its box, which is under the cap"
|
||||||
|
);
|
||||||
|
|
||||||
|
h.resize((400, 200));
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(width(&h), Px::from_int(300));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fraction in a cap is a fraction of the box the widget capping it was
|
||||||
|
/// given, which is the box a declared length of its own would be a fraction
|
||||||
|
/// of -- not of the window, and not of what the cap itself decided.
|
||||||
|
#[test]
|
||||||
|
fn a_cap_is_a_fraction_of_the_box_it_was_given() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
h.set_root(probe.max_width(Len::rel(0.5)).pad(Padding::uniform(50)));
|
||||||
|
|
||||||
|
// Half of the 300 left by the padding, not half of the window.
|
||||||
|
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(150));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Offered {
|
||||||
|
seen: Rc<Cell<PxVec2>>,
|
||||||
|
answer: Size,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Offered {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.seen.set(painter.px_size());
|
||||||
|
painter.primitive(RectPrimitive::color(Color::RED));
|
||||||
|
self.answer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bounds_constrain_the_offer_without_replacing_an_intrinsic_answer() {
|
||||||
|
for axis in Axis::BOTH {
|
||||||
|
for node in [false, true] {
|
||||||
|
let mut h = Harness::new((400, 400));
|
||||||
|
let seen = Rc::new(Cell::new(PxVec2::ZERO));
|
||||||
|
let probe = h.rsc.widgets_mut().add_strong(Offered {
|
||||||
|
seen: seen.clone(),
|
||||||
|
answer: Size::px(Vec2::new(40.0, 40.0)),
|
||||||
|
});
|
||||||
|
let id = probe.id();
|
||||||
|
h.rsc.widgets_mut().set_region_node(id, node);
|
||||||
|
h.rsc.widgets_mut().set_max_len(id, axis, 100.into());
|
||||||
|
h.state.root = Some(probe);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(seen.get()[axis], Px::from_int(100));
|
||||||
|
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40));
|
||||||
|
h.rsc.widgets_mut().set_min_len(id, axis, 60.into());
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(60));
|
||||||
|
h.resize((50, 50));
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(seen.get()[axis], Px::from_int(60));
|
||||||
|
h.rsc.widgets_mut().set_size_rule(id, axis, SizeRule::FREE);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(seen.get()[axis], Px::from_int(50));
|
||||||
|
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_cap_attribute_is_the_scroll_viewport() {
|
||||||
|
for node in [false, true] {
|
||||||
|
let mut h = Harness::new((400, 400));
|
||||||
|
let content = rect(Color::RED).height(400).add(&mut h.rsc);
|
||||||
|
let inner = content.add_strong(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(inner, Axis::Y).max_height(100).add(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_region_node(scroll, node);
|
||||||
|
h.set_root(scroll);
|
||||||
|
assert_corners!(h, scroll, (0, 150), (400, 250));
|
||||||
|
assert_corners!(h, content, (0, -150), (400, 250));
|
||||||
|
h.rsc.widgets_mut().get_mut(&scroll).unwrap().scroll(1000.0);
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, content, (0, 150), (400, 550));
|
||||||
|
h.resize((400, 80));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, scroll, (0, 0), (400, 80));
|
||||||
|
assert_corners!(h, content, (0, 0), (400, 400));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_cap_attribute_wraps_text_before_it_answers() {
|
||||||
|
let mut h = Harness::new((400, 500));
|
||||||
|
let text = wtext("one two three four five six seven eight nine ten")
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.max_width(80)
|
||||||
|
.align(Align::TOP_LEFT)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(text);
|
||||||
|
let capped = h.region(&text).unwrap().size();
|
||||||
|
assert!(capped.x <= Px::from_int(80));
|
||||||
|
assert!(capped.y > Px::from_int(30));
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_size_rule(text, Axis::X, SizeRule::FREE);
|
||||||
|
h.frame();
|
||||||
|
let free = h.region(&text).unwrap().size();
|
||||||
|
assert!(free.x > capped.x);
|
||||||
|
assert!(free.y < capped.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dimensions_and_bounds_are_independent_attributes_in_either_order() {
|
||||||
|
for bounds_first in [false, true] {
|
||||||
|
let mut h = Harness::new((400, 400));
|
||||||
|
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let bounded = if bounds_first {
|
||||||
|
probe
|
||||||
|
.max_width(80)
|
||||||
|
.min_height(60)
|
||||||
|
.width(120)
|
||||||
|
.height(40)
|
||||||
|
.add(&mut h.rsc)
|
||||||
|
} else {
|
||||||
|
probe
|
||||||
|
.width(120)
|
||||||
|
.height(40)
|
||||||
|
.max_width(80)
|
||||||
|
.min_height(60)
|
||||||
|
.add(&mut h.rsc)
|
||||||
|
};
|
||||||
|
assert_eq!(probe.id(), bounded.id());
|
||||||
|
h.set_root(bounded);
|
||||||
|
assert_eq!(
|
||||||
|
h.region(&probe).unwrap().size(),
|
||||||
|
PxVec2::from_f32((80, 60).into())
|
||||||
|
);
|
||||||
|
h.rsc.widgets_mut().set_len(probe, Axis::X, 50);
|
||||||
|
h.rsc.widgets_mut().set_len(probe, Axis::Y, 100);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(
|
||||||
|
h.region(&probe).unwrap().size(),
|
||||||
|
PxVec2::from_f32((50, 100).into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changing_a_share_cap_replaces_it_without_losing_the_share() {
|
||||||
|
let mut h = Harness::new((300, 100));
|
||||||
|
let capped = rect(Color::RED)
|
||||||
|
.max_width(80)
|
||||||
|
.width(leftover(1))
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let sibling = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((capped, sibling).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, capped, (0, 0), (80, 100));
|
||||||
|
assert_corners!(h, sibling, (80, 0), (300, 100));
|
||||||
|
h.rsc.widgets_mut().set_max_len(capped, Axis::X, 160.into());
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, capped, (0, 0), (150, 100));
|
||||||
|
assert_corners!(h, sibling, (150, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduced from seed 104 at depth 5: a widget widening its own window
|
||||||
|
// contract must not erase the bound's crossing at a quarter-window of 173.
|
||||||
|
#[test]
|
||||||
|
fn a_widgets_window_contract_cannot_widen_its_bounds_contract() {
|
||||||
|
fn tree(h: &mut Harness) -> WidgetId {
|
||||||
|
let content = rect(Color::RED)
|
||||||
|
.width(137)
|
||||||
|
.min_height(194)
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(content, Axis::X).add_strong(&mut h.rsc);
|
||||||
|
let probe = rect(Color::RED).add_strong(&mut h.rsc);
|
||||||
|
let id = probe.id();
|
||||||
|
let wide = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||||
|
let narrow = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let branch = iris::random::Branch {
|
||||||
|
probe,
|
||||||
|
wide,
|
||||||
|
narrow,
|
||||||
|
threshold: 459.0,
|
||||||
|
}
|
||||||
|
.min_width(94)
|
||||||
|
.max_width(173)
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
let stack = Stack {
|
||||||
|
children: vec![scroll, branch],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
let mut children: Vec<StrongWidget> = (0..3)
|
||||||
|
.map(|_| rect(Color::RED).add_strong(&mut h.rsc).any())
|
||||||
|
.collect();
|
||||||
|
children.push(stack);
|
||||||
|
h.set_root(
|
||||||
|
Span {
|
||||||
|
children,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.height(rel(1)),
|
||||||
|
);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
let mut warm = Harness::new((1920, 1200));
|
||||||
|
let probe = tree(&mut warm);
|
||||||
|
warm.resize((640, 900));
|
||||||
|
warm.frame();
|
||||||
|
assert_corners!(warm, probe, (480, 0), (640, 40));
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let other = tree(&mut cold);
|
||||||
|
assert_eq!(warm.region(&probe), cold.region(&other));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fixed_declaration_keeps_its_cap_when_the_row_has_no_leftover() {
|
||||||
|
let mut h = Harness::new((100, 100));
|
||||||
|
let fixed = rect(Color::RED).width(200).max_width(100).add(&mut h.rsc);
|
||||||
|
let share = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((fixed, share).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, fixed, (0, 0), (100, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_new_bound_reaches_the_parent_even_when_the_current_answer_is_unchanged() {
|
||||||
|
let mut h = Harness::new((400, 100));
|
||||||
|
let leaf = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let inner = leaf.add_strong(&mut h.rsc);
|
||||||
|
let root = Scroll::new(inner, Axis::Y)
|
||||||
|
.pad(Padding::uniform(0))
|
||||||
|
.width(154)
|
||||||
|
.height(100)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
assert_eq!(h.region(&leaf).unwrap().size().x, Px::from_int(154));
|
||||||
|
h.rsc.widgets_mut().set_min_len(leaf, Axis::X, 140.into());
|
||||||
|
h.rsc.widgets_mut().set_len(root, Axis::X, 78);
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, leaf, (130, 0), (270, 100));
|
||||||
|
}
|
||||||
+44
-9
@@ -5,7 +5,9 @@
|
|||||||
//! and the oracle another. And reducing a plan has to end, or a shrinker
|
//! and the oracle another. And reducing a plan has to end, or a shrinker
|
||||||
//! searching for the smallest counterexample never returns.
|
//! searching for the smallest counterexample never returns.
|
||||||
|
|
||||||
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, plan};
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, grow, plan};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
fn some_edits(seed: u64, of: &Plan) -> Edits {
|
fn some_edits(seed: u64, of: &Plan) -> Edits {
|
||||||
@@ -25,11 +27,27 @@ fn some_edits(seed: u64, of: &Plan) -> Edits {
|
|||||||
Edits {
|
Edits {
|
||||||
sizes: pick(sized, &mut rng)
|
sizes: pick(sized, &mut rng)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|i| (i, [Some(LayoutLen::LEFTOVER), None]))
|
.map(|i| {
|
||||||
|
(
|
||||||
|
i,
|
||||||
|
SizeRules {
|
||||||
|
x: SizeRule::from(LayoutLen::LEFTOVER),
|
||||||
|
y: SizeRule::FREE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
aligns: pick(aligned, &mut rng)
|
aligns: pick(aligned, &mut rng)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|i| (i, [Some(AxisAlign::POS), None]))
|
.map(|i| {
|
||||||
|
(
|
||||||
|
i,
|
||||||
|
Align {
|
||||||
|
x: Some(AxisAlign::POS),
|
||||||
|
y: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
nodes: pick(nodes, &mut rng)
|
nodes: pick(nodes, &mut rng)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -51,8 +69,6 @@ fn some_edits(seed: u64, of: &Plan) -> Edits {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
/// The two routes to an edited tree are one tree. `plan` resolves edits out
|
/// The two routes to an edited tree are one tree. `plan` resolves edits out
|
||||||
/// of the random stream as it draws; `edited` puts them on a tree that
|
/// of the random stream as it draws; `edited` puts them on a tree that
|
||||||
/// already exists, which is the only route a shrunk plan has, since no seed
|
/// already exists, which is the only route a shrunk plan has, since no seed
|
||||||
@@ -70,11 +86,13 @@ fn editing_a_plan_is_growing_one_with_those_edits() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every simplification is strictly smaller, so taking them in turn reaches a
|
/// No simplification is larger, which is the half of "the shrinker stops" a
|
||||||
/// fixed point instead of circling. A shrinker that can return to a tree it
|
/// widget count can see. Most are not smaller either -- a dropped alignment
|
||||||
/// has already tried does not stop.
|
/// and a simpler leaf both keep the count -- so what rules out circling is
|
||||||
|
/// that those are one-way too: a `Some` becomes a `None`, and a kind steps
|
||||||
|
/// down a ladder with no way back up.
|
||||||
#[test]
|
#[test]
|
||||||
fn every_simplification_of_a_plan_is_smaller_than_it() {
|
fn no_simplification_of_a_plan_is_larger_than_it() {
|
||||||
for seed in 1..=60 {
|
for seed in 1..=60 {
|
||||||
let tree = plan(seed, 4, &Edits::default());
|
let tree = plan(seed, 4, &Edits::default());
|
||||||
let mut queue = vec![tree];
|
let mut queue = vec![tree];
|
||||||
@@ -119,3 +137,20 @@ fn reducing_a_plan_all_the_way_ends() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every image in a tree is the same picture, and a handle is a reference to
|
||||||
|
/// the texture rather than a copy of it, so one upload and one slot serve all
|
||||||
|
/// of them however many a tree grows -- and the trees are grown in hundreds.
|
||||||
|
#[test]
|
||||||
|
fn a_tree_of_images_uploads_one_texture() {
|
||||||
|
let mut images = 0;
|
||||||
|
let mut tree = plan(1, 4, &Edits::default());
|
||||||
|
tree.walk_mut(&mut |p| images += (p.kind == Kind::Image) as usize);
|
||||||
|
assert!(images > 1, "a tree of {images} images tests nothing");
|
||||||
|
|
||||||
|
let mut h = Harness::new((900, 1200));
|
||||||
|
let (root, _) = grow(&mut h.rsc, 1, 4, &Edits::default());
|
||||||
|
h.state.root = Some(root);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(h.rsc.ui().textures.count(), 1);
|
||||||
|
}
|
||||||
+97
-40
@@ -45,7 +45,6 @@ fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>
|
|||||||
|
|
||||||
struct Layered {
|
struct Layered {
|
||||||
children: [StrongWidget<Rect>; 2],
|
children: [StrongWidget<Rect>; 2],
|
||||||
_revision: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Layered {
|
impl Widget for Layered {
|
||||||
@@ -65,14 +64,10 @@ fn a_redrawn_layered_widget_keeps_the_layer_it_was_entered_on() {
|
|||||||
rect(Color::RED).add_strong(&mut h.rsc),
|
rect(Color::RED).add_strong(&mut h.rsc),
|
||||||
rect(Color::BLUE).add_strong(&mut h.rsc),
|
rect(Color::BLUE).add_strong(&mut h.rsc),
|
||||||
];
|
];
|
||||||
let root = Layered {
|
let root = Layered { children }.add(&mut h.rsc);
|
||||||
children,
|
|
||||||
_revision: 0,
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(root);
|
h.set_root(root);
|
||||||
|
|
||||||
h.rsc[root]._revision += 1;
|
h.rsc.widgets_mut().mark_for_redraw(root.id());
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
let label = h.rsc.widgets().label(root.id());
|
let label = h.rsc.widgets().label(root.id());
|
||||||
@@ -181,9 +176,9 @@ fn a_repaint_that_keeps_its_size_does_not_relay_out() {
|
|||||||
h.set_root((first, second).span(Dir::RIGHT));
|
h.set_root((first, second).span(Dir::RIGHT));
|
||||||
let settled = draws.get();
|
let settled = draws.get();
|
||||||
|
|
||||||
// Taking mutable access is the ordinary content-change signal. This
|
// Marked with nothing about it changed, and it reports the same size
|
||||||
// widget returns the same size, so the parent has nothing to lay out.
|
// either way, so the parent has nothing to lay out.
|
||||||
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
|
h.rsc.widgets_mut().mark_for_redraw(first.id());
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
assert_eq!(draws.get(), settled + 1);
|
||||||
@@ -198,7 +193,7 @@ fn a_span_child_survives_the_next_frame() {
|
|||||||
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
|
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
|
||||||
h.set_root((top, bottom).span(Dir::DOWN));
|
h.set_root((top, bottom).span(Dir::DOWN));
|
||||||
|
|
||||||
h.rsc.widgets_mut().get_dyn_mut(top.id());
|
h.rsc.widgets_mut().mark_for_redraw(top.id());
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
assert_corners!(h, top, (0, 0), (400, 80));
|
assert_corners!(h, top, (0, 0), (400, 80));
|
||||||
@@ -660,7 +655,7 @@ fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
|
|||||||
let masked = inner.masked().add(&mut h.rsc);
|
let masked = inner.masked().add(&mut h.rsc);
|
||||||
let other = rect(Color::RED).width(100).add(&mut h.rsc);
|
let other = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
h.set_root((other, masked).span(Dir::RIGHT));
|
h.set_root((other, masked).span(Dir::RIGHT));
|
||||||
h.rsc.widgets_mut().get_dyn_mut(masked.id());
|
h.rsc.widgets_mut().mark_for_redraw(masked.id());
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_corners!(h, inner, (100, 0), (400, 200));
|
assert_corners!(h, inner, (100, 0), (400, 200));
|
||||||
}
|
}
|
||||||
@@ -774,6 +769,17 @@ fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where a mask slot clips, in window pixels: the region it holds, carried
|
||||||
|
/// through whatever move entry it hangs from. Taken by slot rather than by
|
||||||
|
/// widget, so a test can name the slot it expects a redraw to keep.
|
||||||
|
fn mask_bounds(h: &Harness, mask: MaskIdx) -> PixelRegion {
|
||||||
|
let mask = &h.rsc.ui().masks[mask.idx()];
|
||||||
|
h.render
|
||||||
|
.moves
|
||||||
|
.resolve(mask.move_idx, mask.region)
|
||||||
|
.to_px(h.render.output_size())
|
||||||
|
}
|
||||||
|
|
||||||
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
|
fn primitive_bounds(h: &Harness, id: WidgetId) -> Vec<PixelRegion> {
|
||||||
h.render.active[&id]
|
h.render.active[&id]
|
||||||
.primitives
|
.primitives
|
||||||
@@ -919,15 +925,10 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() {
|
|||||||
primitive_bounds(&warm, leaf.id()),
|
primitive_bounds(&warm, leaf.id()),
|
||||||
primitive_bounds(&cold, other.id())
|
primitive_bounds(&cold, other.id())
|
||||||
);
|
);
|
||||||
let mask = |h: &Harness, id: WidgetId| {
|
assert_eq!(
|
||||||
let active = &h.render.active[&id];
|
mask_bounds(&warm, warm.render.active[&leaf.id()].mask),
|
||||||
let mask = &h.rsc.ui().masks[active.mask.idx()];
|
mask_bounds(&cold, cold.render.active[&other.id()].mask)
|
||||||
h.render
|
);
|
||||||
.moves
|
|
||||||
.resolve(mask.move_idx, mask.region)
|
|
||||||
.to_px(h.render.output_size())
|
|
||||||
};
|
|
||||||
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -994,7 +995,7 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() {
|
|||||||
assert_eq!(draws.get(), before);
|
assert_eq!(draws.get(), before);
|
||||||
let retained = primitive_bounds(&h, text.id());
|
let retained = primitive_bounds(&h, text.id());
|
||||||
assert!(!retained.is_empty());
|
assert!(!retained.is_empty());
|
||||||
let _ = h.rsc.widgets_mut().get_dyn_mut(text.id());
|
h.rsc.widgets_mut().mark_for_redraw(text.id());
|
||||||
h.frame();
|
h.frame();
|
||||||
assert!(draws.get() > before);
|
assert!(draws.get() > before);
|
||||||
assert_eq!(retained, primitive_bounds(&h, text.id()));
|
assert_eq!(retained, primitive_bounds(&h, text.id()));
|
||||||
@@ -1184,15 +1185,10 @@ fn padding_and_stack_boxes_follow_the_region_without_drawing_again() {
|
|||||||
assert_eq!(warm.region(&a), cold.region(&b));
|
assert_eq!(warm.region(&a), cold.region(&b));
|
||||||
assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b));
|
assert_eq!(primitive_bounds(&warm, a), primitive_bounds(&cold, b));
|
||||||
}
|
}
|
||||||
let mask = |h: &Harness, id: WidgetId| {
|
assert_eq!(
|
||||||
let active = &h.render.active[&id];
|
mask_bounds(&warm, warm.render.active[&leaf.id()].mask),
|
||||||
let mask = &h.rsc.ui().masks[active.mask.idx()];
|
mask_bounds(&cold, cold.render.active[&other.id()].mask)
|
||||||
h.render
|
);
|
||||||
.moves
|
|
||||||
.resolve(mask.move_idx, mask.region)
|
|
||||||
.to_px(h.render.output_size())
|
|
||||||
};
|
|
||||||
assert_eq!(mask(&warm, leaf.id()), mask(&cold, other.id()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1374,20 +1370,14 @@ fn a_redrawn_mask_keeps_reused_primitives_clipped_when_it_moves() {
|
|||||||
h.set_root((first, masked).span(Dir::DOWN));
|
h.set_root((first, masked).span(Dir::DOWN));
|
||||||
let mask = h.render.active[&masked.id()].mask;
|
let mask = h.render.active[&masked.id()].mask;
|
||||||
let settled = draws.get();
|
let settled = draws.get();
|
||||||
h.rsc.widgets_mut().get_dyn_mut(masked.id());
|
h.rsc.widgets_mut().mark_for_redraw(masked.id());
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
|
assert_eq!(primitive_masks(&h, inner.id()), vec![mask]);
|
||||||
assert_eq!(draws.get(), settled, "a mask repaint must reuse its child");
|
assert_eq!(draws.get(), settled, "a mask repaint must reuse its child");
|
||||||
assert_eq!(h.render.active[&masked.id()].mask, mask);
|
assert_eq!(h.render.active[&masked.id()].mask, mask);
|
||||||
h.set_len(first, Axis::Y, 10);
|
h.set_len(first, Axis::Y, 10);
|
||||||
h.frame();
|
h.frame();
|
||||||
let clip = h.rsc.ui().masks[mask.idx()];
|
assert_eq!(mask_bounds(&h, mask), h.region(&masked).unwrap());
|
||||||
let clip = h
|
|
||||||
.render
|
|
||||||
.moves
|
|
||||||
.resolve(clip.move_idx, clip.region)
|
|
||||||
.to_px(h.render.output_size());
|
|
||||||
assert_eq!(clip, h.region(&masked).unwrap());
|
|
||||||
assert_corners!(h, inner, (0, 10), (400, 200));
|
assert_corners!(h, inner, (0, 10), (400, 200));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1480,14 +1470,17 @@ fn a_redrawn_subtree_is_not_undrawn_by_the_parent_it_left() {
|
|||||||
|
|
||||||
/// A leaf that reports less than the box it is given and states which lengths
|
/// A leaf that reports less than the box it is given and states which lengths
|
||||||
/// of that box its drawing holds for, so a test can widen the contract
|
/// of that box its drawing holds for, so a test can widen the contract
|
||||||
/// without changing the answer.
|
/// without changing the answer. It counts its draws, since what a kept
|
||||||
|
/// contract costs is whether the parent has to make it draw again.
|
||||||
struct Contracted {
|
struct Contracted {
|
||||||
holds: std::ops::RangeInclusive<Px>,
|
holds: std::ops::RangeInclusive<Px>,
|
||||||
size: Size,
|
size: Size,
|
||||||
|
draws: Rc<Cell<usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Contracted {
|
impl Widget for Contracted {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.draws.set(self.draws.get() + 1);
|
||||||
painter.holds(Axis::X, self.holds.clone());
|
painter.holds(Axis::X, self.holds.clone());
|
||||||
self.size
|
self.size
|
||||||
}
|
}
|
||||||
@@ -1511,6 +1504,7 @@ fn widening_what_a_drawing_holds_for_does_not_relay_out_the_parent() {
|
|||||||
let child = Contracted {
|
let child = Contracted {
|
||||||
holds: Px::from_int(300)..=Px::from_int(500),
|
holds: Px::from_int(300)..=Px::from_int(500),
|
||||||
size: Size::from((100, 200)),
|
size: Size::from((100, 200)),
|
||||||
|
draws: Rc::new(Cell::new(0)),
|
||||||
}
|
}
|
||||||
.add(&mut h.rsc);
|
.add(&mut h.rsc);
|
||||||
let draws = Rc::new(Cell::new(0));
|
let draws = Rc::new(Cell::new(0));
|
||||||
@@ -1533,3 +1527,66 @@ fn widening_what_a_drawing_holds_for_does_not_relay_out_the_parent() {
|
|||||||
"a wider contract for the same answer is not a change to lay out"
|
"a wider contract for the same answer is not a change to lay out"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The other half of the rule above: a kept contract is the narrower one, so
|
||||||
|
/// it is only worth keeping where it still holds. A window the old range is
|
||||||
|
/// outside is not one its parent can be handed back, and keeping it there
|
||||||
|
/// throws away the drawing the widget just made.
|
||||||
|
#[test]
|
||||||
|
fn a_contract_this_window_is_outside_is_not_kept() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let leaf_draws = Rc::new(Cell::new(0));
|
||||||
|
let child = Contracted {
|
||||||
|
holds: Px::from_int(300)..=Px::from_int(500),
|
||||||
|
size: Size::from((100, 200)),
|
||||||
|
draws: leaf_draws.clone(),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let root = CountedParent {
|
||||||
|
inner: child.upgrade(&mut h.rsc),
|
||||||
|
draws: Rc::new(Cell::new(0)),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
|
||||||
|
// Wide enough that the old contract leaves the new box out, and the leaf
|
||||||
|
// is marked in the same frame -- so it settles itself first and its
|
||||||
|
// parent draws afterwards, asking about what it settled.
|
||||||
|
h.resize((600, 200));
|
||||||
|
h.rsc[child].holds = Px::from_int(200)..=Px::from_int(700);
|
||||||
|
let settled = leaf_draws.get();
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
leaf_draws.get(),
|
||||||
|
settled + 1,
|
||||||
|
"the leaf settled once and its parent kept what it settled"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A container that measures a child by drawing it, takes that drawing back,
|
||||||
|
/// and then places the child, which `place_at` answers by asking again: taking
|
||||||
|
/// a child back takes it out of the child list, and the note on the child
|
||||||
|
/// saying it is in there has to go with it, or the placement re-expresses a
|
||||||
|
/// drawing that no longer exists.
|
||||||
|
#[test]
|
||||||
|
fn a_child_taken_back_and_placed_again_is_asked_again() {
|
||||||
|
struct Retake(StrongWidget);
|
||||||
|
|
||||||
|
impl Widget for Retake {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
painter.widget(&self.0);
|
||||||
|
painter.undraw(&self.0);
|
||||||
|
painter.place_at(&self.0, PlaceDesc::WHOLE).size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut h = Harness::new((600, 200));
|
||||||
|
let child = rect(Color::RED).add_strong(&mut h.rsc);
|
||||||
|
let id = child.id();
|
||||||
|
let root = Retake(child).add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_corners!(h, id, (0, 0), (600, 200));
|
||||||
|
}
|
||||||
+30
-2
@@ -113,7 +113,7 @@ fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
|
|||||||
|
|
||||||
let mut warm = Harness::new((900, 300));
|
let mut warm = Harness::new((900, 300));
|
||||||
let (text, content) = plant(&mut warm);
|
let (text, content) = plant(&mut warm);
|
||||||
warm.rsc.widgets_mut().get_dyn_mut(text);
|
warm.rsc.widgets_mut().mark_for_redraw(text);
|
||||||
warm.frame();
|
warm.frame();
|
||||||
|
|
||||||
let mut cold = Harness::new((900, 300));
|
let mut cold = Harness::new((900, 300));
|
||||||
@@ -127,6 +127,11 @@ fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
|
|||||||
/// parent would place the part it cut off, and the framework would put a
|
/// parent would place the part it cut off, and the framework would put a
|
||||||
/// drawing longer than its box somewhere. `Masked` is the second of these
|
/// drawing longer than its box somewhere. `Masked` is the second of these
|
||||||
/// after `Scroll`, and the assertion in `draw_at` is what says so.
|
/// after `Scroll`, and the assertion in `draw_at` is what says so.
|
||||||
|
// What it checks is a debug assertion, which a release build does not compile
|
||||||
|
// -- and a `should_panic` test of one fails there rather than passing
|
||||||
|
// vacuously, so it is not built either. Every measurement rig here is run in
|
||||||
|
// release, so `cargo test --release` has to pass.
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic = "clips to"]
|
#[should_panic = "clips to"]
|
||||||
fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
||||||
@@ -142,8 +147,8 @@ fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
|||||||
let mut h = Harness::new((100, 100));
|
let mut h = Harness::new((100, 100));
|
||||||
let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc);
|
let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc);
|
||||||
let clipper = Clipper(tall).add(&mut h.rsc);
|
let clipper = Clipper(tall).add(&mut h.rsc);
|
||||||
|
// `set_root` lays the tree out, so this is where it is caught.
|
||||||
h.set_root(clipper);
|
h.set_root(clipper);
|
||||||
h.frame();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Content that fits sits in the viewport, not in a box of the window's
|
/// Content that fits sits in the viewport, not in a box of the window's
|
||||||
@@ -161,3 +166,26 @@ fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() {
|
|||||||
assert_corners!(h, scroll, (0, 100), (400, 400));
|
assert_corners!(h, scroll, (0, 100), (400, 400));
|
||||||
assert_corners!(h, inner, (0, 225), (400, 275));
|
assert_corners!(h, inner, (0, 225), (400, 275));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A cap narrows the box the widget is asked in, which is what a scroll
|
||||||
|
/// measures its viewport from: the content scrolls within the cap rather than
|
||||||
|
/// within the room the cap was cut from.
|
||||||
|
#[test]
|
||||||
|
fn a_capped_scroll_takes_its_viewport_from_the_cap() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
||||||
|
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
||||||
|
let scroll = (top, bottom).span(Dir::DOWN).scrollable().add(&mut h.rsc);
|
||||||
|
let capped = scroll.max_height(100).add(&mut h.rsc);
|
||||||
|
h.set_root(capped);
|
||||||
|
h.move_to((200, 50));
|
||||||
|
|
||||||
|
// 400 of content in a viewport of 100, so 300 to scroll and the end
|
||||||
|
// showing: the top is 300 above the box, which the window centres.
|
||||||
|
assert_eq!(h.region(&scroll).unwrap().size().y, Px::from_int(100));
|
||||||
|
assert_corners!(h, top, (0, -250), (400, -50));
|
||||||
|
|
||||||
|
h.scroll((0, 1));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, top, (0, -200), (400, 0));
|
||||||
|
}
|
||||||
+59
-102
@@ -9,17 +9,45 @@
|
|||||||
//! reached through a region node's own entry rather than through the offer
|
//! reached through a region node's own entry rather than through the offer
|
||||||
//! that node was given. The last is a wrapping text handed back the width
|
//! that node was given. The last is a wrapping text handed back the width
|
||||||
//! it measured, rounded to a step below the line it measured there.
|
//! it measured, rounded to a step below the line it measured there.
|
||||||
|
//!
|
||||||
|
//! Each says which seed it was shrunk from, of the generator as it stood when
|
||||||
|
//! it was found. Those numbers no longer grow those trees -- a seed names one
|
||||||
|
//! only while the generator draws the same things in the same order, and the
|
||||||
|
//! leaves have grown an image since -- so what is written out below is the
|
||||||
|
//! record of the case, and the seed is where it came from.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use iris::harness::Harness;
|
use iris::harness::Harness;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use iris::random::Branch;
|
use iris::random::Branch;
|
||||||
|
|
||||||
|
/// Every widget in the same place warm as cold, reported all at once: which
|
||||||
|
/// of a dozen boxes moved is the whole of what a shrunk case has to say.
|
||||||
|
///
|
||||||
|
/// A list that names one widget twice is an error rather than a redundant
|
||||||
|
/// check. `width`, `sized` and `align` give back the widget they were handed,
|
||||||
|
/// so a fixture built through them can name one text three times, and then a
|
||||||
|
/// case comparing six boxes compares four and says nothing about it. One
|
||||||
|
/// fixture builds both lists, so checking the warm one checks both.
|
||||||
|
#[track_caller]
|
||||||
fn assert_same_regions(
|
fn assert_same_regions(
|
||||||
warm: &Harness,
|
warm: &Harness,
|
||||||
warm_ids: &[WidgetId],
|
warm_ids: &[WidgetId],
|
||||||
cold: &Harness,
|
cold: &Harness,
|
||||||
cold_ids: &[WidgetId],
|
cold_ids: &[WidgetId],
|
||||||
) {
|
) {
|
||||||
|
assert_eq!(
|
||||||
|
warm_ids.len(),
|
||||||
|
cold_ids.len(),
|
||||||
|
"the warm and cold fixtures list different widgets"
|
||||||
|
);
|
||||||
|
let named: HashSet<&WidgetId> = warm_ids.iter().collect();
|
||||||
|
assert_eq!(
|
||||||
|
named.len(),
|
||||||
|
warm_ids.len(),
|
||||||
|
"a widget is listed twice: {warm_ids:?}"
|
||||||
|
);
|
||||||
let mut wrong = Vec::new();
|
let mut wrong = Vec::new();
|
||||||
for (i, (&w, &c)) in warm_ids.iter().zip(cold_ids).enumerate() {
|
for (i, (&w, &c)) in warm_ids.iter().zip(cold_ids).enumerate() {
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
@@ -92,7 +120,7 @@ fn repainting_a_stack_uses_the_box_its_sizing_child_decided() {
|
|||||||
let mut warm = Harness::new((900, 1200));
|
let mut warm = Harness::new((900, 1200));
|
||||||
let ids = plant_stack_in_its_sizing_childs_box(&mut warm);
|
let ids = plant_stack_in_its_sizing_childs_box(&mut warm);
|
||||||
for &id in &ids {
|
for &id in &ids {
|
||||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
warm.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
}
|
}
|
||||||
warm.frame();
|
warm.frame();
|
||||||
|
|
||||||
@@ -188,36 +216,31 @@ fn reordering_nested_spans_keeps_the_answer_from_the_decided_box() {
|
|||||||
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
/// Four widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
||||||
/// the tree changes -- every widget is marked for redraw and the frame is
|
/// the tree changes -- every widget is marked for redraw and the frame is
|
||||||
/// taken again -- so no box may move, and a warm frame has to land where a
|
/// taken again -- so no box may move, and a warm frame has to land where a
|
||||||
/// cold one does.
|
/// cold one does.
|
||||||
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
||||||
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
let wrapped = wtext("Wrapping shapes")
|
||||||
let sized = wrapped.width(76).add(&mut h.rsc);
|
.size(16)
|
||||||
let aligned = sized;
|
.wrap(true)
|
||||||
|
.width(76)
|
||||||
|
.add(&mut h.rsc);
|
||||||
h.rsc
|
h.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
.set_alignment(wrapped, Axis::X, AxisAlign::POS);
|
||||||
h.rsc
|
h.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
.set_alignment(wrapped, Axis::Y, AxisAlign::POS);
|
||||||
let stack = Stack {
|
let stack = Stack {
|
||||||
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
children: vec![plain.add_strong(&mut h.rsc), wrapped.add_strong(&mut h.rsc)],
|
||||||
size: StackSize::Child(0),
|
size: StackSize::Child(0),
|
||||||
}
|
}
|
||||||
.add(&mut h.rsc);
|
.add(&mut h.rsc);
|
||||||
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
h.set_root(root);
|
h.set_root(root);
|
||||||
vec![
|
vec![plain.id(), wrapped.id(), stack.id(), root.id()]
|
||||||
plain.id(),
|
|
||||||
wrapped.id(),
|
|
||||||
sized.id(),
|
|
||||||
aligned.id(),
|
|
||||||
stack.id(),
|
|
||||||
root.id(),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The first frame does not reach the layout a second one does, so "cold" is
|
/// The first frame does not reach the layout a second one does, so "cold" is
|
||||||
@@ -230,7 +253,7 @@ fn one_frame_is_enough() {
|
|||||||
let first = h.region(&ids[1]).unwrap();
|
let first = h.region(&ids[1]).unwrap();
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
for &id in &ids {
|
for &id in &ids {
|
||||||
h.rsc.widgets_mut().get_dyn_mut(id);
|
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
}
|
}
|
||||||
h.frame();
|
h.frame();
|
||||||
}
|
}
|
||||||
@@ -252,46 +275,30 @@ fn repainting_everything_moves_nothing() {
|
|||||||
let mut warm = Harness::new((640, 900));
|
let mut warm = Harness::new((640, 900));
|
||||||
let ids = plant(&mut warm);
|
let ids = plant(&mut warm);
|
||||||
for &id in &ids {
|
for &id in &ids {
|
||||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
warm.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
}
|
}
|
||||||
warm.frame();
|
warm.frame();
|
||||||
|
|
||||||
let mut cold = Harness::new((640, 900));
|
let mut cold = Harness::new((640, 900));
|
||||||
let cold_ids = plant(&mut cold);
|
let cold_ids = plant(&mut cold);
|
||||||
|
|
||||||
let mut wrong = Vec::new();
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
if got != want {
|
|
||||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
|
/// Four widgets, shrunk from 905. Everything inside the declared 189x176 box
|
||||||
/// is the same size whatever the output is, so a resize may not change any of
|
/// is the same size whatever the output is, so a resize may not change any of
|
||||||
/// it -- but the text comes out 3.92px narrower warm than cold.
|
/// it -- but the text comes out 3.92px narrower warm than cold.
|
||||||
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
||||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
let aligned = text;
|
|
||||||
h.rsc
|
h.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
||||||
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
let inner = (text,).span(Dir::RIGHT).sized((189, 176)).add(&mut h.rsc);
|
||||||
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
|
||||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||||
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
let root = (filler, inner).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
h.state.root = Some(root.add_strong(&mut h.rsc));
|
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||||
vec![
|
vec![text.id(), inner.id(), filler.id(), root.id()]
|
||||||
text.id(),
|
|
||||||
aligned.id(),
|
|
||||||
inner.id(),
|
|
||||||
sized.id(),
|
|
||||||
filler.id(),
|
|
||||||
root.id(),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -306,17 +313,10 @@ fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
|
|||||||
let cold_ids = plant_fixed(&mut cold);
|
let cold_ids = plant_fixed(&mut cold);
|
||||||
cold.frame();
|
cold.frame();
|
||||||
|
|
||||||
let mut wrong = Vec::new();
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
if got != want {
|
|
||||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
|
/// Three widgets, shrunk from 486. A span's two children are swapped: warm by
|
||||||
/// moving them, cold by growing them that way. Same widgets, same sizes, one
|
/// moving them, cold by growing them that way. Same widgets, same sizes, one
|
||||||
/// ends up 29.9px from where the other does.
|
/// ends up 29.9px from where the other does.
|
||||||
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
|
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
|
||||||
@@ -340,16 +340,11 @@ fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span
|
|||||||
gap: Px::ZERO,
|
gap: Px::ZERO,
|
||||||
}
|
}
|
||||||
.add(&mut h.rsc);
|
.add(&mut h.rsc);
|
||||||
let span_handle = span;
|
|
||||||
let aligned = span;
|
|
||||||
h.rsc
|
h.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
.set_alignment(span, Axis::X, AxisAlign::CENTER);
|
.set_alignment(span, Axis::X, AxisAlign::CENTER);
|
||||||
h.state.root = Some(aligned.add_strong(&mut h.rsc));
|
h.state.root = Some(span.add_strong(&mut h.rsc));
|
||||||
(
|
(vec![wrapped.id(), plain.id(), span.id()], span)
|
||||||
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
|
|
||||||
span_handle,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -364,17 +359,10 @@ fn swapping_two_children_lands_where_growing_them_that_way_does() {
|
|||||||
let (cold_ids, _) = plant_pair(&mut cold, true);
|
let (cold_ids, _) = plant_pair(&mut cold, true);
|
||||||
cold.frame();
|
cold.frame();
|
||||||
|
|
||||||
let mut wrong = Vec::new();
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
if got != want {
|
|
||||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
|
/// Seven widgets, shrunk from 80. The scroll decides how wide to make its
|
||||||
/// content from what the content says, and hands that box down through a
|
/// content from what the content says, and hands that box down through a
|
||||||
/// pass-through; the span under it was given that box once, so nothing at its
|
/// pass-through; the span under it was given that box once, so nothing at its
|
||||||
/// own edge says the box was its own answer.
|
/// own edge says the box was its own answer.
|
||||||
@@ -393,8 +381,7 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
|
|||||||
gap: Px::ZERO,
|
gap: Px::ZERO,
|
||||||
}
|
}
|
||||||
.add(&mut h.rsc);
|
.add(&mut h.rsc);
|
||||||
let block = rect(Color::RED).add(&mut h.rsc);
|
let fixed = rect(Color::RED).width(87).add(&mut h.rsc);
|
||||||
let fixed = block.width(87).add(&mut h.rsc);
|
|
||||||
let mut outer_children: Vec<StrongWidget> =
|
let mut outer_children: Vec<StrongWidget> =
|
||||||
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
||||||
if swapped {
|
if swapped {
|
||||||
@@ -416,7 +403,6 @@ fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget
|
|||||||
text.id(),
|
text.id(),
|
||||||
filler.id(),
|
filler.id(),
|
||||||
inner.id(),
|
inner.id(),
|
||||||
block.id(),
|
|
||||||
fixed.id(),
|
fixed.id(),
|
||||||
outer.id(),
|
outer.id(),
|
||||||
through.id(),
|
through.id(),
|
||||||
@@ -440,14 +426,7 @@ fn a_span_given_the_box_its_answer_decided_matches_a_cold_layout() {
|
|||||||
let (cold_ids, _) = plant_scrolled(&mut cold, true);
|
let (cold_ids, _) = plant_scrolled(&mut cold, true);
|
||||||
cold.frame();
|
cold.frame();
|
||||||
|
|
||||||
let mut wrong = Vec::new();
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
if got != want {
|
|
||||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reports a width derived from the box it is asked in. Reading through the
|
/// Reports a width derived from the box it is asked in. Reading through the
|
||||||
@@ -471,11 +450,10 @@ impl Widget for Wider {
|
|||||||
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
|
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
|
||||||
let content = Wider { extra }.add(&mut h.rsc);
|
let content = Wider { extra }.add(&mut h.rsc);
|
||||||
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||||
let root = scroll;
|
|
||||||
h.rsc
|
h.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
.set_alignment(scroll, Axis::X, AxisAlign::NEG);
|
.set_alignment(scroll, Axis::X, AxisAlign::NEG);
|
||||||
h.set_root(root);
|
h.set_root(scroll);
|
||||||
(content, scroll.id())
|
(content, scroll.id())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,14 +545,7 @@ fn a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over() {
|
|||||||
let (cold_ids, _) = plant_boundary(&mut cold, true);
|
let (cold_ids, _) = plant_boundary(&mut cold, true);
|
||||||
cold.frame();
|
cold.frame();
|
||||||
|
|
||||||
let mut wrong = Vec::new();
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
if got != want {
|
|
||||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed
|
/// Five widgets, shrunk by `tests/shrink.rs` from the 277 the oracle's seed
|
||||||
@@ -616,20 +587,13 @@ fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
|
|||||||
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
|
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
|
||||||
let mut warm = Harness::new((900, 1200));
|
let mut warm = Harness::new((900, 1200));
|
||||||
let ids = plant_nested_scrolls(&mut warm);
|
let ids = plant_nested_scrolls(&mut warm);
|
||||||
warm.rsc.widgets_mut().get_dyn_mut(ids[0]);
|
warm.rsc.widgets_mut().mark_for_redraw(ids[0]);
|
||||||
warm.frame();
|
warm.frame();
|
||||||
|
|
||||||
let mut cold = Harness::new((900, 1200));
|
let mut cold = Harness::new((900, 1200));
|
||||||
let cold_ids = plant_nested_scrolls(&mut cold);
|
let cold_ids = plant_nested_scrolls(&mut cold);
|
||||||
|
|
||||||
let mut wrong = Vec::new();
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
if got != want {
|
|
||||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
|
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
|
||||||
@@ -720,14 +684,7 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
|
|||||||
let (cold_ids, _) = plant_under_a_node(&mut cold, true);
|
let (cold_ids, _) = plant_under_a_node(&mut cold, true);
|
||||||
cold.frame();
|
cold.frame();
|
||||||
|
|
||||||
let mut wrong = Vec::new();
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
|
||||||
let (got, want) = (warm.region(&w), cold.region(&c));
|
|
||||||
if got != want {
|
|
||||||
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
|
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
|
||||||
|
|||||||
+80
-61
@@ -8,19 +8,22 @@
|
|||||||
//! submitted and waited on, so this is the GPU's cost and not the recording
|
//! submitted and waited on, so this is the GPU's cost and not the recording
|
||||||
//! loop's -- which is what `draw_cost.rs` measures instead.
|
//! loop's -- which is what `draw_cost.rs` measures instead.
|
||||||
//!
|
//!
|
||||||
//! The instances are two pixels wide so that vertex work dominates; a chain
|
//! Two fixtures, because the walk happens in both stages. `chain_cost_by_depth`
|
||||||
//! walk that does not show up against small quads will not show up against
|
//! draws instances two pixels wide so that vertex work dominates; a walk that
|
||||||
//! anything.
|
//! does not show up against small quads will not show up against anything.
|
||||||
//!
|
//! `mask_cost_by_depth` draws one screenful through a mask instead, which is
|
||||||
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
|
//! where a walk in the fragment stage would show and nowhere else.
|
||||||
|
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use iris_core::{
|
use iris_core::{
|
||||||
Len, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
|
Len, Mask, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
|
||||||
UiRenderState, UiSpan,
|
UiRenderState, UiSpan,
|
||||||
};
|
};
|
||||||
use wgpu::{Color as GpuColor, *};
|
use wgpu::{Color as GpuColor, *};
|
||||||
|
|
||||||
|
#[path = "gpu/mod.rs"]
|
||||||
|
mod gpu;
|
||||||
|
|
||||||
const SIZE: u32 = 1024;
|
const SIZE: u32 = 1024;
|
||||||
const INSTANCES: usize = 200_000;
|
const INSTANCES: usize = 200_000;
|
||||||
const FRAMES: u32 = 20;
|
const FRAMES: u32 = 20;
|
||||||
@@ -29,18 +32,7 @@ const FRAMES: u32 = 20;
|
|||||||
const BATCHES: u32 = 8;
|
const BATCHES: u32 = 8;
|
||||||
|
|
||||||
fn gpu() -> Option<(Device, Queue, f32)> {
|
fn gpu() -> Option<(Device, Queue, f32)> {
|
||||||
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
let adapter = gpu::adapter()?;
|
||||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
|
||||||
{
|
|
||||||
Ok(_) => all,
|
|
||||||
Err(_) => Instance::new(InstanceDescriptor {
|
|
||||||
backends: Backends::GL,
|
|
||||||
..InstanceDescriptor::new_without_display_handle()
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
|
||||||
let adapter =
|
|
||||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
|
||||||
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
|
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
|
||||||
println!("no timestamp queries on {:?}", adapter.get_info().name);
|
println!("no timestamp queries on {:?}", adapter.get_info().name);
|
||||||
return None;
|
return None;
|
||||||
@@ -55,22 +47,17 @@ fn gpu() -> Option<(Device, Queue, f32)> {
|
|||||||
Some((device, queue, period))
|
Some((device, queue, period))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
/// Which stage the fill puts the work in: many small quads, where a walk per
|
||||||
SurfaceConfiguration {
|
/// vertex is what shows, or one screenful of masked rows, where a walk per
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
/// fragment would.
|
||||||
format,
|
#[derive(Clone, Copy)]
|
||||||
color_space: SurfaceColorSpace::Auto,
|
enum Fixture {
|
||||||
width: SIZE,
|
Quads,
|
||||||
height: SIZE,
|
Masked,
|
||||||
present_mode: PresentMode::Fifo,
|
|
||||||
desired_maximum_frame_latency: 2,
|
|
||||||
alpha_mode: CompositeAlphaMode::Auto,
|
|
||||||
view_formats: vec![],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A chain `depth` slots long, and instances that all resolve through its end.
|
/// A chain `depth` slots long, and instances that all resolve through its end.
|
||||||
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize, fixture: Fixture) {
|
||||||
let kind = ui.primitives.kind::<RectPrimitive>();
|
let kind = ui.primitives.kind::<RectPrimitive>();
|
||||||
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
||||||
|
|
||||||
@@ -80,20 +67,51 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let px = |v: f32| Len::px(v);
|
let px = |v: f32| Len::px(v);
|
||||||
for i in 0..INSTANCES {
|
let rows = SIZE as usize;
|
||||||
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
|
let mask_idx = match fixture {
|
||||||
let y = (i / (SIZE as usize / 2)) as f32;
|
Fixture::Quads => MaskIdx::NONE,
|
||||||
|
// Its own chain as long as the instances', since a viewport sits as
|
||||||
|
// deep in the tree as the content it clips.
|
||||||
|
Fixture::Masked => {
|
||||||
|
let idx = ui.masks.push(Mask {
|
||||||
|
region: UiRegion::FULL,
|
||||||
|
move_idx: slot,
|
||||||
|
});
|
||||||
|
// Nothing frees it here, but the owner's reference is what a real
|
||||||
|
// one is kept alive by.
|
||||||
|
ui.masks.push_ref(idx);
|
||||||
|
idx
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let instances = match fixture {
|
||||||
|
Fixture::Quads => INSTANCES,
|
||||||
|
Fixture::Masked => rows,
|
||||||
|
};
|
||||||
|
for i in 0..instances {
|
||||||
|
let region = match fixture {
|
||||||
|
Fixture::Quads => {
|
||||||
|
let x = (i % (rows / 2)) as f32 * 2.0;
|
||||||
|
let y = (i / (rows / 2)) as f32;
|
||||||
|
UiRegion::new(
|
||||||
|
UiSpan::new(px(x), px(x + 2.0)),
|
||||||
|
UiSpan::new(px(y), px(y + 1.0)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// A full row each, so one screenful of fragments goes through the
|
||||||
|
// mask and the vertex stage is four corners per row.
|
||||||
|
Fixture::Masked => UiRegion::new(
|
||||||
|
UiSpan::new(px(0.0), px(SIZE as f32)),
|
||||||
|
UiSpan::new(px(i as f32), px(i as f32 + 1.0)),
|
||||||
|
),
|
||||||
|
};
|
||||||
render.layers.write(
|
render.layers.write(
|
||||||
0,
|
0,
|
||||||
PrimitiveInst {
|
PrimitiveInst {
|
||||||
kind,
|
kind,
|
||||||
id,
|
id,
|
||||||
primitive: RectPrimitive::color(UiColor::WHITE),
|
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||||
region: UiRegion::new(
|
region,
|
||||||
UiSpan::new(px(x), px(x + 2.0)),
|
mask_idx,
|
||||||
UiSpan::new(px(y), px(y + 1.0)),
|
|
||||||
),
|
|
||||||
mask_idx: MaskIdx::NONE,
|
|
||||||
move_idx: slot,
|
move_idx: slot,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -101,28 +119,15 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
|
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
|
||||||
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
|
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize, fixture: Fixture) -> f64 {
|
||||||
let format = TextureFormat::Bgra8Unorm;
|
let format = TextureFormat::Bgra8Unorm;
|
||||||
let mut node = UiRenderNode::new(device, &config(format));
|
let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
fill(&mut ui, &mut render, depth);
|
fill(&mut ui, &mut render, depth, fixture);
|
||||||
node.update(device, queue, &mut ui, &mut render);
|
node.update(device, queue, &mut ui, &mut render);
|
||||||
|
|
||||||
let target = device.create_texture(&TextureDescriptor {
|
let target = gpu::target(device, format, SIZE, false);
|
||||||
label: Some("chain cost"),
|
|
||||||
size: Extent3d {
|
|
||||||
width: SIZE,
|
|
||||||
height: SIZE,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
|
||||||
mip_level_count: 1,
|
|
||||||
sample_count: 1,
|
|
||||||
dimension: TextureDimension::D2,
|
|
||||||
format,
|
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
||||||
view_formats: &[],
|
|
||||||
});
|
|
||||||
let view = target.create_view(&TextureViewDescriptor::default());
|
let view = target.create_view(&TextureViewDescriptor::default());
|
||||||
|
|
||||||
let queries = device.create_query_set(&QuerySetDescriptor {
|
let queries = device.create_query_set(&QuerySetDescriptor {
|
||||||
@@ -202,17 +207,15 @@ fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
|
|||||||
best
|
best
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn by_depth(fixture: Fixture, instances: usize) {
|
||||||
#[ignore = "measurement, not a check"]
|
|
||||||
fn chain_cost_by_depth() {
|
|
||||||
let Some((device, queue, period)) = gpu() else {
|
let Some((device, queue, period)) = gpu() else {
|
||||||
println!("no gpu with timestamps; nothing measured");
|
println!("no gpu with timestamps; nothing measured");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
println!("{INSTANCES} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
|
println!("{instances} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
|
||||||
let mut base = None;
|
let mut base = None;
|
||||||
for depth in [1, 2, 4, 8, 16, 32, 64] {
|
for depth in [1, 2, 4, 8, 16, 32, 64] {
|
||||||
let ns = pass_cost(&device, &queue, period, depth);
|
let ns = pass_cost(&device, &queue, period, depth, fixture);
|
||||||
let base = *base.get_or_insert(ns);
|
let base = *base.get_or_insert(ns);
|
||||||
println!(
|
println!(
|
||||||
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
|
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
|
||||||
@@ -221,3 +224,19 @@ fn chain_cost_by_depth() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn chain_cost_by_depth() {
|
||||||
|
by_depth(Fixture::Quads, INSTANCES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One screenful of rows, every one clipped by a mask whose own chain is that
|
||||||
|
/// deep. What this says that the quads cannot is whether a mask costs the walk
|
||||||
|
/// once per instance or once per fragment: at a screenful of fragments per
|
||||||
|
/// chain, the second is the difference between these two tables.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn mask_cost_by_depth() {
|
||||||
|
by_depth(Fixture::Masked, SIZE as usize);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//! What one container's draw costs against the number of children it has.
|
||||||
|
//!
|
||||||
|
//! cargo test --release --test children_cost -- --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! Every other rig here varies depth, the window, or what changed between
|
||||||
|
//! frames; this one varies width, which is the dimension a container's own
|
||||||
|
//! per-child bookkeeping is counted in. A list of rows is the shape that gets
|
||||||
|
//! wide -- a transcript, a file tree -- and a cost per child that is not flat
|
||||||
|
//! down this table is a cost paid twice for every child added.
|
||||||
|
//!
|
||||||
|
//! Wall time rather than instructions, because what is being told apart here
|
||||||
|
//! is a factor rather than a few percent, and the table says which it is: a
|
||||||
|
//! flat right-hand column is linear and a rising one is not.
|
||||||
|
|
||||||
|
mod rig;
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
use rig::env;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// A column of leaves each with a length of its own, so the span asks every
|
||||||
|
/// one of them and reads what each answered.
|
||||||
|
fn build(h: &mut Harness, children: usize) -> WidgetId {
|
||||||
|
let mut col = Span::empty(Dir::DOWN);
|
||||||
|
for _ in 0..children {
|
||||||
|
col.push(
|
||||||
|
rect(Color::RED)
|
||||||
|
.height(LayoutLen::px(4.0))
|
||||||
|
.add_strong(&mut h.rsc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let root = col.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
root.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn draw_cost_by_children() {
|
||||||
|
let frames = env("FRAMES", 40_usize);
|
||||||
|
println!("{frames} full redraws of one span, per child in the last column");
|
||||||
|
for children in [100_usize, 200, 400, 800, 1600] {
|
||||||
|
// Tall enough that no child is collapsed for want of room.
|
||||||
|
let mut h = Harness::new((600.0, children as f32 * 8.0));
|
||||||
|
let root = build(&mut h, children);
|
||||||
|
h.frame();
|
||||||
|
let start = Instant::now();
|
||||||
|
for _ in 0..frames {
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(root);
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
let ms = start.elapsed().as_secs_f64() * 1000.0 / frames as f64;
|
||||||
|
println!(
|
||||||
|
"children {children:>5}: {ms:>8.3} ms per redraw, {:>7.4} ms each",
|
||||||
|
ms / children as f64
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
mod rig;
|
||||||
|
#[path = "scenario/mod.rs"]
|
||||||
|
mod scenario;
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::{Edits, Plan, plan};
|
||||||
|
|
||||||
|
fn check_requests(edit: impl Fn(&mut Plan) + Sync) {
|
||||||
|
let depth = rig::env("IRIS_DEFERRED_DEPTH", 4_usize);
|
||||||
|
let seeds = rig::seeds("IRIS_DEFERRED_SEED", "IRIS_DEFERRED_SEEDS", 20);
|
||||||
|
scenario::over_seeds(seeds, |seed| {
|
||||||
|
let mut grown = plan(seed, depth, &Edits::default());
|
||||||
|
edit(&mut grown);
|
||||||
|
for case in scenario::ALL {
|
||||||
|
if let Some(how) = scenario::diverges(&grown, case, seed) {
|
||||||
|
panic!(
|
||||||
|
"request seed {seed} depth {depth} after {}: {how}",
|
||||||
|
case.name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deferred_requests_agree_warm_and_cold() {
|
||||||
|
check_requests(|grown| {
|
||||||
|
let mut index = 0;
|
||||||
|
grown.walk_mut(&mut |node| {
|
||||||
|
if let Some(rules) = &mut node.size {
|
||||||
|
for axis in Axis::BOTH {
|
||||||
|
index += 1;
|
||||||
|
rules[axis] = match index % 7 {
|
||||||
|
0 => leftover(1).clamp(20, 120).into(),
|
||||||
|
1 => leftover(1).min(rel(0.5)).into(),
|
||||||
|
2 => (leftover(1) + px(30)).min(leftover(2)).into(),
|
||||||
|
// Both sides an expression, the one shape that
|
||||||
|
// copies a request's nodes into another's.
|
||||||
|
3 => leftover(1).min(px(40)).max(leftover(2).min(px(70))).into(),
|
||||||
|
_ => rules[axis].clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_intrinsic_bounds_agree_warm_and_cold() {
|
||||||
|
check_requests(|grown| {
|
||||||
|
grown.walk_mut(&mut |node| {
|
||||||
|
if let Some(rules) = &mut node.size {
|
||||||
|
for axis in Axis::BOTH {
|
||||||
|
let bound = &mut rules[axis].bound;
|
||||||
|
if bound.min.is_some() {
|
||||||
|
bound.min = Some(Len::rel(0.25));
|
||||||
|
}
|
||||||
|
if bound.max.is_some() {
|
||||||
|
bound.max = Some(Len::rel(0.75));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preferred_requests_with_independent_bounds_agree_warm_and_cold() {
|
||||||
|
check_requests(|grown| {
|
||||||
|
let mut index = 0;
|
||||||
|
grown.walk_mut(&mut |node| {
|
||||||
|
if let Some(rules) = &mut node.size {
|
||||||
|
for axis in Axis::BOTH {
|
||||||
|
index += 1;
|
||||||
|
rules[axis].request = Some(match index % 4 {
|
||||||
|
0 => leftover(1).into(),
|
||||||
|
1 => rel(0.5).into(),
|
||||||
|
2 => px(80).into(),
|
||||||
|
_ => (leftover(1) + px(30)).min(leftover(2)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+5
-34
@@ -13,10 +13,6 @@
|
|||||||
//! That is how `PrimitiveRender` was measured against a match in the renderer:
|
//! That is how `PrimitiveRender` was measured against a match in the renderer:
|
||||||
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
|
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
|
||||||
//! one.
|
//! one.
|
||||||
//!
|
|
||||||
//! The instance is leaked deliberately. A Vulkan loader may unload the driver
|
|
||||||
//! when the last one drops, which can fault as a thread that used it exits --
|
|
||||||
//! and every test runs on a spawned thread.
|
|
||||||
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@@ -27,6 +23,9 @@ use iris_core::{
|
|||||||
};
|
};
|
||||||
use wgpu::{Color as GpuColor, *};
|
use wgpu::{Color as GpuColor, *};
|
||||||
|
|
||||||
|
#[path = "gpu/mod.rs"]
|
||||||
|
mod gpu;
|
||||||
|
|
||||||
const SIZE: u32 = 1024;
|
const SIZE: u32 = 1024;
|
||||||
const FRAMES: u32 = 200;
|
const FRAMES: u32 = 200;
|
||||||
/// Reported as the best of this many batches, since the mean moves by more
|
/// Reported as the best of this many batches, since the mean moves by more
|
||||||
@@ -34,39 +33,11 @@ const FRAMES: u32 = 200;
|
|||||||
const BATCHES: u32 = 8;
|
const BATCHES: u32 = 8;
|
||||||
|
|
||||||
fn gpu() -> Option<(Device, Queue)> {
|
fn gpu() -> Option<(Device, Queue)> {
|
||||||
// Probed rather than assumed: there may be no Vulkan adapter, and GL is
|
let adapter = gpu::adapter()?;
|
||||||
// what is left when there is not.
|
|
||||||
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
|
||||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
|
||||||
{
|
|
||||||
Ok(_) => all,
|
|
||||||
Err(_) => Instance::new(InstanceDescriptor {
|
|
||||||
backends: Backends::GL,
|
|
||||||
..InstanceDescriptor::new_without_display_handle()
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
// Leaked rather than dropped: see the note at the top of the file.
|
|
||||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
|
||||||
let adapter =
|
|
||||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
|
||||||
println!("adapter: {:?}", adapter.get_info());
|
println!("adapter: {:?}", adapter.get_info());
|
||||||
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
|
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
|
||||||
SurfaceConfiguration {
|
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
||||||
format,
|
|
||||||
color_space: SurfaceColorSpace::Auto,
|
|
||||||
width: SIZE,
|
|
||||||
height: SIZE,
|
|
||||||
present_mode: PresentMode::Fifo,
|
|
||||||
desired_maximum_frame_latency: 2,
|
|
||||||
alpha_mode: CompositeAlphaMode::Auto,
|
|
||||||
view_formats: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every layer draws all three primitives, so the renderer takes a different
|
/// Every layer draws all three primitives, so the renderer takes a different
|
||||||
/// path for each list it walks -- which is the case a single-primitive layer
|
/// path for each list it walks -- which is the case a single-primitive layer
|
||||||
/// would never exercise. Images are bound per instance, so there are few.
|
/// would never exercise. Images are bound per instance, so there are few.
|
||||||
@@ -136,7 +107,7 @@ fn fill(
|
|||||||
|
|
||||||
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
|
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
|
||||||
let format = TextureFormat::Bgra8Unorm;
|
let format = TextureFormat::Bgra8Unorm;
|
||||||
let mut node = UiRenderNode::new(device, &config(format));
|
let mut node = UiRenderNode::new(device, &gpu::config(format, SIZE));
|
||||||
let mut ui = UiData::default();
|
let mut ui = UiData::default();
|
||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
let _handles = fill(&mut ui, &mut render, layers, per_layer);
|
let _handles = fill(&mut ui, &mut render, layers, per_layer);
|
||||||
|
|||||||
+13
-13
@@ -11,11 +11,13 @@
|
|||||||
//! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH`
|
//! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH`
|
||||||
//! select what the long run covers.
|
//! select what the long run covers.
|
||||||
|
|
||||||
|
mod rig;
|
||||||
#[path = "scenario/mod.rs"]
|
#[path = "scenario/mod.rs"]
|
||||||
mod scenario;
|
mod scenario;
|
||||||
|
|
||||||
use iris::random::{Edits, Plan, plan};
|
use iris::random::{Edits, Plan, plan};
|
||||||
use scenario::{ALL, Case, diverges, env, over_seeds};
|
use rig::env;
|
||||||
|
use scenario::{ALL, Case, diverges, over_seeds};
|
||||||
|
|
||||||
/// How deep the generator branches. The generator widens two to four ways per
|
/// How deep the generator branches. The generator widens two to four ways per
|
||||||
/// level, so depth is exponential in width and a deep narrow tree is not
|
/// level, so depth is exponential in width and a deep narrow tree is not
|
||||||
@@ -25,10 +27,14 @@ fn depth() -> usize {
|
|||||||
env("IRIS_GENERATED_DEPTH", 4)
|
env("IRIS_GENERATED_DEPTH", 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The seeds the ordinary tests take. Seven that have never failed; 86,
|
/// The seeds the ordinary tests take: a corpus rather than a set of
|
||||||
/// which a `Scroll` fixed point once settled differently on; and 20, which
|
/// regression cases, since a seed names a tree only for as long as the
|
||||||
/// caught a locally redrawn widget being placed twice in the box its parent
|
/// generator draws the same things in the same order. Adding images to the
|
||||||
/// had already placed it in.
|
/// leaves moved every one of them, so 20 and 86 -- which once caught a widget
|
||||||
|
/// placed twice in a box its parent had already placed it in, and a `Scroll`
|
||||||
|
/// fixed point settling differently -- no longer grow those trees. Both
|
||||||
|
/// defects are pinned by the shrunk fixtures in `cases/unsettled.rs`, which
|
||||||
|
/// are trees rather than numbers.
|
||||||
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
|
const SEEDS: [u64; 10] = [1, 2, 3, 5, 8, 10, 13, 20, 86, 98];
|
||||||
|
|
||||||
fn check(seed: u64, depth: usize, case: Case) {
|
fn check(seed: u64, depth: usize, case: Case) {
|
||||||
@@ -104,16 +110,10 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "as many seeds as it is asked for, rather than the nine the others check"]
|
#[ignore = "as many seeds as it is asked for, rather than the ten the others check"]
|
||||||
fn a_long_run_of_seeds_agrees() {
|
fn a_long_run_of_seeds_agrees() {
|
||||||
let depth = depth();
|
let depth = depth();
|
||||||
let seeds: Vec<u64> = match std::env::var("IRIS_GENERATED_SEED")
|
let seeds = rig::seeds("IRIS_GENERATED_SEED", "IRIS_GENERATED_SEEDS", 100);
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.parse().ok())
|
|
||||||
{
|
|
||||||
Some(seed) => vec![seed],
|
|
||||||
None => (1..=env("IRIS_GENERATED_SEEDS", 100_u64)).collect(),
|
|
||||||
};
|
|
||||||
over_seeds(seeds, |seed| {
|
over_seeds(seeds, |seed| {
|
||||||
let grown = plan(seed, depth, &Edits::default());
|
let grown = plan(seed, depth, &Edits::default());
|
||||||
for case in ALL {
|
for case in ALL {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
//! The adapter, the surface configuration and the target the GPU rigs share,
|
||||||
|
//! so no two of them can probe for a device or make a target in different
|
||||||
|
//! ways.
|
||||||
|
|
||||||
|
use wgpu::*;
|
||||||
|
|
||||||
|
/// An adapter on whatever this machine has, or `None` where there is none.
|
||||||
|
///
|
||||||
|
/// Probed rather than assumed: there may be no Vulkan adapter, and GL is what
|
||||||
|
/// is left when there is not.
|
||||||
|
///
|
||||||
|
/// The instance is leaked deliberately. A Vulkan loader may unload the driver
|
||||||
|
/// when the last one drops, which can fault as a thread that used it exits --
|
||||||
|
/// and every test runs on a spawned thread.
|
||||||
|
pub fn adapter() -> Option<Adapter> {
|
||||||
|
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
||||||
|
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
||||||
|
{
|
||||||
|
Ok(_) => all,
|
||||||
|
Err(_) => Instance::new(InstanceDescriptor {
|
||||||
|
backends: Backends::GL,
|
||||||
|
..InstanceDescriptor::new_without_display_handle()
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let instance: &'static Instance = Box::leak(Box::new(instance));
|
||||||
|
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config(format: TextureFormat, size: u32) -> SurfaceConfiguration {
|
||||||
|
SurfaceConfiguration {
|
||||||
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format,
|
||||||
|
color_space: SurfaceColorSpace::Auto,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
present_mode: PresentMode::Fifo,
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
alpha_mode: CompositeAlphaMode::Auto,
|
||||||
|
view_formats: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A square colour target to draw a pass into. `copy` adds the usage a rig
|
||||||
|
/// that reads the pixels back needs; one that only times the pass does not.
|
||||||
|
// This module is compiled into each rig target separately, so a helper the
|
||||||
|
// ones that make no target of their own do not call is dead code there.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn target(device: &Device, format: TextureFormat, size: u32, copy: bool) -> Texture {
|
||||||
|
device.create_texture(&TextureDescriptor {
|
||||||
|
label: Some("gpu rig target"),
|
||||||
|
size: Extent3d {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: TextureDimension::D2,
|
||||||
|
format,
|
||||||
|
usage: match copy {
|
||||||
|
true => TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
|
||||||
|
false => TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
},
|
||||||
|
view_formats: &[],
|
||||||
|
})
|
||||||
|
}
|
||||||
+61
-18
@@ -13,15 +13,43 @@
|
|||||||
//!
|
//!
|
||||||
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
|
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
|
||||||
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
|
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
|
||||||
//! `IRIS_DIRTY` how many widgets `many` marks at once.
|
//! `IRIS_DIRTY` how many widgets `many` marks at once. `IRIS_UNBOUNDED=1`
|
||||||
|
//! removes intrinsic bounds while preserving the rest of the generated tree.
|
||||||
|
|
||||||
|
mod rig;
|
||||||
|
|
||||||
use iris::harness::Harness;
|
use iris::harness::Harness;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use iris::random::{Edits, Tree, grow};
|
use iris::random::{Edits, Tree, build, plan};
|
||||||
|
use rig::env;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
||||||
|
|
||||||
|
/// A scroll whose content fits is the same drawing in every box it still
|
||||||
|
/// fits in, so a longer or shorter one relays out nothing. Where the content
|
||||||
|
/// sits in that box is decided by placing its answer in the whole of it,
|
||||||
|
/// which is a fraction of the box and holds at every length -- so the
|
||||||
|
/// contract must not turn on the alignment. It did, and at the default
|
||||||
|
/// alignment, which is the middle, every box change redrew the scroll.
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
#[test]
|
||||||
|
fn a_fitting_scroll_holds_for_every_box_its_content_fits_in() {
|
||||||
|
use iris::core::layout_diagnostics as diag;
|
||||||
|
|
||||||
|
for align in [Align::TOP_LEFT, Align::CENTER, Align::BOT_RIGHT] {
|
||||||
|
let mut harness = Harness::new((400, 200));
|
||||||
|
let inner = rect(Color::RED).height(50).add(&mut harness.rsc);
|
||||||
|
harness.set_root(inner.scrollable().align(align));
|
||||||
|
harness.frame();
|
||||||
|
let _ = diag::take();
|
||||||
|
// Still far longer than the 50 the content needs.
|
||||||
|
harness.resize((400, 180));
|
||||||
|
harness.frame();
|
||||||
|
assert_eq!(diag::take().distinct_widgets(), 0, "{align:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
#[test]
|
#[test]
|
||||||
fn a_selected_widget_retains_its_layout_events() {
|
fn a_selected_widget_retains_its_layout_events() {
|
||||||
@@ -37,8 +65,8 @@ fn a_selected_widget_retains_its_layout_events() {
|
|||||||
diagnostics::trace_widget(leaf.id());
|
diagnostics::trace_widget(leaf.id());
|
||||||
let _ = diagnostics::take();
|
let _ = diagnostics::take();
|
||||||
|
|
||||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(root.id());
|
harness.rsc.widgets_mut().mark_for_redraw(root.id());
|
||||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf.id());
|
harness.rsc.widgets_mut().mark_for_redraw(leaf.id());
|
||||||
harness.frame();
|
harness.frame();
|
||||||
|
|
||||||
let report = diagnostics::take();
|
let report = diagnostics::take();
|
||||||
@@ -69,13 +97,6 @@ fn a_selected_widget_retains_its_layout_events() {
|
|||||||
diagnostics::clear_traced_widgets();
|
diagnostics::clear_traced_widgets();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
||||||
std::env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
fn trace_selected(tree: &Tree) {
|
fn trace_selected(tree: &Tree) {
|
||||||
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
|
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
|
||||||
@@ -101,9 +122,17 @@ fn rig_edits() -> Edits {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fixture(harness: &mut Harness, seed: u64, depth: usize) -> (StrongWidget, Tree) {
|
||||||
|
let mut plan = plan(seed, depth, &rig_edits());
|
||||||
|
if env("IRIS_UNBOUNDED", 0_u8) != 0 {
|
||||||
|
plan.drop_bounds();
|
||||||
|
}
|
||||||
|
build(&mut harness.rsc, &plan)
|
||||||
|
}
|
||||||
|
|
||||||
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
||||||
let mut harness = Harness::new(OUTPUT);
|
let mut harness = Harness::new(OUTPUT);
|
||||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
|
let (root, tree) = fixture(&mut harness, seed, depth);
|
||||||
harness.state.root = Some(root);
|
harness.state.root = Some(root);
|
||||||
harness.frame();
|
harness.frame();
|
||||||
println!(
|
println!(
|
||||||
@@ -185,7 +214,7 @@ fn layout_cost() {
|
|||||||
|
|
||||||
if selected("cold") {
|
if selected("cold") {
|
||||||
let mut harness = Harness::new(OUTPUT);
|
let mut harness = Harness::new(OUTPUT);
|
||||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
|
let (root, tree) = fixture(&mut harness, seed, depth);
|
||||||
harness.state.root = Some(root);
|
harness.state.root = Some(root);
|
||||||
println!(
|
println!(
|
||||||
"fixture: seed {seed}, depth {depth}, {} widgets",
|
"fixture: seed {seed}, depth {depth}, {} widgets",
|
||||||
@@ -195,7 +224,6 @@ fn layout_cost() {
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
let _ = iris::core::layout_diagnostics::take();
|
let _ = iris::core::layout_diagnostics::take();
|
||||||
run("cold", 1, &mut harness, |_, _| {});
|
run("cold", 1, &mut harness, |_, _| {});
|
||||||
drop(tree);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if selected("repaint") {
|
if selected("repaint") {
|
||||||
@@ -203,7 +231,7 @@ fn layout_cost() {
|
|||||||
trace_selected(&tree);
|
trace_selected(&tree);
|
||||||
let leaf = tree.ids[0];
|
let leaf = tree.ids[0];
|
||||||
run("repaint", frames, &mut harness, move |harness, _| {
|
run("repaint", frames, &mut harness, move |harness, _| {
|
||||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
|
harness.rsc.widgets_mut().mark_for_redraw(leaf);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +246,7 @@ fn layout_cost() {
|
|||||||
println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
|
println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
|
||||||
run("many", frames, &mut harness, move |harness, _| {
|
run("many", frames, &mut harness, move |harness, _| {
|
||||||
for &id in &dirty {
|
for &id in &dirty {
|
||||||
harness.rsc.widgets_mut().get_dyn_mut(id);
|
harness.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -232,7 +260,7 @@ fn layout_cost() {
|
|||||||
harness
|
harness
|
||||||
.rsc
|
.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
.set_size_rule(sized, Axis::X, SizeRule::Exact(len));
|
.set_size_rule(sized, Axis::X, SizeRule::from(len));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +279,21 @@ fn layout_cost() {
|
|||||||
run("resize", frames, &mut harness, |harness, frame| {
|
run("resize", frames, &mut harness, |harness, frame| {
|
||||||
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
||||||
});
|
});
|
||||||
drop(tree);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
#[test]
|
||||||
|
fn repainting_measured_text_does_not_invalidate_its_span() {
|
||||||
|
use iris::core::layout_diagnostics as diag;
|
||||||
|
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let text = wtext("a paragraph that fits").wrap(true).add(&mut h.rsc);
|
||||||
|
h.set_root((text, wtext("another paragraph")).span(Dir::DOWN));
|
||||||
|
let _ = diag::take();
|
||||||
|
h.rsc.widgets_mut().mark_for_redraw(text);
|
||||||
|
h.frame();
|
||||||
|
let report = diag::take();
|
||||||
|
assert_eq!(report.distinct_widgets(), 1);
|
||||||
|
assert_eq!(report.hot_widgets()[0].id, text.id());
|
||||||
|
}
|
||||||
+11
-10
@@ -8,17 +8,14 @@
|
|||||||
//!
|
//!
|
||||||
//! then the same after, and `diff` the two. A line is one widget: the seed,
|
//! then the same after, and `diff` the two. A line is one widget: the seed,
|
||||||
//! its index in creation order, and its box in window pixels, or `-` where
|
//! its index in creation order, and its box in window pixels, or `-` where
|
||||||
//! it is not drawn.
|
//! it is not drawn. `IRIS_UNBOUNDED=1` drops the trees' intrinsic bounds, as
|
||||||
|
//! in the diagnostics rig, which compares the two paths over the same shapes.
|
||||||
|
|
||||||
|
mod rig;
|
||||||
|
|
||||||
use iris::harness::Harness;
|
use iris::harness::Harness;
|
||||||
use iris::random::{Edits, grow};
|
use iris::random::{Edits, build, plan};
|
||||||
|
use rig::env;
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
||||||
std::env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "a dump to diff across commits, not a check"]
|
#[ignore = "a dump to diff across commits, not a check"]
|
||||||
@@ -28,7 +25,11 @@ fn every_cold_layout_is_printed() {
|
|||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
for seed in 1..=seeds {
|
for seed in 1..=seeds {
|
||||||
let mut harness = Harness::new((1920.0, 1200.0));
|
let mut harness = Harness::new((1920.0, 1200.0));
|
||||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
let mut plan = plan(seed, depth, &Edits::default());
|
||||||
|
if env("IRIS_UNBOUNDED", 0_u8) != 0 {
|
||||||
|
plan.drop_bounds();
|
||||||
|
}
|
||||||
|
let (root, tree) = build(&mut harness.rsc, &plan);
|
||||||
harness.state.root = Some(root);
|
harness.state.root = Some(root);
|
||||||
harness.frame();
|
harness.frame();
|
||||||
for (index, id) in tree.ids.iter().enumerate() {
|
for (index, id) in tree.ids.iter().enumerate() {
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
//! Which pixels a mask lets through, read back off the GPU.
|
||||||
|
//!
|
||||||
|
//! cargo test --release --test mask_clip -- --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! Ignored because it needs a device, which not every machine running the
|
||||||
|
//! suite has -- and a deliberate run on one without fails rather than passing
|
||||||
|
//! with nothing checked. Nothing else here sees a mask at all: `iris::harness`
|
||||||
|
//! draws no pixels, and a mask's rectangle is resolved through its own move
|
||||||
|
//! chain in the shader, so the CPU's idea of it is not what clips anything.
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris_core::{
|
||||||
|
Len, Mask, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode,
|
||||||
|
UiRenderState, UiSpan,
|
||||||
|
};
|
||||||
|
use wgpu::{Color as GpuColor, *};
|
||||||
|
|
||||||
|
#[path = "gpu/mod.rs"]
|
||||||
|
mod gpu;
|
||||||
|
|
||||||
|
const SIZE: u32 = 256;
|
||||||
|
|
||||||
|
/// The mask is a box inside a move chain two links long and the drawing
|
||||||
|
/// overflows it on both axes, so what comes back is the mask's own rectangle
|
||||||
|
/// composed through that chain -- and a clip resolved through the wrong one,
|
||||||
|
/// or not composed at all, lands somewhere else.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "needs a gpu"]
|
||||||
|
fn a_mask_clips_its_own_box_composed_through_its_chain() {
|
||||||
|
let adapter = gpu::adapter().expect("no adapter to draw with");
|
||||||
|
println!("adapter: {:?}", adapter.get_info().name);
|
||||||
|
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor::default()))
|
||||||
|
.expect("no device on that adapter");
|
||||||
|
let format = TextureFormat::Bgra8Unorm;
|
||||||
|
let mut node = UiRenderNode::new(&device, &gpu::config(format, SIZE));
|
||||||
|
let mut ui = UiData::default();
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
let kind = ui.primitives.kind::<RectPrimitive>();
|
||||||
|
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
||||||
|
let px = Len::px;
|
||||||
|
|
||||||
|
let outer = render.moves.push(MoveIdx::NONE, UiRegion::FULL);
|
||||||
|
let shift = (16.0, 24.0);
|
||||||
|
let inner = render.moves.push(
|
||||||
|
outer,
|
||||||
|
UiRegion::new(
|
||||||
|
UiSpan::new(px(shift.0), px(shift.0) + Len::FULL),
|
||||||
|
UiSpan::new(px(shift.1), px(shift.1) + Len::FULL),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let clip = (20.0, 30.0, 120.0, 90.0);
|
||||||
|
let mask = ui.masks.push(Mask {
|
||||||
|
region: UiRegion::new(
|
||||||
|
UiSpan::new(px(clip.0), px(clip.2)),
|
||||||
|
UiSpan::new(px(clip.1), px(clip.3)),
|
||||||
|
),
|
||||||
|
move_idx: inner,
|
||||||
|
});
|
||||||
|
// The owner's reference, which is what keeps a real one alive.
|
||||||
|
ui.masks.push_ref(mask);
|
||||||
|
render.layers.write(
|
||||||
|
0,
|
||||||
|
PrimitiveInst {
|
||||||
|
kind,
|
||||||
|
id,
|
||||||
|
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||||
|
region: UiRegion::new(
|
||||||
|
UiSpan::new(px(0.0), px(200.0)),
|
||||||
|
UiSpan::new(px(0.0), px(200.0)),
|
||||||
|
),
|
||||||
|
mask_idx: mask,
|
||||||
|
move_idx: inner,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
node.update(&device, &queue, &mut ui, &mut render);
|
||||||
|
|
||||||
|
let target = gpu::target(&device, format, SIZE, true);
|
||||||
|
let view = target.create_view(&TextureViewDescriptor::default());
|
||||||
|
let row = SIZE * 4;
|
||||||
|
let readback = device.create_buffer(&BufferDescriptor {
|
||||||
|
label: Some("mask clip"),
|
||||||
|
size: (row * SIZE) as u64,
|
||||||
|
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
||||||
|
{
|
||||||
|
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
||||||
|
label: None,
|
||||||
|
color_attachments: &[Some(RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: Operations {
|
||||||
|
load: LoadOp::Clear(GpuColor::BLACK),
|
||||||
|
store: StoreOp::Store,
|
||||||
|
},
|
||||||
|
depth_slice: None,
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
multiview_mask: None,
|
||||||
|
});
|
||||||
|
node.draw(pass);
|
||||||
|
}
|
||||||
|
let whole = Extent3d {
|
||||||
|
width: SIZE,
|
||||||
|
height: SIZE,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
};
|
||||||
|
encoder.copy_texture_to_buffer(
|
||||||
|
TexelCopyTextureInfo {
|
||||||
|
texture: &target,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: Origin3d::ZERO,
|
||||||
|
aspect: TextureAspect::All,
|
||||||
|
},
|
||||||
|
TexelCopyBufferInfo {
|
||||||
|
buffer: &readback,
|
||||||
|
layout: TexelCopyBufferLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(row),
|
||||||
|
rows_per_image: Some(SIZE),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
whole,
|
||||||
|
);
|
||||||
|
queue.submit(Some(encoder.finish()));
|
||||||
|
let slice = readback.slice(..);
|
||||||
|
slice.map_async(MapMode::Read, |_| {});
|
||||||
|
device
|
||||||
|
.poll(PollType::Wait {
|
||||||
|
submission_index: None,
|
||||||
|
timeout: None,
|
||||||
|
})
|
||||||
|
.expect("the pass did not finish");
|
||||||
|
let pixels = slice.get_mapped_range().expect("the target did not map");
|
||||||
|
|
||||||
|
let mut lit = 0;
|
||||||
|
let mut bounds: Option<(u32, u32, u32, u32)> = None;
|
||||||
|
for y in 0..SIZE {
|
||||||
|
for x in 0..SIZE {
|
||||||
|
if pixels[(y * row + x * 4) as usize] == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lit += 1;
|
||||||
|
let (x0, y0, x1, y1) = bounds.unwrap_or((x, y, x, y));
|
||||||
|
bounds = Some((x0.min(x), y0.min(y), x1.max(x), y1.max(y)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The clip shifted by the chain. Its far edge is exclusive: a fragment
|
||||||
|
// exactly on it is the first one outside.
|
||||||
|
let want = (
|
||||||
|
(clip.0 + shift.0) as u32,
|
||||||
|
(clip.1 + shift.1) as u32,
|
||||||
|
(clip.2 + shift.0) as u32 - 1,
|
||||||
|
(clip.3 + shift.1) as u32 - 1,
|
||||||
|
);
|
||||||
|
assert_eq!(bounds, Some(want), "{lit} pixels through the mask");
|
||||||
|
let (x0, y0, x1, y1) = want;
|
||||||
|
assert_eq!(lit, (x1 - x0 + 1) * (y1 - y0 + 1), "the clip has a hole");
|
||||||
|
}
|
||||||
+88
-20
@@ -1,22 +1,28 @@
|
|||||||
//! What a resize frame costs and what it holds, on a tree the revision before
|
//! Text-layout workloads with stable paragraphs for comparisons across revisions.
|
||||||
//! #16 also builds.
|
//! PR #19's base uses the older spelling of the fixed 40-pixel width and has
|
||||||
//!
|
//! no diagnostics. The random generator changed with layout, so it cannot
|
||||||
//! Deliberately written in the API subset `43ce8c7` and this branch share, so
|
//! provide the same workload across the full PR.
|
||||||
//! the same source can be dropped into an old worktree and measured there:
|
|
||||||
//! that is the only like-for-like comparison with the code the retained
|
|
||||||
//! layout replaced. The random tree cannot carry one, because the generator
|
|
||||||
//! itself changed with the work.
|
|
||||||
//!
|
//!
|
||||||
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
|
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
|
||||||
//! -- --ignored --nocapture resize_cost
|
//! -- --ignored --nocapture resize_cost
|
||||||
|
//! PHASE=edit ROWS=40 FRAMES=2000 cargo test --release --test revision_cost \
|
||||||
|
//! -- --ignored --nocapture text_updates_cost
|
||||||
//! ROWS=2000 cargo test --release --test revision_cost \
|
//! ROWS=2000 cargo test --release --test revision_cost \
|
||||||
//! -- --ignored --nocapture text_memory
|
//! -- --ignored --nocapture text_memory
|
||||||
//!
|
//!
|
||||||
//! Wall time on this machine varies with CPU frequency; take the number from
|
//! `text_updates_cost` selects idle, repaint, edit, or scroll with `PHASE`.
|
||||||
//! `perf stat -e instructions:u` on the test binary directly.
|
//! It alternates a short suffix for edits so later frames do not get a longer
|
||||||
|
//! paragraph than earlier ones. These are CPU fixtures, with no GPU submission.
|
||||||
|
//!
|
||||||
|
//! Use repeated `perf stat -e instructions:u` runs on the executable directly;
|
||||||
|
//! process totals include font loading and the cold frame, so compare identical
|
||||||
|
//! row and frame counts. Wall time on this machine is not a stable comparison.
|
||||||
|
|
||||||
|
mod rig;
|
||||||
|
|
||||||
use iris::harness::Harness;
|
use iris::harness::Harness;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
|
use rig::env;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
/// xorshift64, so one seed is one set of paragraphs on any machine.
|
/// xorshift64, so one seed is one set of paragraphs on any machine.
|
||||||
@@ -78,13 +84,6 @@ fn words(rng: &mut Rng, least: usize, most: usize) -> String {
|
|||||||
|
|
||||||
const OUTPUT: (f32, f32) = (900.0, 1200.0);
|
const OUTPUT: (f32, f32) = (900.0, 1200.0);
|
||||||
|
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
||||||
std::env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A row of a fixed-width rect beside a column of one wrapping and one
|
/// A row of a fixed-width rect beside a column of one wrapping and one
|
||||||
/// overflowing text: the shape that makes a container measure a child in a
|
/// overflowing text: the shape that makes a container measure a child in a
|
||||||
/// box it will not keep.
|
/// box it will not keep.
|
||||||
@@ -137,9 +136,10 @@ fn resize_cost() {
|
|||||||
println!("paragraph {at}: {:?}", h.region(id));
|
println!("paragraph {at}: {:?}", h.region(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two widths in turn is the friendly case for anything that remembers an
|
// The sweep cycles 256 widths, avoiding the two-width cache-friendly case.
|
||||||
// answer, so `SWEEP=1` never repeats one -- a drag rather than a toggle.
|
|
||||||
let sweep = env("SWEEP", 0_usize) != 0;
|
let sweep = env("SWEEP", 0_usize) != 0;
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _ = iris::core::layout_diagnostics::take();
|
||||||
let mut elapsed = Vec::with_capacity(frames);
|
let mut elapsed = Vec::with_capacity(frames);
|
||||||
for frame in 0..frames {
|
for frame in 0..frames {
|
||||||
let narrower = match sweep {
|
let narrower = match sweep {
|
||||||
@@ -151,6 +151,11 @@ fn resize_cost() {
|
|||||||
h.frame();
|
h.frame();
|
||||||
elapsed.push(start.elapsed().as_secs_f64() * 1000.0);
|
elapsed.push(start.elapsed().as_secs_f64() * 1000.0);
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
print!(
|
||||||
|
"{}",
|
||||||
|
iris::core::layout_diagnostics::take().per_frame(frames)
|
||||||
|
);
|
||||||
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
println!(
|
println!(
|
||||||
"resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
|
"resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
|
||||||
@@ -194,10 +199,73 @@ fn text_memory() {
|
|||||||
h.frame();
|
h.frame();
|
||||||
}
|
}
|
||||||
report("after 40 resizes");
|
report("after 40 resizes");
|
||||||
// Settled: the output holds still and one leaf repaints per frame.
|
// Settled: the output holds still and one leaf repaints per frame. Marked
|
||||||
|
// by taking it mutably because the revision at the top of this file has no
|
||||||
|
// `mark_for_redraw`, and the same source has to build against both.
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]);
|
let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]);
|
||||||
h.frame();
|
h.frame();
|
||||||
}
|
}
|
||||||
report("after settling");
|
report("after settling");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn text_updates_cost() {
|
||||||
|
let rows = env("ROWS", 40_usize);
|
||||||
|
let frames = env("FRAMES", 1000_usize);
|
||||||
|
let phase = env("PHASE", String::from("edit"));
|
||||||
|
assert!(rows > 0 && frames > 0);
|
||||||
|
assert!(["idle", "repaint", "edit", "scroll"].contains(&phase.as_str()));
|
||||||
|
let mut h = Harness::new(OUTPUT);
|
||||||
|
let mut rng = Rng(1);
|
||||||
|
let mut col = Span::empty(Dir::DOWN);
|
||||||
|
let first = wtext(words(&mut rng, 12, 52))
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
col.push(first.add_strong(&mut h.rsc));
|
||||||
|
for _ in 1..rows {
|
||||||
|
col.push(
|
||||||
|
wtext(words(&mut rng, 12, 52))
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.add_strong(&mut h.rsc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let root = col.scrollable().add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _ = iris::core::layout_diagnostics::take();
|
||||||
|
let original = h.rsc[first].content.to_string();
|
||||||
|
let alternate = format!("{original} another word");
|
||||||
|
let start = Instant::now();
|
||||||
|
for frame in 0..frames {
|
||||||
|
match phase.as_str() {
|
||||||
|
"idle" => {}
|
||||||
|
"repaint" => {
|
||||||
|
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
|
||||||
|
}
|
||||||
|
"edit" => {
|
||||||
|
h.rsc[first].content.clear();
|
||||||
|
h.rsc[first].content.push_str(if frame % 2 == 0 {
|
||||||
|
&alternate
|
||||||
|
} else {
|
||||||
|
&original
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"scroll" => h.rsc[root].scroll(if frame % 2 == 0 { -12.0 } else { 12.0 }),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"{phase}: {rows} rows, {frames} frames, {:.1} ms",
|
||||||
|
start.elapsed().as_secs_f64() * 1000.0
|
||||||
|
);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
print!(
|
||||||
|
"{}",
|
||||||
|
iris::core::layout_diagnostics::take().per_frame(frames)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//! What the rigs need and none of them should spell its own way. Every
|
||||||
|
//! fuzzer and measurement here is run by hand with its parameters in the
|
||||||
|
//! environment, so one reader is shared rather than copied into each target.
|
||||||
|
|
||||||
|
/// A rig's parameter from the environment, or its default. A switch is
|
||||||
|
/// `env("NAME", 0_u8) != 0`, so `NAME=1` turns it on.
|
||||||
|
pub fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||||
|
std::env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The seeds a scan runs: the one `one` names on its own, or `1..=` the
|
||||||
|
/// count `many` gives. One seed replaces the range rather than narrowing
|
||||||
|
/// it, which is how a tree a scan failed on is run again by itself.
|
||||||
|
// This module is compiled into each rig target separately, so a helper the
|
||||||
|
// measurement rigs have no seeds to choose is dead code in those builds.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn seeds(one: &str, many: &str, count: u64) -> Vec<u64> {
|
||||||
|
match std::env::var(one).ok().and_then(|seed| seed.parse().ok()) {
|
||||||
|
Some(seed) => vec![seed],
|
||||||
|
None => (1..=env(many, count)).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
-25
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
use iris::harness::Harness;
|
use iris::harness::Harness;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use iris::random::{Aligns, Edits, Kind, Lens, Plan, Rng, SpanEdit, Tree, build};
|
use iris::random::{Edits, Kind, Plan, Rng, SpanEdit, Tree, build};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// A seed per thread but one, since a seed grows, lays out and drops its tree
|
/// A seed per thread but one, since a seed grows, lays out and drops its tree
|
||||||
@@ -30,13 +30,6 @@ pub fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
||||||
std::env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The window a tree is grown in, and the one a resize takes it to.
|
/// The window a tree is grown in, and the one a resize takes it to.
|
||||||
const OUTER: (f32, f32) = (1920.0, 1200.0);
|
const OUTER: (f32, f32) = (1920.0, 1200.0);
|
||||||
const INNER: (f32, f32) = (640.0, 900.0);
|
const INNER: (f32, f32) = (640.0, 900.0);
|
||||||
@@ -190,35 +183,48 @@ impl Case {
|
|||||||
|
|
||||||
fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
|
fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
|
||||||
for &id in tree.ids.iter().step_by(step) {
|
for &id in tree.ids.iter().step_by(step) {
|
||||||
warm.rsc.widgets_mut().get_dyn_mut(id);
|
warm.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn a_len(rng: &mut Rng) -> Option<LayoutLen> {
|
/// A length in pixels, or a cap over one: a rule that reads the box it is
|
||||||
Some(LayoutLen::px(20.0 + rng.below(180) as f32))
|
/// given is the one a resize can change the effect of without changing the
|
||||||
|
/// rule, so a tree that never grows one leaves that unexercised.
|
||||||
|
fn a_rule(rng: &mut Rng) -> SizeRule {
|
||||||
|
let len = Len::px(20.0 + rng.below(180) as f32);
|
||||||
|
match rng.below(4) {
|
||||||
|
0 => SizeRule::max(len),
|
||||||
|
1 => SizeRule::min(len),
|
||||||
|
_ => LayoutLen::from(len).into(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
|
fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> SizeRules {
|
||||||
let lens = [a_len(rng), a_len(rng)];
|
let lens = SizeRules {
|
||||||
|
x: a_rule(rng),
|
||||||
|
y: a_rule(rng),
|
||||||
|
};
|
||||||
warm.rsc
|
warm.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
.set_size_rules(tree.sized[idx], lens[0], lens[1]);
|
.set_size_rules(tree.sized[idx], lens.x.clone(), lens.y.clone());
|
||||||
lens
|
lens
|
||||||
}
|
}
|
||||||
|
|
||||||
fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns {
|
fn realign_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Align {
|
||||||
let side = |rng: &mut Rng| match rng.below(4) {
|
let side = |rng: &mut Rng| match rng.below(4) {
|
||||||
0 => None,
|
0 => None,
|
||||||
1 => Some(AxisAlign::NEG),
|
1 => Some(AxisAlign::NEG),
|
||||||
2 => Some(AxisAlign::CENTER),
|
2 => Some(AxisAlign::CENTER),
|
||||||
_ => Some(AxisAlign::POS),
|
_ => Some(AxisAlign::POS),
|
||||||
};
|
};
|
||||||
let align = [side(rng), side(rng)];
|
let align = Align {
|
||||||
|
x: side(rng),
|
||||||
|
y: side(rng),
|
||||||
|
};
|
||||||
let id = tree.aligned[idx];
|
let id = tree.aligned[idx];
|
||||||
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
|
let taken = RegionAlign::from(align);
|
||||||
warm.rsc
|
for axis in Axis::BOTH {
|
||||||
.widgets_mut()
|
warm.rsc.widgets_mut().set_alignment(id, axis, taken[axis]);
|
||||||
.set_alignment(id, axis, align.unwrap_or_default());
|
|
||||||
}
|
}
|
||||||
align
|
align
|
||||||
}
|
}
|
||||||
@@ -344,9 +350,21 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu
|
|||||||
/// buildable from what the failure printed.
|
/// buildable from what the failure printed.
|
||||||
fn describe(id: WidgetId, h: &Harness) -> String {
|
fn describe(id: WidgetId, h: &Harness) -> String {
|
||||||
let rules = h.rsc.widgets().size_rules(id);
|
let rules = h.rsc.widgets().size_rules(id);
|
||||||
let rule = |r: SizeRule| match r.exact() {
|
// The tree a failure names is written out again from what it printed, so
|
||||||
Some(len) => format!("{len}"),
|
// every part of a rule prints: the preferred length and the bounds are
|
||||||
None => "-".into(),
|
// independent, and a bound lumped into "no rule" could not be rebuilt.
|
||||||
|
let rule = |r: &SizeRule| {
|
||||||
|
let mut out = r
|
||||||
|
.request
|
||||||
|
.as_ref()
|
||||||
|
.map_or_else(String::new, ToString::to_string);
|
||||||
|
if let Some(min) = r.bound.min {
|
||||||
|
out += &format!(">{}", LayoutLen::from(min));
|
||||||
|
}
|
||||||
|
if let Some(max) = r.bound.max {
|
||||||
|
out += &format!("<{}", LayoutLen::from(max));
|
||||||
|
}
|
||||||
|
if out.is_empty() { "-".into() } else { out }
|
||||||
};
|
};
|
||||||
let align = h.rsc.widgets().alignment(id);
|
let align = h.rsc.widgets().alignment(id);
|
||||||
let side = |a: AxisAlign| {
|
let side = |a: AxisAlign| {
|
||||||
@@ -363,8 +381,8 @@ fn describe(id: WidgetId, h: &Harness) -> String {
|
|||||||
// A rule and an alignment are properties of whatever carries them, so
|
// A rule and an alignment are properties of whatever carries them, so
|
||||||
// they print with that widget rather than as widgets of their own.
|
// they print with that widget rather than as widgets of their own.
|
||||||
let mut out = describe_widget(id, h);
|
let mut out = describe_widget(id, h);
|
||||||
if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) {
|
if *rules != SizeRules::default() {
|
||||||
out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y));
|
out += &format!("[x:{},y:{}]", rule(&rules.x), rule(&rules.y));
|
||||||
}
|
}
|
||||||
if align != RegionAlign::default() {
|
if align != RegionAlign::default() {
|
||||||
out += &format!("@{},{}", side(align.x), side(align.y));
|
out += &format!("@{},{}", side(align.x), side(align.y));
|
||||||
|
|||||||
+4
-8
@@ -17,11 +17,13 @@
|
|||||||
//! It is a fuzzer: run it once the ordinary tests pass, and turn what it
|
//! It is a fuzzer: run it once the ordinary tests pass, and turn what it
|
||||||
//! finds into a test of its own rather than leaving a seed as the record.
|
//! finds into a test of its own rather than leaving a seed as the record.
|
||||||
|
|
||||||
|
mod rig;
|
||||||
#[path = "scenario/mod.rs"]
|
#[path = "scenario/mod.rs"]
|
||||||
mod scenario;
|
mod scenario;
|
||||||
|
|
||||||
use iris::random::{Edits, Plan, plan};
|
use iris::random::{Edits, Plan, plan};
|
||||||
use scenario::{ALL, Case, diverges, env, over_seeds};
|
use rig::env;
|
||||||
|
use scenario::{ALL, Case, diverges, over_seeds};
|
||||||
|
|
||||||
/// Takes the first simplification that still fails, until none does. The
|
/// Takes the first simplification that still fails, until none does. The
|
||||||
/// simplifications come biggest first, so this walks down rather than
|
/// simplifications come biggest first, so this walks down rather than
|
||||||
@@ -58,13 +60,7 @@ fn cases() -> Vec<Case> {
|
|||||||
fn no_grown_tree_lays_out_differently_warm_than_cold() {
|
fn no_grown_tree_lays_out_differently_warm_than_cold() {
|
||||||
let depth: usize = env("SHRINK_DEPTH", 5);
|
let depth: usize = env("SHRINK_DEPTH", 5);
|
||||||
let cases = cases();
|
let cases = cases();
|
||||||
let seeds: Vec<u64> = match std::env::var("SHRINK_SEED")
|
let seeds = rig::seeds("SHRINK_SEED", "SHRINK_SEEDS", 400);
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.parse().ok())
|
|
||||||
{
|
|
||||||
Some(seed) => vec![seed],
|
|
||||||
None => (1..=env("SHRINK_SEEDS", 400_u64)).collect(),
|
|
||||||
};
|
|
||||||
let count = seeds.len();
|
let count = seeds.len();
|
||||||
|
|
||||||
over_seeds(seeds, |seed| {
|
over_seeds(seeds, |seed| {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
//! The rigs stay their own targets: `shrink` and `generated` are fuzzers run
|
//! The rigs stay their own targets: `shrink` and `generated` are fuzzers run
|
||||||
//! on their own, and the `*_cost` and `*_diagnostics` ones are measurements.
|
//! on their own, and the `*_cost` and `*_diagnostics` ones are measurements.
|
||||||
|
|
||||||
|
#[path = "cases/deferred.rs"]
|
||||||
|
mod deferred;
|
||||||
#[path = "cases/determinism.rs"]
|
#[path = "cases/determinism.rs"]
|
||||||
mod determinism;
|
mod determinism;
|
||||||
#[path = "cases/drift.rs"]
|
#[path = "cases/drift.rs"]
|
||||||
|
|||||||
+15
-29
@@ -1,5 +1,5 @@
|
|||||||
//! Traces the six-widget tree in `unsettled.rs`, to see what box its text is
|
//! Traces the four-widget trees in `unsettled.rs`, to see what box their text
|
||||||
//! actually drawn in on a first frame against a settled one.
|
//! is actually drawn in on a first frame against a settled one.
|
||||||
|
|
||||||
#![cfg(feature = "layout-diagnostics")]
|
#![cfg(feature = "layout-diagnostics")]
|
||||||
|
|
||||||
@@ -9,30 +9,25 @@ use iris::prelude::*;
|
|||||||
|
|
||||||
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
||||||
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
let wrapped = wtext("Wrapping shapes")
|
||||||
let sized = wrapped.width(76).add(&mut h.rsc);
|
.size(16)
|
||||||
let aligned = sized;
|
.wrap(true)
|
||||||
|
.width(76)
|
||||||
|
.add(&mut h.rsc);
|
||||||
h.rsc
|
h.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
.set_alignment(wrapped, Axis::X, AxisAlign::POS);
|
||||||
h.rsc
|
h.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
.set_alignment(wrapped, Axis::Y, AxisAlign::POS);
|
||||||
let stack = Stack {
|
let stack = Stack {
|
||||||
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
children: vec![plain.add_strong(&mut h.rsc), wrapped.add_strong(&mut h.rsc)],
|
||||||
size: StackSize::Child(0),
|
size: StackSize::Child(0),
|
||||||
}
|
}
|
||||||
.add(&mut h.rsc);
|
.add(&mut h.rsc);
|
||||||
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
h.state.root = Some(root.add_strong(&mut h.rsc));
|
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||||
vec![
|
vec![plain.id(), wrapped.id(), stack.id(), root.id()]
|
||||||
plain.id(),
|
|
||||||
wrapped.id(),
|
|
||||||
sized.id(),
|
|
||||||
aligned.id(),
|
|
||||||
stack.id(),
|
|
||||||
root.id(),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dump(label: &str, report: &diag::Report, text: WidgetId) {
|
fn dump(label: &str, report: &diag::Report, text: WidgetId) {
|
||||||
@@ -81,7 +76,7 @@ fn what_box_the_text_is_drawn_in() {
|
|||||||
|
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
for &id in &ids {
|
for &id in &ids {
|
||||||
h.rsc.widgets_mut().get_dyn_mut(id);
|
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||||
}
|
}
|
||||||
let _ = diag::take();
|
let _ = diag::take();
|
||||||
h.frame();
|
h.frame();
|
||||||
@@ -93,23 +88,14 @@ fn what_box_the_text_is_drawn_in() {
|
|||||||
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
||||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
let aligned = text;
|
|
||||||
h.rsc
|
h.rsc
|
||||||
.widgets_mut()
|
.widgets_mut()
|
||||||
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
||||||
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
let inner = (text,).span(Dir::RIGHT).sized((189, 176)).add(&mut h.rsc);
|
||||||
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
|
||||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||||
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
let root = (filler, inner).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
h.state.root = Some(root.add_strong(&mut h.rsc));
|
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||||
vec![
|
vec![text.id(), inner.id(), filler.id(), root.id()]
|
||||||
text.id(),
|
|
||||||
aligned.id(),
|
|
||||||
inner.id(),
|
|
||||||
sized.id(),
|
|
||||||
filler.id(),
|
|
||||||
root.id(),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user