Make Iris layout dependencies explicit

This commit is contained in:
iris committed 2026-09-09 22:35:03 -04:00
1 parent e5fee03da8
commit 4bc23172fd
28 files changed
+679 -227

No files matched your search

+1 -1
View File
@@ -445,7 +445,7 @@ where
// The wheel handler here is identical to the helper's; only the drag
// differs, and it arrives through `Selection::drag`, which hands
// committed pans and releases to this same span.
list.on(CursorSense::Scroll, |ctx, rsc| {
list.on(CursorSense::Scroll(Axis::Y), |ctx, rsc| {
let delta = ctx.data.scroll_delta.y * 50.0;
ctx.widget(rsc).scroll(delta);
})
+51 -39
View File
@@ -1,4 +1,4 @@
# iris: one `draw` that reports a size
# iris: one `draw` that records a size
Iris, 2026-09-04:
@@ -8,9 +8,13 @@ Iris, 2026-09-04:
> done after as well. This should be done efficiently like everything else
> tries to do right now.
**Implemented 2026-09-04.** Every widget was migrated in one change; none
kept `desired_width`/`desired_height`. What is kept below is the design as
it stands, the five corrections implementation forced (read those before
**Implemented 2026-09-04; size dependencies made explicit 2026-09-09.**
Every widget was migrated in one change; none kept
`desired_width`/`desired_height`. `draw` no longer returns its size directly:
it records it once on its `Painter`, and a parent that reads a child draw's
`DrawResult::size()` records the retained dependency between them. What is
kept below is the design as it stands, the corrections implementation forced
(read those before
touching `Aligned`, `Sized`, `MaxSize`, `Scroll` or the move-slot lifecycle
in `render_state.rs` -- each is a real bug the first draft would have
reproduced), and the two later additions that build on it. The
@@ -25,7 +29,7 @@ having been carried out.
```rust
pub trait Widget: Any {
fn draw(&mut self, painter: &mut Painter) -> Size;
fn draw(&mut self, painter: &mut Painter);
fn size_hint(&self, axis: Axis) -> Option<Len> { None }
@@ -35,6 +39,12 @@ pub trait Widget: Any {
}
```
Every implementation calls `painter.set_size(size)` exactly once. A child
draw returns a `DrawResult` that keeps the painter borrowed; calling `.size()`
on that result reads the child's retained size and records that the current
widget depends on it. Dropping the result without reading it draws the child
without making the parent's own size depend on the child's.
No `available` parameter: `Painter` already carries the region the parent
handed down (`Painter::region()`, `core/src/ui/painter.rs:137`) and already
exposes the pixel-resolved form (`px_size()`, `:156`) and the output surface
@@ -281,22 +291,25 @@ always a leaf: `Rect`, `Image`, a fixed glyph). A widget that returns
`false` (the default) is redrawn in full whenever `available` changes,
which is correct always, just not free.
**Size propagation goes both ways in the same frame.** A resized child first
walks upward through exactly the ancestors whose cached size changes. That
measurement pass gives each parent the new size but necessarily drew the
branch in its old boxes. As the recursion returns, `redraw_and_settle` revisits
those changed widgets from the outside in, after their parents have assigned
the final boxes. Otherwise a newly appended child can retain the provisional
(even inverted) region it was measured in until another update happens. The
downward work is confined to the branch that changed; unchanged descendants
still take `draw_inner`'s retained fast path. Each downward visit is exactly
one redraw, not another upward propagation: a wrapping child can have no fixed
point when an ancestor shrink-wraps it (a trailing space alternated between one
line in the offered width and two lines in its reported natural width). Feeding
that answer back into the same branch recursively overflowed Android's native
UI-thread stack before Rust could report a panic. A container that intends a
wrapping child to occupy its width declares that constraint explicitly; the
message composer does so on both sides of its vertical `ScrollArea`.
**Size invalidation travels upward before drawing; drawing itself travels only
downward.** Every active widget retains the direct children whose size it read
through `DrawResult::size()` or `Painter::known_len`. Before a frame draws,
`redraw_updates` follows only those dependency edges from each dirty child and
marks the affected ancestors dirty. It then selects the highest dirty roots and
draws them top-down. Drawing never synchronously invalidates or invokes a
parent, so there is no layout recursion and no provisional child draw on a
different layer.
An exact `size_hint` stops propagation when both axes still equal the retained
size. Otherwise propagation is deliberately conservative: the child may have
changed size, and only its dependent ancestors can assign the final boxes.
Unchanged descendants still take `draw_inner`'s retained skip-or-move path.
For a stacking container, retained child lengths are cached per axis: a child's
width remains reusable while the parent changes width, and its height remains
reusable while the parent changes height. A change on the orthogonal axis does
invalidate it in both directions. This is a generic constraint rule, not a
text exception; wrapped text is merely the common example of height depending
on width.
### 4. Wrapped text, and "needs child height before choosing width"
@@ -343,17 +356,15 @@ buffer. That check is kept exactly as it is; it is the caching mechanism,
and it already operates at (id, region) granularity, which subsumes "(id,
available size)" once size *is* what a region change means.
What is added: `ActiveData` gains `pub size: Size` — the value `draw`
returned, stored the moment it is (`draw_inner`, alongside building the
`ActiveData` struct at `:134-143`). This is what a parent placing this
widget for a second frame without redrawing it (because nothing changed)
reads instead of recomputing — it replaces `Cache.size`'s role of "answer a
size question without a full draw" with "read the size of the last actual
draw," which is always available because `draw_inner`'s skip path is only
reachable once the widget has been drawn at least once. `Cache::remove`/
`Cache::clear` (`cache.rs:9-17`) are deleted with the type; `ActiveData`
already has an equivalent lifecycle (removed in `remove`/`remove_rec`,
`render_state.rs:171-198`, freed with the widget).
`ActiveData::size` stores the value the widget recorded with
`Painter::set_size`. This is what a parent placing the widget for a second
frame without redrawing it reads instead of recomputing — it replaces
`Cache.size`'s role of "answer a size question without a full draw" with
"read the size of the last actual draw." `ActiveData::size_dependencies`
stores the direct children whose `DrawResult::size()` or known length the
widget observed during that same draw; the next draw replaces the list, so a
dependency disappears as soon as the widget stops reading it. Both fields
have `ActiveData`'s existing lifecycle through `remove`/`remove_rec`.
### 6. Before / after
@@ -374,10 +385,10 @@ impl Widget for Rect {
```rust
// after
impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
painter.primitive(RectPrimitive { color: self.color, radius: self.radius,
thickness: self.thickness, inner_radius: self.inner_radius });
Size::REST // fills whatever it was given -- used == available
painter.set_size(Size::REST); // fills whatever it was given
}
fn is_size_independent(&self) -> bool { true } // content never depends on region size
}
@@ -408,12 +419,12 @@ impl Widget for Aligned {
```rust
// after
impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let full = painter.region();
// Draw once at the full region to learn the child's real size --
// this placement is provisional and corrected below without a
// second draw.
let used = painter.widget_within(&self.inner, full);
let used = painter.widget_within(&self.inner, full).size();
let region = match self.align.tuple() {
(Some(x), Some(y)) => used.to_uivec2().align(RegionAlign { x, y }).within(&full),
(Some(x), None) => used.x.apply_rest().align(x).within(&full),
@@ -421,14 +432,15 @@ impl Widget for Aligned {
(None, None) => full,
};
painter.place(&self.inner, region);
used
painter.set_size(used);
}
}
```
`Painter::widget_within`/`widget`/`widget_at` (`painter.rs:55-76`) change
return type from `()` to `Size`, carrying the child's `draw` result back —
the only signature change needed to let a parent see what its child used.
return type from `()` to `DrawResult`. Calling `.size()` reads the size the
child recorded on its painter and records the parent's dependency on that
answer; leaving it unread records no dependency.
`Painter::place` moves an already-drawn child when its used area fits the
target box, and redraws it when the target changes its size. `SizeCtx` and
`Painter::size_ctx`/`size`/`len_axis` (`painter.rs:141-150,
+18 -1
View File
@@ -1,6 +1,6 @@
# Scrolling in iris
How anything in iris scrolls, as of 2026-09-08. This is the current
How anything in iris scrolls, as of 2026-09-09. This is the current
design, not a history — the git log has the account of
how it got here, and `docs/IRIS_TODO.md` has what is still open.
@@ -247,6 +247,23 @@ drawn (Iris, 2026-09-08). A correction that lands next frame is a frame
drawn wrong, and there may be no next frame — a fling that stopped is not
asking for one.
## Directional input
A scrollable registers `CursorSense::drag(axis)` and
`CursorSense::Scroll(axis)`. Horizontal and vertical gestures are distinct
input semantics, so a higher horizontal row does not consume an undecided
press that may belong to the lower vertical transcript. Both may observe the
press start; after movement crosses `DRAG_SLOP`, the pointer locks to its
dominant axis, only the matching listener receives the drag, and capture
cancels every other listener that had been tracking the press.
Visual layers still decide priority between listeners for the same semantic.
`drag_senses()` remains the deliberately direction-agnostic form for widgets
such as selection that arbitrate the gesture themselves. Wheel input follows
the same axis split; the desktop backend's Shift+wheel mapping produces a
horizontal delta before dispatch, so it reaches the horizontal listener
without a scroll-widget special case.
## The transcript's wiring
`app-rust/src/ui/mod.rs`, `build_tree`.
+5 -1
View File
@@ -11,12 +11,16 @@ pub struct ActiveData {
pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>,
pub children: Vec<WidgetId>,
/// Direct children whose reported size this widget used during its
/// latest draw. Dirtiness propagates across these edges before layout
/// starts, so the resulting draw still travels only parent to child.
pub size_dependencies: Vec<WidgetId>,
/// The inherited mask, not `own_mask`.
pub mask: MaskIdx,
/// The widget's retained mask slot, or `MaskIdx::NONE`.
pub own_mask: MaskIdx,
pub layer: LayerId,
/// The last `Widget::draw` result.
/// The size recorded by the last `Widget::draw` through its painter.
pub size: Size,
/// Retained so descendants' parent links stay valid across redraws.
pub move_slot: MoveIdx,
+1 -1
View File
@@ -9,7 +9,7 @@ mod render_state;
pub use access::*;
pub use active::*;
pub use painter::Painter;
pub use painter::{DrawResult, Painter};
pub use render_state::*;
#[derive(Default)]
+92 -20
View File
@@ -25,12 +25,46 @@ pub struct Painter<'a> {
/// Previous handles, consumed in draw order and freed if left over.
pub(super) recycle: std::iter::Peekable<std::vec::IntoIter<PrimitiveHandle>>,
pub(super) children: Vec<WidgetId>,
pub(super) reuse_child_sizes: bool,
pub(super) size_dependencies: Vec<WidgetId>,
pub(super) size: Option<Size>,
/// Whether a retained child's length on each axis is still valid. A
/// child's length may change when the parent's orthogonal extent changes
/// (most importantly, wrapped text gets taller when it gets narrower),
/// but not merely because a content-sized parent grew along that same
/// axis around one of its siblings.
pub(super) reuse_child_sizes: [bool; 2],
pub layer: usize,
pub(super) id: WidgetId,
}
/// A child draw whose size has not necessarily been observed by its parent.
/// Holding this value keeps the painter borrowed, so `.size()` can only name
/// the child from the immediately preceding draw.
pub struct DrawResult<'p, 'a> {
painter: &'p mut Painter<'a>,
child: WidgetId,
}
impl DrawResult<'_, '_> {
/// Return the child's reported size and record the layout dependency.
pub fn size(self) -> Size {
if !self.painter.size_dependencies.contains(&self.child) {
self.painter.size_dependencies.push(self.child);
}
self.painter.state.active[&self.child].size
}
}
impl<'a> Painter<'a> {
/// Record the size this widget used. Every `Widget::draw` calls this
/// exactly once; parents observe it through [`DrawResult::size`].
pub fn set_size(&mut self, size: Size) {
assert!(
self.size.replace(size).is_none(),
"a widget set its size more than once during one draw"
);
}
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
self.write_primitive(primitive, region, Drawn::Yes);
}
@@ -207,15 +241,19 @@ impl<'a> Painter<'a> {
self.mask = self.own_mask;
}
/// Draws a widget within this widget's region, returning the size it
/// reported using.
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) -> Size {
/// Draw a widget within this widget's region. Reading the result's size
/// records that this widget's layout depends on the child.
pub fn widget<'p, W: ?Sized>(&'p mut self, id: &StrongWidget<W>) -> DrawResult<'p, 'a> {
self.widget_at(id, self.region)
}
/// Draws a widget somewhere within this one.
/// Useful for drawing child widgets in select areas.
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
pub fn widget_within<'p, W: ?Sized>(
&'p mut self,
id: &StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'p, 'a> {
self.widget_at(id, region.within(&self.region))
}
@@ -260,17 +298,29 @@ impl<'a> Painter<'a> {
}
}
pub fn known_len<W: ?Sized>(&self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
if let Some(len) = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) {
return Some(len.fold_dp(self.density()));
pub fn known_len<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
let len = if let Some(len) = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) {
Some(len.fold_dp(self.density()))
} else if !self.reuse_child_sizes[match axis {
Axis::X => 0,
Axis::Y => 1,
}] || self.rsc.widgets().needs_redraw.contains(&id.id())
{
None
} else {
self.state.active.get(&id.id()).map(|a| a.size.axis(axis))
};
if len.is_some() && !self.size_dependencies.contains(&id.id()) {
self.size_dependencies.push(id.id());
}
if !self.reuse_child_sizes || self.rsc.widgets().needs_redraw.contains(&id.id()) {
return None;
}
self.state.active.get(&id.id()).map(|a| a.size.axis(axis))
len
}
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
fn widget_at<'p, W: ?Sized>(
&'p mut self,
id: &StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'p, 'a> {
self.children.push(id.id());
// Passed directly rather than looked up from `self.active`: this
// widget's own `ActiveData` (which would carry its `move_slot`) is
@@ -289,19 +339,26 @@ impl<'a> Painter<'a> {
self.mask,
Retained::default(),
self.rsc,
)
);
DrawResult {
painter: self,
child: id.id(),
}
}
/// Place an already-drawn child's used area, redrawing only if its size changes.
pub fn place<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
pub fn place<'p, W: ?Sized>(
&'p mut self,
id: &StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'p, 'a> {
let region = region.within(&self.region);
let retained = self
.state
.active
.get(&id.id())
.map(|active| (active.layer, active.mask));
if let Some(size) = self.state.place(id.id(), region, self.rsc) {
size
if self.state.place(id.id(), region, self.rsc).is_some() {
} else if let Some((layer, mask)) = retained {
self.children.push(id.id());
self.rsc.widgets_mut().needs_redraw.insert(id.id());
@@ -315,9 +372,24 @@ impl<'a> Painter<'a> {
mask,
Retained::default(),
self.rsc,
)
);
} else {
self.widget_at(id, region)
self.children.push(id.id());
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
self.state.draw_inner(
self.layer,
id.id(),
region,
Some(self.id),
parent_move_slot.idx() as u32,
self.mask,
Retained::default(),
self.rsc,
);
}
DrawResult {
painter: self,
child: id.id(),
}
}
@@ -326,7 +398,7 @@ impl<'a> Painter<'a> {
id: &StrongWidget<W>,
used: Size,
within: UiRegion,
) -> Size {
) -> DrawResult<'_, 'a> {
let region = self.fit_region(used, within);
self.place(id, region)
}
+101 -64
View File
@@ -535,8 +535,16 @@ impl UiRenderState {
let move_slot = Self::move_slot_for(old_move_slot, parent_move_slot, rsc);
let inherited_mask = mask;
let reuse_child_sizes =
old_region.is_some_and(|old| Self::same_size(old, region, self.output_size));
// `Painter::layer` is a cursor widgets advance while assigning
// layers to their children. Retain the layer this widget itself was
// entered on, not wherever that cursor finishes after `draw`.
let inherited_layer = layer;
let reuse_child_sizes = old_region.map_or([false; 2], |old| {
[
Self::same_axis_size(old, region, Axis::Y, self.output_size),
Self::same_axis_size(old, region, Axis::X, self.output_size),
]
});
let mut painter = Painter {
state: self,
region,
@@ -550,6 +558,8 @@ impl UiRenderState {
primitives: Vec::new(),
recycle: recycle.into_iter().peekable(),
children: Vec::new(),
size_dependencies: Vec::new(),
size: None,
reuse_child_sizes,
rsc,
};
@@ -561,7 +571,13 @@ impl UiRenderState {
widget.size_hint(Axis::Y).map(|len| len.fold_dp(density)),
];
painter.state.draw_count += 1;
let size = widget.draw(&mut painter);
widget.draw(&mut painter);
let size = painter.size.unwrap_or_else(|| {
panic!(
"widget '{}' ({id:?}) did not set its size during draw",
painter.rsc.widgets().label(id)
)
});
debug_assert!(
size.x.dp == 0.0 && size.y.dp == 0.0,
"widget {id:?} reported an unresolved `dp` size ({size:?}); \
@@ -593,8 +609,10 @@ impl UiRenderState {
primitives,
recycle,
children,
size_dependencies,
size: _,
reuse_child_sizes: _,
layer,
layer: _,
id,
} = painter;
@@ -615,8 +633,9 @@ impl UiRenderState {
textures,
primitives,
children,
size_dependencies,
mask: inherited_mask,
layer,
layer: inherited_layer,
size,
move_slot,
child_move_slot,
@@ -690,9 +709,13 @@ impl UiRenderState {
}
fn same_size(a: UiRegion, b: UiRegion, output: Vec2) -> bool {
let a = a.size().to_abs(output);
let b = b.size().to_abs(output);
(a.x - b.x).abs() < 0.01 && (a.y - b.y).abs() < 0.01
Self::same_axis_size(a, b, Axis::X, output) && Self::same_axis_size(a, b, Axis::Y, output)
}
fn same_axis_size(mut a: UiRegion, mut b: UiRegion, axis: Axis, output: Vec2) -> bool {
let a = a.axis(axis).len().to_abs(output.axis(axis));
let b = b.axis(axis).len().to_abs(output.axis(axis));
(a - b).abs() < 0.01
}
pub(super) fn place(
@@ -920,12 +943,76 @@ impl UiRenderState {
}
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
while let Some(&id) = rsc.widgets().needs_redraw.iter().next() {
self.redraw(id, rsc);
while rsc.widgets().has_updates() {
// Expand size dependencies before drawing anything. The parent
// links are the retained widget tree already used by hit testing
// and removal; only the direct-child dependency list is new.
let pending: Vec<_> = rsc.widgets().needs_redraw.iter().copied().collect();
for mut child in pending {
for _ in 0..PARENT_CHAIN_LIMIT {
// An exact hint is the child's current answer without a
// draw. If both axes still match the retained size, no
// parent can observe a size change from this mutation.
if self.size_matches_hints(child, rsc) {
break;
}
let Some(parent) = self.active.get(&child).and_then(|active| active.parent)
else {
break;
};
let depends = self
.active
.get(&parent)
.is_some_and(|active| active.size_dependencies.contains(&child));
if !depends {
break;
}
rsc.widgets_mut().needs_redraw.insert(parent);
child = parent;
}
}
// A dirty ancestor draws its dirty descendants on the way down;
// starting those descendants separately would duplicate work.
let dirty: Vec<_> = rsc.widgets().needs_redraw.iter().copied().collect();
let mut roots = Vec::new();
for id in dirty {
let mut ancestor = self.active.get(&id).and_then(|active| active.parent);
let mut covered = false;
for _ in 0..PARENT_CHAIN_LIMIT {
let Some(parent) = ancestor else { break };
if rsc.widgets().needs_redraw.contains(&parent) {
covered = true;
break;
}
ancestor = self.active.get(&parent).and_then(|active| active.parent);
}
if !covered {
roots.push(id);
}
}
for id in roots {
self.redraw(id, rsc);
}
}
rsc.free();
}
fn size_matches_hints(&self, id: WidgetId, rsc: &dyn UiRsc) -> bool {
let Some(active) = self.active.get(&id) else {
return false;
};
let Some(widget) = rsc.widgets().get_dyn(id) else {
return false;
};
[Axis::X, Axis::Y].into_iter().all(|axis| {
widget
.size_hint(axis)
.map(|hint| hint.fold_dp(self.density))
== Some(active.size.axis(axis))
})
}
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
root.into().map(|r| r.id()) != self.old_root
}
@@ -1207,61 +1294,18 @@ impl UiRenderState {
/// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
self.redraw_and_settle(id, rsc);
}
/// Measure a changed branch toward the root, then revisit each widget
/// whose reported size changed on the way back down. The upward pass gives
/// every parent the new child size; the downward pass is what lets those
/// children draw inside the final boxes their parents chose. Without it a
/// newly grown subtree can retain the provisional (even inverted) region
/// it was measured in until an unrelated later update redraws it.
fn redraw_and_settle(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
let Some((parent, changed)) = self.redraw_once(id, rsc) else {
return;
};
if changed {
if let Some(pid) = parent {
self.redraw_and_settle(pid, rsc);
}
// The parent pass above has now placed this widget in its final
// region. Draw it once more there; unchanged descendants still
// take draw_inner's retained fast path. This is deliberately one
// redraw rather than another settling pass: feeding its size
// back into the same upward walk can alternate between the
// provisional and final regions forever (a text edit first did
// that when an Android IME committed a space), overflowing the
// native thread's stack before Rust can report a panic.
let settled_size = self.active.get(&id).map(|active| active.size);
let _ = self.redraw_once(id, rsc);
debug_assert_eq!(
self.active.get(&id).map(|active| active.size),
settled_size,
"a widget changed size after its parent settled its final region"
);
}
}
/// Redraw `id` exactly once, returning its parent and whether the size it
/// reports changed. [`Self::redraw_and_settle`] owns any propagation; in
/// particular, its final downward redraw must not start another upward
/// pass through the same branch.
fn redraw_once(
&mut self,
id: WidgetId,
rsc: &mut dyn UiRsc,
) -> Option<(Option<WidgetId>, bool)> {
rsc.widgets_mut().needs_redraw.remove(&id);
// An ancestor is drawing this widget right now, and that draw is
// about to write fresh primitives for it. Drawing it a second time
// here would leave one of the two copies on screen with nothing
// owning it -- see `draw_started`'s own doc.
if self.draw_started.contains(&id) {
return None;
return;
}
let active = self.remove(id, false, true, rsc)?;
let old_size = active.size;
let Some(active) = self.remove(id, false, true, rsc) else {
return;
};
let parent = active.parent;
// `old_move_slot` being `Some` below means the slot is reused in
// place rather than freshly parented, so this is only reached for
@@ -1285,13 +1329,6 @@ impl UiRenderState {
},
rsc,
);
// If this widget's own reported size changed, its parent's layout
// (which placed it using the old size) is now stale and needs to
// relay out too. Checked after the real draw, not before it --
// there is no query left that answers "what size would this be"
// without actually drawing (LAYOUT.md section 5).
let changed = self.active.get(&id).map(|a| a.size) != Some(old_size);
Some((parent, changed))
}
}
+3 -3
View File
@@ -16,7 +16,7 @@ pub use view::*;
pub use widgets::*;
pub trait Widget: Any {
fn draw(&mut self, painter: &mut Painter) -> Size;
fn draw(&mut self, painter: &mut Painter);
/// An exact, context-free length known without drawing or inspecting children.
fn size_hint(&self, _axis: Axis) -> Option<Len> {
@@ -45,8 +45,8 @@ pub trait Widget: Any {
}
impl Widget for () {
fn draw(&mut self, _: &mut Painter) -> Size {
Size::ZERO
fn draw(&mut self, painter: &mut Painter) {
painter.set_size(Size::ZERO);
}
fn is_size_independent(&self) -> bool {
+209 -6
View File
@@ -5,6 +5,7 @@
//! CPU-side layout/move machinery LAYOUT.md is about.
use crate::prelude::*;
use std::{cell::Cell, cell::RefCell, rc::Rc};
/// The minimal `UiRsc` a test needs: just the shared `UiData`, none of the
/// event/window/state plumbing `DefaultRsc` carries. `pub(crate)` so
@@ -26,14 +27,14 @@ impl UiRsc for TestRsc {
struct FixedRect(f32);
impl Widget for FixedRect {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let size = Size::from_axis(Axis::Y, Len::abs(self.0), Len::REST);
painter.primitive_within(
RectPrimitive::color(UiColor::WHITE),
size.to_uivec2(painter.density())
.align(RegionAlign::TOP_LEFT),
);
size
painter.set_size(size);
}
}
@@ -43,12 +44,214 @@ struct ChildOffset {
}
impl Widget for ChildOffset {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
painter.set_child_offset(self.offset);
painter.widget(&self.child)
let size = painter.widget(&self.child).size();
painter.set_size(size);
}
}
struct TracedLeaf {
height: f32,
trace: Rc<RefCell<Vec<&'static str>>>,
}
impl Widget for TracedLeaf {
fn draw(&mut self, painter: &mut Painter) {
self.trace.borrow_mut().push("child");
painter.set_size(Size::from_axis(Axis::Y, Len::abs(self.height), Len::REST));
}
}
struct TracedParent {
child: StrongWidget,
reads_child_size: bool,
trace: Rc<RefCell<Vec<&'static str>>>,
}
struct CountedLeaf {
height: f32,
draws: Rc<Cell<u32>>,
}
impl Widget for CountedLeaf {
fn draw(&mut self, painter: &mut Painter) {
self.draws.set(self.draws.get() + 1);
painter.set_size(Size::from_axis(Axis::Y, Len::abs(self.height), Len::REST));
}
}
impl Widget for TracedParent {
fn draw(&mut self, painter: &mut Painter) {
self.trace.borrow_mut().push("parent");
let child = painter.widget(&self.child);
let size = if self.reads_child_size {
child.size()
} else {
Size::REST
};
painter.set_size(size);
}
}
/// Minimal reproduction for a container's child-layer cursor being retained
/// as though it were the layer on which the container itself was entered.
///
/// `Stack` is drawn by `Sized` on layer 0. It advances its painter to layers
/// 1 and 2 for its two children. The retained `ActiveData` must still say the
/// stack itself is on layer 0; otherwise an ordinary redraw of `Sized` asks
/// for the stack on 0 again and turns the invented 2 -> 0 change into a full
/// redraw of the stack and both children.
#[test]
fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let back = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
let front = rsc.ui.widgets.add_strong(Rect::new(UiColor::RED));
let stack = rsc.ui.widgets.add_strong(Stack {
children: vec![back.any(), front.any()],
size: StackSize::Default,
});
let stack_id = stack.id();
let outer = rsc.ui.widgets.add_strong(Sized {
inner: stack.any(),
x: None,
y: None,
});
let outer_weak = outer.weak();
let root = outer.any();
let mut render = UiRenderState::new();
render.resize((100.0, 100.0));
render.update(&root, &mut rsc);
assert_eq!(
render.active[&stack_id].layer, 0,
"Stack entered on layer 0; layers 1 and 2 belong only to its children"
);
render.take_counters();
rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None;
render.update(&root, &mut rsc);
assert_eq!(
render.take_counters().0,
1,
"redrawing the parent should retain the unchanged Stack subtree"
);
}
#[test]
fn a_size_dependent_parent_is_invalidated_before_layout_runs_downward() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let trace = Rc::new(RefCell::new(Vec::new()));
let child = rsc.ui.widgets.add_strong(TracedLeaf {
height: 20.0,
trace: trace.clone(),
});
let child_weak = child.weak();
let parent = rsc.ui.widgets.add_strong(TracedParent {
child: child.any(),
reads_child_size: true,
trace: trace.clone(),
});
let parent_weak = parent.weak();
let root = parent.any();
let mut render = UiRenderState::new();
render.resize((100.0, 100.0));
render.update(&root, &mut rsc);
trace.borrow_mut().clear();
rsc.ui.widgets.get_mut(&child_weak).unwrap().height = 40.0;
render.update(&root, &mut rsc);
assert_eq!(&*trace.borrow(), &["parent", "child"]);
assert_eq!(render.active[&parent_weak.id()].size.y, Len::abs(40.0));
}
#[test]
fn a_parent_that_ignores_child_size_is_not_invalidated_with_it() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let trace = Rc::new(RefCell::new(Vec::new()));
let child = rsc.ui.widgets.add_strong(TracedLeaf {
height: 20.0,
trace: trace.clone(),
});
let child_weak = child.weak();
let parent = rsc.ui.widgets.add_strong(TracedParent {
child: child.any(),
reads_child_size: false,
trace: trace.clone(),
});
let root = parent.any();
let mut render = UiRenderState::new();
render.resize((100.0, 100.0));
render.update(&root, &mut rsc);
trace.borrow_mut().clear();
rsc.ui.widgets.get_mut(&child_weak).unwrap().height = 40.0;
render.update(&root, &mut rsc);
assert_eq!(&*trace.borrow(), &["child"]);
}
/// A content-sized vertical container grows vertically when one child grows,
/// but that does not invalidate another child's retained height. Its width is
/// the context that could change that height (for example through wrapping),
/// and that stayed fixed.
#[test]
fn a_span_reuses_unchanged_sibling_sizes_when_only_its_along_extent_changes() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let changed_draws = Rc::new(Cell::new(0));
let sibling_draws = Rc::new(Cell::new(0));
let changed = rsc.ui.widgets.add_strong(CountedLeaf {
height: 20.0,
draws: changed_draws.clone(),
});
let changed_weak = changed.weak();
let sibling = rsc.ui.widgets.add_strong(CountedLeaf {
height: 20.0,
draws: sibling_draws.clone(),
});
let span = rsc.ui.widgets.add_strong(Span {
children: vec![changed.any(), sibling.any()],
dir: Dir::DOWN,
gap: Len::ZERO,
});
// `Sized` settles its child from the full offered box into the content
// height, reproducing the retained-region change a nested content-sized
// row sees when one of its children grows.
let root = rsc
.ui
.widgets
.add_strong(Sized {
inner: span.any(),
x: None,
y: None,
})
.any();
let mut render = UiRenderState::new();
render.resize((100.0, 100.0));
render.update(&root, &mut rsc);
changed_draws.set(0);
sibling_draws.set(0);
rsc.ui.widgets.get_mut(&changed_weak).unwrap().height = 40.0;
render.update(&root, &mut rsc);
assert!(changed_draws.get() > 0, "the changed child was not redrawn");
assert_eq!(
sibling_draws.get(),
0,
"a vertical size change redrew a sibling whose width stayed fixed"
);
}
#[test]
fn a_child_coordinate_offset_moves_only_the_child_subtree() {
let mut rsc = TestRsc {
@@ -721,7 +924,7 @@ struct MoveThenPlace {
}
impl Widget for MoveThenPlace {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let offer = UiRegion::new(
UiSpan::FULL,
UiSpan::new(
@@ -738,7 +941,7 @@ impl Widget for MoveThenPlace {
),
);
painter.place(&self.inner, place);
Size::default()
painter.set_size(Size::default());
}
}
+116 -25
View File
@@ -21,7 +21,7 @@ pub enum CursorSense {
HoverStart,
Hovering,
HoverEnd,
Scroll,
Scroll(Axis),
/// Delivered exactly once, in place of `PressEnd`, to whichever widget
/// currently holds pointer capture (`UiRenderState::capture_pointer`)
/// when the button lifts -- see `iris::sense`'s pointer-capture doc
@@ -47,7 +47,10 @@ pub enum CursorSense {
}
#[derive(Clone)]
pub struct CursorSenses(Vec<CursorSense>);
pub struct CursorSenses {
senses: Vec<CursorSense>,
drag_axis: Option<Axis>,
}
impl Event for CursorSenses {
type Data<'a> = CursorData<'a>;
@@ -74,7 +77,13 @@ impl Event for CursorSenses {
if data.sense == CursorSense::Drop || data.sense == CursorSense::Cancel {
return self.contains(&data.sense).then(|| data.clone());
}
if let Some(sense) = should_run(self, &data.cursor, data.hover) {
if let Some(sense) = should_run(
self,
&data.cursor,
data.hover,
data.drag_axis,
data.captured,
) {
let mut data = data.clone();
data.sense = sense;
Some(data)
@@ -84,6 +93,30 @@ impl Event for CursorSenses {
}
}
impl CursorSenses {
fn consumes(&self, data: &CursorData<'_>, momentary_active: bool) -> bool {
if !momentary_active {
return true;
}
let Some(sense) = should_run(
self,
&data.cursor,
data.hover,
data.drag_axis,
data.captured,
) else {
return false;
};
match (self.drag_axis, sense) {
// Directional drags are candidates until movement chooses an
// axis. Only the matching one consumes its visual layer.
(Some(axis), CursorSense::Pressing(_)) => data.captured || data.drag_axis == Some(axis),
(Some(_), CursorSense::PressStart(_) | CursorSense::PressEnd(_)) => false,
_ => sense.is_momentary(),
}
}
}
impl CursorSense {
pub fn click() -> Self {
Self::PressStart(CursorButton::Left)
@@ -109,6 +142,16 @@ impl CursorSense {
pub fn drag_senses() -> CursorSenses {
Self::click_or_drag() | Self::unclick() | Self::Drop | Self::Cancel
}
/// The frames of a drag along one axis. Before the gesture crosses
/// [`DRAG_SLOP`], directional listeners observe without consuming a
/// visual layer; once its direction is known, only listeners for that
/// axis receive and consume it.
pub fn drag(axis: Axis) -> CursorSenses {
let mut senses = Self::drag_senses();
senses.drag_axis = Some(axis);
senses
}
pub fn is_dragging(&self) -> bool {
matches!(self, CursorSense::Pressing(CursorButton::Left))
}
@@ -261,6 +304,11 @@ pub struct CursorData<'a> {
pub scroll_delta: Vec2,
pub hover: ActivationState,
pub cursor: CursorState,
/// The direction selected after this press crossed [`DRAG_SLOP`].
/// `None` while the gesture is still only a press.
pub drag_axis: Option<Axis>,
/// Whether this sample bypassed hit testing for the pointer holder.
pub captured: bool,
/// the first sense that triggered this
pub sense: CursorSense,
pub render: &'a UiRenderState,
@@ -294,6 +342,8 @@ pub struct CursorData<'a> {
pub struct PointerInput {
captured: Option<WidgetId>,
pressed: Vec<WidgetId>,
press_origin: Option<Vec2>,
drag_axis: Option<Axis>,
}
impl PointerInput {
@@ -393,6 +443,22 @@ impl SensorUi for UiRenderState {
holder: std::cell::Cell::new(pointer.captured),
};
let button_down = cursor.buttons.select(&CursorButton::Left).is_on();
if cursor.buttons.left.is_start() {
pointer.press_origin = Some(cursor.pos);
pointer.drag_axis = None;
} else if button_down
&& pointer.drag_axis.is_none()
&& let Some(origin) = pointer.press_origin
{
let moved = cursor.pos - origin;
if moved.x.abs().max(moved.y.abs()) > DRAG_SLOP {
pointer.drag_axis = Some(if moved.x.abs() > moved.y.abs() {
Axis::X
} else {
Axis::Y
});
}
}
// The platform took the gesture away (`CursorState::cancelled`).
// Everybody still tracking this press hears about it -- the
@@ -410,6 +476,8 @@ impl SensorUi for UiRenderState {
}
}
requests.release();
pointer.press_origin = None;
pointer.drag_axis = None;
for id in told {
deliver_cancel(self, rsc, state, id, &cursor, window_size, &requests);
}
@@ -458,6 +526,8 @@ impl SensorUi for UiRenderState {
scroll_delta: cursor.scroll_delta,
hover: ActivationState::On,
cursor: cursor.clone(),
drag_axis: pointer.drag_axis,
captured: true,
sense,
render: self,
pointer: &requests,
@@ -466,6 +536,8 @@ impl SensorUi for UiRenderState {
if !button_down {
requests.release();
pointer.pressed.clear();
pointer.press_origin = None;
pointer.drag_axis = None;
}
pointer.captured = requests.holder();
rsc.events_mut().get_type::<CursorSense>().global = pointer;
@@ -524,20 +596,6 @@ impl SensorUi for UiRenderState {
// widget that registered a matching non-hover sense
// consumes it -- a button that only registered `click()`
// must not block a scroll meant for the list behind it.
let consumed = if momentary_active {
rsc.events_mut()
.get_type::<CursorSense>()
.registered(*id)
.any(|senses| {
matches!(should_run(senses, &cursor, sensor.hover), Some(s) if s.is_momentary())
})
} else {
true
};
if consumed {
sensed = true;
}
let cursor = cursor.clone();
let data = CursorData {
@@ -546,12 +604,22 @@ impl SensorUi for UiRenderState {
scroll_delta: cursor.scroll_delta,
hover: sensor.hover,
cursor,
drag_axis: pointer.drag_axis,
captured: false,
// this does not have any meaning;
// might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering,
render: self,
pointer: &requests,
};
let consumes = rsc
.events_mut()
.get_type::<CursorSense>()
.registered(*id)
.any(|senses| senses.consumes(&data, momentary_active));
if consumes {
sensed = true;
}
rsc.run_event::<CursorSense>(*id, data, state);
// Anything handed a frame while the button is down may
// have opened a gesture on it, and is owed a `Cancel` if
@@ -563,7 +631,7 @@ impl SensorUi for UiRenderState {
pointer.pressed.push(*id);
}
}
if sensed {
if sensed || requests.holder().is_some() {
break;
}
}
@@ -597,6 +665,10 @@ impl SensorUi for UiRenderState {
// A cancel handler may itself have captured (a widget deciding
// the gesture is now its own); `requests` is still the truth.
pointer.captured = requests.holder();
if !button_down {
pointer.press_origin = None;
pointer.drag_axis = None;
}
rsc.events_mut().get_type::<CursorSense>().global = pointer;
}
}
@@ -625,6 +697,8 @@ fn deliver_cancel<Rsc: HasEvents>(
scroll_delta: cursor.scroll_delta,
hover: ActivationState::On,
cursor: cursor.clone(),
drag_axis: None,
captured: false,
sense: CursorSense::Cancel,
render,
pointer,
@@ -636,6 +710,8 @@ pub fn should_run(
senses: &CursorSenses,
cursor: &CursorState,
hover: ActivationState,
drag_axis: Option<Axis>,
captured: bool,
) -> Option<CursorSense> {
// Every sense below that is about the *pointer* rather than about
// hovering needs the pointer to actually be on this widget, and
@@ -670,7 +746,12 @@ pub fn should_run(
CursorSense::HoverStart => hover.is_start(),
CursorSense::Hovering => hover.is_on(),
CursorSense::HoverEnd => hover.is_end(),
CursorSense::Scroll => on_this && cursor.scroll_delta != Vec2::ZERO,
CursorSense::Scroll(axis) => {
on_this
&& cursor.scroll_delta.axis(*axis) != 0.0
&& cursor.scroll_delta.axis(*axis).abs()
>= cursor.scroll_delta.axis(!*axis).abs()
}
// Never derived here -- `Drop` only ever fires through
// `CursorSenses::should_run`'s own special case, ahead of this
// loop, for the one widget `run_sensors`' capture branch is
@@ -683,7 +764,11 @@ pub fn should_run(
// note above; both are set by `run_sensors` alone, for the one
// widget it is delivering to this frame.
CursorSense::Drop | CursorSense::Cancel => false,
} {
} && (captured
|| !matches!(sense, CursorSense::Pressing(_))
|| senses.drag_axis.is_none()
|| senses.drag_axis == drag_axis)
{
return Some(*sense);
}
}
@@ -744,19 +829,22 @@ impl Deref for CursorSenses {
type Target = Vec<CursorSense>;
fn deref(&self) -> &Self::Target {
&self.0
&self.senses
}
}
impl DerefMut for CursorSenses {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
&mut self.senses
}
}
impl From<CursorSense> for CursorSenses {
fn from(val: CursorSense) -> Self {
CursorSenses(vec![val])
CursorSenses {
senses: vec![val],
drag_axis: None,
}
}
}
@@ -764,7 +852,10 @@ impl BitOr for CursorSense {
type Output = CursorSenses;
fn bitor(self, rhs: Self) -> Self::Output {
CursorSenses(vec![self, rhs])
CursorSenses {
senses: vec![self, rhs],
drag_axis: None,
}
}
}
@@ -772,7 +863,7 @@ impl BitOr<CursorSense> for CursorSenses {
type Output = Self;
fn bitor(mut self, rhs: CursorSense) -> Self::Output {
self.0.push(rhs);
self.senses.push(rhs);
self
}
}
+17 -10
View File
@@ -73,9 +73,13 @@ fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
let clicked = Rc::new(Cell::new(false));
{
let scrolled = scrolled.clone();
rsc.register_event(list_weak, CursorSense::Scroll, move |_ctx, _rsc| {
scrolled.set(true);
});
rsc.register_event(
list_weak,
CursorSense::Scroll(Axis::Y),
move |_ctx, _rsc| {
scrolled.set(true);
},
);
}
{
let clicked = clicked.clone();
@@ -461,10 +465,10 @@ fn taking_the_pointer_cancels_everyone_else_tracking_the_press() {
events: EventManager::default(),
};
// The bystander *contains* the capturer, which is the real shape: a
// transcript's `LazySpan` and one row's own text both track the same
// press, and a `Stack`'s siblings would be on separate layers where
// only the topmost is dispatched to at all.
// The bystander contains the capturer on a lower visual layer: the
// shape of a vertical transcript scroller with a higher horizontal
// scroller inside one row. Both observe the undecided press, then only
// the recognizer matching its direction may consume it.
let capturer = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
let capturer_weak = capturer.weak();
let bystander = rsc.ui.widgets.add_strong(Stack {
@@ -478,7 +482,7 @@ fn taking_the_pointer_cancels_everyone_else_tracking_the_press() {
let capturer_saw = capturer_saw.clone();
rsc.register_event(
capturer_weak,
CursorSense::drag_senses(),
CursorSense::drag(Axis::X),
move |ctx, _rsc| {
capturer_saw.set(capturer_saw.get() + 1);
if matches!(ctx.data.sense, CursorSense::Pressing(_)) {
@@ -493,7 +497,7 @@ fn taking_the_pointer_cancels_everyone_else_tracking_the_press() {
let (cancelled, ended) = (cancelled.clone(), ended.clone());
rsc.register_event(
bystander_weak,
CursorSense::drag_senses(),
CursorSense::drag(Axis::Y),
move |ctx, _rsc| match ctx.data.sense {
CursorSense::Cancel => cancelled.set(cancelled.get() + 1),
CursorSense::PressEnd(_) | CursorSense::Drop => ended.set(ended.get() + 1),
@@ -515,7 +519,7 @@ fn taking_the_pointer_cancels_everyone_else_tracking_the_press() {
render.update(&root, &mut rsc);
assert_eq!(cancelled.get(), 0, "nothing has captured yet");
let mut moved = cursor_at((50.0, 20.0).into());
let mut moved = cursor_at((80.0, 50.0).into());
moved.buttons.left = ActivationState::On;
render.run_sensors(&mut rsc, &mut state, moved, win);
render.update(&root, &mut rsc);
@@ -590,6 +594,9 @@ fn a_drag_pans_whichever_nested_scroll_area_owns_its_axis() {
record.set(Some(id));
id
})
// The horizontal area is visually above the vertical one, as
// it is when a raised transcript row contains sideways content.
.layer_offset(1)
.scrollable(Axis::Y, Pin::Start)
.add_strong(&mut rsc);
let inner = seen.get().unwrap();
+2 -2
View File
@@ -6,10 +6,10 @@ pub struct Image {
}
impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let size = self.handle.size();
painter.texture_within(&self.handle, size.align(Align::TOP_LEFT));
Size::abs(size)
painter.set_size(Size::abs(size));
}
fn size_hint(&self, axis: Axis) -> Option<Len> {
+3 -3
View File
@@ -6,7 +6,7 @@ pub struct Masked {
}
impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
match &self.shape {
Some(shape) => {
painter.child_layer();
@@ -16,9 +16,9 @@ impl Widget for Masked {
}
None => painter.set_mask(painter.region()),
}
let used = painter.widget(&self.inner);
let used = painter.widget(&self.inner).size();
painter.place_used(&self.inner, used, UiRegion::FULL);
used
painter.set_size(used);
}
fn requires_exact_region(&self) -> bool {
+3 -3
View File
@@ -6,8 +6,8 @@ pub struct Aligned {
}
impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) -> Size {
let used = painter.widget(&self.inner);
fn draw(&mut self, painter: &mut Painter) {
let used = painter.widget(&self.inner).size();
let density = painter.density();
let (x, y) = self.align.tuple();
let region = UiRegion::new(
@@ -19,6 +19,6 @@ impl Widget for Aligned {
.align(y.unwrap_or(AxisAlign::Neg)),
);
painter.place(&self.inner, region);
used
painter.set_size(used);
}
}
+3 -3
View File
@@ -6,12 +6,12 @@ pub struct LayerOffset {
}
impl Widget for LayerOffset {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
for _ in 0..self.offset {
painter.next_layer();
}
let used = painter.widget(&self.inner);
let used = painter.widget(&self.inner).size();
painter.place_used(&self.inner, used, UiRegion::FULL);
used
painter.set_size(used);
}
}
+6 -5
View File
@@ -939,7 +939,7 @@ impl LazySpan {
(Some(h), None) => {
let (lead, trail) = placement.edges(h);
let region = self.row_region(lead, trail);
let used = painter.widget_within(self.slot_widget(slot), region);
let used = painter.widget_within(self.slot_widget(slot), region).size();
let height = resolve(used);
if height != h {
let (new_lead, new_trail) = placement.edges(height);
@@ -957,7 +957,7 @@ impl LazySpan {
Placement::Trailing(_) => 0.0,
};
let first = self.row_region(measure_from, measure_from + GENEROUS_PADDING);
let height = resolve(painter.widget_within(self.slot_widget(slot), first));
let height = resolve(painter.widget_within(self.slot_widget(slot), first).size());
let (lead, trail) = placement.edges(height);
if is_anchor {
self.stabilize_lead(painter, measure_from, lead);
@@ -1072,7 +1072,7 @@ impl Widget for LazySpan {
self.tick_fling(now)
}
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let axis = self.dir.axis;
let output_len = painter.output_size().axis(axis);
self.viewport_len = painter.region().axis(axis).len().to_abs(output_len);
@@ -1081,7 +1081,8 @@ impl Widget for LazySpan {
self.repair_anchor();
if self.anchor.is_none() {
self.extents.clear();
return Size::REST;
painter.set_size(Size::REST);
return;
}
// What a wheel, a drag or a fling asked for since the last frame,
@@ -1154,7 +1155,7 @@ impl Widget for LazySpan {
self.rehome_anchor();
self.update_snap_end();
self.ctl.set_travel(self.travel());
Size::REST
painter.set_size(Size::REST);
}
fn size_hint(&self, _axis: Axis) -> Option<Len> {
+3 -3
View File
@@ -35,7 +35,7 @@ impl MaxSize {
}
impl Widget for MaxSize {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let output = painter.output_size();
let density = painter.density();
let offered = painter.px_size();
@@ -43,12 +43,12 @@ impl Widget for MaxSize {
x: Self::clamp_region(offered.x, self.x, output.x, density),
y: Self::clamp_region(offered.y, self.y, output.y, density),
};
let used = painter.widget_within(&self.inner, region);
let used = painter.widget_within(&self.inner, region).size();
let size = Size {
x: Self::clamp(used.x, self.x, output.x, density),
y: Self::clamp(used.y, self.y, output.y, density),
};
painter.place_used(&self.inner, size, UiRegion::FULL);
size
painter.set_size(size);
}
}
+3 -3
View File
@@ -6,10 +6,10 @@ pub struct Offset {
}
impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let region = UiRegion::FULL.offset(self.amt);
let used = painter.widget_within(&self.inner, region);
let used = painter.widget_within(&self.inner, region).size();
painter.place_used(&self.inner, used, region);
used
painter.set_size(used);
}
}
+3 -3
View File
@@ -8,11 +8,11 @@ pub struct Pad {
}
impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let density = painter.density();
let offered = painter.px_size();
let region = self.padding.region(density);
let used = painter.widget_within(&self.inner, region);
let used = painter.widget_within(&self.inner, region).size();
painter.place_used(&self.inner, used, region);
let width =
self.padding.left.apply_rest(density).abs + self.padding.right.apply_rest(density).abs;
@@ -26,7 +26,7 @@ impl Widget for Pad {
if needed.x <= offered.x + 0.01 && needed.y <= offered.y + 0.01 {
self.exact_region = false;
}
size
painter.set_size(size);
}
fn requires_exact_region(&self) -> bool {
+8 -3
View File
@@ -30,7 +30,7 @@ impl Widget for ScrollArea {
self.tick_fling(now)
}
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let axis = self.ctl.axis();
let container_len = painter.px_size().axis(axis);
self.container_len = container_len;
@@ -41,7 +41,9 @@ impl Widget for ScrollArea {
self.ctl.set_amt(travelled);
let hint = self.content_len.unwrap_or(container_len);
let used = painter.widget_within(&self.inner, self.child_region(hint));
let used = painter
.widget_within(&self.inner, self.child_region(hint))
.size();
let measured = used
.axis(axis)
@@ -62,7 +64,10 @@ impl Widget for ScrollArea {
fwd: range - amt,
});
painter.place(&self.inner, self.child_region(measured))
let size = painter
.place(&self.inner, self.child_region(measured))
.size();
painter.set_size(size);
}
}
+2 -2
View File
@@ -502,11 +502,11 @@ where
W: Widget + Scrollable,
WL: WidgetLike<Rsc, Tag, Widget = W>,
{
w.on(CursorSense::Scroll, move |ctx, rsc| {
w.on(CursorSense::Scroll(axis), move |ctx, rsc| {
let delta = ctx.data.scroll_delta.axis(axis) * 50.0;
ctx.widget(rsc).scroll(delta);
})
.on(CursorSense::drag_senses(), |ctx, rsc: &mut Rsc| {
.on(CursorSense::drag(axis), |ctx, rsc: &mut Rsc| {
let id = ctx.widget.id();
let (sense, pos) = (ctx.data.sense, ctx.data.cursor.pos);
let flung = ctx
+3 -3
View File
@@ -7,7 +7,7 @@ pub struct Sized {
}
impl Widget for Sized {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let density = painter.density();
let mut region = UiRegion::FULL;
if let Some(x) = self.x {
@@ -16,13 +16,13 @@ impl Widget for Sized {
if let Some(y) = self.y {
region.y = y.apply_rest(density).align(AxisAlign::Neg);
}
let used = painter.widget_within(&self.inner, region);
let used = painter.widget_within(&self.inner, region).size();
let size = Size {
x: self.x.map(|x| x.fold_dp(density)).unwrap_or(used.x),
y: self.y.map(|y| y.fold_dp(density)).unwrap_or(used.y),
};
painter.place_used(&self.inner, size, UiRegion::FULL);
size
painter.set_size(size);
}
fn size_hint(&self, axis: Axis) -> Option<Len> {
+5 -5
View File
@@ -8,7 +8,7 @@ pub struct Span {
}
impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let axis = self.dir.axis;
let gap = self.gap.apply_rest(painter.density()).abs;
@@ -29,7 +29,7 @@ impl Widget for Span {
slot.flip();
}
let region = UiRegion::from_axis(axis, slot, UiSpan::FULL);
let len = painter.widget_within(child, region).axis(axis);
let len = painter.widget_within(child, region).size().axis(axis);
lens[i] = Some(len);
drawn[i] = true;
len
@@ -65,9 +65,9 @@ impl Widget for Span {
child_region.flip(axis);
}
let used = if drawn[i] {
painter.place(child, child_region)
painter.place(child, child_region).size()
} else {
painter.widget_within(child, child_region)
painter.widget_within(child, child_region).size()
};
placed.push(child_region);
start.abs += gap;
@@ -97,7 +97,7 @@ impl Widget for Span {
Len::default()
};
Size::from_axis(axis, along, ortho_len)
painter.set_size(Size::from_axis(axis, along, ortho_len));
}
}
+6 -6
View File
@@ -8,7 +8,7 @@ pub struct Stack {
}
impl Widget for Stack {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let density = painter.density();
let known = match self.size {
StackSize::Default => Some(Size::REST),
@@ -25,15 +25,15 @@ impl Widget for Stack {
if let Some(child) = iter.next() {
painter.child_layer();
used.push(match region {
Some(region) => painter.widget_within(child, region),
None => painter.widget(child),
Some(region) => painter.widget_within(child, region).size(),
None => painter.widget(child).size(),
});
}
for child in iter {
painter.next_layer();
used.push(match region {
Some(region) => painter.widget_within(child, region),
None => painter.widget(child),
Some(region) => painter.widget_within(child, region).size(),
None => painter.widget(child).size(),
});
}
let size = match self.size {
@@ -50,7 +50,7 @@ impl Widget for Stack {
painter.place(child, child_region);
}
}
size
painter.set_size(size);
}
fn size_hint(&self, _axis: Axis) -> Option<Len> {
+5 -4
View File
@@ -6,14 +6,15 @@ pub struct WidgetPtr {
}
impl Widget for WidgetPtr {
fn draw(&mut self, painter: &mut Painter) -> Size {
if let Some(id) = &self.inner {
let used = painter.widget(id);
fn draw(&mut self, painter: &mut Painter) {
let size = if let Some(id) = &self.inner {
let used = painter.widget(id).size();
painter.place_used(id, used, UiRegion::FULL);
used
} else {
Size::ZERO
}
};
painter.set_size(size);
}
fn is_size_independent(&self) -> bool {
+2 -2
View File
@@ -28,14 +28,14 @@ impl Rect {
}
impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
painter.primitive(RectPrimitive {
color: self.color,
radius: self.radius.fold_dp(painter.density()).abs,
thickness: self.thickness,
inner_radius: self.inner_radius,
});
Size::REST
painter.set_size(Size::REST);
}
fn size_hint(&self, _axis: Axis) -> Option<Len> {
+4 -3
View File
@@ -92,7 +92,7 @@ impl TextEdit {
}
impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
let base = painter.layer;
painter.child_layer();
let used = self.view.draw(painter);
@@ -100,7 +100,8 @@ impl Widget for TextEdit {
let region = self.region();
let Some(selection) = self.selection else {
return used;
painter.set_size(used);
return;
};
let layout = self.view.buf.layout();
@@ -122,7 +123,7 @@ impl Widget for TextEdit {
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(top_left).within(&region),
);
used
painter.set_size(used);
}
fn requires_exact_region(&self) -> bool {
+4 -3
View File
@@ -111,7 +111,7 @@ impl TextView {
if self.is_blank()
&& let Some(hint) = &self.hint
{
return painter.widget(hint);
return painter.widget(hint).size();
}
let region = tex.size.align(self.align);
let within = region.within(&painter.region());
@@ -141,9 +141,10 @@ impl Text {
}
impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) -> Size {
fn draw(&mut self, painter: &mut Painter) {
self.update_buf();
self.view.draw(painter)
let size = self.view.draw(painter);
painter.set_size(size);
}
fn requires_exact_region(&self) -> bool {