Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
950960cccd | ||
|
|
1c80051d57 | ||
|
|
a92c6acdbf | ||
|
|
c8beca5753 | ||
|
|
4bd8607968 | ||
|
|
ffd79f32d3 | ||
|
|
0e0d4af326 |
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);
|
||||||
|
|||||||
+15
-33
@@ -145,20 +145,11 @@ impl<'a> Painter<'a> {
|
|||||||
/// Draws a widget in a box this widget chose from the widget's own
|
/// Draws a widget in a box this widget chose from the widget's own
|
||||||
/// answer along the `decided` axes. On those the answer is not placed
|
/// answer along the `decided` axes. On those the answer is not placed
|
||||||
/// inside the box again: it already is the box, and a fraction the
|
/// inside the box again: it already is the box, and a fraction the
|
||||||
/// widget reported of its offer, taken of this box a second time, would
|
/// widget reported, taken of this box a second time, would shrink it
|
||||||
/// shrink it twice. A container uses this where it hands back exactly
|
/// twice. A container uses this where it hands back exactly what a child
|
||||||
/// what a child asked for -- a span placing a child at the length it
|
/// asked for -- a span placing a child at the length it reported, a
|
||||||
/// reported, a scroll giving its content the content's own length.
|
/// scroll giving its content the content's own length.
|
||||||
pub fn widget_decided<'s, W: ?Sized>(
|
pub fn widget_at<'s, W: ?Sized>(
|
||||||
&'s mut self,
|
|
||||||
id: &'s StrongWidget<W>,
|
|
||||||
region: UiRegion,
|
|
||||||
decided: [bool; 2],
|
|
||||||
) -> DrawResult<'s, 'a, W> {
|
|
||||||
self.widget_at(id, region, decided)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn widget_at<'s, W: ?Sized>(
|
|
||||||
&'s mut self,
|
&'s mut self,
|
||||||
id: &'s StrongWidget<W>,
|
id: &'s StrongWidget<W>,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
@@ -231,10 +222,18 @@ impl<'a> Painter<'a> {
|
|||||||
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(holds[axis as usize].through(local.axis(axis).len()));
|
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
||||||
}
|
}
|
||||||
|
// The answer as it was given. A fraction in it is a fraction of this
|
||||||
|
// widget's box, which is the same thing a rule beside the child
|
||||||
|
// means and the same thing for every box this widget hands out: a
|
||||||
|
// span offers each child the room left from its cursor, because a
|
||||||
|
// text has to wrap at the width actually there, and `rel(0.5)` is
|
||||||
|
// still half the span. Padding is outside what it pads for the same
|
||||||
|
// reason -- inset the fraction and a child's `rel` would mean the
|
||||||
|
// inner box while its `px` meant the outer one.
|
||||||
DrawResult {
|
DrawResult {
|
||||||
child: id,
|
child: id,
|
||||||
painter: self,
|
painter: self,
|
||||||
size: in_parent_frame(size, local, declared),
|
size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +298,7 @@ impl<'a> Painter<'a> {
|
|||||||
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(holds[axis as usize].through(local.axis(axis).len()));
|
*under = under.and(holds[axis as usize].through(local.axis(axis).len()));
|
||||||
}
|
}
|
||||||
Some(in_parent_frame(size, local, declared).axis(axis))
|
Some(size.axis(axis))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this is the first box a child is asked about in during a draw
|
/// Whether this is the first box a child is asked about in during a draw
|
||||||
@@ -518,23 +517,6 @@ impl PrimitiveLike for &TextureHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A child's answer as lengths of the box it was asked from. A widget reports
|
|
||||||
/// a fraction of the box it was given, and the widget that gave it wants the
|
|
||||||
/// same length as a fraction of its own: one composition apart wherever the
|
|
||||||
/// offer was not the whole of the parent's extent, as a span's is after a
|
|
||||||
/// relative child. A declared axis is already the parent's: it resolved the
|
|
||||||
/// rule in its own box, and the rule is what the report says.
|
|
||||||
fn in_parent_frame(size: Size, local: UiRegion, declared: [Option<LayoutLen>; 2]) -> Size {
|
|
||||||
let mut size = size;
|
|
||||||
for (axis, declared) in AXES.into_iter().zip(declared) {
|
|
||||||
if declared.is_none() {
|
|
||||||
let len = local.axis(axis).len();
|
|
||||||
*size.axis_mut(axis) = size.axis(axis).within_len(len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
size
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What a widget declares a length of its box to be. `leftover` is not one: a
|
/// What a widget declares a length of its box to be. `leftover` is not one: a
|
||||||
/// share of what is left over is only a length to the widget dividing one,
|
/// share of what is left over is only a length to the widget dividing one,
|
||||||
/// so it passes up in the size instead.
|
/// so it passes up in the size instead.
|
||||||
|
|||||||
+63
-41
@@ -32,7 +32,7 @@ pub(super) struct DrawInfo {
|
|||||||
pub offered_px: PxVec2,
|
pub offered_px: PxVec2,
|
||||||
/// The axes along which the parent chose this box from the widget's own
|
/// The axes along which the parent chose this box from the widget's own
|
||||||
/// answer, so the answer is not placed inside it again. See
|
/// answer, so the answer is not placed inside it again. See
|
||||||
/// [`Painter::widget_decided`].
|
/// [`Painter::widget_at`].
|
||||||
pub decided: [bool; 2],
|
pub decided: [bool; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +55,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 +71,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,
|
||||||
}
|
}
|
||||||
@@ -192,14 +196,19 @@ 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);
|
||||||
let replace_answer = self.answer_invalid.remove(&id)
|
// Nothing this widget has is an answer while something it measured
|
||||||
|| (self.replace_answers
|
// is dirty: settling that changes what it would report, and a widget
|
||||||
&& (rsc.widgets().needs_redraw.contains(&id)
|
// settled inside its parent's draw tells nobody -- the comparison
|
||||||
|| self.dirty_size_under(id, rsc.widgets())));
|
// that marks a reader is in `redraw`, which is not what asked here.
|
||||||
let retained = match replace_answer {
|
// 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 retained = match replace_answer || stale {
|
||||||
true => None,
|
true => None,
|
||||||
false => self
|
false => self
|
||||||
.retained_answer(id, info, rsc.widgets())
|
.retained_answer(id, info)
|
||||||
.or_else(|| self.try_reuse(id, region, info, rsc)),
|
.or_else(|| self.try_reuse(id, region, info, rsc)),
|
||||||
};
|
};
|
||||||
let answer = retained.unwrap_or_else(|| {
|
let answer = retained.unwrap_or_else(|| {
|
||||||
@@ -489,16 +498,9 @@ impl UiRenderState {
|
|||||||
|
|
||||||
/// The answer to an ask can be retained independently of where its
|
/// 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
|
/// drawing ended up. Alignment is exactly that case: the first box is the
|
||||||
/// question and the smaller placed box holds the drawing.
|
/// question and the smaller placed box holds the drawing. Whether the
|
||||||
fn retained_answer(
|
/// answer is stale at all is its caller's question, asked once there.
|
||||||
&self,
|
fn retained_answer(&self, id: WidgetId, info: DrawInfo) -> Option<(Size, [Holds; 2])> {
|
||||||
id: WidgetId,
|
|
||||||
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 active = self.active.get(&id)?;
|
||||||
let has_region_node = active.move_idx != active.parent_move;
|
let has_region_node = active.move_idx != active.parent_move;
|
||||||
if !active.drawn
|
if !active.drawn
|
||||||
@@ -512,9 +514,11 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
/// dirty. Not needed for the answer to come right -- a changed size
|
/// dirty, which makes what it would answer not yet known. It also keeps
|
||||||
/// reaches its reader in any order -- but a reader that asks first
|
/// a reader that asks first from laying out twice, which is all it was
|
||||||
/// lays out once rather than twice.
|
/// 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 {
|
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
||||||
self.active.get(&id).is_some_and(|active| {
|
self.active.get(&id).is_some_and(|active| {
|
||||||
active.size_deps.iter().any(|child| {
|
active.size_deps.iter().any(|child| {
|
||||||
@@ -827,18 +831,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 +942,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 +968,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 +991,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 +1001,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 +1035,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)
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ impl Widget for Pad {
|
|||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
// The inner's own alignment, not the near edge. This reports the
|
// The inner's own alignment, not the near edge. This reports the
|
||||||
// inner's size plus the padding, so where the box is that answer the
|
// inner's size plus the padding, so where the box is that answer the
|
||||||
// inset box is exactly the inner and alignment has no room to move
|
// inner is exactly what it asked for and alignment has no room to
|
||||||
// it; where the box is bigger -- a share of a row, a rule over this
|
// move it; where the box is bigger -- a share of a row, a rule over
|
||||||
// widget -- the slack is the inner's to sit in, and forcing the near
|
// this widget -- the slack is the inner's to sit in, and forcing the
|
||||||
// edge pinned it to a corner it had not asked for.
|
// near edge pinned it to a corner it had not asked for.
|
||||||
let inner = painter
|
let inner = painter
|
||||||
.widget_within(&self.inner, self.padding.region())
|
.widget_within(&self.inner, self.padding.region())
|
||||||
.size();
|
.size();
|
||||||
@@ -29,6 +29,45 @@ impl Widget for Pad {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Room taken off the inside rather than added round the outside: the child
|
||||||
|
/// draws in what is left once both edges are gone, and this widget is
|
||||||
|
/// exactly as long as the box it was given.
|
||||||
|
///
|
||||||
|
/// So `rel(1.0)` under an [`Inset`] is the room inside it, where the same
|
||||||
|
/// rule under a [`Pad`] is the pad's whole box and overflows it by the
|
||||||
|
/// padding. Both are wanted; which one a layout means is which widget it
|
||||||
|
/// reaches for.
|
||||||
|
pub struct Inset {
|
||||||
|
pub padding: Padding,
|
||||||
|
pub inner: StrongWidget,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Inset {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
let region = self.padding.inset_region();
|
||||||
|
let inner = painter.widget_within(&self.inner, region).size();
|
||||||
|
// What a fraction the child reported is a fraction of is this
|
||||||
|
// widget's to say, and it says the room inside: the child asked for
|
||||||
|
// a part of the box it drew in, and that box is shorter than this
|
||||||
|
// one by both edges. Then the edges go back on, so this widget is
|
||||||
|
// its child and the room taken off around it.
|
||||||
|
let (x, y) = (
|
||||||
|
inner.x.within_len(region.x.len()),
|
||||||
|
inner.y.within_len(region.y.len()),
|
||||||
|
);
|
||||||
|
Size {
|
||||||
|
x: LayoutLen {
|
||||||
|
px: x.px + self.padding.left + self.padding.right,
|
||||||
|
..x
|
||||||
|
},
|
||||||
|
y: LayoutLen {
|
||||||
|
px: y.px + self.padding.top + self.padding.bottom,
|
||||||
|
..y
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Padding {
|
pub struct Padding {
|
||||||
pub left: Px,
|
pub left: Px,
|
||||||
pub right: Px,
|
pub right: Px,
|
||||||
@@ -53,7 +92,24 @@ impl Padding {
|
|||||||
bottom: amt,
|
bottom: amt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// The box a [`Pad`] gives its child: as long as the pad's own, moved in
|
||||||
|
/// by the near edge. Padding is outside what it pads, so a fraction the
|
||||||
|
/// child asks for is a fraction of the same length whether a rule beside
|
||||||
|
/// it states one or it reports one, and its pixels are the same pixels.
|
||||||
|
/// Shrinking the box instead would make `rel` mean the inner box while
|
||||||
|
/// `px` meant the outer one. [`Inset`] is the widget that shrinks.
|
||||||
pub fn region(&self) -> UiRegion {
|
pub fn region(&self) -> UiRegion {
|
||||||
|
let mut region = UiRegion::FULL;
|
||||||
|
region.x.start.px += self.left;
|
||||||
|
region.y.start.px += self.top;
|
||||||
|
region.x.end.px += self.left;
|
||||||
|
region.y.end.px += self.top;
|
||||||
|
region
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The box an [`Inset`] gives its child: shorter than its own by both
|
||||||
|
/// edges, so what the child fills is the room left inside.
|
||||||
|
pub fn inset_region(&self) -> UiRegion {
|
||||||
let mut region = UiRegion::FULL;
|
let mut region = UiRegion::FULL;
|
||||||
region.x.start.px += self.left;
|
region.x.start.px += self.left;
|
||||||
region.y.start.px += self.top;
|
region.y.start.px += self.top;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ impl Widget for Scroll {
|
|||||||
region = region.offset(offset);
|
region = region.offset(offset);
|
||||||
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_decided(&self.inner, region, [true; 2]);
|
painter.widget_at(&self.inner, region, [true; 2]);
|
||||||
// What it occupies is its box, on both axes: it clips its content to
|
// 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
|
// 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
|
// more. The content's length is what it scrolls through, not what it
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ impl Widget for Span {
|
|||||||
span.flip();
|
span.flip();
|
||||||
}
|
}
|
||||||
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||||
|
// Offered the room left from the cursor, because a text has to
|
||||||
|
// wrap at the width actually there, while what it reports is a
|
||||||
|
// fraction of the whole row: `rel(0.5)` is half the span
|
||||||
|
// whatever else is in it and wherever this child sits.
|
||||||
let len = match painter.known_len(child, axis, region) {
|
let len = match painter.known_len(child, axis, region) {
|
||||||
Some(len) => len,
|
Some(len) => len,
|
||||||
None => painter.widget_within(child, region).len(axis),
|
None => painter.widget_at(child, region, [false; 2]).len(axis),
|
||||||
};
|
};
|
||||||
cursor.px += len.px + self.gap;
|
cursor.px += len.px + self.gap;
|
||||||
cursor.rel += len.rel;
|
cursor.rel += len.rel;
|
||||||
@@ -122,7 +126,7 @@ impl Widget for Span {
|
|||||||
// Along the row this box is the child's own answer, so the answer
|
// Along the row this box is the child's own answer, so the answer
|
||||||
// is not placed in it again; across it the child sits where its
|
// is not placed in it again; across it the child sits where its
|
||||||
// alignment says.
|
// alignment says.
|
||||||
let placed = painter.widget_decided(child, region, [axis == Axis::X, axis == Axis::Y]);
|
let placed = painter.widget_at(child, region, [axis == Axis::X, axis == Axis::Y]);
|
||||||
if shrinks {
|
if shrinks {
|
||||||
let used = placed.len(!axis);
|
let used = placed.len(!axis);
|
||||||
// Choosing between a fixed and a relative length from the
|
// Choosing between a fixed and a relative length from the
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ impl Widget for Stack {
|
|||||||
// child is handed a box that owes nothing to its own answer, and
|
// child is handed a box that owes nothing to its own answer, and
|
||||||
// where it sits in one bigger than itself is its own business.
|
// where it sits in one bigger than itself is its own business.
|
||||||
match sizing == Some(i) {
|
match sizing == Some(i) {
|
||||||
true => painter.widget_decided(child, region, [true; 2]),
|
true => painter.widget_at(child, region, [true; 2]),
|
||||||
false => painter.widget_within(child, region),
|
false => painter.widget_within(child, region),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+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)
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ widget_trait! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inset(self, padding: impl Into<Padding>) -> impl WidgetFn<Rsc, Inset> {
|
||||||
|
// Room taken off the inside, where `pad` adds it round the outside:
|
||||||
|
// this is as long as the box it is given and the child fills what is
|
||||||
|
// left of it.
|
||||||
|
|state| Inset {
|
||||||
|
padding: padding.into(),
|
||||||
|
inner: self.add_strong(state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn align(self, align: impl Into<Align>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
fn align(self, align: impl Into<Align>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||||
// An axis left out keeps whatever it had, which is centered unless
|
// An axis left out keeps whatever it had, which is centered unless
|
||||||
// something else set it.
|
// something else set it.
|
||||||
|
|||||||
+76
-19
@@ -20,13 +20,13 @@ fn a_span_gives_each_child_the_width_it_asked_for() {
|
|||||||
assert_corners!(h, right, (100, 0), (400, 200));
|
assert_corners!(h, right, (100, 0), (400, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A drawn child reports a fraction of the box it was given, and a span
|
/// A span offers each child the room left after the one before, because a
|
||||||
/// offers each child what is left after the one before. So a nested span
|
/// text has to wrap at the width actually there, but reads what the child
|
||||||
/// that takes half of the half it was offered has taken a quarter of the row,
|
/// reports as a fraction of the whole row. So two children asking for half
|
||||||
/// and what follows starts three quarters along -- not at the end, which is
|
/// each take the whole row between them, however much of it was left when
|
||||||
/// where adding its report straight into the cursor put it.
|
/// each was asked, and a third overflows.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_span_reads_a_child_report_as_a_fraction_of_what_it_offered() {
|
fn a_span_reads_a_child_report_as_a_fraction_of_the_row() {
|
||||||
let mut h = Harness::new((400, 100));
|
let mut h = Harness::new((400, 100));
|
||||||
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
|
let half = rect(Color::RED).width(rel(0.5)).add(&mut h.rsc);
|
||||||
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||||
@@ -35,18 +35,60 @@ fn a_span_reads_a_child_report_as_a_fraction_of_what_it_offered() {
|
|||||||
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
|
h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
|
||||||
// The nested span is placed at the length it reported and drawn there
|
// The nested span is placed at the length it reported and drawn there
|
||||||
// once more; half of that final box is what its child takes, packed at
|
// once more; half of that final box is what its own child takes.
|
||||||
// the nested span's own start.
|
assert_corners!(h, nested, (200, 0), (400, 100));
|
||||||
assert_corners!(h, nested, (200, 0), (300, 100));
|
assert_corners!(h, inner, (200, 0), (300, 100));
|
||||||
assert_corners!(h, inner, (200, 0), (250, 100));
|
assert_corners!(h, tail, (400, 0), (500, 100));
|
||||||
assert_corners!(h, tail, (300, 0), (400, 100));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The same reading through a pad: its inset is the whole box less the
|
/// The same fraction either way round: after a 100 px child in a 400 px row,
|
||||||
/// padding, so half of the inset plus the padding is half the box plus one
|
/// `rel(0.5)` is 100 to 300 whether the child's own rule says so or the child
|
||||||
/// padding, not two.
|
/// drew half of what it was offered and reported that. Half the row, not half
|
||||||
|
/// of the 300 px left of it.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
|
fn a_reported_fraction_is_of_the_row_like_a_declared_one() {
|
||||||
|
let mut declaring = Harness::new((400, 100));
|
||||||
|
let head = rect(Color::RED).width(100).add(&mut declaring.rsc);
|
||||||
|
let declared = rect(Color::GREEN).width(rel(0.5)).add(&mut declaring.rsc);
|
||||||
|
declaring.set_root((head, declared).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
assert_corners!(declaring, declared, (100, 0), (300, 100));
|
||||||
|
|
||||||
|
let mut reporting = Harness::new((400, 100));
|
||||||
|
let head = rect(Color::RED).width(100).add(&mut reporting.rsc);
|
||||||
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut reporting.rsc);
|
||||||
|
let reported = (inner,).span(Dir::RIGHT).add(&mut reporting.rsc);
|
||||||
|
reporting.set_root((head, reported).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
assert_corners!(reporting, reported, (100, 0), (300, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the fraction a child reports is of and what box it is offered are
|
||||||
|
/// two different lengths, and only the first is the whole row: a text still
|
||||||
|
/// wraps at the room actually left after its neighbour, so the same
|
||||||
|
/// paragraph is taller where less of the row is left for it.
|
||||||
|
#[test]
|
||||||
|
fn a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row() {
|
||||||
|
let paragraph = "Wrapping shapes one source into as many lines as the box \
|
||||||
|
leaves room for, so a paragraph's height is an answer.";
|
||||||
|
let height_after = |head_width: i32| {
|
||||||
|
let mut h = Harness::new((400, 400));
|
||||||
|
let head = rect(Color::RED).width(head_width).add(&mut h.rsc);
|
||||||
|
let text = wtext(paragraph).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
h.set_root((head, text).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
let region = h.region(&text).unwrap();
|
||||||
|
(region.bot_right.y - region.top_left.y).to_f32()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (crowded, whole_row) = (height_after(300), height_after(0));
|
||||||
|
assert!(crowded > whole_row, "{crowded} against {whole_row}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Padding is outside what it pads, so a fraction under one is a fraction of
|
||||||
|
/// the box the padding is measured from: half of a 400 px row is 200, and
|
||||||
|
/// the pad is that plus both edges. Inset it instead and `rel` would mean the
|
||||||
|
/// inner box while `px` meant the outer one, which is the one thing a length
|
||||||
|
/// may not do.
|
||||||
|
#[test]
|
||||||
|
fn a_pad_is_outside_the_fraction_its_child_asked_for() {
|
||||||
let mut h = Harness::new((400, 100));
|
let mut h = Harness::new((400, 100));
|
||||||
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||||
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
|
let padded = (inner,).span(Dir::RIGHT).pad(10).add(&mut h.rsc);
|
||||||
@@ -55,8 +97,23 @@ fn a_pad_reports_a_fraction_of_its_inset_as_a_fraction_of_its_box() {
|
|||||||
// placed inside it by its own alignment, which is not what is under test.
|
// placed inside it by its own alignment, which is not what is under test.
|
||||||
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
|
h.set_root((padded, tail).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
|
||||||
assert_corners!(h, padded, (0, 0), (210, 100));
|
assert_corners!(h, padded, (0, 0), (220, 100));
|
||||||
assert_corners!(h, tail, (210, 0), (310, 100));
|
assert_corners!(h, tail, (220, 0), (320, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The other half of the pair: an inset takes its room off the inside, so it
|
||||||
|
/// is exactly as long as the box it was given and the fraction its child
|
||||||
|
/// asked for is a fraction of what is left inside. Half of the 380 left in a
|
||||||
|
/// 400 px row is 190, and the inset is the whole 400.
|
||||||
|
#[test]
|
||||||
|
fn an_inset_is_inside_the_fraction_its_child_asked_for() {
|
||||||
|
let mut h = Harness::new((400, 100));
|
||||||
|
let inner = rect(Color::GREEN).width(rel(0.5)).add(&mut h.rsc);
|
||||||
|
let inset = (inner,).span(Dir::RIGHT).inset(10).add(&mut h.rsc);
|
||||||
|
h.set_root((inset,).span(Dir::RIGHT).width(rel(1.0)));
|
||||||
|
|
||||||
|
assert_corners!(h, inset, (0, 0), (200, 100));
|
||||||
|
assert_corners!(h, inner, (10, 0), (200, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -197,7 +254,7 @@ fn a_moved_subtree_takes_its_children_with_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);
|
||||||
let row = inner.pad(10).height(40).region_node().add(&mut h.rsc);
|
let row = inner.inset(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
|
// 80 of fixed rows in a 400 window, so the span takes 80 and sits in the
|
||||||
// middle of what it was given.
|
// middle of what it was given.
|
||||||
h.set_root((first, row).span(Dir::DOWN));
|
h.set_root((first, row).span(Dir::DOWN));
|
||||||
@@ -240,7 +297,7 @@ fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
|
|||||||
// impossible to take out of: recovering a fraction of a box needs a
|
// impossible to take out of: recovering a fraction of a box needs a
|
||||||
// relative extent, and it has none on that axis.
|
// relative extent, and it has none on that axis.
|
||||||
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).add(&mut h.rsc);
|
let row = inner.inset(10).height(40).add(&mut h.rsc);
|
||||||
let filler = rect(Color::GREEN).add(&mut h.rsc);
|
let filler = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
// This column is an item in a row, so it takes the width left for it
|
// This column is an item in a row, so it takes the width left for it
|
||||||
// rather than asking for a full row-width in addition to the bar.
|
// rather than asking for a full row-width in addition to the bar.
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
|
|||||||
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true);
|
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), true);
|
||||||
let padded = leaf.pad(10).add(&mut h.rsc);
|
let padded = leaf.pad(10).add(&mut h.rsc);
|
||||||
let below = rect(Color::RED).add(&mut h.rsc);
|
let below = rect(Color::RED).add(&mut h.rsc);
|
||||||
h.set_root((padded, below).span(Dir::DOWN).pad(12));
|
h.set_root((padded, below).span(Dir::DOWN).inset(12));
|
||||||
assert_corners!(h, below, (12, 132), (388, 388));
|
assert_corners!(h, below, (12, 132), (388, 388));
|
||||||
|
|
||||||
h.rsc[leaf].size = Size::px((100, 200).into());
|
h.rsc[leaf].size = Size::px((100, 200).into());
|
||||||
|
|||||||
@@ -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