Constrain offered boxes with independent widget size bounds

This commit is contained in:
iris-ai committed 2026-09-20 19:47:32 -04:00
1 parent 4cb6f6882a
commit 2ac0843cb2
18 files changed
+554 -379

No files matched your search

+66 -65
View File
@@ -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();
if bound.has_fraction() {
inputs[axis].rel_base = Some(self.rel_base[axis]);
let bound = rules[axis].bound;
if bound != Bound::ANY {
if bound.has_fraction() {
inputs[axis].rel_base = Some(self.rel_base[axis]);
}
// 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);
}
}
bounds[axis] = bound.within_len(base);
}
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
View File
@@ -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
+27 -25
View File
@@ -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 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()))
let request = match &rule.request {
Some(request) => self.arena.import(request, self.rel_base),
None => {
let widget = self.widgets.get_dyn(child.id())?;
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: 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 {
+57 -87
View File
@@ -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>,
+10
View File
@@ -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) {