//! 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 { 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 { 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 { 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::::run(); } #[derive(DefaultUiState)] pub struct Client { ui_state: DefaultUiState, #[allow(dead_code)] screen: Option, } 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, _: Proxy, ) -> 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 } } }