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

+99 -28
View File
@@ -1,8 +1,8 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, Holds, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, Widgets,
Axis, Holds, Len, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer,
TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, Widgets,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
PrimitiveKind, TexturePrimitive,
@@ -99,16 +99,7 @@ impl<'a> Painter<'a> {
/// Draws a widget within this widget's region.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_at(id, UiRegion::FULL)
}
/// Draws a widget somewhere within this one.
pub fn widget_within<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region)
self.widget_within(id, UiRegion::FULL)
}
/// What a widget's rules declare its lengths to be, which whoever draws
@@ -127,19 +118,45 @@ impl<'a> Painter<'a> {
self.state.undraw_rec(id.id(), self.rsc);
}
/// `region` in this widget's own coordinates, and with the child's
/// declared lengths still to be taken.
fn widget_at<'s, W: ?Sized>(
/// Draws a widget somewhere within this one. `region` is in this widget's
/// own coordinates, and the child's declared lengths are still to be
/// taken from it. Where the child's drawing sits inside what it is given
/// is the child's alignment, applied where the child is drawn, so a
/// container positions a child either by handing it a box of exactly its
/// length or by leaving it room and letting its alignment decide.
pub fn widget_within<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, None)
}
/// Draws a widget with an alignment chosen by its container rather than
/// the widget's property. Containers use this when the box they hand down
/// already expresses the size they report around the child.
pub fn widget_aligned<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
align: RegionAlign,
) -> DrawResult<'s, 'a, W> {
self.widget_at(id, region, Some(align))
}
fn widget_at<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
align_override: Option<RegionAlign>,
) -> DrawResult<'s, 'a, W> {
let region_node = self.rsc.widgets().is_region_node(id.id());
let declared = self.declared_lens(id);
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.id()));
// Composing `FULL` through a box is not quite the identity in f32,
// so a child with nothing declared keeps the box it would have had.
let local = match declared.iter().any(Option::is_some) {
true => declared_box(region, declared),
true => declared_box(region, declared, align),
false => region,
};
let within = match local == UiRegion::FULL {
@@ -161,7 +178,10 @@ impl<'a> Painter<'a> {
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer),
};
let answers_offer = self.at_offer && local == offer;
let size = self.state.draw_inner(
// The answer and what it holds for, both about the box asked in. The
// child's record may say something else once its drawing has been
// placed: a drawing made again in its placed box holds for that box.
let (size, holds) = self.state.draw_inner(
id.id(),
within,
DrawInfo {
@@ -173,19 +193,18 @@ impl<'a> Painter<'a> {
mask: self.mask,
offer,
offered_px: self.px_within_offer(offer),
align: align_override,
},
None,
self.rsc,
);
let active = self.state.active.get_mut(&id.id()).unwrap();
active.declared = declared;
if answers_offer {
active.answer = (active.size, active.holds);
self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds);
}
// Whatever the child's drawing holds for keeps this one to the boxes
// Whatever the child's answer holds for keeps this one to the boxes
// that give the child a length inside it.
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
*under = under.and(active.holds[axis as usize].through(local.axis(axis).len()));
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
}
DrawResult {
child: id,
@@ -232,7 +251,8 @@ impl<'a> Painter<'a> {
region: UiRegion,
) -> Option<Len> {
let declared = self.declared_lens(child);
let local = declared_box(region, declared);
let align = self.rsc.widgets().alignment(child.id());
let local = declared_box(region, declared, align);
let within = local.within(&self.region);
let first_ask = self.offer(child.id());
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
@@ -326,6 +346,20 @@ impl<'a> Painter<'a> {
self.region
}
/// Where this widget sits in a box longer than the length it takes. A
/// widget that positions its own content reads it to place that content
/// the way the box around it would have placed the widget.
pub fn alignment(&self) -> RegionAlign {
self.rsc.widgets().alignment(self.id)
}
/// The part of this widget's box that something of `size` takes, at the
/// near edge. A container that reports one child's size gives every child
/// this, so what it draws is inside what it says it occupies.
pub fn box_of(&self, size: Size) -> UiRegion {
placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2])
}
/// This widget's box in pixels. Reading it makes the drawing one that
/// holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> Vec2 {
@@ -455,15 +489,52 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<Len>; 2]
})
}
/// The box a drawing occupies: the size the widget reported, on the side of
/// the box it was asked in that its alignment says. An axis reported as a
/// share fills, because a share is a length only to whoever divides one, and
/// whoever did is the one that handed down this box. A declared axis is
/// left alone too: `declared_box` already placed it, in the parent's box,
/// and the rule's length is what the widget reports there.
///
/// A reported fraction is a fraction of the box the widget drew in, where a
/// declared one is a fraction of the box its parent handed down -- a span
/// reporting `rel(1.0)` means all of what it was given, whatever that was a
/// fraction of. So this scales by the box rather than composing into it.
pub(crate) fn placed_box(
region: UiRegion,
size: Size,
align: RegionAlign,
declared: [Option<Len>; 2],
) -> UiRegion {
let mut placed = region;
for (axis, declared) in AXES.into_iter().zip(declared) {
let reported = size.axis(axis);
if reported.leftover != 0.0 || declared.is_some() {
continue;
}
let span = placed.axis_mut(axis);
let len = span.len().scale(reported.rel) + UiScalar::px(reported.px);
span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len;
}
placed
}
/// Takes a widget's declared lengths in the box `region` is given in, since a
/// fraction of a length means a fraction of that one. A caller that already
/// reserved the space hands back the same length, so this is the identity
/// for it.
fn declared_box(mut region: UiRegion, declared: [Option<Len>; 2]) -> UiRegion {
/// fraction of a length means a fraction of that one, and puts what is left
/// over on the side its alignment says. A caller that already reserved the
/// space hands back the same length, so this is the identity for it.
pub(crate) fn declared_box(
mut region: UiRegion,
declared: [Option<Len>; 2],
align: RegionAlign,
) -> UiRegion {
for (axis, len) in AXES.into_iter().zip(declared) {
let Some(len) = len else { continue };
let span = region.axis_mut(axis);
span.end = span.start + UiScalar::new(len.rel, len.px);
let len = UiScalar::new(len.rel, len.px);
span.start += (span.len() - len).scale(align.axis(axis).rel());
span.end = span.start + len;
}
region
}