Give a slot to the children a container places, and nothing else
A widget's region is now held in the coordinates of the slot it draws in rather than the window's, and `Painter::place` is how a container asks for a slot: it draws a child it decides the box of and may decide again. Everything under that slot is a fraction of its box, so placing the child a second time is one entry to write whether it moved or changed length. A child drawn any other way has no slot and shares its nearest ancestor's. That is what keeps the chain short. `chain_cost` measured depth as the cost -- free to 8, +42.6% at 16 -- and a slot per widget put a transcript's glyphs past that for nothing, since almost every slot was zero. `Span`, `Aligned` and `Scroll` are the containers that re-place a child after drawing it, and `tests/layout.rs` pins that four widgets between a span and a leaf leave the leaf's chain one deep. `UiRegion::stretch`, `UiRegion::stretchable` and `UiScalar::stretch` are gone. Nothing is inverted any more: a box that changed length is written to its slot, and the descendants recompose against it in the shader. That also retires the case the guard existed for, where a fixed length has no fraction to recover -- `tests/layout.rs` now stretches a 40-tall row on its other axis, which `stretchable` refused outright. What still walks the CPU is deciding who must draw again, which no chain can answer: `mark_resized` descends from the widget whose box changed and marks anything whose own box changed length and whose drawing reads it. A part of a box with no relative extent on an axis is a fixed length, and composing into it leaves none either, so the walk stops where a length did not change -- an 80-wide child in a widened row is not redrawn though it says `Redraw`. `Span`, `Pad`, `Stack`, `Offset`, `Aligned`, `SetSize` and `LayerOffset` say `Scale`: each places in fractions and offsets of its own box and none reads the box's pixel length. `Scroll` and `MaxSize` do read pixels and stay `Redraw`. 45 tests pass, five of them new. Render verification comes after the CPU side, per the owner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f9dc48b80
commit
d98969158f
18 files changed
+331
-180
No files matched your search
@@ -202,17 +202,6 @@ impl UiScalar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `within` undone against `from` and redone against `to`, for one axis.
|
|
||||||
/// `from`'s relative extent is the denominator, so it must not be zero.
|
|
||||||
fn stretch(&self, from: &UiSpan, to: &UiSpan) -> Self {
|
|
||||||
let frac = (self.rel - from.start.rel) / (from.end.rel - from.start.rel);
|
|
||||||
Self {
|
|
||||||
rel: frac.lerp(to.start.rel, to.end.rel),
|
|
||||||
abs: self.abs - frac.lerp(from.start.abs, from.end.abs)
|
|
||||||
+ frac.lerp(to.start.abs, to.end.abs),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn within_len(&self, len: UiScalar) -> Self {
|
pub fn within_len(&self, len: UiScalar) -> Self {
|
||||||
self.within(&UiSpan {
|
self.within(&UiSpan {
|
||||||
start: UiScalar::ZERO,
|
start: UiScalar::ZERO,
|
||||||
@@ -391,30 +380,6 @@ impl UiRegion {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a stretch out of this box can be expressed. Each part inside a
|
|
||||||
/// box is held as a fraction of it, and a fixed length has no fraction to
|
|
||||||
/// hold one by -- every part of it is just an offset from its start.
|
|
||||||
pub fn stretchable(&self) -> bool {
|
|
||||||
self.x.start.rel != self.x.end.rel && self.y.start.rel != self.y.end.rel
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-expresses a region inside `from` as the same fractions of `to`.
|
|
||||||
/// `from` must be `stretchable`; a translation is `shift` instead, which
|
|
||||||
/// needs no fractions and works out of any box.
|
|
||||||
pub fn stretch(&self, from: &UiRegion, to: &UiRegion) -> UiRegion {
|
|
||||||
debug_assert!(from.stretchable(), "a fixed length has no fraction");
|
|
||||||
UiRegion {
|
|
||||||
x: UiSpan {
|
|
||||||
start: self.x.start.stretch(&from.x, &to.x),
|
|
||||||
end: self.x.end.stretch(&from.x, &to.x),
|
|
||||||
},
|
|
||||||
y: UiSpan {
|
|
||||||
start: self.y.start.stretch(&from.y, &to.y),
|
|
||||||
end: self.y.end.stretch(&from.y, &to.y),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for UiRegion {
|
impl Display for UiRegion {
|
||||||
|
|||||||
@@ -82,10 +82,7 @@ unsafe impl bytemuck::Pod for MoveOffset {}
|
|||||||
unsafe impl bytemuck::Zeroable for MoveOffset {}
|
unsafe impl bytemuck::Zeroable for MoveOffset {}
|
||||||
|
|
||||||
impl MoveOffset {
|
impl MoveOffset {
|
||||||
pub fn root(parent: MoveIdx) -> Self {
|
pub fn new(parent: MoveIdx, region: UiRegion) -> Self {
|
||||||
Self {
|
Self { region, parent }
|
||||||
region: UiRegion::FULL,
|
|
||||||
parent,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,8 +15,11 @@ pub struct ActiveData {
|
|||||||
pub size_deps: Vec<WidgetId>,
|
pub size_deps: Vec<WidgetId>,
|
||||||
/// Whether it read the output's size, and so is wrong when that changes.
|
/// Whether it read the output's size, and so is wrong when that changes.
|
||||||
pub reads_output: bool,
|
pub reads_output: bool,
|
||||||
/// The move slot its primitives are positioned through.
|
/// The slot its primitives are positioned through: its own if its parent
|
||||||
|
/// placed it, otherwise the nearest ancestor that has one.
|
||||||
pub move_idx: MoveIdx,
|
pub move_idx: MoveIdx,
|
||||||
|
/// The slot `region` is given in, which is whatever its parent drew in.
|
||||||
|
pub parent_move: MoveIdx,
|
||||||
pub mask: MaskIdx,
|
pub mask: MaskIdx,
|
||||||
pub layer: LayerId,
|
pub layer: LayerId,
|
||||||
}
|
}
|
||||||
+24
-2
@@ -36,9 +36,19 @@ pub struct Moves {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Moves {
|
impl Moves {
|
||||||
pub fn push(&mut self, parent: MoveIdx) -> MoveIdx {
|
pub fn push(&mut self, parent: MoveIdx, region: UiRegion) -> MoveIdx {
|
||||||
self.changed = true;
|
self.changed = true;
|
||||||
MoveIdx::slot(self.arena.push(MoveOffset::root(parent)).idx())
|
MoveIdx::slot(self.arena.push(MoveOffset::new(parent, region)).idx())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-points a slot at a different parent, for a widget drawn somewhere
|
||||||
|
/// else in the tree than it was.
|
||||||
|
pub fn set_parent(&mut self, idx: MoveIdx, parent: MoveIdx) {
|
||||||
|
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
|
||||||
|
if entry.parent != parent {
|
||||||
|
entry.parent = parent;
|
||||||
|
self.changed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, idx: MoveIdx) {
|
pub fn remove(&mut self, idx: MoveIdx) {
|
||||||
@@ -77,6 +87,18 @@ impl Moves {
|
|||||||
region
|
region
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How many slots a region in `idx` is composed through, which is what
|
||||||
|
/// the shader's walk costs per primitive.
|
||||||
|
pub fn depth(&self, idx: MoveIdx) -> usize {
|
||||||
|
let mut depth = 0;
|
||||||
|
let mut at = idx;
|
||||||
|
while at != MoveIdx::NONE && depth < CHAIN_LIMIT as usize {
|
||||||
|
at = self.arena[at.idx()].parent;
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
depth
|
||||||
|
}
|
||||||
|
|
||||||
pub fn entries(&self) -> &[MoveOffset] {
|
pub fn entries(&self) -> &[MoveOffset] {
|
||||||
&self.arena
|
&self.arena
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-8
@@ -13,6 +13,7 @@ pub struct Painter<'a> {
|
|||||||
pub(super) state: &'a mut UiRenderState,
|
pub(super) state: &'a mut UiRenderState,
|
||||||
pub(super) rsc: &'a mut dyn UiRsc,
|
pub(super) rsc: &'a mut dyn UiRsc,
|
||||||
|
|
||||||
|
/// This widget's box, in the coordinates of `move_idx`.
|
||||||
pub(super) region: UiRegion,
|
pub(super) region: UiRegion,
|
||||||
pub(super) mask: MaskIdx,
|
pub(super) mask: MaskIdx,
|
||||||
pub(super) textures: Vec<TextureHandle>,
|
pub(super) textures: Vec<TextureHandle>,
|
||||||
@@ -21,7 +22,8 @@ pub struct Painter<'a> {
|
|||||||
/// The children whose size this widget read while drawing.
|
/// The children whose size this widget read while drawing.
|
||||||
pub(super) size_deps: Vec<WidgetId>,
|
pub(super) size_deps: Vec<WidgetId>,
|
||||||
pub(super) reads_output: bool,
|
pub(super) reads_output: bool,
|
||||||
/// The move slot this widget's primitives are positioned through.
|
/// The slot this widget's primitives are positioned through: its own if
|
||||||
|
/// its parent placed it, otherwise the nearest ancestor that has one.
|
||||||
pub(super) move_idx: MoveIdx,
|
pub(super) move_idx: MoveIdx,
|
||||||
pub layer: usize,
|
pub layer: usize,
|
||||||
pub(super) id: WidgetId,
|
pub(super) id: WidgetId,
|
||||||
@@ -78,24 +80,39 @@ 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, self.region)
|
self.widget_at(id, self.region, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a widget somewhere within this one. Drawing one a second time
|
/// Draws a widget somewhere within this one.
|
||||||
/// gives it a new box, keeping the drawing it already has where it can.
|
|
||||||
pub fn widget_within<'s, W: ?Sized>(
|
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> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
let region = region.within(&self.region);
|
let region = region.within(&self.region);
|
||||||
self.widget_at(id, region)
|
self.widget_at(id, region, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a child this widget decides the box of, and may decide again
|
||||||
|
/// once it knows what the child came to. The child gets a slot of its
|
||||||
|
/// own, so placing it a second time writes one entry however much it
|
||||||
|
/// drew -- moved or resized alike, since everything under the slot is
|
||||||
|
/// held as a fraction of its box. A child drawn any other way has no slot
|
||||||
|
/// and can only be given a different box by drawing again.
|
||||||
|
pub fn place<'s, W: ?Sized>(
|
||||||
|
&'s mut self,
|
||||||
|
id: &'s StrongWidget<W>,
|
||||||
|
region: UiRegion,
|
||||||
|
) -> DrawResult<'s, 'a, W> {
|
||||||
|
let region = region.within(&self.region);
|
||||||
|
self.widget_at(id, region, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn widget_at<'s, W: ?Sized>(
|
fn widget_at<'s, W: ?Sized>(
|
||||||
&'s mut self,
|
&'s mut self,
|
||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
|
slotted: bool,
|
||||||
) -> DrawResult<'s, 'a, W> {
|
) -> DrawResult<'s, 'a, W> {
|
||||||
// A child listed twice would be moved twice.
|
// A child listed twice would be moved twice.
|
||||||
if !self.children.contains(&id.id()) {
|
if !self.children.contains(&id.id()) {
|
||||||
@@ -106,6 +123,8 @@ impl<'a> Painter<'a> {
|
|||||||
id.id(),
|
id.id(),
|
||||||
region,
|
region,
|
||||||
Some(self.id),
|
Some(self.id),
|
||||||
|
self.move_idx,
|
||||||
|
slotted,
|
||||||
self.mask,
|
self.mask,
|
||||||
None,
|
None,
|
||||||
self.rsc,
|
self.rsc,
|
||||||
@@ -165,6 +184,8 @@ impl<'a> Painter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This widget's box, in the coordinates its own primitives are written
|
||||||
|
/// in -- so a region composed `within` it may be drawn directly.
|
||||||
pub fn region(&self) -> UiRegion {
|
pub fn region(&self) -> UiRegion {
|
||||||
self.region
|
self.region
|
||||||
}
|
}
|
||||||
@@ -176,11 +197,13 @@ impl<'a> Painter<'a> {
|
|||||||
self.state.output_size
|
self.state.output_size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This widget's box in pixels. Resolved against the output's size, so a
|
/// This widget's box in pixels. Resolved against the output's size and
|
||||||
/// widget that reads it draws again when the output changes.
|
/// the boxes it sits within, so a widget that reads it draws again when
|
||||||
|
/// the output changes.
|
||||||
pub fn px_size(&mut self) -> Vec2 {
|
pub fn px_size(&mut self) -> Vec2 {
|
||||||
self.reads_output = true;
|
self.reads_output = true;
|
||||||
self.region.size().to_abs(self.state.output_size)
|
let region = self.state.moves.resolve(self.move_idx, self.region);
|
||||||
|
region.size().to_abs(self.state.output_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_data(&mut self) -> &mut TextData {
|
pub fn text_data(&mut self) -> &mut TextData {
|
||||||
|
|||||||
+126
-70
@@ -4,6 +4,8 @@ use crate::{
|
|||||||
util::{HashMap, HashSet, Vec2, forget_ref},
|
util::{HashMap, HashSet, Vec2, forget_ref},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
||||||
|
|
||||||
pub struct UiRenderState {
|
pub struct UiRenderState {
|
||||||
pub active: HashMap<WidgetId, ActiveData>,
|
pub active: HashMap<WidgetId, ActiveData>,
|
||||||
pub layers: DrawLayers,
|
pub layers: DrawLayers,
|
||||||
@@ -82,7 +84,17 @@ impl UiRenderState {
|
|||||||
self.clear(rsc);
|
self.clear(rsc);
|
||||||
// free all resources & cache
|
// free all resources & cache
|
||||||
if let Some(id) = root {
|
if let Some(id) = root {
|
||||||
self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc);
|
self.draw_inner(
|
||||||
|
0,
|
||||||
|
id.id(),
|
||||||
|
UiRegion::FULL,
|
||||||
|
None,
|
||||||
|
MoveIdx::NONE,
|
||||||
|
false,
|
||||||
|
MaskIdx::NONE,
|
||||||
|
None,
|
||||||
|
rsc,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,13 +106,15 @@ impl UiRenderState {
|
|||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
parent: Option<WidgetId>,
|
parent: Option<WidgetId>,
|
||||||
|
parent_move: MoveIdx,
|
||||||
|
slotted: bool,
|
||||||
mask: MaskIdx,
|
mask: MaskIdx,
|
||||||
old_children: Option<Vec<WidgetId>>,
|
old_children: Option<Vec<WidgetId>>,
|
||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) -> Size {
|
) -> Size {
|
||||||
let mut old_children = old_children.unwrap_or_default();
|
let mut old_children = old_children.unwrap_or_default();
|
||||||
if self.active.contains_key(&id) {
|
if self.active.contains_key(&id) {
|
||||||
if let Some(size) = self.try_reuse(id, region, rsc) {
|
if let Some(size) = self.try_reuse(id, region, parent_move, rsc) {
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
// if not, then maintain resize and track old children to remove unneeded
|
// if not, then maintain resize and track old children to remove unneeded
|
||||||
@@ -109,14 +123,21 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// draw widget
|
// draw widget
|
||||||
let move_idx = self.move_slot(id, parent);
|
let (move_idx, local) = match slotted {
|
||||||
self.moves.set(move_idx, UiRegion::FULL);
|
// Its box becomes its slot's, so it draws in the slot's own
|
||||||
|
// coordinates and the box it was given is one entry to rewrite.
|
||||||
|
true => (self.move_slot(id, parent_move, region), UiRegion::FULL),
|
||||||
|
false => {
|
||||||
|
self.drop_slot(id);
|
||||||
|
(parent_move, region)
|
||||||
|
}
|
||||||
|
};
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
self.draw_started.insert(id);
|
self.draw_started.insert(id);
|
||||||
|
|
||||||
let mut painter = Painter {
|
let mut painter = Painter {
|
||||||
state: self,
|
state: self,
|
||||||
region,
|
region: local,
|
||||||
mask,
|
mask,
|
||||||
layer,
|
layer,
|
||||||
id,
|
id,
|
||||||
@@ -136,7 +157,7 @@ impl UiRenderState {
|
|||||||
let Painter {
|
let Painter {
|
||||||
state: _,
|
state: _,
|
||||||
rsc: _,
|
rsc: _,
|
||||||
region,
|
region: _,
|
||||||
mask,
|
mask,
|
||||||
textures,
|
textures,
|
||||||
primitives,
|
primitives,
|
||||||
@@ -166,6 +187,7 @@ impl UiRenderState {
|
|||||||
size_deps,
|
size_deps,
|
||||||
reads_output,
|
reads_output,
|
||||||
move_idx,
|
move_idx,
|
||||||
|
parent_move,
|
||||||
mask,
|
mask,
|
||||||
layer,
|
layer,
|
||||||
};
|
};
|
||||||
@@ -182,101 +204,133 @@ impl UiRenderState {
|
|||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The slot a widget's drawing is positioned through, made on its first
|
/// The slot a widget's box is held in, made on its first placed draw and
|
||||||
/// draw and kept until it stops being drawn.
|
/// kept until it stops being drawn -- a redraw replaces its `ActiveData`
|
||||||
fn move_slot(&mut self, id: WidgetId, parent: Option<WidgetId>) -> MoveIdx {
|
/// while descendants go on naming the slot.
|
||||||
|
fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx {
|
||||||
if let Some(&idx) = self.slots.get(&id) {
|
if let Some(&idx) = self.slots.get(&id) {
|
||||||
|
self.moves.set_parent(idx, parent);
|
||||||
|
self.moves.set(idx, region);
|
||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
let parent = parent
|
let idx = self.moves.push(parent, region);
|
||||||
.and_then(|p| self.slots.get(&p).copied())
|
|
||||||
.unwrap_or(MoveIdx::NONE);
|
|
||||||
let idx = self.moves.push(parent);
|
|
||||||
self.slots.insert(id, idx);
|
self.slots.insert(id, idx);
|
||||||
idx
|
idx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gives up a slot a widget no longer needs, because it is drawn somewhere
|
||||||
|
/// that does not place it. Its descendants name it, so this is only
|
||||||
|
/// reached where they are about to be drawn again.
|
||||||
|
fn drop_slot(&mut self, id: WidgetId) {
|
||||||
|
if let Some(idx) = self.slots.remove(&id) {
|
||||||
|
self.moves.remove(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The drawing a widget already has, kept for a new box if the box has not
|
/// The drawing a widget already has, kept for a new box if the box has not
|
||||||
/// changed in a way it depends on.
|
/// changed in a way it depends on.
|
||||||
fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option<Size> {
|
fn try_reuse(
|
||||||
|
&mut self,
|
||||||
|
id: WidgetId,
|
||||||
|
region: UiRegion,
|
||||||
|
parent_move: MoveIdx,
|
||||||
|
rsc: &mut dyn UiRsc,
|
||||||
|
) -> Option<Size> {
|
||||||
if rsc.widgets().needs_redraw.contains(&id) {
|
if rsc.widgets().needs_redraw.contains(&id) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let active = self.active.get(&id)?;
|
let active = self.active.get(&id)?;
|
||||||
let (size, old, slot) = (active.size, active.region, active.move_idx);
|
// Drawn somewhere else in the tree: its box is in coordinates it no
|
||||||
// TODO: epsilon?
|
// longer sits in, and its slot names the wrong parent.
|
||||||
if old.size() == region.size() {
|
if active.parent_move != parent_move {
|
||||||
// The right shape and only somewhere else, which is one slot to
|
|
||||||
// write however much is under it. Both boxes are in the
|
|
||||||
// coordinates its parent drew, so the chain above applies alike.
|
|
||||||
let moved =
|
|
||||||
region.to_px(self.output_size).top_left - old.to_px(self.output_size).top_left;
|
|
||||||
self.moves.set(slot, UiRegion::FULL.offset(moved));
|
|
||||||
return Some(size);
|
|
||||||
}
|
|
||||||
if !self.reusable(id, region, rsc) || !old.stretchable() {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// Its drawing stands, re-expressed as the same fractions of the box.
|
let (size, old, slot) = (active.size, active.region, active.move_idx);
|
||||||
self.stretch(id, old, region);
|
if old == region {
|
||||||
|
return Some(size);
|
||||||
|
}
|
||||||
|
// Only a placed widget can be given a different box without drawing
|
||||||
|
// again: everything it drew is a fraction of its slot's box, so one
|
||||||
|
// entry says where all of it went.
|
||||||
|
if slot == parent_move {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut changed = [false; 2];
|
||||||
|
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
|
||||||
|
*c = region.axis(axis).len() != old.axis(axis).len();
|
||||||
|
}
|
||||||
|
if changed.iter().any(|&c| c) {
|
||||||
|
let widget = rsc.widgets().get_dyn(id)?;
|
||||||
|
let redraws = AXES
|
||||||
|
.into_iter()
|
||||||
|
.zip(changed)
|
||||||
|
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale);
|
||||||
|
if redraws {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.moves.set(slot, region);
|
||||||
|
self.active.get_mut(&id).unwrap().region = region;
|
||||||
|
if changed.iter().any(|&c| c) {
|
||||||
|
self.mark_resized(id, changed, rsc);
|
||||||
|
}
|
||||||
Some(size)
|
Some(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the widget can keep the drawing it has and be given `region`
|
/// Marks every descendant whose drawing cannot survive the box it is a
|
||||||
/// instead, asked one axis at a time: a change on an axis it does not
|
/// fraction of changing length, `changed` saying which axes of that box
|
||||||
/// depend on costs nothing, whatever it depends on elsewhere.
|
/// did.
|
||||||
fn reusable(&self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> bool {
|
///
|
||||||
|
/// A part of a box with no relative extent on an axis is a fixed length,
|
||||||
|
/// held as offsets from that box's start, and composing anything into it
|
||||||
|
/// leaves no relative extent either. So a widget whose own box did not
|
||||||
|
/// change length has no descendant whose box did, and the walk stops
|
||||||
|
/// there.
|
||||||
|
fn mark_resized(&mut self, id: WidgetId, changed: [bool; 2], rsc: &mut dyn UiRsc) {
|
||||||
let Some(active) = self.active.get(&id) else {
|
let Some(active) = self.active.get(&id) else {
|
||||||
return false;
|
return;
|
||||||
};
|
};
|
||||||
let Some(widget) = rsc.widgets().get_dyn(id) else {
|
// SAFETY: children cannot be recursive
|
||||||
return false;
|
let children = unsafe { forget_ref(&active.children) };
|
||||||
|
for &child in children {
|
||||||
|
let Some(data) = self.active.get(&child) else {
|
||||||
|
continue;
|
||||||
};
|
};
|
||||||
[Axis::X, Axis::Y].into_iter().all(|axis| {
|
let region = data.region;
|
||||||
let offered = region.axis(axis).len();
|
let mut own = changed;
|
||||||
let had = active.region.axis(axis).len();
|
for (axis, c) in AXES.into_iter().zip(own.iter_mut()) {
|
||||||
match widget.on_resize(axis) {
|
*c &= region.axis(axis).len().rel != 0.0;
|
||||||
OnResize::Scale => true,
|
}
|
||||||
// `Translate` is not acted on yet, and cannot be until a
|
if !own.iter().any(|&c| c) {
|
||||||
// drawing can sit somewhere other than its box. `region` is
|
continue;
|
||||||
// both the box a widget was given and the box its primitives
|
}
|
||||||
// are in, and `mov` remaps from it -- so carrying a drawing at
|
let redraws = match rsc.widgets().get_dyn(child) {
|
||||||
// its old size while the box grows makes the next move stretch
|
Some(widget) => AXES
|
||||||
// it. The offset chain is what separates the two.
|
.into_iter()
|
||||||
OnResize::Translate | OnResize::Redraw => offered == had,
|
.zip(own)
|
||||||
|
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale),
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
match redraws {
|
||||||
|
true => {
|
||||||
|
rsc.widgets_mut().needs_redraw.insert(child);
|
||||||
|
}
|
||||||
|
false => self.mark_resized(child, own, rsc),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
|
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
|
||||||
let Some(widget) = rsc.widgets().get_dyn(id) else {
|
let Some(widget) = rsc.widgets().get_dyn(id) else {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
[Axis::X, Axis::Y].into_iter().all(|axis| {
|
AXES.into_iter().all(|axis| {
|
||||||
widget
|
widget
|
||||||
.size_hint(axis)
|
.size_hint(axis)
|
||||||
.is_none_or(|hint| hint == size.axis(axis))
|
.is_none_or(|hint| hint == size.axis(axis))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rewrites a subtree's regions as the same fractions of a new box, for a
|
|
||||||
/// change of length that a slot cannot express. Every region it rewrites
|
|
||||||
/// is a region some slot was a delta from, so those go back to zero.
|
|
||||||
fn stretch(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
|
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
|
||||||
for h in &active.primitives {
|
|
||||||
let region = self.layers[h.layer].region_mut(h);
|
|
||||||
*region = region.stretch(&from, &to);
|
|
||||||
}
|
|
||||||
active.region = active.region.stretch(&from, &to);
|
|
||||||
self.moves.set(active.move_idx, UiRegion::FULL);
|
|
||||||
// SAFETY: children cannot be recursive
|
|
||||||
let children = unsafe { forget_ref(&active.children) };
|
|
||||||
for child in children {
|
|
||||||
self.stretch(*child, from, to);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NOTE: instance textures are cleared and self.textures freed
|
/// NOTE: instance textures are cleared and self.textures freed
|
||||||
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
|
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
|
||||||
let mut active = self.active.remove(&id);
|
let mut active = self.active.remove(&id);
|
||||||
@@ -363,11 +417,11 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where a widget is on screen: the box it drew against plus whatever the
|
/// Where a widget is on screen: its box composed through the boxes it
|
||||||
/// chain above has moved since, which is the walk the vertex shader does.
|
/// sits within, which is the walk the vertex shader does.
|
||||||
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||||
let active = self.active.get(&id.id())?;
|
let active = self.active.get(&id.id())?;
|
||||||
let region = self.moves.resolve(active.move_idx, active.region);
|
let region = self.moves.resolve(active.parent_move, active.region);
|
||||||
Some(region.to_px(self.output_size))
|
Some(region.to_px(self.output_size))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,6 +454,8 @@ impl UiRenderState {
|
|||||||
id,
|
id,
|
||||||
active.region,
|
active.region,
|
||||||
active.parent,
|
active.parent,
|
||||||
|
active.parent_move,
|
||||||
|
active.move_idx != active.parent_move,
|
||||||
active.mask,
|
active.mask,
|
||||||
Some(active.children),
|
Some(active.children),
|
||||||
rsc,
|
rsc,
|
||||||
|
|||||||
@@ -9,14 +9,20 @@ impl Widget for Aligned {
|
|||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
// Drawn where it may be too big, then given its aligned box once its
|
// Drawn where it may be too big, then given its aligned box once its
|
||||||
// size is known.
|
// size is known.
|
||||||
let size = painter.widget(&self.inner).size();
|
let size = painter.place(&self.inner, UiRegion::FULL).size();
|
||||||
let region = match self.align.tuple() {
|
let region = match self.align.tuple() {
|
||||||
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
|
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
|
||||||
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
|
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
|
||||||
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
|
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
|
||||||
(None, None) => UiRegion::FULL,
|
(None, None) => UiRegion::FULL,
|
||||||
};
|
};
|
||||||
painter.widget_within(&self.inner, region);
|
painter.place(&self.inner, region);
|
||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The aligned box is a fraction of its own, so the child keeps its
|
||||||
|
/// length and stays against the edge it was aligned to.
|
||||||
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
|
OnResize::Scale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,4 +12,8 @@ impl Widget for LayerOffset {
|
|||||||
}
|
}
|
||||||
painter.widget(&self.inner).size()
|
painter.widget(&self.inner).size()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
|
OnResize::Scale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,4 +10,8 @@ impl Widget for Offset {
|
|||||||
let region = UiRegion::FULL.offset(self.amt);
|
let region = UiRegion::FULL.offset(self.amt);
|
||||||
painter.widget_within(&self.inner, region).size()
|
painter.widget_within(&self.inner, region).size()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
|
OnResize::Scale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,12 @@ impl Widget for Pad {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The padding is an offset from each edge, so a longer box pads the same
|
||||||
|
/// amount and the child takes the rest.
|
||||||
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
|
OnResize::Scale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Padding {
|
pub struct Padding {
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ pub struct Scroll {
|
|||||||
impl Widget for Scroll {
|
impl Widget for Scroll {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let output_len = painter.output_size().axis(self.axis);
|
let output_len = painter.output_size().axis(self.axis);
|
||||||
let container_len = painter.region().axis(self.axis).len();
|
let container_len = UiScalar::abs(painter.px_size().axis(self.axis));
|
||||||
// Drawn in the whole container to learn its length, then placed at
|
// Drawn in the whole container to learn its length, then placed at
|
||||||
// the scrolled offset.
|
// the scrolled offset.
|
||||||
let child = painter.widget(&self.inner).size();
|
let child = painter.place(&self.inner, UiRegion::FULL).size();
|
||||||
let content_len = child
|
let content_len = child
|
||||||
.axis(self.axis)
|
.axis(self.axis)
|
||||||
.apply_rest()
|
.apply_rest()
|
||||||
@@ -31,7 +31,7 @@ impl Widget for Scroll {
|
|||||||
|
|
||||||
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -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);
|
||||||
painter.widget_within(&self.inner, region);
|
painter.place(&self.inner, region);
|
||||||
child
|
child
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,8 @@ impl Widget for SetSize {
|
|||||||
Axis::Y => self.y,
|
Axis::Y => self.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
|
OnResize::Scale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ impl Widget for Span {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|child| match painter.size_hint(child, axis) {
|
.map(|child| match painter.size_hint(child, axis) {
|
||||||
Some(len) => len,
|
Some(len) => len,
|
||||||
None => painter.widget(child).len(axis),
|
None => painter.place(child, UiRegion::FULL).len(axis),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ impl Widget for Span {
|
|||||||
if self.dir.sign == Sign::Neg {
|
if self.dir.sign == Sign::Neg {
|
||||||
region.flip(axis);
|
region.flip(axis);
|
||||||
}
|
}
|
||||||
let used = painter.widget_within(child, region).size().axis(!axis);
|
let used = painter.place(child, region).size().axis(!axis);
|
||||||
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
|
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
|
||||||
if used.rel > 0.0 || used.rest > 0.0 {
|
if used.rel > 0.0 || used.rest > 0.0 {
|
||||||
ortho = Len::REST;
|
ortho = Len::REST;
|
||||||
@@ -58,6 +58,12 @@ impl Widget for Span {
|
|||||||
};
|
};
|
||||||
Size::from_axis(axis, along, ortho)
|
Size::from_axis(axis, along, ortho)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every child is placed in fractions and offsets of the span's own box,
|
||||||
|
/// so a longer box holds the same layout and the children follow it.
|
||||||
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
|
OnResize::Scale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Span {
|
impl Span {
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ impl Widget for Stack {
|
|||||||
}
|
}
|
||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
|
OnResize::Scale
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
|
|||||||
+1
-1
@@ -77,7 +77,7 @@ fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
|||||||
|
|
||||||
let mut slot = MoveIdx::NONE;
|
let mut slot = MoveIdx::NONE;
|
||||||
for _ in 0..depth {
|
for _ in 0..depth {
|
||||||
slot = render.moves.push(slot);
|
slot = render.moves.push(slot, UiRegion::FULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
let px = |v: f32| UiScalar { rel: 0.0, abs: v };
|
let px = |v: f32| UiScalar { rel: 0.0, abs: v };
|
||||||
|
|||||||
@@ -126,3 +126,59 @@ fn a_moved_subtree_takes_its_children_with_it() {
|
|||||||
// `inner`'s own region was never rewritten.
|
// `inner`'s own region was never rewritten.
|
||||||
assert_corners!(h, inner, (10, 90), (390, 110));
|
assert_corners!(h, inner, (10, 90), (390, 110));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc);
|
||||||
|
let rest = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
let panel = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
// Changing the bar's width is the only thing that changes the box the
|
||||||
|
// panel and everything under it was drawn for.
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, panel).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, fixed, (100, 0), (150, 200));
|
||||||
|
assert_corners!(h, rest, (150, 0), (400, 200));
|
||||||
|
|
||||||
|
h.rsc[bar].x = Some(Len::abs(200));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
|
||||||
|
// against its new start and the one taking the rest absorbs the change.
|
||||||
|
assert_corners!(h, fixed, (200, 0), (250, 200));
|
||||||
|
assert_corners!(h, rest, (250, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// The row is 40 tall whatever happens, which used to make its drawing
|
||||||
|
// impossible to take out of: recovering a fraction of a box needs a
|
||||||
|
// relative extent, and it has none on that axis.
|
||||||
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let row = inner.pad(10).height(40).add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, column).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, inner, (110, 10), (390, 30));
|
||||||
|
|
||||||
|
h.rsc[bar].x = Some(Len::abs(200));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_corners!(h, inner, (210, 10), (390, 30));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_a_container_that_places_its_children_lengthens_the_chain() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let leaf = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
// Four widgets between the span and the leaf, none of which places what
|
||||||
|
// it draws, so all of them share the span's slot.
|
||||||
|
let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc);
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, buried).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
let slot = h.render.active[&leaf.id()].parent_move;
|
||||||
|
assert_eq!(h.render.moves.depth(slot), 1, "one span above the leaf");
|
||||||
|
}
|
||||||
+46
-5
@@ -251,9 +251,8 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
|
|||||||
assert_corners!(h, below, (12, 232), (388, 388));
|
assert_corners!(h, below, (12, 232), (388, 388));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Claims its drawing may be stretched, and has a child so that the stretch
|
/// Claims its drawing survives its box changing length, and has a child so
|
||||||
/// has to reach one. No shipped container claims `Scale` -- `Rect`, `Image`
|
/// that the walk looking for what does not has one to reach.
|
||||||
/// and `()` are all childless -- so nothing else walks a subtree to remap it.
|
|
||||||
struct Stretchy {
|
struct Stretchy {
|
||||||
inner: StrongWidget,
|
inner: StrongWidget,
|
||||||
draws: Rc<Cell<usize>>,
|
draws: Rc<Cell<usize>>,
|
||||||
@@ -271,7 +270,7 @@ impl Widget for Stretchy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stretching_a_subtree_remaps_the_children_in_it() {
|
fn stretching_a_subtree_carries_the_children_in_it() {
|
||||||
let mut h = Harness::new((400, 400));
|
let mut h = Harness::new((400, 400));
|
||||||
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);
|
||||||
@@ -291,8 +290,50 @@ fn stretching_a_subtree_remaps_the_children_in_it() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
draws.get(),
|
draws.get(),
|
||||||
settled,
|
settled,
|
||||||
"its drawing is stretched, not redrawn"
|
"its drawing follows its box, rather than being made again"
|
||||||
);
|
);
|
||||||
assert_corners!(h, outer, (0, 80), (400, 400));
|
assert_corners!(h, outer, (0, 80), (400, 400));
|
||||||
assert_corners!(h, inner, (0, 80), (400, 400));
|
assert_corners!(h, inner, (0, 80), (400, 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// What a transcript row is: something whose shaping depends on the width
|
||||||
|
// it is given, beside something that only has to be the right shape.
|
||||||
|
let (wraps, wrap_draws) = counted(&mut h, Size::REST, OnResize::Redraw);
|
||||||
|
let (backing, back_draws) = counted(&mut h, Size::REST, OnResize::Scale);
|
||||||
|
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, row).span(Dir::RIGHT));
|
||||||
|
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
|
||||||
|
|
||||||
|
h.rsc[bar].x = Some(Len::abs(200));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
// The span reads every child's size, so redrawing one takes the span
|
||||||
|
// with it -- and the span then measures and places the redrawn child.
|
||||||
|
assert!(wrap_draws.get() > settled_wrap, "reads the width it got");
|
||||||
|
assert_eq!(back_draws.get(), settled_back, "only has to be the shape");
|
||||||
|
assert_corners!(h, backing, (200, 0), (300, 200));
|
||||||
|
assert_corners!(h, wraps, (300, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fixed_length_child_is_not_redrawn_when_the_box_around_it_grows() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// It would be drawn again for a width it does not have: its own box is
|
||||||
|
// a fixed 80 wherever the row's edges end up.
|
||||||
|
let (fixed, draws) = counted(&mut h, Size::from((80, 200)), OnResize::Redraw);
|
||||||
|
let (rest, _) = counted(&mut h, Size::REST, OnResize::Scale);
|
||||||
|
let row = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, row).span(Dir::RIGHT));
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
h.rsc[bar].x = Some(Len::abs(200));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(draws.get(), settled, "its own length did not change");
|
||||||
|
assert_corners!(h, fixed, (200, 0), (280, 200));
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
//! What a drawing can be taken out of, and what it cannot.
|
|
||||||
|
|
||||||
use iris::core::{UiRegion, UiScalar, UiSpan};
|
|
||||||
|
|
||||||
/// A box `size` tall whose top is `rel` of the way down the window.
|
|
||||||
fn fixed(rel: f32, size: f32) -> UiRegion {
|
|
||||||
UiRegion::new(
|
|
||||||
UiSpan::FULL,
|
|
||||||
UiSpan::new(UiScalar { rel, abs: 0.0 }, UiScalar { rel, abs: size }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_fixed_length_cannot_be_stretched_out_of() {
|
|
||||||
assert!(!fixed(0.0, 164.0).stretchable());
|
|
||||||
assert!(!fixed(0.5, 164.0).stretchable());
|
|
||||||
assert!(UiRegion::FULL.stretchable());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_stretch_keeps_each_part_at_its_fraction() {
|
|
||||||
let to = fixed(0.0, 98.0);
|
|
||||||
// A part filling the window fills what replaced it.
|
|
||||||
assert_eq!(UiRegion::FULL.stretch(&UiRegion::FULL, &to), to);
|
|
||||||
// And the middle half of it stays the middle half.
|
|
||||||
let half = UiRegion::new(
|
|
||||||
UiSpan::FULL,
|
|
||||||
UiSpan::new(UiScalar::rel(0.25), UiScalar::rel(0.75)),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
half.stretch(&UiRegion::FULL, &to),
|
|
||||||
UiRegion::new(
|
|
||||||
UiSpan::FULL,
|
|
||||||
UiSpan::new(
|
|
||||||
UiScalar {
|
|
||||||
rel: 0.0,
|
|
||||||
abs: 24.5
|
|
||||||
},
|
|
||||||
UiScalar {
|
|
||||||
rel: 0.0,
|
|
||||||
abs: 73.5
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user