Track retained layout validity explicitly

This commit is contained in:
iris-ai committed 2026-09-15 16:03:53 -04:00
1 parent 691e3eb23c
commit 29c7881c8a
25 files changed
+836 -795

No files matched your search

+4 -16
View File
@@ -26,7 +26,6 @@ use std::{
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(crate) enum Counter { pub(crate) enum Counter {
Updates, Updates,
ResizeDependents,
DrawRequests, DrawRequests,
WidgetDraws, WidgetDraws,
PlaceCalls, PlaceCalls,
@@ -40,13 +39,9 @@ pub(crate) enum Counter {
ReuseDirty, ReuseDirty,
ReuseWrongParent, ReuseWrongParent,
ReuseUnslotted, ReuseUnslotted,
ReuseOwnResize, ReuseOutside,
ReuseDescendantResize,
ResizeChecks,
ResizeCheckChildren,
QueuePops, QueuePops,
DepthReads, DepthReads,
EagerReaderRedraws,
LocalRedraws, LocalRedraws,
SizeChanges, SizeChanges,
ReaderEdges, ReaderEdges,
@@ -63,7 +58,6 @@ impl Counter {
const NAMES: [&'static str; Self::COUNT] = [ const NAMES: [&'static str; Self::COUNT] = [
"updates", "updates",
"resize dependents",
"draw requests", "draw requests",
"widget draws", "widget draws",
"place calls", "place calls",
@@ -77,13 +71,9 @@ impl Counter {
"reuse: dirty", "reuse: dirty",
"reuse: wrong parent", "reuse: wrong parent",
"reuse: unslotted", "reuse: unslotted",
"reuse: own resize", "reuse: outside what it holds for",
"reuse: descendant resize",
"resize checks",
"resize children checked",
"redraw queue pops", "redraw queue pops",
"depth reads", "depth reads",
"eager reader redraws",
"local redraws", "local redraws",
"size changes", "size changes",
"reader edges", "reader edges",
@@ -100,7 +90,6 @@ impl Counter {
pub(crate) enum TimerKind { pub(crate) enum TimerKind {
Update, Update,
FullLayout, FullLayout,
ResizeMarking,
IncrementalLayout, IncrementalLayout,
TextRender, TextRender,
TextShape, TextShape,
@@ -114,7 +103,6 @@ impl TimerKind {
const NAMES: [&'static str; Self::COUNT] = [ const NAMES: [&'static str; Self::COUNT] = [
"update total", "update total",
"full layout", "full layout",
"resize marking",
"incremental layout", "incremental layout",
"text render", "text render",
"text shape", "text shape",
@@ -255,8 +243,8 @@ pub enum ReuseOutcome {
Dirty, Dirty,
WrongParent, WrongParent,
Unslotted, Unslotted,
OwnResize, Outside,
DescendantResize, Undrawn,
} }
/// One targeted layout event. Events are retained in execution order, making /// One targeted layout event. Events are retained in execution order, making
+23 -20
View File
@@ -1,23 +1,27 @@
use crate::{ use crate::{
LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId, Holds, LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId,
util::Vec2,
}; };
/// important non rendering data for retained drawing /// What is kept of a widget its parent has asked about. `drawn` says whether
/// it currently draws; one that does not is kept so that a change to it, or
/// under it, still reaches whoever asked.
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
pub region: UiRegion, pub region: UiRegion,
/// What the widget said it used of `region`, the last time it drew. /// The box its parent first asked about it in, as a part of the box the
/// parent was itself asked in. Any later box it was given was decided
/// knowing its answer, so this is where a question about it is asked
/// again -- and it is kept relative so that it follows the parent's.
pub offer: UiRegion,
/// What it answered there: the size and what that held for.
pub answer: (Size, [Holds; 2]),
/// What the widget said it used of its box, the last time it drew.
pub size: Size, pub size: Size,
/// The pixel size of the box it drew against. `region` alone cannot say: /// The pixel lengths of its box, per axis, that its drawing and `size`
/// it is a fraction of a slot's box, and the same fraction of a box that /// hold for.
/// has since changed is a different number of pixels. pub holds: [Holds; 2],
pub px: Vec2, pub drawn: bool,
/// The pixel size of the box its parent first asked about it in, before
/// knowing what it came to. `px` may be a box derived from that answer,
/// and a size measured there is only the same answer asked again.
pub offered_px: Vec2,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
/// How far down the tree it was drawn, the root being 1. Carried down a /// How far down the tree it was drawn, the root being 1. Carried down a
/// draw rather than worked out by walking up, so it is right for every /// draw rather than worked out by walking up, so it is right for every
@@ -28,14 +32,6 @@ pub struct ActiveData {
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>, pub size_deps: Vec<WidgetId>,
/// Offered pixel axes which flowed into this widget's reported size,
/// directly or through a child size it read.
pub size_box_inputs: [bool; 2],
/// Output axes read while producing `size`, distinct from the widget's
/// own box when that box has a fixed pixel length.
pub size_output_inputs: [bool; 2],
/// The output dimensions against which those dependencies were observed.
pub output_px: Vec2,
/// The slot its primitives are positioned through: its own if its parent /// The slot its primitives are positioned through: its own if its parent
/// placed it, otherwise the nearest ancestor that has one. /// placed it, otherwise the nearest ancestor that has one.
pub move_idx: MoveIdx, pub move_idx: MoveIdx,
@@ -48,3 +44,10 @@ pub struct ActiveData {
pub mask: MaskIdx, pub mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
impl ActiveData {
/// Whether its drawing and size hold for a box of these pixel lengths.
pub fn holds_at(&self, px: crate::util::Vec2) -> bool {
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
}
}
+81
View File
@@ -0,0 +1,81 @@
use crate::UiScalar;
use std::ops::RangeInclusive;
/// The lengths of a box, in pixels, that one drawing of a widget holds for:
/// give the widget any box in this range and it draws the same thing and
/// reports the same size. A widget that never reads its box in pixels holds
/// for every length; one that does holds for the one it read unless it says
/// otherwise, and a parent holds for whatever keeps every child it asked
/// about or drew inside its own range.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Holds {
pub lo: f32,
pub hi: f32,
}
/// How far outside a range a length may fall and still be inside it: a box
/// offered back to a widget at the length it reported comes back through the
/// chain a few bits off, and nothing a reader could see lives in that gap.
pub const HOLDS_EPSILON_PX: f32 = 0.05;
impl Holds {
pub const ANY: Self = Self {
lo: f32::NEG_INFINITY,
hi: f32::INFINITY,
};
pub const fn at(len: f32) -> Self {
Self { lo: len, hi: len }
}
pub fn contains(&self, len: f32) -> bool {
len >= self.lo - HOLDS_EPSILON_PX && len <= self.hi + HOLDS_EPSILON_PX
}
pub fn and(self, other: Self) -> Self {
Self {
lo: self.lo.max(other.lo),
hi: self.hi.min(other.hi),
}
}
/// What a box has to be for a part of it, `len` of the box long, to stay
/// in this range. A part with no relative extent is a fixed length: it
/// was drawn at that length and any box keeps it there.
pub fn through(self, len: UiScalar) -> Self {
if len.rel == 0.0 {
return Self::ANY;
}
let a = (self.lo - len.px) / len.rel;
let b = (self.hi - len.px) / len.rel;
Self {
lo: a.min(b),
hi: a.max(b),
}
}
}
impl From<RangeInclusive<f32>> for Holds {
fn from(range: RangeInclusive<f32>) -> Self {
Self {
lo: *range.start(),
hi: *range.end(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn through_reverses_a_range_for_a_negative_fraction() {
assert_eq!(
Holds { lo: 20.0, hi: 40.0 }.through(UiScalar::new(-0.5, 10.0)),
Holds {
lo: -60.0,
hi: -20.0
}
);
}
}
+2
View File
@@ -10,10 +10,12 @@ use crate::{
pub const CHAIN_LIMIT: u32 = 64; pub const CHAIN_LIMIT: u32 = 64;
mod active; mod active;
mod holds;
mod painter; mod painter;
mod render_state; mod render_state;
pub use active::*; pub use active::*;
pub use holds::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use render_state::*; pub use render_state::*;
+142 -128
View File
@@ -1,14 +1,18 @@
#[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, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, Axis, Holds, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
PrimitiveKind, TexturePrimitive, PrimitiveKind, TexturePrimitive,
}, },
ui::render_state::DrawInfo,
util::Vec2, util::Vec2,
}; };
use std::ops::RangeInclusive;
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
/// makes your surfaces look pretty /// makes your surfaces look pretty
pub struct Painter<'a> { pub struct Painter<'a> {
@@ -21,14 +25,21 @@ pub struct Painter<'a> {
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<PrimitiveHandle>,
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first box each was asked /// The children asked about so far, so the first box each was asked in
/// about 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.
pub(super) offered_px: Vec2,
/// Whether this draw is in that box, which makes the questions it asks
/// the ones a cold layout asks and their answers the ones to keep.
pub(super) at_offer: bool,
/// 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>,
/// Offered pixel axes which can affect the size this draw reports. /// What this draw itself read of its box in pixels, per axis: every
pub(super) size_box_inputs: [bool; 2], /// length until it reads one, then that one, unless it says otherwise.
pub(super) size_output_inputs: [bool; 2], pub(super) own: [Holds; 2],
/// What the children it asked about and drew keep it to.
pub(super) under: [Holds; 2],
/// The slot this widget's primitives are positioned through: its own if /// The slot this widget's primitives are positioned through: its own if
/// its parent placed it, otherwise the nearest ancestor that has one. /// its parent placed it, otherwise the nearest ancestor that has one.
pub(super) move_idx: MoveIdx, pub(super) move_idx: MoveIdx,
@@ -90,14 +101,7 @@ impl<'a> Painter<'a> {
/// Draws a widget within this widget's region. /// Draws a widget within this widget's region.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
let declared = self.declared_lens(id); self.widget_at(id, UiRegion::FULL, false)
// 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.
let region = match declared.iter().any(Option::is_some) {
true => declared_box(UiRegion::FULL, declared).within(&self.region),
false => self.region,
};
self.widget_at(id, region, false, declared)
} }
/// Draws a widget somewhere within this one. /// Draws a widget somewhere within this one.
@@ -106,9 +110,7 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let declared = self.declared_lens(id); self.widget_at(id, region, false)
let region = declared_box(region, declared).within(&self.region);
self.widget_at(id, region, false, declared)
} }
/// What a widget declares its lengths to be, which whoever draws it /// What a widget declares its lengths to be, which whoever draws it
@@ -120,7 +122,7 @@ impl<'a> Painter<'a> {
let Some(widget) = self.rsc.widgets().get_dyn(id.id()) else { let Some(widget) = self.rsc.widgets().get_dyn(id.id()) else {
return [None; 2]; return [None; 2];
}; };
[Axis::X, Axis::Y].map(|axis| declared_len(widget, axis)) AXES.map(|axis| declared_len(widget, axis))
} }
/// Draws a child this widget decides the box of, and may decide again /// Draws a child this widget decides the box of, and may decide again
@@ -136,39 +138,75 @@ impl<'a> Painter<'a> {
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceCalls); diag::bump(Counter::PlaceCalls);
let declared = self.declared_lens(id); self.widget_at(id, region, true)
let region = declared_box(region, declared).within(&self.region);
#[cfg(feature = "layout-diagnostics")]
diag::placed(id.id(), self.id, region);
self.widget_at(id, region, true, declared)
} }
/// Takes back a child that was drawn only to find out how long it is.
/// Its drawing is dropped and it is not one of this widget's children
/// this frame; what it answered is still something this widget asked.
pub fn undraw<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.children.retain(|child| *child != id.id());
self.state.undraw_rec(id.id(), self.rsc);
}
/// `region` in this widget's own coordinates, and with the child's
/// declared lengths still to be taken.
fn widget_at<'s, W: ?Sized>( fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
slotted: bool, slotted: bool,
declared: [Option<Len>; 2],
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let declared = self.declared_lens(id);
// 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.
let local = match declared.iter().any(Option::is_some) {
true => declared_box(region, declared),
false => region,
};
let within = match local == UiRegion::FULL {
true => self.region,
false => local.within(&self.region),
};
#[cfg(feature = "layout-diagnostics")]
if slotted {
diag::placed(id.id(), self.id, within);
}
// A child listed twice would be moved twice. // A child listed twice would be moved twice.
if !self.children.contains(&id.id()) { if !self.children.contains(&id.id()) {
self.children.push(id.id()); self.children.push(id.id());
} }
let first_ask = self.offer(id.id());
let offer = match first_ask {
true => local,
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer),
};
let answers_offer = self.at_offer && local == offer;
let size = self.state.draw_inner( let size = self.state.draw_inner(
self.layer,
id.id(), id.id(),
region, within,
Some(self.id), DrawInfo {
self.depth + 1, layer: self.layer,
self.move_idx, parent: Some(self.id),
slotted, depth: self.depth + 1,
self.mask, parent_move: self.move_idx,
slotted,
mask: self.mask,
offer,
offered_px: self.px_within_offer(offer),
},
None, None,
self.rsc, self.rsc,
); );
self.offer(id.id(), region); let active = self.state.active.get_mut(&id.id()).unwrap();
if let Some(active) = self.state.active.get_mut(&id.id()) { active.declared = declared;
active.declared = declared; if answers_offer {
active.answer = (active.size, active.holds);
}
// Whatever the child's drawing holds for keeps this one to the boxes
// that give the child a length inside it.
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(active.holds[axis as usize].through(local.axis(axis).len()));
} }
DrawResult { DrawResult {
child: id, child: id,
@@ -191,7 +229,7 @@ impl<'a> Painter<'a> {
Some(hint) => { Some(hint) => {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintHits); diag::bump(Counter::HintHits);
self.depend_on_hint(id); self.depend_on(id);
Some(hint) Some(hint)
} }
None => { None => {
@@ -202,87 +240,66 @@ impl<'a> Painter<'a> {
} }
} }
/// A retained child length valid under the region it is about to be /// A child's length in the box it is about to be offered, if it can be
/// offered. Unlike a hint, this is contextual: it is kept only when none /// had without drawing it: from its hint, or from a drawing it already
/// of the offered pixel axes which produced it changed. /// has that holds for that box.
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,
) -> Option<Len> { ) -> Option<Len> {
let region = region.within(&self.region); let declared = self.declared_lens(child);
self.offer(child.id(), region); let local = declared_box(region, declared);
let within = local.within(&self.region);
let first_ask = self.offer(child.id());
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
active.offer = local;
}
if let Some(hint) = self.size_hint(child, axis) { if let Some(hint) = self.size_hint(child, axis) {
return Some(hint); return Some(hint);
} }
self.retained_size(child, region) let px = self.state.px_of(self.move_idx, within);
.map(|size| size.axis(axis)) let (size, holds) =
}
/// `region` in this widget's own coordinates.
fn retained_size<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
region: UiRegion,
) -> Option<Size> {
let (size, box_inputs, output_inputs) =
self.state self.state
.retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?; .retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::RetainedSizeHits); diag::bump(Counter::RetainedSizeHits);
self.depend_on_size_inputs(child, box_inputs, output_inputs); self.depend_on(child);
Some(size) if first_ask {
let active = self.state.active.get_mut(&child.id()).unwrap();
active.answer = (size, holds);
}
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
}
Some(size.axis(axis))
} }
/// Records the box a child was first asked about in this draw. Any later /// Whether this is the first box a child is asked about in during a draw
/// box this draw gives it was decided knowing its answer, so a size the /// that is itself in the box it was asked in -- the question a cold
/// child measures there is not an answer to this widget's question. /// layout asks, whose answer is the one to keep.
fn offer(&mut self, child: WidgetId, region: UiRegion) { fn offer(&mut self, child: WidgetId) -> bool {
if self.offered.contains(&child) { if !self.at_offer || self.offered.contains(&child) {
return; return false;
} }
self.offered.push(child); self.offered.push(child);
let px = self.state.px_of(self.move_idx, region); true
if let Some(active) = self.state.active.get_mut(&child) {
active.offered_px = px;
}
} }
/// Depends on a length the child gave without being drawn. A hint is /// The pixel size of a part of the box this widget was asked in.
/// context-free, so this depends on the child but on no pixel axis. fn px_within_offer(&self, local: UiRegion) -> Vec2 {
fn depend_on_hint<W: ?Sized>(&mut self, child: &StrongWidget<W>) { let size = local.size();
self.depend_on_size_inputs(child, [false; 2], [false; 2]); Vec2::new(
size.x.to_px(self.offered_px.x),
size.y.to_px(self.offered_px.y),
)
} }
/// Depends on a size the child produced by drawing, which carries fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
/// whatever the child read to produce it.
fn depend_on_drawn_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
let (box_inputs, output_inputs) = self
.state
.active
.get(&child.id())
.map_or(([false; 2], [false; 2]), |active| {
(active.size_box_inputs, active.size_output_inputs)
});
self.depend_on_size_inputs(child, box_inputs, output_inputs);
}
fn depend_on_size_inputs<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
box_inputs: [bool; 2],
output_inputs: [bool; 2],
) {
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());
} }
for (own, child) in self.size_box_inputs.iter_mut().zip(box_inputs) {
*own |= child;
}
for (own, child) in self.size_output_inputs.iter_mut().zip(output_inputs) {
*own |= child;
}
} }
pub fn render_text<'b>( pub fn render_text<'b>(
@@ -327,45 +344,42 @@ impl<'a> Painter<'a> {
self.region self.region
} }
/// The output's size in pixels. A widget that reads it draws again when /// This widget's box in pixels. Reading it makes the drawing one that
/// the output changes, since nothing else can put that right. /// holds for this box only, until `holds` says how far it goes.
pub fn output_size(&mut self) -> Vec2 {
self.size_output_inputs = [true; 2];
self.state.output_size
}
/// One axis of the output in pixels. Prefer this to [`Self::output_size`]
/// when the other axis cannot affect the size this widget reports.
pub fn output_len(&mut self, axis: Axis) -> f32 {
self.size_output_inputs[axis as usize] = true;
self.state.output_size.axis(axis)
}
/// This widget's box in pixels. Resolved against the output's size and
/// the boxes it sits within, so a widget that reads it draws again when
/// the output changes.
pub fn px_size(&mut self) -> Vec2 { pub fn px_size(&mut self) -> Vec2 {
self.size_box_inputs = [true; 2]; let px = self.state.px_of(self.move_idx, self.region);
let region = self.state.moves.resolve(self.move_idx, self.region); for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
region.size().to_px(self.state.output_size) if *own == Holds::ANY {
*own = Holds::at(len);
}
}
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 reported size. /// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> f32 { pub fn px_len(&mut self, axis: Axis) -> f32 {
self.size_box_inputs[axis as usize] = true; let len = self.state.px_of(self.move_idx, self.region).axis(axis);
self.px_len_for_draw(axis) let own = &mut self.own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
}
len
} }
/// One axis of this widget's box in pixels, for a draw whose reported /// The lengths of this widget's box on `axis` that what it is drawing
/// size does not follow from it -- a clamp or a position. Nothing records /// holds for -- the same primitives, in the same fractions and offsets
/// the read, so a size that does depend on it would go stale. /// of the box, and the same reported size. A widget that read its
pub fn px_len_for_draw(&self, axis: Axis) -> f32 { /// length in pixels holds for that one alone until it says otherwise.
let region = self.state.moves.resolve(self.move_idx, self.region); pub fn holds(&mut self, axis: Axis, range: RangeInclusive<f32>) {
region let holds = Holds::from(range);
.size() debug_assert!(
.axis(axis) holds.contains(self.state.px_of(self.move_idx, self.region).axis(axis)),
.to_px(self.state.output_size.axis(axis)) "'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(),
self.id
);
self.own[axis as usize] = holds;
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -405,7 +419,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
diag::bump(Counter::SizeReads); diag::bump(Counter::SizeReads);
diag::size_read(self.child.id(), self.painter.id, self.size); diag::size_read(self.child.id(), self.painter.id, self.size);
} }
self.painter.depend_on_drawn_size(self.child); self.painter.depend_on(self.child);
self.size self.size
} }
@@ -451,7 +465,7 @@ pub(crate) fn declared_len(widget: &dyn Widget, axis: Axis) -> Option<Len> {
/// reserved the space hands back the same length, so this is the identity /// reserved the space hands back the same length, so this is the identity
/// for it. /// for it.
fn declared_box(mut region: UiRegion, declared: [Option<Len>; 2]) -> UiRegion { fn declared_box(mut region: UiRegion, declared: [Option<Len>; 2]) -> UiRegion {
for (axis, len) in [Axis::X, Axis::Y].into_iter().zip(declared) { for (axis, len) in AXES.into_iter().zip(declared) {
let Some(len) = len else { continue }; let Some(len) = len else { continue };
let span = region.axis_mut(axis); let span = region.axis_mut(axis);
span.end = span.start + UiScalar::new(len.rel, len.px); span.end = span.start + UiScalar::new(len.rel, len.px);
File diff suppressed because it is too large. Load diff
-22
View File
@@ -15,20 +15,6 @@ pub use tag::*;
pub use view::*; pub use view::*;
pub use widgets::*; pub use widgets::*;
/// What may be done to a widget's drawing when the box it was given changes
/// on this axis, instead of drawing it again. Asked per axis, because wrapped
/// text reads the width it is offered and not the height.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OnResize {
Scale,
/// Reserved: nothing reads this yet, so a widget saying it is redrawn.
/// Keeping an unchanged drawing in a bigger box needs the widget to say
/// *where* in that box it should sit, which is the alignment work.
Translate,
#[default]
Redraw,
}
pub trait Widget: Any { pub trait Widget: Any {
/// Draws the widget, and returns what it used of the box it was given. /// Draws the widget, and returns what it used of the box it was given.
fn draw(&mut self, painter: &mut Painter) -> Size; fn draw(&mut self, painter: &mut Painter) -> Size;
@@ -39,10 +25,6 @@ pub trait Widget: Any {
fn size_hint(&self, _axis: Axis) -> Option<Len> { fn size_hint(&self, _axis: Axis) -> Option<Len> {
None None
} }
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::default()
}
} }
impl Widget for () { impl Widget for () {
@@ -54,10 +36,6 @@ impl Widget for () {
fn size_hint(&self, _axis: Axis) -> Option<Len> { fn size_hint(&self, _axis: Axis) -> Option<Len> {
Some(Len::default()) Some(Len::default())
} }
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::Scale
}
} }
impl dyn Widget { impl dyn Widget {
-4
View File
@@ -14,10 +14,6 @@ impl Widget for Image {
fn size_hint(&self, axis: Axis) -> Option<Len> { fn size_hint(&self, axis: Axis) -> Option<Len> {
Some(Len::px(self.handle.size().axis(axis))) Some(Len::px(self.handle.size().axis(axis)))
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
pub fn image<State: UiRsc>(image: impl LoadableImage) -> impl WidgetFn<State, Image> { pub fn image<State: UiRsc>(image: impl LoadableImage) -> impl WidgetFn<State, Image> {
-5
View File
@@ -9,9 +9,4 @@ impl Widget for Masked {
painter.set_mask(painter.region()); painter.set_mask(painter.region());
painter.widget(&self.inner).size() painter.widget(&self.inner).size()
} }
/// It clips to the box it was given, not to the part its child used.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Redraw
}
} }
-6
View File
@@ -39,10 +39,4 @@ impl Widget for Aligned {
let placed = painter.place(&self.inner, region).size(); let placed = painter.place(&self.inner, region).size();
if had_size { placed } else { size } if had_size { placed } else { size }
} }
/// The aligned box is a fraction of its own, so the child keeps its
/// length and stays against the edge it was aligned to.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
-4
View File
@@ -12,8 +12,4 @@ impl Widget for LayerOffset {
} }
painter.widget(&self.inner).size() painter.widget(&self.inner).size()
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
+5 -5
View File
@@ -9,17 +9,17 @@ pub struct MaxSize {
impl Widget for MaxSize { impl Widget for MaxSize {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let child = painter.widget(&self.inner).size(); let child = painter.widget(&self.inner).size();
let output = painter.output_size(); let own = painter.px_size();
Size { Size {
x: capped(child.x, self.x, output.x), x: capped(child.x, self.x, own.x),
y: capped(child.y, self.y, output.y), y: capped(child.y, self.y, own.y),
} }
} }
} }
fn capped(len: Len, max: Option<Len>, output: f32) -> Len { fn capped(len: Len, max: Option<Len>, own: f32) -> Len {
match max { match max {
Some(max) if len.apply_leftover().to_px(output) > max.apply_leftover().to_px(output) => max, Some(max) if len.apply_leftover().to_px(own) > max.apply_leftover().to_px(own) => max,
_ => len, _ => len,
} }
} }
-4
View File
@@ -10,8 +10,4 @@ impl Widget for Offset {
let region = UiRegion::FULL.offset(self.amt); let region = UiRegion::FULL.offset(self.amt);
painter.widget_within(&self.inner, region).size() painter.widget_within(&self.inner, region).size()
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
-6
View File
@@ -21,12 +21,6 @@ impl Widget for Pad {
}, },
} }
} }
/// The padding is an offset from each edge, so a longer box pads the same
/// amount and the child takes what is left over.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
pub struct Padding { pub struct Padding {
+23 -14
View File
@@ -11,31 +11,40 @@ pub struct Scroll {
impl Widget for Scroll { impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let output_len = painter.output_len(self.axis); let container_len = painter.px_len(self.axis);
// Its size is its content's, whatever box that is scrolled within.
let container_len = UiScalar::px(painter.px_len_for_draw(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 place it at the scrolled offset. // not already known, then place it at the scrolled offset.
let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL); let (answer_len, measured) = match painter.known_len(&self.inner, self.axis, UiRegion::FULL)
let measured = known_len.is_none(); {
let child = measured.then(|| painter.place(&self.inner, UiRegion::FULL).size()); Some(len) => (len, None),
let content_len = known_len None => {
.unwrap_or_else(|| child.unwrap().axis(self.axis)) let size = painter.place(&self.inner, UiRegion::FULL).size();
.apply_leftover() (size.axis(self.axis), Some(size))
.within_len(container_len) }
.to_px(output_len); };
self.container_len = container_len.to_px(output_len); let content = answer_len.apply_leftover();
self.content_len = content_len; self.container_len = container_len;
self.content_len = content.to_px(container_len);
if self.snap_end { if self.snap_end {
self.amt = self.content_len - self.container_len; self.amt = self.content_len - self.container_len;
} }
self.update_amt(); self.update_amt();
// Content of a fixed length that fits sits at the start of any box
// it fits in; one scrolled part way sits where it is until the box
// shrinks past what is left of it. Kept to the end, it moves with
// every length.
if content.rel == 0.0 && self.content_len <= self.container_len {
painter.holds(self.axis, self.content_len..=f32::INFINITY);
} else if content.rel == 0.0 && !self.snap_end {
let left = self.content_len - self.amt;
painter.holds(self.axis, f32::NEG_INFINITY..=left);
}
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0)); let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
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);
let placed = painter.place(&self.inner, region).size(); let placed = painter.place(&self.inner, region).size();
child.unwrap_or(placed) measured.unwrap_or_else(|| Size::from_axis(self.axis, answer_len, placed.axis(!self.axis)))
} }
} }
-4
View File
@@ -27,8 +27,4 @@ impl Widget for SetSize {
Axis::Y => self.y, Axis::Y => self.y,
} }
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
+33 -7
View File
@@ -32,12 +32,44 @@ impl Widget for Span {
let gap = self.gap * self.children.len().saturating_sub(1) as f32; let gap = self.gap * self.children.len().saturating_sub(1) as f32;
let total = lens.iter().fold(Len::px(gap), |sum, len| sum + *len); let total = lens.iter().fold(Len::px(gap), |sum, len| sum + *len);
// 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 answer is
// the same on either side of the length the fixed parts alone fill.
let fixed = 1.0 - total.rel;
let shares = total.leftover > 0.0 && fixed * painter.px_len(axis) > total.px;
if total.leftover > 0.0 {
let range = if fixed > 0.0 {
let full = total.px / fixed;
match shares {
true => full..=f32::INFINITY,
false => f32::NEG_INFINITY..=full,
}
} else if fixed < 0.0 {
let full = total.px / fixed;
match shares {
true => f32::NEG_INFINITY..=full,
false => full..=f32::INFINITY,
}
} else {
f32::NEG_INFINITY..=f32::INFINITY
};
painter.holds(axis, range);
}
let mut start = UiScalar::rel_min(); let mut start = UiScalar::rel_min();
let mut ortho = Len::ZERO; let mut ortho = Len::ZERO;
for (child, len) in self.children.iter().zip(&lens) { for (child, len) in self.children.iter().zip(&lens) {
// A child asking for nothing but a part of what is left over,
// when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows.
if len.leftover > 0.0 && len.px == 0.0 && len.rel == 0.0 && !shares {
painter.undraw(child);
start.px += self.gap;
continue;
}
let mut span = UiSpan::FULL; let mut span = UiSpan::FULL;
span.start = start; span.start = start;
if len.leftover > 0.0 { if len.leftover > 0.0 && shares {
let offset = UiScalar::new(total.rel, total.px); let offset = UiScalar::new(total.rel, total.px);
let rel_end = UiScalar::rel(len.leftover / total.leftover); let rel_end = UiScalar::rel(len.leftover / total.leftover);
let end = (UiScalar::rel_max() + start) - offset; let end = (UiScalar::rel_max() + start) - offset;
@@ -70,12 +102,6 @@ impl Widget for Span {
let along = total; let along = total;
Size::from_axis(axis, along, ortho) Size::from_axis(axis, along, ortho)
} }
/// Every child is placed in fractions and offsets of the span's own box,
/// so a longer box holds the same layout and the children follow it.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
impl Span { impl Span {
-4
View File
@@ -28,10 +28,6 @@ impl Widget for Stack {
} }
size size
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
-5
View File
@@ -41,11 +41,6 @@ impl Widget for Rect {
fn size_hint(&self, _: Axis) -> Option<Len> { fn size_hint(&self, _: Axis) -> Option<Len> {
Some(Len::LEFTOVER) Some(Len::LEFTOVER)
} }
/// Its box is its primitive's own region, so a new one is written there.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
pub fn rect(color: UiColor) -> Rect { pub fn rect(color: UiColor) -> Rect {
-4
View File
@@ -93,10 +93,6 @@ impl Widget for TextEdit {
); );
size size
} }
fn on_resize(&self, axis: Axis) -> OnResize {
self.view.on_resize(axis)
}
} }
const CARET_WIDTH: f32 = 1.0; const CARET_WIDTH: f32 = 1.0;
+10 -26
View File
@@ -47,12 +47,16 @@ impl TextView {
/// answers under the attrs too, so changing those asks a new question /// answers under the attrs too, so changing those asks a new question
/// rather than invalidating anything. /// rather than invalidating anything.
fn render(&mut self, painter: &mut Painter) -> &RenderedText { fn render(&mut self, painter: &mut Painter) -> &RenderedText {
let width = if self.attrs.wrap { let width = self.attrs.wrap.then(|| painter.px_len(Axis::X));
Some(painter.px_len(Axis::X)) let text = painter.render_text(&mut self.buf, &self.attrs, width);
} else { // A greedy break is the same break at every width from its longest
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
painter.render_text(&mut self.buf, &self.attrs, width) // long to fit at all says nothing about narrower boxes.
if let Some(width) = width {
painter.holds(Axis::X, text.size.x.min(width)..=width);
}
text
} }
pub fn tex(&self) -> Option<&RenderedText> { pub fn tex(&self) -> Option<&RenderedText> {
@@ -78,22 +82,6 @@ impl TextView {
(region, size) (region, size)
} }
/// Wrapping reads the width it is offered, so a wider box reshapes it and
/// a taller one does not. Alignment matters too, and separately: glyphs
/// anchored to the start of an axis stay put when that extent changes,
/// but centred or end-aligned ones move even though the shaping stands.
pub fn on_resize(&self, axis: Axis) -> OnResize {
let reshapes = axis == Axis::X && self.attrs.wrap;
let anchored = match axis {
Axis::X => self.align.x,
Axis::Y => self.align.y,
} == AxisAlign::Neg;
match reshapes || !anchored {
true => OnResize::Redraw,
false => OnResize::Translate,
}
}
pub fn content(&self) -> String { pub fn content(&self) -> String {
self.buf.text().to_string() self.buf.text().to_string()
} }
@@ -120,10 +108,6 @@ impl Widget for Text {
self.update_buf(); self.update_buf();
self.view.draw(painter).1 self.view.draw(painter).1
} }
fn on_resize(&self, axis: Axis) -> OnResize {
self.view.on_resize(axis)
}
} }
impl Deref for Text { impl Deref for Text {
+2 -11
View File
@@ -30,7 +30,7 @@ fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
.and_then(|value| value.parse().ok()) .and_then(|value| value.parse().ok())
.unwrap_or(fallback) .unwrap_or(fallback)
} }
const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98]; const SEEDS: [u64; 9] = [1, 2, 3, 5, 8, 10, 13, 86, 98];
const REGION_EPSILON_PX: f32 = 0.05; const REGION_EPSILON_PX: f32 = 0.05;
fn same_coordinate(got: f32, want: f32) -> bool { fn same_coordinate(got: f32, want: f32) -> bool {
@@ -280,7 +280,6 @@ fn changed_size(seed: u64) {
..Default::default() ..Default::default()
}, },
); );
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same)); assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
} }
@@ -299,16 +298,9 @@ fn reshuffled(seed: u64, shuffle: Shuffle) {
if !shuffles { if !shuffles {
return; return;
} }
let before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect();
let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle); let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle);
warm.frame(); warm.frame();
// Or the two trees would agree for want of anything having happened.
let after = grown.ids.iter().map(|id| warm.region(id));
let moved = before.iter().zip(after).filter(|(a, b)| *a != b).count();
assert!(moved > 0, "seed {seed}: {shuffle:?} changed nothing");
let mut cold = Harness::new((900, 1200)); let mut cold = Harness::new((900, 1200));
let same = plant( let same = plant(
&mut cold, &mut cold,
@@ -343,7 +335,6 @@ fn changed_every_size(seed: u64) {
..Default::default() ..Default::default()
}, },
); );
assert_same(seed, "every size at once", (&warm, &grown), (&cold, &same)); assert_same(seed, "every size at once", (&warm, &grown), (&cold, &same));
} }
@@ -448,7 +439,7 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
/// had decided. `tests/shrink.rs` is how a seed from here becomes a tree /// had decided. `tests/shrink.rs` is how a seed from here becomes a tree
/// small enough to read. /// small enough to read.
#[test] #[test]
#[ignore = "a hundred seeds, rather than the seven the others check"] #[ignore = "a hundred seeds, rather than the nine the others check"]
fn a_long_run_of_seeds_agrees() { fn a_long_run_of_seeds_agrees() {
let seeds = std::env::var("IRIS_GENERATED_SEED") let seeds = std::env::var("IRIS_GENERATED_SEED")
.ok() .ok()
+31
View File
@@ -349,3 +349,34 @@ fn a_span_out_of_room_shrinks_its_shares_and_not_its_fixed_lengths() {
} }
} }
} }
#[test]
fn only_a_pure_leftover_child_disappears_when_nothing_is_left() {
let mut h = Harness::new((100, 20));
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
let leftover = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((fixed, leftover).span(Dir::RIGHT));
assert_corners!(h, fixed, (0, 0), (100, 20));
assert_eq!(h.region(&leftover), None);
// An undrawn child remains a dependency of the span, so making room for
// it draws it without rebuilding the tree.
h.rsc[fixed].x = Some(Len::px(60));
h.frame();
assert_corners!(h, leftover, (60, 0), (100, 20));
let mut h = Harness::new((100, 20));
let fixed = rect(Color::RED).width(100).add(&mut h.rsc);
let mixed = SetSize {
inner: rect(Color::BLUE).add_strong(&mut h.rsc),
x: Some(Len::px(20) + Len::LEFTOVER),
y: None,
}
.add(&mut h.rsc);
h.set_root((fixed, mixed).span(Dir::RIGHT));
// Pixels and fractions still overflow; only a child whose entire length
// is leftover is omitted.
assert_corners!(h, mixed, (100, 0), (120, 20));
}
+83 -53
View File
@@ -6,22 +6,22 @@ use iris::harness::{Harness, assert_corners};
use iris::prelude::*; use iris::prelude::*;
/// A leaf that counts its draws and reports whatever size it is given, so a /// A leaf that counts its draws and reports whatever size it is given, so a
/// test can see what the retained path skipped. /// test can see what the retained path skipped. One that reads its box in
/// pixels has a drawing that holds for that box alone.
struct Counted { struct Counted {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
size: Size, size: Size,
dependence: OnResize, reads_box: bool,
} }
impl Widget for Counted { impl Widget for Counted {
fn draw(&mut self, _: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
if self.reads_box {
painter.px_size();
}
self.size self.size
} }
fn on_resize(&self, _: Axis) -> OnResize {
self.dependence
}
} }
struct Counts(Rc<Cell<usize>>); struct Counts(Rc<Cell<usize>>);
@@ -32,22 +32,63 @@ impl Counts {
} }
} }
fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Counted>, Counts) { fn counted(h: &mut Harness, size: Size, reads_box: bool) -> (WeakWidget<Counted>, Counts) {
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
let id = Counted { let id = Counted {
draws: draws.clone(), draws: draws.clone(),
size, size,
dependence, reads_box,
} }
.add(&mut h.rsc); .add(&mut h.rsc);
(id, Counts(draws)) (id, Counts(draws))
} }
struct Layered {
children: [StrongWidget<Rect>; 2],
_revision: usize,
}
impl Widget for Layered {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.child_layer();
painter.widget(&self.children[0]);
painter.next_layer();
painter.widget(&self.children[1]);
Size::default()
}
}
#[test]
fn a_redrawn_layered_widget_keeps_the_layer_it_was_entered_on() {
let mut h = Harness::new((400, 200));
let children = [
rect(Color::RED).add_strong(&mut h.rsc),
rect(Color::BLUE).add_strong(&mut h.rsc),
];
let root = Layered {
children,
_revision: 0,
}
.add(&mut h.rsc);
h.set_root(root);
h.rsc[root]._revision += 1;
h.frame();
let label = h.rsc.widgets().label(root.id());
let active = h
.render
.debug(h.rsc.widgets(), label)
.find(|active| active.id == root.id())
.unwrap();
assert_eq!(active.layer, 0);
}
/// A fixed-width leaf beside one that takes what is left over, so changing /// A fixed-width leaf beside one that takes what is left over, so changing
/// the first hands the second a different box without the output changing. /// the first hands the second a different box without the output changing.
fn pair(h: &mut Harness, leftover: OnResize) -> (WeakWidget<Counted>, Counts, WidgetId) { fn pair(h: &mut Harness, reads_box: bool) -> (WeakWidget<Counted>, Counts, WidgetId) {
let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate); let (first, _) = counted(h, Size::from((100, 200)), false);
let (second, draws) = counted(h, Size::LEFTOVER, leftover); let (second, draws) = counted(h, Size::LEFTOVER, reads_box);
h.set_root((first, second).span(Dir::RIGHT)); h.set_root((first, second).span(Dir::RIGHT));
(first, draws, second.id()) (first, draws, second.id())
} }
@@ -55,7 +96,7 @@ fn pair(h: &mut Harness, leftover: OnResize) -> (WeakWidget<Counted>, Counts, Wi
#[test] #[test]
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() { fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Scale); let (first, draws, second) = pair(&mut h, false);
let settled = draws.get(); let settled = draws.get();
assert_corners!(h, second, (100, 0), (400, 200)); assert_corners!(h, second, (100, 0), (400, 200));
@@ -73,7 +114,7 @@ fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
#[test] #[test]
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() { fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Redraw); let (first, draws, second) = pair(&mut h, true);
let settled = draws.get(); let settled = draws.get();
h.rsc[first].size = Size::from((150, 200)); h.rsc[first].size = Size::from((150, 200));
@@ -88,8 +129,8 @@ fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
#[test] #[test]
fn a_span_child_that_declares_its_length_is_drawn_once() { fn a_span_child_that_declares_its_length_is_drawn_once() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate); let (told, told_draws) = counted(&mut h, Size::from((100, 200)), false);
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate); let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), true);
// The span takes one child's length from its hint and has to draw the // The span takes one child's length from its hint and has to draw the
// other to find out, so only the second is drawn before it is placed. // other to find out, so only the second is drawn before it is placed.
let hinted = told.width(100).add(&mut h.rsc); let hinted = told.width(100).add(&mut h.rsc);
@@ -106,7 +147,7 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
#[test] #[test]
fn a_span_relays_out_when_a_child_it_measured_changes() { fn a_span_relays_out_when_a_child_it_measured_changes() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (first, _, second) = pair(&mut h, OnResize::Translate); let (first, _, second) = pair(&mut h, false);
h.rsc[first].size = Size::from((250, 200)); h.rsc[first].size = Size::from((250, 200));
h.frame(); h.frame();
@@ -118,8 +159,8 @@ fn a_span_relays_out_when_a_child_it_measured_changes() {
#[test] #[test]
fn a_repaint_that_keeps_its_size_does_not_relay_out() { fn a_repaint_that_keeps_its_size_does_not_relay_out() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (first, draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate); let (first, draws) = counted(&mut h, Size::from((100, 200)), false);
let (second, _) = counted(&mut h, Size::LEFTOVER, OnResize::Translate); let (second, _) = counted(&mut h, Size::LEFTOVER, false);
h.set_root((first, second).span(Dir::RIGHT)); h.set_root((first, second).span(Dir::RIGHT));
let settled = draws.get(); let settled = draws.get();
@@ -179,20 +220,20 @@ fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
assert_corners!(h, inner, (0, 0), (400, 120)); assert_corners!(h, inner, (0, 0), (400, 120));
} }
/// Reads the output's size, which nothing but its own draw can put right. /// Reads its box's size, which nothing but its own draw can put right.
struct ReadsOutput { struct ReadsBox {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
} }
impl Widget for ReadsOutput { impl Widget for ReadsBox {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::px(painter.output_size() / 4.0) Size::px(painter.px_size() / 4.0)
} }
} }
/// Reads the output across one axis only, and says so: its drawing follows /// Reads its box across one axis only, so its drawing holds for a taller
/// a taller box on its own, so only a wider one is worth a draw. /// box on its own and only a wider one is worth a draw.
struct ReadsWidth { struct ReadsWidth {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
} }
@@ -200,21 +241,14 @@ struct ReadsWidth {
impl Widget for ReadsWidth { impl Widget for ReadsWidth {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::px((painter.output_len(Axis::X) / 4.0, 20.0).into()) Size::px((painter.px_len(Axis::X) / 4.0, 20.0).into())
}
fn on_resize(&self, axis: Axis) -> OnResize {
match axis {
Axis::X => OnResize::Redraw,
Axis::Y => OnResize::Scale,
}
} }
} }
#[test] #[test]
fn a_resize_does_not_redraw_what_the_shader_can_move() { fn a_resize_does_not_redraw_what_the_shader_can_move() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, OnResize::Scale); let (leaf, draws) = counted(&mut h, Size::LEFTOVER, false);
h.set_root(leaf); h.set_root(leaf);
let settled = draws.get(); let settled = draws.get();
@@ -231,12 +265,12 @@ fn a_resize_does_not_redraw_what_the_shader_can_move() {
} }
/// The output is the root of the box chain, so a resize is a box that changed /// The output is the root of the box chain, so a resize is a box that changed
/// length and `OnResize` answers for it -- there is not a second rule for the /// length like any other -- there is not a second rule for the window. A
/// window. A drawing that does not scale is redrawn whichever box moved. /// drawing that holds for one length is drawn again whichever box moved.
#[test] #[test]
fn a_resize_redraws_what_does_not_scale() { fn a_resize_redraws_what_does_not_scale() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::LEFTOVER, OnResize::Redraw); let (leaf, draws) = counted(&mut h, Size::LEFTOVER, true);
h.set_root(leaf); h.set_root(leaf);
let settled = draws.get(); let settled = draws.get();
@@ -248,10 +282,10 @@ fn a_resize_redraws_what_does_not_scale() {
} }
#[test] #[test]
fn a_resize_redraws_what_read_the_output() { fn a_resize_redraws_what_read_its_box() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
let leaf = ReadsOutput { let leaf = ReadsBox {
draws: draws.clone(), draws: draws.clone(),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
@@ -265,7 +299,7 @@ fn a_resize_redraws_what_read_the_output() {
} }
#[test] #[test]
fn a_resize_only_redraws_read_output_axes() { fn a_resize_only_redraws_read_axes() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth { let leaf = ReadsWidth {
@@ -309,7 +343,7 @@ fn subpixel_resize_changes_accumulate_from_the_last_layout() {
#[test] #[test]
fn subpixel_box_changes_accumulate_from_the_last_draw() { fn subpixel_box_changes_accumulate_from_the_last_draw() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let (first, draws, _) = pair(&mut h, OnResize::Redraw); let (first, draws, _) = pair(&mut h, true);
let settled = draws.get(); let settled = draws.get();
for width in [100.02, 100.04, 100.05] { for width in [100.02, 100.04, 100.05] {
@@ -327,7 +361,7 @@ fn subpixel_box_changes_accumulate_from_the_last_draw() {
fn reporting_the_same_output_size_does_not_start_a_resize() { fn reporting_the_same_output_size_does_not_start_a_resize() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0)); let draws = Rc::new(Cell::new(0));
let leaf = ReadsOutput { let leaf = ReadsBox {
draws: draws.clone(), draws: draws.clone(),
} }
.add(&mut h.rsc); .add(&mut h.rsc);
@@ -368,7 +402,7 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
// Every wrapper up to the outer pad read the size below it, so the outer // Every wrapper up to the outer pad read the size below it, so the outer
// pad is what draws again -- and the span it hands the box to is the same // pad is what draws again -- and the span it hands the box to is the same
// size as before, which is what lets a draw reuse its way past the leaf. // size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), OnResize::Redraw); let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true);
let padded = leaf.pad(10).add(&mut h.rsc); let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc); let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12)); h.set_root((padded, below).span(Dir::DOWN).pad(12));
@@ -380,8 +414,8 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
assert_corners!(h, below, (12, 232), (388, 388)); assert_corners!(h, below, (12, 232), (388, 388));
} }
/// Claims its drawing survives its box changing length, and has a child so /// Reads nothing of its box, so its drawing holds for any length, and has a
/// that the walk looking for what does not has one to reach. /// child so that whatever asks about the subtree has one to reach.
struct Stretchy { struct Stretchy {
inner: StrongWidget, inner: StrongWidget,
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
@@ -392,10 +426,6 @@ impl Widget for Stretchy {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
painter.widget(&self.inner).size() painter.widget(&self.inner).size()
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
#[test] #[test]
@@ -430,8 +460,8 @@ fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
// What a transcript row is: something whose shaping depends on the width // What a transcript row is: something whose shaping depends on the width
// it is given, beside something that only has to be the right shape. // it is given, beside something that only has to be the right shape.
let (wraps, wrap_draws) = counted(&mut h, Size::LEFTOVER, OnResize::Redraw); let (wraps, wrap_draws) = counted(&mut h, Size::LEFTOVER, true);
let (backing, back_draws) = counted(&mut h, Size::LEFTOVER, OnResize::Scale); let (backing, back_draws) = counted(&mut h, Size::LEFTOVER, false);
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc); let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc); let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, row).span(Dir::RIGHT)); h.set_root((bar, row).span(Dir::RIGHT));
@@ -455,9 +485,9 @@ fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
// again would be for a width it does not have. The declared width is what // again would be for a width it does not have. The declared width is what
// lets the span say that without drawing it: a width the span learnt by // lets the span say that without drawing it: a width the span learnt by
// drawing the child in its own box is only an answer for that box. // drawing the child in its own box is only an answer for that box.
let (counter, draws) = counted(&mut h, Size::from((80, 200)), OnResize::Redraw); let (counter, draws) = counted(&mut h, Size::from((80, 200)), true);
let fixed = counter.width(80).add(&mut h.rsc); let fixed = counter.width(80).add(&mut h.rsc);
let (leftover, _) = counted(&mut h, Size::LEFTOVER, OnResize::Scale); let (leftover, _) = counted(&mut h, Size::LEFTOVER, false);
let row = (fixed, leftover).span(Dir::RIGHT).add(&mut h.rsc); let row = (fixed, leftover).span(Dir::RIGHT).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc); let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, row).span(Dir::RIGHT)); h.set_root((bar, row).span(Dir::RIGHT));
+43
View File
@@ -296,3 +296,46 @@ fn a_span_placed_once_in_a_box_its_answer_decided() {
} }
assert!(wrong.is_empty(), "{}", wrong.join("\n")); assert!(wrong.is_empty(), "{}", wrong.join("\n"));
} }
/// Reports a width derived from the box it is asked in. Reading through the
/// painter is its declaration that the answer holds for that width only.
struct Wider {
extra: f32,
}
impl Widget for Wider {
fn draw(&mut self, painter: &mut Painter) -> Size {
Size {
x: Len::px(painter.px_len(Axis::X) + self.extra),
y: Len::LEFTOVER,
}
}
}
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
let content = Wider { extra }.add(&mut h.rsc);
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
let root = Aligned {
inner: scroll.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Neg),
y: None,
},
}
.add(&mut h.rsc);
h.set_root(root);
(content, scroll.id())
}
#[test]
fn a_scrolls_retained_answer_is_the_one_a_cold_layout_asks_for() {
let mut warm = Harness::new((100, 100));
let (content, scroll) = plant_wider(&mut warm, 50.0);
warm.rsc[content].extra = 70.0;
warm.frame();
let mut cold = Harness::new((100, 100));
let (_, cold_scroll) = plant_wider(&mut cold, 70.0);
assert_eq!(warm.region(&scroll), cold.region(&cold_scroll));
}