Make Iris layout dependencies explicit

This commit is contained in:
iris committed 2026-09-09 22:35:03 -04:00
1 parent 2bc0ff1866
commit 3ae034a47b
25 files changed
+609 -186

No files matched your search

+5 -1
View File
@@ -11,12 +11,16 @@ pub struct ActiveData {
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>, pub primitives: Vec<PrimitiveHandle>,
pub children: Vec<WidgetId>, 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`. /// The inherited mask, not `own_mask`.
pub mask: MaskIdx, pub mask: MaskIdx,
/// The widget's retained mask slot, or `MaskIdx::NONE`. /// The widget's retained mask slot, or `MaskIdx::NONE`.
pub own_mask: MaskIdx, pub own_mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
/// The last `Widget::draw` result. /// The size recorded by the last `Widget::draw` through its painter.
pub size: Size, pub size: Size,
/// Retained so descendants' parent links stay valid across redraws. /// Retained so descendants' parent links stay valid across redraws.
pub move_slot: MoveIdx, pub move_slot: MoveIdx,
+1 -1
View File
@@ -9,7 +9,7 @@ mod render_state;
pub use access::*; pub use access::*;
pub use active::*; pub use active::*;
pub use painter::Painter; pub use painter::{DrawResult, Painter};
pub use render_state::*; pub use render_state::*;
#[derive(Default)] #[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. /// Previous handles, consumed in draw order and freed if left over.
pub(super) recycle: std::iter::Peekable<std::vec::IntoIter<PrimitiveHandle>>, pub(super) recycle: std::iter::Peekable<std::vec::IntoIter<PrimitiveHandle>>,
pub(super) children: Vec<WidgetId>, 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 layer: usize,
pub(super) id: WidgetId, 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> { 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) { fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
self.write_primitive(primitive, region, Drawn::Yes); self.write_primitive(primitive, region, Drawn::Yes);
} }
@@ -207,15 +241,19 @@ impl<'a> Painter<'a> {
self.mask = self.own_mask; self.mask = self.own_mask;
} }
/// Draws a widget within this widget's region, returning the size it /// Draw a widget within this widget's region. Reading the result's size
/// reported using. /// records that this widget's layout depends on the child.
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) -> Size { pub fn widget<'p, W: ?Sized>(&'p mut self, id: &StrongWidget<W>) -> DrawResult<'p, 'a> {
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.
/// Useful for drawing child widgets in select areas. /// 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)) 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> { pub fn known_len<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
if let Some(len) = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) { let len = if let Some(len) = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) {
return Some(len.fold_dp(self.density())); 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()) { len
return None;
}
self.state.active.get(&id.id()).map(|a| a.size.axis(axis))
} }
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()); self.children.push(id.id());
// Passed directly rather than looked up from `self.active`: this // Passed directly rather than looked up from `self.active`: this
// widget's own `ActiveData` (which would carry its `move_slot`) is // widget's own `ActiveData` (which would carry its `move_slot`) is
@@ -289,19 +339,26 @@ impl<'a> Painter<'a> {
self.mask, self.mask,
Retained::default(), Retained::default(),
self.rsc, self.rsc,
) );
DrawResult {
painter: self,
child: id.id(),
}
} }
/// Place an already-drawn child's used area, redrawing only if its size changes. /// 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 region = region.within(&self.region);
let retained = self let retained = self
.state .state
.active .active
.get(&id.id()) .get(&id.id())
.map(|active| (active.layer, active.mask)); .map(|active| (active.layer, active.mask));
if let Some(size) = self.state.place(id.id(), region, self.rsc) { if self.state.place(id.id(), region, self.rsc).is_some() {
size
} else if let Some((layer, mask)) = retained { } else if let Some((layer, mask)) = retained {
self.children.push(id.id()); self.children.push(id.id());
self.rsc.widgets_mut().needs_redraw.insert(id.id()); self.rsc.widgets_mut().needs_redraw.insert(id.id());
@@ -315,9 +372,24 @@ impl<'a> Painter<'a> {
mask, mask,
Retained::default(), Retained::default(),
self.rsc, self.rsc,
) );
} else { } 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>, id: &StrongWidget<W>,
used: Size, used: Size,
within: UiRegion, within: UiRegion,
) -> Size { ) -> DrawResult<'_, 'a> {
let region = self.fit_region(used, within); let region = self.fit_region(used, within);
self.place(id, region) 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 move_slot = Self::move_slot_for(old_move_slot, parent_move_slot, rsc);
let inherited_mask = mask; let inherited_mask = mask;
let reuse_child_sizes = // `Painter::layer` is a cursor widgets advance while assigning
old_region.is_some_and(|old| Self::same_size(old, region, self.output_size)); // 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 { let mut painter = Painter {
state: self, state: self,
region, region,
@@ -550,6 +558,8 @@ impl UiRenderState {
primitives: Vec::new(), primitives: Vec::new(),
recycle: recycle.into_iter().peekable(), recycle: recycle.into_iter().peekable(),
children: Vec::new(), children: Vec::new(),
size_dependencies: Vec::new(),
size: None,
reuse_child_sizes, reuse_child_sizes,
rsc, rsc,
}; };
@@ -561,7 +571,13 @@ impl UiRenderState {
widget.size_hint(Axis::Y).map(|len| len.fold_dp(density)), widget.size_hint(Axis::Y).map(|len| len.fold_dp(density)),
]; ];
painter.state.draw_count += 1; 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!( debug_assert!(
size.x.dp == 0.0 && size.y.dp == 0.0, size.x.dp == 0.0 && size.y.dp == 0.0,
"widget {id:?} reported an unresolved `dp` size ({size:?}); \ "widget {id:?} reported an unresolved `dp` size ({size:?}); \
@@ -593,8 +609,10 @@ impl UiRenderState {
primitives, primitives,
recycle, recycle,
children, children,
size_dependencies,
size: _,
reuse_child_sizes: _, reuse_child_sizes: _,
layer, layer: _,
id, id,
} = painter; } = painter;
@@ -615,8 +633,9 @@ impl UiRenderState {
textures, textures,
primitives, primitives,
children, children,
size_dependencies,
mask: inherited_mask, mask: inherited_mask,
layer, layer: inherited_layer,
size, size,
move_slot, move_slot,
child_move_slot, child_move_slot,
@@ -690,9 +709,13 @@ impl UiRenderState {
} }
fn same_size(a: UiRegion, b: UiRegion, output: Vec2) -> bool { fn same_size(a: UiRegion, b: UiRegion, output: Vec2) -> bool {
let a = a.size().to_abs(output); Self::same_axis_size(a, b, Axis::X, output) && Self::same_axis_size(a, b, Axis::Y, output)
let b = b.size().to_abs(output); }
(a.x - b.x).abs() < 0.01 && (a.y - b.y).abs() < 0.01
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( pub(super) fn place(
@@ -920,12 +943,76 @@ impl UiRenderState {
} }
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) { pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
while let Some(&id) = rsc.widgets().needs_redraw.iter().next() { while rsc.widgets().has_updates() {
self.redraw(id, rsc); // 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(); 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 { pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
root.into().map(|r| r.id()) != self.old_root root.into().map(|r| r.id()) != self.old_root
} }
@@ -1207,61 +1294,18 @@ 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) {
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); rsc.widgets_mut().needs_redraw.remove(&id);
// An ancestor is drawing this widget right now, and that draw is // An ancestor is drawing this widget right now, and that draw is
// about to write fresh primitives for it. Drawing it a second time // about to write fresh primitives for it. Drawing it a second time
// here would leave one of the two copies on screen with nothing // here would leave one of the two copies on screen with nothing
// owning it -- see `draw_started`'s own doc. // owning it -- see `draw_started`'s own doc.
if self.draw_started.contains(&id) { if self.draw_started.contains(&id) {
return None; return;
} }
let active = self.remove(id, false, true, rsc)?; let Some(active) = self.remove(id, false, true, rsc) else {
let old_size = active.size; return;
};
let parent = active.parent; let parent = active.parent;
// `old_move_slot` being `Some` below means the slot is reused in // `old_move_slot` being `Some` below means the slot is reused in
// place rather than freshly parented, so this is only reached for // place rather than freshly parented, so this is only reached for
@@ -1285,13 +1329,6 @@ impl UiRenderState {
}, },
rsc, 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 use widgets::*;
pub trait Widget: Any { 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. /// An exact, context-free length known without drawing or inspecting children.
fn size_hint(&self, _axis: Axis) -> Option<Len> { fn size_hint(&self, _axis: Axis) -> Option<Len> {
@@ -45,8 +45,8 @@ pub trait Widget: Any {
} }
impl Widget for () { impl Widget for () {
fn draw(&mut self, _: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
Size::ZERO painter.set_size(Size::ZERO);
} }
fn is_size_independent(&self) -> bool { fn is_size_independent(&self) -> bool {
+209 -6
View File
@@ -5,6 +5,7 @@
//! CPU-side layout/move machinery LAYOUT.md is about. //! CPU-side layout/move machinery LAYOUT.md is about.
use crate::prelude::*; use crate::prelude::*;
use std::{cell::Cell, cell::RefCell, rc::Rc};
/// The minimal `UiRsc` a test needs: just the shared `UiData`, none of the /// The minimal `UiRsc` a test needs: just the shared `UiData`, none of the
/// event/window/state plumbing `DefaultRsc` carries. `pub(crate)` so /// event/window/state plumbing `DefaultRsc` carries. `pub(crate)` so
@@ -26,14 +27,14 @@ impl UiRsc for TestRsc {
struct FixedRect(f32); struct FixedRect(f32);
impl Widget for FixedRect { 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); let size = Size::from_axis(Axis::Y, Len::abs(self.0), Len::REST);
painter.primitive_within( painter.primitive_within(
RectPrimitive::color(UiColor::WHITE), RectPrimitive::color(UiColor::WHITE),
size.to_uivec2(painter.density()) size.to_uivec2(painter.density())
.align(RegionAlign::TOP_LEFT), .align(RegionAlign::TOP_LEFT),
); );
size painter.set_size(size);
} }
} }
@@ -43,12 +44,214 @@ struct ChildOffset {
} }
impl Widget for 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.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] #[test]
fn a_child_coordinate_offset_moves_only_the_child_subtree() { fn a_child_coordinate_offset_moves_only_the_child_subtree() {
let mut rsc = TestRsc { let mut rsc = TestRsc {
@@ -721,7 +924,7 @@ struct MoveThenPlace {
} }
impl Widget for MoveThenPlace { impl Widget for MoveThenPlace {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let offer = UiRegion::new( let offer = UiRegion::new(
UiSpan::FULL, UiSpan::FULL,
UiSpan::new( UiSpan::new(
@@ -738,7 +941,7 @@ impl Widget for MoveThenPlace {
), ),
); );
painter.place(&self.inner, place); painter.place(&self.inner, place);
Size::default() painter.set_size(Size::default());
} }
} }
+116 -25
View File
@@ -21,7 +21,7 @@ pub enum CursorSense {
HoverStart, HoverStart,
Hovering, Hovering,
HoverEnd, HoverEnd,
Scroll, Scroll(Axis),
/// Delivered exactly once, in place of `PressEnd`, to whichever widget /// Delivered exactly once, in place of `PressEnd`, to whichever widget
/// currently holds pointer capture (`UiRenderState::capture_pointer`) /// currently holds pointer capture (`UiRenderState::capture_pointer`)
/// when the button lifts -- see `iris::sense`'s pointer-capture doc /// when the button lifts -- see `iris::sense`'s pointer-capture doc
@@ -47,7 +47,10 @@ pub enum CursorSense {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct CursorSenses(Vec<CursorSense>); pub struct CursorSenses {
senses: Vec<CursorSense>,
drag_axis: Option<Axis>,
}
impl Event for CursorSenses { impl Event for CursorSenses {
type Data<'a> = CursorData<'a>; type Data<'a> = CursorData<'a>;
@@ -74,7 +77,13 @@ impl Event for CursorSenses {
if data.sense == CursorSense::Drop || data.sense == CursorSense::Cancel { if data.sense == CursorSense::Drop || data.sense == CursorSense::Cancel {
return self.contains(&data.sense).then(|| data.clone()); 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(); let mut data = data.clone();
data.sense = sense; data.sense = sense;
Some(data) 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 { impl CursorSense {
pub fn click() -> Self { pub fn click() -> Self {
Self::PressStart(CursorButton::Left) Self::PressStart(CursorButton::Left)
@@ -109,6 +142,16 @@ impl CursorSense {
pub fn drag_senses() -> CursorSenses { pub fn drag_senses() -> CursorSenses {
Self::click_or_drag() | Self::unclick() | Self::Drop | Self::Cancel 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 { pub fn is_dragging(&self) -> bool {
matches!(self, CursorSense::Pressing(CursorButton::Left)) matches!(self, CursorSense::Pressing(CursorButton::Left))
} }
@@ -261,6 +304,11 @@ pub struct CursorData<'a> {
pub scroll_delta: Vec2, pub scroll_delta: Vec2,
pub hover: ActivationState, pub hover: ActivationState,
pub cursor: CursorState, 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 /// the first sense that triggered this
pub sense: CursorSense, pub sense: CursorSense,
pub render: &'a UiRenderState, pub render: &'a UiRenderState,
@@ -294,6 +342,8 @@ pub struct CursorData<'a> {
pub struct PointerInput { pub struct PointerInput {
captured: Option<WidgetId>, captured: Option<WidgetId>,
pressed: Vec<WidgetId>, pressed: Vec<WidgetId>,
press_origin: Option<Vec2>,
drag_axis: Option<Axis>,
} }
impl PointerInput { impl PointerInput {
@@ -393,6 +443,22 @@ impl SensorUi for UiRenderState {
holder: std::cell::Cell::new(pointer.captured), holder: std::cell::Cell::new(pointer.captured),
}; };
let button_down = cursor.buttons.select(&CursorButton::Left).is_on(); 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`). // The platform took the gesture away (`CursorState::cancelled`).
// Everybody still tracking this press hears about it -- the // Everybody still tracking this press hears about it -- the
@@ -410,6 +476,8 @@ impl SensorUi for UiRenderState {
} }
} }
requests.release(); requests.release();
pointer.press_origin = None;
pointer.drag_axis = None;
for id in told { for id in told {
deliver_cancel(self, rsc, state, id, &cursor, window_size, &requests); deliver_cancel(self, rsc, state, id, &cursor, window_size, &requests);
} }
@@ -458,6 +526,8 @@ impl SensorUi for UiRenderState {
scroll_delta: cursor.scroll_delta, scroll_delta: cursor.scroll_delta,
hover: ActivationState::On, hover: ActivationState::On,
cursor: cursor.clone(), cursor: cursor.clone(),
drag_axis: pointer.drag_axis,
captured: true,
sense, sense,
render: self, render: self,
pointer: &requests, pointer: &requests,
@@ -466,6 +536,8 @@ impl SensorUi for UiRenderState {
if !button_down { if !button_down {
requests.release(); requests.release();
pointer.pressed.clear(); pointer.pressed.clear();
pointer.press_origin = None;
pointer.drag_axis = None;
} }
pointer.captured = requests.holder(); pointer.captured = requests.holder();
rsc.events_mut().get_type::<CursorSense>().global = pointer; rsc.events_mut().get_type::<CursorSense>().global = pointer;
@@ -524,20 +596,6 @@ impl SensorUi for UiRenderState {
// widget that registered a matching non-hover sense // widget that registered a matching non-hover sense
// consumes it -- a button that only registered `click()` // consumes it -- a button that only registered `click()`
// must not block a scroll meant for the list behind it. // 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 cursor = cursor.clone();
let data = CursorData { let data = CursorData {
@@ -546,12 +604,22 @@ impl SensorUi for UiRenderState {
scroll_delta: cursor.scroll_delta, scroll_delta: cursor.scroll_delta,
hover: sensor.hover, hover: sensor.hover,
cursor, cursor,
drag_axis: pointer.drag_axis,
captured: false,
// this does not have any meaning; // this does not have any meaning;
// might wanna set up Event to have a prepare stage // might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering, sense: CursorSense::Hovering,
render: self, render: self,
pointer: &requests, 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); rsc.run_event::<CursorSense>(*id, data, state);
// Anything handed a frame while the button is down may // Anything handed a frame while the button is down may
// have opened a gesture on it, and is owed a `Cancel` if // have opened a gesture on it, and is owed a `Cancel` if
@@ -563,7 +631,7 @@ impl SensorUi for UiRenderState {
pointer.pressed.push(*id); pointer.pressed.push(*id);
} }
} }
if sensed { if sensed || requests.holder().is_some() {
break; break;
} }
} }
@@ -597,6 +665,10 @@ impl SensorUi for UiRenderState {
// A cancel handler may itself have captured (a widget deciding // A cancel handler may itself have captured (a widget deciding
// the gesture is now its own); `requests` is still the truth. // the gesture is now its own); `requests` is still the truth.
pointer.captured = requests.holder(); pointer.captured = requests.holder();
if !button_down {
pointer.press_origin = None;
pointer.drag_axis = None;
}
rsc.events_mut().get_type::<CursorSense>().global = pointer; rsc.events_mut().get_type::<CursorSense>().global = pointer;
} }
} }
@@ -625,6 +697,8 @@ fn deliver_cancel<Rsc: HasEvents>(
scroll_delta: cursor.scroll_delta, scroll_delta: cursor.scroll_delta,
hover: ActivationState::On, hover: ActivationState::On,
cursor: cursor.clone(), cursor: cursor.clone(),
drag_axis: None,
captured: false,
sense: CursorSense::Cancel, sense: CursorSense::Cancel,
render, render,
pointer, pointer,
@@ -636,6 +710,8 @@ pub fn should_run(
senses: &CursorSenses, senses: &CursorSenses,
cursor: &CursorState, cursor: &CursorState,
hover: ActivationState, hover: ActivationState,
drag_axis: Option<Axis>,
captured: bool,
) -> Option<CursorSense> { ) -> Option<CursorSense> {
// Every sense below that is about the *pointer* rather than about // Every sense below that is about the *pointer* rather than about
// hovering needs the pointer to actually be on this widget, and // 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::HoverStart => hover.is_start(),
CursorSense::Hovering => hover.is_on(), CursorSense::Hovering => hover.is_on(),
CursorSense::HoverEnd => hover.is_end(), 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 // Never derived here -- `Drop` only ever fires through
// `CursorSenses::should_run`'s own special case, ahead of this // `CursorSenses::should_run`'s own special case, ahead of this
// loop, for the one widget `run_sensors`' capture branch is // 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 // note above; both are set by `run_sensors` alone, for the one
// widget it is delivering to this frame. // widget it is delivering to this frame.
CursorSense::Drop | CursorSense::Cancel => false, CursorSense::Drop | CursorSense::Cancel => false,
} { } && (captured
|| !matches!(sense, CursorSense::Pressing(_))
|| senses.drag_axis.is_none()
|| senses.drag_axis == drag_axis)
{
return Some(*sense); return Some(*sense);
} }
} }
@@ -744,19 +829,22 @@ impl Deref for CursorSenses {
type Target = Vec<CursorSense>; type Target = Vec<CursorSense>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.senses
} }
} }
impl DerefMut for CursorSenses { impl DerefMut for CursorSenses {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0 &mut self.senses
} }
} }
impl From<CursorSense> for CursorSenses { impl From<CursorSense> for CursorSenses {
fn from(val: CursorSense) -> Self { 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; type Output = CursorSenses;
fn bitor(self, rhs: Self) -> Self::Output { 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; type Output = Self;
fn bitor(mut self, rhs: CursorSense) -> Self::Output { fn bitor(mut self, rhs: CursorSense) -> Self::Output {
self.0.push(rhs); self.senses.push(rhs);
self 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 clicked = Rc::new(Cell::new(false));
{ {
let scrolled = scrolled.clone(); let scrolled = scrolled.clone();
rsc.register_event(list_weak, CursorSense::Scroll, move |_ctx, _rsc| { rsc.register_event(
scrolled.set(true); list_weak,
}); CursorSense::Scroll(Axis::Y),
move |_ctx, _rsc| {
scrolled.set(true);
},
);
} }
{ {
let clicked = clicked.clone(); let clicked = clicked.clone();
@@ -461,10 +465,10 @@ fn taking_the_pointer_cancels_everyone_else_tracking_the_press() {
events: EventManager::default(), events: EventManager::default(),
}; };
// The bystander *contains* the capturer, which is the real shape: a // The bystander contains the capturer on a lower visual layer: the
// transcript's `LazySpan` and one row's own text both track the same // shape of a vertical transcript scroller with a higher horizontal
// press, and a `Stack`'s siblings would be on separate layers where // scroller inside one row. Both observe the undecided press, then only
// only the topmost is dispatched to at all. // the recognizer matching its direction may consume it.
let capturer = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE)); let capturer = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
let capturer_weak = capturer.weak(); let capturer_weak = capturer.weak();
let bystander = rsc.ui.widgets.add_strong(Stack { 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(); let capturer_saw = capturer_saw.clone();
rsc.register_event( rsc.register_event(
capturer_weak, capturer_weak,
CursorSense::drag_senses(), CursorSense::drag(Axis::X),
move |ctx, _rsc| { move |ctx, _rsc| {
capturer_saw.set(capturer_saw.get() + 1); capturer_saw.set(capturer_saw.get() + 1);
if matches!(ctx.data.sense, CursorSense::Pressing(_)) { 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()); let (cancelled, ended) = (cancelled.clone(), ended.clone());
rsc.register_event( rsc.register_event(
bystander_weak, bystander_weak,
CursorSense::drag_senses(), CursorSense::drag(Axis::Y),
move |ctx, _rsc| match ctx.data.sense { move |ctx, _rsc| match ctx.data.sense {
CursorSense::Cancel => cancelled.set(cancelled.get() + 1), CursorSense::Cancel => cancelled.set(cancelled.get() + 1),
CursorSense::PressEnd(_) | CursorSense::Drop => ended.set(ended.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); render.update(&root, &mut rsc);
assert_eq!(cancelled.get(), 0, "nothing has captured yet"); 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; moved.buttons.left = ActivationState::On;
render.run_sensors(&mut rsc, &mut state, moved, win); render.run_sensors(&mut rsc, &mut state, moved, win);
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
@@ -590,6 +594,9 @@ fn a_drag_pans_whichever_nested_scroll_area_owns_its_axis() {
record.set(Some(id)); record.set(Some(id));
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) .scrollable(Axis::Y, Pin::Start)
.add_strong(&mut rsc); .add_strong(&mut rsc);
let inner = seen.get().unwrap(); let inner = seen.get().unwrap();
+2 -2
View File
@@ -6,10 +6,10 @@ pub struct Image {
} }
impl Widget for 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(); let size = self.handle.size();
painter.texture_within(&self.handle, size.align(Align::TOP_LEFT)); 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> { fn size_hint(&self, axis: Axis) -> Option<Len> {
+3 -3
View File
@@ -6,7 +6,7 @@ pub struct Masked {
} }
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
match &self.shape { match &self.shape {
Some(shape) => { Some(shape) => {
painter.child_layer(); painter.child_layer();
@@ -16,9 +16,9 @@ impl Widget for Masked {
} }
None => painter.set_mask(painter.region()), 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); painter.place_used(&self.inner, used, UiRegion::FULL);
used painter.set_size(used);
} }
fn requires_exact_region(&self) -> bool { fn requires_exact_region(&self) -> bool {
+3 -3
View File
@@ -6,8 +6,8 @@ pub struct Aligned {
} }
impl Widget for Aligned { impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let used = painter.widget(&self.inner); let used = painter.widget(&self.inner).size();
let density = painter.density(); let density = painter.density();
let (x, y) = self.align.tuple(); let (x, y) = self.align.tuple();
let region = UiRegion::new( let region = UiRegion::new(
@@ -19,6 +19,6 @@ impl Widget for Aligned {
.align(y.unwrap_or(AxisAlign::Neg)), .align(y.unwrap_or(AxisAlign::Neg)),
); );
painter.place(&self.inner, region); 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 { impl Widget for LayerOffset {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
for _ in 0..self.offset { for _ in 0..self.offset {
painter.next_layer(); painter.next_layer();
} }
let used = painter.widget(&self.inner); let used = painter.widget(&self.inner).size();
painter.place_used(&self.inner, used, UiRegion::FULL); 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) => { (Some(h), None) => {
let (lead, trail) = placement.edges(h); let (lead, trail) = placement.edges(h);
let region = self.row_region(lead, trail); 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); let height = resolve(used);
if height != h { if height != h {
let (new_lead, new_trail) = placement.edges(height); let (new_lead, new_trail) = placement.edges(height);
@@ -957,7 +957,7 @@ impl LazySpan {
Placement::Trailing(_) => 0.0, Placement::Trailing(_) => 0.0,
}; };
let first = self.row_region(measure_from, measure_from + GENEROUS_PADDING); 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); let (lead, trail) = placement.edges(height);
if is_anchor { if is_anchor {
self.stabilize_lead(painter, measure_from, lead); self.stabilize_lead(painter, measure_from, lead);
@@ -1072,7 +1072,7 @@ impl Widget for LazySpan {
self.tick_fling(now) 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 axis = self.dir.axis;
let output_len = painter.output_size().axis(axis); let output_len = painter.output_size().axis(axis);
self.viewport_len = painter.region().axis(axis).len().to_abs(output_len); self.viewport_len = painter.region().axis(axis).len().to_abs(output_len);
@@ -1081,7 +1081,8 @@ impl Widget for LazySpan {
self.repair_anchor(); self.repair_anchor();
if self.anchor.is_none() { if self.anchor.is_none() {
self.extents.clear(); 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, // 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.rehome_anchor();
self.update_snap_end(); self.update_snap_end();
self.ctl.set_travel(self.travel()); self.ctl.set_travel(self.travel());
Size::REST painter.set_size(Size::REST);
} }
fn size_hint(&self, _axis: Axis) -> Option<Len> { fn size_hint(&self, _axis: Axis) -> Option<Len> {
+3 -3
View File
@@ -35,7 +35,7 @@ impl MaxSize {
} }
impl Widget for 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 output = painter.output_size();
let density = painter.density(); let density = painter.density();
let offered = painter.px_size(); let offered = painter.px_size();
@@ -43,12 +43,12 @@ impl Widget for MaxSize {
x: Self::clamp_region(offered.x, self.x, output.x, density), x: Self::clamp_region(offered.x, self.x, output.x, density),
y: Self::clamp_region(offered.y, self.y, output.y, 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 { let size = Size {
x: Self::clamp(used.x, self.x, output.x, density), x: Self::clamp(used.x, self.x, output.x, density),
y: Self::clamp(used.y, self.y, output.y, density), y: Self::clamp(used.y, self.y, output.y, density),
}; };
painter.place_used(&self.inner, size, UiRegion::FULL); 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 { 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 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); 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 { impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let density = painter.density(); let density = painter.density();
let offered = painter.px_size(); let offered = painter.px_size();
let region = self.padding.region(density); 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); painter.place_used(&self.inner, used, region);
let width = let width =
self.padding.left.apply_rest(density).abs + self.padding.right.apply_rest(density).abs; 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 { if needed.x <= offered.x + 0.01 && needed.y <= offered.y + 0.01 {
self.exact_region = false; self.exact_region = false;
} }
size painter.set_size(size);
} }
fn requires_exact_region(&self) -> bool { fn requires_exact_region(&self) -> bool {
+8 -3
View File
@@ -30,7 +30,7 @@ impl Widget for ScrollArea {
self.tick_fling(now) 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 axis = self.ctl.axis();
let container_len = painter.px_size().axis(axis); let container_len = painter.px_size().axis(axis);
self.container_len = container_len; self.container_len = container_len;
@@ -41,7 +41,9 @@ impl Widget for ScrollArea {
self.ctl.set_amt(travelled); self.ctl.set_amt(travelled);
let hint = self.content_len.unwrap_or(container_len); 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 let measured = used
.axis(axis) .axis(axis)
@@ -62,7 +64,10 @@ impl Widget for ScrollArea {
fwd: range - amt, 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, W: Widget + Scrollable,
WL: WidgetLike<Rsc, Tag, Widget = W>, 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; let delta = ctx.data.scroll_delta.axis(axis) * 50.0;
ctx.widget(rsc).scroll(delta); 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 id = ctx.widget.id();
let (sense, pos) = (ctx.data.sense, ctx.data.cursor.pos); let (sense, pos) = (ctx.data.sense, ctx.data.cursor.pos);
let flung = ctx let flung = ctx
+3 -3
View File
@@ -7,7 +7,7 @@ pub struct Sized {
} }
impl Widget for 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 density = painter.density();
let mut region = UiRegion::FULL; let mut region = UiRegion::FULL;
if let Some(x) = self.x { if let Some(x) = self.x {
@@ -16,13 +16,13 @@ impl Widget for Sized {
if let Some(y) = self.y { if let Some(y) = self.y {
region.y = y.apply_rest(density).align(AxisAlign::Neg); 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 { let size = Size {
x: self.x.map(|x| x.fold_dp(density)).unwrap_or(used.x), 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), y: self.y.map(|y| y.fold_dp(density)).unwrap_or(used.y),
}; };
painter.place_used(&self.inner, size, UiRegion::FULL); painter.place_used(&self.inner, size, UiRegion::FULL);
size painter.set_size(size);
} }
fn size_hint(&self, axis: Axis) -> Option<Len> { fn size_hint(&self, axis: Axis) -> Option<Len> {
+5 -5
View File
@@ -8,7 +8,7 @@ pub struct Span {
} }
impl Widget for 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 axis = self.dir.axis;
let gap = self.gap.apply_rest(painter.density()).abs; let gap = self.gap.apply_rest(painter.density()).abs;
@@ -29,7 +29,7 @@ impl Widget for Span {
slot.flip(); slot.flip();
} }
let region = UiRegion::from_axis(axis, slot, UiSpan::FULL); 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); lens[i] = Some(len);
drawn[i] = true; drawn[i] = true;
len len
@@ -65,9 +65,9 @@ impl Widget for Span {
child_region.flip(axis); child_region.flip(axis);
} }
let used = if drawn[i] { let used = if drawn[i] {
painter.place(child, child_region) painter.place(child, child_region).size()
} else { } else {
painter.widget_within(child, child_region) painter.widget_within(child, child_region).size()
}; };
placed.push(child_region); placed.push(child_region);
start.abs += gap; start.abs += gap;
@@ -97,7 +97,7 @@ impl Widget for Span {
Len::default() 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 { impl Widget for Stack {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let density = painter.density(); let density = painter.density();
let known = match self.size { let known = match self.size {
StackSize::Default => Some(Size::REST), StackSize::Default => Some(Size::REST),
@@ -25,15 +25,15 @@ impl Widget for Stack {
if let Some(child) = iter.next() { if let Some(child) = iter.next() {
painter.child_layer(); painter.child_layer();
used.push(match region { used.push(match region {
Some(region) => painter.widget_within(child, region), Some(region) => painter.widget_within(child, region).size(),
None => painter.widget(child), None => painter.widget(child).size(),
}); });
} }
for child in iter { for child in iter {
painter.next_layer(); painter.next_layer();
used.push(match region { used.push(match region {
Some(region) => painter.widget_within(child, region), Some(region) => painter.widget_within(child, region).size(),
None => painter.widget(child), None => painter.widget(child).size(),
}); });
} }
let size = match self.size { let size = match self.size {
@@ -50,7 +50,7 @@ impl Widget for Stack {
painter.place(child, child_region); painter.place(child, child_region);
} }
} }
size painter.set_size(size);
} }
fn size_hint(&self, _axis: Axis) -> Option<Len> { fn size_hint(&self, _axis: Axis) -> Option<Len> {
+5 -4
View File
@@ -6,14 +6,15 @@ pub struct WidgetPtr {
} }
impl Widget for WidgetPtr { impl Widget for WidgetPtr {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
if let Some(id) = &self.inner { let size = if let Some(id) = &self.inner {
let used = painter.widget(id); let used = painter.widget(id).size();
painter.place_used(id, used, UiRegion::FULL); painter.place_used(id, used, UiRegion::FULL);
used used
} else { } else {
Size::ZERO Size::ZERO
} };
painter.set_size(size);
} }
fn is_size_independent(&self) -> bool { fn is_size_independent(&self) -> bool {
+2 -2
View File
@@ -28,14 +28,14 @@ impl Rect {
} }
impl Widget for Rect { impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
painter.primitive(RectPrimitive { painter.primitive(RectPrimitive {
color: self.color, color: self.color,
radius: self.radius.fold_dp(painter.density()).abs, radius: self.radius.fold_dp(painter.density()).abs,
thickness: self.thickness, thickness: self.thickness,
inner_radius: self.inner_radius, inner_radius: self.inner_radius,
}); });
Size::REST painter.set_size(Size::REST);
} }
fn size_hint(&self, _axis: Axis) -> Option<Len> { fn size_hint(&self, _axis: Axis) -> Option<Len> {
+4 -3
View File
@@ -92,7 +92,7 @@ impl TextEdit {
} }
impl Widget for TextEdit { impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let base = painter.layer; let base = painter.layer;
painter.child_layer(); painter.child_layer();
let used = self.view.draw(painter); let used = self.view.draw(painter);
@@ -100,7 +100,8 @@ impl Widget for TextEdit {
let region = self.region(); let region = self.region();
let Some(selection) = self.selection else { let Some(selection) = self.selection else {
return used; painter.set_size(used);
return;
}; };
let layout = self.view.buf.layout(); let layout = self.view.buf.layout();
@@ -122,7 +123,7 @@ impl Widget for TextEdit {
RectPrimitive::color(Color::WHITE), RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(top_left).within(&region), size.align(Align::TOP_LEFT).offset(top_left).within(&region),
); );
used painter.set_size(used);
} }
fn requires_exact_region(&self) -> bool { fn requires_exact_region(&self) -> bool {
+4 -3
View File
@@ -111,7 +111,7 @@ impl TextView {
if self.is_blank() if self.is_blank()
&& let Some(hint) = &self.hint && let Some(hint) = &self.hint
{ {
return painter.widget(hint); return painter.widget(hint).size();
} }
let region = tex.size.align(self.align); let region = tex.size.align(self.align);
let within = region.within(&painter.region()); let within = region.within(&painter.region());
@@ -141,9 +141,10 @@ impl Text {
} }
impl Widget for Text { impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
self.update_buf(); self.update_buf();
self.view.draw(painter) let size = self.view.draw(painter);
painter.set_size(size);
} }
fn requires_exact_region(&self) -> bool { fn requires_exact_region(&self) -> bool {