Compare commits

..
Author SHA1 Message Date
iris-ai 46547563c1 Take a strong widget to depend on, and use the question mark
`depend_on_size` took a bare `WidgetId`, which any id at all satisfies.
It takes a `&StrongWidget` now, so `DrawResult` carries the handle it
was drawn from rather than an id copied out of it and nothing can claim
a dependency on a widget it does not hold.

`UiSpan::outside` matched on a pair of `Option`s where `?` says it. It
was written that way while the method was still `const`, and it is not.

No call site changed. Checked: fmt, clippy and 35 tests; `tabs`, `view`,
`minimal` and `text` render unchanged, and the live sway resize round
trip still matches a cold start at each size.
2026-09-14 02:44:40 -04:00
iris-ai 53e61289f5 Say when a drawing cannot be taken out of its box, rather than guessing
`lerp_inv` returns `Option`. Inverting a lerp over a range of zero
length has no one answer, and `div_or`'s fallback picked one: the start
of the range. Measured, that is not a wrong number so much as a
plausible one -- taking a part out of a box fixed at the top of the
window hands back exactly what went in, and out of a box fixed at the
middle hands back the parent's own `rel` of 0.5 as if it were the
child's fraction. Either way the caller cannot tell that nothing was
recovered, which is the defect; the previous commit's claim that it
"returns a rel of 0" is right only for the first case.

`UiScalar::outside` and `UiSpan::outside` follow it to `Option`, and
`Remap` is the answer at the region level: `new` says whether a drawing
in one box can be put in another and `apply` then cannot fail, so
`try_reuse` asks once for a whole subtree and `reusable` goes back to
reporting only what the widget claims. That replaces the predicate the
last commit put in `reusable`, which stated the same rule in a second
place.

`DivOr` existed only for the fallback and is gone, along with
`UiRegion::outside` and `UiVec2::outside`, which had no callers once
`Remap` owned the operation. `UiRegion::axis` took `&mut self` to
return a shared reference; `Remap::new` needs it on a shared one.

This does not redraw less. `Remap::new` refuses exactly what the
predicate refused; what it buys is one statement of the rule and a
remap that cannot half-apply. Counted on a resize, with the old
wipe-everything for comparison:

  20 padded rows, 101 widgets     101 draws -> 0
  the `tabs` example               73 draws -> 0
  the `text` example               49 draws -> 48

So the saving is whole where a resize does not change any widget's
size, and nil in `text`, where both paragraphs rewrap to a different
height and the relayout that forces reaches the root. The last commit's
message oversold that case.

Checked: fmt, clippy and 35 tests. `tabs` (with the image replay),
`view` and `minimal` still byte-identical to `upstream/main`, `text`
unchanged, and the live sway resize round trip still matches a cold
start at each size.
2026-09-14 01:27:16 -04:00
iris-ai 984f482a7f Move a resized drawing instead of redrawing it
A region is a fraction of the output plus an offset and the shader
resolves it against the window every frame, so a resize already moves
the whole drawing without the CPU. Wiping the tree and drawing it again
was throwing that away.

`Painter::output_size` and `px_size` now record that a widget read
pixels, the way reading a child's size records a dependency on it, and a
resize marks only those. In the `text` example that is the two wrapping
paragraphs out of forty-odd widgets; everything else keeps its drawing
and the window uniform puts it in the right place.

Two defects the change surfaced, both of which made a resize land
somewhere a cold start would not:

`redraw` climbed to the highest reader of the changed widget and drew
from there, trusting that draw to reach back down. It does not: an
intermediate whose own box has not changed is reused as it stands and
the draw stops there. Everything between the two is now marked as well,
which is the only thing that stops the reuse. Not resize-specific --
`a_change_two_levels_under_its_reader_still_reaches_it` fails on the
mutation path too.

`mov` cannot stretch a drawing out of a box with no relative extent.
`UiScalar::within` puts a part into such a box as a plain offset from
its start, and `lerp_inv`'s divide-by-zero fallback then returns a
rel of 0 rather than saying it cannot invert, so the remap silently
leaves the drawing its old size. `OnResize::Scale` now only reuses
across a length change when the old box had a relative extent. The
underlying loss belongs to the position chain, which separates the
drawn box from the offered one; until then this is the honest
predicate.

Checked: fmt, clippy and 33 tests. `tabs` (with the image replay),
`view`, `minimal` still byte-identical to `upstream/main`, and `text`
unchanged at 1920x1200 and 900x1200. Driven live under the GPU as well:
started at 1920x1200, resized to 900x1200 and back through sway, and
each screenshot matches a cold start at that size byte for byte.
2026-09-14 01:09:03 -04:00
iris-ai 9520996623 Return the size from draw, and fold placing back into drawing
Review response.

`Painter::set_size` is gone: `Widget::draw` returns the `Size` instead, so
a widget that does not say what it used cannot compile rather than
panicking at the widget that forgot. That also settles setting it twice --
a branch that learns something late just returns a different value.

`Painter::place` and `UiRenderState::place` are gone too. `draw_inner`
already tried to reuse an active widget's drawing before redrawing it, so
`place` was `widget_within` with its own bookkeeping bolted on; drawing a
child a second time now *is* how a parent puts it where it belongs, and a
child is deduplicated in `children` because listing one twice would move
it twice. The unification also drops `place`'s use of `ActiveData::layer`,
which is the layer a widget's own `child_layer()` left the painter on
rather than the layer it was drawn into.

What `place` did unconditionally and `widget_within` did not is record the
size dependency, so `Painter::size_hint` now records one: reading a
child's length to lay out around it is reading its size, whether it came
from a draw or from a hint. `tests/retained.rs` has a parent that only
ever reads the hint, which is the case no existing widget exercises.

`()` sizes itself `Size::default()` -- rest -- rather than zero, so it is
a gap that takes an even share of a span; `WidgetPtr` with nothing in it
does the same, since it is the same situation. `Widget::on_resize`'s
default body said `Translate` while the enum's `#[default]` said `Redraw`;
it now defers to the enum.

`was` is `old` throughout, the `OnResize` variant comments are gone, and
so are two empty `impl` blocks.

