A ninth sweep, over the part no earlier round named -- the widget vocabulary and the builder methods, `Widgets`, the examples, the `util` additions and the manifests -- and once more over `77ed7a2`, the eighth sweep's own commit and so itself unreviewed. A hint overrode a rule. `declared_lens` asked `rules[axis].declared()` first and fell through to the widget's own `size_hint` whenever that answered `None` -- which it does for a share, since a share is not a declaration. So a widget carrying `width(leftover(1))` and hinting a pixel length of its own was handed a box of the hint, against the rule and against the comment inside the function: "a hint still narrows the box where no rule does". `Painter::size_hint` spells the same rule-else-hint step three hundred lines up and gets it right, with the reason written on it; both read `Widgets::exact_len` now, and `declared_lens` is the part of its answer that needs nobody to divide it. `Image` is the only widget here whose hint is a declared length, and neither the tests nor the generator builds one, so nothing in this repository could reach the difference -- which is why the dump is unchanged and why the test builds a widget of its own. It records the box it was asked in: 400 with the rule and 50 without, and 50 either way before this. Marking a widget for redraw had no name. Twenty-one sites under `tests/` said it as `widgets_mut().get_dyn_mut(id);` with the widget thrown away, five with a `let _ =` in front, one with a comment explaining what the line was for, and one wrapped in a local function called `mark`. `Widgets::mark_for_redraw` says it. `revision_cost.rs` keeps the long spelling and now says why in place: it is deliberately in the API subset an old worktree also has. `assert_same_regions` could not see the defect the eighth sweep had just fixed. It zips the warm and cold id lists, so a list naming one widget twice -- which is what `width`, `sized` and `align` giving back their own argument produces -- compares fewer boxes than it lists and says nothing about it. It now rejects a repeated id and two lists of different lengths, which also checks the nine fixtures that round left alone: all eighteen cases pass. Bare pairs where the framework has named ones. `random.rs`'s `Lens` and `Aligns` were `[Option<LayoutLen>; 2]` and `[Option<AxisAlign>; 2]`, read as `[0]`/`[1]` and zipped against a hand-written `[Axis::X, Axis::Y]`. They are `SizeRules` and `Align`; `Align` took the `Index<Axis>` every other per-axis pair on this branch has, and `RegionAlign::from` does the "an axis left out is centred" step two rigs were spelling per axis. The three sites that wrote the axis pair out say `Axis::BOTH`, which is what the rest of the layout code says. `BothAxis<T>`, `AxisT`, `XAxis` and `YAxis` -- 45 lines with a const trait, two marker types and three accessors -- have no user anywhere in the workspace. They are the mechanism `impl_axis_index!` replaced, in the file this branch took `Vec2::axis`/`axis_mut` out of. Deleted, which is a drive-by in a block the branch was already rewriting; drop it if the scope matters more. Smaller things, each in its own place: `Wrapper` arrived beside core's `WidgetWrapper`, one word for a widget that wraps a child and for a dynamic borrow guard, so the alias is gone and its two uses name `DynBorrower` -- which is what they are. `Wrapper::new`, `Wrapper::empty` and its `Default` were three names for one value, two of them unused. `Arena::get_mut` was the only `pub(crate)` among `pub` siblings on a public type. `Selector` rounded the pointer onto the pixel grid to do arithmetic on two values already there, losing the precision the platform gave it for nothing; the step between the regions is taken on the grid instead. And the two `debug` profile settings carry their reason where the next reader looks rather than only in the commit that made them, one of which was about renaming `rest`. Format, clippy with and without layout-diagnostics, and the suite (132 + 19 + 13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to `77ed7a2` across all 34,488 boxes, and all three seed scans pass: 400 at depth 5 in 63.27s, 1,000 at depth 6 in 160.45s, 2,000 at depth 4 in 302.52s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
164 lines
5.8 KiB
Rust
164 lines
5.8 KiB
Rust
//! Scrolling moves content and stops at its ends.
|
|
|
|
use iris::harness::{Harness, assert_corners};
|
|
use iris::prelude::*;
|
|
|
|
#[test]
|
|
fn scrollable_enables_a_region_node_but_raw_scroll_does_not() {
|
|
let mut h = Harness::new((100, 100));
|
|
let default_child = ().add(&mut h.rsc);
|
|
let _default = default_child.scrollable().add(&mut h.rsc);
|
|
assert!(h.rsc.widgets().is_region_node(default_child));
|
|
h.rsc.widgets_mut().set_region_node(default_child, false);
|
|
assert!(!h.rsc.widgets().is_region_node(default_child));
|
|
|
|
let raw_child = ().add(&mut h.rsc);
|
|
let _raw = Scroll::new(raw_child.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
|
assert!(!h.rsc.widgets().is_region_node(raw_child));
|
|
|
|
let explicit = ().region_node().add(&mut h.rsc);
|
|
assert!(h.rsc.widgets().is_region_node(explicit));
|
|
}
|
|
|
|
#[test]
|
|
fn a_scrollable_child_can_drop_its_region_node() {
|
|
let mut h = Harness::new((400, 200));
|
|
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
|
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
|
let content = (top, bottom).span(Dir::DOWN).add(&mut h.rsc);
|
|
h.set_root(content.scrollable());
|
|
h.rsc.widgets_mut().set_region_node(content, false);
|
|
h.frame();
|
|
|
|
h.move_to((200, 100));
|
|
h.scroll((0, 1));
|
|
h.frame();
|
|
|
|
assert!(!h.rsc.widgets().is_region_node(content));
|
|
assert_corners!(h, top, (0, -150), (400, 50));
|
|
}
|
|
|
|
#[test]
|
|
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
|
|
let mut h = Harness::new((400, 200));
|
|
// Twice the window's height, so there is 200 to scroll.
|
|
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
|
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
|
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
|
|
h.move_to((200, 100));
|
|
|
|
// `Scroll` starts snapped to the end.
|
|
assert_corners!(h, top, (0, -200), (400, 0));
|
|
|
|
// The handler scales a wheel line by 50.
|
|
h.scroll((0, 1));
|
|
h.frame();
|
|
assert_corners!(h, top, (0, -150), (400, 50));
|
|
|
|
h.scroll((0, 10));
|
|
h.frame();
|
|
assert_corners!(h, top, (0, 0), (400, 200));
|
|
}
|
|
|
|
#[test]
|
|
fn fixed_content_and_a_share_fill_one_viewport() {
|
|
let mut h = Harness::new((900, 100));
|
|
let content = rect(Color::RED)
|
|
.width(LayoutLen {
|
|
px: Px::from_int(600),
|
|
rel: Rel::ZERO,
|
|
leftover: Weight::ONE,
|
|
})
|
|
.add(&mut h.rsc);
|
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
|
h.set_root(scroll);
|
|
|
|
assert_corners!(h, content, (0, 0), (900, 100));
|
|
}
|
|
|
|
#[test]
|
|
fn fixed_content_wider_than_the_viewport_still_scrolls() {
|
|
let mut h = Harness::new((900, 100));
|
|
let content = rect(Color::RED).width(1200).add(&mut h.rsc);
|
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
|
h.set_root(scroll);
|
|
|
|
assert_corners!(h, content, (-300, 0), (900, 100));
|
|
}
|
|
|
|
#[test]
|
|
fn a_lone_share_fills_without_scrolling() {
|
|
let mut h = Harness::new((900, 100));
|
|
let content = rect(Color::RED).width(LayoutLen::LEFTOVER).add(&mut h.rsc);
|
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
|
h.set_root(scroll);
|
|
|
|
assert_corners!(h, content, (0, 0), (900, 100));
|
|
}
|
|
|
|
#[test]
|
|
fn wrapping_content_beside_a_fixed_length_is_stable_warm_and_cold() {
|
|
fn plant(h: &mut Harness) -> (WidgetId, WidgetId) {
|
|
let fixed = rect(Color::RED).width(600).add(&mut h.rsc);
|
|
let text = wtext("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.")
|
|
.size(16)
|
|
.wrap(true)
|
|
.width(LayoutLen::LEFTOVER)
|
|
.add(&mut h.rsc);
|
|
let content = (fixed, text).span(Dir::RIGHT).add(&mut h.rsc);
|
|
let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X);
|
|
h.set_root(scroll);
|
|
(text.id(), content.id())
|
|
}
|
|
|
|
let mut warm = Harness::new((900, 300));
|
|
let (text, content) = plant(&mut warm);
|
|
warm.rsc.widgets_mut().mark_for_redraw(text);
|
|
warm.frame();
|
|
|
|
let mut cold = Harness::new((900, 300));
|
|
let (cold_text, cold_content) = plant(&mut cold);
|
|
|
|
assert_eq!(warm.region(&text), cold.region(&cold_text));
|
|
assert_eq!(warm.region(&content), cold.region(&cold_content));
|
|
}
|
|
|
|
/// A widget that clips to its box may not report more than the box: its
|
|
/// parent would place the part it cut off, and the framework would put a
|
|
/// drawing longer than its box somewhere. `Masked` is the second of these
|
|
/// after `Scroll`, and the assertion in `draw_at` is what says so.
|
|
#[test]
|
|
#[should_panic = "clips to"]
|
|
fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
|
|
struct Clipper(StrongWidget);
|
|
|
|
impl Widget for Clipper {
|
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
painter.set_mask(UiRegion::FULL);
|
|
painter.widget(&self.0).size()
|
|
}
|
|
}
|
|
|
|
let mut h = Harness::new((100, 100));
|
|
let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc);
|
|
let clipper = Clipper(tall).add(&mut h.rsc);
|
|
// `set_root` lays the tree out, so this is where it is caught.
|
|
h.set_root(clipper);
|
|
}
|
|
|
|
/// Content that fits sits in the viewport, not in a box of the window's
|
|
/// length anchored at the viewport's start. `Part::From` takes window
|
|
/// lengths, so a `rel(1.0)` span in one is the window, and only a scroll
|
|
/// filling the window would land right.
|
|
#[test]
|
|
fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() {
|
|
let mut h = Harness::new((400, 400));
|
|
let head = rect(Color::RED).height(100).add(&mut h.rsc);
|
|
let inner = rect(Color::BLUE).height(50).add(&mut h.rsc);
|
|
let scroll = Scroll::new(inner.add_strong(&mut h.rsc), Axis::Y).add(&mut h.rsc);
|
|
h.set_root((head, scroll).span(Dir::DOWN));
|
|
|
|
assert_corners!(h, scroll, (0, 100), (400, 400));
|
|
assert_corners!(h, inner, (0, 225), (400, 275));
|
|
}
|