Iris: "the organization of the rust rewrite is a mess right now... there shouldn't be anything related to the app inside of iris. Iris is supposed to be the UI framework alone." And, on the crate count: "I'm confused why the app only code needs more than one crate though." Nine cargo workspaces become three, and the port's project code -- which sat in five places, four of them inside the framework -- becomes one crate, `ai-app`, in `app-rust/`: client-core -> app-rust/src/client iris/transcript-ui -> app-rust/src/ui iris/transcript-fixture -> app-rust/src/ui/fixture.rs + tests/ + touch/ iris/desktop-app -> app-rust/src/desktop + src/bin_desktop.rs iris/android-app -> app-rust/src/android + android-project/ android-shell -> app-rust/src/shell iris/ keeps core, macro, the iris crate, tabs-ui and rig-input, and now mentions no session, transcript, setup or server anywhere. Only two of the old splits had a reason that survived reading. event-model stays a crate at the repo root because server/ depends on it too, so a crate is what makes the backend and the app agree by construction. The two Android .so names looked like a hard constraint -- a package produces one library artifact -- until P2 turned out to already plan merging those two Android apps into one; both faces now come out of libai_app.so, picked apart by features so `--no-default-features --features shell` keeps wgpu, parley and iris out of the Compose app's APK. docs/RUST.md's "One app crate" has the rest, including what each remaining feature is for. DECISIONS.md and SUBAGENTS.md move into docs/ with everything else. Verified: ./run-tests.sh and `cd iris && cargo test` green, clippy and fmt clean in all five workspaces, `cargo ndk -t x86_64` links libai_app.so, build-apk.sh produces an APK that installs and launches on this checkout's emulator (Gl ... virgl, as expected), and the phone-sized headless screenshot renders the transcript unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
4.5 KiB
Rust
123 lines
4.5 KiB
Rust
//! 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; `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::<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 = 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 }
|
|
}
|
|
}
|