Constrain offered boxes with independent widget size bounds
This commit is contained in:
1 parent
4cb6f6882a
commit
2ac0843cb2
18 files changed
+546
-371
No files matched your search
+64
-63
@@ -3,8 +3,8 @@ use crate::layout_diagnostics::{self as diag, Counter};
|
||||
use crate::{
|
||||
Axis, Bound, Bounds, Declared, DrawScratch, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc,
|
||||
PlaceFit, Px, PxVec2, RegionAlign, Rel, RenderedText, RequestArena, RequestedLen,
|
||||
RetainedPrimitive, Size, SizeRequests, SizeRule, StrongWidget, TextAttrs, TextBuffer,
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
|
||||
RetainedPrimitive, Size, SizeRequests, StrongWidget, TextAttrs, TextBuffer, TextureHandle,
|
||||
UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
|
||||
render::{
|
||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
|
||||
TexturePrimitive,
|
||||
@@ -88,9 +88,7 @@ impl<'a> Painter<'a> {
|
||||
child: &StrongWidget<W>,
|
||||
axis: Axis,
|
||||
) -> Option<RequestedLen> {
|
||||
if self.rsc.widgets().size_rules(child.id())[axis].bound() == Bound::ANY
|
||||
&& let Some(len) = self.size_hint(child, axis)
|
||||
{
|
||||
if let Some(len) = self.size_hint(child, axis) {
|
||||
self.request_deps.push(child.id());
|
||||
return Some(len.into());
|
||||
}
|
||||
@@ -106,10 +104,9 @@ impl<'a> Painter<'a> {
|
||||
// only a declaration or a share chooses the box it is drawn in.
|
||||
let request = requests.widget(child, axis).filter(|request| {
|
||||
request.has_leftover()
|
||||
|| matches!(
|
||||
self.rsc.widgets().size_rules(child.id())[axis],
|
||||
SizeRule::Request(_)
|
||||
)
|
||||
|| self.rsc.widgets().size_rules(child.id())[axis]
|
||||
.request
|
||||
.is_some()
|
||||
});
|
||||
if request.is_some() {
|
||||
self.rel_base(axis);
|
||||
@@ -130,7 +127,7 @@ impl<'a> Painter<'a> {
|
||||
len: LayoutLen,
|
||||
) -> RequestedLen {
|
||||
let start = self.request_deps.len();
|
||||
let bound = self.rsc.widgets().size_rules(child.id())[axis].bound();
|
||||
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),
|
||||
@@ -442,6 +439,9 @@ impl<'a> Painter<'a> {
|
||||
/// against this widget's rel base, which is the rel base a child asked with
|
||||
/// nothing narrowed gets. Asking counts as reading its size.
|
||||
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
|
||||
if self.rsc.widgets().size_rules(id.id())[axis].bound != Bound::ANY {
|
||||
return None;
|
||||
}
|
||||
let hint = self.rsc.widgets().exact_len(id.id(), axis);
|
||||
let rel_base = self.rel_base[axis];
|
||||
let resolved = hint.map(|hint| hint.within_len(rel_base));
|
||||
@@ -559,10 +559,9 @@ 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 {
|
||||
matches!(
|
||||
self.rsc.widgets().size_rules(self.id)[axis],
|
||||
SizeRule::Exact(_) | SizeRule::Request(_)
|
||||
)
|
||||
self.rsc.widgets().size_rules(self.id)[axis]
|
||||
.request
|
||||
.is_some()
|
||||
}
|
||||
|
||||
/// This widget's own box in pixels. Reading it makes the drawing one
|
||||
@@ -809,10 +808,11 @@ impl Widgets {
|
||||
// narrow the box in its place: what the request comes to is not known
|
||||
// until the parent allocates, and it is the parent's answer, not this
|
||||
// widget's.
|
||||
if matches!(self.size_rules(id)[axis], SizeRule::Request(_)) {
|
||||
let rule = &self.size_rules(id)[axis];
|
||||
if rule.deferred().is_some() {
|
||||
return None;
|
||||
}
|
||||
self.size_rules(id)[axis].exact().or_else(|| {
|
||||
rule.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
|
||||
// size rather than the whole offer. That is the offer's business
|
||||
@@ -821,12 +821,6 @@ impl Widgets {
|
||||
self.get_dyn(id)?.size_hint(axis)
|
||||
})
|
||||
}
|
||||
|
||||
/// What a widget's box is where a rule or its own hint gives one outright,
|
||||
/// rather than a share for whoever draws it to divide.
|
||||
pub(super) fn declared_lens(&self, id: WidgetId) -> Declared {
|
||||
Declared::from_axes(|axis| self.exact_len(id, axis)?.declared())
|
||||
}
|
||||
}
|
||||
|
||||
/// One ask of a widget: the box it draws in, what its fractions are of, and
|
||||
@@ -838,9 +832,8 @@ pub(super) struct Ask {
|
||||
/// past the box its parent offered.
|
||||
pub place: PlaceDesc,
|
||||
/// What the widget's box is on each axis where something says so
|
||||
/// outright: its rule or its hint, or a bound of its own that the box it
|
||||
/// was offered falls outside -- a bound that binds is a declaration, and
|
||||
/// the same one the widget answers with.
|
||||
/// outright: its rule, its hint, or a bound the offer fell outside.
|
||||
/// An intrinsic answer can still occupy less than a capped box.
|
||||
pub declared: Declared,
|
||||
/// Its bounds, resolved against the rel base its rules were resolved
|
||||
/// against, for the answer to be held to where the box was not.
|
||||
@@ -876,11 +869,18 @@ impl Placing {
|
||||
let rules = widgets.size_rules(id);
|
||||
let mut holds = LayoutHolds::ANY;
|
||||
let mut inputs = LayoutHolds::ANY;
|
||||
let mut declared = widgets.declared_lens(id);
|
||||
let mut declared = Declared::NONE;
|
||||
let mut bounds = Bounds::ANY;
|
||||
for axis in Axis::BOTH {
|
||||
let base = place.base(axis, self.rel_base);
|
||||
if let SizeRule::Request(request) = &rules[axis] {
|
||||
let stated = widgets.exact_len(id, axis);
|
||||
if let Some(len) = stated.and_then(|len| len.declared()) {
|
||||
if len.rel != Rel::ZERO {
|
||||
inputs[axis].rel_base = Some(self.rel_base[axis]);
|
||||
}
|
||||
declared[axis] = Some(len.within_len(base));
|
||||
}
|
||||
if let Some(request) = rules[axis].deferred() {
|
||||
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]);
|
||||
@@ -889,6 +889,7 @@ impl Placing {
|
||||
offer.to_px(window[axis])
|
||||
} else {
|
||||
let request = requests.import(request, base);
|
||||
let request = requests.bounded(request, rules[axis].bound.within_len(base));
|
||||
holds[axis].window = Holds::at(window[axis]);
|
||||
requests
|
||||
.allocate(&[request], offer.to_px(window[axis]), window[axis])
|
||||
@@ -906,24 +907,38 @@ impl Placing {
|
||||
// a span follows with one child. Only the overflow is a box of
|
||||
// the widget's own: a share that fits is the box it was given,
|
||||
// which is what this place already says.
|
||||
let (share, kept) =
|
||||
self.share_past_the_offer(widgets, window[axis], id, place, align, axis);
|
||||
let (share, kept) = self.share_past_the_offer(stated, window[axis], place, align, axis);
|
||||
holds[axis].window = holds[axis].window.and(kept);
|
||||
if let Some(len) = share {
|
||||
place[axis] = len.as_desc().fills();
|
||||
}
|
||||
// A bound holds what the widget answers, not the box it is asked
|
||||
// in: the box it is given is whoever asked's to decide, and a
|
||||
// rule that read it would be decided again by every path that
|
||||
// hands the widget a box -- including the ones that never ask it
|
||||
// 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.
|
||||
let bound = rules[axis].bound();
|
||||
let bound = rules[axis].bound;
|
||||
if bound != Bound::ANY {
|
||||
if bound.has_fraction() {
|
||||
inputs[axis].rel_base = Some(self.rel_base[axis]);
|
||||
}
|
||||
bounds[axis] = bound.within_len(base);
|
||||
// A solved slot may narrow the widget's rel base, but the
|
||||
// allocator evaluated its bounds against this parent's base.
|
||||
let bound_base = if place[axis].fit == PlaceFit::Allocated {
|
||||
self.rel_base[axis]
|
||||
} else {
|
||||
base
|
||||
};
|
||||
bounds[axis] = bound.within_len(bound_base);
|
||||
let offer =
|
||||
declared[axis].unwrap_or_else(|| place.of(self.region, align)[axis].len());
|
||||
let (held, kept) = bounds[axis].outside(offer, window[axis]);
|
||||
holds[axis].window = holds[axis].window.and(kept);
|
||||
// The comparison reads the incoming box, before a bound
|
||||
// replaces it. Placement keeps that decision; only an ask
|
||||
// may compare a new offer.
|
||||
if declared[axis].is_none() {
|
||||
inputs[axis].region_len = Some(self.region[axis].len());
|
||||
}
|
||||
if let Some(len) = held {
|
||||
declared[axis] = Some(len);
|
||||
}
|
||||
}
|
||||
}
|
||||
let (rel_base, region) =
|
||||
place.rel_base_and_region(self.region, self.rel_base, declared, align);
|
||||
@@ -950,9 +965,8 @@ impl Placing {
|
||||
/// crossing between them is a question in pixels.
|
||||
fn share_past_the_offer(
|
||||
&self,
|
||||
widgets: &Widgets,
|
||||
stated: Option<LayoutLen>,
|
||||
window: Px,
|
||||
id: WidgetId,
|
||||
place: PlaceDesc,
|
||||
align: RegionAlign,
|
||||
axis: Axis,
|
||||
@@ -966,7 +980,7 @@ impl Placing {
|
||||
}
|
||||
// A share with nothing beside it is the box whatever the box is, so
|
||||
// there is no comparison to make and no range to keep for one.
|
||||
let Some(stated) = widgets.exact_len(id, axis) else {
|
||||
let Some(stated) = stated else {
|
||||
return (None, Holds::ANY);
|
||||
};
|
||||
if stated.leftover == Weight::ZERO || stated.is_only_leftover() {
|
||||
@@ -985,12 +999,10 @@ impl LayoutLen {
|
||||
/// Whether what a widget reported along an axis is the whole of the box
|
||||
/// it is in rather than a part to be placed inside it. 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 does too:
|
||||
/// the rule already gave the region its length, and the rule's length is
|
||||
/// what the widget reports there. And an axis the parent decided from
|
||||
/// the answer is the answer already.
|
||||
pub(super) fn fills(&self, declared: Option<Len>, decided: bool) -> bool {
|
||||
self.leftover != Weight::ZERO || declared.is_some() || decided
|
||||
/// did is the one that handed down this box. An axis the parent decided
|
||||
/// from the answer is the answer already.
|
||||
pub(super) fn fills(&self, decided: bool) -> bool {
|
||||
self.leftover != Weight::ZERO || decided
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1004,17 +1016,11 @@ impl PlaceDesc {
|
||||
/// part. That is what makes a fraction the same fraction wherever the part
|
||||
/// it is placed in sits and however long it is -- the fraction is resolved
|
||||
/// once, here, against the rel base it was reported of.
|
||||
pub(super) fn placement(
|
||||
self,
|
||||
region: UiRegion,
|
||||
size: Size,
|
||||
declared: Declared,
|
||||
align: RegionAlign,
|
||||
) -> UiRegion {
|
||||
pub(super) fn placement(self, region: UiRegion, size: Size, align: RegionAlign) -> UiRegion {
|
||||
let mut placed = region;
|
||||
for axis in Axis::BOTH {
|
||||
let reported = size[axis];
|
||||
if reported.fills(declared[axis], self[axis].fit.fills()) {
|
||||
if reported.fills(self[axis].fit.fills()) {
|
||||
continue;
|
||||
}
|
||||
placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
|
||||
@@ -1027,11 +1033,8 @@ impl PlaceDesc {
|
||||
///
|
||||
/// `own` is that widget's own box, and `place` what of it the child is
|
||||
/// given, including any rel base it states -- a row's slot, or padding's rel
|
||||
/// base less its pixels. That is a window length, like every other length
|
||||
/// here, since a slot of a row is not a fraction of anything the row can
|
||||
/// name. The child's declaration is a fraction of whichever reached it, and
|
||||
/// is the only one that also places the box: a box the caller decided is
|
||||
/// what `place` names.
|
||||
/// base less its pixels. The ask has already resolved declarations into
|
||||
/// window lengths, so placement only aligns them inside the given box.
|
||||
pub(super) fn rel_base_and_region(
|
||||
self,
|
||||
own: UiRegion,
|
||||
@@ -1044,9 +1047,7 @@ impl PlaceDesc {
|
||||
let mut region = given;
|
||||
for axis in Axis::BOTH {
|
||||
let base = self.base(axis, parent_rel_base);
|
||||
let len = declared[axis]
|
||||
.map(|len| len.within_len(base))
|
||||
.unwrap_or(base);
|
||||
let len = declared[axis].unwrap_or(base);
|
||||
rel_base[axis] = len;
|
||||
if declared[axis].is_some() {
|
||||
region[axis] = given[axis].place(len, align[axis]);
|
||||
|
||||
+24
-28
@@ -254,7 +254,6 @@ impl UiRenderState {
|
||||
);
|
||||
}
|
||||
let align = rsc.widgets().alignment(id);
|
||||
let declared = info.declared;
|
||||
// Nothing this widget measured can be dirty while it draws: layout is
|
||||
// one bottom-up walk, so anything deeper has settled or deferred to
|
||||
// its own parent, and a deferred one leaves that parent marked.
|
||||
@@ -268,7 +267,7 @@ impl UiRenderState {
|
||||
.then(|| self.retained_answer(id, region, info))
|
||||
.flatten()
|
||||
.and_then(|answer| {
|
||||
let placed = info.placed.placement(region, answer.size, declared, align);
|
||||
let placed = info.placed.placement(region, answer.size, align);
|
||||
self.try_reuse(id, region, placed, info, rsc)
|
||||
.then_some(answer)
|
||||
});
|
||||
@@ -280,7 +279,7 @@ impl UiRenderState {
|
||||
// Where the drawing goes: the part its parent gave it, with the
|
||||
// answer placed inside that part on any axis the parent left
|
||||
// open.
|
||||
let placed = info.placed.placement(region, answer.size, declared, align);
|
||||
let placed = info.placed.placement(region, answer.size, align);
|
||||
if placed != region {
|
||||
self.relocate(id, placed, info, rsc);
|
||||
}
|
||||
@@ -379,10 +378,7 @@ impl UiRenderState {
|
||||
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,
|
||||
own: LayoutHolds::ANY,
|
||||
under,
|
||||
answer_under: LayoutHolds::ANY,
|
||||
depth: info.depth,
|
||||
@@ -437,9 +433,9 @@ impl UiRenderState {
|
||||
// rel base is the answer wherever the ask declared a length: it was
|
||||
// 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).clone();
|
||||
let rules = rsc.widgets().size_rules(id);
|
||||
let ruled = |axis: Axis, reported: LayoutLen| {
|
||||
if matches!(rules[axis], crate::SizeRule::Request(_)) {
|
||||
if rules[axis].deferred().is_some() {
|
||||
return info.rel_base[axis].into();
|
||||
}
|
||||
match rules[axis].exact() {
|
||||
@@ -470,18 +466,30 @@ impl UiRenderState {
|
||||
// it is a length only to whoever divides one, and the box that
|
||||
// divider gives is a box this widget is asked in, where the bound is
|
||||
// applied to it.
|
||||
let mut bounded = LayoutHolds::ANY;
|
||||
// Widgets may widen their own read ranges, but not the ask's constraints.
|
||||
let mut own_holds = own.and(info.ask_holds);
|
||||
for axis in Axis::BOTH {
|
||||
let answer = size[axis];
|
||||
if answer.leftover != Weight::ZERO {
|
||||
continue;
|
||||
}
|
||||
let (held, kept) = info.bounds[axis].outside(answer.without_leftover(), window[axis]);
|
||||
bounded[axis].window = kept;
|
||||
own_holds[axis].window = own_holds[axis].window.and(kept);
|
||||
if let Some(held) = held {
|
||||
size[axis] = held.into();
|
||||
}
|
||||
}
|
||||
// A rule that is a fraction of the rel base is answered with the
|
||||
// rel base's own length, so the answer is that rel base's and not just
|
||||
// that many pixels of this window -- the same pin a widget that read
|
||||
// its rel base took for its drawing. A bound counts: which side of it
|
||||
// the box fell was decided against this rel base, and the same box of
|
||||
// a different one can fall on the other.
|
||||
for axis in Axis::BOTH {
|
||||
if rules[axis].has_fraction() {
|
||||
own_holds[axis].rel_base = Some(info.rel_base[axis]);
|
||||
}
|
||||
}
|
||||
// A widget that clipped its contents to its box drew nothing outside
|
||||
// it, so reporting more than the box asks to be placed at a length it
|
||||
// does not occupy -- and its parent would place the part it cut off.
|
||||
@@ -509,18 +517,6 @@ impl UiRenderState {
|
||||
if let Some(idx) = retired_move {
|
||||
self.moves.remove(idx);
|
||||
}
|
||||
// A rule that is a fraction of the rel base is answered with the
|
||||
// rel base's own length, so the answer is that rel base's and not just
|
||||
// that many pixels of this window -- the same pin a widget that read
|
||||
// its rel base took for its drawing. A bound counts: which side of it
|
||||
// the box fell was decided against this rel base, and the same box of
|
||||
// a different one can fall on the other.
|
||||
let mut own_holds = own.and(bounded);
|
||||
for axis in Axis::BOTH {
|
||||
if rules[axis].has_fraction() {
|
||||
own_holds[axis].rel_base = Some(info.rel_base[axis]);
|
||||
}
|
||||
}
|
||||
let answer_holds = own_holds.and(answer_under);
|
||||
let holds = under
|
||||
.iter()
|
||||
@@ -798,7 +794,6 @@ impl UiRenderState {
|
||||
let placed = place.placement(
|
||||
region,
|
||||
active.measured().unwrap_or(active.size),
|
||||
active.declared,
|
||||
active.own_align,
|
||||
);
|
||||
let info = DrawInfo {
|
||||
@@ -823,8 +818,8 @@ impl UiRenderState {
|
||||
|
||||
/// The rel base and the box a widget already drawn is given at `place` of
|
||||
/// the box its parent is being taken as. What narrowed its rel base and what
|
||||
/// it declared are its own record's, so both are resolved against that
|
||||
/// parent's rel base again exactly as the first ask resolved them.
|
||||
/// it declared are its own record's. Declared lengths stay as the ask
|
||||
/// resolved them; the destination only decides where they sit.
|
||||
fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) {
|
||||
place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align)
|
||||
}
|
||||
@@ -1195,10 +1190,11 @@ impl UiRenderState {
|
||||
active.asked,
|
||||
);
|
||||
let active = &self.active[&id];
|
||||
let declared_changed = ask.declared != active.declared;
|
||||
// Even an inactive bound changes what the parent must track about its offer.
|
||||
let constraints_changed = ask.declared != active.declared || ask.bounds != active.bounds;
|
||||
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
|
||||
if let Some(parent) = active.parent
|
||||
&& (declared_changed
|
||||
&& (constraints_changed
|
||||
|| alignment_changed
|
||||
|| active.re_asked
|
||||
|| !active.drawn
|
||||
|
||||
+22
-20
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
ActiveData, Axis, Bound, LayoutLen, Len, Px, Rel, SizeRule, StrongWidget, UiNum, Weight,
|
||||
WidgetId, Widgets, util::HashMap,
|
||||
ActiveData, Axis, Bound, LayoutLen, Len, Px, Rel, StrongWidget, UiNum, Weight, WidgetId,
|
||||
Widgets, util::HashMap,
|
||||
};
|
||||
use std::{cmp::Ordering, sync::Arc};
|
||||
|
||||
@@ -169,6 +169,17 @@ impl RequestArena {
|
||||
let b = self.import(&pair.1, base);
|
||||
self.combine(op, a, b)
|
||||
}
|
||||
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen {
|
||||
let request = match bound.min {
|
||||
Some(min) => self.combine(Op::Max, request, min.into()),
|
||||
None => request,
|
||||
};
|
||||
match bound.max {
|
||||
Some(max) => self.combine(Op::Min, request, max.into()),
|
||||
None => request,
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -391,32 +402,23 @@ impl SizeRequests<'_> {
|
||||
self.dependencies.push(child.id());
|
||||
let rules = self.widgets.size_rules(child.id());
|
||||
let rule = &rules[axis];
|
||||
if let 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 request = match &rule.request {
|
||||
Some(request) => self.arena.import(request, self.rel_base),
|
||||
None => {
|
||||
let widget = self.widgets.get_dyn(child.id())?;
|
||||
let request = widget.size_request(self, axis).or_else(|| {
|
||||
widget.size_request(self, axis).or_else(|| {
|
||||
self.measured?
|
||||
.get(&child.id())?
|
||||
.measured()
|
||||
.map(|size| size[axis].into())
|
||||
})?;
|
||||
Some(self.bounded(request, rule.bound()))
|
||||
})?
|
||||
}
|
||||
};
|
||||
Some(self.bounded(request, rule.bound))
|
||||
}
|
||||
|
||||
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: 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,
|
||||
}
|
||||
self.arena.bounded(request, bound.within_len(self.rel_base))
|
||||
}
|
||||
|
||||
pub fn length(&self, len: LayoutLen) -> RequestedLen {
|
||||
|
||||
@@ -1,108 +1,80 @@
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Axis, LayoutLen, Len, Rel, SizeRequest};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// A rule and a drawn size are not two opinions to reconcile: a rule wins on
|
||||
/// the axis it names, and the `Size` returned by `draw` answers only the axes
|
||||
/// with no rule. That is what lets a span divide its space around a length
|
||||
/// 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.
|
||||
///
|
||||
/// Exact expressions can bound a share before allocation. Bounds on an
|
||||
/// intrinsic answer are applied after that answer becomes known.
|
||||
/// A preferred length and independent bounds on one axis. Without a
|
||||
/// request, the widget's drawing supplies the preferred length.
|
||||
#[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(Arc<SizeRequest>),
|
||||
/// At least this long, and otherwise whatever the box gives it.
|
||||
Min(Len),
|
||||
/// At most this long.
|
||||
Max(Len),
|
||||
/// Between the two.
|
||||
Clamp { min: Len, max: Len },
|
||||
pub struct SizeRule {
|
||||
pub request: Option<SizeRequest>,
|
||||
pub bound: Bound,
|
||||
}
|
||||
|
||||
impl SizeRule {
|
||||
/// What this rule allows the length to be where it does not give one
|
||||
/// outright.
|
||||
pub fn bound(&self) -> Bound {
|
||||
match *self {
|
||||
Self::Free | Self::Exact(_) | Self::Request(_) => Bound::ANY,
|
||||
Self::Min(min) => Bound {
|
||||
min: Some(min),
|
||||
max: None,
|
||||
},
|
||||
Self::Max(max) => Bound {
|
||||
min: None,
|
||||
max: Some(max),
|
||||
},
|
||||
Self::Clamp { min, max } => Bound {
|
||||
min: Some(min),
|
||||
max: Some(max),
|
||||
},
|
||||
pub const FREE: Self = Self {
|
||||
request: None,
|
||||
bound: Bound::ANY,
|
||||
};
|
||||
|
||||
pub fn min(min: Len) -> Self {
|
||||
Self::FREE.at_least(min)
|
||||
}
|
||||
|
||||
pub fn max(max: Len) -> Self {
|
||||
Self::FREE.at_most(max)
|
||||
}
|
||||
|
||||
pub fn clamp(min: Len, max: Len) -> Self {
|
||||
Self::min(min).at_most(max)
|
||||
}
|
||||
|
||||
/// Whether what this rule says is a fraction of the rel base, so that
|
||||
/// the same rule against a different one is a different length.
|
||||
pub fn has_fraction(&self) -> bool {
|
||||
self.exact().is_some_and(|len| len.rel != Rel::ZERO) || self.bound().has_fraction()
|
||||
self.exact().is_some_and(|len| len.rel != Rel::ZERO) || self.bound.has_fraction()
|
||||
}
|
||||
|
||||
/// This rule with a floor under it, which is the whole of it where there
|
||||
/// was no rule.
|
||||
/// Replaces the floor while preserving the request and cap.
|
||||
pub fn at_least(&self, min: Len) -> Self {
|
||||
match *self {
|
||||
Self::Free | Self::Min(_) => Self::Min(min),
|
||||
Self::Max(max) | Self::Clamp { max, .. } => Self::Clamp { min, max },
|
||||
Self::Request(ref request) => request.as_ref().clone().max(min).into(),
|
||||
Self::Exact(len) => len.max(min).into(),
|
||||
}
|
||||
let mut rule = self.clone();
|
||||
rule.bound.min = Some(min);
|
||||
rule
|
||||
}
|
||||
|
||||
/// This rule with a cap over it, which is the whole of it where there was
|
||||
/// no rule.
|
||||
/// Replaces the cap while preserving the request and floor.
|
||||
pub fn at_most(&self, max: Len) -> Self {
|
||||
match *self {
|
||||
Self::Free | Self::Max(_) => Self::Max(max),
|
||||
Self::Min(min) | Self::Clamp { min, .. } => Self::Clamp { min, max },
|
||||
Self::Request(ref request) => request.as_ref().clone().min(max).into(),
|
||||
Self::Exact(len) => len.min(max).into(),
|
||||
}
|
||||
let mut rule = self.clone();
|
||||
rule.bound.max = Some(max);
|
||||
rule
|
||||
}
|
||||
|
||||
/// The length this rule gives without the widget being drawn, if it can
|
||||
/// give one.
|
||||
pub fn declared(&self) -> Option<Len> {
|
||||
self.exact().and_then(|len| len.declared())
|
||||
}
|
||||
|
||||
/// The length this rule gives outright, whatever the widget reports --
|
||||
/// which makes the widget's answer on that axis moot. A share counts: it
|
||||
/// is a length the widget's parent still has to divide, so it is exact
|
||||
/// here and resolved there, unlike `declared`, which is only the ones
|
||||
/// that give a box directly.
|
||||
/// A linear preferred length, before applying the independent bounds.
|
||||
pub fn exact(&self) -> Option<LayoutLen> {
|
||||
match self {
|
||||
Self::Exact(len) => Some(*len),
|
||||
Self::Free | Self::Request(_) | Self::Min(_) | Self::Max(_) | Self::Clamp { .. } => {
|
||||
match self.request {
|
||||
Some(SizeRequest::Linear(len)) => Some(len),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests whose final length needs allocation, including a linear
|
||||
/// share constrained by an independent bound.
|
||||
pub(crate) fn deferred(&self) -> Option<&SizeRequest> {
|
||||
let request = self.request.as_ref()?;
|
||||
match request {
|
||||
SizeRequest::Linear(len)
|
||||
if len.leftover == crate::Weight::ZERO || self.bound == Bound::ANY =>
|
||||
{
|
||||
None
|
||||
}
|
||||
_ => Some(request),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// A floor, a cap, or both, independent of the preferred length.
|
||||
/// Fractions use the incoming rel base. An allocated slot keeps the
|
||||
/// allocator's base for bounds, even when the slot narrows the widget's own.
|
||||
///
|
||||
/// A bound is a [`Len`]. Comparisons involving shares are [`SizeRequest`]s.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
@@ -162,22 +134,22 @@ impl_axis_index!(Bounds => Bound);
|
||||
|
||||
impl From<LayoutLen> for SizeRule {
|
||||
fn from(len: LayoutLen) -> Self {
|
||||
Self::Exact(len)
|
||||
SizeRequest::from(len).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SizeRequest> for SizeRule {
|
||||
fn from(request: SizeRequest) -> Self {
|
||||
match request {
|
||||
SizeRequest::Linear(len) => Self::Exact(len),
|
||||
request => Self::Request(Arc::new(request)),
|
||||
Self {
|
||||
request: Some(request),
|
||||
bound: Bound::ANY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<LayoutLen>> for SizeRule {
|
||||
fn from(len: Option<LayoutLen>) -> Self {
|
||||
len.map_or(Self::Free, Self::Exact)
|
||||
len.map_or(Self::FREE, Self::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,12 +163,10 @@ pub struct SizeRules {
|
||||
|
||||
impl_axis_index!(SizeRules => SizeRule);
|
||||
|
||||
/// What a widget's box is on each axis where something says so outright,
|
||||
/// before it is drawn: a rule beside it, or a hint it gives about itself.
|
||||
/// Whoever draws the widget resolves these against its rel base.
|
||||
///
|
||||
/// A [`Len`] rather than a [`LayoutLen`], because a share can never be one
|
||||
/// -- see [`LayoutLen::declared`].
|
||||
/// Box lengths chosen by the ask: a declaration, a natural-size hint, or
|
||||
/// a binding bound. These are window lengths, already resolved against the
|
||||
/// incoming rel base. Moving a drawing preserves them instead of resolving
|
||||
/// a fraction against its destination a second time.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Declared {
|
||||
pub x: Option<Len>,
|
||||
|
||||
@@ -145,6 +145,16 @@ impl Widgets {
|
||||
self.needs_redraw.insert(id);
|
||||
}
|
||||
|
||||
/// Changes the preferred length without changing its bounds.
|
||||
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<crate::SizeRequest>) {
|
||||
let id = id.id();
|
||||
let rule = SizeRule {
|
||||
request: Some(len.into()),
|
||||
bound: self.size_rules(id)[axis].bound,
|
||||
};
|
||||
self.set_size_rule(id, axis, rule);
|
||||
}
|
||||
|
||||
/// Puts a floor under this widget's length on one axis, keeping a cap it
|
||||
/// already had. See [`SizeRule::at_least`].
|
||||
pub fn set_min_len(&mut self, id: impl IdLike, axis: Axis, min: Len) {
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ impl Harness {
|
||||
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) {
|
||||
self.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(id, axis, SizeRule::Exact(len.into()));
|
||||
.set_size_rule(id, axis, SizeRule::from(len.into()));
|
||||
}
|
||||
|
||||
/// Sets the root and lays it out, so a pointer event has something to hit.
|
||||
|
||||
+5
-10
@@ -350,9 +350,7 @@ impl Plan {
|
||||
self.walk_mut(&mut |node| {
|
||||
let Some(rules) = &mut node.size else { return };
|
||||
for axis in Axis::BOTH {
|
||||
if rules[axis].bound() != Bound::ANY {
|
||||
rules[axis] = SizeRule::Free;
|
||||
}
|
||||
rules[axis].bound = Bound::ANY;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -682,8 +680,8 @@ impl Sow<'_> {
|
||||
match self.rng.below(8) {
|
||||
0 | 1 => self.len().into(),
|
||||
2 => LayoutLen::LEFTOVER.into(),
|
||||
3 => SizeRule::Min(self.bound()),
|
||||
4 => SizeRule::Max(self.bound()),
|
||||
3 => SizeRule::min(self.bound()),
|
||||
4 => SizeRule::max(self.bound()),
|
||||
// Both in pixels, so one can be put under the other: a floor and
|
||||
// a cap that change sides with the window bound nothing, which
|
||||
// is a caller's bug rather than a tree to grow.
|
||||
@@ -692,12 +690,9 @@ impl Sow<'_> {
|
||||
Px::from_f32(20.0 + self.rng.below(180) as f32),
|
||||
Px::from_f32(20.0 + self.rng.below(180) as f32),
|
||||
);
|
||||
SizeRule::Clamp {
|
||||
min: Len::px(a.min(b).to_f32()),
|
||||
max: Len::px(a.max(b).to_f32()),
|
||||
SizeRule::clamp(Len::px(a.min(b).to_f32()), Len::px(a.max(b).to_f32()))
|
||||
}
|
||||
}
|
||||
_ => SizeRule::Free,
|
||||
_ => SizeRule::FREE,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Asks its child in the shorter of a cap and the box this widget was given,
|
||||
/// and answers what the child used, held to the same cap.
|
||||
///
|
||||
/// A cap on the box is a widget rather than a [`SizeRule`] because a box is
|
||||
/// whoever asked's to decide: a rule that read the box it was given would be
|
||||
/// decided again by every path that hands a widget one, including the ones
|
||||
/// that re-place a drawing without asking it anything, and the decision would
|
||||
/// then depend on which path arrived last. A widget is drawn again whenever
|
||||
/// its own box changes, so the comparison is made where the answer can be
|
||||
/// kept -- `longer_than` narrows the windows this drawing holds for, and
|
||||
/// `holds` says the box lengths.
|
||||
///
|
||||
/// The box is what a text wraps at and what a scroll takes its viewport from,
|
||||
/// which is why capping the answer alone is not the same thing.
|
||||
pub struct MaxSize {
|
||||
pub inner: StrongWidget,
|
||||
pub x: Option<Len>,
|
||||
pub y: Option<Len>,
|
||||
}
|
||||
|
||||
impl MaxSize {
|
||||
fn max(&self, axis: Axis) -> Option<Len> {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for MaxSize {
|
||||
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
|
||||
let inner = requests.widget(&self.inner, axis)?;
|
||||
Some(match self.max(axis) {
|
||||
Some(max) => requests.min(inner, max.into()),
|
||||
None => inner,
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let align = painter.alignment();
|
||||
let mut region = UiRegion::FULL;
|
||||
for axis in Axis::BOTH {
|
||||
let Some(max) = self.max(axis) else {
|
||||
continue;
|
||||
};
|
||||
let own = painter.region_len(axis);
|
||||
if painter.longer_than(own, max, axis) {
|
||||
region[axis] = max.align(align[axis]);
|
||||
}
|
||||
}
|
||||
let mut size = painter.widget_at(&self.inner, region).size();
|
||||
for axis in Axis::BOTH {
|
||||
// The child may draw past the box it was given -- a text too tall
|
||||
// for it -- and the cap is a promise about the length as well. A
|
||||
// share passes through: it is a length only to whoever divides
|
||||
// one, and that is this widget's parent rather than this widget,
|
||||
// which has already given the share the box the cap allows.
|
||||
if let Some(max) = self.max(axis)
|
||||
&& painter.longer_than(size[axis].without_leftover(), max, axis)
|
||||
{
|
||||
size[axis] = max.into();
|
||||
}
|
||||
}
|
||||
size
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
mod layer;
|
||||
mod max_size;
|
||||
mod offset;
|
||||
mod pad;
|
||||
mod scroll;
|
||||
@@ -7,7 +6,6 @@ mod span;
|
||||
mod stack;
|
||||
|
||||
pub use layer::*;
|
||||
pub use max_size::*;
|
||||
pub use offset::*;
|
||||
pub use pad::*;
|
||||
pub use scroll::*;
|
||||
|
||||
+16
-22
@@ -53,8 +53,8 @@ widget_trait! {
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
let widgets = &mut state.ui_mut().widgets;
|
||||
widgets.set_size_rule(id, Axis::X, SizeRule::Exact(size.x));
|
||||
widgets.set_size_rule(id, Axis::Y, SizeRule::Exact(size.y));
|
||||
widgets.set_len(id, Axis::X, size.x);
|
||||
widgets.set_len(id, Axis::Y, size.y);
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -66,15 +66,12 @@ widget_trait! {
|
||||
state
|
||||
.ui_mut()
|
||||
.widgets
|
||||
.set_size_rule(id, Axis::X, SizeRule::from(len));
|
||||
.set_len(id, Axis::X, len);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
/// Answers at least this wide, whatever it drew: a rule beside the
|
||||
/// widget, so what a row gives it is at least this even where the widget
|
||||
/// itself wanted less. The box it draws in is untouched -- for that, see
|
||||
/// [`MaxSize`].
|
||||
/// Sets a floor on this widget's offered width and reported width.
|
||||
fn min_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
let len = len.into();
|
||||
move |state| {
|
||||
@@ -93,25 +90,22 @@ widget_trait! {
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts this in a [`MaxSize`]: it is asked in the shorter of the cap and
|
||||
/// the box that widget was given, and is as long as it used, held to the
|
||||
/// cap. A widget rather than a rule because the box is whoever asked's to
|
||||
/// decide -- see [`MaxSize`].
|
||||
fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
|
||||
/// Caps this widget's offered width and reported width.
|
||||
fn max_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
let len = len.into();
|
||||
move |state| MaxSize {
|
||||
inner: self.add_strong(state),
|
||||
x: Some(len),
|
||||
y: None,
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
state.ui_mut().widgets.set_max_len(id, Axis::X, len);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
fn max_height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
|
||||
fn max_height(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
let len = len.into();
|
||||
move |state| MaxSize {
|
||||
inner: self.add_strong(state),
|
||||
x: None,
|
||||
y: Some(len),
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
state.ui_mut().widgets.set_max_len(id, Axis::Y, len);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +116,7 @@ widget_trait! {
|
||||
state
|
||||
.ui_mut()
|
||||
.widgets
|
||||
.set_size_rule(id, Axis::Y, SizeRule::from(len));
|
||||
.set_len(id, Axis::Y, len);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,16 +35,16 @@ static ALLOCATOR: Counting = Counting;
|
||||
|
||||
#[test]
|
||||
fn unchanged_tree_reuses_layout_storage() {
|
||||
for deferred in [false, true] {
|
||||
for rule in [
|
||||
SizeRule::FREE,
|
||||
leftover(1).clamp(20, 80).into(),
|
||||
SizeRule::clamp(20.into(), 80.into()),
|
||||
] {
|
||||
let mut h = Harness::new((600, 200));
|
||||
let mut children: Vec<StrongWidget> = Vec::new();
|
||||
for _ in 0..8 {
|
||||
let a = rect(Color::RED).add(&mut h.rsc);
|
||||
if deferred {
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(a, Axis::X, leftover(1).clamp(20, 80).into());
|
||||
}
|
||||
h.rsc.widgets_mut().set_size_rule(a, Axis::X, rule.clone());
|
||||
let row = (a, rect(Color::BLUE))
|
||||
.span(Dir::RIGHT)
|
||||
.add_strong(&mut h.rsc);
|
||||
@@ -74,7 +74,7 @@ fn unchanged_tree_reuses_layout_storage() {
|
||||
h.frame();
|
||||
}
|
||||
let allocations = COUNT.replace(None).unwrap();
|
||||
println!("deferred={deferred}: {allocations} allocations over 100 resize frames");
|
||||
println!("rule={rule:?}: {allocations} allocations over 100 resize frames");
|
||||
assert_eq!(allocations, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! CPU comparison of bounds attributes and the former wrapper, using the
|
||||
//! same builder calls and geometry. Run the release executable under perf;
|
||||
//! process totals include the cold frame. MODE=plain|exact|cap, REDRAW=0|1.
|
||||
use iris::{harness::Harness, prelude::*};
|
||||
|
||||
#[test]
|
||||
#[ignore = "instruction-count measurement"]
|
||||
fn bounds_cost() {
|
||||
let mode = std::env::var("MODE").unwrap_or_else(|_| "cap".into());
|
||||
let redraw = std::env::var("REDRAW").is_ok_and(|value| value == "1");
|
||||
let frames = std::env::var("FRAMES")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(2000);
|
||||
let mut h = Harness::new((300, 512));
|
||||
let mut column = Span::empty(Dir::DOWN);
|
||||
let mut leaves = Vec::new();
|
||||
for _ in 0..128 {
|
||||
let first = match mode.as_str() {
|
||||
"plain" => rect(Color::RED).add_strong(&mut h.rsc).any(),
|
||||
"exact" => rect(Color::RED).width(40).add_strong(&mut h.rsc).any(),
|
||||
"cap" => rect(Color::RED).max_width(80).add_strong(&mut h.rsc).any(),
|
||||
_ => panic!("unknown MODE {mode}"),
|
||||
};
|
||||
leaves.push(first.id());
|
||||
let second = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||
let row = Span {
|
||||
children: vec![first, second],
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
};
|
||||
column.push(row.height(4).add_strong(&mut h.rsc));
|
||||
}
|
||||
h.set_root(column);
|
||||
let ids: Vec<_> = h.render.active.keys().copied().collect();
|
||||
println!(
|
||||
"mode={mode}, widgets={}, rule_bytes={}",
|
||||
ids.len(),
|
||||
std::mem::size_of::<SizeRule>()
|
||||
);
|
||||
for frame in 0..frames {
|
||||
if redraw {
|
||||
for &id in &ids {
|
||||
h.rsc.widgets_mut().mark_for_redraw(id);
|
||||
}
|
||||
}
|
||||
let width = if frame % 2 == 0 { 100 } else { 300 };
|
||||
h.resize((width, 512));
|
||||
h.frame();
|
||||
let expected = match mode.as_str() {
|
||||
"plain" => width / 2,
|
||||
"exact" => 40,
|
||||
"cap" => (width / 2).min(80),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
for id in &leaves {
|
||||
assert_eq!(h.region(id).unwrap().size().x, Px::from_int(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-13
@@ -84,7 +84,7 @@ fn request_edits_in_a_nested_child_reach_the_allocator() {
|
||||
let inner = (a,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||
let tail = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((inner, tail).span(Dir::RIGHT));
|
||||
h.rsc.ui_mut().widgets.get_mut(&a).unwrap().x = Some(Len::px(40.0));
|
||||
h.rsc.widgets_mut().set_max_len(a, Axis::X, Len::px(40.0));
|
||||
h.frame();
|
||||
assert_corners!(h, a, (0, 0), (40, 100));
|
||||
assert_corners!(h, tail, (40, 0), (300, 100));
|
||||
@@ -109,7 +109,7 @@ fn adding_a_bound_to_a_previously_unbounded_share_reallocates_the_row() {
|
||||
h.frame();
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(id, Axis::X, SizeRule::Max(Len::px(80.0)));
|
||||
.set_size_rule(id, Axis::X, SizeRule::max(Len::px(80.0)));
|
||||
h.frame();
|
||||
assert_corners!(h, id, (0, 0), (80, 100));
|
||||
assert_corners!(h, b, (80, 0), (400, 100));
|
||||
@@ -209,7 +209,7 @@ fn bounds_also_apply_to_shares_discovered_by_drawing() {
|
||||
let a = h.rsc.widgets_mut().add_strong(Unhinted);
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(a.id(), Axis::X, SizeRule::Max(Len::px(80.0)));
|
||||
.set_size_rule(a.id(), Axis::X, SizeRule::max(Len::px(80.0)));
|
||||
let id = a.id();
|
||||
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||
let b_id = b.id();
|
||||
@@ -245,7 +245,7 @@ fn a_measured_nested_share_keeps_its_comparison_for_the_outer_span() {
|
||||
let a_id = a.id();
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(a_id, Axis::X, SizeRule::Max(Len::px(80.0)));
|
||||
.set_size_rule(a_id, Axis::X, SizeRule::max(Len::px(80.0)));
|
||||
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||
let b_id = b.id();
|
||||
let inner = h.rsc.widgets_mut().add_strong(Span {
|
||||
@@ -289,14 +289,11 @@ fn relative_bounds_on_a_hinted_child_track_the_offer_before_its_declared_size()
|
||||
let id = natural.id();
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(id, Axis::X, SizeRule::Max(Len::rel(0.75)));
|
||||
.set_size_rule(id, Axis::X, SizeRule::max(Len::rel(0.75)));
|
||||
h.rsc.widgets_mut().set_size_rule(
|
||||
id,
|
||||
Axis::Y,
|
||||
SizeRule::Clamp {
|
||||
min: Len::rel(0.25),
|
||||
max: Len::rel(0.75),
|
||||
},
|
||||
SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)),
|
||||
);
|
||||
let inner = h.rsc.widgets_mut().add_strong(Stack {
|
||||
children: vec![natural],
|
||||
@@ -313,10 +310,7 @@ fn relative_bounds_on_a_hinted_child_track_the_offer_before_its_declared_size()
|
||||
h.rsc.widgets_mut().set_size_rule(
|
||||
bounded.id(),
|
||||
Axis::X,
|
||||
SizeRule::Clamp {
|
||||
min: Len::rel(0.25),
|
||||
max: Len::rel(0.75),
|
||||
},
|
||||
SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)),
|
||||
);
|
||||
let fixed = wtext("one line, overflowing whatever it is given")
|
||||
.size(16)
|
||||
@@ -389,3 +383,18 @@ fn moving_scroll_content_preserves_its_resolved_expression_size() {
|
||||
h.frame();
|
||||
assert_corners!(h, leaf_id, (0, 240), (300, 360));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_intrinsic_share_cap_uses_the_allocators_fractional_base() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let head = rect(Color::BLUE).width(30).add(&mut h.rsc);
|
||||
let bounded = rect(Color::RED).max_width(Len::rel(0.25)).add(&mut h.rsc);
|
||||
let tail = rect(Color::GREEN).add(&mut h.rsc);
|
||||
h.set_root((head, bounded, tail).span(Dir::RIGHT));
|
||||
assert_corners!(h, bounded, (30, 0), (105, 100));
|
||||
assert_corners!(h, tail, (105, 0), (300, 100));
|
||||
h.resize((400, 100));
|
||||
h.frame();
|
||||
assert_corners!(h, bounded, (30, 0), (130, 100));
|
||||
assert_corners!(h, tail, (130, 0), (400, 100));
|
||||
}
|
||||
+226
-25
@@ -824,7 +824,7 @@ fn row_under_fractions(kid: Option<LayoutLen>, gap: f32, box_w: f32) -> (Vec<Px>
|
||||
if let Some(len) = kid {
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(r.id(), Axis::X, SizeRule::Exact(len));
|
||||
.set_size_rule(r.id(), Axis::X, SizeRule::from(len));
|
||||
}
|
||||
ids.push(r.id());
|
||||
kids.push(r.add_strong(&mut h.rsc));
|
||||
@@ -1005,10 +1005,6 @@ fn a_region_node_root_is_a_region_node() {
|
||||
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900));
|
||||
}
|
||||
|
||||
/// A bound is a rule about what a widget answers: it holds the length that
|
||||
/// reaches whoever asked and leaves the box alone. Here the content is 400
|
||||
/// wide in a 250 window, so a cap cuts what the row reports and a floor
|
||||
/// raises it, while the rects inside stay where the 250 box put them.
|
||||
#[test]
|
||||
fn a_bound_holds_what_a_widget_answers() {
|
||||
let row = |rule: SizeRule| {
|
||||
@@ -1023,51 +1019,37 @@ fn a_bound_holds_what_a_widget_answers() {
|
||||
h.region(&left).unwrap().size().x,
|
||||
)
|
||||
};
|
||||
let (capped, left) = row(SizeRule::Max(Len::px(300.0)));
|
||||
let (capped, left) = row(SizeRule::max(Len::px(300.0)));
|
||||
assert_eq!(capped, Px::from_int(300), "the cap, not the 400 drawn");
|
||||
assert_eq!(left, Px::from_int(200), "the box the children were given");
|
||||
|
||||
let (floored, _) = row(SizeRule::Min(Len::px(600.0)));
|
||||
let (floored, _) = row(SizeRule::min(Len::px(600.0)));
|
||||
assert_eq!(floored, Px::from_int(600), "the floor, not the 400 drawn");
|
||||
|
||||
let (free, _) = row(SizeRule::Free);
|
||||
let (free, _) = row(SizeRule::FREE);
|
||||
assert_eq!(free, Px::from_int(400), "what it drew");
|
||||
}
|
||||
|
||||
/// A cap on the box is `MaxSize`, which asks its child in the shorter of the
|
||||
/// cap and its own box. That is the box a text wraps at and a scroll takes
|
||||
/// its viewport from, so it cannot be had by holding the answer.
|
||||
#[test]
|
||||
fn a_cap_widget_asks_its_child_in_the_shorter_box() {
|
||||
fn a_cap_attribute_narrows_the_widgets_box() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
// A fraction of its box, so it says what box it was asked in.
|
||||
let fills = rect(Color::RED).width(rel(1.0)).add(&mut h.rsc);
|
||||
let capped = fills.max_width(300).add(&mut h.rsc);
|
||||
assert_eq!(fills.id(), capped.id());
|
||||
h.set_root(capped);
|
||||
|
||||
assert_eq!(h.region(&fills).unwrap().size().x, Px::from_int(300));
|
||||
assert_eq!(
|
||||
h.region(&capped).unwrap().size().x,
|
||||
Px::from_int(300),
|
||||
"as long as its child used"
|
||||
);
|
||||
|
||||
// A child that asked for a share takes the box the cap allows, and the
|
||||
// share itself passes up: whoever divides one is this widget's parent.
|
||||
let mut h = Harness::new((400, 200));
|
||||
let share = rect(Color::RED).add(&mut h.rsc);
|
||||
let capped = share.max_width(300).add(&mut h.rsc);
|
||||
h.set_root(capped);
|
||||
|
||||
assert_eq!(h.region(&share).unwrap().size().x, Px::from_int(300));
|
||||
assert_eq!(h.region(&capped).unwrap().size().x, Px::from_int(400));
|
||||
}
|
||||
|
||||
/// Which of the cap and the box is shorter is a question in pixels, so it is
|
||||
/// asked again wherever the answer can change -- and the widget asking it is
|
||||
/// drawn again whenever its own box is, which is what keeps the two in step.
|
||||
#[test]
|
||||
fn a_cap_widget_is_decided_again_on_either_side_of_the_crossing() {
|
||||
fn a_cap_attribute_is_decided_again_on_either_side_of_the_crossing() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
h.set_root(probe.max_width(300));
|
||||
@@ -1116,3 +1098,222 @@ fn a_cap_holds_an_answer_that_overflowed_its_box() {
|
||||
// row draws 400 of it. Its answer is the cap, and the window centres it.
|
||||
assert_corners!(h, row, (-25, 0), (275, 200));
|
||||
}
|
||||
|
||||
struct Offered {
|
||||
seen: Rc<Cell<PxVec2>>,
|
||||
answer: Size,
|
||||
}
|
||||
|
||||
impl Widget for Offered {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.seen.set(painter.px_size());
|
||||
painter.primitive(RectPrimitive::color(Color::RED));
|
||||
self.answer
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounds_constrain_the_offer_without_replacing_an_intrinsic_answer() {
|
||||
for axis in Axis::BOTH {
|
||||
for node in [false, true] {
|
||||
let mut h = Harness::new((400, 400));
|
||||
let seen = Rc::new(Cell::new(PxVec2::ZERO));
|
||||
let probe = h.rsc.widgets_mut().add_strong(Offered {
|
||||
seen: seen.clone(),
|
||||
answer: Size::px(Vec2::new(40.0, 40.0)),
|
||||
});
|
||||
let id = probe.id();
|
||||
h.rsc.widgets_mut().set_region_node(id, node);
|
||||
h.rsc.widgets_mut().set_max_len(id, axis, 100.into());
|
||||
h.state.root = Some(probe);
|
||||
h.frame();
|
||||
assert_eq!(seen.get()[axis], Px::from_int(100));
|
||||
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40));
|
||||
h.rsc.widgets_mut().set_min_len(id, axis, 60.into());
|
||||
h.frame();
|
||||
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(60));
|
||||
h.resize((50, 50));
|
||||
h.frame();
|
||||
assert_eq!(seen.get()[axis], Px::from_int(60));
|
||||
h.rsc.widgets_mut().set_size_rule(id, axis, SizeRule::FREE);
|
||||
h.frame();
|
||||
assert_eq!(seen.get()[axis], Px::from_int(50));
|
||||
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cap_attribute_is_the_scroll_viewport() {
|
||||
for node in [false, true] {
|
||||
let mut h = Harness::new((400, 400));
|
||||
let content = rect(Color::RED).height(400).add(&mut h.rsc);
|
||||
let inner = content.add_strong(&mut h.rsc);
|
||||
let scroll = Scroll::new(inner, Axis::Y).max_height(100).add(&mut h.rsc);
|
||||
h.rsc.widgets_mut().set_region_node(scroll, node);
|
||||
h.set_root(scroll);
|
||||
assert_corners!(h, scroll, (0, 150), (400, 250));
|
||||
assert_corners!(h, content, (0, -150), (400, 250));
|
||||
h.rsc.widgets_mut().get_mut(&scroll).unwrap().scroll(1000.0);
|
||||
h.frame();
|
||||
assert_corners!(h, content, (0, 150), (400, 550));
|
||||
h.resize((400, 80));
|
||||
h.frame();
|
||||
assert_corners!(h, scroll, (0, 0), (400, 80));
|
||||
assert_corners!(h, content, (0, 0), (400, 400));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cap_attribute_wraps_text_before_it_answers() {
|
||||
let mut h = Harness::new((400, 500));
|
||||
let text = wtext("one two three four five six seven eight nine ten")
|
||||
.size(16)
|
||||
.wrap(true)
|
||||
.max_width(80)
|
||||
.align(Align::TOP_LEFT)
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(text);
|
||||
let capped = h.region(&text).unwrap().size();
|
||||
assert!(capped.x <= Px::from_int(80));
|
||||
assert!(capped.y > Px::from_int(30));
|
||||
h.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(text, Axis::X, SizeRule::FREE);
|
||||
h.frame();
|
||||
let free = h.region(&text).unwrap().size();
|
||||
assert!(free.x > capped.x);
|
||||
assert!(free.y < capped.y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimensions_and_bounds_are_independent_attributes_in_either_order() {
|
||||
for bounds_first in [false, true] {
|
||||
let mut h = Harness::new((400, 400));
|
||||
let probe = rect(Color::RED).add(&mut h.rsc);
|
||||
let bounded = if bounds_first {
|
||||
probe
|
||||
.max_width(80)
|
||||
.min_height(60)
|
||||
.width(120)
|
||||
.height(40)
|
||||
.add(&mut h.rsc)
|
||||
} else {
|
||||
probe
|
||||
.width(120)
|
||||
.height(40)
|
||||
.max_width(80)
|
||||
.min_height(60)
|
||||
.add(&mut h.rsc)
|
||||
};
|
||||
assert_eq!(probe.id(), bounded.id());
|
||||
h.set_root(bounded);
|
||||
assert_eq!(
|
||||
h.region(&probe).unwrap().size(),
|
||||
PxVec2::from_f32((80, 60).into())
|
||||
);
|
||||
h.rsc.widgets_mut().set_len(probe, Axis::X, 50);
|
||||
h.rsc.widgets_mut().set_len(probe, Axis::Y, 100);
|
||||
h.frame();
|
||||
assert_eq!(
|
||||
h.region(&probe).unwrap().size(),
|
||||
PxVec2::from_f32((50, 100).into())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_a_share_cap_replaces_it_without_losing_the_share() {
|
||||
let mut h = Harness::new((300, 100));
|
||||
let capped = rect(Color::RED)
|
||||
.max_width(80)
|
||||
.width(leftover(1))
|
||||
.add(&mut h.rsc);
|
||||
let sibling = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((capped, sibling).span(Dir::RIGHT));
|
||||
assert_corners!(h, capped, (0, 0), (80, 100));
|
||||
assert_corners!(h, sibling, (80, 0), (300, 100));
|
||||
h.rsc.widgets_mut().set_max_len(capped, Axis::X, 160.into());
|
||||
h.frame();
|
||||
assert_corners!(h, capped, (0, 0), (150, 100));
|
||||
assert_corners!(h, sibling, (150, 0), (300, 100));
|
||||
}
|
||||
|
||||
// Reduced from seed 104 at depth 5: a widget widening its own window
|
||||
// contract must not erase the bound's crossing at a quarter-window of 173.
|
||||
#[test]
|
||||
fn a_widgets_window_contract_cannot_widen_its_bounds_contract() {
|
||||
fn tree(h: &mut Harness) -> WidgetId {
|
||||
let content = rect(Color::RED)
|
||||
.width(137)
|
||||
.min_height(194)
|
||||
.add_strong(&mut h.rsc);
|
||||
let scroll = Scroll::new(content, Axis::X).add_strong(&mut h.rsc);
|
||||
let probe = rect(Color::RED).add_strong(&mut h.rsc);
|
||||
let id = probe.id();
|
||||
let wide = rect(Color::GREEN).add_strong(&mut h.rsc);
|
||||
let narrow = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||
let branch = iris::random::Branch {
|
||||
probe,
|
||||
wide,
|
||||
narrow,
|
||||
threshold: 459.0,
|
||||
}
|
||||
.min_width(94)
|
||||
.max_width(173)
|
||||
.add_strong(&mut h.rsc);
|
||||
let stack = Stack {
|
||||
children: vec![scroll, branch],
|
||||
size: StackSize::Child(0),
|
||||
}
|
||||
.add_strong(&mut h.rsc);
|
||||
let mut children: Vec<StrongWidget> = (0..3)
|
||||
.map(|_| rect(Color::RED).add_strong(&mut h.rsc).any())
|
||||
.collect();
|
||||
children.push(stack);
|
||||
h.set_root(
|
||||
Span {
|
||||
children,
|
||||
dir: Dir::RIGHT,
|
||||
gap: Px::ZERO,
|
||||
}
|
||||
.height(rel(1)),
|
||||
);
|
||||
id
|
||||
}
|
||||
let mut warm = Harness::new((1920, 1200));
|
||||
let probe = tree(&mut warm);
|
||||
warm.resize((640, 900));
|
||||
warm.frame();
|
||||
assert_corners!(warm, probe, (480, 0), (640, 40));
|
||||
let mut cold = Harness::new((640, 900));
|
||||
let other = tree(&mut cold);
|
||||
assert_eq!(warm.region(&probe), cold.region(&other));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fixed_declaration_keeps_its_cap_when_the_row_has_no_leftover() {
|
||||
let mut h = Harness::new((100, 100));
|
||||
let fixed = rect(Color::RED).width(200).max_width(100).add(&mut h.rsc);
|
||||
let share = rect(Color::BLUE).add(&mut h.rsc);
|
||||
h.set_root((fixed, share).span(Dir::RIGHT));
|
||||
assert_corners!(h, fixed, (0, 0), (100, 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_bound_reaches_the_parent_even_when_the_current_answer_is_unchanged() {
|
||||
let mut h = Harness::new((400, 100));
|
||||
let leaf = rect(Color::RED).add(&mut h.rsc);
|
||||
let inner = leaf.add_strong(&mut h.rsc);
|
||||
let root = Scroll::new(inner, Axis::Y)
|
||||
.pad(Padding::uniform(0))
|
||||
.width(154)
|
||||
.height(100)
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(root);
|
||||
assert_eq!(h.region(&leaf).unwrap().size().x, Px::from_int(154));
|
||||
h.rsc.widgets_mut().set_min_len(leaf, Axis::X, 140.into());
|
||||
h.rsc.widgets_mut().set_len(root, Axis::X, 78);
|
||||
h.frame();
|
||||
assert_corners!(h, leaf, (130, 0), (270, 100));
|
||||
}
|
||||
+2
-2
@@ -31,8 +31,8 @@ fn some_edits(seed: u64, of: &Plan) -> Edits {
|
||||
(
|
||||
i,
|
||||
SizeRules {
|
||||
x: SizeRule::Exact(LayoutLen::LEFTOVER),
|
||||
y: SizeRule::Free,
|
||||
x: SizeRule::from(LayoutLen::LEFTOVER),
|
||||
y: SizeRule::FREE,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -51,15 +51,33 @@ fn relative_intrinsic_bounds_agree_warm_and_cold() {
|
||||
grown.walk_mut(&mut |node| {
|
||||
if let Some(rules) = &mut node.size {
|
||||
for axis in Axis::BOTH {
|
||||
rules[axis] = match rules[axis] {
|
||||
SizeRule::Min(_) => SizeRule::Min(Len::rel(0.25)),
|
||||
SizeRule::Max(_) => SizeRule::Max(Len::rel(0.75)),
|
||||
SizeRule::Clamp { .. } => SizeRule::Clamp {
|
||||
min: Len::rel(0.25),
|
||||
max: Len::rel(0.75),
|
||||
},
|
||||
ref rule => rule.clone(),
|
||||
};
|
||||
let bound = &mut rules[axis].bound;
|
||||
if bound.min.is_some() {
|
||||
bound.min = Some(Len::rel(0.25));
|
||||
}
|
||||
if bound.max.is_some() {
|
||||
bound.max = Some(Len::rel(0.75));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferred_requests_with_independent_bounds_agree_warm_and_cold() {
|
||||
check_requests(|grown| {
|
||||
let mut index = 0;
|
||||
grown.walk_mut(&mut |node| {
|
||||
if let Some(rules) = &mut node.size {
|
||||
for axis in Axis::BOTH {
|
||||
index += 1;
|
||||
rules[axis].request = Some(match index % 4 {
|
||||
0 => leftover(1).into(),
|
||||
1 => rel(0.5).into(),
|
||||
2 => px(80).into(),
|
||||
_ => (leftover(1) + px(30)).min(leftover(2)),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -264,7 +264,7 @@ fn layout_cost() {
|
||||
harness
|
||||
.rsc
|
||||
.widgets_mut()
|
||||
.set_size_rule(sized, Axis::X, SizeRule::Exact(len));
|
||||
.set_size_rule(sized, Axis::X, SizeRule::from(len));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+3
-14
@@ -200,8 +200,8 @@ fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
|
||||
fn a_rule(rng: &mut Rng) -> SizeRule {
|
||||
let len = Len::px(20.0 + rng.below(180) as f32);
|
||||
match rng.below(4) {
|
||||
0 => SizeRule::Max(len),
|
||||
1 => SizeRule::Min(len),
|
||||
0 => SizeRule::max(len),
|
||||
1 => SizeRule::min(len),
|
||||
_ => LayoutLen::from(len).into(),
|
||||
}
|
||||
}
|
||||
@@ -357,18 +357,7 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu
|
||||
/// buildable from what the failure printed.
|
||||
fn describe(id: WidgetId, h: &Harness) -> String {
|
||||
let rules = h.rsc.widgets().size_rules(id).clone();
|
||||
// A bound prints as itself: a failure is reproduced from what it printed,
|
||||
// and a rule shown as "no rule" cannot be written out again.
|
||||
let rule = |r: SizeRule| match r {
|
||||
SizeRule::Free => "-".into(),
|
||||
SizeRule::Exact(len) => format!("{len}"),
|
||||
SizeRule::Request(request) => format!("{request:?}"),
|
||||
SizeRule::Min(min) => format!(">{}", LayoutLen::from(min)),
|
||||
SizeRule::Max(max) => format!("<{}", LayoutLen::from(max)),
|
||||
SizeRule::Clamp { min, max } => {
|
||||
format!(">{}<{}", LayoutLen::from(min), LayoutLen::from(max))
|
||||
}
|
||||
};
|
||||
let rule = |r: SizeRule| format!("{r:?}");
|
||||
let align = h.rsc.widgets().alignment(id);
|
||||
let side = |a: AxisAlign| {
|
||||
if a == AxisAlign::NEG {
|
||||
|
||||
Reference in new issue
Block a user