Iris asked for this 2026-09-06 (IRIS_TODO.md, "a third length kind beside relative and pixels ... a unit resolved against the display's density at layout time"): before this, a Len was abs (physical pixels) or rel/rest (a fraction of the parent), and the only way to make a design size look the same physical size on a denser display was a single global multiply applied after layout -- which the previous commit found is also what made text blurry. Len gains a `dp` field, resolved against a `density: f32` (physical pixels per dp) now carried on UiRenderState/Painter (`UiRenderState::set_density`/`density()`, `Painter::density()`) and threaded through every `apply_rest`/`to_uivec2` call site. `len_fns::dp` / `Len::dp` construct one, exactly parallel to the existing `abs`/`rel`/ `rest`. A bare number is unaffected (still `abs`, physical pixels) -- `dp` is opt-in. Text: `TextBuffer::shape` now takes `density` and multiplies `font_size`/`line_height` (and any span override) by it before handing them to parley, so the size that reaches the shaper and the rasteriser (`TextData::place`) is the display's real physical size -- the atlas holds a bitmap at the resolution it is actually shown at, instead of a low-resolution one stretched afterward. `GlyphKey.size` already keys on the resolved `font_size`, so a cache entry is naturally per physical size with no further change. `TextData` also carries its own `density` copy for `TextEditCtx::layout` (cursor movement/hit-testing), which shapes text from an input callback with no `Painter` to read it from. `Span::gap` and `Padding`'s four sides move from bare `f32` to `Len`, so `.gap(dp(4))`/`.pad(dp(10))` work the same way any other size does; a bare number still means physical pixels, unchanged. Migrated transcript-ui's non-text sizes (row gap/padding, composer padding) and one example to the new unit, per IRIS_TODO.md's "done when" list. Android's own density (`DisplayMetrics.density`) is wired to both copies in `new_peer`; the winit backend has no per-monitor density wired up yet and stays at the default (1.0). docs/IRIS.md, docs/LAYOUT.md and IRIS_TODO.md updated next. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
120 lines
4.3 KiB
Rust
120 lines
4.3 KiB
Rust
//! RUST.md's I3: `iris::widget::List` 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; `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 `List::place`'s oversized-background bug (see
|
|
//! list.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::<State>::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: UiRsc + 'static>(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::<Rsc>(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<Self>,
|
|
_: Proxy<Self::Event>,
|
|
) -> Self {
|
|
let mut list = List::new(Axis::Y);
|
|
for i in 0..ROWS {
|
|
let row = build_row(rsc, i);
|
|
list.push_back(ListRow::new(i as u64, row));
|
|
}
|
|
|
|
let root = list
|
|
.on(CursorSense::Scroll, |ctx, rsc| {
|
|
let delta = ctx.data.scroll_delta.y * 50.0;
|
|
ctx.widget(rsc).scroll(delta);
|
|
})
|
|
.masked()
|
|
.background(rect(Color::WHITE))
|
|
.add_strong(rsc);
|
|
ui_state.set_root(root.any());
|
|
|
|
Self { ui_state }
|
|
}
|
|
}
|