Make alignment a widget property

This commit is contained in:
iris-ai committed 2026-09-15 23:12:16 -04:00
1 parent 8220a78d4a
commit d3b0ebf90c
21 files changed
+823 -300

No files matched your search

+68 -16
View File
@@ -11,6 +11,10 @@ use std::collections::HashMap;
/// The declared lengths of one widget carrying a size rule, by axis.
pub type Lens = [Option<Len>; 2];
/// Where one widget carrying an alignment sits, by axis. `None` uses the
/// centered default.
pub type Aligns = [Option<AxisAlign>; 2];
/// What a test changes between two trees grown from the same seed, so the
/// warm one can be mutated and the cold one grown that way to begin with.
#[derive(Default)]
@@ -19,6 +23,12 @@ pub struct Edits {
pub sizes: HashMap<usize, Lens>,
/// Which children a span has, by the order the spans were made.
pub spans: HashMap<usize, SpanEdit>,
/// Alignments, by the order they were put on.
pub aligns: HashMap<usize, Aligns>,
/// Which widgets own a movable region, by the order they were offered
/// one. Region nodes change what a move writes and how deep a primitive's
/// chain is, so a tree that never grows one leaves both untested.
pub nodes: HashMap<usize, bool>,
}
#[derive(Default, Clone)]
@@ -76,6 +86,8 @@ const WORDS: &str = "Wrapping shapes one source into as many lines as the box \
pub struct Tree {
pub ids: Vec<WidgetId>,
pub sized: Vec<WidgetId>,
pub aligned: Vec<WidgetId>,
pub nodes: Vec<WidgetId>,
pub spans: Vec<Spanned>,
pub scrolls: Vec<WeakWidget<Scroll>>,
/// Children a `SpanEdit` took out, held so that dropping the last share
@@ -180,26 +192,32 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
fn align(&mut self) -> Align {
let mut axis = || match self.rng.below(4) {
0 => None,
1 => Some(AxisAlign::Neg),
2 => Some(AxisAlign::Center),
_ => Some(AxisAlign::Pos),
1 => Some(AxisAlign::NEG),
2 => Some(AxisAlign::CENTER),
_ => Some(AxisAlign::POS),
};
let (mut x, y) = (axis(), axis());
// Aligning on neither axis is just another transparent wrapper and
// would leave this branch unexercised.
// Aligning on neither axis leaves the branch unexercised.
if x.is_none() && y.is_none() {
x = Some(AxisAlign::Center);
x = Some(AxisAlign::CENTER);
}
Align { x, y }
}
/// A declared size over half the tree, kept where a test can change it.
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
if !self.rng.chance() {
// A rule is a property now, so a node already carrying one would take
// a second entry in `sized` -- and two edits naming one widget settle
// in the order they are applied, which is grow order cold and edit
// order warm. One entry per widget instead. Both draws are taken
// whatever is decided, and the decision is grow order alone, so the
// two trees consume the same random stream.
let take = self.rng.chance();
let lens = [self.len(), self.len()];
if !take || self.tree.sized.contains(&inner.id()) {
return inner;
}
let idx = self.tree.sized.len();
let lens = [self.len(), self.len()];
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
let id = inner.id();
self.rsc
@@ -210,6 +228,41 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
inner
}
/// An alignment over some of the tree, kept where a test can change it.
/// One entry per widget for the reason `sized` gives.
fn aligned(&mut self, inner: StrongWidget) -> StrongWidget {
let align = self.align();
let align = [align.x, align.y];
if self.tree.aligned.contains(&inner.id()) {
return inner;
}
let idx = self.tree.aligned.len();
let align = self.edits.aligns.get(&idx).copied().unwrap_or(align);
let id = inner.id();
let widgets = &mut self.rsc.ui_mut().widgets;
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
widgets.set_alignment(id, axis, align.unwrap_or_default());
}
self.tree.aligned.push(id);
inner
}
/// A movable region of its own over some of the tree. What it changes is
/// how a move is written and how long a primitive's chain is, neither of
/// which any other branch here varies.
fn noded(&mut self, inner: StrongWidget) -> StrongWidget {
let take = self.rng.below(4) == 0;
if self.tree.nodes.contains(&inner.id()) {
return inner;
}
let idx = self.tree.nodes.len();
let take = self.edits.nodes.get(&idx).copied().unwrap_or(take);
let id = inner.id();
self.rsc.ui_mut().widgets.set_region_node(id, take);
self.tree.nodes.push(id);
inner
}
fn node(&mut self, depth: usize) -> StrongWidget {
if depth == 0 {
return self.leaf();
@@ -220,6 +273,7 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
// else here does, and gives its child a box longer than its own.
let inner = self.node(depth - 1);
let inner = self.sized(inner);
let inner = self.noded(inner);
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
let id = Scroll::new(inner, axis).add(self.rsc);
self.tree.scrolls.push(id);
@@ -246,17 +300,13 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
if positioned == 1 {
let inner = self.node(depth - 1);
let inner = self.sized(inner);
let id = Aligned {
inner,
align: self.align(),
}
.add_strong(self.rsc);
self.tree.ids.push(id.id());
return id;
let inner = self.noded(inner);
return self.aligned(inner);
}
if self.rng.below(4) == 0 {
let inner = self.node(depth - 1);
let inner = self.sized(inner);
let inner = self.noded(inner);
// Each side its own, since a padding that is the same all round
// hides anything that treats one edge differently from another.
let mut side = || self.rng.below(24) as f32;
@@ -274,7 +324,9 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
let mut children = Vec::with_capacity(grown);
for _ in 0..grown {
let child = self.node(depth - 1);
children.push(self.sized(child));
let child = self.sized(child);
let child = self.noded(child);
children.push(child);
}
if self.rng.chance() {
let id = Stack {
-43
View File
@@ -1,43 +0,0 @@
use crate::prelude::*;
pub struct Aligned {
pub inner: StrongWidget,
pub align: Align,
}
impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) -> Size {
let known = match self.align.tuple() {
(Some(_), Some(_)) => painter
.known_len(&self.inner, Axis::X, UiRegion::FULL)
.zip(painter.known_len(&self.inner, Axis::Y, UiRegion::FULL))
.map(|(x, y)| Size { x, y }),
(Some(_), None) => painter
.known_len(&self.inner, Axis::X, UiRegion::FULL)
.map(|x| Size {
x,
y: Len::LEFTOVER,
}),
(None, Some(_)) => painter
.known_len(&self.inner, Axis::Y, UiRegion::FULL)
.map(|y| Size {
x: Len::LEFTOVER,
y,
}),
(None, None) => Some(Size::LEFTOVER),
};
// Drawn where it may be too big only when the aligned axes are not
// already known, then given its aligned box once its size is known.
let had_size = known.is_some();
let size =
known.unwrap_or_else(|| painter.widget_within(&self.inner, UiRegion::FULL).size());
let region = match self.align.tuple() {
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
(Some(x), None) => UiRegion::new(size.x.apply_leftover().align(x), UiSpan::FULL),
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_leftover().align(y)),
(None, None) => UiRegion::FULL,
};
let placed = painter.widget_within(&self.inner, region).size();
if had_size { placed } else { size }
}
}
-2
View File
@@ -1,4 +1,3 @@
mod align;
mod layer;
mod offset;
mod pad;
@@ -6,7 +5,6 @@ mod scroll;
mod span;
mod stack;
pub use align::*;
pub use layer::*;
pub use offset::*;
pub use pad::*;
+1 -1
View File
@@ -8,7 +8,7 @@ pub struct Pad {
impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) -> Size {
let inner = painter
.widget_within(&self.inner, self.padding.region())
.widget_aligned(&self.inner, self.padding.region(), RegionAlign::NEAR)
.size();
Size {
x: Len {
+23 -15
View File
@@ -14,13 +14,9 @@ impl Widget for Scroll {
let container_len = painter.px_len(self.axis);
// Draw in the whole container only when its scrolling-axis length is
// not already known, then draw it at the scrolled offset.
let (answer_len, measured) = match painter.known_len(&self.inner, self.axis, UiRegion::FULL)
{
Some(len) => (len, None),
None => {
let size = painter.widget_within(&self.inner, UiRegion::FULL).size();
(size.axis(self.axis), Some(size))
}
let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) {
Some(len) => len,
None => painter.widget(&self.inner).size().axis(self.axis),
};
let content = answer_len.apply_leftover();
self.container_len = container_len;
@@ -30,21 +26,33 @@ impl Widget for Scroll {
self.amt = self.content_len - self.container_len;
}
self.update_amt();
// Content of a fixed length that fits sits at the start of any box
// it fits in; one scrolled part way sits where it is until the box
// shrinks past what is left of it. Kept to the end, it moves with
// every length.
if content.rel == 0.0 && self.content_len <= self.container_len {
let align = painter.alignment().axis(self.axis);
// Content of a fixed length that fits sits at the start of any box it
// fits in -- but only anchored there. Anywhere else it is a part of
// the room left over, so it moves with every length the box takes and
// the drawing holds for that length alone. One scrolled part way sits
// where it is until the box shrinks past what is left of it. Kept to
// the end, it moves with every length.
if content.rel == 0.0 && self.content_len <= self.container_len && align == AxisAlign::NEG {
painter.holds(self.axis, self.content_len..=f32::INFINITY);
} else if content.rel == 0.0 && !self.snap_end {
let left = self.content_len - self.amt;
painter.holds(self.axis, f32::NEG_INFINITY..=left);
}
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
// Content shorter than the viewport has room to sit in, and where it
// sits is this widget's own alignment -- the same property that would
// have placed the whole scroll in a box longer than it.
let slack = (self.container_len - self.content_len).max(0.0);
let anchor = slack * align.rel();
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, anchor - self.amt, 0.0));
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
let placed = painter.widget_within(&self.inner, region).size();
measured.unwrap_or_else(|| Size::from_axis(self.axis, answer_len, placed.axis(!self.axis)))
painter.widget_aligned(&self.inner, region, RegionAlign::NEAR);
// What it occupies is its box, on both axes: it clips its content to
// that box, so it can neither take less of one nor honestly ask for
// more. The content's length is what it scrolls through, not what it
// is.
Size::LEFTOVER
}
}
+9 -7
View File
@@ -13,18 +13,20 @@ impl Widget for Stack {
StackSize::Default => None,
StackSize::Child(i) => Some(i),
};
let mut size = Size::default();
// Whichever child sizes the stack decides the box every child gets.
// The stack reports that size, so a child given a longer box would
// draw outside what the stack says it occupies.
let size = match sizing.and_then(|i| self.children.get(i)) {
Some(child) => painter.widget(child).size(),
None => Size::LEFTOVER,
};
let region = painter.box_of(size);
for (i, child) in self.children.iter().enumerate() {
match i {
0 => painter.child_layer(),
_ => painter.next_layer(),
}
let drawn = painter.widget(child);
// Only the child that sizes the stack is read, so the others
// changing size does not redraw it.
if sizing == Some(i) {
size = drawn.size();
}
painter.widget_aligned(child, region, RegionAlign::NEAR);
}
size
}
+14 -5
View File
@@ -12,14 +12,23 @@ widget_trait! {
}
}
fn align(self, align: impl Into<Align>) -> impl WidgetFn<Rsc, Aligned> {
move |state| Aligned {
inner: self.add_strong(state),
align: align.into(),
fn align(self, align: impl Into<Align>) -> impl WidgetIdFn<Rsc, WL::Widget> {
// An axis left out keeps whatever it had, which is centered unless
// something else set it.
let align = align.into();
move |state| {
let id = self.add(state);
let widgets = &mut state.ui_mut().widgets;
for (axis, align) in [(Axis::X, align.x), (Axis::Y, align.y)] {
if let Some(align) = align {
widgets.set_alignment(id, axis, align);
}
}
id
}
}
fn center(self) -> impl WidgetFn<Rsc, Aligned> {
fn center(self) -> impl WidgetIdFn<Rsc, WL::Widget> {
self.align(Align::CENTER)
}