Files
iris/core/src/widget/widgets.rs
T
iris-ai 05e6ced31d Hold a request in the arena its nodes are allocated in
A size request was a second expression shape beside the one the layout
pass already has. `SizeRequest` held `Sum`/`Min`/`Max` over `Arc` pairs;
`RequestArena` held the same three operators as `Node { op, a, b }` in a
`Vec`, with the same fold over `independent_order` written a second time,
and `import` walked the first rebuilding it as the second.

There is one node type now. An expression is the pass's nodes in an arena
of its own that lasts as long as the rule holding it, and `import` grafts
those nodes into the pass's arena, resolving fractions as they land. The
fold is `Nodes::combine`, which both the builder and the importer call.

So the `Arc` goes, and no refcount replaces it: nothing shares a request
and nothing outside widget code holds one. A plain length stays inline,
so `size_of::<SizeRule>()` is 40 either way and only an actual expression
allocates. A node's operand is a number within its own arena, and
`RequestedLen` -- the only form that leaves one -- is that plus the epoch
saying which pass numbered it, so the epoch is now checked once where a
handle comes back in rather than at every level of the walk it starts.

`SizeRule::at_least`/`at_most` were the only clones of a request in the
framework, and both read a rule out, moved one end of its bound, and
wrote it back into the slot it came from. `Widgets::edit_bound` does it
where it sits, so nothing copies an expression to cap it.

`SizeRequest` grew a `Display`, since the shrinker prints one and a
derived `Debug` of an arena is not something a tree can be rebuilt from:
`min(30 px;1 leftover;, 2 leftover;)<0.5 rel;`.

Measured, medians of three release runs under `perf stat -e
instructions:u`, each set within 0.005% of its median: bounds_cost
MODE=cap FRAMES=2000 is 5.665B against 5.743B (-1.34%), and
revision_cost resize ROWS=40 FRAMES=500 is 4.855B against 4.893B
(-0.79%).

Format, workspace clippy under -D warnings with and without
layout-diagnostics, 206 ordinary and 210 diagnostic tests, the cold dump
byte-identical to 2ac0843 across all 34,986 boxes, 400 depth-5 trees in
64.24s, 1,000 depth-6 in 160.35s, 2,000 depth-4 in 298.82s, and 400
depth-5 trees in each of the three deferred-request corpora in 205.42s.
2026-09-20 21:07:19 -04:00

254 lines
8.1 KiB
Rust

