Make alignment a widget property
This commit is contained in:
1 parent
8220a78d4a
commit
d3b0ebf90c
21 files changed
+823
-300
No files matched your search
+68
-16
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user