Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e44dea34b4 | ||
|
|
a0693acc56 | ||
|
|
25e456e0b5 | ||
|
|
53b00c68e9 | ||
|
|
a92c6acdbf | ||
|
|
c8beca5753 | ||
|
|
4bd8607968 | ||
|
|
ffd79f32d3 | ||
|
|
0e0d4af326 | ||
|
|
ea6dbae0dc | ||
|
|
32542d0c0b | ||
|
|
5b7800264d | ||
|
|
5f16617511 | ||
|
|
45a717695b | ||
|
|
d21a21524f |
No files matched your search
@@ -109,6 +109,18 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The first step at or above `v`, where [`Self::from_f32`] takes the
|
||||||
|
/// nearest one and is below it half the time. For a bound that has to
|
||||||
|
/// admit the value it came from: a measurement rounded down is a bound
|
||||||
|
/// that leaves out the thing it was measured from.
|
||||||
|
pub const fn ceil_from_f32(v: f32) -> Self {
|
||||||
|
let nearest = Self::from_f32(v);
|
||||||
|
match nearest.to_f32() < v {
|
||||||
|
true => nearest.next_up(),
|
||||||
|
false => nearest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// From a number as it is written in source -- `16`, `1.5` -- which is
|
/// From a number as it is written in source -- `16`, `1.5` -- which is
|
||||||
/// the other place a value enters the grid.
|
/// the other place a value enters the grid.
|
||||||
pub fn from_num(v: impl UiNum) -> Self {
|
pub fn from_num(v: impl UiNum) -> Self {
|
||||||
@@ -372,6 +384,12 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
|||||||
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
|
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The first step at or above each part, for a measurement reported as a
|
||||||
|
/// box: what it occupies is not less than what was measured.
|
||||||
|
pub fn ceil_from_f32(v: Vec2) -> Self {
|
||||||
|
Self::new(Fixed::ceil_from_f32(v.x), Fixed::ceil_from_f32(v.y))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_f32(self) -> Vec2 {
|
pub fn to_f32(self) -> Vec2 {
|
||||||
Vec2::new(self.x.to_f32(), self.y.to_f32())
|
Vec2::new(self.x.to_f32(), self.y.to_f32())
|
||||||
}
|
}
|
||||||
@@ -488,6 +506,25 @@ mod tests {
|
|||||||
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
|
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
|
||||||
|
/// not on the grid, and the narrowest box the break still holds for is
|
||||||
|
/// the step at or above it, never the one below.
|
||||||
|
#[test]
|
||||||
|
fn a_ceiling_never_lands_below_the_number_it_came_from() {
|
||||||
|
let step = 1.0 / (1 << PX_SHIFT) as f32;
|
||||||
|
for n in 0..64 {
|
||||||
|
let v = 189.0 + n as f32 * step / 3.0;
|
||||||
|
let up = Px::ceil_from_f32(v);
|
||||||
|
assert!(up.to_f32() >= v, "{up:?} is below {v}");
|
||||||
|
assert!(
|
||||||
|
up.to_f32() - v < step,
|
||||||
|
"{up:?} is more than a step above {v}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// An exact step is its own ceiling.
|
||||||
|
assert_eq!(Px::ceil_from_f32(189.5), Px::from_f32(189.5));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
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);
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ pub struct RegionAlign {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RegionAlign {
|
impl RegionAlign {
|
||||||
/// Both axes at the near edge. What a container passes as an override for
|
/// Both axes at the near edge: the start of a box in its own orientation.
|
||||||
/// a child it is going to position itself.
|
|
||||||
pub const NEAR: Self = Self {
|
pub const NEAR: Self = Self {
|
||||||
x: AxisAlign::NEG,
|
x: AxisAlign::NEG,
|
||||||
y: AxisAlign::NEG,
|
y: AxisAlign::NEG,
|
||||||
|
|||||||
@@ -125,6 +125,13 @@ impl Size {
|
|||||||
Axis::Y => self.y,
|
Axis::Y => self.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &mut self.x,
|
||||||
|
Axis::Y => &mut self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutLen {
|
impl LayoutLen {
|
||||||
@@ -151,6 +158,18 @@ impl LayoutLen {
|
|||||||
Len::from_parts(self.rel.add(share), self.px)
|
Len::from_parts(self.rel.add(share), self.px)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This length, given as a part of a box `len` long, as a part of the
|
||||||
|
/// box `len` is itself a part of. The share is untouched: it is a claim
|
||||||
|
/// on whoever divides the room, not a fraction of anything.
|
||||||
|
pub const fn within_len(self, len: Len) -> Self {
|
||||||
|
let part = Len::from_parts(self.rel, self.px).within_len(len);
|
||||||
|
Self {
|
||||||
|
px: part.px,
|
||||||
|
rel: part.rel,
|
||||||
|
leftover: self.leftover,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn px(px: impl UiNum) -> Self {
|
pub fn px(px: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
px: Px::from_num(px),
|
px: Px::from_num(px),
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ impl Len {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn within_len(&self, len: Len) -> Self {
|
pub const fn within_len(&self, len: Len) -> Self {
|
||||||
self.within(&UiSpan {
|
self.within(&UiSpan {
|
||||||
start: Len::ZERO,
|
start: Len::ZERO,
|
||||||
end: len,
|
end: len,
|
||||||
|
|||||||
@@ -107,13 +107,6 @@ impl Default for TextAttrs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How far below the longest line a width may fall and still be answered by
|
|
||||||
/// the break in hand. A parent that offers a child the length it reported
|
|
||||||
/// composes that length back through the box chain, so the two differ in the
|
|
||||||
/// last bits -- and at exactly the longest line, that decides whether a line
|
|
||||||
/// fits. Sub-pixel, so no break it admits is one a reader could see.
|
|
||||||
const BREAK_EPSILON_PX: f32 = 0.05;
|
|
||||||
|
|
||||||
/// Keeps text and its corresponding layout from getting out of sync.
|
/// Keeps text and its corresponding layout from getting out of sync.
|
||||||
pub struct TextBuffer {
|
pub struct TextBuffer {
|
||||||
text: String,
|
text: String,
|
||||||
@@ -200,15 +193,19 @@ impl TextBuffer {
|
|||||||
// A greedy break at one width is the same break at every width down
|
// 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
|
// 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
|
// take a word that would not fit in the wider box. So the layout in
|
||||||
// hand already answers, and re-breaking would only be a chance to
|
// hand already answers, and re-breaking would only be work.
|
||||||
// disagree with itself -- which is what happens when a parent offers
|
//
|
||||||
// a child the length that child just reported, and the two land
|
// At the longest line exactly, with no margin below it. A narrower
|
||||||
// either side of a float.
|
// 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
|
if let Some(key) = &self.layout_key
|
||||||
&& key.attrs == *attrs
|
&& key.attrs == *attrs
|
||||||
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
||||||
&& want <= broke_at
|
&& want <= broke_at
|
||||||
&& want + BREAK_EPSILON_PX >= self.layout.width()
|
&& want >= self.layout.width()
|
||||||
{
|
{
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::TextShapeHits);
|
diag::bump(Counter::TextShapeHits);
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ struct MoveOffset {
|
|||||||
// belongs to the pixel above it. Flooring the product instead drops a pixel
|
// belongs to the pixel above it. Flooring the product instead drops a pixel
|
||||||
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
|
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
|
||||||
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
|
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
|
||||||
|
//
|
||||||
|
// Taken over the whole coordinate, fraction and pixels summed, since a floor
|
||||||
|
// does not distribute over a sum: floored apart, a half of one and a half of
|
||||||
|
// the other lose the pixel the two together make.
|
||||||
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
|
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
|
||||||
return floor(v + PX_STEP * 0.5);
|
return floor(v + PX_STEP * 0.5);
|
||||||
}
|
}
|
||||||
@@ -147,8 +151,8 @@ fn vs_main(
|
|||||||
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
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 bot_right_px = vec2(r.x.end.px, r.y.end.px);
|
||||||
|
|
||||||
let top_left = snap_floor(top_left_rel * window.dim) + snap_floor(top_left_px);
|
let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
|
||||||
let bot_right = snap_floor(bot_right_rel * window.dim) + snap_floor(bot_right_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;
|
||||||
|
|
||||||
let uv = vec2<f32>(
|
let uv = vec2<f32>(
|
||||||
@@ -179,8 +183,8 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
|||||||
let br = vec2(m.x.end.rel, m.y.end.rel);
|
let br = vec2(m.x.end.rel, m.y.end.rel);
|
||||||
let br_px = vec2(m.x.end.px, m.y.end.px);
|
let br_px = vec2(m.x.end.px, m.y.end.px);
|
||||||
|
|
||||||
let top_left = snap_floor(tl * window.dim) + snap_floor(tl_px);
|
let top_left = snap_floor(tl * window.dim + tl_px);
|
||||||
let bot_right = snap_floor(br * window.dim) + snap_floor(br_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;
|
||||||
|
|||||||
+35
-14
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle,
|
Holds, LayerId, LayoutLen, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle,
|
||||||
UiRegion, WidgetId,
|
UiRegion, UiVec2, WidgetId,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -11,11 +11,20 @@ pub struct ActiveData {
|
|||||||
pub id: WidgetId,
|
pub id: WidgetId,
|
||||||
/// The box its drawing is in, in `parent_move`'s coordinates.
|
/// The box its drawing is in, in `parent_move`'s coordinates.
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
/// The box its parent first asked about it in, as a part of the box the
|
/// The box its parent gave it, in the same coordinates: what it was
|
||||||
/// parent was itself asked in. Any later box it was given was decided
|
/// asked about, before its own answer placed its drawing inside it.
|
||||||
/// knowing its answer, so this is where a question about it is asked
|
/// `region` is that placement, and a local redraw asks here.
|
||||||
/// again -- and it is kept relative so that it follows the parent's.
|
pub given: UiRegion,
|
||||||
pub offer: UiRegion,
|
/// The same box as lengths of its parent's box, which is the one route
|
||||||
|
/// to a box in pixels: a draw threads these down a level at a time, and
|
||||||
|
/// [`crate::UiRenderState::redraw`] takes the same steps back up.
|
||||||
|
pub given_len: UiVec2,
|
||||||
|
/// The lengths of the box its parent first asked about it in, as
|
||||||
|
/// lengths of the box the parent was itself offered. Any later box it
|
||||||
|
/// was given was decided knowing its answer, so this is the question
|
||||||
|
/// asked again -- and a chain of fractions has no frame in it, which is
|
||||||
|
/// why a region node between two widgets cannot break it.
|
||||||
|
pub offer_len: UiVec2,
|
||||||
/// What it answered there: the size and what that held for.
|
/// What it answered there: the size and what that held for.
|
||||||
pub answer: (Size, [Holds; 2]),
|
pub answer: (Size, [Holds; 2]),
|
||||||
/// What the widget said it used of its box, the last time it drew.
|
/// What the widget said it used of its box, the last time it drew.
|
||||||
@@ -41,18 +50,22 @@ 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: [Option<LayoutLen>; 2],
|
pub declared: [Option<LayoutLen>; 2],
|
||||||
/// The alignment its parent asked it with. A local redraw repeats that
|
/// The axes along which its parent chose its box from its own answer,
|
||||||
/// question, including an override chosen by a container.
|
/// so a local redraw asks the question its parent asked.
|
||||||
pub align: RegionAlign,
|
pub decided: [bool; 2],
|
||||||
/// Whether that alignment was the parent's override rather than the
|
/// Its alignment when it was last drawn, which a change to the property
|
||||||
/// widget's own property.
|
/// is found against.
|
||||||
pub align_override: bool,
|
|
||||||
/// Its own alignment when it was last drawn. A change to the property is
|
|
||||||
/// found against this even when its parent overrode the alignment.
|
|
||||||
pub own_align: RegionAlign,
|
pub own_align: RegionAlign,
|
||||||
/// The movable region whose coordinates `region` uses.
|
/// The movable region whose coordinates `region` uses.
|
||||||
pub parent_move: MoveIdx,
|
pub parent_move: MoveIdx,
|
||||||
|
/// The mask its drawing is clipped to: one it set itself, or the one it
|
||||||
|
/// inherited from whoever drew it.
|
||||||
pub mask: MaskIdx,
|
pub mask: MaskIdx,
|
||||||
|
/// That inherited one. The two differ exactly where the widget set a
|
||||||
|
/// mask of its own, which is the one it owns and the one a move rewrites
|
||||||
|
/// -- and the one a redraw of it must not be handed back, since setting
|
||||||
|
/// a mask asserts there is none.
|
||||||
|
pub parent_mask: MaskIdx,
|
||||||
pub layer: LayerId,
|
pub layer: LayerId,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,4 +74,12 @@ impl ActiveData {
|
|||||||
pub fn holds_at(&self, px: crate::PxVec2) -> bool {
|
pub fn holds_at(&self, px: crate::PxVec2) -> bool {
|
||||||
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
|
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether what it answered still stands for a box of these pixel
|
||||||
|
/// lengths -- the box it was asked in, where `holds` is about the box its
|
||||||
|
/// answer then chose.
|
||||||
|
pub fn answers_at(&self, px: crate::PxVec2) -> bool {
|
||||||
|
let (_, holds) = self.answer;
|
||||||
|
holds[0].contains(px.x) && holds[1].contains(px.y)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+43
-54
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Len, Px, REL_SHIFT, Rel, fixed::div_toward, fixed::narrow};
|
use crate::{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:
|
||||||
@@ -10,9 +10,9 @@ use std::ops::RangeInclusive;
|
|||||||
///
|
///
|
||||||
/// The ends are lengths on the grid rather than floats with a tolerance
|
/// The ends are lengths on the grid rather than floats with a tolerance
|
||||||
/// around them: a box offered back at the length a widget reported comes back
|
/// around them: a box offered back at the length a widget reported comes back
|
||||||
/// as the same number, so a range means what it says. What widening there is
|
/// as the same number, so a range means what it says. The one place a range
|
||||||
/// belongs to [`Self::through`], which has a rounding to undo, and is derived
|
/// is wider than the length it came from is [`Self::through`], and what it is
|
||||||
/// from that rounding rather than chosen.
|
/// wider by is the floor that inverting a fraction undoes.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct Holds {
|
pub struct Holds {
|
||||||
pub lo: Px,
|
pub lo: Px,
|
||||||
@@ -41,51 +41,29 @@ impl Holds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// What a box has to be for a part of it, `len` of the box long, to stay
|
/// What a box has to be for a part of it, `len` of the box long, to stay
|
||||||
/// in this range. A part with no relative extent is a fixed length: it
|
/// in this range: the exact preimage of `px + floor(rel * box)`, which is
|
||||||
/// was drawn at that length and any box keeps it there.
|
/// the one way a box in pixels is reached. A part with no relative extent
|
||||||
|
/// is a fixed length -- it was drawn at that length and any box keeps it
|
||||||
|
/// there.
|
||||||
///
|
///
|
||||||
/// The way in is `px + rel * box` taken to the nearest step, so a part
|
/// The answer is an interval even where this range is a single length,
|
||||||
/// of exactly `lo` came from anything within half a step of it and the
|
/// because the multiply on the way in drops to the step below and many
|
||||||
/// answer is an interval even where this range is one length. Inverting
|
/// boxes therefore give one length. That is a floor rather than an
|
||||||
/// the length alone instead gives a point that need not even contain the
|
/// allowance: inverting it is two divisions and nothing else, and the
|
||||||
/// box the part was drawn in, which is a range excluding the drawing it
|
/// whole of a box maps back to itself.
|
||||||
/// was made for.
|
|
||||||
pub const fn through(self, len: Len) -> Self {
|
pub const fn through(self, len: Len) -> Self {
|
||||||
let rel = len.rel.raw() as i64;
|
let rel = len.rel.raw() as i64;
|
||||||
if rel == 0 {
|
if rel == 0 {
|
||||||
return Self::ANY;
|
return Self::ANY;
|
||||||
}
|
}
|
||||||
// In half steps, and no more than the arithmetic needs: too wide a
|
|
||||||
// range admits reusing a drawing where it does not hold, and too
|
|
||||||
// narrow a one leaves out the box a drawing was made in, which the
|
|
||||||
// `Holds` assertion in `draw_at` catches. `ROUTES` is at that floor
|
|
||||||
// -- two half steps fires it -- and tightening both ends moved not
|
|
||||||
// one of the rig's work counters, so the slack is not buying reuse.
|
|
||||||
//
|
|
||||||
// `ROUTES` covers a box composed down the chain against the same box
|
|
||||||
// measured against the window: two routes to one number, each
|
|
||||||
// rounding where the other does not. `way_in` covers the multiply
|
|
||||||
// this inverts, which drops a step and only ever downward, so it
|
|
||||||
// belongs at the top of the range alone -- and the whole of a box has
|
|
||||||
// no multiply in it, however many pixels were added to it, since
|
|
||||||
// multiplying by one is exact and taking the pixels off again is too.
|
|
||||||
// Allowing for it there anyway compounded, a step a level down a
|
|
||||||
// chain of widgets each taking the whole of its parent.
|
|
||||||
//
|
|
||||||
// Shifted by half of what a `Rel` counts in, to divide by the
|
|
||||||
// fraction: exact until the division takes it back to the grid.
|
|
||||||
const ROUTES: i64 = 3;
|
|
||||||
let px = len.px.raw() as i64;
|
let px = len.px.raw() as i64;
|
||||||
let half_rel = REL_SHIFT - 1;
|
// `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and
|
||||||
let way_in = match rel == Rel::ONE.raw() as i64 {
|
// `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`.
|
||||||
true => 0,
|
let lo = (self.lo.raw() as i64 - px) << REL_SHIFT;
|
||||||
false => 2,
|
let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1;
|
||||||
};
|
// Dividing by a negative fraction turns the ends around, so which
|
||||||
let lo = ((self.lo.raw() as i64 - px) * 2 - ROUTES) << half_rel;
|
// bound each comes from is decided before dividing rather than by
|
||||||
let hi = ((self.hi.raw() as i64 - px) * 2 + ROUTES + way_in) << half_rel;
|
// taking the min and max of four divisions.
|
||||||
// Dividing by a negative turns the ends around, so which end each
|
|
||||||
// bound comes from is decided before dividing rather than by taking
|
|
||||||
// the min and max of four divisions.
|
|
||||||
match rel > 0 {
|
match rel > 0 {
|
||||||
true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)),
|
true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)),
|
||||||
false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)),
|
false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)),
|
||||||
@@ -136,26 +114,37 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A widget handed the whole of its parent's box, with or without pixels
|
/// A widget handed the whole of its parent's box, with or without pixels
|
||||||
/// taken off it, brings no multiply of its own: only the two routes to
|
/// taken off it, has no fraction to invert: multiplying by one is exact
|
||||||
/// the same length are left to allow for, and not a rounding that did
|
/// and taking the pixels off again is too, so the box maps back to
|
||||||
/// not happen. Widening for it as well grew the interval a level at a
|
/// itself. Allowing for anything here compounded a step a level down a
|
||||||
/// time down a chain of them. Three half steps come back as one whole
|
/// chain of widgets each taking the whole of its parent.
|
||||||
/// one, since dividing by a whole box is dividing by one.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_whole_of_a_box_widens_by_the_routes_alone() {
|
fn the_whole_of_a_box_maps_back_to_itself() {
|
||||||
let at = Px::from_int(956);
|
let at = Px::from_int(956);
|
||||||
let one_step = |len: Px| Holds {
|
assert_eq!(Holds::at(at).through(Len::FULL), Holds::at(at));
|
||||||
lo: len - Px::STEP,
|
|
||||||
hi: len + Px::STEP,
|
|
||||||
};
|
|
||||||
assert_eq!(Holds::at(at).through(Len::FULL), one_step(at));
|
|
||||||
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
|
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Holds::at(at).through(less_eight),
|
Holds::at(at).through(less_eight),
|
||||||
one_step(at + Px::from_int(8))
|
Holds::at(at + Px::from_int(8))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The range is the exact preimage at both ends, so a box one step
|
||||||
|
/// outside it really does give a length outside this range. What a wider
|
||||||
|
/// range costs is a drawing reused where it does not hold.
|
||||||
|
#[test]
|
||||||
|
fn a_box_one_step_outside_the_range_is_outside_it() {
|
||||||
|
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
|
||||||
|
let at = Px::from_int(300);
|
||||||
|
let holds = Holds::at(at).through(part);
|
||||||
|
for inside in [holds.lo, holds.hi] {
|
||||||
|
assert_eq!(part.to_px(inside), at, "{inside:?} left out of {holds:?}");
|
||||||
|
}
|
||||||
|
for outside in [holds.lo.next_down(), holds.hi.next_up()] {
|
||||||
|
assert_ne!(part.to_px(outside), at, "{outside:?} admitted by {holds:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A truncating multiply only ever drops, so the step it needs allowing
|
/// A truncating multiply only ever drops, so the step it needs allowing
|
||||||
/// for on the way in belongs at the top of the range and not the bottom.
|
/// for on the way in belongs at the top of the range and not the bottom.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+12
-6
@@ -68,17 +68,24 @@ impl Moves {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Composes a region held in `idx`'s coordinates down the chain, which is
|
/// The same walk the vertex shader does, in the same `Len` the shader is
|
||||||
/// the same walk the vertex shader does.
|
/// handed, for asking where a drawing will actually land -- hit testing,
|
||||||
|
/// and nothing layout decides on. Layout threads its lengths down the
|
||||||
|
/// draw instead, so no box it compares is composed back up this chain.
|
||||||
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
|
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
|
||||||
let mut region = local;
|
let mut region = local;
|
||||||
|
self.walk(idx, |entry| region = region.within(entry));
|
||||||
|
region
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk(&self, idx: MoveIdx, mut step: impl FnMut(&UiRegion)) {
|
||||||
let mut at = idx;
|
let mut at = idx;
|
||||||
for _ in 0..CHAIN_LIMIT {
|
for _ in 0..CHAIN_LIMIT {
|
||||||
if at == MoveIdx::NONE {
|
if at == MoveIdx::NONE {
|
||||||
return region;
|
return;
|
||||||
}
|
}
|
||||||
let entry = self.arena[at.idx()];
|
let entry = &self.arena[at.idx()];
|
||||||
region = region.within(&entry.region);
|
step(&entry.region);
|
||||||
at = entry.parent;
|
at = entry.parent;
|
||||||
}
|
}
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
@@ -86,7 +93,6 @@ impl Moves {
|
|||||||
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
|
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
|
||||||
and the shader stops at the same depth"
|
and the shader stops at the same depth"
|
||||||
);
|
);
|
||||||
region
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How many slots a region in `idx` is composed through, which is what
|
/// How many slots a region in `idx` is composed through, which is what
|
||||||
|
|||||||
+118
-63
@@ -1,7 +1,7 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
use crate::layout_diagnostics::{self as diag, Counter};
|
use crate::layout_diagnostics::{self as diag, Counter};
|
||||||
use crate::{
|
use crate::{
|
||||||
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget,
|
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget,
|
||||||
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight,
|
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight,
|
||||||
WidgetId, Widgets,
|
WidgetId, Widgets,
|
||||||
render::{
|
render::{
|
||||||
@@ -19,6 +19,11 @@ pub struct Painter<'a> {
|
|||||||
|
|
||||||
/// This widget's box, in the coordinates of `move_idx`.
|
/// This widget's box, in the coordinates of `move_idx`.
|
||||||
pub(super) region: UiRegion,
|
pub(super) region: UiRegion,
|
||||||
|
/// That box in pixels, which its children's are a length of: threaded
|
||||||
|
/// down from the box this widget was given rather than composed back up
|
||||||
|
/// the chain, so every length in layout is one multiply from its
|
||||||
|
/// parent's and [`Holds::through`] inverts exactly that.
|
||||||
|
pub(super) px: PxVec2,
|
||||||
pub(super) mask: MaskIdx,
|
pub(super) mask: MaskIdx,
|
||||||
pub(super) textures: Vec<TextureHandle>,
|
pub(super) textures: Vec<TextureHandle>,
|
||||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||||
@@ -26,10 +31,12 @@ pub struct Painter<'a> {
|
|||||||
/// The children asked about so far, so the first box each was asked in
|
/// The children asked about so far, so the first box each was asked in
|
||||||
/// is the one recorded as its offer.
|
/// is the one recorded as its offer.
|
||||||
pub(super) offered: Vec<WidgetId>,
|
pub(super) offered: Vec<WidgetId>,
|
||||||
/// The box this widget was first asked about in, in pixels.
|
/// The lengths of the box this widget was first asked about in, in
|
||||||
|
/// pixels. Its children's offers are a fraction of it.
|
||||||
pub(super) offered_px: PxVec2,
|
pub(super) offered_px: PxVec2,
|
||||||
/// Whether this draw is in that box, which makes the questions it asks
|
/// Whether this draw is in a box of those lengths, which makes the
|
||||||
/// the ones a cold layout asks and their answers the ones to keep.
|
/// questions it asks the ones a cold layout asks and their answers the
|
||||||
|
/// ones to keep.
|
||||||
pub(super) at_offer: bool,
|
pub(super) at_offer: bool,
|
||||||
/// 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>,
|
||||||
@@ -132,30 +139,35 @@ impl<'a> Painter<'a> {
|
|||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
) -> DrawResult<'s, 'a, W> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
self.widget_at(id, region, None)
|
self.widget_at(id, region, region.size(), [false; 2])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a widget with an alignment chosen by its container rather than
|
/// Draws a widget in `region`, saying what the answer means.
|
||||||
/// the widget's property. Containers use this when the box they hand down
|
///
|
||||||
/// already expresses the size they report around the child.
|
/// `reports_of` is what a fraction the child reports is a fraction of, as
|
||||||
pub fn widget_aligned<'s, W: ?Sized>(
|
/// lengths of this widget's own box. It is the box the child was given
|
||||||
|
/// wherever that box is the child's whole area -- a pad's inset, a stack
|
||||||
|
/// child, a scroll's content -- and a span passes its own extent along
|
||||||
|
/// the row instead: it offers each child the room left from its cursor,
|
||||||
|
/// because a text has to wrap at the width actually there, while
|
||||||
|
/// `rel(0.5)` still means half the span wherever the child sits in it.
|
||||||
|
///
|
||||||
|
/// A `decided` axis is one where this box was chosen from the widget's
|
||||||
|
/// own answer. On those the answer is not placed inside the box again: it
|
||||||
|
/// already is the box, and a fraction taken of it a second time would
|
||||||
|
/// shrink it twice. A container uses that where it hands back exactly
|
||||||
|
/// what a child asked for -- a span placing a child at the length it
|
||||||
|
/// reported, a scroll giving its content the content's own length.
|
||||||
|
pub fn widget_at<'s, W: ?Sized>(
|
||||||
&'s mut self,
|
&'s mut self,
|
||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
align: RegionAlign,
|
reports_of: UiVec2,
|
||||||
) -> DrawResult<'s, 'a, W> {
|
decided: [bool; 2],
|
||||||
self.widget_at(id, region, Some(align))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn widget_at<'s, W: ?Sized>(
|
|
||||||
&'s mut self,
|
|
||||||
id: &'s StrongWidget<W>,
|
|
||||||
region: UiRegion,
|
|
||||||
align_override: Option<RegionAlign>,
|
|
||||||
) -> DrawResult<'s, 'a, W> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
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 declared = self.declared_lens(id);
|
||||||
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.id()));
|
let align = self.rsc.widgets().alignment(id.id());
|
||||||
// Composing `FULL` through a box is not quite the identity in f32,
|
// Composing `FULL` through a box is not quite the identity in f32,
|
||||||
// so a child with nothing declared keeps the box it would have had.
|
// so a child with nothing declared keeps the box it would have had.
|
||||||
let local = match declared.iter().any(Option::is_some) {
|
let local = match declared.iter().any(Option::is_some) {
|
||||||
@@ -176,11 +188,20 @@ impl<'a> Painter<'a> {
|
|||||||
self.children.push(id.id());
|
self.children.push(id.id());
|
||||||
}
|
}
|
||||||
let first_ask = self.offer(id.id());
|
let first_ask = self.offer(id.id());
|
||||||
let offer = match first_ask {
|
let given_len = local.size();
|
||||||
true => local,
|
let offer_len = match first_ask {
|
||||||
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer),
|
true => given_len,
|
||||||
|
false => self
|
||||||
|
.state
|
||||||
|
.active
|
||||||
|
.get(&id.id())
|
||||||
|
.map_or(given_len, |a| a.offer_len),
|
||||||
};
|
};
|
||||||
let answers_offer = self.at_offer && local == offer;
|
let px = given_len.to_px(self.px);
|
||||||
|
let offered_px = offer_len.to_px(self.offered_px);
|
||||||
|
// Whether this ask is the child's offer question, which is a question
|
||||||
|
// about lengths: the same lengths somewhere else is the same question.
|
||||||
|
let answers_offer = self.at_offer && px == offered_px;
|
||||||
// The answer and what it holds for, both about the box asked in. The
|
// The answer and what it holds for, both about the box asked in. The
|
||||||
// child's record may say something else once its drawing has been
|
// child's record may say something else once its drawing has been
|
||||||
// placed: a drawing made again in its placed box holds for that box.
|
// placed: a drawing made again in its placed box holds for that box.
|
||||||
@@ -194,9 +215,11 @@ impl<'a> Painter<'a> {
|
|||||||
parent_move: self.move_idx,
|
parent_move: self.move_idx,
|
||||||
region_node,
|
region_node,
|
||||||
mask: self.mask,
|
mask: self.mask,
|
||||||
offer,
|
given_len,
|
||||||
offered_px: self.px_within_offer(offer),
|
offer_len,
|
||||||
align: align_override,
|
px,
|
||||||
|
offered_px,
|
||||||
|
decided,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
self.rsc,
|
self.rsc,
|
||||||
@@ -212,7 +235,7 @@ impl<'a> Painter<'a> {
|
|||||||
DrawResult {
|
DrawResult {
|
||||||
child: id,
|
child: id,
|
||||||
painter: self,
|
painter: self,
|
||||||
size,
|
size: in_parent_frame(size, reports_of, declared),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,25 +269,26 @@ impl<'a> Painter<'a> {
|
|||||||
|
|
||||||
/// A child's length in the box it is about to be offered, if it can be
|
/// A child's length in the box it is about to be offered, if it can be
|
||||||
/// had without drawing it: from its hint, or from a drawing it already
|
/// had without drawing it: from its hint, or from a drawing it already
|
||||||
/// has that holds for that box.
|
/// has that holds for that box. `reports_of` is what a fraction in the
|
||||||
|
/// answer is a fraction of, as it is for [`Self::widget_at`].
|
||||||
pub fn known_len<W: ?Sized>(
|
pub fn known_len<W: ?Sized>(
|
||||||
&mut self,
|
&mut self,
|
||||||
child: &StrongWidget<W>,
|
child: &StrongWidget<W>,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
|
reports_of: UiVec2,
|
||||||
) -> Option<LayoutLen> {
|
) -> Option<LayoutLen> {
|
||||||
let declared = self.declared_lens(child);
|
let declared = self.declared_lens(child);
|
||||||
let align = self.rsc.widgets().alignment(child.id());
|
let align = self.rsc.widgets().alignment(child.id());
|
||||||
let local = declared_box(region, declared, align);
|
let local = declared_box(region, declared, align);
|
||||||
let within = local.within(&self.region);
|
|
||||||
let first_ask = self.offer(child.id());
|
let first_ask = self.offer(child.id());
|
||||||
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
|
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
|
||||||
active.offer = local;
|
active.offer_len = local.size();
|
||||||
}
|
}
|
||||||
if let Some(hint) = self.size_hint(child, axis) {
|
if let Some(hint) = self.size_hint(child, axis) {
|
||||||
return Some(hint);
|
return Some(hint);
|
||||||
}
|
}
|
||||||
let px = self.state.px_of(self.move_idx, within);
|
let px = local.size().to_px(self.px);
|
||||||
let (size, holds) =
|
let (size, holds) =
|
||||||
self.state
|
self.state
|
||||||
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
|
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
|
||||||
@@ -278,7 +302,7 @@ impl<'a> Painter<'a> {
|
|||||||
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
||||||
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
||||||
}
|
}
|
||||||
Some(size.axis(axis))
|
Some(in_parent_frame(size, reports_of, declared).axis(axis))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this is the first box a child is asked about in during a draw
|
/// Whether this is the first box a child is asked about in during a draw
|
||||||
@@ -292,15 +316,6 @@ impl<'a> Painter<'a> {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pixel size of a part of the box this widget was asked in.
|
|
||||||
fn px_within_offer(&self, local: UiRegion) -> PxVec2 {
|
|
||||||
let size = local.size();
|
|
||||||
PxVec2::new(
|
|
||||||
size.x.to_px(self.offered_px.x),
|
|
||||||
size.y.to_px(self.offered_px.y),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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()) {
|
if !self.size_deps.contains(&child.id()) {
|
||||||
self.size_deps.push(child.id());
|
self.size_deps.push(child.id());
|
||||||
@@ -382,25 +397,25 @@ impl<'a> Painter<'a> {
|
|||||||
/// near edge. A container that reports one child's size gives every child
|
/// near edge. A container that reports one child's size gives every child
|
||||||
/// this, so what it draws is inside what it says it occupies.
|
/// this, so what it draws is inside what it says it occupies.
|
||||||
pub fn box_of(&self, size: Size) -> UiRegion {
|
pub fn box_of(&self, size: Size) -> UiRegion {
|
||||||
placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2])
|
let lens = placed_lens(size, [None; 2], [false; 2]);
|
||||||
|
placed_box(UiRegion::FULL, lens, RegionAlign::NEAR)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This widget's box in pixels. Reading it makes the drawing one that
|
/// This widget's box in pixels. Reading it makes the drawing one that
|
||||||
/// holds for this box only, until `holds` says how far it goes.
|
/// holds for this box only, until `holds` says how far it goes.
|
||||||
pub fn px_size(&mut self) -> PxVec2 {
|
pub fn px_size(&mut self) -> PxVec2 {
|
||||||
let px = self.state.px_of(self.move_idx, self.region);
|
for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) {
|
||||||
for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
|
|
||||||
if *own == Holds::ANY {
|
if *own == Holds::ANY {
|
||||||
*own = Holds::at(len);
|
*own = Holds::at(len);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
px
|
self.px
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One axis of this widget's box in pixels. Prefer this to
|
/// One axis of this widget's box in pixels. Prefer this to
|
||||||
/// [`Self::px_size`] when the other axis cannot affect the drawing.
|
/// [`Self::px_size`] when the other axis cannot affect the drawing.
|
||||||
pub fn px_len(&mut self, axis: Axis) -> Px {
|
pub fn px_len(&mut self, axis: Axis) -> Px {
|
||||||
let len = self.state.px_of(self.move_idx, self.region).axis(axis);
|
let len = self.px.axis(axis);
|
||||||
let own = &mut self.own[axis as usize];
|
let own = &mut self.own[axis as usize];
|
||||||
if *own == Holds::ANY {
|
if *own == Holds::ANY {
|
||||||
*own = Holds::at(len);
|
*own = Holds::at(len);
|
||||||
@@ -415,7 +430,7 @@ impl<'a> Painter<'a> {
|
|||||||
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
|
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
|
||||||
let holds = holds.into();
|
let holds = holds.into();
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
holds.contains(self.state.px_of(self.move_idx, self.region).axis(axis)),
|
holds.contains(self.px.axis(axis)),
|
||||||
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
|
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
|
||||||
self.label(),
|
self.label(),
|
||||||
self.id
|
self.id
|
||||||
@@ -506,6 +521,24 @@ impl PrimitiveLike for &TextureHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A child's answer as lengths of the parent's own box. A widget reports a
|
||||||
|
/// fraction, and `reports_of` is the length that fraction is of: the box the
|
||||||
|
/// child was given wherever that is the child's whole area, and the parent's
|
||||||
|
/// own extent wherever the box is a positional remainder, as a span's is
|
||||||
|
/// after an earlier child. Pixels come through untouched either way, being
|
||||||
|
/// that many pixels wherever they end up. A declared axis is already the
|
||||||
|
/// parent's: it resolved the rule in its own box, and the rule is what the
|
||||||
|
/// report says.
|
||||||
|
fn in_parent_frame(size: Size, reports_of: UiVec2, declared: [Option<LayoutLen>; 2]) -> Size {
|
||||||
|
let mut size = size;
|
||||||
|
for (axis, declared) in AXES.into_iter().zip(declared) {
|
||||||
|
if declared.is_none() {
|
||||||
|
*size.axis_mut(axis) = size.axis(axis).within_len(reports_of.axis(axis));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
/// What a widget declares a length of its box to be. `leftover` is not one: a
|
/// What a widget declares a length of its box to be. `leftover` is not one: a
|
||||||
/// share of what is left over is only a length to the widget dividing one,
|
/// share of what is left over is only a length to the widget dividing one,
|
||||||
/// so it passes up in the size instead.
|
/// so it passes up in the size instead.
|
||||||
@@ -526,31 +559,53 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<LayoutLe
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The box a drawing occupies: the size the widget reported, on the side of
|
/// Whether what a widget reported along an axis is the whole of the box it
|
||||||
/// the box it was asked in that its alignment says. An axis reported as a
|
/// is in rather than a part to be placed inside it. A share fills, because a
|
||||||
/// share fills, because a share is a length only to whoever divides one, and
|
/// share is a length only to whoever divides one, and whoever did is the one
|
||||||
/// whoever did is the one that handed down this box. A declared axis is
|
/// that handed down this box. A declared axis does too: `declared_box`
|
||||||
/// left alone too: `declared_box` already placed it, in the parent's box,
|
/// already placed it, in the parent's box, and the rule's length is what the
|
||||||
/// and the rule's length is what the widget reports there.
|
/// widget reports there. And an axis the parent decided from the answer is
|
||||||
|
/// the answer already.
|
||||||
|
pub(crate) fn fills(reported: LayoutLen, declared: Option<LayoutLen>, decided: bool) -> bool {
|
||||||
|
reported.leftover != Weight::ZERO || declared.is_some() || decided
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What of the box it was given a widget's drawing occupies, as lengths of
|
||||||
|
/// that box: the size it reported wherever that is a part to be placed, and
|
||||||
|
/// the whole of the box wherever the answer fills it.
|
||||||
///
|
///
|
||||||
/// A reported fraction is a fraction of the box the widget drew in, where a
|
/// A reported fraction is a fraction of the box the widget drew in, where a
|
||||||
/// declared one is a fraction of the box its parent handed down -- a span
|
/// declared one is a fraction of the box its parent handed down -- a span
|
||||||
/// reporting `rel(1.0)` means all of what it was given, whatever that was a
|
/// reporting `rel(1.0)` means all of what it was given, whatever that was a
|
||||||
/// fraction of. So this scales by the box rather than composing into it.
|
/// fraction of. So this is a length of the box rather than a length composed
|
||||||
pub(crate) fn placed_box(
|
/// into it, and a box in pixels is this step from the given box's pixels.
|
||||||
region: UiRegion,
|
pub(crate) fn placed_lens(
|
||||||
size: Size,
|
size: Size,
|
||||||
align: RegionAlign,
|
|
||||||
declared: [Option<LayoutLen>; 2],
|
declared: [Option<LayoutLen>; 2],
|
||||||
) -> UiRegion {
|
decided: [bool; 2],
|
||||||
let mut placed = region;
|
) -> UiVec2 {
|
||||||
for (axis, declared) in AXES.into_iter().zip(declared) {
|
let mut lens = UiVec2::FULL_SIZE;
|
||||||
|
for (axis, (declared, decided)) in AXES.into_iter().zip(declared.into_iter().zip(decided)) {
|
||||||
let reported = size.axis(axis);
|
let reported = size.axis(axis);
|
||||||
if reported.leftover != Weight::ZERO || declared.is_some() {
|
if !fills(reported, declared, decided) {
|
||||||
|
*lens.axis_mut(axis) = Len::from_parts(reported.rel, reported.px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lens
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where that drawing sits: those lengths taken of the box the widget was
|
||||||
|
/// asked in, on the side of it that the widget's alignment says.
|
||||||
|
pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> UiRegion {
|
||||||
|
let mut placed = region;
|
||||||
|
for axis in AXES {
|
||||||
|
// The whole of the box is already where it sits, and the arithmetic
|
||||||
|
// below is the identity for it.
|
||||||
|
if lens.axis(axis) == Len::FULL {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let span = placed.axis_mut(axis);
|
let span = placed.axis_mut(axis);
|
||||||
let len = span.len().scale(reported.rel) + Len::from_parts(Rel::ZERO, reported.px);
|
let len = lens.axis(axis).within_len(span.len());
|
||||||
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
||||||
span.end = span.start + len;
|
span.end = span.start + len;
|
||||||
}
|
}
|
||||||
|
|||||||
+285
-270
@@ -1,9 +1,9 @@
|
|||||||
#[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::ui::painter::{declared_box, declared_lens, placed_box};
|
use crate::ui::painter::{declared_box, declared_lens, placed_box, placed_lens};
|
||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
|
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
|
||||||
PixelRegion, Px, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight,
|
PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight,
|
||||||
WidgetId, Widgets,
|
WidgetId, Widgets,
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
@@ -20,13 +20,20 @@ pub(super) struct DrawInfo {
|
|||||||
pub parent_move: MoveIdx,
|
pub parent_move: MoveIdx,
|
||||||
pub region_node: bool,
|
pub region_node: bool,
|
||||||
pub mask: MaskIdx,
|
pub mask: MaskIdx,
|
||||||
/// The box it was first asked about in, as a part of its parent's, and
|
/// The box its parent gave it, as lengths of the parent's own box, and
|
||||||
/// that box in pixels.
|
/// the lengths of the box it was first asked about in the same form.
|
||||||
pub offer: UiRegion,
|
/// Both describe the box the *parent* stated, so the second, placing ask
|
||||||
|
/// carries them unchanged while its own region is the placement inside.
|
||||||
|
pub given_len: UiVec2,
|
||||||
|
pub offer_len: UiVec2,
|
||||||
|
/// This ask's box in pixels, and the offer's: one multiply from the
|
||||||
|
/// parent's own, which is where every pixel length in layout comes from.
|
||||||
|
pub px: PxVec2,
|
||||||
pub offered_px: PxVec2,
|
pub offered_px: PxVec2,
|
||||||
/// A container's answer for where the widget sits. `None` uses the
|
/// The axes along which the parent chose this box from the widget's own
|
||||||
/// widget's own property.
|
/// answer, so the answer is not placed inside it again. See
|
||||||
pub align: Option<RegionAlign>,
|
/// [`Painter::widget_at`].
|
||||||
|
pub decided: [bool; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UiRenderState {
|
pub struct UiRenderState {
|
||||||
@@ -35,10 +42,10 @@ pub struct UiRenderState {
|
|||||||
pub(super) output_size: PxVec2,
|
pub(super) output_size: PxVec2,
|
||||||
|
|
||||||
old_root: Option<WidgetId>,
|
old_root: Option<WidgetId>,
|
||||||
/// The slot every chain bottoms out in, holding the output as a box.
|
|
||||||
root_move: MoveIdx,
|
|
||||||
/// Whether the output has changed since the last update. A frame is
|
/// Whether the output has changed since the last update. A frame is
|
||||||
/// owed for that whether or not anything has to be drawn again.
|
/// owed for that whether or not anything has to be drawn again: every
|
||||||
|
/// fraction becomes pixels against the output, in the shader's uniform
|
||||||
|
/// as well as here.
|
||||||
resized: bool,
|
resized: bool,
|
||||||
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
||||||
/// replaces that while its children go on pointing at the slot.
|
/// replaces that while its children go on pointing at the slot.
|
||||||
@@ -50,6 +57,9 @@ pub struct UiRenderState {
|
|||||||
/// Whether this frame contains a declared-length change, so any dirty
|
/// Whether this frame contains a declared-length change, so any dirty
|
||||||
/// dependent replaces its answer too.
|
/// dependent replaces its answer too.
|
||||||
replace_answers: bool,
|
replace_answers: bool,
|
||||||
|
/// Widgets waiting for an ancestor to draw them, so the walk down the
|
||||||
|
/// depths does not pick one up again at its own depth.
|
||||||
|
deferred: crate::util::HashSet<WidgetId>,
|
||||||
pub moves: Moves,
|
pub moves: Moves,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,50 +73,61 @@ impl UiRenderState {
|
|||||||
slots: Default::default(),
|
slots: Default::default(),
|
||||||
answer_invalid: Default::default(),
|
answer_invalid: Default::default(),
|
||||||
replace_answers: false,
|
replace_answers: false,
|
||||||
|
deferred: Default::default(),
|
||||||
moves: Default::default(),
|
moves: Default::default(),
|
||||||
root_move: MoveIdx::NONE,
|
|
||||||
resized: false,
|
resized: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The window as a box, so a chain bottoms out in one rather than in a
|
|
||||||
/// multiplication applied after it. Composing through a box held in
|
|
||||||
/// pixels leaves everything below it in pixels, which is why nothing
|
|
||||||
/// downstream has to know the output's size to resolve a position.
|
|
||||||
fn write_root(&mut self) {
|
|
||||||
let region = UiRegion::new(
|
|
||||||
UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, self.output_size.x)),
|
|
||||||
UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, self.output_size.y)),
|
|
||||||
);
|
|
||||||
match self.root_move == MoveIdx::NONE {
|
|
||||||
true => self.root_move = self.moves.push(MoveIdx::NONE, region),
|
|
||||||
false => self.moves.set(self.root_move, region),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The window, in whatever the platform measures it in, onto the grid
|
/// The window, in whatever the platform measures it in, onto the grid
|
||||||
/// everything below it is decided on.
|
/// everything below it is decided on. No move entry holds it: a chain
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
/// bottoms out in `MoveIdx::NONE`, which is the window, and the window's
|
||||||
|
/// size is applied where a fraction becomes pixels -- here in `to_px`,
|
||||||
|
/// and in the shader by its uniform. A resize therefore rewrites no
|
||||||
|
/// retained entry at all.
|
||||||
|
///
|
||||||
|
/// The root is the only widget a resize marks, and only where the new
|
||||||
|
/// output falls outside what its answer holds for: that range is the
|
||||||
|
/// intersection of everything under it, so admitting the new output says
|
||||||
|
/// the whole tree still stands. Where it does not, the ordinary walk
|
||||||
|
/// draws the root, and each widget's own range decides how far down the
|
||||||
|
/// new length reaches.
|
||||||
|
pub fn resize(&mut self, size: impl Into<Vec2>, widgets: &mut Widgets) {
|
||||||
let size = PxVec2::from_f32(size.into());
|
let size = PxVec2::from_f32(size.into());
|
||||||
if size == self.output_size {
|
if size == self.output_size {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.output_size = size;
|
self.output_size = size;
|
||||||
self.write_root();
|
|
||||||
self.resized = true;
|
self.resized = true;
|
||||||
|
let Some(root) = self.old_root else { return };
|
||||||
|
let stands = self
|
||||||
|
.active
|
||||||
|
.get(&root)
|
||||||
|
.is_some_and(|active| active.answers_at(active.given_len.to_px(size)));
|
||||||
|
if !stands {
|
||||||
|
widgets.needs_redraw.insert(root);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn root_info(&self) -> DrawInfo {
|
/// The root is asked about in the output: the window is where a fraction
|
||||||
|
/// becomes pixels rather than a box of its own, so the root's box is the
|
||||||
|
/// first length threaded down. Its own rules narrow that box, and where
|
||||||
|
/// they do the narrowed box is also the offer -- nothing above it chose
|
||||||
|
/// anything else.
|
||||||
|
fn root_info(&self, region: UiRegion) -> DrawInfo {
|
||||||
|
let px = region.size().to_px(self.output_size);
|
||||||
DrawInfo {
|
DrawInfo {
|
||||||
layer: 0,
|
layer: 0,
|
||||||
parent: None,
|
parent: None,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
parent_move: self.root_move,
|
parent_move: MoveIdx::NONE,
|
||||||
region_node: false,
|
region_node: false,
|
||||||
mask: MaskIdx::NONE,
|
mask: MaskIdx::NONE,
|
||||||
offer: UiRegion::FULL,
|
given_len: region.size(),
|
||||||
offered_px: self.output_size,
|
offer_len: UiVec2::FULL_SIZE,
|
||||||
align: None,
|
px,
|
||||||
|
offered_px: px,
|
||||||
|
decided: [false; 2],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,17 +160,6 @@ impl UiRenderState {
|
|||||||
if self.root_changed(root) {
|
if self.root_changed(root) {
|
||||||
self.redraw_all(root, rsc);
|
self.redraw_all(root, rsc);
|
||||||
self.old_root = root.map(|r| r.id());
|
self.old_root = root.map(|r| r.id());
|
||||||
} else if let Some(root) = root
|
|
||||||
&& self.resized
|
|
||||||
{
|
|
||||||
// The output is the root's box, so a resize is that box changing
|
|
||||||
// length, found the way every other box change is found. Before
|
|
||||||
// anything dirty settles, so that whatever a new output draws
|
|
||||||
// again is drawn once, in the box it will have.
|
|
||||||
let info = self.root_info();
|
|
||||||
let region = Self::root_region(root.id(), rsc.widgets());
|
|
||||||
let answer = self.draw_inner(root.id(), region, info, None, rsc);
|
|
||||||
self.active.get_mut(&root.id()).unwrap().answer = answer;
|
|
||||||
}
|
}
|
||||||
self.resized = false;
|
self.resized = false;
|
||||||
if rsc.widgets().has_updates() {
|
if rsc.widgets().has_updates() {
|
||||||
@@ -163,11 +173,9 @@ impl UiRenderState {
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
let _layout = diag::timer(TimerKind::FullLayout);
|
let _layout = diag::timer(TimerKind::FullLayout);
|
||||||
self.clear(rsc);
|
self.clear(rsc);
|
||||||
// free all resources & cache
|
|
||||||
self.write_root();
|
|
||||||
if let Some(id) = root {
|
if let Some(id) = root {
|
||||||
let info = self.root_info();
|
|
||||||
let region = Self::root_region(id.id(), rsc.widgets());
|
let region = Self::root_region(id.id(), rsc.widgets());
|
||||||
|
let info = self.root_info(region);
|
||||||
self.draw_inner(id.id(), region, info, None, rsc);
|
self.draw_inner(id.id(), region, info, None, rsc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,88 +199,94 @@ impl UiRenderState {
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
{
|
{
|
||||||
diag::bump(Counter::DrawRequests);
|
diag::bump(Counter::DrawRequests);
|
||||||
diag::draw_request(
|
diag::draw_request(id, info.parent, region, info.px, info.region_node);
|
||||||
id,
|
|
||||||
info.parent,
|
|
||||||
region,
|
|
||||||
self.px_of(info.parent_move, region),
|
|
||||||
info.region_node,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let own_align = rsc.widgets().alignment(id);
|
let align = rsc.widgets().alignment(id);
|
||||||
let align = info.align.unwrap_or(own_align);
|
// Nothing this widget measured can be dirty while it draws: layout is
|
||||||
let replace_answer = self.answer_invalid.remove(&id)
|
// one bottom-up walk, so anything deeper has settled or deferred to
|
||||||
|| (self.replace_answers
|
// its own parent, and a deferred one leaves that parent marked.
|
||||||
&& (rsc.widgets().needs_redraw.contains(&id)
|
let stale = rsc.widgets().needs_redraw.contains(&id);
|
||||||
|| self.dirty_size_under(id, rsc.widgets())));
|
let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale);
|
||||||
let retained = match replace_answer {
|
let retained = match replace_answer || stale {
|
||||||
true => None,
|
true => None,
|
||||||
false => self
|
false => self
|
||||||
.retained_answer(id, region, info, rsc.widgets())
|
.retained_answer(id, info)
|
||||||
.or_else(|| self.try_reuse(id, region, info, rsc)),
|
.or_else(|| self.try_reuse(id, region, info, rsc)),
|
||||||
};
|
};
|
||||||
let answer = retained.unwrap_or_else(|| {
|
let answer = retained.unwrap_or_else(|| {
|
||||||
if old.is_none() {
|
if old.is_none() {
|
||||||
old = self.remove(id, false, rsc);
|
old = self.remove(id, false, rsc);
|
||||||
}
|
}
|
||||||
self.draw_at(id, region, info, align, old.take(), rsc)
|
self.draw_at(id, region, info, old.take(), rsc)
|
||||||
});
|
});
|
||||||
|
|
||||||
let declared = declared_lens(rsc.widgets(), id);
|
let declared = declared_lens(rsc.widgets(), id);
|
||||||
// A near-edge override means the caller already chose this box from
|
// The second, final ask is in a box chosen from the answer on both
|
||||||
// the child's answer. Applying the answer again would compound the
|
// axes, which is also what makes it terminate.
|
||||||
// placement; it is also how the second, final ask terminates.
|
let lens = placed_lens(answer.0, declared, info.decided);
|
||||||
let placed = match info.align == Some(RegionAlign::NEAR) {
|
let placed = placed_box(region, lens, align);
|
||||||
true => region,
|
|
||||||
false => placed_box(region, answer.0, align, declared),
|
|
||||||
};
|
|
||||||
let placed_info = DrawInfo {
|
let placed_info = DrawInfo {
|
||||||
align: Some(RegionAlign::NEAR),
|
px: lens.to_px(info.px),
|
||||||
|
decided: [true; 2],
|
||||||
..info
|
..info
|
||||||
};
|
};
|
||||||
// The symbolic box can be unchanged while its parent slot changed
|
self.place(id, placed, placed_info, rsc);
|
||||||
// pixel size. Reuse checks the resolved box even in that case.
|
|
||||||
if self.try_reuse(id, placed, placed_info, rsc).is_none() {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::PlaceRedraws);
|
|
||||||
let old = self.remove(id, false, rsc);
|
|
||||||
self.draw_at(id, placed, placed_info, RegionAlign::NEAR, old, rsc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The answer is only reusable while both parts of the operation are:
|
// The answer is only reusable while both parts of the operation are:
|
||||||
// what the widget reported in the offered box, and what it drew in
|
// what the widget reported in the box it was asked in, and what it
|
||||||
// the box its report selected. Express the latter's contract back in
|
// drew in the box its report selected. Express the latter's contract
|
||||||
// terms of the offered box before handing it to the parent.
|
// back in terms of the box asked in before handing it to the parent.
|
||||||
let drawing_holds = self.active[&id].holds;
|
let drawing_holds = self.active[&id].holds;
|
||||||
let mut settled = answer;
|
let mut settled = answer;
|
||||||
for axis in AXES {
|
for axis in AXES {
|
||||||
let reported = answer.0.axis(axis);
|
|
||||||
let placed_len =
|
|
||||||
match reported.leftover != Weight::ZERO || declared[axis as usize].is_some() {
|
|
||||||
true => Len::FULL,
|
|
||||||
false => Len::from_parts(reported.rel, reported.px),
|
|
||||||
};
|
|
||||||
settled.1[axis as usize] =
|
settled.1[axis as usize] =
|
||||||
settled.1[axis as usize].and(drawing_holds[axis as usize].through(placed_len));
|
settled.1[axis as usize].and(drawing_holds[axis as usize].through(lens.axis(axis)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
active.offer = info.offer;
|
// Whoever asked owns how the box was reached: the box it stated, and
|
||||||
|
// what of that box the answer then took. A local redraw asks the
|
||||||
|
// same question again from these.
|
||||||
|
active.given = region;
|
||||||
|
active.given_len = info.given_len;
|
||||||
|
active.offer_len = info.offer_len;
|
||||||
active.answer = settled;
|
active.answer = settled;
|
||||||
active.align = align;
|
active.decided = info.decided;
|
||||||
active.align_override = info.align.is_some();
|
active.own_align = align;
|
||||||
active.own_align = own_align;
|
// A subtree can be reused whole under a different parent -- same box,
|
||||||
active.depth = info.depth;
|
// same layer, same region node -- and nothing in the drawing says it
|
||||||
|
// changed hands. Two things read who its parent is: a deferral, which
|
||||||
|
// marks whoever has it to draw, and the old parent's list of children,
|
||||||
|
// which its next draw undraws whatever is missing from.
|
||||||
|
let old_parent = std::mem::replace(&mut active.parent, info.parent);
|
||||||
|
if old_parent != info.parent
|
||||||
|
&& let Some(old_parent) = old_parent
|
||||||
|
&& let Some(old_parent) = self.active.get_mut(&old_parent)
|
||||||
|
{
|
||||||
|
old_parent.children.retain(|child| *child != id);
|
||||||
|
}
|
||||||
settled
|
settled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draws a widget in the final box its answer chose, reusing the drawing
|
||||||
|
/// already there where its retained contract holds for that box. The
|
||||||
|
/// symbolic box can be unchanged while the box it sits in changed pixel
|
||||||
|
/// length, so what reuse checks is the box in pixels.
|
||||||
|
fn place(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
|
||||||
|
if self.try_reuse(id, placed, info, rsc).is_none() {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::PlaceRedraws);
|
||||||
|
let old = self.remove(id, false, rsc);
|
||||||
|
self.draw_at(id, placed, info, old, rsc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Calls a widget's `draw` and keeps what it drew in `region`.
|
/// Calls a widget's `draw` and keeps what it drew in `region`.
|
||||||
fn draw_at(
|
fn draw_at(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
info: DrawInfo,
|
info: DrawInfo,
|
||||||
align: RegionAlign,
|
|
||||||
old: Option<ActiveData>,
|
old: Option<ActiveData>,
|
||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) -> (Size, [Holds; 2]) {
|
) -> (Size, [Holds; 2]) {
|
||||||
@@ -293,12 +307,17 @@ impl UiRenderState {
|
|||||||
None => (Vec::new(), None),
|
None => (Vec::new(), None),
|
||||||
};
|
};
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
let px = self.px_of(move_idx, local);
|
// A box of the offered lengths asks the offer's question wherever it
|
||||||
let at_offer = same_px(px, info.offered_px);
|
// sits, since what a drawing depends on is its lengths -- and
|
||||||
|
// equality is the comparison, these being counts of a step rather
|
||||||
|
// than floats to be compared for nearness.
|
||||||
|
let px = info.px;
|
||||||
|
let at_offer = px == info.offered_px;
|
||||||
|
|
||||||
let mut painter = Painter {
|
let mut painter = Painter {
|
||||||
state: self,
|
state: self,
|
||||||
region: local,
|
region: local,
|
||||||
|
px,
|
||||||
mask: info.mask,
|
mask: info.mask,
|
||||||
layer: info.layer,
|
layer: info.layer,
|
||||||
own_layer: info.layer,
|
own_layer: info.layer,
|
||||||
@@ -332,6 +351,7 @@ impl UiRenderState {
|
|||||||
state: _,
|
state: _,
|
||||||
rsc: _,
|
rsc: _,
|
||||||
region: _,
|
region: _,
|
||||||
|
px: _,
|
||||||
mask,
|
mask,
|
||||||
textures,
|
textures,
|
||||||
primitives,
|
primitives,
|
||||||
@@ -401,9 +421,11 @@ impl UiRenderState {
|
|||||||
parent_move: move_idx,
|
parent_move: move_idx,
|
||||||
region_node: false,
|
region_node: false,
|
||||||
mask,
|
mask,
|
||||||
offer: UiRegion::FULL,
|
given_len: UiVec2::FULL_SIZE,
|
||||||
|
offer_len: UiVec2::FULL_SIZE,
|
||||||
|
px,
|
||||||
offered_px: px,
|
offered_px: px,
|
||||||
align: None,
|
decided: [false; 2],
|
||||||
},
|
},
|
||||||
rsc,
|
rsc,
|
||||||
);
|
);
|
||||||
@@ -414,7 +436,12 @@ impl UiRenderState {
|
|||||||
let active = ActiveData {
|
let active = ActiveData {
|
||||||
id,
|
id,
|
||||||
region,
|
region,
|
||||||
offer: info.offer,
|
// The box a placing ask draws in is a part of the one its parent
|
||||||
|
// gave, which `draw_inner` writes back over these once the
|
||||||
|
// placement is done.
|
||||||
|
given: region,
|
||||||
|
given_len: info.given_len,
|
||||||
|
offer_len: info.offer_len,
|
||||||
// Whoever asked writes the answer, if this was the asking.
|
// Whoever asked writes the answer, if this was the asking.
|
||||||
answer: old_answer.unwrap_or((size, holds)),
|
answer: old_answer.unwrap_or((size, holds)),
|
||||||
size,
|
size,
|
||||||
@@ -427,12 +454,12 @@ impl UiRenderState {
|
|||||||
children,
|
children,
|
||||||
size_deps,
|
size_deps,
|
||||||
declared: declared_lens(rsc.widgets(), id),
|
declared: declared_lens(rsc.widgets(), id),
|
||||||
align,
|
decided: info.decided,
|
||||||
align_override: info.align.is_some(),
|
|
||||||
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,
|
||||||
mask,
|
mask,
|
||||||
|
parent_mask: info.mask,
|
||||||
layer: info.layer,
|
layer: info.layer,
|
||||||
};
|
};
|
||||||
rsc.on_draw(&active);
|
rsc.on_draw(&active);
|
||||||
@@ -460,20 +487,6 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pixel size of a region held in `slot`'s coordinates.
|
|
||||||
pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> PxVec2 {
|
|
||||||
self.moves
|
|
||||||
.resolve(slot, region)
|
|
||||||
.size()
|
|
||||||
.to_px(self.output_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Where a region held in `slot`'s coordinates lands on screen, which is
|
|
||||||
/// the walk the vertex shader does.
|
|
||||||
fn px_region(&self, slot: MoveIdx, region: UiRegion) -> PixelRegion {
|
|
||||||
self.moves.resolve(slot, region).to_px(self.output_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A clean widget's retained answer, if that answer holds for a box of
|
/// A clean widget's retained answer, if that answer holds for a box of
|
||||||
/// `px`. This does not move its drawing, which may already be in the box
|
/// `px`. This does not move its drawing, which may already be in the box
|
||||||
/// that answer placed it in.
|
/// that answer placed it in.
|
||||||
@@ -484,7 +497,7 @@ impl UiRenderState {
|
|||||||
parent_move: MoveIdx,
|
parent_move: MoveIdx,
|
||||||
widgets: &Widgets,
|
widgets: &Widgets,
|
||||||
) -> Option<(Size, [Holds; 2])> {
|
) -> Option<(Size, [Holds; 2])> {
|
||||||
if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) {
|
if widgets.needs_redraw.contains(&id) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let active = self.active.get(&id)?;
|
let active = self.active.get(&id)?;
|
||||||
@@ -498,17 +511,9 @@ impl UiRenderState {
|
|||||||
|
|
||||||
/// The answer to an ask can be retained independently of where its
|
/// The answer to an ask can be retained independently of where its
|
||||||
/// drawing ended up. Alignment is exactly that case: the first box is the
|
/// drawing ended up. Alignment is exactly that case: the first box is the
|
||||||
/// question and the smaller placed box holds the drawing.
|
/// question and the smaller placed box holds the drawing. Whether the
|
||||||
fn retained_answer(
|
/// answer is stale at all is its caller's question, asked once there.
|
||||||
&self,
|
fn retained_answer(&self, id: WidgetId, info: DrawInfo) -> Option<(Size, [Holds; 2])> {
|
||||||
id: WidgetId,
|
|
||||||
region: UiRegion,
|
|
||||||
info: DrawInfo,
|
|
||||||
widgets: &Widgets,
|
|
||||||
) -> Option<(Size, [Holds; 2])> {
|
|
||||||
if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let active = self.active.get(&id)?;
|
let active = self.active.get(&id)?;
|
||||||
let has_region_node = active.move_idx != active.parent_move;
|
let has_region_node = active.move_idx != active.parent_move;
|
||||||
if !active.drawn
|
if !active.drawn
|
||||||
@@ -517,48 +522,42 @@ impl UiRenderState {
|
|||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let px = self.px_of(info.parent_move, region);
|
active.answers_at(info.px).then_some(active.answer)
|
||||||
let (size, holds) = active.answer;
|
|
||||||
(holds[0].contains(px.x) && holds[1].contains(px.y)).then_some((size, holds))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether anything whose size this widget's own size was read from is
|
/// The pixel lengths of the box a widget was given and of the box it was
|
||||||
/// dirty. Not needed for the answer to come right -- a changed size
|
/// first asked about, which is what a local redraw needs to ask the
|
||||||
/// reaches its reader in any order -- but a reader that asks first
|
/// question its parent asked.
|
||||||
/// lays out once rather than twice.
|
///
|
||||||
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
/// Both are threaded down from the window a length of a box at a time,
|
||||||
self.active.get(&id).is_some_and(|active| {
|
/// and this takes the same steps back up: a widget's box is a length of
|
||||||
active.size_deps.iter().any(|child| {
|
/// the box its parent drew in, and its offer a length of the box its
|
||||||
widgets.needs_redraw.contains(child) || self.dirty_size_under(*child, widgets)
|
/// parent was itself offered. Neither chain has a coordinate frame in it,
|
||||||
})
|
/// so neither breaks at a region node -- and both land on the numbers a
|
||||||
})
|
/// cold layout computes, rather than near them.
|
||||||
}
|
fn asked_px(&self, id: WidgetId) -> (PxVec2, PxVec2) {
|
||||||
|
|
||||||
/// The first box a widget was asked about, re-expressed in the coordinate
|
|
||||||
/// space its drawing uses. Keeping the relative box and composing it
|
|
||||||
/// again avoids rebuilding a shifted box from rounded pixel lengths.
|
|
||||||
fn offered_region(&self, id: WidgetId) -> UiRegion {
|
|
||||||
let active = &self.active[&id];
|
let active = &self.active[&id];
|
||||||
let parent_region = match active.parent.and_then(|id| self.active.get(&id)) {
|
// Nothing above the root: the window is where a fraction becomes
|
||||||
Some(parent) if parent.move_idx == active.parent_move => {
|
// pixels, which is also the whole of the box the root is given.
|
||||||
if parent.move_idx == parent.parent_move {
|
let (parent_px, parent_offer) = match active.parent.and_then(|p| self.active.get(&p)) {
|
||||||
self.offered_region(parent.id)
|
Some(parent) => {
|
||||||
} else {
|
let (given, offer) = self.asked_px(parent.id);
|
||||||
UiRegion::FULL
|
let lens = placed_lens(parent.answer.0, parent.declared, parent.decided);
|
||||||
}
|
(lens.to_px(given), offer)
|
||||||
}
|
}
|
||||||
_ => UiRegion::FULL,
|
None => (self.output_size, self.output_size),
|
||||||
};
|
|
||||||
let mut offered = match active.offer == UiRegion::FULL {
|
|
||||||
true => parent_region,
|
|
||||||
false => active.offer.within(&parent_region),
|
|
||||||
};
|
};
|
||||||
|
let px = active.given_len.to_px(parent_px);
|
||||||
|
let mut offered = active.offer_len.to_px(parent_offer);
|
||||||
for axis in AXES {
|
for axis in AXES {
|
||||||
|
// A declared length is resolved by whoever drew the widget, in
|
||||||
|
// the box that widget drew in, so the box it has is the box it
|
||||||
|
// was asked about however the offer above it moved.
|
||||||
if active.declared[axis as usize].is_some() {
|
if active.declared[axis as usize].is_some() {
|
||||||
*offered.axis_mut(axis) = *active.region.axis(axis);
|
*offered.axis_mut(axis) = px.axis(axis);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
offered
|
(px, offered)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reuses the actual drawing in a new box if its retained contract holds
|
/// Reuses the actual drawing in a new box if its retained contract holds
|
||||||
@@ -615,10 +614,10 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// In pixels, because `region` is a fraction of a slot's box and that
|
// In pixels, because `region` is a fraction of the box its parent
|
||||||
// box may be what changed -- an unchanged fraction of a box half the
|
// drew in and that box may be what changed -- an unchanged fraction
|
||||||
// size is half the widget.
|
// of a box half the size is half the widget.
|
||||||
if !active.holds_at(self.px_of(info.parent_move, region)) {
|
if !active.holds_at(info.px) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
{
|
{
|
||||||
diag::bump(Counter::ReuseOutside);
|
diag::bump(Counter::ReuseOutside);
|
||||||
@@ -627,24 +626,22 @@ impl UiRenderState {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let moved = active.region != region;
|
let moved = active.region != region;
|
||||||
let (answer, old_region, slot, mask) = (
|
let (answer, old_region, slot) =
|
||||||
(active.size, active.holds),
|
((active.size, active.holds), active.region, active.move_idx);
|
||||||
active.region,
|
|
||||||
active.move_idx,
|
|
||||||
info.mask,
|
|
||||||
);
|
|
||||||
if moved {
|
if moved {
|
||||||
if has_region_node {
|
if has_region_node {
|
||||||
self.moves.set(slot, region);
|
self.moves.set(slot, region);
|
||||||
} else {
|
} else {
|
||||||
let remap = RegionRemap::new(old_region, region)?;
|
let remap = RegionRemap::new(old_region, region)?;
|
||||||
self.remap_subtree(id, &remap, info.parent_move, mask, rsc);
|
self.remap_subtree(id, &remap, info.parent_move, rsc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.redepth(id, info.depth);
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
active.region = region;
|
active.region = region;
|
||||||
active.offer = info.offer;
|
active.given = region;
|
||||||
active.depth = info.depth;
|
active.given_len = info.given_len;
|
||||||
|
active.offer_len = info.offer_len;
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
{
|
{
|
||||||
match (moved, has_region_node) {
|
match (moved, has_region_node) {
|
||||||
@@ -668,6 +665,24 @@ impl UiRenderState {
|
|||||||
Some(answer)
|
Some(answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A reused subtree keeps its shape, so every widget in it moves by the
|
||||||
|
/// same amount -- and where the top of it did not move, none of it did,
|
||||||
|
/// which is what makes this free in the ordinary case.
|
||||||
|
fn redepth(&mut self, id: WidgetId, depth: usize) {
|
||||||
|
let Some(active) = self.active.get_mut(&id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if active.depth == depth {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
active.depth = depth;
|
||||||
|
let children = active.children.len();
|
||||||
|
for index in 0..children {
|
||||||
|
let child = self.active[&id].children[index];
|
||||||
|
self.redepth(child, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Re-expresses an ordinary retained subtree in a new parent region.
|
/// Re-expresses an ordinary retained subtree in a new parent region.
|
||||||
/// An independently movable descendant needs only its own region changed;
|
/// An independently movable descendant needs only its own region changed;
|
||||||
/// its contents stay in that region's coordinate space.
|
/// its contents stay in that region's coordinate space.
|
||||||
@@ -676,10 +691,10 @@ impl UiRenderState {
|
|||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
remap: &RegionRemap,
|
remap: &RegionRemap,
|
||||||
parent_move: MoveIdx,
|
parent_move: MoveIdx,
|
||||||
inherited_mask: MaskIdx,
|
|
||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) {
|
) {
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
|
active.given = remap.apply(active.given);
|
||||||
if active.move_idx != parent_move {
|
if active.move_idx != parent_move {
|
||||||
let region = remap.apply(active.region);
|
let region = remap.apply(active.region);
|
||||||
active.region = region;
|
active.region = region;
|
||||||
@@ -691,16 +706,18 @@ impl UiRenderState {
|
|||||||
*region = remap.apply(*region);
|
*region = remap.apply(*region);
|
||||||
}
|
}
|
||||||
active.region = remap.apply(active.region);
|
active.region = remap.apply(active.region);
|
||||||
let mask = active.mask;
|
let own_mask = (active.mask != active.parent_mask).then_some(active.mask);
|
||||||
let children = active.children.len();
|
let children = active.children.len();
|
||||||
if mask != inherited_mask && mask != MaskIdx::NONE {
|
// A mask the widget set itself moves with it; one it inherited
|
||||||
let mask = rsc.ui_mut().masks.get_mut(mask);
|
// belongs to the widget that set it, and moves there or not at all.
|
||||||
|
if let Some(idx) = own_mask {
|
||||||
|
let mask = rsc.ui_mut().masks.get_mut(idx);
|
||||||
debug_assert_eq!(mask.move_idx, parent_move);
|
debug_assert_eq!(mask.move_idx, parent_move);
|
||||||
mask.region = remap.apply(mask.region);
|
mask.region = remap.apply(mask.region);
|
||||||
}
|
}
|
||||||
for index in 0..children {
|
for index in 0..children {
|
||||||
let child = self.active[&id].children[index];
|
let child = self.active[&id].children[index];
|
||||||
self.remap_subtree(child, remap, parent_move, mask, rsc);
|
self.remap_subtree(child, remap, parent_move, rsc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,7 +792,9 @@ impl UiRenderState {
|
|||||||
ActiveData {
|
ActiveData {
|
||||||
id,
|
id,
|
||||||
region: UiRegion::FULL,
|
region: UiRegion::FULL,
|
||||||
offer: UiRegion::FULL,
|
given: UiRegion::FULL,
|
||||||
|
given_len: UiVec2::FULL_SIZE,
|
||||||
|
offer_len: UiVec2::FULL_SIZE,
|
||||||
answer: (size, [Holds::ANY; 2]),
|
answer: (size, [Holds::ANY; 2]),
|
||||||
size,
|
size,
|
||||||
holds: [Holds::ANY; 2],
|
holds: [Holds::ANY; 2],
|
||||||
@@ -788,11 +807,11 @@ impl UiRenderState {
|
|||||||
size_deps: Vec::new(),
|
size_deps: Vec::new(),
|
||||||
move_idx: info.parent_move,
|
move_idx: info.parent_move,
|
||||||
declared: [None; 2],
|
declared: [None; 2],
|
||||||
align: RegionAlign::default(),
|
decided: [false; 2],
|
||||||
align_override: false,
|
|
||||||
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,
|
||||||
|
parent_mask: info.mask,
|
||||||
layer: info.layer,
|
layer: info.layer,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -808,7 +827,6 @@ impl UiRenderState {
|
|||||||
self.answer_invalid.clear();
|
self.answer_invalid.clear();
|
||||||
self.replace_answers = false;
|
self.replace_answers = false;
|
||||||
self.moves.clear();
|
self.moves.clear();
|
||||||
self.root_move = MoveIdx::NONE;
|
|
||||||
self.layers.clear();
|
self.layers.clear();
|
||||||
rsc.widgets_mut().needs_redraw.clear();
|
rsc.widgets_mut().needs_redraw.clear();
|
||||||
self.free(rsc);
|
self.free(rsc);
|
||||||
@@ -829,18 +847,33 @@ 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);
|
||||||
// Deepest first: a reader whose children have all settled asks each
|
// Deepest first, and strictly: a widget that cannot settle where it
|
||||||
// once, where any other order has it lay out again for whatever
|
// is defers to its parent rather than drawing the parent from
|
||||||
// settles under it afterwards. Equal-depth widgets are independent,
|
// inside itself. It marks the parent, stays marked, and waits here
|
||||||
// so their order does not matter.
|
// until the walk reaches its parent's depth.
|
||||||
while let Some(id) = {
|
//
|
||||||
let dirty = rsc.widgets().needs_redraw.iter().copied();
|
// What that buys is that nothing shallower is ever drawn while
|
||||||
dirty.max_by_key(|&id| self.depth(id))
|
// anything deeper is still dirty. A parent drawing can therefore
|
||||||
} {
|
// trust every answer it reads without descending to check whether
|
||||||
|
// something below is about to change it -- which is the whole class
|
||||||
|
// of defect where a widget settles inside its parent's draw, clears
|
||||||
|
// its mark there, and tells nobody its answer moved.
|
||||||
|
loop {
|
||||||
|
let next = rsc
|
||||||
|
.widgets()
|
||||||
|
.needs_redraw
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|id| !self.deferred.contains(id))
|
||||||
|
.max_by_key(|&id| self.depth(id));
|
||||||
|
let Some(id) = next else { break };
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::QueuePops);
|
diag::bump(Counter::QueuePops);
|
||||||
self.redraw(id, rsc);
|
if !self.redraw(id, rsc) {
|
||||||
|
self.deferred.insert(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
self.deferred.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn depth(&self, id: WidgetId) -> usize {
|
fn depth(&self, id: WidgetId) -> usize {
|
||||||
@@ -909,20 +942,29 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Where a widget is on screen: its box composed through the boxes it
|
/// Where a widget is on screen: its box composed through the boxes it
|
||||||
/// sits within. `None` for one that is not drawn.
|
/// sits within, the same walk the vertex shader does. `None` for one that
|
||||||
|
/// is not drawn.
|
||||||
|
///
|
||||||
|
/// This is for asking where a drawing landed: hit testing, and a test
|
||||||
|
/// reading a box back. Layout decides on the lengths threaded down the
|
||||||
|
/// draw instead, and a position is not one of its inputs.
|
||||||
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||||
let active = self.active.get(&id.id())?;
|
let active = self.active.get(&id.id())?;
|
||||||
active
|
active.drawn.then(|| {
|
||||||
.drawn
|
self.moves
|
||||||
.then(|| self.px_region(active.parent_move, active.region))
|
.resolve(active.parent_move, active.region)
|
||||||
|
.to_px(self.output_size)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Settles a dirty widget: asks it again where its parent asked, and
|
/// Settles a dirty widget: asks it again where its parent asked, and
|
||||||
/// tells the parent if the answer changed.
|
/// tells the parent if the answer changed. `false` where the question is
|
||||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
/// its parent's rather than its own, which leaves it marked for the
|
||||||
|
/// parent to draw when the walk reaches that depth.
|
||||||
|
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> bool {
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
let Some(active) = self.active.get(&id) else {
|
let Some(active) = self.active.get(&id) else {
|
||||||
return;
|
return true;
|
||||||
};
|
};
|
||||||
// Its parent resolved its declared lengths into its box and decided
|
// Its parent resolved its declared lengths into its box and decided
|
||||||
// whether to draw it at all, so a change to either is the parent's
|
// whether to draw it at all, so a change to either is the parent's
|
||||||
@@ -942,91 +984,74 @@ impl UiRenderState {
|
|||||||
at = self.active[&next].parent;
|
at = self.active[&next].parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Both stay marked: the parent because it has this to draw, and
|
||||||
|
// this because the parent must draw it rather than keep what it
|
||||||
|
// has. The mark comes off in `draw_at`, where the parent draws.
|
||||||
rsc.widgets_mut().needs_redraw.insert(id);
|
rsc.widgets_mut().needs_redraw.insert(id);
|
||||||
self.redraw(parent, rsc);
|
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||||
// Whatever the parent did not draw again is nothing it holds now.
|
return false;
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if !active.drawn {
|
if !active.drawn {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
let region = active.region;
|
// Nothing above the root resolved its rules or its alignment, so its
|
||||||
let region_node = active.parent.is_some() && rsc.widgets().is_region_node(id);
|
// box is its own to work out again against the output. Every other
|
||||||
let asked_in = match active.parent {
|
// widget was given one.
|
||||||
Some(_) => self.offered_region(id),
|
let Some(parent) = active.parent else {
|
||||||
None => Self::root_region(id, rsc.widgets()),
|
let region = Self::root_region(id, rsc.widgets());
|
||||||
|
let info = DrawInfo {
|
||||||
|
mask: active.parent_mask,
|
||||||
|
..self.root_info(region)
|
||||||
|
};
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::LocalRedraws);
|
||||||
|
let old = self.remove(id, false, rsc);
|
||||||
|
self.draw_inner(id, region, info, old, rsc);
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
let offered_px = self.px_of(active.parent_move, asked_in);
|
let (given_px, offered_px) = self.asked_px(id);
|
||||||
// Whole boxes rather than lengths: an offer as long as the final box
|
// Asked again in the box its parent gave it, which is the question
|
||||||
// but somewhere else is a different box, and a region node drawing at
|
// its parent asked only while that box is as long as the offer. Any
|
||||||
// its offer writes the box it drew in into its own entry.
|
// other box is a different question, so the parent asks it, with the
|
||||||
let at_offer = same_pixel_region(
|
// mark left on. Lengths and not whole boxes: what a drawing depends
|
||||||
self.px_region(active.parent_move, region),
|
// on is its lengths, so the same lengths elsewhere is one question.
|
||||||
self.px_region(active.parent_move, asked_in),
|
if given_px != offered_px {
|
||||||
);
|
|
||||||
let parent_must_place =
|
|
||||||
active.parent.is_some() && (!region_node || active.align_override) && !at_offer;
|
|
||||||
// An independently positioned region node can redraw at its offer
|
|
||||||
// and move its slot to its own placement. Every other widget needs
|
|
||||||
// its parent to reproduce a different final position.
|
|
||||||
if let Some(parent) = active.parent
|
|
||||||
&& parent_must_place
|
|
||||||
{
|
|
||||||
rsc.widgets_mut().needs_redraw.insert(id);
|
rsc.widgets_mut().needs_redraw.insert(id);
|
||||||
self.redraw(parent, rsc);
|
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
let info = DrawInfo {
|
let info = DrawInfo {
|
||||||
layer: active.layer,
|
layer: active.layer,
|
||||||
parent: active.parent,
|
parent: active.parent,
|
||||||
depth: active.depth,
|
depth: active.depth,
|
||||||
parent_move: active.parent_move,
|
parent_move: active.parent_move,
|
||||||
region_node,
|
region_node: rsc.widgets().is_region_node(id),
|
||||||
mask: active.mask,
|
mask: active.parent_mask,
|
||||||
offer: active.offer,
|
given_len: active.given_len,
|
||||||
|
offer_len: active.offer_len,
|
||||||
|
px: given_px,
|
||||||
offered_px,
|
offered_px,
|
||||||
align: active.align_override.then_some(active.align),
|
decided: active.decided,
|
||||||
};
|
};
|
||||||
let (was_answer, was) = (active.answer, (active.size, active.holds));
|
let (given, was_answer) = (active.given, active.answer);
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::LocalRedraws);
|
diag::bump(Counter::LocalRedraws);
|
||||||
|
|
||||||
let old = self.remove(id, false, rsc);
|
let old = self.remove(id, false, rsc);
|
||||||
let answer = self.draw_inner(id, asked_in, info, old, rsc);
|
// `draw_inner` places the answer inside that box itself, which is the
|
||||||
self.active.get_mut(&id).unwrap().answer = answer;
|
// ask that leaves the widget where its parent put it.
|
||||||
let Some(parent) = info.parent else {
|
let answer = self.draw_inner(id, given, info, old, rsc);
|
||||||
return;
|
|
||||||
};
|
|
||||||
if answer != was_answer {
|
if answer != was_answer {
|
||||||
// Left where it was asked: the parent lays out again and chooses
|
// Its parent chose its box knowing the old answer, so it lays out
|
||||||
// its final box.
|
// again and chooses the box the new one asks for.
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
{
|
{
|
||||||
diag::bump(Counter::SizeChanges);
|
diag::bump(Counter::SizeChanges);
|
||||||
diag::bump(Counter::ReaderEdges);
|
diag::bump(Counter::ReaderEdges);
|
||||||
}
|
}
|
||||||
rsc.widgets_mut().needs_redraw.insert(parent);
|
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if at_offer {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Then in the final box its parent chose from that answer. That box
|
|
||||||
// is already placed, so the near edge goes with it: applying the
|
|
||||||
// widget's own alignment to it again would place its content twice,
|
|
||||||
// the way it did for a region node under a `Stack` once the stack
|
|
||||||
// stopped overriding every child's alignment.
|
|
||||||
let placed_info = DrawInfo {
|
|
||||||
align: Some(RegionAlign::NEAR),
|
|
||||||
..info
|
|
||||||
};
|
|
||||||
self.draw_inner(id, region, placed_info, None, rsc);
|
|
||||||
let active = &self.active[&id];
|
|
||||||
if (active.size, active.holds) != was {
|
|
||||||
rsc.widgets_mut().needs_redraw.insert(parent);
|
|
||||||
}
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,16 +1064,6 @@ fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool {
|
|||||||
len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len
|
len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The same box is the same number of steps, both of these being lengths on
|
|
||||||
/// the grid rather than floats to be compared for nearness.
|
|
||||||
fn same_px(a: PxVec2, b: PxVec2) -> bool {
|
|
||||||
a == b
|
|
||||||
}
|
|
||||||
|
|
||||||
fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool {
|
|
||||||
same_px(a.top_left, b.top_left) && same_px(a.bot_right, b.bot_right)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A retained region rewritten from one parent box into another. A fixed
|
/// A retained region rewritten from one parent box into another. A fixed
|
||||||
/// source extent can be translated but cannot recover fractions for a resize.
|
/// source extent can be translated but cannot recover fractions for a resize.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ impl DefaultAppState for Client {
|
|||||||
let pad_test = (
|
let pad_test = (
|
||||||
rrect.color(Color::BLUE),
|
rrect.color(Color::BLUE),
|
||||||
(
|
(
|
||||||
|
// The square is one widget and the two shares of the row it
|
||||||
|
// sits centred in are another: a length is a property of a
|
||||||
|
// widget, so `.width` here would overwrite the `.sized`.
|
||||||
rrect
|
rrect
|
||||||
.color(Color::RED)
|
.color(Color::RED)
|
||||||
.sized((100, 100))
|
.sized((100, 100))
|
||||||
.center()
|
.center()
|
||||||
|
.wrapper()
|
||||||
.width(leftover(2)),
|
.width(leftover(2)),
|
||||||
(
|
(
|
||||||
rrect.color(Color::ORANGE),
|
rrect.color(Color::ORANGE),
|
||||||
@@ -143,7 +147,7 @@ impl DefaultAppState for Client {
|
|||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.add(rsc);
|
.add(rsc);
|
||||||
|
|
||||||
let main = WidgetPtr::new().add(rsc);
|
let main = Wrapper::new().add(rsc);
|
||||||
|
|
||||||
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
||||||
let mut switch_button = |color, to: WeakWidget, label| {
|
let mut switch_button = |color, to: WeakWidget, label| {
|
||||||
|
|||||||
+7
-3
@@ -28,10 +28,14 @@ impl DefaultAppState for State {
|
|||||||
.pad(16)
|
.pad(16)
|
||||||
.background(panel());
|
.background(panel());
|
||||||
|
|
||||||
|
// Each one takes the whole width, because `text_align` puts the
|
||||||
|
// glyphs somewhere in the box the text is given and a text that
|
||||||
|
// reports the width of its own glyphs is given exactly that.
|
||||||
|
let label = |text: &str, align| wtext(text).size(24).text_align(align).width(rel(1.0));
|
||||||
let aligned = (
|
let aligned = (
|
||||||
wtext("left").size(24).text_align(Align::LEFT),
|
label("left", Align::LEFT),
|
||||||
wtext("centred").size(24).text_align(Align::CENTER),
|
label("centred", Align::H_CENTER),
|
||||||
wtext("right").size(24).text_align(Align::RIGHT),
|
label("right", Align::RIGHT),
|
||||||
)
|
)
|
||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.gap(8)
|
.gap(8)
|
||||||
|
|||||||
+1
-1
@@ -251,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
ui_state.renderer.draw();
|
ui_state.renderer.draw();
|
||||||
}
|
}
|
||||||
WindowEvent::Resized(size) => {
|
WindowEvent::Resized(size) => {
|
||||||
render.resize((size.width, size.height));
|
render.resize((size.width, size.height), rsc.widgets_mut());
|
||||||
ui_state.renderer.resize(size)
|
ui_state.renderer.resize(size)
|
||||||
}
|
}
|
||||||
WindowEvent::KeyboardInput { event, .. } => {
|
WindowEvent::KeyboardInput { event, .. } => {
|
||||||
|
|||||||
+3
-3
@@ -144,9 +144,9 @@ impl Harness {
|
|||||||
// bound that comes with `SyncSender` is far past anything a test
|
// bound that comes with `SyncSender` is far past anything a test
|
||||||
// leaves unread.
|
// leaves unread.
|
||||||
let (send, updates) = sync_channel(1024);
|
let (send, updates) = sync_channel(1024);
|
||||||
let rsc = DefaultRsc::init(Arc::new(Queue(send)));
|
let mut rsc = DefaultRsc::init(Arc::new(Queue(send)));
|
||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
render.resize(size);
|
render.resize(size, rsc.widgets_mut());
|
||||||
Self {
|
Self {
|
||||||
rsc,
|
rsc,
|
||||||
render,
|
render,
|
||||||
@@ -161,7 +161,7 @@ impl Harness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
self.render.resize(size);
|
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.
|
||||||
|
|||||||
+2
-2
@@ -1,15 +1,15 @@
|
|||||||
mod image;
|
mod image;
|
||||||
mod mask;
|
mod mask;
|
||||||
mod position;
|
mod position;
|
||||||
mod ptr;
|
|
||||||
mod rect;
|
mod rect;
|
||||||
mod text;
|
mod text;
|
||||||
mod trait_fns;
|
mod trait_fns;
|
||||||
|
mod wrapper;
|
||||||
|
|
||||||
pub use image::*;
|
pub use image::*;
|
||||||
pub use mask::*;
|
pub use mask::*;
|
||||||
pub use position::*;
|
pub use position::*;
|
||||||
pub use ptr::*;
|
|
||||||
pub use rect::*;
|
pub use rect::*;
|
||||||
pub use text::*;
|
pub use text::*;
|
||||||
pub use trait_fns::*;
|
pub use trait_fns::*;
|
||||||
|
pub use wrapper::*;
|
||||||
@@ -14,7 +14,8 @@ impl Widget for Scroll {
|
|||||||
let container_len = painter.px_len(self.axis);
|
let container_len = painter.px_len(self.axis);
|
||||||
// Draw in the whole container only when its scrolling-axis length is
|
// Draw in the whole container only when its scrolling-axis length is
|
||||||
// not already known, then draw it at the scrolled offset.
|
// not already known, then draw it at the scrolled offset.
|
||||||
let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) {
|
let whole = UiRegion::FULL;
|
||||||
|
let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) {
|
||||||
Some(len) => len,
|
Some(len) => len,
|
||||||
None => painter.widget(&self.inner).size().axis(self.axis),
|
None => painter.widget(&self.inner).size().axis(self.axis),
|
||||||
};
|
};
|
||||||
@@ -63,7 +64,7 @@ impl Widget for Scroll {
|
|||||||
region = region.offset(offset);
|
region = region.offset(offset);
|
||||||
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
||||||
}
|
}
|
||||||
painter.widget_aligned(&self.inner, region, RegionAlign::NEAR);
|
painter.widget_at(&self.inner, region, region.size(), [true; 2]);
|
||||||
// What it occupies is its box, on both axes: it clips its content to
|
// What it occupies is its box, on both axes: it clips its content to
|
||||||
// that box, so it can neither take less of one nor honestly ask for
|
// that box, so it can neither take less of one nor honestly ask for
|
||||||
// more. The content's length is what it scrolls through, not what it
|
// more. The content's length is what it scrolls through, not what it
|
||||||
|
|||||||
+33
-37
@@ -20,9 +20,15 @@ impl Widget for Span {
|
|||||||
span.flip();
|
span.flip();
|
||||||
}
|
}
|
||||||
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||||
let len = match painter.known_len(child, axis, region) {
|
// Offered the room left from the cursor, because a text has to
|
||||||
|
// wrap at the width actually there, but reporting a fraction of
|
||||||
|
// the whole row: `rel(0.5)` is half the span whatever else is in
|
||||||
|
// it and wherever this child sits among them.
|
||||||
|
let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) {
|
||||||
Some(len) => len,
|
Some(len) => len,
|
||||||
None => painter.widget_within(child, region).len(axis),
|
None => painter
|
||||||
|
.widget_at(child, region, UiVec2::FULL_SIZE, [false; 2])
|
||||||
|
.len(axis),
|
||||||
};
|
};
|
||||||
cursor.px += len.px + self.gap;
|
cursor.px += len.px + self.gap;
|
||||||
cursor.rel += len.rel;
|
cursor.rel += len.rel;
|
||||||
@@ -40,43 +46,26 @@ impl Widget for Span {
|
|||||||
|sum, len| sum + *len,
|
|sum, len| sum + *len,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// What is left for the shares to divide: the box less everything
|
||||||
|
// fixed, as a length of the box rather than a number of pixels.
|
||||||
|
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
|
||||||
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
||||||
// beside 300 px is full at 600 and overfull at 400. The room to
|
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
|
||||||
// divide is `len * fixed - total.px`, and the length where it runs
|
// itself, and answered back through the same expression, so the
|
||||||
// out is exactly the box a parent sizing itself from this answer
|
// boundary is the drawing's own and not a second way of finding it:
|
||||||
// hands back -- which is why this used to need a margin either side
|
// the three cases a rounded division needed -- the fixed parts
|
||||||
// of the boundary, and why it does not now: that box and this sum are
|
// growing slower than the box, faster, or exactly with it -- are the
|
||||||
// whole counts of the same step, and both routes to it land on the
|
// sign of `room.rel`, which `through` already reads. What the
|
||||||
// same count. What the generated oracle checks is the consequence,
|
// generated oracle checks is the consequence, since which children
|
||||||
// since which children exist at all turns on this.
|
// exist at all turns on this.
|
||||||
let fixed = Rel::ONE - total.rel;
|
|
||||||
let mut shares = false;
|
let mut shares = false;
|
||||||
if total.leftover > Weight::ZERO {
|
if total.leftover > Weight::ZERO {
|
||||||
let current = painter.px_len(axis);
|
shares = room.to_px(painter.px_len(axis)) > Px::ZERO;
|
||||||
let holds = if fixed > Rel::ZERO {
|
let holds = match shares {
|
||||||
// The box length the fixed parts alone fill.
|
true => Holds::from(Px::STEP..=Px::MAX),
|
||||||
let full = total.px.div(fixed);
|
false => Holds::from(Px::MIN..=Px::ZERO),
|
||||||
shares = current > full;
|
|
||||||
match shares {
|
|
||||||
true => Holds::from(full.next_up()..=Px::MAX),
|
|
||||||
false => Holds::from(Px::MIN..=full),
|
|
||||||
}
|
|
||||||
} else if fixed < Rel::ZERO {
|
|
||||||
// The relative parts grow faster than the box does, so here
|
|
||||||
// a shorter box is the one that leaves room.
|
|
||||||
let full = total.px.div(fixed);
|
|
||||||
shares = current < full;
|
|
||||||
match shares {
|
|
||||||
true => Holds::from(Px::MIN..=full.next_down()),
|
|
||||||
false => Holds::from(full..=Px::MAX),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// The relative parts take exactly the box, whatever it is, so
|
|
||||||
// the only room is what negative pixels leave.
|
|
||||||
shares = total.px < Px::ZERO;
|
|
||||||
Holds::ANY
|
|
||||||
};
|
};
|
||||||
painter.holds(axis, holds);
|
painter.holds(axis, holds.through(room));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -93,7 +82,6 @@ impl Widget for Span {
|
|||||||
// row.
|
// row.
|
||||||
let mut fixed = Len::rel_min();
|
let mut fixed = Len::rel_min();
|
||||||
let mut taken = Weight::ZERO;
|
let mut taken = Weight::ZERO;
|
||||||
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
|
|
||||||
let mut start = Len::rel_min();
|
let mut start = Len::rel_min();
|
||||||
let mut ortho = LayoutLen::ZERO;
|
let mut ortho = LayoutLen::ZERO;
|
||||||
for (child, len) in self.children.iter().zip(&lens) {
|
for (child, len) in self.children.iter().zip(&lens) {
|
||||||
@@ -119,7 +107,15 @@ impl Widget for Span {
|
|||||||
if self.dir.sign == Sign::Neg {
|
if self.dir.sign == Sign::Neg {
|
||||||
region.flip(axis);
|
region.flip(axis);
|
||||||
}
|
}
|
||||||
let placed = painter.widget_within(child, region);
|
// Along the row this box is the child's own answer, so the answer
|
||||||
|
// is not placed in it again; across it the child sits where its
|
||||||
|
// alignment says.
|
||||||
|
let placed = painter.widget_at(
|
||||||
|
child,
|
||||||
|
region,
|
||||||
|
UiVec2::FULL_SIZE,
|
||||||
|
[axis == Axis::X, axis == Axis::Y],
|
||||||
|
);
|
||||||
if shrinks {
|
if shrinks {
|
||||||
let used = placed.len(!axis);
|
let used = placed.len(!axis);
|
||||||
// Choosing between a fixed and a relative length from the
|
// Choosing between a fixed and a relative length from the
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ impl Widget for Stack {
|
|||||||
// child is handed a box that owes nothing to its own answer, and
|
// child is handed a box that owes nothing to its own answer, and
|
||||||
// where it sits in one bigger than itself is its own business.
|
// where it sits in one bigger than itself is its own business.
|
||||||
match sizing == Some(i) {
|
match sizing == Some(i) {
|
||||||
true => painter.widget_aligned(child, region, RegionAlign::NEAR),
|
true => painter.widget_at(child, region, region.size(), [true; 2]),
|
||||||
false => painter.widget_within(child, region),
|
false => painter.widget_within(child, region),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -55,8 +55,13 @@ impl TextView {
|
|||||||
// line up to the one it was made at: each line still fits, and none
|
// line up to the one it was made at: each line still fits, and none
|
||||||
// could take a word that did not fit in the wider box. A line too
|
// could take a word that did not fit in the wider box. A line too
|
||||||
// long to fit at all says nothing about narrower boxes.
|
// long to fit at all says nothing about narrower boxes.
|
||||||
|
//
|
||||||
|
// The step at or above that longest line rather than the nearest
|
||||||
|
// one, since the shaper measures in floats: the nearest step is
|
||||||
|
// under the line half the time, and a range starting there admits a
|
||||||
|
// box the line does not fit in, where the break is not this one.
|
||||||
if let Some(width) = width {
|
if let Some(width) = width {
|
||||||
painter.holds(Axis::X, Px::from_f32(text.size.x).min(width)..=width);
|
painter.holds(Axis::X, Px::ceil_from_f32(text.size.x).min(width)..=width);
|
||||||
}
|
}
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
@@ -78,7 +83,12 @@ impl TextView {
|
|||||||
|
|
||||||
let tex = self.render(painter);
|
let tex = self.render(painter);
|
||||||
let region = tex.size.align(align);
|
let region = tex.size.align(align);
|
||||||
let size = Size::px(tex.size);
|
// The step at or above what the shaper measured, so a parent that
|
||||||
|
// hands back the length this reports hands back a box the longest
|
||||||
|
// line fits in. Rounded to the nearest step it is half the time a
|
||||||
|
// hair under that line, and the break made in it is not the break a
|
||||||
|
// cold layout makes there.
|
||||||
|
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
|
||||||
let within = region.within(&painter.region());
|
let within = region.within(&painter.region());
|
||||||
painter.glyphs(tex, within);
|
painter.glyphs(tex, within);
|
||||||
(region, size)
|
(region, size)
|
||||||
|
|||||||
@@ -134,9 +134,13 @@ widget_trait! {
|
|||||||
|state| self.add(state)
|
|state| self.add(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) {
|
// Named for the type it makes rather than as `wrapped`, which would read
|
||||||
let id = self.add_strong(state);
|
// as the text setting. `widget_trait!` takes no attributes, so what it is
|
||||||
state.ui_mut().widgets[ptr].inner = Some(id);
|
// for is on `Wrapper` itself.
|
||||||
|
fn wrapper(self) -> impl WidgetFn<Rsc, Wrapper> {
|
||||||
|
|state| Wrapper {
|
||||||
|
inner: Some(self.add_strong(state)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::marker::Unsize;
|
use std::marker::Unsize;
|
||||||
|
|
||||||
pub struct WidgetPtr {
|
/// One widget in a box of its own, doing as little as possible on the way:
|
||||||
|
/// it draws its child in the whole of its box and reports back what the child
|
||||||
|
/// said. It exists because a length and an alignment are properties of one
|
||||||
|
/// widget, so a widget cannot both be 100 wide and take two shares of a row
|
||||||
|
/// -- the two lengths need two widgets, and this is the smaller one.
|
||||||
|
///
|
||||||
|
/// Its child is optional so it can also be the swappable slot a tab bar
|
||||||
|
/// needs, which is what it was written for.
|
||||||
|
pub struct Wrapper {
|
||||||
pub inner: Option<StrongWidget>,
|
pub inner: Option<StrongWidget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for WidgetPtr {
|
impl Widget for Wrapper {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
Some(id) => painter.widget(id).size(),
|
Some(id) => painter.widget(id).size(),
|
||||||
@@ -14,7 +22,7 @@ impl Widget for WidgetPtr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetPtr {
|
impl Wrapper {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
@@ -35,7 +43,7 @@ impl WidgetPtr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WidgetPtr {
|
impl Default for Wrapper {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::empty()
|
Self::empty()
|
||||||
}
|
}
|
||||||
+87
-9
@@ -20,6 +20,85 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
|
|||||||
assert_corners!(h, right, (100, 0), (400, 200));
|
assert_corners!(h, right, (100, 0), (400, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A span offers each child the room left after the one before, because a
|
||||||
|
/// text has to wrap at the width actually there, but reads what the child
|
||||||
|
/// reports as a fraction of the whole row. So two children asking for half
|
||||||
|
/// each take the whole row between them, however much of it was left when
|
||||||
|
/// each was asked, and a third overflows.
|
||||||
|
#[test]
|
||||||
|
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
|
||||||
|
let mut h = Harness::new((400, 100));
|
||||||
|
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
|
||||||
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||||
|
let nested = (inner,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
|
||||||
|
// The nested span is placed at the length it reported and drawn there
|
||||||
|
// once more; half of that final box is what its own child takes.
|
||||||
|
assert_corners!(h, nested, (200, 0), (400, 100));
|
||||||
|
assert_corners!(h, inner, (200, 0), (300, 100));
|
||||||
|
assert_corners!(h, tail, (400, 0), (500, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same fraction either way round: after a 100 px child in a 400 px row,
|
||||||
|
/// `rel(0.5)` is 100 to 300 whether the child's own rule says so or the child
|
||||||
|
/// drew half of what it was offered and reported that. Half the row, not half
|
||||||
|
/// of the 300 px left of it.
|
||||||
|
#[test]
|
||||||
|
fn a_reported_fraction_is_of_the_row_like_a_declared_one() {
|
||||||
|
let mut declaring = Harness::new((400, 100));
|
||||||
|
let head = rect(Color::RED).width(100).add(&mut declaring.rsc);
|
||||||
|
let declared = rect(Color::GREEN).width(rel(0.5)).add(&mut declaring.rsc);
|
||||||
|
declaring.set_root((head, declared).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
assert_corners!(declaring, declared, (100, 0), (300, 100));
|
||||||
|
|
||||||
|
let mut reporting = Harness::new((400, 100));
|
||||||
|
let head = rect(Color::RED).width(100).add(&mut reporting.rsc);
|
||||||
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut reporting.rsc);
|
||||||
|
let reported = (inner,).span(Dir::RIGHT).add(&mut reporting.rsc);
|
||||||
|
reporting.set_root((head, reported).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
assert_corners!(reporting, reported, (100, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the fraction a child reports is of and what box it is offered are
|
||||||
|
/// two different lengths, and only the first is the whole row: a text still
|
||||||
|
/// wraps at the room actually left after its neighbour, so the same
|
||||||
|
/// paragraph is taller where less of the row is left for it.
|
||||||
|
#[test]
|
||||||
|
fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
|
||||||
|
let paragraph = "Wrapping shapes one source into as many lines as the box \
|
||||||
|
leaves room for, so a paragraph's height is an answer.";
|
||||||
|
let height_after = |head_width: i32| {
|
||||||
|
let mut h = Harness::new((400, 400));
|
||||||
|
let head = rect(Color::RED).width(head_width).add(&mut h.rsc);
|
||||||
|
let text = wtext(paragraph).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
h.set_root((head, text).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
let region = h.region(&text).unwrap();
|
||||||
|
(region.bot_right.y - region.top_left.y).to_f32()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (crowded, whole_row) = (height_after(300), height_after(0));
|
||||||
|
assert!(crowded > whole_row, "{crowded} against {whole_row}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same reading through a pad: its inset is the whole box less the
|
||||||
|
/// padding, so half of the inset plus the padding is half the box plus one
|
||||||
|
/// padding, not two.
|
||||||
|
#[test]
|
||||||
|
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
|
||||||
|
let mut h = Harness::new((400, 100));
|
||||||
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||||
|
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
|
||||||
|
let tail = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
||||||
|
// Ruled to the window: a root reporting a fraction of it is otherwise
|
||||||
|
// placed inside it by its own alignment, which is not what is under test.
|
||||||
|
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
|
||||||
|
assert_corners!(h, padded, (0, 0), (210, 100));
|
||||||
|
assert_corners!(h, tail, (210, 0), (310, 100));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
|
fn a_span_ruled_across_itself_does_not_measure_its_children_there() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
@@ -225,21 +304,21 @@ fn only_a_region_node_lengthens_the_chain_and_it_can_be_removed() {
|
|||||||
h.set_root((bar, buried).span(Dir::RIGHT));
|
h.set_root((bar, buried).span(Dir::RIGHT));
|
||||||
|
|
||||||
let move_idx = h.render.active[&leaf.id()].parent_move;
|
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||||
assert_eq!(h.render.moves.depth(move_idx), 1, "only the root region");
|
assert_eq!(h.render.moves.depth(move_idx), 0, "the window is no entry");
|
||||||
|
|
||||||
h.rsc.widgets_mut().set_region_node(buried, true);
|
h.rsc.widgets_mut().set_region_node(buried, true);
|
||||||
h.frame();
|
h.frame();
|
||||||
let move_idx = h.render.active[&leaf.id()].parent_move;
|
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
h.render.moves.depth(move_idx),
|
h.render.moves.depth(move_idx),
|
||||||
2,
|
1,
|
||||||
"the opted-in widget's region and the root region"
|
"the opted-in widget's region alone"
|
||||||
);
|
);
|
||||||
|
|
||||||
h.rsc.widgets_mut().set_region_node(buried, false);
|
h.rsc.widgets_mut().set_region_node(buried, false);
|
||||||
h.frame();
|
h.frame();
|
||||||
let move_idx = h.render.active[&leaf.id()].parent_move;
|
let move_idx = h.render.active[&leaf.id()].parent_move;
|
||||||
assert_eq!(h.render.moves.depth(move_idx), 1);
|
assert_eq!(h.render.moves.depth(move_idx), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A span that sizes from its children passes their `leftover` weight up
|
/// A span that sizes from its children passes their `leftover` weight up
|
||||||
@@ -341,16 +420,15 @@ fn a_row_of_equal_shares_fills_it_exactly() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where the shader puts an edge: the two parts of a scalar are floored
|
/// Where the shader puts an edge: the fraction resolved against the window
|
||||||
/// apart, so a fraction and a pixel offset snap independently, and each is
|
/// plus the pixel offset, taken to the boundary it composes to within half
|
||||||
/// taken to the boundary it composes to within half a step of. Kept in step
|
/// a step of. Kept in step with `snap_floor` in `prelude.wgsl`.
|
||||||
/// with `snap_floor` in `prelude.wgsl`.
|
|
||||||
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
|
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
|
||||||
let active = &h.render.active[&id];
|
let active = &h.render.active[&id];
|
||||||
let region = h.render.moves.resolve(active.parent_move, active.region);
|
let region = h.render.moves.resolve(active.parent_move, active.region);
|
||||||
let dim = h.size().axis(axis);
|
let dim = h.size().axis(axis);
|
||||||
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
|
let snap = |v: f32| (v + Px::STEP.to_f32() * 0.5).floor();
|
||||||
let edge = |s: Len| snap(s.rel.to_f32() * dim) + snap(s.px.to_f32());
|
let edge = |s: Len| snap(s.rel.to_f32() * dim + s.px.to_f32());
|
||||||
let span = region.axis(axis);
|
let span = region.axis(axis);
|
||||||
(edge(span.start), edge(span.end))
|
(edge(span.start), edge(span.end))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -613,3 +613,127 @@ fn a_stacks_sizing_child_is_drawn_once_where_it_belongs() {
|
|||||||
assert_ne!(layer(front.id()), layer(background.id()));
|
assert_ne!(layer(front.id()), layer(background.id()));
|
||||||
assert_eq!(draws.get(), 1);
|
assert_eq!(draws.get(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A widget's own mask is not the one it inherited, and a redraw of it
|
||||||
|
/// inherits the second: handing back the first is handing it its own mask to
|
||||||
|
/// set a second time, which `set_mask` asserts against.
|
||||||
|
#[test]
|
||||||
|
fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let masked = inner.masked().add(&mut h.rsc);
|
||||||
|
let other = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((other, masked).span(Dir::RIGHT));
|
||||||
|
h.rsc.widgets_mut().get_dyn_mut(masked.id());
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, inner, (100, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The two spans a subtree changes hands between, and the branch that is not
|
||||||
|
/// in the tree yet -- kept alive by the test until it is.
|
||||||
|
struct Handover {
|
||||||
|
leaf: WidgetId,
|
||||||
|
first: WeakWidget<Span>,
|
||||||
|
second: WeakWidget<Span>,
|
||||||
|
root: WeakWidget<Span>,
|
||||||
|
spare: StrongWidget,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A subtree that changes hands while its box does not move, so nothing about
|
||||||
|
/// reusing its drawing says it changed parents. `deeper` puts a span between
|
||||||
|
/// the root and `second`, so it changes depth by changing hands as well.
|
||||||
|
fn plant_handover(h: &mut Harness, moved: bool, deeper: bool, width: f32) -> Handover {
|
||||||
|
let leaf = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let sized = leaf.width(width).add(&mut h.rsc);
|
||||||
|
let holder = (sized,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let first = Span {
|
||||||
|
children: match moved {
|
||||||
|
true => Vec::new(),
|
||||||
|
false => vec![holder.add_strong(&mut h.rsc)],
|
||||||
|
},
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let second = Span {
|
||||||
|
children: match moved {
|
||||||
|
true => vec![holder.add_strong(&mut h.rsc)],
|
||||||
|
false => Vec::new(),
|
||||||
|
},
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let branch = match deeper {
|
||||||
|
true => (second,).span(Dir::RIGHT).add_strong(&mut h.rsc),
|
||||||
|
false => second.add_strong(&mut h.rsc),
|
||||||
|
};
|
||||||
|
let (in_tree, spare) = match moved {
|
||||||
|
true => (branch, first.add_strong(&mut h.rsc)),
|
||||||
|
false => (first.add_strong(&mut h.rsc), branch),
|
||||||
|
};
|
||||||
|
let root = Span {
|
||||||
|
children: vec![in_tree],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||||
|
Handover {
|
||||||
|
leaf: sized.id(),
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
root,
|
||||||
|
spare,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the subtree and swaps the branch it sits in for the one it left.
|
||||||
|
fn hand_over(h: &mut Harness, tree: Handover) -> WidgetId {
|
||||||
|
let holder = h.rsc[tree.first].children.remove(0);
|
||||||
|
h.rsc[tree.second].children.push(holder);
|
||||||
|
h.rsc[tree.root].children.clear();
|
||||||
|
h.rsc[tree.root].children.push(tree.spare);
|
||||||
|
h.frame();
|
||||||
|
tree.leaf
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_subtree_that_changed_parents_is_not_undrawn_by_the_one_it_left() {
|
||||||
|
let mut warm = Harness::new((400, 200));
|
||||||
|
let tree = plant_handover(&mut warm, false, false, 40.0);
|
||||||
|
warm.frame();
|
||||||
|
let leaf = hand_over(&mut warm, tree);
|
||||||
|
|
||||||
|
let mut cold = Harness::new((400, 200));
|
||||||
|
let grown = plant_handover(&mut cold, true, false, 40.0);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
warm.region(&leaf),
|
||||||
|
cold.region(&grown.leaf),
|
||||||
|
"the span it left still listed it and undrew it"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
|
||||||
|
let mut warm = Harness::new((400, 200));
|
||||||
|
let tree = plant_handover(&mut warm, false, true, 40.0);
|
||||||
|
warm.frame();
|
||||||
|
let leaf = hand_over(&mut warm, tree);
|
||||||
|
// After it has changed hands, so what has to reach the new parent is a
|
||||||
|
// change made under the subtree it now holds.
|
||||||
|
warm.set_len(leaf, Axis::X, LayoutLen::px(90.0));
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((400, 200));
|
||||||
|
let grown = plant_handover(&mut cold, true, true, 90.0);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
warm.region(&leaf),
|
||||||
|
cold.region(&grown.leaf),
|
||||||
|
"the span it moved to is the one the change has to reach"
|
||||||
|
);
|
||||||
|
}
|
||||||
+170
-10
@@ -3,13 +3,16 @@
|
|||||||
//! frame that had not settled: a wrapping text shaped at a width it was
|
//! frame that had not settled: a wrapping text shaped at a width it was
|
||||||
//! measured in rather than the one it was given. The rest are a widget
|
//! measured in rather than the one it was given. The rest are a widget
|
||||||
//! measured again in a box its own answer had decided, where the old answer
|
//! measured again in a box its own answer had decided, where the old answer
|
||||||
//! is a fixed point whatever the content now says. The last two are neither:
|
//! is a fixed point whatever the content now says. The last three are
|
||||||
//! one box length, composed two ways, landing either side of the boundary
|
//! neither: one box length, composed two ways, landing either side of the
|
||||||
//! that decided whether a child was drawn at all, and one box as long as the
|
//! boundary that decided whether a child was drawn at all, and two boxes
|
||||||
//! box a widget was offered but somewhere else.
|
//! 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
|
||||||
|
//! it measured, rounded to a step below the line it measured there.
|
||||||
|
|
||||||
use iris::harness::Harness;
|
use iris::harness::Harness;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
|
use iris::random::Branch;
|
||||||
|
|
||||||
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
/// Six 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
|
||||||
@@ -428,12 +431,12 @@ fn plant_nested_scrolls(h: &mut Harness) -> Vec<WidgetId> {
|
|||||||
vec![text.id(), inner.id(), filler.id(), span.id(), root.id()]
|
vec![text.id(), inner.id(), filler.id(), span.id(), root.id()]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A local redraw asks a dirty widget in the box its parent asked it in, and
|
/// A local redraw asks a dirty widget in the box its parent gave it, and only
|
||||||
/// then again in the box its parent chose from that answer. Skipping the
|
/// where that box is as long as the one it was offered; anything else is a
|
||||||
/// second ask because the two boxes are the same *length* left this inner
|
/// question its parent has to ask. This inner scroll's offer is the outer
|
||||||
/// scroll, which owns a region node, drawn at its offer. The offer is the
|
/// scroll's whole viewport and the box it was given is 24px shorter -- the
|
||||||
/// outer scroll's whole viewport and the final box is 24px above it -- the
|
/// height of the sized child the outer scroll snaps to the end of -- so what
|
||||||
/// height of the sized child the outer scroll snaps to the end of -- so the
|
/// it must not do is settle itself. It was drawn at its offer once, and the
|
||||||
/// inner scroll and its text stayed 24px too low.
|
/// inner scroll and its text stayed 24px too low.
|
||||||
#[test]
|
#[test]
|
||||||
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
|
fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
|
||||||
@@ -454,3 +457,160 @@ fn redrawing_one_widget_does_not_move_what_scrolls_around_it() {
|
|||||||
}
|
}
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ten widgets, of the shape `tests/shrink.rs` reduces the oracle's seed 220
|
||||||
|
/// to. The pad owns a movable region and is the scroll's content, so the box
|
||||||
|
/// the scroll places it in is as long as that content while the box it was
|
||||||
|
/// offered is the viewport -- and with no padding to tell those two apart,
|
||||||
|
/// the span inside it looked like it was still at its offer. So everything
|
||||||
|
/// under the pad was asked again in the *placed* box, the offer resolving
|
||||||
|
/// against the node's own entry, which holds that box: the texts kept the
|
||||||
|
/// widths they had, the content stayed the length those widths make, and the
|
||||||
|
/// old answer confirmed itself. What the branch adds is a tree that differs
|
||||||
|
/// rather than a box that moved, since a probe measured at the wrong width
|
||||||
|
/// takes the other side.
|
||||||
|
fn plant_under_a_node(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
||||||
|
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let wide = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
let narrow = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let branch = Branch {
|
||||||
|
probe: probe.add_strong(&mut h.rsc),
|
||||||
|
wide: wide.add_strong(&mut h.rsc),
|
||||||
|
narrow: narrow.add_strong(&mut h.rsc),
|
||||||
|
threshold: 213.0,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let wrapped = wtext(
|
||||||
|
"Wrapping shapes one source into as many lines as the box \
|
||||||
|
leaves room for, so a paragraph's height is an answer and not a setting.",
|
||||||
|
)
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let plain = wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let row = |h: &mut Harness, mut children: Vec<StrongWidget>| {
|
||||||
|
if swapped {
|
||||||
|
children.rotate_left(1);
|
||||||
|
}
|
||||||
|
Span {
|
||||||
|
children,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc)
|
||||||
|
};
|
||||||
|
let texts: Vec<StrongWidget> =
|
||||||
|
vec![wrapped.add_strong(&mut h.rsc), plain.add_strong(&mut h.rsc)];
|
||||||
|
let inner = row(h, texts);
|
||||||
|
let pair: Vec<StrongWidget> = vec![branch.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
||||||
|
let outer = row(h, pair);
|
||||||
|
let pad = Pad {
|
||||||
|
padding: Padding::ZERO,
|
||||||
|
inner: outer.add_strong(&mut h.rsc),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.rsc.widgets_mut().set_region_node(pad.id(), true);
|
||||||
|
let root = Scroll::new(pad.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
probe.id(),
|
||||||
|
wide.id(),
|
||||||
|
narrow.id(),
|
||||||
|
branch.id(),
|
||||||
|
wrapped.id(),
|
||||||
|
plain.id(),
|
||||||
|
inner.id(),
|
||||||
|
outer.id(),
|
||||||
|
pad.id(),
|
||||||
|
root.id(),
|
||||||
|
],
|
||||||
|
[outer, inner],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let (ids, spans) = plant_under_a_node(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
for span in spans {
|
||||||
|
warm.rsc[span].children.rotate_left(1);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let (cold_ids, _) = plant_under_a_node(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
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 \
|
||||||
|
box leaves room for, so a paragraph's height is an answer and not a setting.";
|
||||||
|
|
||||||
|
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
|
||||||
|
/// `shuffle-swap-for-three`). The stack takes its size from the span above,
|
||||||
|
/// the span takes its width from the longest line of the texts in it, and
|
||||||
|
/// the text below the span is then wrapped at that width -- so a width the
|
||||||
|
/// shaper measured comes back to it as the box to break in.
|
||||||
|
fn plant_a_measured_width(h: &mut Harness, swapped: bool) -> (WeakWidget<Span>, WidgetId) {
|
||||||
|
let first: StrongWidget = rect(Color::YELLOW).add_strong(&mut h.rsc);
|
||||||
|
let mut inner = Span::empty(Dir::UP);
|
||||||
|
inner.children = match swapped {
|
||||||
|
true => swapped_in(h),
|
||||||
|
false => vec![first],
|
||||||
|
};
|
||||||
|
let inner = inner.height(142).add(&mut h.rsc);
|
||||||
|
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let stack = Stack {
|
||||||
|
children: vec![inner.add_strong(&mut h.rsc), text.add_strong(&mut h.rsc)],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root((stack,).span(Dir::DOWN).width(195));
|
||||||
|
(inner, text.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the span holds once its children have been swapped, which is what
|
||||||
|
/// the warm tree is changed to and what the cold one is grown with.
|
||||||
|
fn swapped_in(h: &mut Harness) -> Vec<StrongWidget> {
|
||||||
|
let paragraph = |h: &mut Harness| -> StrongWidget {
|
||||||
|
wtext(PARAGRAPH).size(16).wrap(true).add_strong(&mut h.rsc)
|
||||||
|
};
|
||||||
|
vec![
|
||||||
|
paragraph(h),
|
||||||
|
rect(Color::YELLOW).add_strong(&mut h.rsc),
|
||||||
|
paragraph(h),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text handed back the width it measured breaks there the way it broke
|
||||||
|
/// when it measured it. The width the shaper answers is not on the grid, and
|
||||||
|
/// a report rounded to the nearest step is under the longest line half the
|
||||||
|
/// time: a warm tree then keeps a break made in a wider box while a cold one
|
||||||
|
/// makes a narrower break in the same box, and the paragraph gains a line.
|
||||||
|
#[test]
|
||||||
|
fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let (inner, text) = plant_a_measured_width(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
warm.rsc[inner].children = swapped_in(&mut warm);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let (_, cold_text) = plant_a_measured_width(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_eq!(warm.region(&text), cold.region(&cold_text));
|
||||||
|
}
|
||||||
+13
-5
@@ -42,11 +42,19 @@ const OUTER: (f32, f32) = (1920.0, 1200.0);
|
|||||||
const INNER: (f32, f32) = (640.0, 900.0);
|
const INNER: (f32, f32) = (640.0, 900.0);
|
||||||
const STILL: (f32, f32) = (900.0, 1200.0);
|
const STILL: (f32, f32) = (900.0, 1200.0);
|
||||||
|
|
||||||
/// The same box, to a step of the grid per level of nesting between the two
|
/// The same box, to two steps of the grid between the two ways of reaching
|
||||||
/// ways of reaching it. A move, a repaint and a row of shares land on the
|
/// it. A move, a repaint, a row of shares and every length in pixels land on
|
||||||
/// same number; what is left is a box centred in a fraction of its parent
|
/// the same number. What needs the slack is a position: a box centred in a
|
||||||
/// against the same box centred in its own pixels. A step is a thousandth of
|
/// fraction of its parent against the same box centred in its own pixels,
|
||||||
/// a pixel, where this was a twentieth of one before any of it was on a grid.
|
/// and a box re-expressed as a fraction of a parent that changed length.
|
||||||
|
/// A step is a thousandth of a pixel, where this was a twentieth of one
|
||||||
|
/// before any of it was on a grid.
|
||||||
|
///
|
||||||
|
/// **One step is not enough**, tried 2026-09-17 once a length in pixels
|
||||||
|
/// stopped being composed: it passes the 100-seed oracle and fails the
|
||||||
|
/// 400-seed shrinker on `resize-size`, seeds 384 and 162, by 0.002 px. So
|
||||||
|
/// what is left here is the resize path's own rounding rather than a length
|
||||||
|
/// reached two ways.
|
||||||
const AGREE_STEPS: i32 = 2;
|
const AGREE_STEPS: i32 = 2;
|
||||||
|
|
||||||
/// A way of changing what a span holds. Each is a shape worth its own case:
|
/// A way of changing what a span holds. Each is a shape worth its own case:
|
||||||
|
|||||||
Reference in new issue
Block a user