An eighth sweep, over the part no earlier round named: the 6,300 lines of
tests, and once more over the seventh sweep's own commit, which was itself
unreviewed.
Four of the shrunk fuzz fixtures name one widget two or three times. `width`,
`sized` and `align` set a rule on the widget they are given and return its own
id -- only `pad` and `wrapper` make a new one -- so `let sized =
wrapped.width(76).add(..)` and the `let aligned = sized` beside it are three
names for one text. Each name then went into the list of ids the case compares
warm against cold, so a case that says it checks six boxes checks four, and
three doc comments quote that inflated count as the size of the tree the
shrinker reduced to. Measured: `plant` and `plant_fixed` list 6 and hold 4,
`plant_pair` lists 4 and holds 3, `plant_scrolled` lists 8 and holds 7. The
aliases are gone and the counts say what the fixtures build; each rebuilt
fixture was diffed against the old one, and both the widget slots and every
region are identical, for both settings of `swapped`.
`assert_same_regions` sits at the top of `unsettled.rs` and six tests call it.
Seven more spell its body out instead, byte for byte. They call it now, and it
is `#[track_caller]` so the panic names the case.
`tests/gpu/mod.rs` holds the adapter probe and the surface configuration that
`draw_cost` and `chain_cost` had a copy of each -- `config` identical, and the
probe identical but for the feature it asks for. The leak's justification lived
in one file with the other referring to it; it now sits on the thing it is
about. Shared through `#[path]`, the way `scenario/mod.rs` already is.
The mask a widget is clipped by was resolved in three places, two of them a
byte-identical closure. `mask_bounds` takes the slot rather than the widget,
because the third site deliberately reads the slot it saved before the frame:
that a redraw keeps the slot is what it is checking.
`Layered::_revision` was a field nothing reads, incremented to mark the widget
dirty. Two tests in the same file already do that with
`get_dyn_mut`, which is what the underscore was hiding.
`plan.rs` claimed every simplification is strictly smaller, and asserted `<=`.
Measured: 53 of one tree's 101 simplifications keep the widget count, since a
dropped alignment and a simpler leaf both do. The assertion is right and the
claim was not; the comment now gives the argument that does hold.
`generated.rs` said "Seven that have never failed" and "the nine the others
check" of a ten-seed array. The `should_panic` scroll test ended in an
`h.frame()` that cannot run, since `set_root` lays out and is where the panic
comes from. Two `drop(tree)` at the end of their own scope did nothing.
Format, clippy with and without layout-diagnostics, and the suite (131 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
f8aa0c5 across all 34,490 boxes. No library code changed, so the seed scans
have nothing to find. Both GPU rigs were rebuilt and run: chain cost +470% at
depth 64, draw cost ~4.4 us per layer.
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().get_dyn_mut(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));
|
|
}
|