use std::sync::mpsc::{Receiver, Sender, channel};
use crate::{
Axis, AxisAlign, Bound, IdLike, Len, RegionAlign, SizeRequest, SizeRule, SizeRules,
StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
};
pub struct Widgets {
pub needs_redraw: HashSet<WidgetId>,
vec: SlotVec<WidgetData>,
send: Sender<WidgetId>,
recv: Receiver<WidgetId>,
pub(crate) waiting: HashSet<WidgetId>,
}
impl Widgets {
pub fn new() -> Self {
let (send, recv) = channel();
Self {
needs_redraw: Default::default(),
vec: Default::default(),
waiting: Default::default(),
send,
recv,
}
}
pub fn has_updates(&self) -> bool {
!self.needs_redraw.is_empty()
}
/// Marks this widget for the next frame to draw again, with nothing about
/// it changed. Taking a widget mutably marks it too, which is the ordinary
/// content-change signal; this is for a change the borrow cannot express,
/// and for asking for the same tree over again.
pub fn mark_for_redraw(&mut self, id: impl IdLike) {
self.needs_redraw.insert(id.id());
}
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
Some(self.vec.get(id)?.widget.as_ref())
}
pub fn get_dyn_mut(&mut self, id: WidgetId) -> Option<&mut dyn Widget> {
self.needs_redraw.insert(id);
Some(self.vec.get_mut(id)?.widget.as_mut())
}
/// get_dyn but dynamic borrow checking of widgets
/// lets you do recursive (tree) operations, like the painter does
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> DynBorrower<'a, dyn Widget> {
// SAFETY: must guarantee no other mutable references to this widget exist
// done through the borrow variable
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
if data.borrowed {
panic!("tried to mutably borrow the same widget twice");
}
DynBorrower::new(data.widget.as_mut(), &mut data.borrowed)
}
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
where
I::Widget: Sized + Widget,
{
self.get_dyn(id.id())?.as_any().downcast_ref()
}
pub fn get_mut<I: IdLike>(&mut self, id: &I) -> Option<&mut I::Widget>
where
I::Widget: Sized + Widget,
{
self.get_dyn_mut(id.id())?.as_any_mut().downcast_mut()
}
pub fn add_strong<W: Widget>(&mut self, widget: W) -> StrongWidget<W> {
let id = self.vec.add(WidgetData::new(widget));
StrongWidget::new(id, self.send.clone())
}
pub fn add_weak<W: Widget>(&mut self, widget: W) -> WeakWidget<W> {
let id = self.vec.add(WidgetData::new(widget));
self.waiting.insert(id);
WeakWidget::new(id)
}
#[track_caller]
pub fn upgrade<W: ?Sized>(&mut self, rf: WeakWidget<W>) -> StrongWidget<W> {
if !self.waiting.remove(&rf.id()) {
let label = self.label(rf);
let id = rf.id();
panic!(
"widget '{label}' ({id:?}) was already added\ncannot add a widget twice; consider creating two"
)
}
StrongWidget::new(rf.id(), self.send.clone())
}
pub fn data(&self, id: impl IdLike) -> Option<&WidgetData> {
self.vec.get(id.id())
}
pub fn label(&self, id: impl IdLike) -> &String {
&self.data(id.id()).unwrap().label
}
/// useful for debugging
pub fn set_label(&mut self, id: impl IdLike, label: String) {
self.data_mut(id.id()).unwrap().label = label;
}
/// Whether this widget owns a movable retained region.
pub fn is_region_node(&self, id: impl IdLike) -> bool {
self.data(id).unwrap().region_node
}
/// Chooses whether this widget's retained drawing has one movable region
/// of its own. Changing the boundary redraws the subtree once so every
/// primitive names the right coordinate space.
pub fn set_region_node(&mut self, id: impl IdLike, region_node: bool) {
let id = id.id();
let data = self.data_mut(id).unwrap();
if data.region_node == region_node {
return;
}
data.region_node = region_node;
self.needs_redraw.insert(id);
}
/// The length rules whoever draws this widget applies to its box.
pub fn size_rules(&self, id: impl IdLike) -> &SizeRules {
&self.data(id).unwrap().size
}
/// Sets one axis's rule. The widget is marked rather than its parent
/// because the parent is not known here; `redraw` escalates a changed
/// declared length to whoever resolves it.
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
let id = id.id();
let data = self.data_mut(id).unwrap();
if data.size[axis] == rule {
return;
}
data.size[axis] = rule;
self.needs_redraw.insert(id);
}
/// Changes the preferred length, leaving the bounds beside it alone.
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<SizeRequest>) {
let id = id.id();
let request = Some(len.into());
let rule = &mut self.data_mut(id).unwrap().size[axis];
if rule.request == request {
return;
}
rule.request = request;
self.needs_redraw.insert(id);
}
/// Puts a floor under this widget's length on one axis, keeping a cap it
/// already had and the preferred length beside it.
pub fn set_min_len(&mut self, id: impl IdLike, axis: Axis, min: Len) {
self.edit_bound(id.id(), axis, |bound| bound.min = Some(min));
}
/// Puts a cap over it, keeping a floor it already had.
pub fn set_max_len(&mut self, id: impl IdLike, axis: Axis, max: Len) {
self.edit_bound(id.id(), axis, |bound| bound.max = Some(max));
}
/// Edits one axis's bound where it sits, rather than reading the whole
/// rule out and writing it back: an expression beside the bound is not
/// this edit's business, and copying it to move one end would be the
/// only thing here that ever copies one.
fn edit_bound(&mut self, id: WidgetId, axis: Axis, edit: impl FnOnce(&mut Bound)) {
let bound = &mut self.data_mut(id).unwrap().size[axis].bound;
let before = *bound;
edit(bound);
if *bound != before {
self.needs_redraw.insert(id);
}
}
/// Where this widget sits in a box longer than the length it takes.
pub fn alignment(&self, id: impl IdLike) -> RegionAlign {
self.data(id).unwrap().align
}
/// Sets one axis's alignment. Which box a widget ends up in is its
/// parent's to decide, so this is escalated the way a length rule is.
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
let id = id.id();
let data = self.data_mut(id).unwrap();
if data.align[axis] == align {
return;
}
data.align[axis] = align;
self.needs_redraw.insert(id);
}
/// Both axes at once.
pub fn set_size_rules(
&mut self,
id: impl IdLike,
x: impl Into<SizeRule>,
y: impl Into<SizeRule>,
) {
let id = id.id();
self.set_size_rule(id, Axis::X, x.into());
self.set_size_rule(id, Axis::Y, y.into());
}
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
self.vec.get_mut(id.id())
}
pub fn free_next(&mut self) -> Option<WidgetId> {
let next = self.recv.try_recv().ok()?;
self.vec.free(next);
Some(next)
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.vec.len()
}
}
impl Default for Widgets {
fn default() -> Self {
Self::new()
}
}
impl<I: IdLike> std::ops::Index<I> for Widgets
where
I::Widget: Sized + Widget,
{
type Output = I::Widget;
fn index(&self, id: I) -> &Self::Output {
self.get(&id).unwrap()
}
}
impl<I: IdLike> std::ops::IndexMut<I> for Widgets
where
I::Widget: Sized + Widget,
{
fn index_mut(&mut self, id: I) -> &mut Self::Output {
self.get_mut(&id).unwrap()
}
}