Resolve deferred size comparisons before allocating span slots

This commit is contained in:
iris-ai committed 2026-09-20 16:50:18 -04:00
1 parent de1eb7e406
commit 8780b40bb7
23 files changed
+1471 -130

No files matched your search

+11
View File
@@ -51,6 +51,8 @@ pub struct ActiveData {
/// An owned mask holds one reference independently of its primitives.
pub mask_region: Option<UiRegion>,
pub children: Vec<WidgetId>,
pub request_deps: Vec<WidgetId>,
pub(crate) scratch: DrawScratch,
/// The movable region its primitives are positioned through: its own when
/// opted in, otherwise the nearest ancestor's.
pub move_idx: MoveIdx,
@@ -101,3 +103,12 @@ pub struct Answer {
pub size: Size,
pub holds: LayoutHolds,
}
#[derive(Debug, Default)]
pub(crate) struct DrawScratch {
pub children: Vec<WidgetId>,
pub size_deps: Vec<WidgetId>,
pub under: Vec<(WidgetId, LayoutHolds)>,
pub requests: Vec<crate::RequestedLen>,
pub lengths: Vec<crate::Px>,
}
+1 -1
View File
@@ -20,7 +20,7 @@ pub use active::*;
pub use holds::*;
pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike};
pub use place::{PlaceDesc, PlaceDescAxis, RetainedPrimitive};
pub use place::{PlaceDesc, PlaceDescAxis, PlaceFit, RetainedPrimitive};
pub use render_state::*;
#[derive(Default)]
+161 -12
View File
@@ -2,8 +2,9 @@
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, Bounds, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign,
Rel, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
Rel, RenderedText, RequestArena, RequestedLen, RetainedPrimitive, Size, SizeRequests,
StrongWidget, TextAttrs, TextBuffer, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2,
Weight, WidgetId, Widgets,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
@@ -41,6 +42,8 @@ pub struct Painter<'a> {
pub(super) children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>,
pub(super) request_deps: Vec<WidgetId>,
pub(super) scratch: crate::DrawScratch,
/// What this draw itself reads, as against what its children's drawings
/// hold for: every window and every length of its own region until it
/// reads one, then that one unless it says otherwise, and the rel base or
@@ -62,6 +65,106 @@ pub struct Painter<'a> {
}
impl<'a> Painter<'a> {
/// Reuses this widget's allocation buffers across draws. Nested painters
/// have independent buffers, so discovering a child cannot overwrite them.
pub fn with_requests<T>(
&mut self,
f: impl FnOnce(&mut Self, &mut Vec<RequestedLen>, &mut Vec<Px>) -> T,
) -> T {
let mut requests = std::mem::take(&mut self.scratch.requests);
let mut lengths = std::mem::take(&mut self.scratch.lengths);
requests.clear();
lengths.clear();
let result = f(self, &mut requests, &mut lengths);
self.scratch.requests = requests;
self.scratch.lengths = lengths;
result
}
/// Discovers a composable request without painting a provisional box.
pub fn size_request<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
) -> Option<RequestedLen> {
self.request_deps.push(child.id());
if self.rsc.widgets().size_rules(child.id())[axis].bound() == crate::Bound::ANY
&& let Some(len) = self.size_hint(child, axis)
{
return Some(len.into());
}
let rel_base = self.rel_base(axis);
let mut requests = SizeRequests {
arena: &mut self.state.requests,
measured: None,
widgets: self.rsc.widgets(),
dependencies: &mut self.request_deps,
rel_base,
};
let request = requests.widget(child, axis)?;
// An intrinsic fixed answer still draws in the offered room and is
// moved afterwards. Only a declaration or a share chooses its ask box.
(request.has_leftover()
|| matches!(
self.rsc.widgets().size_rules(child.id())[axis],
crate::SizeRule::Request(_)
))
.then_some(request)
}
/// Completes discovery after a child was measured. Only this call may use
/// drawn answers: before the ask they could belong to an obsolete box.
pub fn measured_request<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
len: LayoutLen,
) -> RequestedLen {
let rel_base = self.rel_base(axis);
let bound = self.rsc.widgets().size_rules(child.id())[axis].bound();
let mut requests = SizeRequests {
arena: &mut self.state.requests,
measured: Some(&self.state.active),
widgets: self.rsc.widgets(),
dependencies: &mut self.request_deps,
rel_base,
};
match requests.widget(child, axis) {
Some(request) if request.linear().is_none() && request.has_leftover() => request,
_ if len.leftover > Weight::ZERO => requests.bounded(len.into(), bound),
_ => len.into(),
}
}
/// A deferred comparison reads this window when the allocation is solved.
pub fn allocate(
&mut self,
requests: &[RequestedLen],
room: Len,
axis: Axis,
output: &mut Vec<Px>,
) {
let window = self.window[axis];
self.own[axis].window = self.own[axis].window.and(Holds::at(window));
output.clear();
output.extend(
self.state
.requests
.allocate(requests, room.to_px(window), window),
)
}
pub fn minimum_request(&mut self, request: &RequestedLen, axis: Axis) -> Len {
match request.linear() {
Some(len) => len.without_leftover(),
None => {
let window = self.window[axis];
self.own[axis].window = self.own[axis].window.and(Holds::at(window));
Len::from_parts(Rel::ZERO, self.state.requests.minimum(*request, window))
}
}
}
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region);
@@ -195,9 +298,15 @@ impl<'a> Painter<'a> {
declared,
bounds,
holds: ask_holds,
} = self
.placing()
.ask(self.rsc.widgets(), self.window, id.id(), offer);
inputs,
} = self.placing().ask(
self.rsc.widgets(),
&mut self.state.requests,
self.window,
id.id(),
offer,
);
self.own = self.own.and(inputs);
let region_node = self.rsc.widgets().is_region_node(id.id());
#[cfg(feature = "layout-diagnostics")]
if region_node {
@@ -422,9 +531,10 @@ impl<'a> Painter<'a> {
/// worth anything, since reading one is also what makes its own size
/// depend on it.
pub fn has_exact_size(&self, axis: Axis) -> bool {
self.rsc.widgets().size_rules(self.id)[axis]
.exact()
.is_some()
matches!(
self.rsc.widgets().size_rules(self.id)[axis],
crate::SizeRule::Exact(_) | crate::SizeRule::Request(_)
)
}
/// This widget's own box in pixels. Reading it makes the drawing one
@@ -667,6 +777,9 @@ impl Widgets {
/// share included, since a share is a length only to whoever divides one,
/// and that is the parent rather than this widget.
fn exact_len(&self, id: WidgetId, axis: Axis) -> Option<LayoutLen> {
if matches!(self.size_rules(id)[axis], crate::SizeRule::Request(_)) {
return None;
}
self.size_rules(id)[axis].exact().or_else(|| {
// A hint still narrows the box where no rule does, which is how a
// widget with a natural pixel size -- an image, a gap -- gets that
@@ -706,6 +819,9 @@ pub(super) struct Ask {
/// drawing it is part of. Kept on the widget asked about rather than on
/// the asker because the root has no asker.
pub holds: LayoutHolds,
/// Inputs read against the parent before declarations choose a new base.
/// These belong to the asker; the widget's own holds describe its output box.
pub inputs: LayoutHolds,
}
impl Placing {
@@ -719,6 +835,7 @@ impl Placing {
pub(super) fn ask(
&self,
widgets: &Widgets,
requests: &mut RequestArena,
window: PxVec2,
id: WidgetId,
mut place: PlaceDesc,
@@ -726,10 +843,33 @@ impl Placing {
let align = widgets.alignment(id);
let rules = widgets.size_rules(id);
let mut holds = LayoutHolds::ANY;
let declared = widgets.declared_lens(id);
let mut inputs = LayoutHolds::ANY;
let mut declared = widgets.declared_lens(id);
let mut bounds = Bounds::ANY;
for axis in Axis::BOTH {
let base = place.base(axis, self.rel_base);
if let crate::SizeRule::Request(request) = &rules[axis] {
inputs[axis].rel_base = Some(self.rel_base[axis]);
inputs[axis].region_len = Some(self.region[axis].len());
inputs[axis].window = Holds::at(window[axis]);
let offer = place.of(self.region, align)[axis].len();
let len = if place[axis].fit == crate::PlaceFit::Allocated {
offer
} else {
let request = requests.import(request, base);
let len = requests
.allocate(&[request], offer.to_px(window[axis]), window[axis])
.next()
.unwrap();
holds[axis].window = Holds::at(window[axis]);
Len::from_parts(Rel::ZERO, len)
};
let len = Len::from_parts(Rel::ZERO, len.to_px(window[axis]));
place[axis].rel_base = RelBase::Len(len);
declared[axis] = Some(len);
holds[axis].rel_base = Some(len);
continue;
}
// A share fills what the pixels and fraction beside it leave of
// the box and overflows where they are longer, which is the rule
// a span follows with one child. Only the overflow is a box of
@@ -748,7 +888,15 @@ impl Placing {
// anything. Resolved here because only the ask knows the rel base
// a fraction in it is of. `MaxSize` is the box version, and it is
// a widget because a widget is drawn again when its box changes.
bounds[axis] = rules[axis].bound().within_len(base);
let bound = rules[axis].bound();
if [bound.min, bound.max]
.into_iter()
.flatten()
.any(|len| len.rel != Rel::ZERO)
{
inputs[axis].rel_base = Some(self.rel_base[axis]);
}
bounds[axis] = bound.within_len(base);
}
let (rel_base, region) =
place.rel_base_and_region(self.region, self.rel_base, declared, align);
@@ -759,6 +907,7 @@ impl Placing {
declared,
bounds,
holds,
inputs,
}
}
@@ -785,7 +934,7 @@ impl Placing {
// decided, and a parent that divides one has already given the share
// whatever it was owed. Only an offer -- a box with the answer still
// to be placed inside it -- is a box a share reads.
if place[axis].fills {
if place[axis].fit.fills() {
return (None, Holds::ANY);
}
// A share with nothing beside it is the box whatever the box is, so
@@ -838,7 +987,7 @@ impl PlaceDesc {
let mut placed = region;
for axis in Axis::BOTH {
let reported = size[axis];
if reported.fills(declared[axis], self[axis].fills) {
if reported.fills(declared[axis], self[axis].fit.fills()) {
continue;
}
placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
+25 -5
View File
@@ -12,10 +12,24 @@ use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlaceDescAxis {
pub span: PlaceSpan,
pub fills: bool,
pub fit: PlaceFit,
pub rel_base: RelBase,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PlaceFit {
Align,
Fill,
/// The parent has already evaluated the child's size request.
Allocated,
}
impl PlaceFit {
pub fn fills(self) -> bool {
self != Self::Align
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PlaceSpan {
Within(UiSpan),
@@ -44,7 +58,13 @@ impl PlaceDescAxis {
/// it again. A container uses it where it hands back exactly what the
/// child asked for -- a row placing a child at the length it reported.
pub const fn fills(mut self) -> Self {
self.fills = true;
self.fit = PlaceFit::Fill;
self
}
/// A final allocation, including any comparisons in the child's request.
pub const fn allocated(mut self) -> Self {
self.fit = PlaceFit::Allocated;
self
}
@@ -153,7 +173,7 @@ impl UiSpan {
pub const fn within_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Within(self),
fills: false,
fit: PlaceFit::Align,
rel_base: RelBase::WithRegion,
}
}
@@ -171,7 +191,7 @@ impl UiSpan {
pub const fn shifted_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Shifted(self),
fills: false,
fit: PlaceFit::Align,
rel_base: RelBase::Inherit,
}
}
@@ -185,7 +205,7 @@ impl Len {
pub const fn as_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Sized(self),
fills: false,
fit: PlaceFit::Align,
rel_base: RelBase::Len(self),
}
}
+100 -28
View File
@@ -94,7 +94,10 @@ pub struct UiRenderState {
deferred: crate::util::HashSet<WidgetId>,
/// What the walk has left to settle, deepest last. Ordered rather than
/// searched for, so finding the next one is not a pass over the marks.
pending: std::collections::BTreeSet<(usize, WidgetId)>,
pending: std::collections::BinaryHeap<(usize, WidgetId)>,
pub(super) requests: crate::RequestArena,
changed: Vec<WidgetId>,
request_readers: HashMap<WidgetId, crate::util::HashSet<WidgetId>>,
pub moves: Moves,
}
@@ -108,6 +111,9 @@ impl UiRenderState {
slots: Default::default(),
deferred: Default::default(),
pending: Default::default(),
requests: Default::default(),
changed: Vec::new(),
request_readers: Default::default(),
moves: Default::default(),
resized: false,
}
@@ -194,6 +200,7 @@ impl UiRenderState {
weak widgets: {all:#?}"
);
}
self.requests.reset();
let root = root.into();
if self.root_changed(root) {
self.redraw_all(root, rsc);
@@ -211,8 +218,13 @@ impl UiRenderState {
let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc);
if let Some(id) = root {
let ask =
Placing::WINDOW.ask(rsc.widgets(), self.output_size, id.id(), PlaceDesc::WHOLE);
let ask = Placing::WINDOW.ask(
rsc.widgets(),
&mut self.requests,
self.output_size,
id.id(),
PlaceDesc::WHOLE,
);
let info = self.root_info(&ask, rsc.widgets().is_region_node(id.id()));
self.draw_inner(id.id(), info, None, rsc);
}
@@ -327,7 +339,19 @@ impl UiRenderState {
let mask_slot = old
.as_ref()
.and_then(|old| old.mask_region.map(|_| old.mask));
let old_children = old.map_or_else(Vec::new, |old| old.children);
let (mut old_children, textures, primitives, request_deps, mut scratch) = match old {
Some(old) => (
old.children,
old.textures,
old.primitives,
old.request_deps,
old.scratch,
),
None => Default::default(),
};
let children = std::mem::take(&mut scratch.children);
let size_deps = std::mem::take(&mut scratch.size_deps);
let under = std::mem::take(&mut scratch.under);
rsc.widgets_mut().needs_redraw.remove(&id);
let window = self.output_size;
let mut painter = Painter {
@@ -339,17 +363,19 @@ impl UiRenderState {
layer: info.layer,
own_layer: info.layer,
id,
textures: Vec::new(),
primitives: Vec::new(),
textures,
primitives,
mask_region: None,
mask_slot,
children: Vec::new(),
size_deps: Vec::new(),
children,
size_deps,
request_deps,
scratch,
// What the ask holds for is part of what the drawing holds for:
// a box the widget's own rule took past the offer was decided in
// this window, and at the root nobody else keeps that range.
own: info.ask_holds,
under: Vec::new(),
under,
answer_under: LayoutHolds::ANY,
depth: info.depth,
move_idx,
@@ -381,8 +407,10 @@ impl UiRenderState {
own,
answer_under,
children,
size_deps,
under,
mut size_deps,
request_deps,
mut scratch,
mut under,
move_idx,
layer,
own_layer: _,
@@ -402,14 +430,19 @@ impl UiRenderState {
// resolved into the rel base when the widget was asked, and resolving
// it again here would take the fraction of a fraction.
let rules = rsc.widgets().size_rules(id);
let ruled = |axis: Axis, reported: LayoutLen| match rules[axis].exact() {
None => reported,
Some(len) if len.leftover == Weight::ZERO => LayoutLen {
rel: info.rel_base[axis].rel,
px: info.rel_base[axis].px,
leftover: Weight::ZERO,
},
Some(len) => len.within_len(info.rel_base[axis]),
let ruled = |axis: Axis, reported: LayoutLen| {
if matches!(rules[axis], crate::SizeRule::Request(_)) {
return info.rel_base[axis].into();
}
match rules[axis].exact() {
None => reported,
Some(len) if len.leftover == Weight::ZERO => LayoutLen {
rel: info.rel_base[axis].rel,
px: info.rel_base[axis].px,
leftover: Weight::ZERO,
},
Some(len) => len.within_len(info.rel_base[axis]),
}
};
let mut size = Size {
x: ruled(Axis::X, size.x),
@@ -483,8 +516,8 @@ impl UiRenderState {
}
let answer_holds = own_holds.and(answer_under);
let holds = under
.into_iter()
.fold(answer_holds, |holds, (_, child)| holds.and(child));
.iter()
.fold(answer_holds, |holds, (_, child)| holds.and(*child));
debug_assert!(
holds.contains(self.output_size, info.rel_base, region),
"'{}' ({id:?}) drew in {}, outside the ranges it reported: {holds:?}",
@@ -520,6 +553,15 @@ impl UiRenderState {
}
}
for &dep in &request_deps {
self.request_readers.entry(dep).or_default().insert(id);
}
old_children.clear();
size_deps.clear();
under.clear();
scratch.children = old_children;
scratch.size_deps = size_deps;
scratch.under = under;
let active = ActiveData {
id,
placement: region,
@@ -539,6 +581,8 @@ impl UiRenderState {
primitives,
mask_region,
children,
request_deps,
scratch,
declared: info.declared,
bounds: info.bounds,
own_align: rsc.widgets().alignment(id),
@@ -585,6 +629,7 @@ impl UiRenderState {
if !active.drawn
|| active.is_region_node() != info.region_node
|| active.parent_move != info.parent_move
|| active.bounds != info.bounds
{
return None;
}
@@ -841,6 +886,11 @@ impl UiRenderState {
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
let mut active = self.active.remove(&id);
if let Some(active) = &mut active {
for dep in active.request_deps.drain(..) {
if let Some(readers) = self.request_readers.get_mut(&dep) {
readers.remove(&id);
}
}
for primitive in &active.primitives {
let mask = self.layers.free(&primitive.handle);
if mask != MaskIdx::NONE {
@@ -914,6 +964,8 @@ impl UiRenderState {
primitives: Vec::new(),
mask_region: None,
children: Vec::new(),
request_deps: Vec::new(),
scratch: Default::default(),
move_idx: info.parent_move,
declared: Declared::NONE,
bounds: Bounds::ANY,
@@ -927,6 +979,7 @@ impl UiRenderState {
}
fn clear(&mut self, rsc: &mut dyn UiRsc) {
self.request_readers.clear();
for (_, active) in self.active.drain() {
if active.drawn {
rsc.on_undraw(&active);
@@ -947,6 +1000,7 @@ impl UiRenderState {
rsc.on_remove(id);
self.remove(id, true, rsc);
self.drop_slot(id);
self.request_readers.remove(&id);
}
rsc.ui_mut().textures.free();
}
@@ -954,6 +1008,18 @@ impl UiRenderState {
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::IncrementalLayout);
self.changed.clear();
self.changed
.extend(rsc.widgets().needs_redraw.iter().copied());
while let Some(id) = self.changed.pop() {
if let Some(readers) = self.request_readers.get(&id) {
for &reader in readers {
if rsc.widgets_mut().needs_redraw.insert(reader) {
self.changed.push(reader);
}
}
}
}
// Deepest first, and strictly: a widget that cannot settle where it
// is defers to its parent rather than drawing the parent from
// inside itself. It marks the parent, stays marked, and waits here
@@ -965,21 +1031,21 @@ impl UiRenderState {
// something below is about to change it -- which is the whole class
// of defect where a widget settles inside its parent's draw, clears
// its mark there, and tells nobody its answer moved.
// The queue is that set, ordered: a mark made while the walk runs
// queues itself through `mark`. What ends the walk is still the set
// A mark made while the walk runs queues itself through `mark`.
// What ends the walk is still the set
// being spent, not the queue, so a mark that reached it another way
// cannot be left for the next frame.
loop {
for &id in rsc.widgets().needs_redraw.iter() {
if !self.deferred.contains(&id) {
let depth = self.depth(id);
self.pending.insert((depth, id));
self.pending.push((depth, id));
}
}
if self.pending.is_empty() {
break;
}
while let Some((depth, id)) = self.pending.pop_last() {
while let Some((depth, id)) = self.pending.pop() {
// Settled inside an ancestor's draw, or deferred to one,
// since the mark that queued it.
if self.deferred.contains(&id) || !rsc.widgets().needs_redraw.contains(&id) {
@@ -990,7 +1056,7 @@ impl UiRenderState {
// depth it had under the parent it left.
let now = self.depth(id);
if now != depth {
self.pending.insert((now, id));
self.pending.push((now, id));
continue;
}
#[cfg(feature = "layout-diagnostics")]
@@ -1007,7 +1073,7 @@ impl UiRenderState {
fn mark(&mut self, id: WidgetId, widgets: &mut Widgets) {
if widgets.needs_redraw.insert(id) && !self.deferred.contains(&id) {
let depth = self.depth(id);
self.pending.insert((depth, id));
self.pending.push((depth, id));
}
}
@@ -1114,7 +1180,13 @@ impl UiRenderState {
Some(parent) => self.placing_of(parent, self.active[&parent].region),
None => Placing::WINDOW,
};
let ask = at.ask(rsc.widgets(), self.output_size, id, active.asked);
let ask = at.ask(
rsc.widgets(),
&mut self.requests,
self.output_size,
id,
active.asked,
);
let active = &self.active[&id];
let declared_changed = ask.declared != active.declared;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
+8
View File
@@ -4,6 +4,7 @@ use std::any::Any;
mod data;
mod handle;
mod like;
mod request;
mod size_rule;
mod tag;
mod view;
@@ -12,6 +13,7 @@ mod widgets;
pub use data::*;
pub use handle::*;
pub use like::*;
pub use request::*;
pub use size_rule::*;
pub use tag::*;
pub use view::*;
@@ -21,6 +23,12 @@ pub trait Widget: Any {
/// Draws the widget, and returns what it used of the box it was given.
fn draw(&mut self, painter: &mut Painter) -> Size;
/// Describes an axis before painting. Return `None` when discovering it
/// needs a concrete box or work performed by `draw`.
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
self.size_hint(axis).map(|len| requests.length(len))
}
/// An exact length the widget can give without a painter or its children.
/// Optional, and saves a draw rather than changing one: a hint that
/// disagrees with the eventual draw fails a debug assertion.
+438
View File
@@ -0,0 +1,438 @@
use crate::{Axis, LayoutLen, Len, Px, StrongWidget, Weight, WidgetId, Widgets};
impl<N: crate::UiNum> From<N> for SizeRequest {
fn from(value: N) -> Self {
LayoutLen::px(value).into()
}
}
impl LayoutLen {
pub fn min(self, other: impl Into<SizeRequest>) -> SizeRequest {
SizeRequest::from(self).min(other)
}
pub fn max(self, other: impl Into<SizeRequest>) -> SizeRequest {
SizeRequest::from(self).max(other)
}
pub fn clamp(self, min: impl Into<SizeRequest>, max: impl Into<SizeRequest>) -> SizeRequest {
SizeRequest::from(self).clamp(min, max)
}
}
/// A size request before a container has divided its leftover space.
/// Comparisons keep both operands until the share is known.
#[derive(Clone, Debug, PartialEq)]
pub enum SizeRequest {
Linear(LayoutLen),
Sum(std::sync::Arc<(Self, Self)>),
Min(std::sync::Arc<(Self, Self)>),
Max(std::sync::Arc<(Self, Self)>),
}
impl From<LayoutLen> for SizeRequest {
fn from(len: LayoutLen) -> Self {
Self::Linear(len)
}
}
impl From<Len> for SizeRequest {
fn from(len: Len) -> Self {
LayoutLen::from(len).into()
}
}
impl SizeRequest {
pub fn min(self, other: impl Into<Self>) -> Self {
let other = other.into();
if let (Self::Linear(a), Self::Linear(b)) = (&self, &other)
&& let Some(order) = independent_order(*a, *b)
{
return if !order.is_gt() { self } else { other };
}
if self == other {
self
} else {
Self::Min(std::sync::Arc::new((self, other)))
}
}
pub fn max(self, other: impl Into<Self>) -> Self {
let other = other.into();
if let (Self::Linear(a), Self::Linear(b)) = (&self, &other)
&& let Some(order) = independent_order(*a, *b)
{
return if !order.is_lt() { self } else { other };
}
if self == other {
self
} else {
Self::Max(std::sync::Arc::new((self, other)))
}
}
pub fn clamp(self, min: impl Into<Self>, max: impl Into<Self>) -> Self {
self.max(min).min(max)
}
}
impl std::ops::Add for SizeRequest {
type Output = Self;
fn add(self, other: Self) -> Self {
match (self, other) {
(Self::Linear(a), Self::Linear(b)) => Self::Linear(a + b),
(a, b) => Self::Sum(std::sync::Arc::new((a, b))),
}
}
}
/// A discovered length. Deferred values belong to the current layout pass;
/// widgets must not retain them. Ordinary requests remain inline lengths.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RequestedLen(RequestValue);
#[derive(Clone, Copy, Debug, PartialEq)]
enum RequestValue {
Linear(LayoutLen),
Deferred {
index: usize,
epoch: u64,
leftover: bool,
},
}
impl From<LayoutLen> for RequestedLen {
fn from(len: LayoutLen) -> Self {
Self(RequestValue::Linear(len))
}
}
impl From<Len> for RequestedLen {
fn from(len: Len) -> Self {
LayoutLen::from(len).into()
}
}
impl RequestedLen {
pub fn linear(self) -> Option<LayoutLen> {
match self.0 {
RequestValue::Linear(len) => Some(len),
_ => None,
}
}
pub fn has_leftover(self) -> bool {
match self.0 {
RequestValue::Linear(len) => len.leftover > Weight::ZERO,
RequestValue::Deferred { leftover, .. } => leftover,
}
}
}
#[derive(Clone, Copy)]
enum Op {
Sum,
Min,
Max,
}
struct Node {
op: Op,
a: RequestedLen,
b: RequestedLen,
}
#[derive(Default)]
pub(crate) struct RequestArena {
nodes: Vec<Node>,
epoch: u64,
}
impl RequestArena {
pub(crate) fn reset(&mut self) {
self.nodes.clear();
self.epoch = self
.epoch
.checked_add(1)
.expect("layout generation exhausted");
}
pub(crate) fn import(&mut self, request: &SizeRequest, base: Len) -> RequestedLen {
let (op, pair) = match request {
SizeRequest::Linear(len) => return len.within_len(base).into(),
SizeRequest::Sum(pair) => (Op::Sum, pair),
SizeRequest::Min(pair) => (Op::Min, pair),
SizeRequest::Max(pair) => (Op::Max, pair),
};
let a = self.import(&pair.0, base);
let b = self.import(&pair.1, base);
self.combine(op, a, b)
}
fn combine(&mut self, op: Op, a: RequestedLen, b: RequestedLen) -> RequestedLen {
if let (Some(x), Some(y)) = (a.linear(), b.linear()) {
if matches!(op, Op::Sum) {
return (x + y).into();
}
let order = independent_order(x, y);
if let Some(order) = order {
let take_a = match op {
Op::Min => !order.is_gt(),
_ => !order.is_lt(),
};
return if take_a { a } else { b };
}
}
if a == b && !matches!(op, Op::Sum) {
return a;
}
let index = self.nodes.len();
self.nodes.push(Node { op, a, b });
RequestedLen(RequestValue::Deferred {
index,
epoch: self.epoch,
leftover: a.has_leftover() || b.has_leftover(),
})
}
pub(crate) fn minimum(&self, request: RequestedLen, window: Px) -> Px {
Px::from_raw(self.segment(request, Ratio::ZERO, window).fixed as i32)
}
fn segment(&self, request: RequestedLen, at: Ratio, window: Px) -> Segment {
match request.0 {
RequestValue::Linear(len) => {
assert!(
len.leftover >= Weight::ZERO,
"a leftover weight cannot be negative"
);
Segment {
fixed: i64::from(len.without_leftover().to_px(window).raw()),
weight: i64::from(len.leftover.raw()),
end: None,
}
}
RequestValue::Deferred { index, epoch, .. } => {
assert_eq!(epoch, self.epoch, "request retained beyond its layout pass");
let Node { op, a, b } = self.nodes[index];
let a = self.segment(a, at, window);
let b = self.segment(b, at, window);
if matches!(op, Op::Sum) {
return a + b;
}
// At a crossing choose the branch to its right, so the next
// iteration advances rather than selecting that crossing again.
let order = a.value(at).cmp(&b.value(at)).then(a.weight.cmp(&b.weight));
let take_a = match op {
Op::Min => !order.is_gt(),
_ => !order.is_lt(),
};
let mut selected = if take_a { a } else { b };
selected.end = first(a.end, b.end);
if a.weight != b.weight {
let crossing = Ratio::new(b.fixed - a.fixed, a.weight - b.weight);
if crossing > at {
selected.end = first(selected.end, Some(crossing));
}
}
selected
}
}
}
/// Allocates one scope of nonnegative shares. Floors can overflow; caps
/// can leave unused room. Prefix rounding keeps adjacent slot edges equal.
pub(crate) fn allocate<'a>(
&'a self,
requests: &'a [RequestedLen],
room: Px,
window: Px,
) -> impl Iterator<Item = Px> + 'a {
let mut at = Ratio::ZERO;
loop {
let total = requests.iter().fold(Segment::ZERO, |total, request| {
total + self.segment(*request, at, window)
});
if total.value(at) >= i128::from(room.raw()) * i128::from(at.den) {
break;
}
if total.weight != 0 {
let solution = Ratio::new(i64::from(room.raw()) - total.fixed, total.weight);
if total.end.is_none_or(|end| solution <= end) {
at = solution;
break;
}
}
match total.end {
Some(end) => at = end,
None => break,
}
}
let mut prefix = 0_i128;
let mut previous = 0_i128;
requests.iter().map(move |request| {
prefix += self.segment(*request, at, window).value(at);
let den = i128::from(at.den);
let edge = prefix.signum() * ((prefix.abs() + den / 2) / den);
let len = Px::from_raw((edge - previous) as i32);
previous = edge;
len
})
}
}
#[derive(Clone, Copy, Debug, Eq)]
struct Ratio {
num: i64,
den: i64,
}
impl PartialEq for Ratio {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Ratio {
const ZERO: Self = Self { num: 0, den: 1 };
fn new(num: i64, den: i64) -> Self {
assert_ne!(den, 0);
if den < 0 {
Self {
num: -num,
den: -den,
}
} else {
Self { num, den }
}
}
}
impl Ord for Ratio {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(i128::from(self.num) * i128::from(other.den))
.cmp(&(i128::from(other.num) * i128::from(self.den)))
}
}
impl PartialOrd for Ratio {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Copy)]
struct Segment {
fixed: i64,
weight: i64,
end: Option<Ratio>,
}
impl Segment {
const ZERO: Self = Self {
fixed: 0,
weight: 0,
end: None,
};
fn value(self, at: Ratio) -> i128 {
i128::from(self.fixed) * i128::from(at.den) + i128::from(self.weight) * i128::from(at.num)
}
}
impl std::ops::Add for Segment {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
fixed: self.fixed + other.fixed,
weight: self.weight + other.weight,
end: first(self.end, other.end),
}
}
}
fn first(a: Option<Ratio>, b: Option<Ratio>) -> Option<Ratio> {
match (a, b) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
}
/// Read-only discovery of requests through a widget's children. A request is
/// expressed in window lengths; `rel_base` supplies the base for declarations.
pub struct SizeRequests<'a> {
pub(crate) arena: &'a mut RequestArena,
pub(crate) measured: Option<&'a crate::util::HashMap<WidgetId, crate::ActiveData>>,
pub(crate) widgets: &'a Widgets,
pub(crate) dependencies: &'a mut Vec<WidgetId>,
pub(crate) rel_base: Len,
}
impl SizeRequests<'_> {
pub fn sum(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
self.arena.combine(Op::Sum, a, b)
}
pub fn min(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
self.arena.combine(Op::Min, a, b)
}
pub fn max(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
self.arena.combine(Op::Max, a, b)
}
pub fn widget<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
) -> Option<RequestedLen> {
self.dependencies.push(child.id());
let rules = self.widgets.size_rules(child.id());
let rule = &rules[axis];
if let crate::SizeRule::Request(request) = rule {
return Some(self.arena.import(request, self.rel_base));
}
if let Some(exact) = rule.exact() {
return Some(exact.within_len(self.rel_base).into());
}
let widget = self.widgets.get_dyn(child.id())?;
let request = widget.size_request(self, axis).or_else(|| {
self.measured?
.get(&child.id())?
.measured()
.map(|size| size[axis].into())
})?;
Some(self.bounded(request, rule.bound()))
}
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: crate::Bound) -> RequestedLen {
let bound = bound.within_len(self.rel_base);
let request = match bound.min {
Some(min) => self.max(request, min.into()),
None => request,
};
match bound.max {
Some(max) => self.min(request, max.into()),
None => request,
}
}
pub fn length(&self, len: LayoutLen) -> RequestedLen {
len.within_len(self.rel_base).into()
}
pub fn inset<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
padding: Px,
) -> Option<RequestedLen> {
let base = self.rel_base;
self.rel_base.px -= padding;
let request = self.widget(child, axis);
self.rel_base = base;
request.map(|request| self.sum(request, Len::from_parts(crate::Rel::ZERO, padding).into()))
}
}
// Equal fractions keep this valid even when padding makes a rel base negative.
fn independent_order(a: LayoutLen, b: LayoutLen) -> Option<std::cmp::Ordering> {
if a.rel == b.rel && a.leftover == b.leftover {
Some(a.px.cmp(&b.px))
} else if a.px == b.px && a.rel == b.rel {
Some(a.leftover.cmp(&b.leftover))
} else {
None
}
}
+26 -22
View File
@@ -1,5 +1,5 @@
use crate::util::impl_axis_index;
use crate::{Axis, LayoutLen, Len, Rel};
use crate::{Axis, LayoutLen, Len, Rel, SizeRequest};
/// What a widget's length on one axis is, as a rule its parent applies where
/// it draws it rather than an answer the widget gives about itself.
@@ -10,16 +10,17 @@ use crate::{Axis, LayoutLen, Len, Rel};
/// nobody has drawn yet, and it is why a rule lives beside the widget rather
/// than inside it -- the widget under the rule never has to know about it.
///
/// A rule gives a length or bounds one, never both: a share that is also
/// capped wants two widgets, one taking the share and one capping what is
/// inside it.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
/// Exact expressions can bound a share before allocation. Bounds on an
/// intrinsic answer are applied after that answer becomes known.
#[derive(Debug, Clone, PartialEq, Default)]
pub enum SizeRule {
/// Whatever the widget reports from drawing.
#[default]
Free,
/// This length, whatever the widget reports.
Exact(LayoutLen),
/// An exact request whose comparisons await the parent's allocation.
Request(std::sync::Arc<SizeRequest>),
/// At least this long, and otherwise whatever the box gives it.
Min(Len),
/// At most this long.
@@ -33,7 +34,7 @@ impl SizeRule {
/// outright.
pub fn bound(&self) -> Bound {
match *self {
Self::Free | Self::Exact(_) => Bound::ANY,
Self::Free | Self::Exact(_) | Self::Request(_) => Bound::ANY,
Self::Min(min) => Bound {
min: Some(min),
max: None,
@@ -66,10 +67,8 @@ impl SizeRule {
match *self {
Self::Free | Self::Min(_) => Self::Min(min),
Self::Max(max) | Self::Clamp { max, .. } => Self::Clamp { min, max },
Self::Exact(len) => {
debug_assert!(false, "{len:?} is a length, so bounding it says nothing");
Self::Min(min)
}
Self::Request(ref request) => request.as_ref().clone().max(min).into(),
Self::Exact(len) => len.max(min).into(),
}
}
@@ -79,10 +78,8 @@ impl SizeRule {
match *self {
Self::Free | Self::Max(_) => Self::Max(max),
Self::Min(min) | Self::Clamp { min, .. } => Self::Clamp { min, max },
Self::Exact(len) => {
debug_assert!(false, "{len:?} is a length, so bounding it says nothing");
Self::Max(max)
}
Self::Request(ref request) => request.as_ref().clone().min(max).into(),
Self::Exact(len) => len.min(max).into(),
}
}
@@ -100,20 +97,18 @@ impl SizeRule {
pub fn exact(&self) -> Option<LayoutLen> {
match self {
Self::Exact(len) => Some(*len),
Self::Free | Self::Min(_) | Self::Max(_) | Self::Clamp { .. } => None,
Self::Free | Self::Request(_) | Self::Min(_) | Self::Max(_) | Self::Clamp { .. } => {
None
}
}
}
}
/// What a rule allows a length to be where it does not give one outright: a
/// floor, a cap, or both. Each is a length of the rel base the widget is
/// asked with, which is the base a declared length is a fraction of too, and
/// a bound that binds is a declaration -- the box comes to what it says.
/// asked with, which is the base a declared length is a fraction of too.
///
/// A bound is a [`Len`] and never a share. Which of a fixed and a relative
/// child is longer, asked at the length the cap is itself deciding, admits
/// several self-sizing fixed points, so a cap containing `leftover` has no
/// one answer: see `docs/LAYOUT.md` under the failed hypotheses.
/// A bound is a [`Len`]. Comparisons involving shares are [`SizeRequest`]s.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Bound {
pub min: Option<Len>,
@@ -183,6 +178,15 @@ impl From<LayoutLen> for SizeRule {
}
}
impl From<SizeRequest> for SizeRule {
fn from(request: SizeRequest) -> Self {
match request {
SizeRequest::Linear(len) => Self::Exact(len),
request => Self::Request(std::sync::Arc::new(request)),
}
}
}
impl From<Option<LayoutLen>> for SizeRule {
fn from(len: Option<LayoutLen>) -> Self {
len.map_or(Self::Free, Self::Exact)
@@ -191,7 +195,7 @@ impl From<Option<LayoutLen>> for SizeRule {
/// One rule per axis, which is how a widget carries a length on one axis and
/// leaves the other to whatever it draws.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct SizeRules {
pub x: SizeRule,
pub y: SizeRule,
+1 -1
View File
@@ -129,7 +129,7 @@ impl Widgets {
/// 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
self.data(id).unwrap().size.clone()
}
/// Sets one axis's rule. The widget is marked rather than its parent