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>
147 lines
5.7 KiB
Rust
147 lines
5.7 KiB
Rust
//! Layer 2 of docs/RUST.md's "Three test layers": the fixture-backed
|
|
//! transcript screen in a phone-shaped window, for looking at.
|
|
//!
|
|
//! iris/run-headless.sh phone --phone --shot /tmp/phone.png -- -p transcript-fixture
|
|
//!
|
|
//! `--phone` sets the headless sway output to the phone's own 1080x2424
|
|
//! and exports `IRIS_SCALE=2.55`, so this draws at the density Iris's
|
|
//! phone reports (`ai_app::ui::fixture::PHONE_SCALE`) rather than the
|
|
//! desktop's 1.0 -- same screen, same fixture and the same folding as
|
|
//! the Android bench and the headless tests, so what differs between a
|
|
//! screenshot here and one from the phone is the renderer, never the
|
|
//! data.
|
|
//!
|
|
//! `--message TEXT` (through `RUN_HEADLESS_ARGS`) starts with that text
|
|
//! already in the composer, `\n` for a newline -- the composer's grown
|
|
//! and overflowing states are otherwise unreachable here, since this
|
|
//! window has no keyboard to type into (UI_RULES.md's "check the states
|
|
//! you can't see by default"). `--typed TEXT` *enters* the same text
|
|
//! instead, one character per 100ms: laying the composer out from
|
|
//! scratch and growing one already on screen are different cases, and
|
|
//! only the second reproduced the caret landing in the bar's padding
|
|
//! (IRIS.md, 2026-09-08).
|
|
//!
|
|
//! No server: `transcript-fixture` embeds the transcript. Colour,
|
|
//! spacing, type and anything a person has to *see* is answered here;
|
|
//! anything with an assertion behind it belongs in `tests/
|
|
//! phone_screen.rs` one layer down.
|
|
|
|
use iris::prelude::*;
|
|
use winit::{dpi::PhysicalSize, window::WindowAttributes};
|
|
|
|
/// The `--ime PX` argument: the bottom inset a keyboard would report,
|
|
/// applied after the first frame the way Android's `on_insets_changed`
|
|
/// does. The composer's keyboard-open layout is otherwise unreachable
|
|
/// here, and it is where its mask went wrong before (see
|
|
/// `ActiveData::own_mask`).
|
|
fn ime_argv() -> Option<f32> {
|
|
let mut args = std::env::args().skip(1);
|
|
while let Some(arg) = args.next() {
|
|
if arg == "--ime" {
|
|
return args.next()?.parse().ok();
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// The `--message TEXT` argument, with `\n` taken as a newline so a
|
|
/// multi-line message survives one shell word.
|
|
fn message_argv() -> Option<String> {
|
|
let mut args = std::env::args().skip(1);
|
|
while let Some(arg) = args.next() {
|
|
if arg == "--message" {
|
|
return Some(args.next()?.replace("\\n", "\n"));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// The `--typed TEXT` argument: the same text as `--message`, but
|
|
/// *entered* rather than preloaded -- one insertion per 100ms, into a
|
|
/// focused field, the way a person types. The two are different cases
|
|
/// for layout: `--message` is laid out from scratch on the first frame,
|
|
/// while this grows an already-drawn composer, which is the path
|
|
/// Iris's 2026-09-08 phone report is about.
|
|
fn typed_argv() -> Option<String> {
|
|
let mut args = std::env::args().skip(1);
|
|
while let Some(arg) = args.next() {
|
|
if arg == "--typed" {
|
|
return Some(args.next()?.replace("\\n", "\n"));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn main() {
|
|
DefaultApp::<Client>::run();
|
|
}
|
|
|
|
#[derive(DefaultUiState)]
|
|
pub struct Client {
|
|
ui_state: DefaultUiState,
|
|
#[allow(dead_code)]
|
|
screen: Option<ai_app::ui::TranscriptScreen>,
|
|
}
|
|
|
|
impl DefaultAppState for Client {
|
|
fn window_attributes() -> WindowAttributes {
|
|
WindowAttributes::default()
|
|
.with_title("iris transcript (bench fixture)")
|
|
.with_inner_size(PhysicalSize::new(
|
|
ai_app::ui::fixture::PHONE_WIDTH,
|
|
ai_app::ui::fixture::PHONE_HEIGHT,
|
|
))
|
|
}
|
|
|
|
fn new(
|
|
mut ui_state: DefaultUiState,
|
|
rsc: &mut DefaultRsc<Self>,
|
|
_: Proxy<Self::Event>,
|
|
) -> Self {
|
|
let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) {
|
|
Ok(opened) => {
|
|
if let Some(message) = message_argv() {
|
|
opened.screen.composer.field.edit(rsc).set(&message);
|
|
}
|
|
if let Some(text) = typed_argv() {
|
|
let field = opened.screen.composer.field;
|
|
let redraw = rsc.tasks.redraw_handle();
|
|
rsc.spawn_task(async move |mut ctx| {
|
|
for ch in text.chars() {
|
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
ctx.update(move |state: &mut Client, rsc| {
|
|
state.set_focus(Some(field));
|
|
let end = rsc[field].text().len();
|
|
let mut edit = field.edit(rsc);
|
|
if edit.text.caret().is_none() {
|
|
edit.set_cursor_byte(end);
|
|
}
|
|
edit.insert(&ch.to_string());
|
|
});
|
|
redraw.request_redraw();
|
|
}
|
|
});
|
|
}
|
|
if let Some(inset) = ime_argv() {
|
|
opened.screen.composer.set_bottom_inset(rsc, inset);
|
|
}
|
|
Some(opened.screen)
|
|
}
|
|
// On screen rather than a panic: this window exists to be
|
|
// looked at, and "the fixture stopped folding" is something
|
|
// to read, not a process that vanished (UI_RULES.md).
|
|
Err(message) => {
|
|
let text = wtext(format!("Couldn't fold the bench fixture: {message}"))
|
|
.color(Color::WHITE)
|
|
.wrap(true)
|
|
.pad(dp(16))
|
|
.add_strong(rsc)
|
|
.any();
|
|
ui_state.set_root(text);
|
|
None
|
|
}
|
|
};
|
|
Self { ui_state, screen }
|
|
}
|
|
}
|