Checked: fmt, clippy and 27 tests across the workspace; `tabs` on each of
its five tabs, and `tabs` with a replay that adds two images, all
byte-identical to `upstream/main`; `view` and `minimal` likewise; and the
`text` example rendered at 1920x1200 and 900x1200 to see the paragraph
reflow and its container follow.
2026-09-14 00:39:39 -04:00
iris b108645240 Say what may be done to a drawing, and default to nothing
`SizeDependence::{None, Internal, External}` becomes
`OnResize::{Scale, Translate, Redraw}`, which says what the retained path
may do rather than leaving the reader to work it out from a dependency.

`Redraw` is now the default, and that is the substance of this rather
than the naming. `Translate` was, and nothing opted into it: `SetSize`
reports one size and hands its child the whole box, so its pixels change
with the box and carrying them stretched a 100x100 rect across half the
window. A default that is only right for widgets that happen to qualify
is the same fault as an unchecked reuse flag.

`Translate` still does nothing, and now for the reason rather than the
one I gave before: `mov` translates perfectly well, but `ActiveData`'s
`region` is both the box a widget was given and the box its primitives
occupy, and `mov` remaps out of it. Keeping a drawing at its old size
while the box grows leaves those two disagreeing, and the next move
stretches it. Separating them is what the offset chain does.

Caught by rendering `tabs` against `main` rather than by a test, which is
the argument for keeping that check in the loop.
2026-09-13 23:47:12 -04:00
iris ec012c4552 Text depends on its box for where it sits, not only for shaping
Alignment was not accounted for. Glyphs anchored to the start of an axis
stay where they are when that extent changes, but the default is
`CENTER_LEFT`: a label in a box that grows taller has to re-centre, even
though its shaping is untouched. Saying `Internal` there would keep a
drawing that belongs somewhere else once the retained path can act on it.

It costs nothing today, since neither `Internal` nor `External` reuses
anything yet. It would be silent when the move chain lands, which is why
the classification wants to be right while it is being written rather
than when something starts trusting it.
2026-09-13 23:28:47 -04:00
25 changed files with 482 additions and 286 deletions

No files matched your search

