Files
iris/tests/cases/scroll.rs
T
iris-ai 97fca76108 Answer "have I asked this child?" in one read
A container's draw asked it once per child by searching the list of children
it had added so far, and four other per-child steps searched a list too, so
one draw cost the square of its children: 70% of a 1,600-child redraw was
those searches. A draw takes a DrawId and leaves it on every widget it asks
about; one note per widget is enough because the handle a container holds a
child by cannot be cloned. tests/children_cost.rs is the rig that shows it,
and it is the only one here that varies width: 3.680 ms to 0.811 ms at 1,600
children, and flat per child.

Beside it, the rest of the fourteenth sweep of #19: a mask's rectangle
resolved once per fragment instead of once per instance, which takes the
storage buffers out of the fragment stage and is 8.8x on a screenful of
deeply nested clips; TextBuffer::shape copying its attrs before the check
that would not need them, which allocated once per named-family text per
frame; a should_panic test on a debug assertion that made cargo test
--release fail; Fixed::div, reached only by its own test; Moves::remove
re-uploading an array it cannot have changed; and two comments the branch
itself falsified.

docs/LAYOUT_LOG.md has all eight with their measurements, the five things
looked at and left, and what was verified.
2026-09-20 23:56:12 -04:00

192 lines
7.0 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.
// What it checks is a debug assertion, which a release build does not compile
// -- and a `should_panic` test of one fails there rather than passing
// vacuously, so it is not built either. Every measurement rig here is run in
// release, so `cargo test --release` has to pass.
#[cfg(debug_assertions)]
#[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));
}
/// A cap narrows the box the widget is asked in, which is what a scroll
/// measures its viewport from: the content scrolls within the cap rather than
/// within the room the cap was cut from.
#[test]
fn a_capped_scroll_takes_its_viewport_from_the_cap() {
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 scroll = (top, bottom).span(Dir::DOWN).scrollable().add(&mut h.rsc);
let capped = scroll.max_height(100).add(&mut h.rsc);
h.set_root(capped);
h.move_to((200, 50));
// 400 of content in a viewport of 100, so 300 to scroll and the end
// showing: the top is 300 above the box, which the window centres.
assert_eq!(h.region(&scroll).unwrap().size().y, Px::from_int(100));
assert_corners!(h, top, (0, -250), (400, -50));
h.scroll((0, 1));
h.frame();
assert_corners!(h, top, (0, -200), (400, 0));
}