//! RUST.md's I3: `iris::widget::LazySpan` with 800 rows of varied-length //! wrapped text, one in twelve carrying a small image, scrollable with the //! mouse wheel. Run headless with `iris/run-headless.sh message_list --shot //! /tmp/message_list.png` -- there is no display on this machine, so that //! is the only way to see it rendered; `scripts/run-tests.sh`/`cargo test` never //! touch this file. //! //! Rows alternate two background tints so a screenshot can show the //! boundary between adjacent rows even where the text itself wraps to a //! different number of lines -- exactly the "variable-height rows" I3 //! asks for, and the thing a virtualised list gets wrong first if it is //! wrong at all (a gap, an overlap, a row the wrong colour). This example //! is also what found `LazySpan::place`'s oversized-background bug (see //! lazy_span.rs's module doc and its `a_fill_shaped_background_is_not_left_ //! oversized` test) -- a plain unit test could have (and now does) catch //! it directly, but it was this screenshot rendering as a single blank //! tinted rectangle that pointed at it first. use iris::prelude::*; use winit::{dpi::LogicalSize, window::WindowAttributes}; fn main() { DefaultApp::::run(); } #[derive(DefaultUiState)] struct State { ui_state: DefaultUiState, } const ROWS: usize = 800; const IMAGE_EVERY: usize = 12; /// Repeats a short sentence a varying number of times per row so real /// wrapping happens at every row height from one line to several, rather /// than every row being identically tall (which would render correctly /// even with a broken height measurement). fn row_text(i: usize) -> String { const SENTENCE: &str = "Iris lays out this row once and moves it on scroll, never re-laying it out. "; let repeats = 1 + (i * 7) % 5; format!("Message {i}: {}", SENTENCE.repeat(repeats)) } /// A small solid-colour square standing in for a real decoded image -- /// what matters for I3 is that a row can carry an `Image` widget at all, /// not what the picture shows. fn row_image(i: usize) -> image::DynamicImage { let hue = ((i * 47) % 255) as u8; image::RgbaImage::from_pixel(48, 48, image::Rgba([hue, 128, 255 - hue, 255])).into() } fn build_row(rsc: &mut Rsc, i: usize) -> StrongWidget { let tint = if i.is_multiple_of(2) { Color::rgb(120, 130, 170) } else { Color::rgb(70, 80, 140) }; let text_color = Color::BLACK; if i.is_multiple_of(IMAGE_EVERY) { let text = wtext(row_text(i)) .wrap(true) .color(text_color) .add_strong(rsc) .any(); let img = image::(row_image(i))(rsc); let img = rsc.widgets_mut().add_strong(img).any(); let mut span = Span::empty(Dir::DOWN); span.push(text); span.push(img); span.pad(dp(8.0)) .background(rect(tint)) .add_strong(rsc) .any() } else { wtext(row_text(i)) .wrap(true) .color(text_color) .pad(dp(8.0)) .background(rect(tint)) .add_strong(rsc) .any() } } impl DefaultAppState for State { // A phone-plausible portrait shape (the transcript screen this is // standing in for). The tiling headless compositor `run-headless.sh` // uses ignores this and fills its own 1920x1200 output regardless, but // it's a correct hint for any other backend (a real window manager, or // android-view) and costs nothing to state. fn window_attributes() -> WindowAttributes { WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0)) } fn new( mut ui_state: DefaultUiState, rsc: &mut DefaultRsc, _: Proxy, ) -> Self { let mut list = LazySpan::new(Dir::DOWN, Pin::End); for i in 0..ROWS { let row = build_row(rsc, i); list.push_back(LazyItem::new(i as u64, row)); } // `.scrollable()`, like anything else that scrolls -- here the // span's own inherent one, which registers the wheel and the drag // against the controller it already owns rather than wrapping it // in a `ScrollArea`. Masked outside it, since a `LazySpan` draws // the row straddling each edge in full and asserts something clips // it. let root = list .scrollable() .masked() .background(rect(Color::WHITE)) .add_strong(rsc); ui_state.set_root(root.any()); Self { ui_state } } }