Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e44dea34b4 | ||
|
|
a0693acc56 | ||
|
|
25e456e0b5 | ||
|
|
53b00c68e9 | ||
|
|
a92c6acdbf | ||
|
|
c8beca5753 | ||
|
|
4bd8607968 |
No files matched your search
@@ -109,6 +109,18 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The first step at or above `v`, where [`Self::from_f32`] takes the
|
||||||
|
/// nearest one and is below it half the time. For a bound that has to
|
||||||
|
/// admit the value it came from: a measurement rounded down is a bound
|
||||||
|
/// that leaves out the thing it was measured from.
|
||||||
|
pub const fn ceil_from_f32(v: f32) -> Self {
|
||||||
|
let nearest = Self::from_f32(v);
|
||||||
|
match nearest.to_f32() < v {
|
||||||
|
true => nearest.next_up(),
|
||||||
|
false => nearest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// From a number as it is written in source -- `16`, `1.5` -- which is
|
/// From a number as it is written in source -- `16`, `1.5` -- which is
|
||||||
/// the other place a value enters the grid.
|
/// the other place a value enters the grid.
|
||||||
pub fn from_num(v: impl UiNum) -> Self {
|
pub fn from_num(v: impl UiNum) -> Self {
|
||||||
@@ -372,6 +384,12 @@ impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
|||||||
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
|
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The first step at or above each part, for a measurement reported as a
|
||||||
|
/// box: what it occupies is not less than what was measured.
|
||||||
|
pub fn ceil_from_f32(v: Vec2) -> Self {
|
||||||
|
Self::new(Fixed::ceil_from_f32(v.x), Fixed::ceil_from_f32(v.y))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_f32(self) -> Vec2 {
|
pub fn to_f32(self) -> Vec2 {
|
||||||
Vec2::new(self.x.to_f32(), self.y.to_f32())
|
Vec2::new(self.x.to_f32(), self.y.to_f32())
|
||||||
}
|
}
|
||||||
@@ -488,6 +506,25 @@ mod tests {
|
|||||||
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
|
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The bound a greedy line break needs: the width it was measured at is
|
||||||
|
/// not on the grid, and the narrowest box the break still holds for is
|
||||||
|
/// the step at or above it, never the one below.
|
||||||
|
#[test]
|
||||||
|
fn a_ceiling_never_lands_below_the_number_it_came_from() {
|
||||||
|
let step = 1.0 / (1 << PX_SHIFT) as f32;
|
||||||
|
for n in 0..64 {
|
||||||
|
let v = 189.0 + n as f32 * step / 3.0;
|
||||||
|
let up = Px::ceil_from_f32(v);
|
||||||
|
assert!(up.to_f32() >= v, "{up:?} is below {v}");
|
||||||
|
assert!(
|
||||||
|
up.to_f32() - v < step,
|
||||||
|
"{up:?} is more than a step above {v}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// An exact step is its own ceiling.
|
||||||
|
assert_eq!(Px::ceil_from_f32(189.5), Px::from_f32(189.5));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_number_from_outside_is_clamped_to_the_grid() {
|
fn a_number_from_outside_is_clamped_to_the_grid() {
|
||||||
assert_eq!(Px::from_f32(1e12), Px::MAX);
|
assert_eq!(Px::from_f32(1e12), Px::MAX);
|
||||||
|
|||||||
@@ -107,13 +107,6 @@ impl Default for TextAttrs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How far below the longest line a width may fall and still be answered by
|
|
||||||
/// the break in hand. A parent that offers a child the length it reported
|
|
||||||
/// composes that length back through the box chain, so the two differ in the
|
|
||||||
/// last bits -- and at exactly the longest line, that decides whether a line
|
|
||||||
/// fits. Sub-pixel, so no break it admits is one a reader could see.
|
|
||||||
const BREAK_EPSILON_PX: f32 = 0.05;
|
|
||||||
|
|
||||||
/// Keeps text and its corresponding layout from getting out of sync.
|
/// Keeps text and its corresponding layout from getting out of sync.
|
||||||
pub struct TextBuffer {
|
pub struct TextBuffer {
|
||||||
text: String,
|
text: String,
|
||||||
@@ -200,15 +193,19 @@ impl TextBuffer {
|
|||||||
// A greedy break at one width is the same break at every width down
|
// A greedy break at one width is the same break at every width down
|
||||||
// to the longest line it produced: each line still fits, and none can
|
// to the longest line it produced: each line still fits, and none can
|
||||||
// take a word that would not fit in the wider box. So the layout in
|
// take a word that would not fit in the wider box. So the layout in
|
||||||
// hand already answers, and re-breaking would only be a chance to
|
// hand already answers, and re-breaking would only be work.
|
||||||
// disagree with itself -- which is what happens when a parent offers
|
//
|
||||||
// a child the length that child just reported, and the two land
|
// At the longest line exactly, with no margin below it. A narrower
|
||||||
// either side of a float.
|
// width really does break differently, so answering one from the
|
||||||
|
// break in hand is how a warm tree keeps lines a cold tree would
|
||||||
|
// never produce. The margin was here because a text reports the
|
||||||
|
// width it used and a parent hands that back; the report is the step
|
||||||
|
// at or above its longest line now, so what comes back fits.
|
||||||
if let Some(key) = &self.layout_key
|
if let Some(key) = &self.layout_key
|
||||||
&& key.attrs == *attrs
|
&& key.attrs == *attrs
|
||||||
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
||||||
&& want <= broke_at
|
&& want <= broke_at
|
||||||
&& want + BREAK_EPSILON_PX >= self.layout.width()
|
&& want >= self.layout.width()
|
||||||
{
|
{
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::TextShapeHits);
|
diag::bump(Counter::TextShapeHits);
|
||||||
|
|||||||
@@ -74,4 +74,12 @@ impl ActiveData {
|
|||||||
pub fn holds_at(&self, px: crate::PxVec2) -> bool {
|
pub fn holds_at(&self, px: crate::PxVec2) -> bool {
|
||||||
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
|
self.holds[0].contains(px.x) && self.holds[1].contains(px.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether what it answered still stands for a box of these pixel
|
||||||
|
/// lengths -- the box it was asked in, where `holds` is about the box its
|
||||||
|
/// answer then chose.
|
||||||
|
pub fn answers_at(&self, px: crate::PxVec2) -> bool {
|
||||||
|
let (_, holds) = self.answer;
|
||||||
|
holds[0].contains(px.x) && holds[1].contains(px.y)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+99
-61
@@ -43,7 +43,9 @@ pub struct UiRenderState {
|
|||||||
|
|
||||||
old_root: Option<WidgetId>,
|
old_root: Option<WidgetId>,
|
||||||
/// Whether the output has changed since the last update. A frame is
|
/// Whether the output has changed since the last update. A frame is
|
||||||
/// owed for that whether or not anything has to be drawn again.
|
/// owed for that whether or not anything has to be drawn again: every
|
||||||
|
/// fraction becomes pixels against the output, in the shader's uniform
|
||||||
|
/// as well as here.
|
||||||
resized: bool,
|
resized: bool,
|
||||||
/// 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.
|
||||||
@@ -55,6 +57,9 @@ pub struct UiRenderState {
|
|||||||
/// Whether this frame contains a declared-length change, so any dirty
|
/// Whether this frame contains a declared-length change, so any dirty
|
||||||
/// dependent replaces its answer too.
|
/// dependent replaces its answer too.
|
||||||
replace_answers: bool,
|
replace_answers: bool,
|
||||||
|
/// Widgets waiting for an ancestor to draw them, so the walk down the
|
||||||
|
/// depths does not pick one up again at its own depth.
|
||||||
|
deferred: crate::util::HashSet<WidgetId>,
|
||||||
pub moves: Moves,
|
pub moves: Moves,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +73,7 @@ impl UiRenderState {
|
|||||||
slots: Default::default(),
|
slots: Default::default(),
|
||||||
answer_invalid: Default::default(),
|
answer_invalid: Default::default(),
|
||||||
replace_answers: false,
|
replace_answers: false,
|
||||||
|
deferred: Default::default(),
|
||||||
moves: Default::default(),
|
moves: Default::default(),
|
||||||
resized: false,
|
resized: false,
|
||||||
}
|
}
|
||||||
@@ -79,13 +85,28 @@ impl UiRenderState {
|
|||||||
/// size is applied where a fraction becomes pixels -- here in `to_px`,
|
/// size is applied where a fraction becomes pixels -- here in `to_px`,
|
||||||
/// and in the shader by its uniform. A resize therefore rewrites no
|
/// and in the shader by its uniform. A resize therefore rewrites no
|
||||||
/// retained entry at all.
|
/// retained entry at all.
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
///
|
||||||
|
/// The root is the only widget a resize marks, and only where the new
|
||||||
|
/// output falls outside what its answer holds for: that range is the
|
||||||
|
/// intersection of everything under it, so admitting the new output says
|
||||||
|
/// the whole tree still stands. Where it does not, the ordinary walk
|
||||||
|
/// draws the root, and each widget's own range decides how far down the
|
||||||
|
/// new length reaches.
|
||||||
|
pub fn resize(&mut self, size: impl Into<Vec2>, widgets: &mut Widgets) {
|
||||||
let size = PxVec2::from_f32(size.into());
|
let size = PxVec2::from_f32(size.into());
|
||||||
if size == self.output_size {
|
if size == self.output_size {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.output_size = size;
|
self.output_size = size;
|
||||||
self.resized = true;
|
self.resized = true;
|
||||||
|
let Some(root) = self.old_root else { return };
|
||||||
|
let stands = self
|
||||||
|
.active
|
||||||
|
.get(&root)
|
||||||
|
.is_some_and(|active| active.answers_at(active.given_len.to_px(size)));
|
||||||
|
if !stands {
|
||||||
|
widgets.needs_redraw.insert(root);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The root is asked about in the output: the window is where a fraction
|
/// The root is asked about in the output: the window is where a fraction
|
||||||
@@ -139,17 +160,6 @@ impl UiRenderState {
|
|||||||
if self.root_changed(root) {
|
if self.root_changed(root) {
|
||||||
self.redraw_all(root, rsc);
|
self.redraw_all(root, rsc);
|
||||||
self.old_root = root.map(|r| r.id());
|
self.old_root = root.map(|r| r.id());
|
||||||
} else if let Some(root) = root
|
|
||||||
&& self.resized
|
|
||||||
{
|
|
||||||
// The output is the root's box, so a resize is that box changing
|
|
||||||
// length, found the way every other box change is found. Before
|
|
||||||
// anything dirty settles, so that whatever a new output draws
|
|
||||||
// again is drawn once, in the box it will have.
|
|
||||||
let region = Self::root_region(root.id(), rsc.widgets());
|
|
||||||
let info = self.root_info(region);
|
|
||||||
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() {
|
||||||
@@ -192,14 +202,10 @@ impl UiRenderState {
|
|||||||
diag::draw_request(id, info.parent, region, info.px, info.region_node);
|
diag::draw_request(id, info.parent, region, info.px, info.region_node);
|
||||||
}
|
}
|
||||||
let align = rsc.widgets().alignment(id);
|
let align = rsc.widgets().alignment(id);
|
||||||
// Nothing this widget has is an answer while something it measured
|
// Nothing this widget measured can be dirty while it draws: layout is
|
||||||
// is dirty: settling that changes what it would report, and a widget
|
// one bottom-up walk, so anything deeper has settled or deferred to
|
||||||
// settled inside its parent's draw tells nobody -- the comparison
|
// its own parent, and a deferred one leaves that parent marked.
|
||||||
// that marks a reader is in `redraw`, which is not what asked here.
|
let stale = rsc.widgets().needs_redraw.contains(&id);
|
||||||
// Both retained routes are an answer, so the question is asked once
|
|
||||||
// rather than by each of them.
|
|
||||||
let stale =
|
|
||||||
rsc.widgets().needs_redraw.contains(&id) || self.dirty_size_under(id, rsc.widgets());
|
|
||||||
let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale);
|
let replace_answer = self.answer_invalid.remove(&id) || (self.replace_answers && stale);
|
||||||
let retained = match replace_answer || stale {
|
let retained = match replace_answer || stale {
|
||||||
true => None,
|
true => None,
|
||||||
@@ -247,7 +253,18 @@ impl UiRenderState {
|
|||||||
active.answer = settled;
|
active.answer = settled;
|
||||||
active.decided = info.decided;
|
active.decided = info.decided;
|
||||||
active.own_align = align;
|
active.own_align = align;
|
||||||
active.depth = info.depth;
|
// A subtree can be reused whole under a different parent -- same box,
|
||||||
|
// same layer, same region node -- and nothing in the drawing says it
|
||||||
|
// changed hands. Two things read who its parent is: a deferral, which
|
||||||
|
// marks whoever has it to draw, and the old parent's list of children,
|
||||||
|
// which its next draw undraws whatever is missing from.
|
||||||
|
let old_parent = std::mem::replace(&mut active.parent, info.parent);
|
||||||
|
if old_parent != info.parent
|
||||||
|
&& let Some(old_parent) = old_parent
|
||||||
|
&& let Some(old_parent) = self.active.get_mut(&old_parent)
|
||||||
|
{
|
||||||
|
old_parent.children.retain(|child| *child != id);
|
||||||
|
}
|
||||||
settled
|
settled
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,7 +497,7 @@ impl UiRenderState {
|
|||||||
parent_move: MoveIdx,
|
parent_move: MoveIdx,
|
||||||
widgets: &Widgets,
|
widgets: &Widgets,
|
||||||
) -> Option<(Size, [Holds; 2])> {
|
) -> Option<(Size, [Holds; 2])> {
|
||||||
if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) {
|
if widgets.needs_redraw.contains(&id) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let active = self.active.get(&id)?;
|
let active = self.active.get(&id)?;
|
||||||
@@ -505,22 +522,7 @@ impl UiRenderState {
|
|||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let (size, holds) = active.answer;
|
active.answers_at(info.px).then_some(active.answer)
|
||||||
(holds[0].contains(info.px.x) && holds[1].contains(info.px.y)).then_some((size, holds))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether anything whose size this widget's own size was read from is
|
|
||||||
/// dirty, which makes what it would answer not yet known. It also keeps
|
|
||||||
/// a reader that asks first from laying out twice, which is all it was
|
|
||||||
/// here for while a changed size was thought to reach its reader in any
|
|
||||||
/// order; it does not, where the change settles inside the reader's own
|
|
||||||
/// draw.
|
|
||||||
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
|
||||||
self.active.get(&id).is_some_and(|active| {
|
|
||||||
active.size_deps.iter().any(|child| {
|
|
||||||
widgets.needs_redraw.contains(child) || self.dirty_size_under(*child, widgets)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pixel lengths of the box a widget was given and of the box it was
|
/// The pixel lengths of the box a widget was given and of the box it was
|
||||||
@@ -634,12 +636,12 @@ impl UiRenderState {
|
|||||||
self.remap_subtree(id, &remap, info.parent_move, rsc);
|
self.remap_subtree(id, &remap, info.parent_move, rsc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.redepth(id, info.depth);
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
active.region = region;
|
active.region = region;
|
||||||
active.given = region;
|
active.given = region;
|
||||||
active.given_len = info.given_len;
|
active.given_len = info.given_len;
|
||||||
active.offer_len = info.offer_len;
|
active.offer_len = info.offer_len;
|
||||||
active.depth = info.depth;
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
{
|
{
|
||||||
match (moved, has_region_node) {
|
match (moved, has_region_node) {
|
||||||
@@ -663,6 +665,24 @@ impl UiRenderState {
|
|||||||
Some(answer)
|
Some(answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A reused subtree keeps its shape, so every widget in it moves by the
|
||||||
|
/// same amount -- and where the top of it did not move, none of it did,
|
||||||
|
/// which is what makes this free in the ordinary case.
|
||||||
|
fn redepth(&mut self, id: WidgetId, depth: usize) {
|
||||||
|
let Some(active) = self.active.get_mut(&id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if active.depth == depth {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
active.depth = depth;
|
||||||
|
let children = active.children.len();
|
||||||
|
for index in 0..children {
|
||||||
|
let child = self.active[&id].children[index];
|
||||||
|
self.redepth(child, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Re-expresses an ordinary retained subtree in a new parent region.
|
/// Re-expresses an ordinary retained subtree in a new parent region.
|
||||||
/// An independently movable descendant needs only its own region changed;
|
/// An independently movable descendant needs only its own region changed;
|
||||||
/// its contents stay in that region's coordinate space.
|
/// its contents stay in that region's coordinate space.
|
||||||
@@ -827,18 +847,33 @@ impl UiRenderState {
|
|||||||
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
let _layout = diag::timer(TimerKind::IncrementalLayout);
|
let _layout = diag::timer(TimerKind::IncrementalLayout);
|
||||||
// Deepest first: a reader whose children have all settled asks each
|
// Deepest first, and strictly: a widget that cannot settle where it
|
||||||
// once, where any other order has it lay out again for whatever
|
// is defers to its parent rather than drawing the parent from
|
||||||
// settles under it afterwards. Equal-depth widgets are independent,
|
// inside itself. It marks the parent, stays marked, and waits here
|
||||||
// so their order does not matter.
|
// until the walk reaches its parent's depth.
|
||||||
while let Some(id) = {
|
//
|
||||||
let dirty = rsc.widgets().needs_redraw.iter().copied();
|
// What that buys is that nothing shallower is ever drawn while
|
||||||
dirty.max_by_key(|&id| self.depth(id))
|
// anything deeper is still dirty. A parent drawing can therefore
|
||||||
} {
|
// trust every answer it reads without descending to check whether
|
||||||
|
// something below is about to change it -- which is the whole class
|
||||||
|
// of defect where a widget settles inside its parent's draw, clears
|
||||||
|
// its mark there, and tells nobody its answer moved.
|
||||||
|
loop {
|
||||||
|
let next = rsc
|
||||||
|
.widgets()
|
||||||
|
.needs_redraw
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|id| !self.deferred.contains(id))
|
||||||
|
.max_by_key(|&id| self.depth(id));
|
||||||
|
let Some(id) = next else { break };
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::QueuePops);
|
diag::bump(Counter::QueuePops);
|
||||||
self.redraw(id, rsc);
|
if !self.redraw(id, rsc) {
|
||||||
|
self.deferred.insert(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
self.deferred.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn depth(&self, id: WidgetId) -> usize {
|
fn depth(&self, id: WidgetId) -> usize {
|
||||||
@@ -923,11 +958,13 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Settles a dirty widget: asks it again where its parent asked, and
|
/// Settles a dirty widget: asks it again where its parent asked, and
|
||||||
/// tells the parent if the answer changed.
|
/// tells the parent if the answer changed. `false` where the question is
|
||||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
/// its parent's rather than its own, which leaves it marked for the
|
||||||
|
/// parent to draw when the walk reaches that depth.
|
||||||
|
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> bool {
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
let Some(active) = self.active.get(&id) else {
|
let Some(active) = self.active.get(&id) else {
|
||||||
return;
|
return true;
|
||||||
};
|
};
|
||||||
// Its parent resolved its declared lengths into its box and decided
|
// Its parent resolved its declared lengths into its box and decided
|
||||||
// whether to draw it at all, so a change to either is the parent's
|
// whether to draw it at all, so a change to either is the parent's
|
||||||
@@ -947,14 +984,15 @@ impl UiRenderState {
|
|||||||
at = self.active[&next].parent;
|
at = self.active[&next].parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Both stay marked: the parent because it has this to draw, and
|
||||||
|
// this because the parent must draw it rather than keep what it
|
||||||
|
// has. The mark comes off in `draw_at`, where the parent draws.
|
||||||
rsc.widgets_mut().needs_redraw.insert(id);
|
rsc.widgets_mut().needs_redraw.insert(id);
|
||||||
self.redraw(parent, rsc);
|
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||||
// Whatever the parent did not draw again is nothing it holds now.
|
return false;
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if !active.drawn {
|
if !active.drawn {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
// Nothing above the root resolved its rules or its alignment, so its
|
// Nothing above the root resolved its rules or its alignment, so its
|
||||||
// box is its own to work out again against the output. Every other
|
// box is its own to work out again against the output. Every other
|
||||||
@@ -969,7 +1007,7 @@ impl UiRenderState {
|
|||||||
diag::bump(Counter::LocalRedraws);
|
diag::bump(Counter::LocalRedraws);
|
||||||
let old = self.remove(id, false, rsc);
|
let old = self.remove(id, false, rsc);
|
||||||
self.draw_inner(id, region, info, old, rsc);
|
self.draw_inner(id, region, info, old, rsc);
|
||||||
return;
|
return true;
|
||||||
};
|
};
|
||||||
let (given_px, offered_px) = self.asked_px(id);
|
let (given_px, offered_px) = self.asked_px(id);
|
||||||
// Asked again in the box its parent gave it, which is the question
|
// Asked again in the box its parent gave it, which is the question
|
||||||
@@ -979,9 +1017,8 @@ impl UiRenderState {
|
|||||||
// on is its lengths, so the same lengths elsewhere is one question.
|
// on is its lengths, so the same lengths elsewhere is one question.
|
||||||
if given_px != offered_px {
|
if given_px != offered_px {
|
||||||
rsc.widgets_mut().needs_redraw.insert(id);
|
rsc.widgets_mut().needs_redraw.insert(id);
|
||||||
self.redraw(parent, rsc);
|
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
let info = DrawInfo {
|
let info = DrawInfo {
|
||||||
layer: active.layer,
|
layer: active.layer,
|
||||||
@@ -1014,6 +1051,7 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
rsc.widgets_mut().needs_redraw.insert(parent);
|
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||||
}
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -28,10 +28,14 @@ impl DefaultAppState for State {
|
|||||||
.pad(16)
|
.pad(16)
|
||||||
.background(panel());
|
.background(panel());
|
||||||
|
|
||||||
|
// Each one takes the whole width, because `text_align` puts the
|
||||||
|
// glyphs somewhere in the box the text is given and a text that
|
||||||
|
// reports the width of its own glyphs is given exactly that.
|
||||||
|
let label = |text: &str, align| wtext(text).size(24).text_align(align).width(rel(1.0));
|
||||||
let aligned = (
|
let aligned = (
|
||||||
wtext("left").size(24).text_align(Align::LEFT),
|
label("left", Align::LEFT),
|
||||||
wtext("centred").size(24).text_align(Align::CENTER),
|
label("centred", Align::H_CENTER),
|
||||||
wtext("right").size(24).text_align(Align::RIGHT),
|
label("right", Align::RIGHT),
|
||||||
)
|
)
|
||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.gap(8)
|
.gap(8)
|
||||||
|
|||||||
+1
-1
@@ -251,7 +251,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
ui_state.renderer.draw();
|
ui_state.renderer.draw();
|
||||||
}
|
}
|
||||||
WindowEvent::Resized(size) => {
|
WindowEvent::Resized(size) => {
|
||||||
render.resize((size.width, size.height));
|
render.resize((size.width, size.height), rsc.widgets_mut());
|
||||||
ui_state.renderer.resize(size)
|
ui_state.renderer.resize(size)
|
||||||
}
|
}
|
||||||
WindowEvent::KeyboardInput { event, .. } => {
|
WindowEvent::KeyboardInput { event, .. } => {
|
||||||
|
|||||||
+3
-3
@@ -144,9 +144,9 @@ impl Harness {
|
|||||||
// bound that comes with `SyncSender` is far past anything a test
|
// bound that comes with `SyncSender` is far past anything a test
|
||||||
// leaves unread.
|
// leaves unread.
|
||||||
let (send, updates) = sync_channel(1024);
|
let (send, updates) = sync_channel(1024);
|
||||||
let rsc = DefaultRsc::init(Arc::new(Queue(send)));
|
let mut rsc = DefaultRsc::init(Arc::new(Queue(send)));
|
||||||
let mut render = UiRenderState::new();
|
let mut render = UiRenderState::new();
|
||||||
render.resize(size);
|
render.resize(size, rsc.widgets_mut());
|
||||||
Self {
|
Self {
|
||||||
rsc,
|
rsc,
|
||||||
render,
|
render,
|
||||||
@@ -161,7 +161,7 @@ impl Harness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
self.render.resize(size);
|
self.render.resize(size, self.rsc.widgets_mut());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Changes a length rule after the fact, the way `.width()` sets one.
|
/// Changes a length rule after the fact, the way `.width()` sets one.
|
||||||
|
|||||||
+16
-34
@@ -46,43 +46,26 @@ impl Widget for Span {
|
|||||||
|sum, len| sum + *len,
|
|sum, len| sum + *len,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// What is left for the shares to divide: the box less everything
|
||||||
|
// fixed, as a length of the box rather than a number of pixels.
|
||||||
|
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
|
||||||
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
// Whether anything is left over is a question in pixels: `rel(0.5)`
|
||||||
// beside 300 px is full at 600 and overfull at 400. The room to
|
// beside 300 px is full at 600 and overfull at 400. Asked of `room`
|
||||||
// divide is `len * fixed - total.px`, and the length where it runs
|
// itself, and answered back through the same expression, so the
|
||||||
// out is exactly the box a parent sizing itself from this answer
|
// boundary is the drawing's own and not a second way of finding it:
|
||||||
// hands back -- which is why this used to need a margin either side
|
// the three cases a rounded division needed -- the fixed parts
|
||||||
// of the boundary, and why it does not now: that box and this sum are
|
// growing slower than the box, faster, or exactly with it -- are the
|
||||||
// whole counts of the same step, and both routes to it land on the
|
// sign of `room.rel`, which `through` already reads. What the
|
||||||
// same count. What the generated oracle checks is the consequence,
|
// generated oracle checks is the consequence, since which children
|
||||||
// since which children exist at all turns on this.
|
// exist at all turns on this.
|
||||||
let fixed = Rel::ONE - total.rel;
|
|
||||||
let mut shares = false;
|
let mut shares = false;
|
||||||
if total.leftover > Weight::ZERO {
|
if total.leftover > Weight::ZERO {
|
||||||
let current = painter.px_len(axis);
|
shares = room.to_px(painter.px_len(axis)) > Px::ZERO;
|
||||||
let holds = if fixed > Rel::ZERO {
|
let holds = match shares {
|
||||||
// The box length the fixed parts alone fill.
|
true => Holds::from(Px::STEP..=Px::MAX),
|
||||||
let full = total.px.div(fixed);
|
false => Holds::from(Px::MIN..=Px::ZERO),
|
||||||
shares = current > full;
|
|
||||||
match shares {
|
|
||||||
true => Holds::from(full.next_up()..=Px::MAX),
|
|
||||||
false => Holds::from(Px::MIN..=full),
|
|
||||||
}
|
|
||||||
} else if fixed < Rel::ZERO {
|
|
||||||
// The relative parts grow faster than the box does, so here
|
|
||||||
// a shorter box is the one that leaves room.
|
|
||||||
let full = total.px.div(fixed);
|
|
||||||
shares = current < full;
|
|
||||||
match shares {
|
|
||||||
true => Holds::from(Px::MIN..=full.next_down()),
|
|
||||||
false => Holds::from(full..=Px::MAX),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// The relative parts take exactly the box, whatever it is, so
|
|
||||||
// the only room is what negative pixels leave.
|
|
||||||
shares = total.px < Px::ZERO;
|
|
||||||
Holds::ANY
|
|
||||||
};
|
};
|
||||||
painter.holds(axis, holds);
|
painter.holds(axis, holds.through(room));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Across itself a span is as long as its longest child -- unless a
|
// Across itself a span is as long as its longest child -- unless a
|
||||||
@@ -99,7 +82,6 @@ impl Widget for Span {
|
|||||||
// row.
|
// row.
|
||||||
let mut fixed = Len::rel_min();
|
let mut fixed = Len::rel_min();
|
||||||
let mut taken = Weight::ZERO;
|
let mut taken = Weight::ZERO;
|
||||||
let room = Len::rel_max() - Len::from_parts(total.rel, total.px);
|
|
||||||
let mut start = Len::rel_min();
|
let mut start = Len::rel_min();
|
||||||
let mut ortho = LayoutLen::ZERO;
|
let mut ortho = LayoutLen::ZERO;
|
||||||
for (child, len) in self.children.iter().zip(&lens) {
|
for (child, len) in self.children.iter().zip(&lens) {
|
||||||
|
|||||||
+12
-2
@@ -55,8 +55,13 @@ impl TextView {
|
|||||||
// line up to the one it was made at: each line still fits, and none
|
// line up to the one it was made at: each line still fits, and none
|
||||||
// could take a word that did not fit in the wider box. A line too
|
// could take a word that did not fit in the wider box. A line too
|
||||||
// long to fit at all says nothing about narrower boxes.
|
// long to fit at all says nothing about narrower boxes.
|
||||||
|
//
|
||||||
|
// The step at or above that longest line rather than the nearest
|
||||||
|
// one, since the shaper measures in floats: the nearest step is
|
||||||
|
// under the line half the time, and a range starting there admits a
|
||||||
|
// box the line does not fit in, where the break is not this one.
|
||||||
if let Some(width) = width {
|
if let Some(width) = width {
|
||||||
painter.holds(Axis::X, Px::from_f32(text.size.x).min(width)..=width);
|
painter.holds(Axis::X, Px::ceil_from_f32(text.size.x).min(width)..=width);
|
||||||
}
|
}
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
@@ -78,7 +83,12 @@ impl TextView {
|
|||||||
|
|
||||||
let tex = self.render(painter);
|
let tex = self.render(painter);
|
||||||
let region = tex.size.align(align);
|
let region = tex.size.align(align);
|
||||||
let size = Size::px(tex.size);
|
// The step at or above what the shaper measured, so a parent that
|
||||||
|
// hands back the length this reports hands back a box the longest
|
||||||
|
// line fits in. Rounded to the nearest step it is half the time a
|
||||||
|
// hair under that line, and the break made in it is not the break a
|
||||||
|
// cold layout makes there.
|
||||||
|
let size = Size::from_px(PxVec2::ceil_from_f32(tex.size));
|
||||||
let within = region.within(&painter.region());
|
let within = region.within(&painter.region());
|
||||||
painter.glyphs(tex, within);
|
painter.glyphs(tex, within);
|
||||||
(region, size)
|
(region, size)
|
||||||
|
|||||||
@@ -628,3 +628,112 @@ fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
|
|||||||
h.frame();
|
h.frame();
|
||||||
assert_corners!(h, inner, (100, 0), (400, 200));
|
assert_corners!(h, inner, (100, 0), (400, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The two spans a subtree changes hands between, and the branch that is not
|
||||||
|
/// in the tree yet -- kept alive by the test until it is.
|
||||||
|
struct Handover {
|
||||||
|
leaf: WidgetId,
|
||||||
|
first: WeakWidget<Span>,
|
||||||
|
second: WeakWidget<Span>,
|
||||||
|
root: WeakWidget<Span>,
|
||||||
|
spare: StrongWidget,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A subtree that changes hands while its box does not move, so nothing about
|
||||||
|
/// reusing its drawing says it changed parents. `deeper` puts a span between
|
||||||
|
/// the root and `second`, so it changes depth by changing hands as well.
|
||||||
|
fn plant_handover(h: &mut Harness, moved: bool, deeper: bool, width: f32) -> Handover {
|
||||||
|
let leaf = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let sized = leaf.width(width).add(&mut h.rsc);
|
||||||
|
let holder = (sized,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let first = Span {
|
||||||
|
children: match moved {
|
||||||
|
true => Vec::new(),
|
||||||
|
false => vec![holder.add_strong(&mut h.rsc)],
|
||||||
|
},
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let second = Span {
|
||||||
|
children: match moved {
|
||||||
|
true => vec![holder.add_strong(&mut h.rsc)],
|
||||||
|
false => Vec::new(),
|
||||||
|
},
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let branch = match deeper {
|
||||||
|
true => (second,).span(Dir::RIGHT).add_strong(&mut h.rsc),
|
||||||
|
false => second.add_strong(&mut h.rsc),
|
||||||
|
};
|
||||||
|
let (in_tree, spare) = match moved {
|
||||||
|
true => (branch, first.add_strong(&mut h.rsc)),
|
||||||
|
false => (first.add_strong(&mut h.rsc), branch),
|
||||||
|
};
|
||||||
|
let root = Span {
|
||||||
|
children: vec![in_tree],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||||
|
Handover {
|
||||||
|
leaf: sized.id(),
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
root,
|
||||||
|
spare,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the subtree and swaps the branch it sits in for the one it left.
|
||||||
|
fn hand_over(h: &mut Harness, tree: Handover) -> WidgetId {
|
||||||
|
let holder = h.rsc[tree.first].children.remove(0);
|
||||||
|
h.rsc[tree.second].children.push(holder);
|
||||||
|
h.rsc[tree.root].children.clear();
|
||||||
|
h.rsc[tree.root].children.push(tree.spare);
|
||||||
|
h.frame();
|
||||||
|
tree.leaf
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_subtree_that_changed_parents_is_not_undrawn_by_the_one_it_left() {
|
||||||
|
let mut warm = Harness::new((400, 200));
|
||||||
|
let tree = plant_handover(&mut warm, false, false, 40.0);
|
||||||
|
warm.frame();
|
||||||
|
let leaf = hand_over(&mut warm, tree);
|
||||||
|
|
||||||
|
let mut cold = Harness::new((400, 200));
|
||||||
|
let grown = plant_handover(&mut cold, true, false, 40.0);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
warm.region(&leaf),
|
||||||
|
cold.region(&grown.leaf),
|
||||||
|
"the span it left still listed it and undrew it"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_subtree_that_changed_parents_settles_at_the_depth_it_moved_to() {
|
||||||
|
let mut warm = Harness::new((400, 200));
|
||||||
|
let tree = plant_handover(&mut warm, false, true, 40.0);
|
||||||
|
warm.frame();
|
||||||
|
let leaf = hand_over(&mut warm, tree);
|
||||||
|
// After it has changed hands, so what has to reach the new parent is a
|
||||||
|
// change made under the subtree it now holds.
|
||||||
|
warm.set_len(leaf, Axis::X, LayoutLen::px(90.0));
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((400, 200));
|
||||||
|
let grown = plant_handover(&mut cold, true, true, 90.0);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
warm.region(&leaf),
|
||||||
|
cold.region(&grown.leaf),
|
||||||
|
"the span it moved to is the one the change has to reach"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
//! neither: one box length, composed two ways, landing either side of the
|
//! neither: one box length, composed two ways, landing either side of the
|
||||||
//! boundary that decided whether a child was drawn at all, and two boxes
|
//! boundary that decided whether a child was drawn at all, and two boxes
|
||||||
//! reached through a region node's own entry rather than through the offer
|
//! reached through a region node's own entry rather than through the offer
|
||||||
//! that node was given.
|
//! that node was given. The last is a wrapping text handed back the width
|
||||||
|
//! it measured, rounded to a step below the line it measured there.
|
||||||
|
|
||||||
use iris::harness::Harness;
|
use iris::harness::Harness;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
@@ -554,3 +555,62 @@ fn a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered() {
|
|||||||
}
|
}
|
||||||
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PARAGRAPH: &str = "Wrapping shapes one source into as many lines as the \
|
||||||
|
box leaves room for, so a paragraph's height is an answer and not a setting.";
|
||||||
|
|
||||||
|
/// Eight widgets, shrunk from a 118-widget tree (seed 1121, depth 4,
|
||||||
|
/// `shuffle-swap-for-three`). The stack takes its size from the span above,
|
||||||
|
/// the span takes its width from the longest line of the texts in it, and
|
||||||
|
/// the text below the span is then wrapped at that width -- so a width the
|
||||||
|
/// shaper measured comes back to it as the box to break in.
|
||||||
|
fn plant_a_measured_width(h: &mut Harness, swapped: bool) -> (WeakWidget<Span>, WidgetId) {
|
||||||
|
let first: StrongWidget = rect(Color::YELLOW).add_strong(&mut h.rsc);
|
||||||
|
let mut inner = Span::empty(Dir::UP);
|
||||||
|
inner.children = match swapped {
|
||||||
|
true => swapped_in(h),
|
||||||
|
false => vec![first],
|
||||||
|
};
|
||||||
|
let inner = inner.height(142).add(&mut h.rsc);
|
||||||
|
let text = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let stack = Stack {
|
||||||
|
children: vec![inner.add_strong(&mut h.rsc), text.add_strong(&mut h.rsc)],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root((stack,).span(Dir::DOWN).width(195));
|
||||||
|
(inner, text.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the span holds once its children have been swapped, which is what
|
||||||
|
/// the warm tree is changed to and what the cold one is grown with.
|
||||||
|
fn swapped_in(h: &mut Harness) -> Vec<StrongWidget> {
|
||||||
|
let paragraph = |h: &mut Harness| -> StrongWidget {
|
||||||
|
wtext(PARAGRAPH).size(16).wrap(true).add_strong(&mut h.rsc)
|
||||||
|
};
|
||||||
|
vec![
|
||||||
|
paragraph(h),
|
||||||
|
rect(Color::YELLOW).add_strong(&mut h.rsc),
|
||||||
|
paragraph(h),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text handed back the width it measured breaks there the way it broke
|
||||||
|
/// when it measured it. The width the shaper answers is not on the grid, and
|
||||||
|
/// a report rounded to the nearest step is under the longest line half the
|
||||||
|
/// time: a warm tree then keeps a break made in a wider box while a cold one
|
||||||
|
/// makes a narrower break in the same box, and the paragraph gains a line.
|
||||||
|
#[test]
|
||||||
|
fn a_text_is_given_back_a_box_the_line_it_measured_fits_in() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let (inner, text) = plant_a_measured_width(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
warm.rsc[inner].children = swapped_in(&mut warm);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let (_, cold_text) = plant_a_measured_width(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_eq!(warm.region(&text), cold.region(&cold_text));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user