diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 02400c9..6d19417 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -139,29 +139,30 @@ impl<'a> Painter<'a> { id: &'s StrongWidget, region: UiRegion, ) -> DrawResult<'s, 'a, W> { - self.widget_at(id, region, [false; 2]) + self.widget_at(id, region, region.size(), [false; 2]) } - /// 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 - /// 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 - /// shrink it twice. A container uses this where it hands back exactly + /// Draws a widget in `region`, saying what the answer means. + /// + /// `reports_of` is what a fraction the child reports is a fraction of, as + /// lengths of this widget's own box. It is the box the child was given + /// wherever that box is the child's whole area -- a pad's inset, a stack + /// child, a scroll's content -- and a span passes its own extent along + /// the row instead: it offers each child the room left from its cursor, + /// because a text has to wrap at the width actually there, while + /// `rel(0.5)` still means half the span wherever the child sits in it. + /// + /// A `decided` axis is one where this box was chosen from the widget's + /// own answer. On those the answer is not placed inside the box again: it + /// already is the box, and a fraction taken of it a second time would + /// shrink it twice. A container uses that where it hands back exactly /// what a child asked for -- a span placing a child at the length it /// reported, a scroll giving its content the content's own length. - pub fn widget_decided<'s, W: ?Sized>( - &'s mut self, - id: &'s StrongWidget, - region: UiRegion, - decided: [bool; 2], - ) -> DrawResult<'s, 'a, W> { - self.widget_at(id, region, decided) - } - - fn widget_at<'s, W: ?Sized>( + pub fn widget_at<'s, W: ?Sized>( &'s mut self, id: &'s StrongWidget, region: UiRegion, + reports_of: UiVec2, decided: [bool; 2], ) -> DrawResult<'s, 'a, W> { let region_node = self.rsc.widgets().is_region_node(id.id()); @@ -234,7 +235,7 @@ impl<'a> Painter<'a> { DrawResult { child: id, painter: self, - size: in_parent_frame(size, local, declared), + size: in_parent_frame(size, reports_of, declared), } } @@ -268,12 +269,14 @@ impl<'a> Painter<'a> { /// A child's length in the box it is about to be offered, if it can be /// had without drawing it: from its hint, or from a drawing it already - /// has that holds for that box. + /// has that holds for that box. `reports_of` is what a fraction in the + /// answer is a fraction of, as it is for [`Self::widget_at`]. pub fn known_len( &mut self, child: &StrongWidget, axis: Axis, region: UiRegion, + reports_of: UiVec2, ) -> Option { let declared = self.declared_lens(child); let align = self.rsc.widgets().alignment(child.id()); @@ -299,7 +302,7 @@ impl<'a> Painter<'a> { for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { *under = under.and(holds[axis as usize].through(local.axis(axis).len())); } - Some(in_parent_frame(size, local, declared).axis(axis)) + Some(in_parent_frame(size, reports_of, declared).axis(axis)) } /// Whether this is the first box a child is asked about in during a draw @@ -518,18 +521,19 @@ 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; 2]) -> Size { +/// A child's answer as lengths of the parent's own box. A widget reports a +/// fraction, and `reports_of` is the length that fraction is of: the box the +/// child was given wherever that is the child's whole area, and the parent's +/// own extent wherever the box is a positional remainder, as a span's is +/// after an earlier child. Pixels come through untouched either way, being +/// that many pixels wherever they end up. 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, reports_of: UiVec2, declared: [Option; 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.axis_mut(axis) = size.axis(axis).within_len(reports_of.axis(axis)); } } size diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index c23956b..48e2aea 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -32,7 +32,7 @@ pub(super) struct DrawInfo { pub offered_px: PxVec2, /// 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 - /// [`Painter::widget_decided`]. + /// [`Painter::widget_at`]. pub decided: [bool; 2], } diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index ff2baaa..199ff70 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -14,7 +14,8 @@ impl Widget for Scroll { let container_len = painter.px_len(self.axis); // Draw in the whole container only when its scrolling-axis length is // not already known, then draw it at the scrolled offset. - let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) { + let whole = UiRegion::FULL; + let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) { Some(len) => len, None => painter.widget(&self.inner).size().axis(self.axis), }; @@ -63,7 +64,7 @@ impl Widget for Scroll { region = region.offset(offset); 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, region.size(), [true; 2]); // What it occupies is its box, on both axes: it clips its content to // that box, so it can neither take less of one nor honestly ask for // more. The content's length is what it scrolls through, not what it diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 0375470..f2d338e 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -20,9 +20,15 @@ impl Widget for Span { span.flip(); } let region = UiRegion::from_axis(axis, span, UiSpan::FULL); - let len = match painter.known_len(child, axis, region) { + // Offered the room left from the cursor, because a text has to + // wrap at the width actually there, but reporting a fraction of + // the whole row: `rel(0.5)` is half the span whatever else is in + // it and wherever this child sits among them. + let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) { Some(len) => len, - None => painter.widget_within(child, region).len(axis), + None => painter + .widget_at(child, region, UiVec2::FULL_SIZE, [false; 2]) + .len(axis), }; cursor.px += len.px + self.gap; cursor.rel += len.rel; @@ -122,7 +128,12 @@ impl Widget for Span { // 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 // alignment says. - let placed = painter.widget_decided(child, region, [axis == Axis::X, axis == Axis::Y]); + let placed = painter.widget_at( + child, + region, + UiVec2::FULL_SIZE, + [axis == Axis::X, axis == Axis::Y], + ); if shrinks { let used = placed.len(!axis); // Choosing between a fixed and a relative length from the diff --git a/src/widget/position/stack.rs b/src/widget/position/stack.rs index 723ebdc..2fd06e5 100644 --- a/src/widget/position/stack.rs +++ b/src/widget/position/stack.rs @@ -35,7 +35,7 @@ impl Widget for Stack { // 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. match sizing == Some(i) { - true => painter.widget_decided(child, region, [true; 2]), + true => painter.widget_at(child, region, region.size(), [true; 2]), false => painter.widget_within(child, region), }; } diff --git a/tests/cases/layout.rs b/tests/cases/layout.rs index 8af6d78..acd7e31 100644 --- a/tests/cases/layout.rs +++ b/tests/cases/layout.rs @@ -20,13 +20,13 @@ fn a_span_gives_each_child_the_width_it_asked_for() { assert_corners!(h, right, (100, 0), (400, 200)); } -/// A drawn child reports a fraction of the box it was given, and a span -/// offers each child what is left after the one before. So a nested span -/// that takes half of the half it was offered has taken a quarter of the row, -/// and what follows starts three quarters along -- not at the end, which is -/// where adding its report straight into the cursor put it. +/// A span offers each child the room left after the one before, because a +/// text has to wrap at the width actually there, but reads what the child +/// reports as a fraction of the whole row. So two children asking for half +/// each take the whole row between them, however much of it was left when +/// each was asked, and a third overflows. #[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 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); @@ -35,11 +35,51 @@ 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))); // 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 - // the nested span's own start. - assert_corners!(h, nested, (200, 0), (300, 100)); - assert_corners!(h, inner, (200, 0), (250, 100)); - assert_corners!(h, tail, (300, 0), (400, 100)); + // once more; half of that final box is what its own child takes. + assert_corners!(h, nested, (200, 0), (400, 100)); + assert_corners!(h, inner, (200, 0), (300, 100)); + assert_corners!(h, tail, (400, 0), (500, 100)); +} + +/// The same fraction either way round: after a 100 px child in a 400 px row, +/// `rel(0.5)` is 100 to 300 whether the child's own rule says so or the child +/// drew half of what it was offered and reported that. Half the row, not half +/// of the 300 px left of it. +#[test] +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}"); } /// The same reading through a pad: its inset is the whole box less the