+55 -23
View File
@@ -56,13 +56,6 @@ impl UiVec2 {
} }
} }
pub const fn outside(&self, region: &UiRegion) -> UiVec2 {
UiVec2 {
x: self.x.outside(&region.x),
y: self.y.outside(&region.y),
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar { pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
match axis { match axis {
Axis::X => &mut self.x, Axis::X => &mut self.x,
@@ -209,10 +202,12 @@ impl UiScalar {
} }
} }
pub const fn outside(&self, span: &UiSpan) -> Self { /// Undoes `within`, and `None` where the span has a fixed length: every
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel); /// fraction of it lands on the same `rel`, so none can be told apart.
pub fn outside(&self, span: &UiSpan) -> Option<Self> {
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel)?;
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs); let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
Self { rel, abs } Some(Self { rel, abs })
} }
pub fn within_len(&self, len: UiScalar) -> Self { pub fn within_len(&self, len: UiScalar) -> Self {
@@ -283,11 +278,11 @@ impl UiSpan {
} }
} }
pub const fn outside(&self, parent: &Self) -> Self { pub fn outside(&self, parent: &Self) -> Option<Self> {
Self { Some(Self {
start: self.start.outside(parent), start: self.start.outside(parent)?,
end: self.end.outside(parent), end: self.end.outside(parent)?,
} })
} }
pub const fn len(&self) -> UiScalar { pub const fn len(&self) -> UiScalar {
@@ -324,14 +319,7 @@ impl UiRegion {
y: self.y.within(&parent.y), y: self.y.within(&parent.y),
} }
} }
pub const fn outside(&self, parent: &Self) -> Self { pub const fn axis(&self, axis: Axis) -> &UiSpan {
Self {
x: self.x.outside(&parent.x),
y: self.y.outside(&parent.y),
}
}
pub const fn axis(&mut self, axis: Axis) -> &UiSpan {
match axis { match axis {
Axis::X => &self.x, Axis::X => &self.x,
Axis::Y => &self.y, Axis::Y => &self.y,
@@ -409,6 +397,50 @@ impl UiRegion {
} }
} }
/// Taking a drawing out of one box and putting it in another, checked once
/// for a whole subtree so that applying it cannot fail.
///
/// A box of a fixed length holds each part as an offset from its start rather
/// than as a fraction of it, so those parts can be carried to a box of the
/// same length but never stretched to a different one.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Remap {
from: UiRegion,
to: UiRegion,
}
impl Remap {
pub fn new(from: UiRegion, to: UiRegion) -> Option<Self> {
[Axis::X, Axis::Y]
.into_iter()
.all(|axis| {
let (from, to) = (from.axis(axis), to.axis(axis));
from.start.rel != from.end.rel || from.len() == to.len()
})
.then_some(Self { from, to })
}
pub fn apply(&self, region: UiRegion) -> UiRegion {
UiRegion {
x: Self::span(region.x, self.from.x, self.to.x),
y: Self::span(region.y, self.from.y, self.to.y),
}
}
fn span(span: UiSpan, from: UiSpan, to: UiSpan) -> UiSpan {
match span.outside(&from) {
Some(out) => out.within(&to),
// `new` admits this only where the two are the same length, so
// the difference between their starts is the whole move.
None => {
let mut span = span;
span.shift(to.start - from.start);
span
}
}
}
}
impl Display for UiRegion { impl Display for UiRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
+2
View File
@@ -13,6 +13,8 @@ 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>,
/// Whether it read the output's size, and so is wrong when that changes.
pub reads_output: bool,
pub mask: MaskIdx, pub mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
+36 -57
View File
@@ -20,7 +20,7 @@ pub struct Painter<'a> {
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>, pub(super) size_deps: Vec<WidgetId>,
pub(super) size: Option<Size>, pub(super) reads_output: bool,
pub layer: usize, pub layer: usize,
pub(super) id: WidgetId, pub(super) id: WidgetId,
} }
@@ -70,39 +70,31 @@ impl<'a> Painter<'a> {
self.mask = self.rsc.ui_mut().masks.push(Mask { region }); self.mask = self.rsc.ui_mut().masks.push(Mask { region });
} }
/// States what this widget uses of the box it was handed, once per draw.
/// This is the size a parent reads.
pub fn set_size(&mut self, size: impl Into<Size>) {
debug_assert!(
self.size.is_none(),
"{} set its size twice in one draw",
self.label(),
);
self.size = Some(size.into());
}
/// Draws a widget within this widget's region. /// Draws a widget within this widget's region.
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) -> DrawResult<'_, 'a> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_at(id, self.region) self.widget_at(id, self.region)
} }
/// Draws a widget somewhere within this one. /// Draws a widget somewhere within this one. Drawing one a second time
/// Useful for drawing child widgets in select areas. /// gives it a new box, keeping the drawing it already has where it can.
pub fn widget_within<W: ?Sized>( pub fn widget_within<'s, W: ?Sized>(
&mut self, &'s mut self,
id: &StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> DrawResult<'_, 'a> { ) -> DrawResult<'s, 'a, W> {
let region = region.within(&self.region); let region = region.within(&self.region);
self.widget_at(id, region) self.widget_at(id, region)
} }
fn widget_at<W: ?Sized>( fn widget_at<'s, W: ?Sized>(
&mut self, &'s mut self,
id: &StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> DrawResult<'_, 'a> { ) -> DrawResult<'s, 'a, W> {
self.children.push(id.id()); // A child listed twice would be moved twice.
if !self.children.contains(&id.id()) {
self.children.push(id.id());
}
let size = self.state.draw_inner( let size = self.state.draw_inner(
self.layer, self.layer,
id.id(), id.id(),
@@ -113,43 +105,24 @@ impl<'a> Painter<'a> {
self.rsc, self.rsc,
); );
DrawResult { DrawResult {
child: id.id(), child: id,
painter: self, painter: self,
size, size,
} }
} }
/// What a child says its length is without being drawn, if it can say. /// What a child says its length is without being drawn, if it can say.
pub fn size_hint<W: ?Sized>(&self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> { /// Asking counts as reading its size.
self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
let hint = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis)?;
self.depend_on_size(id);
Some(hint)
} }
/// Gives a child its final box once this widget knows what that is, fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
/// keeping the drawing it already has where it can. if !self.size_deps.contains(&child.id()) {
pub fn place<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size { self.size_deps.push(child.id());
let region = region.within(&self.region);
let id = id.id();
// Choosing a child's box depends on its size, from a draw or a hint.
// Claiming it is separate: one left out of `children` is removed.
if !self.size_deps.contains(&id) {
self.size_deps.push(id);
} }
if !self.children.contains(&id) {
self.children.push(id);
}
if self.state.active.contains_key(&id) {
return self.state.place(id, region, self.rsc);
}
// Not drawn yet: its length came from a hint, so this is its one draw.
self.state.draw_inner(
self.layer,
id,
region,
Some(self.id),
self.mask,
None,
self.rsc,
)
} }
pub fn render_text( pub fn render_text(
@@ -190,11 +163,17 @@ impl<'a> Painter<'a> {
self.region self.region
} }
pub fn output_size(&self) -> Vec2 { /// The output's size in pixels. A widget that reads it draws again when
/// the output changes, since nothing else can put that right.
pub fn output_size(&mut self) -> Vec2 {
self.reads_output = true;
self.state.output_size self.state.output_size
} }
/// This widget's box in pixels. Resolved against the output's size, 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.reads_output = true;
self.region.size().to_abs(self.state.output_size) self.region.size().to_abs(self.state.output_size)
} }
@@ -222,15 +201,15 @@ impl<'a> Painter<'a> {
/// A child that has just been drawn. Reading its size records that this /// A child that has just been drawn. Reading its size records that this
/// widget's own size depends on it; dropping it without reading draws the /// widget's own size depends on it; dropping it without reading draws the
/// child and leaves the parent independent of what it came to. /// child and leaves the parent independent of what it came to.
pub struct DrawResult<'p, 'a> { pub struct DrawResult<'p, 'a, W: ?Sized> {
painter: &'p mut Painter<'a>, painter: &'p mut Painter<'a>,
child: WidgetId, child: &'p StrongWidget<W>,
size: Size, size: Size,
} }
impl DrawResult<'_, '_> { impl<W: ?Sized> DrawResult<'_, '_, W> {
pub fn size(self) -> Size { pub fn size(self) -> Size {
self.painter.size_deps.push(self.child); self.painter.depend_on_size(self.child);
self.size self.size
} }
+54 -69
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, Size, SizeDependence, ActiveData, Axis, DrawLayers, IdLike, MaskIdx, OnResize, Painter, PixelRegion, Remap, Size,
StrongWidget, UiRegion, UiRsc, WidgetId, Widgets, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
util::{HashMap, HashSet, Vec2, forget_ref}, util::{HashMap, HashSet, Vec2, forget_ref},
}; };
@@ -53,11 +53,21 @@ impl UiRenderState {
); );
} }
let root = root.into(); let root = root.into();
if self.needs_full_redraw(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());
self.resized = false; } else if self.resized {
} else if rsc.widgets().has_updates() { // A region is a fraction of the output plus an offset, resolved
// against the window in the shader, so a resize moves the whole
// drawing on its own. Only a widget that read pixels can be wrong.
for (&id, active) in &self.active {
if active.reads_output {
rsc.widgets_mut().needs_redraw.insert(id);
}
}
}
self.resized = false;
if rsc.widgets().has_updates() {
self.redraw_updates(rsc); self.redraw_updates(rsc);
} }
} }
@@ -93,6 +103,7 @@ impl UiRenderState {
} }
// draw widget // draw widget
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.insert(id); self.draw_started.insert(id);
let mut painter = Painter { let mut painter = Painter {
@@ -105,12 +116,12 @@ impl UiRenderState {
primitives: Vec::new(), primitives: Vec::new(),
children: Vec::new(), children: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
size: None, reads_output: false,
rsc, rsc,
}; };
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id); let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
widget.draw(&mut painter); let size = widget.draw(&mut painter);
drop(widget); drop(widget);
let Painter { let Painter {
@@ -122,17 +133,11 @@ impl UiRenderState {
primitives, primitives,
children, children,
size_deps, size_deps,
size, reads_output,
layer, layer,
id, id,
} = painter; } = painter;
let size = size.unwrap_or_else(|| {
panic!(
"'{}' ({id:?}) drew without a size; every widget calls Painter::set_size",
rsc.widgets().label(id)
)
});
debug_assert!( debug_assert!(
Self::hints_agree(id, size, rsc), Self::hints_agree(id, size, rsc),
"'{}' ({id:?}) drew a size its size_hint disagrees with", "'{}' ({id:?}) drew a size its size_hint disagrees with",
@@ -149,6 +154,7 @@ impl UiRenderState {
primitives, primitives,
children, children,
size_deps, size_deps,
reads_output,
mask, mask,
layer, layer,
}; };
@@ -165,26 +171,6 @@ impl UiRenderState {
size size
} }
/// Gives an already drawn widget a new box, keeping its drawing if it can
/// and drawing it again if it cannot.
pub(super) fn place(&mut self, id: WidgetId, region: UiRegion, rsc: &mut dyn UiRsc) -> Size {
if let Some(size) = self.try_reuse(id, region, rsc) {
return size;
}
let Some(active) = self.remove(id, false, rsc) else {
return Size::ZERO;
};
self.draw_inner(
active.layer,
id,
region,
active.parent,
active.mask,
Some(active.children),
rsc,
)
}
/// The drawing a widget already has, kept for a new box if the box has not /// The drawing a widget already has, kept for a new box if the box has not
/// changed in a way it depends on. /// changed in a way it depends on.
fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option<Size> { fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option<Size> {
@@ -192,43 +178,41 @@ impl UiRenderState {
return None; return None;
} }
let active = self.active.get(&id)?; let active = self.active.get(&id)?;
let (size, was) = (active.size, active.region); let (size, old) = (active.size, active.region);
if was == region { if old == region {
return Some(size); return Some(size);
} }
// TODO: epsilon? // TODO: epsilon?
if was.size() == region.size() { if old.size() != region.size() && !self.reusable(id, region, rsc) {
self.mov(id, was, region); return None;
return Some(size);
} }
if self.reusable(id, region, rsc) { // Its drawing stands, if the new box can be reached from the old one.
// Its drawing stands; the box is written into the primitives. self.mov(id, &Remap::new(old, region)?);
self.mov(id, was, region); Some(size)
return Some(size);
}
None
} }
/// Whether the widget can keep the drawing it has and be given `region` /// Whether the widget can keep the drawing it has and be given `region`
/// instead, asked one axis at a time: a change on an axis it does not /// instead, asked one axis at a time: a change on an axis it does not
/// depend on costs nothing, whatever it depends on elsewhere. /// depend on costs nothing, whatever it depends on elsewhere.
fn reusable(&self, id: WidgetId, mut region: UiRegion, rsc: &dyn UiRsc) -> bool { fn reusable(&self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> bool {
let Some(active) = self.active.get(&id) else { let Some(active) = self.active.get(&id) else {
return false; return false;
}; };
let Some(widget) = rsc.widgets().get_dyn(id) else { let Some(widget) = rsc.widgets().get_dyn(id) else {
return false; return false;
}; };
let mut was = active.region;
[Axis::X, Axis::Y].into_iter().all(|axis| { [Axis::X, Axis::Y].into_iter().all(|axis| {
let offered = region.axis_mut(axis).len(); let offered = region.axis(axis).len();
let had = was.axis_mut(axis).len(); let had = active.region.axis(axis).len();
match widget.size_dependence(axis) { match widget.on_resize(axis) {
SizeDependence::None => true, OnResize::Scale => true,
// `Internal` could also keep its drawing when only the room // `Translate` is not acted on yet, and cannot be until a
// around it changed, but that is a translation rather than a // drawing can sit somewhere other than its box. `region` is
// remap, so it waits for the move chain. // both the box a widget was given and the box its primitives
SizeDependence::Internal | SizeDependence::External => offered == had, // are in, and `mov` remaps from it -- so carrying a drawing at
// its old size while the box grows makes the next move stretch
// it. The offset chain is what separates the two.
OnResize::Translate | OnResize::Redraw => offered == had,
} }
}) })
} }
@@ -244,17 +228,17 @@ impl UiRenderState {
}) })
} }
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) { fn mov(&mut self, id: WidgetId, remap: &Remap) {
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
for h in &active.primitives { for h in &active.primitives {
let region = self.layers[h.layer].region_mut(h); let region = self.layers[h.layer].region_mut(h);
*region = region.outside(&from).within(&to); *region = remap.apply(*region);
} }
active.region = active.region.outside(&from).within(&to); active.region = remap.apply(active.region);
// SAFETY: children cannot be recursive // SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) }; let children = unsafe { forget_ref(&active.children) };
for child in children { for child in children {
self.mov(*child, from, to); self.mov(*child, remap);
} }
} }
@@ -307,17 +291,12 @@ impl UiRenderState {
root.into().map(|r| r.id()) != self.old_root root.into().map(|r| r.id()) != self.old_root
} }
// Scheduling and drawing must use the same full-redraw predicate.
fn needs_full_redraw<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
self.root_changed(root) || self.resized
}
pub fn needs_redraw<'a>( pub fn needs_redraw<'a>(
&self, &self,
root: impl Into<Option<&'a StrongWidget>>, root: impl Into<Option<&'a StrongWidget>>,
widgets: &Widgets, widgets: &Widgets,
) -> bool { ) -> bool {
self.needs_full_redraw(root) || widgets.has_updates() self.root_changed(root) || self.resized || widgets.has_updates()
} }
pub fn active_widgets(&self) -> usize { pub fn active_widgets(&self) -> usize {
@@ -350,14 +329,19 @@ impl UiRenderState {
/// redraws a widget that's currently active (drawn) /// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.remove(&id); self.draw_started.remove(&id);
// Whoever read this widget's size may be a different size now, so the // Whoever read this widget's size may be a different size now, so the
// highest reader is what draws; it reaches this one on the way down. // highest reader is what draws. Everything between the two is marked
if let Some(top) = self.highest_reader(id) { // as well: their own boxes have not changed, so the mark is the only
// thing stopping the draw reusing its way past this widget.
if let Some(top) = self.mark_readers(id, rsc) {
self.redraw(top, rsc); self.redraw(top, rsc);
// Cleared by that draw if it reached here; if it did not, this is
// no longer drawn and asking again would not end.
rsc.widgets_mut().needs_redraw.remove(&id);
return; return;
} }
rsc.widgets_mut().needs_redraw.remove(&id);
if self.draw_started.contains(&id) { if self.draw_started.contains(&id) {
return; return;
@@ -379,8 +363,8 @@ impl UiRenderState {
} }
/// The furthest ancestor that read this widget's size, directly or through /// The furthest ancestor that read this widget's size, directly or through
/// widgets that did the same. /// widgets that did the same, marking everything below it on the way.
fn highest_reader(&self, id: WidgetId) -> Option<WidgetId> { fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> {
let mut top = None; let mut top = None;
let mut at = id; let mut at = id;
while let Some(active) = self.active.get(&at) while let Some(active) = self.active.get(&at)
@@ -390,6 +374,7 @@ impl UiRenderState {
.get(&parent) .get(&parent)
.is_some_and(|p| p.size_deps.contains(&at)) .is_some_and(|p| p.size_deps.contains(&at))
{ {
rsc.widgets_mut().needs_redraw.insert(at);
top = Some(parent); top = Some(parent);
at = parent; at = parent;
} }
+10 -22
View File
@@ -1,33 +1,21 @@
use std::ops::*; pub const trait LerpUtil: Sized {
pub const trait LerpUtil {
fn lerp(self, from: Self, to: Self) -> Self; fn lerp(self, from: Self, to: Self) -> Self;
fn lerp_inv(self, from: Self, to: Self) -> Self; fn lerp_inv(self, from: Self, to: Self) -> Option<Self>;
} }
pub const trait DivOr { const impl LerpUtil for f32 {
fn div_or(self, rhs: Self, other: Self) -> Self;
}
const impl DivOr for f32 {
fn div_or(self, rhs: Self, other: Self) -> Self {
let res = self / rhs;
if res.is_nan() { other } else { res }
}
}
const impl<
T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy,
> LerpUtil for T
{
/// linear interpolation /// linear interpolation
/// from * (1.0 - self) + to * self /// from * (1.0 - self) + to * self
fn lerp(self, from: Self, to: Self) -> Self { fn lerp(self, from: Self, to: Self) -> Self {
from + (to - from) * self from + (to - from) * self
} }
/// inverse of lerp /// inverse of lerp, and `None` where `from` and `to` are the same point:
fn lerp_inv(self, from: Self, to: Self) -> Self { /// every input lerps to it, so there is no one answer to come back to.
(self - from).div_or(to - from, from) fn lerp_inv(self, from: Self, to: Self) -> Option<Self> {
match to == from {
true => None,
false => Some((self - from) / (to - from)),
}
} }
} }
+1 -10
View File
@@ -1,4 +1,4 @@
use crate::util::{DivOr, impl_op}; use crate::util::impl_op;
use std::{hash::Hash, ops::*}; use std::{hash::Hash, ops::*};
#[repr(C)] #[repr(C)]
@@ -67,15 +67,6 @@ impl_op!(Vec2 Sub sub; x y);
impl_op!(Vec2 Mul mul; x y); impl_op!(Vec2 Mul mul; x y);
impl_op!(Vec2 Div div; x y); impl_op!(Vec2 Div div; x y);
const impl DivOr for Vec2 {
fn div_or(self, rhs: Self, other: Self) -> Self {
Self {
x: self.x.div_or(rhs.x, other.x),
y: self.y.div_or(rhs.y, other.y),
}
}
}
impl Neg for Vec2 { impl Neg for Vec2 {
type Output = Self; type Output = Self;
+17 -21
View File
@@ -15,25 +15,20 @@ pub use tag::*;
pub use view::*; pub use view::*;
pub use widgets::*; pub use widgets::*;
/// How much of the box a widget was handed its drawing depends on, and so /// What may be done to a widget's drawing when the box it was given changes
/// what has to change before it must be drawn again. Asked per axis, because /// on this axis, instead of drawing it again. Asked per axis, because wrapped
/// wrapped text depends on the width it is offered and not on the height. /// text reads the width it is offered and not the height.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SizeDependence { pub enum OnResize {
/// None of it: the box only says where the primitives go, so a new one is Scale,
/// written into them instead of drawn. Translate,
None,
/// Its own extent, whatever box that sits in. Reusable in any box that
/// leaves that extent unchanged, including a larger one it does not fill.
#[default] #[default]
Internal, Redraw,
/// The extent of the box itself, used or not.
External,
} }
pub trait Widget: Any { pub trait Widget: Any {
/// Draws the widget, and states what it used with `Painter::set_size`. /// Draws the widget, and returns what it used of the box it was given.
fn draw(&mut self, painter: &mut Painter); fn draw(&mut self, painter: &mut Painter) -> Size;
/// An exact length the widget can give without a painter or its children. /// An exact length the widget can give without a painter or its children.
/// Optional, and saves a draw rather than changing one: a hint that /// Optional, and saves a draw rather than changing one: a hint that
@@ -42,22 +37,23 @@ pub trait Widget: Any {
None None
} }
fn size_dependence(&self, _axis: Axis) -> SizeDependence { fn on_resize(&self, _axis: Axis) -> OnResize {
SizeDependence::Internal OnResize::default()
} }
} }
impl Widget for () { impl Widget for () {
fn draw(&mut self, painter: &mut Painter) { /// A gap: nothing drawn, at the default length, so a span gives it a share.
painter.set_size(Size::ZERO); fn draw(&mut self, _: &mut Painter) -> Size {
Size::default()
} }
fn size_hint(&self, _axis: Axis) -> Option<Len> { fn size_hint(&self, _axis: Axis) -> Option<Len> {
Some(Len::ZERO) Some(Len::default())
} }
fn size_dependence(&self, _axis: Axis) -> SizeDependence { fn on_resize(&self, _axis: Axis) -> OnResize {
SizeDependence::None OnResize::Scale
} }
} }
+4 -4
View File
@@ -6,17 +6,17 @@ pub struct Image {
} }
impl Widget for Image { impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.primitive(&self.handle); painter.primitive(&self.handle);
painter.set_size(Size::abs(self.handle.size())); Size::abs(self.handle.size())
} }
fn size_hint(&self, axis: Axis) -> Option<Len> { fn size_hint(&self, axis: Axis) -> Option<Len> {
Some(Len::abs(self.handle.size().axis(axis))) Some(Len::abs(self.handle.size().axis(axis)))
} }
fn size_dependence(&self, _: Axis) -> SizeDependence { fn on_resize(&self, _: Axis) -> OnResize {
SizeDependence::None OnResize::Scale
} }
} }
+4 -5
View File
@@ -5,14 +5,13 @@ pub struct Masked {
} }
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region()); painter.set_mask(painter.region());
let size = painter.widget(&self.inner).size(); painter.widget(&self.inner).size()
painter.set_size(size);
} }
/// It clips to the box it was given, not to the part its child used. /// It clips to the box it was given, not to the part its child used.
fn size_dependence(&self, _: Axis) -> SizeDependence { fn on_resize(&self, _: Axis) -> OnResize {
SizeDependence::External OnResize::Redraw
} }
} }
+3 -3
View File
@@ -6,7 +6,7 @@ pub struct Aligned {
} }
impl Widget for Aligned { impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
// Drawn where it may be too big, then given its aligned box once its // Drawn where it may be too big, then given its aligned box once its
// size is known. // size is known.
let size = painter.widget(&self.inner).size(); let size = painter.widget(&self.inner).size();
@@ -16,7 +16,7 @@ impl Widget for Aligned {
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)), (None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
(None, None) => UiRegion::FULL, (None, None) => UiRegion::FULL,
}; };
painter.place(&self.inner, region); painter.widget_within(&self.inner, region);
painter.set_size(size); size
} }
} }
+2 -3
View File
@@ -6,11 +6,10 @@ pub struct LayerOffset {
} }
impl Widget for LayerOffset { impl Widget for LayerOffset {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
for _ in 0..self.offset { for _ in 0..self.offset {
painter.next_layer(); painter.next_layer();
} }
let size = painter.widget(&self.inner).size(); painter.widget(&self.inner).size()
painter.set_size(size);
} }
} }
+3 -5
View File
@@ -6,16 +6,14 @@ pub struct MaxSize {
pub y: Option<Len>, pub y: Option<Len>,
} }
impl MaxSize {}
impl Widget for MaxSize { impl Widget for MaxSize {
fn draw(&mut self, painter: &mut Painter) { 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 output = painter.output_size();
painter.set_size(Size { Size {
x: capped(child.x, self.x, output.x), x: capped(child.x, self.x, output.x),
y: capped(child.y, self.y, output.y), y: capped(child.y, self.y, output.y),
}); }
} }
} }
+2 -3
View File
@@ -6,9 +6,8 @@ pub struct Offset {
} }
impl Widget for Offset { impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let region = UiRegion::FULL.offset(self.amt); let region = UiRegion::FULL.offset(self.amt);
let size = painter.widget_within(&self.inner, region).size(); painter.widget_within(&self.inner, region).size()
painter.set_size(size);
} }
} }
+3 -3
View File
@@ -6,11 +6,11 @@ pub struct Pad {
} }
impl Widget for Pad { impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let inner = painter let inner = painter
.widget_within(&self.inner, self.padding.region()) .widget_within(&self.inner, self.padding.region())
.size(); .size();
painter.set_size(Size { Size {
x: Len { x: Len {
abs: inner.x.abs + self.padding.left + self.padding.right, abs: inner.x.abs + self.padding.left + self.padding.right,
..inner.x ..inner.x
@@ -19,7 +19,7 @@ impl Widget for Pad {
abs: inner.y.abs + self.padding.top + self.padding.bottom, abs: inner.y.abs + self.padding.top + self.padding.bottom,
..inner.y ..inner.y
}, },
}); }
} }
} }
+3 -3
View File
@@ -10,7 +10,7 @@ pub struct Scroll {
} }
impl Widget for Scroll { impl Widget for Scroll {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let output_len = painter.output_size().axis(self.axis); let output_len = painter.output_size().axis(self.axis);
let container_len = painter.region().axis(self.axis).len(); let container_len = painter.region().axis(self.axis).len();
// Drawn in the whole container to learn its length, then placed at // Drawn in the whole container to learn its length, then placed at
@@ -31,8 +31,8 @@ impl Widget for Scroll {
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);
painter.place(&self.inner, region); painter.widget_within(&self.inner, region);
painter.set_size(child); child
} }
} }
+3 -5
View File
@@ -6,15 +6,13 @@ pub struct SetSize {
pub y: Option<Len>, pub y: Option<Len>,
} }
impl SetSize {}
impl Widget for SetSize { impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let child = painter.widget(&self.inner).size(); let child = painter.widget(&self.inner).size();
painter.set_size(Size { Size {
x: self.x.unwrap_or(child.x), x: self.x.unwrap_or(child.x),
y: self.y.unwrap_or(child.y), y: self.y.unwrap_or(child.y),
}); }
} }
/// A declared axis is known without looking at the child, which is what /// A declared axis is known without looking at the child, which is what
+3 -3
View File
@@ -8,7 +8,7 @@ pub struct Span {
} }
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let axis = self.dir.axis; let axis = self.dir.axis;
// A length for every child before any is placed: from its own hint // A length for every child before any is placed: from its own hint
// where it has one, and from drawing it where it does not. // where it has one, and from drawing it where it does not.
@@ -42,7 +42,7 @@ impl Widget for Span {
if self.dir.sign == Sign::Neg { if self.dir.sign == Sign::Neg {
region.flip(axis); region.flip(axis);
} }
let used = painter.place(child, region).axis(!axis); let used = painter.widget_within(child, region).size().axis(!axis);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels // TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if used.rel > 0.0 || used.rest > 0.0 { if used.rel > 0.0 || used.rest > 0.0 {
ortho = Len::REST; ortho = Len::REST;
@@ -56,7 +56,7 @@ impl Widget for Span {
true => total, true => total,
false => Len::default(), false => Len::default(),
}; };
painter.set_size(Size::from_axis(axis, along, ortho)); Size::from_axis(axis, along, ortho)
} }
} }
+2 -2
View File
@@ -8,7 +8,7 @@ pub struct Stack {
} }
impl Widget for Stack { impl Widget for Stack {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let sizing = match self.size { let sizing = match self.size {
StackSize::Default => None, StackSize::Default => None,
StackSize::Child(i) => Some(i), StackSize::Child(i) => Some(i),
@@ -26,7 +26,7 @@ impl Widget for Stack {
size = drawn.size(); size = drawn.size();
} }
} }
painter.set_size(size); size
} }
} }
+4 -5
View File
@@ -6,12 +6,11 @@ pub struct WidgetPtr {
} }
impl Widget for WidgetPtr { impl Widget for WidgetPtr {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let size = match &self.inner { match &self.inner {
Some(id) => painter.widget(id).size(), Some(id) => painter.widget(id).size(),
None => Size::ZERO, None => Size::default(),
}; }
painter.set_size(size);
} }
} }
+4 -4
View File
@@ -28,14 +28,14 @@ impl Rect {
} }
impl Widget for Rect { impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.primitive(RectPrimitive { painter.primitive(RectPrimitive {
color: self.color, color: self.color,
radius: self.radius, radius: self.radius,
thickness: self.thickness, thickness: self.thickness,
inner_radius: self.inner_radius, inner_radius: self.inner_radius,
}); });
painter.set_size(Size::REST); Size::REST
} }
fn size_hint(&self, _: Axis) -> Option<Len> { fn size_hint(&self, _: Axis) -> Option<Len> {
@@ -43,8 +43,8 @@ impl Widget for Rect {
} }
/// Its box is its primitive's own region, so a new one is written there. /// Its box is its primitive's own region, so a new one is written there.
fn size_dependence(&self, _: Axis) -> SizeDependence { fn on_resize(&self, _: Axis) -> OnResize {
SizeDependence::None OnResize::Scale
} }
} }
+15 -9
View File
@@ -55,41 +55,47 @@ impl TextEdit {
} }
impl Widget for TextEdit { impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
let base = painter.layer; let base = painter.layer;
painter.child_layer(); painter.child_layer();
let (_, size) = self.view.draw(painter); let (_, size) = self.view.draw(painter);
painter.set_size(size);
painter.layer = base; painter.layer = base;
let region = self.region(); let region = self.region();
let Some(selection) = self.selection else { let Some(selection) = self.selection else {
return; return size;
}; };
let layout = self.view.buf.layout(); let layout = self.view.buf.layout();
// parley reports selection as boxes in layout space, so bidi and // parley reports selection as boxes in layout space, so bidi and
// wrapped lines come out right without this code knowing about either. // wrapped lines come out right without this code knowing about either.
for (rect, _) in selection.geometry(layout) { for (rect, _) in selection.geometry(layout) {
let size = vec2(rect.width() as f32, rect.height() as f32); let rect_size = vec2(rect.width() as f32, rect.height() as f32);
let top_left = vec2(rect.x0 as f32, rect.y0 as f32); let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
painter.primitive_within( painter.primitive_within(
RectPrimitive::color(Color::SKY), RectPrimitive::color(Color::SKY),
size.align(Align::TOP_LEFT).offset(top_left).within(&region), rect_size
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
); );
} }
let caret = selection.focus().geometry(layout, CARET_WIDTH); let caret = selection.focus().geometry(layout, CARET_WIDTH);
let size = vec2(caret.width() as f32, caret.height() as f32); let caret_size = vec2(caret.width() as f32, caret.height() as f32);
let top_left = vec2(caret.x0 as f32, caret.y0 as f32); let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
painter.primitive_within( painter.primitive_within(
RectPrimitive::color(Color::WHITE), RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(top_left).within(&region), caret_size
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
); );
size
} }
fn size_dependence(&self, axis: Axis) -> SizeDependence { fn on_resize(&self, axis: Axis) -> OnResize {
self.view.size_dependence(axis) self.view.on_resize(axis)
} }
} }
+17 -11
View File
@@ -89,12 +89,19 @@ impl TextView {
(region, size) (region, size)
} }
/// Wrapping reads the width it is offered, so a wider box reshapes it; a /// Wrapping reads the width it is offered, so a wider box reshapes it and
/// taller one never does. /// a taller one does not. Alignment matters too, and separately: glyphs
pub fn size_dependence(&self, axis: Axis) -> SizeDependence { /// anchored to the start of an axis stay put when that extent changes,
match axis == Axis::X && self.attrs.wrap { /// but centred or end-aligned ones move even though the shaping stands.
true => SizeDependence::External, pub fn on_resize(&self, axis: Axis) -> OnResize {
false => SizeDependence::Internal, 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,
} }
} }
@@ -120,14 +127,13 @@ impl Text {
} }
impl Widget for Text { impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) -> Size {
self.update_buf(); self.update_buf();
let (_, size) = self.view.draw(painter); self.view.draw(painter).1
painter.set_size(size);
} }
fn size_dependence(&self, axis: Axis) -> SizeDependence { fn on_resize(&self, axis: Axis) -> OnResize {
self.view.size_dependence(axis) self.view.on_resize(axis)
} }
} }
+77
View File
@@ -32,3 +32,80 @@ fn resizing_relays_out_against_the_new_output() {
assert_corners!(h, left, (0, 0), (100, 100)); assert_corners!(h, left, (0, 0), (100, 100));
assert_corners!(h, right, (100, 0), (800, 100)); assert_corners!(h, right, (100, 0), (800, 100));
} }
#[test]
fn an_empty_widget_takes_a_share_of_a_span() {
let mut h = Harness::new((400, 200));
let gap = ().add(&mut h.rsc);
let right = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((gap, right).span(Dir::RIGHT));
assert_corners!(h, gap, (0, 0), (300, 200));
assert_corners!(h, right, (300, 0), (400, 200));
}
#[test]
fn a_child_drawn_twice_moves_once() {
let mut h = Harness::new((400, 200));
// `Aligned` draws its child twice; listing it twice would move it twice.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let centered = inner.center().width(200).add(&mut h.rsc);
let left = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((left, centered).span(Dir::RIGHT));
assert_corners!(h, inner, (100, 0), (300, 200));
h.rsc[left].x = Some(Len::abs(150));
h.frame();
assert_corners!(h, inner, (150, 0), (350, 200));
}
#[test]
fn a_resize_lands_where_a_cold_start_would() {
let build = |h: &mut Harness| {
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves room \
for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.pad(16)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
let root = (para, below).span(Dir::DOWN).pad(12);
h.set_root(root);
(para, below)
};
let mut cold = Harness::new((900, 1200));
let (cold_para, cold_below) = build(&mut cold);
let mut resized = Harness::new((1920, 1200));
let (para, below) = build(&mut resized);
resized.resize((900, 1200));
resized.frame();
assert_eq!(resized.region(&para), cold.region(&cold_para), "paragraph");
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
}
#[test]
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
let mut h = Harness::new((400, 400));
// The panel fills a stack sized by its sibling, so it is drawn in the
// whole box and then placed in the shorter one. Reusing it in that fixed
// box afterwards would leave it whatever height it happened to have.
let panel = rect(Color::BLUE).add(&mut h.rsc);
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
let stack = (panel, leaf)
.stack()
.size(StackSize::Child(1))
.add(&mut h.rsc);
h.set_root(stack.align(Align::TOP));
assert_corners!(h, panel, (0, 0), (400, 100));
h.rsc[leaf].y = Some(Len::abs(250));
h.frame();
assert_corners!(h, panel, (0, 0), (400, 250));
}
+27
View File
@@ -0,0 +1,27 @@
//! What a drawing can be taken out of, and what it cannot.
use iris::core::{Remap, UiRegion, UiScalar, UiSpan};
/// A box `size` tall whose top is `rel` of the way down the window.
fn fixed(rel: f32, size: f32) -> UiRegion {
UiRegion::new(
UiSpan::FULL,
UiSpan::new(UiScalar { rel, abs: 0.0 }, UiScalar { rel, abs: size }),
)
}
#[test]
fn a_fixed_box_can_be_carried_but_not_stretched() {
let from = fixed(0.0, 164.0);
assert!(Remap::new(from, UiRegion::FULL).is_none());
assert!(Remap::new(from, fixed(0.5, 164.0)).is_some());
assert!(Remap::new(from, fixed(0.0, 98.0)).is_none());
}
#[test]
fn a_relative_box_can_be_stretched_to_any_other() {
let remap = Remap::new(UiRegion::FULL, fixed(0.0, 98.0)).expect("relative boxes remap");
// A part that filled the window keeps filling what replaced it, which is
// exactly what `outside` could not say for a box of a fixed length.
assert_eq!(remap.apply(UiRegion::FULL), fixed(0.0, 98.0));
}
+131 -16
View File
@@ -10,16 +10,16 @@ use iris::prelude::*;
struct Counted { struct Counted {
draws: Rc<Cell<usize>>, draws: Rc<Cell<usize>>,
size: Size, size: Size,
dependence: SizeDependence, dependence: OnResize,
} }
impl Widget for Counted { impl Widget for Counted {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, _: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
painter.set_size(self.size); self.size
} }
fn size_dependence(&self, _: Axis) -> SizeDependence { fn on_resize(&self, _: Axis) -> OnResize {
self.dependence self.dependence
} }
} }
@@ -32,11 +32,7 @@ impl Counts {
} }
} }
fn counted( fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Counted>, Counts) {
h: &mut Harness,
size: Size,
dependence: SizeDependence,
) -> (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(),
@@ -49,8 +45,8 @@ fn counted(
/// A fixed-width leaf beside one that takes the rest, so changing the first /// A fixed-width leaf beside one that takes the rest, so changing the first
/// hands the second a different box without the output changing. /// hands the second a different box without the output changing.
fn pair(h: &mut Harness, rest: SizeDependence) -> (WeakWidget<Counted>, Counts, WidgetId) { fn pair(h: &mut Harness, rest: OnResize) -> (WeakWidget<Counted>, Counts, WidgetId) {
let (first, _) = counted(h, Size::from((100, 200)), SizeDependence::Internal); let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate);
let (second, draws) = counted(h, Size::REST, rest); let (second, draws) = counted(h, Size::REST, rest);
h.set_root((first, second).span(Dir::RIGHT)); h.set_root((first, second).span(Dir::RIGHT));
(first, draws, second.id()) (first, draws, second.id())
@@ -59,7 +55,7 @@ fn pair(h: &mut Harness, rest: SizeDependence) -> (WeakWidget<Counted>, Counts,
#[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, SizeDependence::None); let (first, draws, second) = pair(&mut h, OnResize::Scale);
let settled = draws.get(); let settled = draws.get();
assert_corners!(h, second, (100, 0), (400, 200)); assert_corners!(h, second, (100, 0), (400, 200));
@@ -77,7 +73,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, SizeDependence::External); let (first, draws, second) = pair(&mut h, OnResize::Redraw);
let settled = draws.get(); let settled = draws.get();
h.rsc[first].size = Size::from((150, 200)); h.rsc[first].size = Size::from((150, 200));
@@ -93,8 +89,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)), SizeDependence::Internal); let (told, told_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), SizeDependence::Internal); let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
// 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);
@@ -111,7 +107,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, SizeDependence::Internal); let (first, _, second) = pair(&mut h, OnResize::Translate);
h.rsc[first].size = Size::from((250, 200)); h.rsc[first].size = Size::from((250, 200));
h.frame(); h.frame();
@@ -135,3 +131,122 @@ fn a_placed_child_survives_the_next_frame() {
assert_corners!(h, top, (0, 0), (400, 80)); assert_corners!(h, top, (0, 0), (400, 80));
assert_corners!(h, bottom, (0, 80), (400, 200)); assert_corners!(h, bottom, (0, 80), (400, 200));
} }
/// Lays its child out from the hint alone, never reading what it drew.
struct FromHint {
inner: StrongWidget,
}
impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL;
region.y.end = region.y.start.offset(len.abs);
painter.widget_within(&self.inner, region);
Size::REST
}
}
#[test]
fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::RED).height(80).add(&mut h.rsc);
let parent = FromHint {
inner: inner.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
h.set_root(parent);
assert_corners!(h, inner, (0, 0), (400, 80));
h.rsc[inner].y = Some(Len::abs(120));
h.frame();
assert_corners!(h, inner, (0, 0), (400, 120));
}
/// Reads the output's size, which nothing but its own draw can put right.
struct ReadsOutput {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsOutput {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::abs(painter.output_size() / 4.0)
}
}
#[test]
fn a_resize_does_not_redraw_what_the_shader_can_move() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_eq!(
draws.get(),
settled,
"its box is the same fraction of a different output"
);
assert_corners!(h, leaf, (0, 0), (800, 100));
}
#[test]
fn a_resize_redraws_what_read_the_output() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsOutput {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
let mut h = Harness::new((600, 400));
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves \
room for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((para, below).span(Dir::DOWN));
let top = h.region(&below).expect("drew nothing").top_left.y;
h.resize((300, 400));
h.frame();
let lower = h.region(&below).expect("drew nothing").top_left.y;
assert!(lower > top, "same words, half the width: {top} -> {lower}");
}
#[test]
fn a_change_two_levels_under_its_reader_still_reaches_it() {
let mut h = Harness::new((400, 400));
// 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
// size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::abs((100, 100).into()), OnResize::Redraw);
let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12));
assert_corners!(h, below, (12, 132), (388, 388));
h.rsc[leaf].size = Size::abs((100, 200).into());
h.frame();
assert_corners!(h, below, (12, 232), (388, 388));
}