Make alignment a widget property
This commit is contained in:
1 parent
8220a78d4a
commit
d3b0ebf90c
21 files changed
+823
-300
No files matched your search
@@ -40,6 +40,7 @@ pub(crate) enum Counter {
|
|||||||
ReuseWrongParent,
|
ReuseWrongParent,
|
||||||
ReuseRemapped,
|
ReuseRemapped,
|
||||||
ReuseOutside,
|
ReuseOutside,
|
||||||
|
PlaceRedraws,
|
||||||
QueuePops,
|
QueuePops,
|
||||||
DepthReads,
|
DepthReads,
|
||||||
LocalRedraws,
|
LocalRedraws,
|
||||||
@@ -72,6 +73,7 @@ impl Counter {
|
|||||||
"reuse: wrong parent",
|
"reuse: wrong parent",
|
||||||
"reuse remapped",
|
"reuse remapped",
|
||||||
"reuse: outside what it holds for",
|
"reuse: outside what it holds for",
|
||||||
|
"placed by redrawing",
|
||||||
"redraw queue pops",
|
"redraw queue pops",
|
||||||
"depth reads",
|
"depth reads",
|
||||||
"local redraws",
|
"local redraws",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::vec2;
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
pub struct Align {
|
pub struct Align {
|
||||||
pub x: Option<AxisAlign>,
|
pub x: Option<AxisAlign>,
|
||||||
pub y: Option<AxisAlign>,
|
pub y: Option<AxisAlign>,
|
||||||
@@ -30,20 +30,30 @@ impl Align {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
/// Where a widget sits in a box longer than it is. The default is the middle,
|
||||||
pub enum AxisAlign {
|
/// because the two edges are the ones that assume a direction: which of them
|
||||||
Neg,
|
/// is the near one depends on the writing system and on which way a container
|
||||||
Center,
|
/// runs, and the middle is the same either way.
|
||||||
Pos,
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
}
|
pub struct AxisAlign(f32);
|
||||||
|
|
||||||
impl AxisAlign {
|
impl AxisAlign {
|
||||||
pub const fn rel(&self) -> f32 {
|
pub const NEG: Self = Self::new(0.0);
|
||||||
match self {
|
pub const CENTER: Self = Self::new(0.5);
|
||||||
Self::Neg => 0.0,
|
pub const POS: Self = Self::new(1.0);
|
||||||
Self::Center => 0.5,
|
|
||||||
Self::Pos => 1.0,
|
pub const fn new(rel: f32) -> Self {
|
||||||
|
Self(rel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn rel(&self) -> f32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AxisAlign {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::CENTER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,34 +63,57 @@ pub struct CardinalAlign {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CardinalAlign {
|
impl CardinalAlign {
|
||||||
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::Neg);
|
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::NEG);
|
||||||
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::Center);
|
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::CENTER);
|
||||||
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::Pos);
|
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::POS);
|
||||||
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::Neg);
|
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::NEG);
|
||||||
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::Center);
|
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::CENTER);
|
||||||
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::Pos);
|
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::POS);
|
||||||
|
|
||||||
pub const fn new(axis: Axis, align: AxisAlign) -> Self {
|
pub const fn new(axis: Axis, align: AxisAlign) -> Self {
|
||||||
Self { axis, align }
|
Self { axis, align }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
pub struct RegionAlign {
|
pub struct RegionAlign {
|
||||||
pub x: AxisAlign,
|
pub x: AxisAlign,
|
||||||
pub y: AxisAlign,
|
pub y: AxisAlign,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegionAlign {
|
impl RegionAlign {
|
||||||
pub const TOP_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Neg);
|
/// Both axes at the near edge. What a container passes as an override for
|
||||||
pub const TOP_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Neg);
|
/// a child it is going to position itself.
|
||||||
pub const TOP_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Neg);
|
pub const NEAR: Self = Self {
|
||||||
pub const CENTER_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Center);
|
x: AxisAlign::NEG,
|
||||||
pub const CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Center);
|
y: AxisAlign::NEG,
|
||||||
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Center);
|
};
|
||||||
pub const BOT_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Pos);
|
|
||||||
pub const BOT_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Pos);
|
pub fn axis(&self, axis: Axis) -> AxisAlign {
|
||||||
pub const BOT_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Pos);
|
match axis {
|
||||||
|
Axis::X => self.x,
|
||||||
|
Axis::Y => self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut AxisAlign {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &mut self.x,
|
||||||
|
Axis::Y => &mut self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegionAlign {
|
||||||
|
pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG);
|
||||||
|
pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG);
|
||||||
|
pub const TOP_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::NEG);
|
||||||
|
pub const CENTER_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::CENTER);
|
||||||
|
pub const CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::CENTER);
|
||||||
|
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::CENTER);
|
||||||
|
pub const BOT_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::POS);
|
||||||
|
pub const BOT_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::POS);
|
||||||
|
pub const BOT_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::POS);
|
||||||
|
|
||||||
pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
|
pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
|
||||||
Self { x, y }
|
Self { x, y }
|
||||||
@@ -165,8 +198,8 @@ impl From<RegionAlign> for Align {
|
|||||||
impl From<Align> for RegionAlign {
|
impl From<Align> for RegionAlign {
|
||||||
fn from(align: Align) -> Self {
|
fn from(align: Align) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: align.x.unwrap_or(AxisAlign::Center),
|
x: align.x.unwrap_or(AxisAlign::CENTER),
|
||||||
y: align.y.unwrap_or(AxisAlign::Center),
|
y: align.y.unwrap_or(AxisAlign::CENTER),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,6 +188,15 @@ impl UiScalar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Both channels by the same factor, which is what a fraction of a
|
||||||
|
/// length means when the length is part pixels and part a share.
|
||||||
|
pub const fn scale(&self, by: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
rel: self.rel * by,
|
||||||
|
px: self.px * by,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn offset(mut self, amt: f32) -> Self {
|
pub const fn offset(mut self, amt: f32) -> Self {
|
||||||
self.px += amt;
|
self.px += amt;
|
||||||
self
|
self
|
||||||
|
|||||||
+13
-2
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Holds, LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId,
|
Holds, LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle,
|
||||||
|
UiRegion, WidgetId,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// What is kept of a widget its parent has asked about. `drawn` says whether
|
/// What is kept of a widget its parent has asked about. `drawn` says whether
|
||||||
@@ -8,6 +9,7 @@ use crate::{
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ActiveData {
|
pub struct ActiveData {
|
||||||
pub id: WidgetId,
|
pub id: WidgetId,
|
||||||
|
/// The box its drawing is in, in `parent_move`'s coordinates.
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
/// The box its parent first asked about it in, as a part of the box the
|
/// The box its parent first asked about it in, as a part of the box the
|
||||||
/// parent was itself asked in. Any later box it was given was decided
|
/// parent was itself asked in. Any later box it was given was decided
|
||||||
@@ -18,7 +20,7 @@ pub struct ActiveData {
|
|||||||
pub answer: (Size, [Holds; 2]),
|
pub answer: (Size, [Holds; 2]),
|
||||||
/// What the widget said it used of its box, the last time it drew.
|
/// What the widget said it used of its box, the last time it drew.
|
||||||
pub size: Size,
|
pub size: Size,
|
||||||
/// The pixel lengths of its box, per axis, that its drawing and `size`
|
/// The pixel lengths of `region`, per axis, that its drawing and `size`
|
||||||
/// hold for.
|
/// hold for.
|
||||||
pub holds: [Holds; 2],
|
pub holds: [Holds; 2],
|
||||||
pub drawn: bool,
|
pub drawn: bool,
|
||||||
@@ -39,6 +41,15 @@ pub struct ActiveData {
|
|||||||
/// A change to one moves a box this widget cannot fix by drawing again,
|
/// A change to one moves a box this widget cannot fix by drawing again,
|
||||||
/// and comparing them is what says so.
|
/// and comparing them is what says so.
|
||||||
pub declared: [Option<Len>; 2],
|
pub declared: [Option<Len>; 2],
|
||||||
|
/// The alignment its parent asked it with. A local redraw repeats that
|
||||||
|
/// question, including an override chosen by a container.
|
||||||
|
pub align: RegionAlign,
|
||||||
|
/// Whether that alignment was the parent's override rather than the
|
||||||
|
/// widget's own property.
|
||||||
|
pub align_override: bool,
|
||||||
|
/// Its own alignment when it was last drawn. A change to the property is
|
||||||
|
/// found against this even when its parent overrode the alignment.
|
||||||
|
pub own_align: RegionAlign,
|
||||||
/// The movable region whose coordinates `region` uses.
|
/// The movable region whose coordinates `region` uses.
|
||||||
pub parent_move: MoveIdx,
|
pub parent_move: MoveIdx,
|
||||||
pub mask: MaskIdx,
|
pub mask: MaskIdx,
|
||||||
|
|||||||
+99
-28
@@ -1,8 +1,8 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
use crate::layout_diagnostics::{self as diag, Counter};
|
use crate::layout_diagnostics::{self as diag, Counter};
|
||||||
use crate::{
|
use crate::{
|
||||||
Axis, Holds, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
|
Axis, Holds, Len, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer,
|
||||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, Widgets,
|
TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, Widgets,
|
||||||
render::{
|
render::{
|
||||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
||||||
PrimitiveKind, TexturePrimitive,
|
PrimitiveKind, TexturePrimitive,
|
||||||
@@ -99,16 +99,7 @@ impl<'a> Painter<'a> {
|
|||||||
|
|
||||||
/// Draws a widget within this widget's region.
|
/// Draws a widget within this widget's region.
|
||||||
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
||||||
self.widget_at(id, UiRegion::FULL)
|
self.widget_within(id, UiRegion::FULL)
|
||||||
}
|
|
||||||
|
|
||||||
/// Draws a widget somewhere within this one.
|
|
||||||
pub fn widget_within<'s, W: ?Sized>(
|
|
||||||
&'s mut self,
|
|
||||||
id: &'s StrongWidget<W>,
|
|
||||||
region: UiRegion,
|
|
||||||
) -> DrawResult<'s, 'a, W> {
|
|
||||||
self.widget_at(id, region)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a widget's rules declare its lengths to be, which whoever draws
|
/// What a widget's rules declare its lengths to be, which whoever draws
|
||||||
@@ -127,19 +118,45 @@ impl<'a> Painter<'a> {
|
|||||||
self.state.undraw_rec(id.id(), self.rsc);
|
self.state.undraw_rec(id.id(), self.rsc);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `region` in this widget's own coordinates, and with the child's
|
/// Draws a widget somewhere within this one. `region` is in this widget's
|
||||||
/// declared lengths still to be taken.
|
/// own coordinates, and the child's declared lengths are still to be
|
||||||
fn widget_at<'s, W: ?Sized>(
|
/// taken from it. Where the child's drawing sits inside what it is given
|
||||||
|
/// is the child's alignment, applied where the child is drawn, so a
|
||||||
|
/// container positions a child either by handing it a box of exactly its
|
||||||
|
/// length or by leaving it room and letting its alignment decide.
|
||||||
|
pub fn widget_within<'s, W: ?Sized>(
|
||||||
&'s mut self,
|
&'s mut self,
|
||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
|
) -> DrawResult<'s, 'a, W> {
|
||||||
|
self.widget_at(id, region, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a widget with an alignment chosen by its container rather than
|
||||||
|
/// the widget's property. Containers use this when the box they hand down
|
||||||
|
/// already expresses the size they report around the child.
|
||||||
|
pub fn widget_aligned<'s, W: ?Sized>(
|
||||||
|
&'s mut self,
|
||||||
|
id: &'s StrongWidget<W>,
|
||||||
|
region: UiRegion,
|
||||||
|
align: RegionAlign,
|
||||||
|
) -> DrawResult<'s, 'a, W> {
|
||||||
|
self.widget_at(id, region, Some(align))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn widget_at<'s, W: ?Sized>(
|
||||||
|
&'s mut self,
|
||||||
|
id: &'s StrongWidget<W>,
|
||||||
|
region: UiRegion,
|
||||||
|
align_override: Option<RegionAlign>,
|
||||||
) -> DrawResult<'s, 'a, W> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
let region_node = self.rsc.widgets().is_region_node(id.id());
|
let region_node = self.rsc.widgets().is_region_node(id.id());
|
||||||
let declared = self.declared_lens(id);
|
let declared = self.declared_lens(id);
|
||||||
|
let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.id()));
|
||||||
// Composing `FULL` through a box is not quite the identity in f32,
|
// Composing `FULL` through a box is not quite the identity in f32,
|
||||||
// so a child with nothing declared keeps the box it would have had.
|
// so a child with nothing declared keeps the box it would have had.
|
||||||
let local = match declared.iter().any(Option::is_some) {
|
let local = match declared.iter().any(Option::is_some) {
|
||||||
true => declared_box(region, declared),
|
true => declared_box(region, declared, align),
|
||||||
false => region,
|
false => region,
|
||||||
};
|
};
|
||||||
let within = match local == UiRegion::FULL {
|
let within = match local == UiRegion::FULL {
|
||||||
@@ -161,7 +178,10 @@ impl<'a> Painter<'a> {
|
|||||||
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer),
|
false => self.state.active.get(&id.id()).map_or(local, |a| a.offer),
|
||||||
};
|
};
|
||||||
let answers_offer = self.at_offer && local == offer;
|
let answers_offer = self.at_offer && local == offer;
|
||||||
let size = self.state.draw_inner(
|
// The answer and what it holds for, both about the box asked in. The
|
||||||
|
// child's record may say something else once its drawing has been
|
||||||
|
// placed: a drawing made again in its placed box holds for that box.
|
||||||
|
let (size, holds) = self.state.draw_inner(
|
||||||
id.id(),
|
id.id(),
|
||||||
within,
|
within,
|
||||||
DrawInfo {
|
DrawInfo {
|
||||||
@@ -173,19 +193,18 @@ impl<'a> Painter<'a> {
|
|||||||
mask: self.mask,
|
mask: self.mask,
|
||||||
offer,
|
offer,
|
||||||
offered_px: self.px_within_offer(offer),
|
offered_px: self.px_within_offer(offer),
|
||||||
|
align: align_override,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
self.rsc,
|
self.rsc,
|
||||||
);
|
);
|
||||||
let active = self.state.active.get_mut(&id.id()).unwrap();
|
|
||||||
active.declared = declared;
|
|
||||||
if answers_offer {
|
if answers_offer {
|
||||||
active.answer = (active.size, active.holds);
|
self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds);
|
||||||
}
|
}
|
||||||
// Whatever the child's drawing holds for keeps this one to the boxes
|
// Whatever the child's answer holds for keeps this one to the boxes
|
||||||
// that give the child a length inside it.
|
// that give the child a length inside it.
|
||||||
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) {
|
||||||
*under = under.and(active.holds[axis as usize].through(local.axis(axis).len()));
|
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
||||||
}
|
}
|
||||||
DrawResult {
|
DrawResult {
|
||||||
child: id,
|
child: id,
|
||||||
@@ -232,7 +251,8 @@ impl<'a> Painter<'a> {
|
|||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
) -> Option<Len> {
|
) -> Option<Len> {
|
||||||
let declared = self.declared_lens(child);
|
let declared = self.declared_lens(child);
|
||||||
let local = declared_box(region, declared);
|
let align = self.rsc.widgets().alignment(child.id());
|
||||||
|
let local = declared_box(region, declared, align);
|
||||||
let within = local.within(&self.region);
|
let within = local.within(&self.region);
|
||||||
let first_ask = self.offer(child.id());
|
let first_ask = self.offer(child.id());
|
||||||
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
|
if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) {
|
||||||
@@ -326,6 +346,20 @@ impl<'a> Painter<'a> {
|
|||||||
self.region
|
self.region
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where this widget sits in a box longer than the length it takes. A
|
||||||
|
/// widget that positions its own content reads it to place that content
|
||||||
|
/// the way the box around it would have placed the widget.
|
||||||
|
pub fn alignment(&self) -> RegionAlign {
|
||||||
|
self.rsc.widgets().alignment(self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The part of this widget's box that something of `size` takes, at the
|
||||||
|
/// near edge. A container that reports one child's size gives every child
|
||||||
|
/// this, so what it draws is inside what it says it occupies.
|
||||||
|
pub fn box_of(&self, size: Size) -> UiRegion {
|
||||||
|
placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2])
|
||||||
|
}
|
||||||
|
|
||||||
/// This widget's box in pixels. Reading it makes the drawing one that
|
/// This widget's box in pixels. Reading it makes the drawing one that
|
||||||
/// holds for this box only, until `holds` says how far it goes.
|
/// holds for this box only, until `holds` says how far it goes.
|
||||||
pub fn px_size(&mut self) -> Vec2 {
|
pub fn px_size(&mut self) -> Vec2 {
|
||||||
@@ -455,15 +489,52 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option<Len>; 2]
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The box a drawing occupies: the size the widget reported, on the side of
|
||||||
|
/// the box it was asked in that its alignment says. An axis reported as a
|
||||||
|
/// share fills, because a share is a length only to whoever divides one, and
|
||||||
|
/// whoever did is the one that handed down this box. A declared axis is
|
||||||
|
/// left alone too: `declared_box` already placed it, in the parent's box,
|
||||||
|
/// and the rule's length is what the widget reports there.
|
||||||
|
///
|
||||||
|
/// A reported fraction is a fraction of the box the widget drew in, where a
|
||||||
|
/// declared one is a fraction of the box its parent handed down -- a span
|
||||||
|
/// reporting `rel(1.0)` means all of what it was given, whatever that was a
|
||||||
|
/// fraction of. So this scales by the box rather than composing into it.
|
||||||
|
pub(crate) fn placed_box(
|
||||||
|
region: UiRegion,
|
||||||
|
size: Size,
|
||||||
|
align: RegionAlign,
|
||||||
|
declared: [Option<Len>; 2],
|
||||||
|
) -> UiRegion {
|
||||||
|
let mut placed = region;
|
||||||
|
for (axis, declared) in AXES.into_iter().zip(declared) {
|
||||||
|
let reported = size.axis(axis);
|
||||||
|
if reported.leftover != 0.0 || declared.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let span = placed.axis_mut(axis);
|
||||||
|
let len = span.len().scale(reported.rel) + UiScalar::px(reported.px);
|
||||||
|
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
||||||
|
span.end = span.start + len;
|
||||||
|
}
|
||||||
|
placed
|
||||||
|
}
|
||||||
|
|
||||||
/// Takes a widget's declared lengths in the box `region` is given in, since a
|
/// Takes a widget's declared lengths in the box `region` is given in, since a
|
||||||
/// fraction of a length means a fraction of that one. A caller that already
|
/// fraction of a length means a fraction of that one, and puts what is left
|
||||||
/// reserved the space hands back the same length, so this is the identity
|
/// over on the side its alignment says. A caller that already reserved the
|
||||||
/// for it.
|
/// space hands back the same length, so this is the identity for it.
|
||||||
fn declared_box(mut region: UiRegion, declared: [Option<Len>; 2]) -> UiRegion {
|
pub(crate) fn declared_box(
|
||||||
|
mut region: UiRegion,
|
||||||
|
declared: [Option<Len>; 2],
|
||||||
|
align: RegionAlign,
|
||||||
|
) -> UiRegion {
|
||||||
for (axis, len) in AXES.into_iter().zip(declared) {
|
for (axis, len) in AXES.into_iter().zip(declared) {
|
||||||
let Some(len) = len else { continue };
|
let Some(len) = len else { continue };
|
||||||
let span = region.axis_mut(axis);
|
let span = region.axis_mut(axis);
|
||||||
span.end = span.start + UiScalar::new(len.rel, len.px);
|
let len = UiScalar::new(len.rel, len.px);
|
||||||
|
span.start += (span.len() - len).scale(align.axis(axis).rel());
|
||||||
|
span.end = span.start + len;
|
||||||
}
|
}
|
||||||
region
|
region
|
||||||
}
|
}
|
||||||
+221
-55
@@ -1,9 +1,10 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
||||||
use crate::ui::painter::declared_lens;
|
use crate::ui::painter::{declared_box, declared_lens, placed_box};
|
||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, Axis, DrawLayers, Holds, IdLike, Len, MaskIdx, MoveIdx, Moves, Painter,
|
ActiveData, Axis, DrawLayers, Holds, IdLike, Len, MaskIdx, MoveIdx, Moves, Painter,
|
||||||
PixelRegion, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, Widgets,
|
PixelRegion, RegionAlign, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId,
|
||||||
|
Widgets,
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ pub(super) struct DrawInfo {
|
|||||||
/// that box in pixels.
|
/// that box in pixels.
|
||||||
pub offer: UiRegion,
|
pub offer: UiRegion,
|
||||||
pub offered_px: Vec2,
|
pub offered_px: Vec2,
|
||||||
|
/// A container's answer for where the widget sits. `None` uses the
|
||||||
|
/// widget's own property.
|
||||||
|
pub align: Option<RegionAlign>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UiRenderState {
|
pub struct UiRenderState {
|
||||||
@@ -39,6 +43,13 @@ pub struct UiRenderState {
|
|||||||
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
||||||
/// replaces that while its children go on pointing at the slot.
|
/// replaces that while its children go on pointing at the slot.
|
||||||
slots: HashMap<WidgetId, MoveIdx>,
|
slots: HashMap<WidgetId, MoveIdx>,
|
||||||
|
/// Answers invalidated by a declared-length change below them. These are
|
||||||
|
/// replaced even when retained placement means the redraw is not at the
|
||||||
|
/// old offer.
|
||||||
|
answer_invalid: crate::util::HashSet<WidgetId>,
|
||||||
|
/// Whether this frame contains a declared-length change, so any dirty
|
||||||
|
/// dependent replaces its answer too.
|
||||||
|
replace_answers: bool,
|
||||||
pub moves: Moves,
|
pub moves: Moves,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +61,8 @@ impl UiRenderState {
|
|||||||
output_size: Vec2::ZERO,
|
output_size: Vec2::ZERO,
|
||||||
old_root: None,
|
old_root: None,
|
||||||
slots: Default::default(),
|
slots: Default::default(),
|
||||||
|
answer_invalid: Default::default(),
|
||||||
|
replace_answers: false,
|
||||||
moves: Default::default(),
|
moves: Default::default(),
|
||||||
root_move: MoveIdx::NONE,
|
root_move: MoveIdx::NONE,
|
||||||
resized: false,
|
resized: false,
|
||||||
@@ -91,6 +104,7 @@ impl UiRenderState {
|
|||||||
mask: MaskIdx::NONE,
|
mask: MaskIdx::NONE,
|
||||||
offer: UiRegion::FULL,
|
offer: UiRegion::FULL,
|
||||||
offered_px: self.output_size,
|
offered_px: self.output_size,
|
||||||
|
align: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,12 +145,15 @@ impl UiRenderState {
|
|||||||
// anything dirty settles, so that whatever a new output draws
|
// anything dirty settles, so that whatever a new output draws
|
||||||
// again is drawn once, in the box it will have.
|
// again is drawn once, in the box it will have.
|
||||||
let info = self.root_info();
|
let info = self.root_info();
|
||||||
self.draw_inner(root.id(), UiRegion::FULL, info, None, rsc);
|
let region = Self::root_region(root.id(), rsc.widgets());
|
||||||
|
let answer = self.draw_inner(root.id(), region, info, None, rsc);
|
||||||
|
self.active.get_mut(&root.id()).unwrap().answer = answer;
|
||||||
}
|
}
|
||||||
self.resized = false;
|
self.resized = false;
|
||||||
if rsc.widgets().has_updates() {
|
if rsc.widgets().has_updates() {
|
||||||
self.redraw_updates(rsc);
|
self.redraw_updates(rsc);
|
||||||
}
|
}
|
||||||
|
self.replace_answers = false;
|
||||||
self.free(rsc);
|
self.free(rsc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,10 +165,19 @@ impl UiRenderState {
|
|||||||
self.write_root();
|
self.write_root();
|
||||||
if let Some(id) = root {
|
if let Some(id) = root {
|
||||||
let info = self.root_info();
|
let info = self.root_info();
|
||||||
self.draw_inner(id.id(), UiRegion::FULL, info, None, rsc);
|
let region = Self::root_region(id.id(), rsc.widgets());
|
||||||
|
self.draw_inner(id.id(), region, info, None, rsc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion {
|
||||||
|
declared_box(
|
||||||
|
UiRegion::FULL,
|
||||||
|
declared_lens(widgets, id),
|
||||||
|
widgets.alignment(id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn draw_inner(
|
pub(super) fn draw_inner(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
@@ -159,7 +185,7 @@ impl UiRenderState {
|
|||||||
info: DrawInfo,
|
info: DrawInfo,
|
||||||
mut old: Option<ActiveData>,
|
mut old: Option<ActiveData>,
|
||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) -> Size {
|
) -> (Size, [Holds; 2]) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
{
|
{
|
||||||
diag::bump(Counter::DrawRequests);
|
diag::bump(Counter::DrawRequests);
|
||||||
@@ -171,15 +197,82 @@ impl UiRenderState {
|
|||||||
info.region_node,
|
info.region_node,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if self.active.contains_key(&id) {
|
let own_align = rsc.widgets().alignment(id);
|
||||||
if let Some(size) = self.try_reuse(id, region, info, rsc) {
|
let align = info.align.unwrap_or(own_align);
|
||||||
return size;
|
let replace_answer = self.answer_invalid.remove(&id)
|
||||||
}
|
|| (self.replace_answers
|
||||||
// if not, then maintain resize and track old children to remove unneeded
|
&& (rsc.widgets().needs_redraw.contains(&id)
|
||||||
|
|| self.dirty_size_under(id, rsc.widgets())));
|
||||||
|
let retained = match replace_answer {
|
||||||
|
true => None,
|
||||||
|
false => self
|
||||||
|
.retained_answer(id, region, info, rsc.widgets())
|
||||||
|
.or_else(|| self.try_reuse(id, region, info, rsc)),
|
||||||
|
};
|
||||||
|
let answer = retained.unwrap_or_else(|| {
|
||||||
|
if old.is_none() {
|
||||||
old = self.remove(id, false, rsc);
|
old = self.remove(id, false, rsc);
|
||||||
}
|
}
|
||||||
|
self.draw_at(id, region, info, align, old.take(), rsc)
|
||||||
|
});
|
||||||
|
|
||||||
// draw widget
|
let declared = declared_lens(rsc.widgets(), id);
|
||||||
|
// A near-edge override means the caller already chose this box from
|
||||||
|
// the child's answer. Applying the answer again would compound the
|
||||||
|
// placement; it is also how the second, final ask terminates.
|
||||||
|
let placed = match info.align == Some(RegionAlign::NEAR) {
|
||||||
|
true => region,
|
||||||
|
false => placed_box(region, answer.0, align, declared),
|
||||||
|
};
|
||||||
|
let placed_info = DrawInfo {
|
||||||
|
align: Some(RegionAlign::NEAR),
|
||||||
|
..info
|
||||||
|
};
|
||||||
|
// The symbolic box can be unchanged while its parent slot changed
|
||||||
|
// pixel size. Reuse checks the resolved box even in that case.
|
||||||
|
if self.try_reuse(id, placed, placed_info, rsc).is_none() {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::PlaceRedraws);
|
||||||
|
let old = self.remove(id, false, rsc);
|
||||||
|
self.draw_at(id, placed, placed_info, RegionAlign::NEAR, old, rsc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The answer is only reusable while both parts of the operation are:
|
||||||
|
// what the widget reported in the offered box, and what it drew in
|
||||||
|
// the box its report selected. Express the latter's contract back in
|
||||||
|
// terms of the offered box before handing it to the parent.
|
||||||
|
let drawing_holds = self.active[&id].holds;
|
||||||
|
let mut settled = answer;
|
||||||
|
for axis in AXES {
|
||||||
|
let reported = answer.0.axis(axis);
|
||||||
|
let placed_len = match reported.leftover != 0.0 || declared[axis as usize].is_some() {
|
||||||
|
true => UiScalar::FULL,
|
||||||
|
false => UiScalar::new(reported.rel, reported.px),
|
||||||
|
};
|
||||||
|
settled.1[axis as usize] =
|
||||||
|
settled.1[axis as usize].and(drawing_holds[axis as usize].through(placed_len));
|
||||||
|
}
|
||||||
|
|
||||||
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
|
active.offer = info.offer;
|
||||||
|
active.answer = settled;
|
||||||
|
active.align = align;
|
||||||
|
active.align_override = info.align.is_some();
|
||||||
|
active.own_align = own_align;
|
||||||
|
active.depth = info.depth;
|
||||||
|
settled
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls a widget's `draw` and keeps what it drew in `region`.
|
||||||
|
fn draw_at(
|
||||||
|
&mut self,
|
||||||
|
id: WidgetId,
|
||||||
|
region: UiRegion,
|
||||||
|
info: DrawInfo,
|
||||||
|
align: RegionAlign,
|
||||||
|
old: Option<ActiveData>,
|
||||||
|
rsc: &mut dyn UiRsc,
|
||||||
|
) -> (Size, [Holds; 2]) {
|
||||||
let (move_idx, local, retired_move) = match info.region_node {
|
let (move_idx, local, retired_move) = match info.region_node {
|
||||||
// Its box becomes its movable region, so it draws in that
|
// Its box becomes its movable region, so it draws in that
|
||||||
// region's coordinates and its box is one entry to rewrite.
|
// region's coordinates and its box is one entry to rewrite.
|
||||||
@@ -295,6 +388,7 @@ impl UiRenderState {
|
|||||||
mask,
|
mask,
|
||||||
offer: UiRegion::FULL,
|
offer: UiRegion::FULL,
|
||||||
offered_px: px,
|
offered_px: px,
|
||||||
|
align: None,
|
||||||
},
|
},
|
||||||
rsc,
|
rsc,
|
||||||
);
|
);
|
||||||
@@ -302,7 +396,6 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add to active
|
|
||||||
let active = ActiveData {
|
let active = ActiveData {
|
||||||
id,
|
id,
|
||||||
region,
|
region,
|
||||||
@@ -318,8 +411,10 @@ impl UiRenderState {
|
|||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
size_deps,
|
size_deps,
|
||||||
// Written by whoever draws it, which is what resolves them.
|
declared: declared_lens(rsc.widgets(), id),
|
||||||
declared: [None; 2],
|
align,
|
||||||
|
align_override: info.align.is_some(),
|
||||||
|
own_align: rsc.widgets().alignment(id),
|
||||||
move_idx,
|
move_idx,
|
||||||
parent_move: info.parent_move,
|
parent_move: info.parent_move,
|
||||||
mask,
|
mask,
|
||||||
@@ -327,7 +422,7 @@ impl UiRenderState {
|
|||||||
};
|
};
|
||||||
rsc.on_draw(&active);
|
rsc.on_draw(&active);
|
||||||
self.active.insert(id, active);
|
self.active.insert(id, active);
|
||||||
size
|
(size, holds)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keeps a region node's entry across redraws because descendants retain
|
/// Keeps a region node's entry across redraws because descendants retain
|
||||||
@@ -358,9 +453,9 @@ impl UiRenderState {
|
|||||||
.to_px(self.output_size)
|
.to_px(self.output_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A clean, drawn widget's retained size, if its drawing holds for a box
|
/// A clean widget's retained answer, if that answer holds for a box of
|
||||||
/// of `px`. This observes the answer only; it does not move or otherwise
|
/// `px`. This does not move its drawing, which may already be in the box
|
||||||
/// reuse the widget's drawing.
|
/// that answer placed it in.
|
||||||
pub(super) fn retained_size(
|
pub(super) fn retained_size(
|
||||||
&self,
|
&self,
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
@@ -372,8 +467,38 @@ impl UiRenderState {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let active = self.active.get(&id)?;
|
let active = self.active.get(&id)?;
|
||||||
let valid = active.drawn && active.parent_move == parent_move && active.holds_at(px);
|
let (size, holds) = active.answer;
|
||||||
valid.then_some((active.size, active.holds))
|
let valid = active.drawn
|
||||||
|
&& active.parent_move == parent_move
|
||||||
|
&& holds[0].contains(px.x)
|
||||||
|
&& holds[1].contains(px.y);
|
||||||
|
valid.then_some((size, holds))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The answer to an ask can be retained independently of where its
|
||||||
|
/// drawing ended up. Alignment is exactly that case: the first box is the
|
||||||
|
/// question and the smaller placed box holds the drawing.
|
||||||
|
fn retained_answer(
|
||||||
|
&self,
|
||||||
|
id: WidgetId,
|
||||||
|
region: UiRegion,
|
||||||
|
info: DrawInfo,
|
||||||
|
widgets: &Widgets,
|
||||||
|
) -> Option<(Size, [Holds; 2])> {
|
||||||
|
if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let active = self.active.get(&id)?;
|
||||||
|
let has_region_node = active.move_idx != active.parent_move;
|
||||||
|
if !active.drawn
|
||||||
|
|| has_region_node != info.region_node
|
||||||
|
|| active.parent_move != info.parent_move
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let px = self.px_of(info.parent_move, region);
|
||||||
|
let (size, holds) = active.answer;
|
||||||
|
(holds[0].contains(px.x) && holds[1].contains(px.y)).then_some((size, holds))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether anything whose size this widget's own size was read from is
|
/// Whether anything whose size this widget's own size was read from is
|
||||||
@@ -388,29 +513,42 @@ impl UiRenderState {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pixel size of the box a widget was first asked about in, composed
|
/// The first box a widget was asked about, re-expressed in the coordinate
|
||||||
/// through the boxes its ancestors were asked in.
|
/// space its drawing uses. Keeping the relative box and composing it
|
||||||
fn offered_px(&self, id: WidgetId) -> Vec2 {
|
/// again avoids rebuilding a shifted box from rounded pixel lengths.
|
||||||
let Some(active) = self.active.get(&id) else {
|
fn offered_region(&self, id: WidgetId) -> UiRegion {
|
||||||
return self.output_size;
|
let active = &self.active[&id];
|
||||||
|
let parent_region = match active.parent.and_then(|id| self.active.get(&id)) {
|
||||||
|
Some(parent) if parent.move_idx == active.parent_move => {
|
||||||
|
if parent.move_idx == parent.parent_move {
|
||||||
|
self.offered_region(parent.id)
|
||||||
|
} else {
|
||||||
|
UiRegion::FULL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => UiRegion::FULL,
|
||||||
};
|
};
|
||||||
let parent = match active.parent {
|
let mut offered = match active.offer == UiRegion::FULL {
|
||||||
Some(parent) => self.offered_px(parent),
|
true => parent_region,
|
||||||
None => self.output_size,
|
false => active.offer.within(&parent_region),
|
||||||
};
|
};
|
||||||
let size = active.offer.size();
|
for axis in AXES {
|
||||||
Vec2::new(size.x.to_px(parent.x), size.y.to_px(parent.y))
|
if active.declared[axis as usize].is_some() {
|
||||||
|
*offered.axis_mut(axis) = *active.region.axis(axis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offered
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The drawing a widget already has, kept for a new box if it holds for
|
/// Reuses the actual drawing in a new box if its retained contract holds
|
||||||
/// that box.
|
/// there. Answers retained from a different ask are handled separately.
|
||||||
fn try_reuse(
|
fn try_reuse(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
info: DrawInfo,
|
info: DrawInfo,
|
||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) -> Option<Size> {
|
) -> Option<(Size, [Holds; 2])> {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::ReuseAttempts);
|
diag::bump(Counter::ReuseAttempts);
|
||||||
if rsc.widgets().needs_redraw.contains(&id) {
|
if rsc.widgets().needs_redraw.contains(&id) {
|
||||||
@@ -453,8 +591,12 @@ impl UiRenderState {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let moved = active.region != region;
|
let moved = active.region != region;
|
||||||
let (size, old_region, slot, mask) =
|
let (answer, old_region, slot, mask) = (
|
||||||
(active.size, active.region, active.move_idx, info.mask);
|
(active.size, active.holds),
|
||||||
|
active.region,
|
||||||
|
active.move_idx,
|
||||||
|
info.mask,
|
||||||
|
);
|
||||||
if moved {
|
if moved {
|
||||||
if has_region_node {
|
if has_region_node {
|
||||||
self.moves.set(slot, region);
|
self.moves.set(slot, region);
|
||||||
@@ -487,7 +629,7 @@ impl UiRenderState {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(size)
|
Some(answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-expresses an ordinary retained subtree in a new parent region.
|
/// Re-expresses an ordinary retained subtree in a new parent region.
|
||||||
@@ -610,6 +752,9 @@ impl UiRenderState {
|
|||||||
size_deps: Vec::new(),
|
size_deps: Vec::new(),
|
||||||
move_idx: info.parent_move,
|
move_idx: info.parent_move,
|
||||||
declared: [None; 2],
|
declared: [None; 2],
|
||||||
|
align: RegionAlign::default(),
|
||||||
|
align_override: false,
|
||||||
|
own_align: rsc.widgets().alignment(id),
|
||||||
parent_move: info.parent_move,
|
parent_move: info.parent_move,
|
||||||
mask: info.mask,
|
mask: info.mask,
|
||||||
layer: info.layer,
|
layer: info.layer,
|
||||||
@@ -624,6 +769,8 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.slots.clear();
|
self.slots.clear();
|
||||||
|
self.answer_invalid.clear();
|
||||||
|
self.replace_answers = false;
|
||||||
self.moves.clear();
|
self.moves.clear();
|
||||||
self.root_move = MoveIdx::NONE;
|
self.root_move = MoveIdx::NONE;
|
||||||
self.layers.clear();
|
self.layers.clear();
|
||||||
@@ -638,6 +785,7 @@ impl UiRenderState {
|
|||||||
rsc.on_remove(id);
|
rsc.on_remove(id);
|
||||||
self.remove(id, true, rsc);
|
self.remove(id, true, rsc);
|
||||||
self.drop_slot(id);
|
self.drop_slot(id);
|
||||||
|
self.answer_invalid.remove(&id);
|
||||||
}
|
}
|
||||||
rsc.ui_mut().textures.free();
|
rsc.ui_mut().textures.free();
|
||||||
}
|
}
|
||||||
@@ -748,9 +896,19 @@ impl UiRenderState {
|
|||||||
// to draw -- with the mark left on, so the parent draws it rather
|
// to draw -- with the mark left on, so the parent draws it rather
|
||||||
// than keeping it.
|
// than keeping it.
|
||||||
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared;
|
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared;
|
||||||
|
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
|
||||||
if let Some(parent) = active.parent
|
if let Some(parent) = active.parent
|
||||||
&& (declared_changed || !active.drawn)
|
&& (declared_changed || alignment_changed || !active.drawn)
|
||||||
{
|
{
|
||||||
|
if declared_changed {
|
||||||
|
self.replace_answers = true;
|
||||||
|
let mut at = Some(id);
|
||||||
|
while let Some(next) = at {
|
||||||
|
self.answer_invalid.insert(next);
|
||||||
|
rsc.widgets_mut().needs_redraw.insert(next);
|
||||||
|
at = self.active[&next].parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
rsc.widgets_mut().needs_redraw.insert(id);
|
rsc.widgets_mut().needs_redraw.insert(id);
|
||||||
self.redraw(parent, rsc);
|
self.redraw(parent, rsc);
|
||||||
// Whatever the parent did not draw again is nothing it holds now.
|
// Whatever the parent did not draw again is nothing it holds now.
|
||||||
@@ -762,15 +920,27 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
let region = active.region;
|
let region = active.region;
|
||||||
let region_node = active.parent.is_some() && rsc.widgets().is_region_node(id);
|
let region_node = active.parent.is_some() && rsc.widgets().is_region_node(id);
|
||||||
let offered_px = self.offered_px(id);
|
let asked_in = match active.parent {
|
||||||
|
Some(_) => self.offered_region(id),
|
||||||
|
None => Self::root_region(id, rsc.widgets()),
|
||||||
|
};
|
||||||
|
let offered_px = self.px_of(active.parent_move, asked_in);
|
||||||
let at_offer = same_px(self.px_of(active.parent_move, region), offered_px);
|
let at_offer = same_px(self.px_of(active.parent_move, region), offered_px);
|
||||||
// Asked again where its parent asked: a box decided from its own
|
let parent_must_place = active.parent.is_some()
|
||||||
// answer gives that answer back whatever the content now says. Only
|
&& (!region_node || active.align_override)
|
||||||
// a region node can be redrawn away from its current box without
|
&& !same_pixel_region(
|
||||||
// first involving the parent that chose that box.
|
self.moves
|
||||||
|
.resolve(active.parent_move, region)
|
||||||
|
.to_px(self.output_size),
|
||||||
|
self.moves
|
||||||
|
.resolve(active.parent_move, asked_in)
|
||||||
|
.to_px(self.output_size),
|
||||||
|
);
|
||||||
|
// An independently positioned region node can redraw at its offer
|
||||||
|
// and move its slot to its own placement. Every other widget needs
|
||||||
|
// its parent to reproduce a different final position.
|
||||||
if let Some(parent) = active.parent
|
if let Some(parent) = active.parent
|
||||||
&& !region_node
|
&& parent_must_place
|
||||||
&& !at_offer
|
|
||||||
{
|
{
|
||||||
rsc.widgets_mut().needs_redraw.insert(id);
|
rsc.widgets_mut().needs_redraw.insert(id);
|
||||||
self.redraw(parent, rsc);
|
self.redraw(parent, rsc);
|
||||||
@@ -786,23 +956,15 @@ impl UiRenderState {
|
|||||||
mask: active.mask,
|
mask: active.mask,
|
||||||
offer: active.offer,
|
offer: active.offer,
|
||||||
offered_px,
|
offered_px,
|
||||||
|
align: active.align_override.then_some(active.align),
|
||||||
};
|
};
|
||||||
let (was_answer, was) = (active.answer, (active.size, active.holds));
|
let (was_answer, was) = (active.answer, (active.size, active.holds));
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::LocalRedraws);
|
diag::bump(Counter::LocalRedraws);
|
||||||
|
|
||||||
let mut asked_in = region;
|
|
||||||
if !at_offer {
|
|
||||||
for axis in AXES {
|
|
||||||
let span = asked_in.axis_mut(axis);
|
|
||||||
span.end = span.start + UiScalar::px(offered_px.axis(axis));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let old = self.remove(id, false, rsc);
|
let old = self.remove(id, false, rsc);
|
||||||
let size = self.draw_inner(id, asked_in, info, old, rsc);
|
let answer = self.draw_inner(id, asked_in, info, old, rsc);
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
self.active.get_mut(&id).unwrap().answer = answer;
|
||||||
let answer = (size, active.holds);
|
|
||||||
active.answer = answer;
|
|
||||||
let Some(parent) = info.parent else {
|
let Some(parent) = info.parent else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -834,6 +996,10 @@ fn same_px(a: Vec2, b: Vec2) -> bool {
|
|||||||
Holds::at(a.x).contains(b.x) && Holds::at(a.y).contains(b.y)
|
Holds::at(a.x).contains(b.x) && Holds::at(a.y).contains(b.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool {
|
||||||
|
same_px(a.top_left, b.top_left) && same_px(a.bot_right, b.bot_right)
|
||||||
|
}
|
||||||
|
|
||||||
/// A retained region rewritten from one parent box into another. A fixed
|
/// A retained region rewritten from one parent box into another. A fixed
|
||||||
/// source extent can be translated but cannot recover fractions for a resize.
|
/// source extent can be translated but cannot recover fractions for a resize.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use crate::{SizeRules, Widget};
|
use crate::{RegionAlign, SizeRules, Widget};
|
||||||
|
|
||||||
pub struct WidgetData {
|
pub struct WidgetData {
|
||||||
pub widget: Box<dyn Widget>,
|
pub widget: Box<dyn Widget>,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub(super) region_node: bool,
|
pub(super) region_node: bool,
|
||||||
pub(super) size: SizeRules,
|
pub(super) size: SizeRules,
|
||||||
|
pub(super) align: RegionAlign,
|
||||||
/// dynamic borrow checking
|
/// dynamic borrow checking
|
||||||
pub borrowed: bool,
|
pub borrowed: bool,
|
||||||
}
|
}
|
||||||
@@ -20,6 +21,7 @@ impl WidgetData {
|
|||||||
label,
|
label,
|
||||||
region_node: false,
|
region_node: false,
|
||||||
size: SizeRules::default(),
|
size: SizeRules::default(),
|
||||||
|
align: RegionAlign::default(),
|
||||||
borrowed: false,
|
borrowed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Axis, IdLike, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
|
Axis, AxisAlign, IdLike, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget,
|
||||||
|
WidgetData, WidgetId,
|
||||||
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,6 +137,23 @@ impl Widgets {
|
|||||||
self.needs_redraw.insert(id);
|
self.needs_redraw.insert(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where this widget sits in a box longer than the length it takes.
|
||||||
|
pub fn alignment(&self, id: impl IdLike) -> RegionAlign {
|
||||||
|
self.data(id).unwrap().align
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets one axis's alignment. Which box a widget ends up in is its
|
||||||
|
/// parent's to decide, so this is escalated the way a length rule is.
|
||||||
|
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
|
||||||
|
let id = id.id();
|
||||||
|
let data = self.data_mut(id).unwrap();
|
||||||
|
if *data.align.axis_mut(axis) == align {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*data.align.axis_mut(axis) = align;
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
/// Both axes at once, for a caller holding a pair.
|
/// Both axes at once, for a caller holding a pair.
|
||||||
pub fn set_size_rules(
|
pub fn set_size_rules(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|||||||
+68
-16
@@ -11,6 +11,10 @@ use std::collections::HashMap;
|
|||||||
/// The declared lengths of one widget carrying a size rule, by axis.
|
/// The declared lengths of one widget carrying a size rule, by axis.
|
||||||
pub type Lens = [Option<Len>; 2];
|
pub type Lens = [Option<Len>; 2];
|
||||||
|
|
||||||
|
/// Where one widget carrying an alignment sits, by axis. `None` uses the
|
||||||
|
/// centered default.
|
||||||
|
pub type Aligns = [Option<AxisAlign>; 2];
|
||||||
|
|
||||||
/// What a test changes between two trees grown from the same seed, so the
|
/// What a test changes between two trees grown from the same seed, so the
|
||||||
/// warm one can be mutated and the cold one grown that way to begin with.
|
/// warm one can be mutated and the cold one grown that way to begin with.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -19,6 +23,12 @@ pub struct Edits {
|
|||||||
pub sizes: HashMap<usize, Lens>,
|
pub sizes: HashMap<usize, Lens>,
|
||||||
/// Which children a span has, by the order the spans were made.
|
/// Which children a span has, by the order the spans were made.
|
||||||
pub spans: HashMap<usize, SpanEdit>,
|
pub spans: HashMap<usize, SpanEdit>,
|
||||||
|
/// Alignments, by the order they were put on.
|
||||||
|
pub aligns: HashMap<usize, Aligns>,
|
||||||
|
/// Which widgets own a movable region, by the order they were offered
|
||||||
|
/// one. Region nodes change what a move writes and how deep a primitive's
|
||||||
|
/// chain is, so a tree that never grows one leaves both untested.
|
||||||
|
pub nodes: HashMap<usize, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
@@ -76,6 +86,8 @@ const WORDS: &str = "Wrapping shapes one source into as many lines as the box \
|
|||||||
pub struct Tree {
|
pub struct Tree {
|
||||||
pub ids: Vec<WidgetId>,
|
pub ids: Vec<WidgetId>,
|
||||||
pub sized: Vec<WidgetId>,
|
pub sized: Vec<WidgetId>,
|
||||||
|
pub aligned: Vec<WidgetId>,
|
||||||
|
pub nodes: Vec<WidgetId>,
|
||||||
pub spans: Vec<Spanned>,
|
pub spans: Vec<Spanned>,
|
||||||
pub scrolls: Vec<WeakWidget<Scroll>>,
|
pub scrolls: Vec<WeakWidget<Scroll>>,
|
||||||
/// Children a `SpanEdit` took out, held so that dropping the last share
|
/// Children a `SpanEdit` took out, held so that dropping the last share
|
||||||
@@ -180,26 +192,32 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
|||||||
fn align(&mut self) -> Align {
|
fn align(&mut self) -> Align {
|
||||||
let mut axis = || match self.rng.below(4) {
|
let mut axis = || match self.rng.below(4) {
|
||||||
0 => None,
|
0 => None,
|
||||||
1 => Some(AxisAlign::Neg),
|
1 => Some(AxisAlign::NEG),
|
||||||
2 => Some(AxisAlign::Center),
|
2 => Some(AxisAlign::CENTER),
|
||||||
_ => Some(AxisAlign::Pos),
|
_ => Some(AxisAlign::POS),
|
||||||
};
|
};
|
||||||
let (mut x, y) = (axis(), axis());
|
let (mut x, y) = (axis(), axis());
|
||||||
// Aligning on neither axis is just another transparent wrapper and
|
// Aligning on neither axis leaves the branch unexercised.
|
||||||
// would leave this branch unexercised.
|
|
||||||
if x.is_none() && y.is_none() {
|
if x.is_none() && y.is_none() {
|
||||||
x = Some(AxisAlign::Center);
|
x = Some(AxisAlign::CENTER);
|
||||||
}
|
}
|
||||||
Align { x, y }
|
Align { x, y }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A declared size over half the tree, kept where a test can change it.
|
/// A declared size over half the tree, kept where a test can change it.
|
||||||
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
|
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
|
||||||
if !self.rng.chance() {
|
// A rule is a property now, so a node already carrying one would take
|
||||||
|
// a second entry in `sized` -- and two edits naming one widget settle
|
||||||
|
// in the order they are applied, which is grow order cold and edit
|
||||||
|
// order warm. One entry per widget instead. Both draws are taken
|
||||||
|
// whatever is decided, and the decision is grow order alone, so the
|
||||||
|
// two trees consume the same random stream.
|
||||||
|
let take = self.rng.chance();
|
||||||
|
let lens = [self.len(), self.len()];
|
||||||
|
if !take || self.tree.sized.contains(&inner.id()) {
|
||||||
return inner;
|
return inner;
|
||||||
}
|
}
|
||||||
let idx = self.tree.sized.len();
|
let idx = self.tree.sized.len();
|
||||||
let lens = [self.len(), self.len()];
|
|
||||||
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
|
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
|
||||||
let id = inner.id();
|
let id = inner.id();
|
||||||
self.rsc
|
self.rsc
|
||||||
@@ -210,6 +228,41 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
|||||||
inner
|
inner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An alignment over some of the tree, kept where a test can change it.
|
||||||
|
/// One entry per widget for the reason `sized` gives.
|
||||||
|
fn aligned(&mut self, inner: StrongWidget) -> StrongWidget {
|
||||||
|
let align = self.align();
|
||||||
|
let align = [align.x, align.y];
|
||||||
|
if self.tree.aligned.contains(&inner.id()) {
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
let idx = self.tree.aligned.len();
|
||||||
|
let align = self.edits.aligns.get(&idx).copied().unwrap_or(align);
|
||||||
|
let id = inner.id();
|
||||||
|
let widgets = &mut self.rsc.ui_mut().widgets;
|
||||||
|
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
|
||||||
|
widgets.set_alignment(id, axis, align.unwrap_or_default());
|
||||||
|
}
|
||||||
|
self.tree.aligned.push(id);
|
||||||
|
inner
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A movable region of its own over some of the tree. What it changes is
|
||||||
|
/// how a move is written and how long a primitive's chain is, neither of
|
||||||
|
/// which any other branch here varies.
|
||||||
|
fn noded(&mut self, inner: StrongWidget) -> StrongWidget {
|
||||||
|
let take = self.rng.below(4) == 0;
|
||||||
|
if self.tree.nodes.contains(&inner.id()) {
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
let idx = self.tree.nodes.len();
|
||||||
|
let take = self.edits.nodes.get(&idx).copied().unwrap_or(take);
|
||||||
|
let id = inner.id();
|
||||||
|
self.rsc.ui_mut().widgets.set_region_node(id, take);
|
||||||
|
self.tree.nodes.push(id);
|
||||||
|
inner
|
||||||
|
}
|
||||||
|
|
||||||
fn node(&mut self, depth: usize) -> StrongWidget {
|
fn node(&mut self, depth: usize) -> StrongWidget {
|
||||||
if depth == 0 {
|
if depth == 0 {
|
||||||
return self.leaf();
|
return self.leaf();
|
||||||
@@ -220,6 +273,7 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
|||||||
// else here does, and gives its child a box longer than its own.
|
// else here does, and gives its child a box longer than its own.
|
||||||
let inner = self.node(depth - 1);
|
let inner = self.node(depth - 1);
|
||||||
let inner = self.sized(inner);
|
let inner = self.sized(inner);
|
||||||
|
let inner = self.noded(inner);
|
||||||
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
|
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
|
||||||
let id = Scroll::new(inner, axis).add(self.rsc);
|
let id = Scroll::new(inner, axis).add(self.rsc);
|
||||||
self.tree.scrolls.push(id);
|
self.tree.scrolls.push(id);
|
||||||
@@ -246,17 +300,13 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
|||||||
if positioned == 1 {
|
if positioned == 1 {
|
||||||
let inner = self.node(depth - 1);
|
let inner = self.node(depth - 1);
|
||||||
let inner = self.sized(inner);
|
let inner = self.sized(inner);
|
||||||
let id = Aligned {
|
let inner = self.noded(inner);
|
||||||
inner,
|
return self.aligned(inner);
|
||||||
align: self.align(),
|
|
||||||
}
|
|
||||||
.add_strong(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id;
|
|
||||||
}
|
}
|
||||||
if self.rng.below(4) == 0 {
|
if self.rng.below(4) == 0 {
|
||||||
let inner = self.node(depth - 1);
|
let inner = self.node(depth - 1);
|
||||||
let inner = self.sized(inner);
|
let inner = self.sized(inner);
|
||||||
|
let inner = self.noded(inner);
|
||||||
// Each side its own, since a padding that is the same all round
|
// Each side its own, since a padding that is the same all round
|
||||||
// hides anything that treats one edge differently from another.
|
// hides anything that treats one edge differently from another.
|
||||||
let mut side = || self.rng.below(24) as f32;
|
let mut side = || self.rng.below(24) as f32;
|
||||||
@@ -274,7 +324,9 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
|||||||
let mut children = Vec::with_capacity(grown);
|
let mut children = Vec::with_capacity(grown);
|
||||||
for _ in 0..grown {
|
for _ in 0..grown {
|
||||||
let child = self.node(depth - 1);
|
let child = self.node(depth - 1);
|
||||||
children.push(self.sized(child));
|
let child = self.sized(child);
|
||||||
|
let child = self.noded(child);
|
||||||
|
children.push(child);
|
||||||
}
|
}
|
||||||
if self.rng.chance() {
|
if self.rng.chance() {
|
||||||
let id = Stack {
|
let id = Stack {
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
pub struct Aligned {
|
|
||||||
pub inner: StrongWidget,
|
|
||||||
pub align: Align,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Aligned {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
let known = match self.align.tuple() {
|
|
||||||
(Some(_), Some(_)) => painter
|
|
||||||
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
|
||||||
.zip(painter.known_len(&self.inner, Axis::Y, UiRegion::FULL))
|
|
||||||
.map(|(x, y)| Size { x, y }),
|
|
||||||
(Some(_), None) => painter
|
|
||||||
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
|
||||||
.map(|x| Size {
|
|
||||||
x,
|
|
||||||
y: Len::LEFTOVER,
|
|
||||||
}),
|
|
||||||
(None, Some(_)) => painter
|
|
||||||
.known_len(&self.inner, Axis::Y, UiRegion::FULL)
|
|
||||||
.map(|y| Size {
|
|
||||||
x: Len::LEFTOVER,
|
|
||||||
y,
|
|
||||||
}),
|
|
||||||
(None, None) => Some(Size::LEFTOVER),
|
|
||||||
};
|
|
||||||
// Drawn where it may be too big only when the aligned axes are not
|
|
||||||
// already known, then given its aligned box once its size is known.
|
|
||||||
let had_size = known.is_some();
|
|
||||||
let size =
|
|
||||||
known.unwrap_or_else(|| painter.widget_within(&self.inner, UiRegion::FULL).size());
|
|
||||||
let region = match self.align.tuple() {
|
|
||||||
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
|
|
||||||
(Some(x), None) => UiRegion::new(size.x.apply_leftover().align(x), UiSpan::FULL),
|
|
||||||
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_leftover().align(y)),
|
|
||||||
(None, None) => UiRegion::FULL,
|
|
||||||
};
|
|
||||||
let placed = painter.widget_within(&self.inner, region).size();
|
|
||||||
if had_size { placed } else { size }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
mod align;
|
|
||||||
mod layer;
|
mod layer;
|
||||||
mod offset;
|
mod offset;
|
||||||
mod pad;
|
mod pad;
|
||||||
@@ -6,7 +5,6 @@ mod scroll;
|
|||||||
mod span;
|
mod span;
|
||||||
mod stack;
|
mod stack;
|
||||||
|
|
||||||
pub use align::*;
|
|
||||||
pub use layer::*;
|
pub use layer::*;
|
||||||
pub use offset::*;
|
pub use offset::*;
|
||||||
pub use pad::*;
|
pub use pad::*;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub struct Pad {
|
|||||||
impl Widget for Pad {
|
impl Widget for Pad {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let inner = painter
|
let inner = painter
|
||||||
.widget_within(&self.inner, self.padding.region())
|
.widget_aligned(&self.inner, self.padding.region(), RegionAlign::NEAR)
|
||||||
.size();
|
.size();
|
||||||
Size {
|
Size {
|
||||||
x: Len {
|
x: Len {
|
||||||
|
|||||||
@@ -14,13 +14,9 @@ impl Widget for Scroll {
|
|||||||
let container_len = painter.px_len(self.axis);
|
let container_len = painter.px_len(self.axis);
|
||||||
// Draw in the whole container only when its scrolling-axis length is
|
// Draw in the whole container only when its scrolling-axis length is
|
||||||
// not already known, then draw it at the scrolled offset.
|
// not already known, then draw it at the scrolled offset.
|
||||||
let (answer_len, measured) = match painter.known_len(&self.inner, self.axis, UiRegion::FULL)
|
let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) {
|
||||||
{
|
Some(len) => len,
|
||||||
Some(len) => (len, None),
|
None => painter.widget(&self.inner).size().axis(self.axis),
|
||||||
None => {
|
|
||||||
let size = painter.widget_within(&self.inner, UiRegion::FULL).size();
|
|
||||||
(size.axis(self.axis), Some(size))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let content = answer_len.apply_leftover();
|
let content = answer_len.apply_leftover();
|
||||||
self.container_len = container_len;
|
self.container_len = container_len;
|
||||||
@@ -30,21 +26,33 @@ impl Widget for Scroll {
|
|||||||
self.amt = self.content_len - self.container_len;
|
self.amt = self.content_len - self.container_len;
|
||||||
}
|
}
|
||||||
self.update_amt();
|
self.update_amt();
|
||||||
// Content of a fixed length that fits sits at the start of any box
|
let align = painter.alignment().axis(self.axis);
|
||||||
// it fits in; one scrolled part way sits where it is until the box
|
// Content of a fixed length that fits sits at the start of any box it
|
||||||
// shrinks past what is left of it. Kept to the end, it moves with
|
// fits in -- but only anchored there. Anywhere else it is a part of
|
||||||
// every length.
|
// the room left over, so it moves with every length the box takes and
|
||||||
if content.rel == 0.0 && self.content_len <= self.container_len {
|
// the drawing holds for that length alone. One scrolled part way sits
|
||||||
|
// where it is until the box shrinks past what is left of it. Kept to
|
||||||
|
// the end, it moves with every length.
|
||||||
|
if content.rel == 0.0 && self.content_len <= self.container_len && align == AxisAlign::NEG {
|
||||||
painter.holds(self.axis, self.content_len..=f32::INFINITY);
|
painter.holds(self.axis, self.content_len..=f32::INFINITY);
|
||||||
} else if content.rel == 0.0 && !self.snap_end {
|
} else if content.rel == 0.0 && !self.snap_end {
|
||||||
let left = self.content_len - self.amt;
|
let left = self.content_len - self.amt;
|
||||||
painter.holds(self.axis, f32::NEG_INFINITY..=left);
|
painter.holds(self.axis, f32::NEG_INFINITY..=left);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
// Content shorter than the viewport has room to sit in, and where it
|
||||||
|
// sits is this widget's own alignment -- the same property that would
|
||||||
|
// have placed the whole scroll in a box longer than it.
|
||||||
|
let slack = (self.container_len - self.content_len).max(0.0);
|
||||||
|
let anchor = slack * align.rel();
|
||||||
|
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, anchor - self.amt, 0.0));
|
||||||
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
||||||
let placed = painter.widget_within(&self.inner, region).size();
|
painter.widget_aligned(&self.inner, region, RegionAlign::NEAR);
|
||||||
measured.unwrap_or_else(|| Size::from_axis(self.axis, answer_len, placed.axis(!self.axis)))
|
// What it occupies is its box, on both axes: it clips its content to
|
||||||
|
// that box, so it can neither take less of one nor honestly ask for
|
||||||
|
// more. The content's length is what it scrolls through, not what it
|
||||||
|
// is.
|
||||||
|
Size::LEFTOVER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,18 +13,20 @@ impl Widget for Stack {
|
|||||||
StackSize::Default => None,
|
StackSize::Default => None,
|
||||||
StackSize::Child(i) => Some(i),
|
StackSize::Child(i) => Some(i),
|
||||||
};
|
};
|
||||||
let mut size = Size::default();
|
// Whichever child sizes the stack decides the box every child gets.
|
||||||
|
// The stack reports that size, so a child given a longer box would
|
||||||
|
// draw outside what the stack says it occupies.
|
||||||
|
let size = match sizing.and_then(|i| self.children.get(i)) {
|
||||||
|
Some(child) => painter.widget(child).size(),
|
||||||
|
None => Size::LEFTOVER,
|
||||||
|
};
|
||||||
|
let region = painter.box_of(size);
|
||||||
for (i, child) in self.children.iter().enumerate() {
|
for (i, child) in self.children.iter().enumerate() {
|
||||||
match i {
|
match i {
|
||||||
0 => painter.child_layer(),
|
0 => painter.child_layer(),
|
||||||
_ => painter.next_layer(),
|
_ => painter.next_layer(),
|
||||||
}
|
}
|
||||||
let drawn = painter.widget(child);
|
painter.widget_aligned(child, region, RegionAlign::NEAR);
|
||||||
// Only the child that sizes the stack is read, so the others
|
|
||||||
// changing size does not redraw it.
|
|
||||||
if sizing == Some(i) {
|
|
||||||
size = drawn.size();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-5
@@ -12,14 +12,23 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn align(self, align: impl Into<Align>) -> impl WidgetFn<Rsc, Aligned> {
|
fn align(self, align: impl Into<Align>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
move |state| Aligned {
|
// An axis left out keeps whatever it had, which is centered unless
|
||||||
inner: self.add_strong(state),
|
// something else set it.
|
||||||
align: align.into(),
|
let align = align.into();
|
||||||
|
move |state| {
|
||||||
|
let id = self.add(state);
|
||||||
|
let widgets = &mut state.ui_mut().widgets;
|
||||||
|
for (axis, align) in [(Axis::X, align.x), (Axis::Y, align.y)] {
|
||||||
|
if let Some(align) = align {
|
||||||
|
widgets.set_alignment(id, axis, align);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn center(self) -> impl WidgetFn<Rsc, Aligned> {
|
fn center(self) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
self.align(Align::CENTER)
|
self.align(Align::CENTER)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+113
-15
@@ -13,7 +13,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use iris::harness::Harness;
|
use iris::harness::Harness;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
|
use iris::random::{Aligns, Edits, Lens, Rng, SpanEdit, Tree, grow};
|
||||||
|
|
||||||
/// How deep the generator branches. The generator widens two to four ways per
|
/// How deep the generator branches. The generator widens two to four ways per
|
||||||
/// level, so depth is exponential in width and a deep narrow tree is not
|
/// level, so depth is exponential in width and a deep narrow tree is not
|
||||||
@@ -179,12 +179,28 @@ fn describe(id: WidgetId, h: &Harness) -> String {
|
|||||||
Some(len) => format!("{len}"),
|
Some(len) => format!("{len}"),
|
||||||
None => "-".into(),
|
None => "-".into(),
|
||||||
};
|
};
|
||||||
// A size rule is a property of whatever carries it, so it prints with
|
let align = h.rsc.widgets().alignment(id);
|
||||||
// that widget rather than as one of its own.
|
let side = |a: AxisAlign| {
|
||||||
match (rules.x, rules.y) {
|
if a == AxisAlign::NEG {
|
||||||
(SizeRule::Free, SizeRule::Free) => describe_widget(id, h),
|
"neg".into()
|
||||||
(x, y) => format!("{}[x:{},y:{}]", describe_widget(id, h), rule(x), rule(y)),
|
} else if a == AxisAlign::CENTER {
|
||||||
|
"mid".into()
|
||||||
|
} else if a == AxisAlign::POS {
|
||||||
|
"pos".into()
|
||||||
|
} else {
|
||||||
|
format!("{:.2}", a.rel())
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
// A rule and an alignment are properties of whatever carries them, so
|
||||||
|
// they print with that widget rather than as widgets of their own.
|
||||||
|
let mut out = describe_widget(id, h);
|
||||||
|
if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) {
|
||||||
|
out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y));
|
||||||
|
}
|
||||||
|
if align != RegionAlign::default() {
|
||||||
|
out += &format!("@{},{}", side(align.x), side(align.y));
|
||||||
|
}
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn describe_widget(id: WidgetId, h: &Harness) -> String {
|
fn describe_widget(id: WidgetId, h: &Harness) -> String {
|
||||||
@@ -210,15 +226,6 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String {
|
|||||||
p.left, p.right, p.top, p.bottom
|
p.left, p.right, p.top, p.bottom
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if let Some(w) = any.downcast_ref::<Aligned>() {
|
|
||||||
let a = |v: Option<AxisAlign>| match v {
|
|
||||||
None => "-",
|
|
||||||
Some(AxisAlign::Neg) => "neg",
|
|
||||||
Some(AxisAlign::Center) => "mid",
|
|
||||||
Some(AxisAlign::Pos) => "pos",
|
|
||||||
};
|
|
||||||
return format!("Aligned{{x:{},y:{}}}", a(w.align.x), a(w.align.y));
|
|
||||||
}
|
|
||||||
if let Some(w) = any.downcast_ref::<Stack>() {
|
if let Some(w) = any.downcast_ref::<Stack>() {
|
||||||
return format!("Stack{{n:{}}}", w.children.len());
|
return format!("Stack{{n:{}}}", w.children.len());
|
||||||
}
|
}
|
||||||
@@ -290,6 +297,87 @@ fn changed_size(seed: u64) {
|
|||||||
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
|
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves one widget to a different corner of the box it is given.
|
||||||
|
fn realign_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns {
|
||||||
|
let mut side = || match rng.below(4) {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(AxisAlign::NEG),
|
||||||
|
2 => Some(AxisAlign::CENTER),
|
||||||
|
_ => Some(AxisAlign::POS),
|
||||||
|
};
|
||||||
|
let aligns = [side(), side()];
|
||||||
|
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(aligns) {
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(tree.aligned[idx], axis, align.unwrap_or_default());
|
||||||
|
}
|
||||||
|
aligns
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed_alignment(seed: u64) {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let grown = plant(&mut warm, seed, &Edits::default());
|
||||||
|
if grown.aligned.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rng = Rng::new(seed ^ 0xa11);
|
||||||
|
let aligns = (0..grown.aligned.len())
|
||||||
|
.step_by(3)
|
||||||
|
.map(|idx| (idx, realign_one(&mut warm, &grown, idx, &mut rng)))
|
||||||
|
.collect();
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let same = plant(
|
||||||
|
&mut cold,
|
||||||
|
seed,
|
||||||
|
&Edits {
|
||||||
|
aligns,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_same(seed, "an alignment change", (&warm, &grown), (&cold, &same));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Giving a widget a movable region of its own, or taking it away, is a
|
||||||
|
/// structural change: every primitive under it changes which chain resolves
|
||||||
|
/// it. A cold tree built that way is what says the rebuild was complete.
|
||||||
|
fn changed_region_node(seed: u64) {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let grown = plant(&mut warm, seed, &Edits::default());
|
||||||
|
if grown.nodes.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodes: HashMap<usize, bool> = (0..grown.nodes.len())
|
||||||
|
.step_by(2)
|
||||||
|
.map(|idx| {
|
||||||
|
let id = grown.nodes[idx];
|
||||||
|
let was = warm.rsc.widgets().is_region_node(id);
|
||||||
|
warm.rsc.widgets_mut().set_region_node(id, !was);
|
||||||
|
(idx, !was)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let same = plant(
|
||||||
|
&mut cold,
|
||||||
|
seed,
|
||||||
|
&Edits {
|
||||||
|
nodes,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_same(
|
||||||
|
seed,
|
||||||
|
"a region-node change",
|
||||||
|
(&warm, &grown),
|
||||||
|
(&cold, &same),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn reshuffled(seed: u64, shuffle: Shuffle) {
|
fn reshuffled(seed: u64, shuffle: Shuffle) {
|
||||||
let mut warm = Harness::new((900, 1200));
|
let mut warm = Harness::new((900, 1200));
|
||||||
let mut grown = plant(&mut warm, seed, &Edits::default());
|
let mut grown = plant(&mut warm, seed, &Edits::default());
|
||||||
@@ -411,6 +499,16 @@ fn a_changed_size_lands_where_growing_it_that_way_would() {
|
|||||||
SEEDS.into_iter().for_each(changed_size);
|
SEEDS.into_iter().for_each(changed_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_changed_alignment_lands_where_growing_it_that_way_would() {
|
||||||
|
SEEDS.into_iter().for_each(changed_alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_toggled_region_node_lands_where_growing_it_that_way_would() {
|
||||||
|
SEEDS.into_iter().for_each(changed_region_node);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_size_changing_at_once_lands_where_growing_it_that_way_would() {
|
fn every_size_changing_at_once_lands_where_growing_it_that_way_would() {
|
||||||
SEEDS.into_iter().for_each(changed_every_size);
|
SEEDS.into_iter().for_each(changed_every_size);
|
||||||
|
|||||||
+29
-5
@@ -74,17 +74,39 @@ fn an_empty_widget_takes_a_share_of_a_span() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_child_drawn_twice_moves_once() {
|
fn a_child_drawn_twice_moves_once() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
// `Aligned` draws its child twice; listing it twice would move it twice.
|
// The span measures a child and then places it; listing it twice would
|
||||||
|
// move it twice. The span's own fixed total is shorter than the window,
|
||||||
|
// so the span is centred in it and everything under it carries that.
|
||||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
let centered = inner.center().width(200).add(&mut h.rsc);
|
let centered = inner.center().width(200).add(&mut h.rsc);
|
||||||
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
h.set_root((left, centered).span(Dir::RIGHT));
|
h.set_root((left, centered).span(Dir::RIGHT));
|
||||||
assert_corners!(h, inner, (100, 0), (300, 200));
|
assert_corners!(h, inner, (150, 0), (350, 200));
|
||||||
|
|
||||||
h.set_len(left, Axis::X, 150);
|
h.set_len(left, Axis::X, 150);
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
assert_corners!(h, inner, (150, 0), (350, 200));
|
assert_corners!(h, inner, (175, 0), (375, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alignment_accepts_an_arbitrary_fraction_and_changes_at_runtime() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let fixed = rect(Color::BLUE).sized((100, 100)).add(&mut h.rsc);
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(fixed, Axis::X, AxisAlign::new(0.25));
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(fixed, Axis::Y, AxisAlign::NEG);
|
||||||
|
h.set_root(fixed);
|
||||||
|
assert_corners!(h, fixed, (75, 0), (175, 100));
|
||||||
|
|
||||||
|
h.rsc
|
||||||
|
.widgets_mut()
|
||||||
|
.set_alignment(fixed, Axis::X, AxisAlign::new(0.75));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, fixed, (225, 0), (325, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -143,15 +165,17 @@ fn a_moved_subtree_takes_its_children_with_it() {
|
|||||||
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
||||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
let row = inner.pad(10).height(40).region_node().add(&mut h.rsc);
|
let row = inner.pad(10).height(40).region_node().add(&mut h.rsc);
|
||||||
|
// 80 of fixed rows in a 400 window, so the span takes 80 and sits in the
|
||||||
|
// middle of what it was given.
|
||||||
h.set_root((first, row).span(Dir::DOWN));
|
h.set_root((first, row).span(Dir::DOWN));
|
||||||
assert_corners!(h, inner, (10, 50), (390, 70));
|
assert_corners!(h, inner, (10, 210), (390, 230));
|
||||||
|
|
||||||
h.set_len(first, Axis::Y, 80);
|
h.set_len(first, Axis::Y, 80);
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
// The row opted into one movable region, so its descendants follow one
|
// The row opted into one movable region, so its descendants follow one
|
||||||
// entry rather than having their primitive regions rewritten.
|
// entry rather than having their primitive regions rewritten.
|
||||||
assert_corners!(h, inner, (10, 90), (390, 110));
|
assert_corners!(h, inner, (10, 230), (390, 250));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+15
-5
@@ -159,10 +159,15 @@ fn a_span_child_that_declares_its_length_is_drawn_once() {
|
|||||||
h.set_root((hinted, asked).span(Dir::RIGHT));
|
h.set_root((hinted, asked).span(Dir::RIGHT));
|
||||||
|
|
||||||
assert_eq!(told_draws.get(), 1);
|
assert_eq!(told_draws.get(), 1);
|
||||||
|
// Reading its box makes its drawing hold for the measuring box alone,
|
||||||
|
// and it reports less than that box: so it is drawn again in the box its
|
||||||
|
// answer places it in, and once more in the final box the span chooses.
|
||||||
|
// A widget that says what it holds for, as text does, skips the middle
|
||||||
|
// one.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
asked_draws.get(),
|
asked_draws.get(),
|
||||||
2,
|
3,
|
||||||
"drawn to be measured, then again in its final box"
|
"drawn to be measured, in its placed box, then in its final box"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +261,11 @@ impl Widget for ReadsBox {
|
|||||||
|
|
||||||
/// Reads its box across one axis only, so its drawing holds for a taller
|
/// Reads its box across one axis only, so its drawing holds for a taller
|
||||||
/// box on its own and only a wider one is worth a draw.
|
/// box on its own and only a wider one is worth a draw.
|
||||||
|
///
|
||||||
|
/// Both of these report a quarter of what they read, without saying that the
|
||||||
|
/// drawing holds there too, so each length they are asked at costs two draws:
|
||||||
|
/// one to answer, and one in the quarter-sized box that answer places them
|
||||||
|
/// in. The counts below are in those pairs.
|
||||||
struct ReadsWidth {
|
struct ReadsWidth {
|
||||||
draws: Rc<Cell<usize>>,
|
draws: Rc<Cell<usize>>,
|
||||||
}
|
}
|
||||||
@@ -336,7 +346,7 @@ fn a_resize_redraws_what_read_its_box() {
|
|||||||
h.resize((800, 100));
|
h.resize((800, 100));
|
||||||
h.frame();
|
h.frame();
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
assert_eq!(draws.get(), settled + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -356,7 +366,7 @@ fn a_resize_only_redraws_read_axes() {
|
|||||||
|
|
||||||
h.resize((800, 300));
|
h.resize((800, 300));
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_eq!(draws.get(), settled + 1, "width changes its answer");
|
assert_eq!(draws.get(), settled + 2, "width changes its answer");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -378,7 +388,7 @@ fn subpixel_resize_changes_accumulate_from_the_last_layout() {
|
|||||||
|
|
||||||
h.resize((400.06, 200.0));
|
h.resize((400.06, 200.0));
|
||||||
h.frame();
|
h.frame();
|
||||||
assert_eq!(draws.get(), settled + 1);
|
assert_eq!(draws.get(), settled + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+92
-21
@@ -76,9 +76,9 @@ enum Node {
|
|||||||
fn axis_align(v: u8) -> Option<AxisAlign> {
|
fn axis_align(v: u8) -> Option<AxisAlign> {
|
||||||
match v % 4 {
|
match v % 4 {
|
||||||
0 => None,
|
0 => None,
|
||||||
1 => Some(AxisAlign::Neg),
|
1 => Some(AxisAlign::NEG),
|
||||||
2 => Some(AxisAlign::Center),
|
2 => Some(AxisAlign::CENTER),
|
||||||
_ => Some(AxisAlign::Pos),
|
_ => Some(AxisAlign::POS),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +94,7 @@ impl Node {
|
|||||||
h: &mut Harness,
|
h: &mut Harness,
|
||||||
out: &mut Vec<WidgetId>,
|
out: &mut Vec<WidgetId>,
|
||||||
spans: &mut Vec<WeakWidget<Span>>,
|
spans: &mut Vec<WeakWidget<Span>>,
|
||||||
|
sized: &mut Vec<WidgetId>,
|
||||||
) -> StrongWidget {
|
) -> StrongWidget {
|
||||||
let id: StrongWidget = match self {
|
let id: StrongWidget = match self {
|
||||||
Node::Text(words, wrap) => {
|
Node::Text(words, wrap) => {
|
||||||
@@ -106,7 +107,10 @@ impl Node {
|
|||||||
Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc),
|
Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc),
|
||||||
Node::Rect => rect(Color::RED).add_strong(&mut h.rsc),
|
Node::Rect => rect(Color::RED).add_strong(&mut h.rsc),
|
||||||
Node::Span(down, gap, kids, order) => {
|
Node::Span(down, gap, kids, order) => {
|
||||||
let mut built: Vec<_> = kids.iter().map(|k| Some(k.build(h, out, spans))).collect();
|
let mut built: Vec<_> = kids
|
||||||
|
.iter()
|
||||||
|
.map(|k| Some(k.build(h, out, spans, sized)))
|
||||||
|
.collect();
|
||||||
// `order` is a permutation, so each is taken exactly once.
|
// `order` is a permutation, so each is taken exactly once.
|
||||||
let children = order
|
let children = order
|
||||||
.iter()
|
.iter()
|
||||||
@@ -126,7 +130,7 @@ impl Node {
|
|||||||
handle.add_strong(&mut h.rsc)
|
handle.add_strong(&mut h.rsc)
|
||||||
}
|
}
|
||||||
Node::Stack(kids) => {
|
Node::Stack(kids) => {
|
||||||
let children = kids.iter().map(|k| k.build(h, out, spans)).collect();
|
let children = kids.iter().map(|k| k.build(h, out, spans, sized)).collect();
|
||||||
Stack {
|
Stack {
|
||||||
children,
|
children,
|
||||||
size: StackSize::Child(0),
|
size: StackSize::Child(0),
|
||||||
@@ -134,7 +138,7 @@ impl Node {
|
|||||||
.add_strong(&mut h.rsc)
|
.add_strong(&mut h.rsc)
|
||||||
}
|
}
|
||||||
Node::Pad(p, kid) => {
|
Node::Pad(p, kid) => {
|
||||||
let inner = kid.build(h, out, spans);
|
let inner = kid.build(h, out, spans, sized);
|
||||||
Pad {
|
Pad {
|
||||||
padding: Padding {
|
padding: Padding {
|
||||||
left: *p,
|
left: *p,
|
||||||
@@ -147,30 +151,29 @@ impl Node {
|
|||||||
.add_strong(&mut h.rsc)
|
.add_strong(&mut h.rsc)
|
||||||
}
|
}
|
||||||
Node::Aligned(x, y, kid) => {
|
Node::Aligned(x, y, kid) => {
|
||||||
let inner = kid.build(h, out, spans);
|
let inner = kid.build(h, out, spans, sized);
|
||||||
Aligned {
|
for (axis, align) in [(Axis::X, axis_align(*x)), (Axis::Y, axis_align(*y))] {
|
||||||
inner,
|
if let Some(align) = align {
|
||||||
align: Align {
|
h.rsc.widgets_mut().set_alignment(&inner, axis, align);
|
||||||
x: axis_align(*x),
|
|
||||||
y: axis_align(*y),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
.add_strong(&mut h.rsc)
|
}
|
||||||
|
inner
|
||||||
}
|
}
|
||||||
Node::Sized(x, y, kid) => {
|
Node::Sized(x, y, kid) => {
|
||||||
let inner = kid.build(h, out, spans);
|
let inner = kid.build(h, out, spans, sized);
|
||||||
h.rsc.widgets_mut().set_size_rules(&inner, *x, *y);
|
h.rsc.widgets_mut().set_size_rules(&inner, *x, *y);
|
||||||
|
sized.push(inner.id());
|
||||||
inner
|
inner
|
||||||
}
|
}
|
||||||
Node::Scroll(down, kid) => {
|
Node::Scroll(down, kid) => {
|
||||||
let inner = kid.build(h, out, spans);
|
let inner = kid.build(h, out, spans, sized);
|
||||||
let axis = if *down { Axis::Y } else { Axis::X };
|
let axis = if *down { Axis::Y } else { Axis::X };
|
||||||
Scroll::new(inner, axis).add_strong(&mut h.rsc)
|
Scroll::new(inner, axis).add_strong(&mut h.rsc)
|
||||||
}
|
}
|
||||||
Node::Branch(probe, a, b, at) => {
|
Node::Branch(probe, a, b, at) => {
|
||||||
let probe = probe.build(h, out, spans);
|
let probe = probe.build(h, out, spans, sized);
|
||||||
let wide = a.build(h, out, spans);
|
let wide = a.build(h, out, spans, sized);
|
||||||
let narrow = b.build(h, out, spans);
|
let narrow = b.build(h, out, spans, sized);
|
||||||
Branch {
|
Branch {
|
||||||
probe,
|
probe,
|
||||||
wide,
|
wide,
|
||||||
@@ -184,6 +187,27 @@ impl Node {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The lengths every `Sized` node would carry after `resized`, in the
|
||||||
|
/// order `build` pushes them.
|
||||||
|
fn sized_lens(&self, out: &mut Vec<(Option<Len>, Option<Len>)>) {
|
||||||
|
match self {
|
||||||
|
Node::Text(..) | Node::OneLine | Node::Rect => {}
|
||||||
|
Node::Span(_, _, kids, _) | Node::Stack(kids) => {
|
||||||
|
kids.iter().for_each(|k| k.sized_lens(out));
|
||||||
|
}
|
||||||
|
Node::Pad(_, k) | Node::Aligned(_, _, k) | Node::Scroll(_, k) => k.sized_lens(out),
|
||||||
|
Node::Sized(x, y, k) => {
|
||||||
|
k.sized_lens(out);
|
||||||
|
out.push((resized_len(*x), resized_len(*y)));
|
||||||
|
}
|
||||||
|
Node::Branch(p, a, b, _) => {
|
||||||
|
p.sized_lens(out);
|
||||||
|
a.sized_lens(out);
|
||||||
|
b.sized_lens(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn size(&self) -> usize {
|
fn size(&self) -> usize {
|
||||||
1 + match self {
|
1 + match self {
|
||||||
Node::Text(..) | Node::OneLine | Node::Rect => 0,
|
Node::Text(..) | Node::OneLine | Node::Rect => 0,
|
||||||
@@ -377,6 +401,41 @@ enum Case {
|
|||||||
Repaint,
|
Repaint,
|
||||||
ResizeRepaint,
|
ResizeRepaint,
|
||||||
Reorder,
|
Reorder,
|
||||||
|
SizeChange,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A different declared length, kept the same kind so the change is to the
|
||||||
|
/// value alone.
|
||||||
|
fn resized_len(len: Option<Len>) -> Option<Len> {
|
||||||
|
len.map(|len| Len {
|
||||||
|
px: len.px * 0.5 + 13.0,
|
||||||
|
rel: len.rel * 0.5,
|
||||||
|
leftover: len.leftover,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every declared size changed, as a tree rather than as a change.
|
||||||
|
fn resized(node: &Node) -> Node {
|
||||||
|
match node {
|
||||||
|
Node::Span(down, gap, kids, order) => Node::Span(
|
||||||
|
*down,
|
||||||
|
*gap,
|
||||||
|
kids.iter().map(resized).collect(),
|
||||||
|
order.clone(),
|
||||||
|
),
|
||||||
|
Node::Stack(kids) => Node::Stack(kids.iter().map(resized).collect()),
|
||||||
|
Node::Pad(p, k) => Node::Pad(*p, Box::new(resized(k))),
|
||||||
|
Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(resized(k))),
|
||||||
|
Node::Sized(x, y, k) => Node::Sized(resized_len(*x), resized_len(*y), Box::new(resized(k))),
|
||||||
|
Node::Scroll(d, k) => Node::Scroll(*d, Box::new(resized(k))),
|
||||||
|
Node::Branch(p, a, b, at) => Node::Branch(
|
||||||
|
Box::new(resized(p)),
|
||||||
|
Box::new(resized(a)),
|
||||||
|
Box::new(resized(b)),
|
||||||
|
*at,
|
||||||
|
),
|
||||||
|
leaf => leaf.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every span's children rotated by one, as a tree rather than as a change:
|
/// Every span's children rotated by one, as a tree rather than as a change:
|
||||||
@@ -413,7 +472,8 @@ fn diverges(node: &Node, case: Case) -> Option<String> {
|
|||||||
let mut warm = Harness::new(start);
|
let mut warm = Harness::new(start);
|
||||||
let mut warm_ids = Vec::new();
|
let mut warm_ids = Vec::new();
|
||||||
let mut warm_spans = Vec::new();
|
let mut warm_spans = Vec::new();
|
||||||
let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans);
|
let mut warm_sized = Vec::new();
|
||||||
|
let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans, &mut warm_sized);
|
||||||
warm.state.root = Some(root);
|
warm.state.root = Some(root);
|
||||||
// The frame that makes it warm: without it there is nothing retained and
|
// The frame that makes it warm: without it there is nothing retained and
|
||||||
// the comparison is two cold starts agreeing with each other.
|
// the comparison is two cold starts agreeing with each other.
|
||||||
@@ -434,16 +494,26 @@ fn diverges(node: &Node, case: Case) -> Option<String> {
|
|||||||
}
|
}
|
||||||
warm.frame();
|
warm.frame();
|
||||||
}
|
}
|
||||||
|
if case == Case::SizeChange {
|
||||||
|
let mut lens = Vec::new();
|
||||||
|
node.sized_lens(&mut lens);
|
||||||
|
for (id, (x, y)) in warm_sized.iter().zip(lens) {
|
||||||
|
warm.rsc.widgets_mut().set_size_rules(*id, x, y);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
}
|
||||||
|
|
||||||
// What the warm tree was moved into, grown that way from the start.
|
// What the warm tree was moved into, grown that way from the start.
|
||||||
let want = match case {
|
let want = match case {
|
||||||
Case::Reorder => reordered(node),
|
Case::Reorder => reordered(node),
|
||||||
|
Case::SizeChange => resized(node),
|
||||||
_ => node.clone(),
|
_ => node.clone(),
|
||||||
};
|
};
|
||||||
let mut cold = Harness::new(INNER);
|
let mut cold = Harness::new(INNER);
|
||||||
let mut cold_ids = Vec::new();
|
let mut cold_ids = Vec::new();
|
||||||
let mut cold_spans = Vec::new();
|
let mut cold_spans = Vec::new();
|
||||||
let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans);
|
let mut cold_sized = Vec::new();
|
||||||
|
let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans, &mut cold_sized);
|
||||||
cold.state.root = Some(root);
|
cold.state.root = Some(root);
|
||||||
cold.frame();
|
cold.frame();
|
||||||
|
|
||||||
@@ -497,6 +567,7 @@ fn no_grown_tree_lays_out_differently_warm_than_cold() {
|
|||||||
"repaint" => Case::Repaint,
|
"repaint" => Case::Repaint,
|
||||||
"resize-repaint" => Case::ResizeRepaint,
|
"resize-repaint" => Case::ResizeRepaint,
|
||||||
"reorder" => Case::Reorder,
|
"reorder" => Case::Reorder,
|
||||||
|
"size-change" => Case::SizeChange,
|
||||||
_ => Case::Resize,
|
_ => Case::Resize,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+11
-16
@@ -11,14 +11,13 @@ fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
|||||||
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
||||||
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
||||||
let sized = wrapped.width(76).add(&mut h.rsc);
|
let sized = wrapped.width(76).add(&mut h.rsc);
|
||||||
let aligned = Aligned {
|
let aligned = sized;
|
||||||
inner: sized.add_strong(&mut h.rsc),
|
h.rsc
|
||||||
align: Align {
|
.widgets_mut()
|
||||||
x: Some(AxisAlign::Pos),
|
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
||||||
y: Some(AxisAlign::Pos),
|
h.rsc
|
||||||
},
|
.widgets_mut()
|
||||||
}
|
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
||||||
.add(&mut h.rsc);
|
|
||||||
let stack = Stack {
|
let stack = Stack {
|
||||||
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
||||||
size: StackSize::Child(0),
|
size: StackSize::Child(0),
|
||||||
@@ -94,14 +93,10 @@ fn what_box_the_text_is_drawn_in() {
|
|||||||
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
||||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
let aligned = Aligned {
|
let aligned = text;
|
||||||
inner: text.add_strong(&mut h.rsc),
|
h.rsc
|
||||||
align: Align {
|
.widgets_mut()
|
||||||
x: Some(AxisAlign::Neg),
|
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
||||||
y: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
||||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
|||||||
+19
-32
@@ -16,14 +16,13 @@ fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
|||||||
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
||||||
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
||||||
let sized = wrapped.width(76).add(&mut h.rsc);
|
let sized = wrapped.width(76).add(&mut h.rsc);
|
||||||
let aligned = Aligned {
|
let aligned = sized;
|
||||||
inner: sized.add_strong(&mut h.rsc),
|
h.rsc
|
||||||
align: Align {
|
.widgets_mut()
|
||||||
x: Some(AxisAlign::Pos),
|
.set_alignment(sized, Axis::X, AxisAlign::POS);
|
||||||
y: Some(AxisAlign::Pos),
|
h.rsc
|
||||||
},
|
.widgets_mut()
|
||||||
}
|
.set_alignment(sized, Axis::Y, AxisAlign::POS);
|
||||||
.add(&mut h.rsc);
|
|
||||||
let stack = Stack {
|
let stack = Stack {
|
||||||
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
||||||
size: StackSize::Child(0),
|
size: StackSize::Child(0),
|
||||||
@@ -96,14 +95,10 @@ fn repainting_everything_moves_nothing() {
|
|||||||
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
||||||
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
let aligned = Aligned {
|
let aligned = text;
|
||||||
inner: text.add_strong(&mut h.rsc),
|
h.rsc
|
||||||
align: Align {
|
.widgets_mut()
|
||||||
x: Some(AxisAlign::Neg),
|
.set_alignment(text, Axis::X, AxisAlign::NEG);
|
||||||
y: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
let sized = inner.sized((189, 176)).add(&mut h.rsc);
|
||||||
let filler = rect(Color::RED).add(&mut h.rsc);
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||||
@@ -167,14 +162,10 @@ fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span
|
|||||||
}
|
}
|
||||||
.add(&mut h.rsc);
|
.add(&mut h.rsc);
|
||||||
let span_handle = span;
|
let span_handle = span;
|
||||||
let aligned = Aligned {
|
let aligned = span;
|
||||||
inner: span.add_strong(&mut h.rsc),
|
h.rsc
|
||||||
align: Align {
|
.widgets_mut()
|
||||||
x: Some(AxisAlign::Center),
|
.set_alignment(span, Axis::X, AxisAlign::CENTER);
|
||||||
y: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.state.root = Some(aligned.add_strong(&mut h.rsc));
|
h.state.root = Some(aligned.add_strong(&mut h.rsc));
|
||||||
(
|
(
|
||||||
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
|
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
|
||||||
@@ -300,14 +291,10 @@ impl Widget for Wider {
|
|||||||
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
|
fn plant_wider(h: &mut Harness, extra: f32) -> (WeakWidget<Wider>, WidgetId) {
|
||||||
let content = Wider { extra }.add(&mut h.rsc);
|
let content = Wider { extra }.add(&mut h.rsc);
|
||||||
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||||
let root = Aligned {
|
let root = scroll;
|
||||||
inner: scroll.add_strong(&mut h.rsc),
|
h.rsc
|
||||||
align: Align {
|
.widgets_mut()
|
||||||
x: Some(AxisAlign::Neg),
|
.set_alignment(scroll, Axis::X, AxisAlign::NEG);
|
||||||
y: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(root);
|
h.set_root(root);
|
||||||
(content, scroll.id())
|
(content, scroll.id())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user