iris is the framework alone; the app is one crate in app-rust/

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>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-08 23:36:38 -04:00
1 parent 7b54aaf3c4
commit a9312e9431
113 files changed
+23221 -2992

No files matched your search

+39 -7
View File
@@ -83,19 +83,51 @@ widget_trait! {
}
}
fn scrollable(self) -> impl WidgetIdFn<Rsc, Scroll> where Rsc: HasEvents {
/// Wrap this widget in a [`ScrollArea`] that pans along `axis`, with
/// the wheel and a finger drag both registered -- how anything with a
/// fixed layout becomes scrollable.
///
/// `pin` says which end the area opens at and clings to as its content
/// grows, and it is spelled out rather than defaulted because the two
/// cases are not variations on each other: a composer wants the end,
/// where what is being typed is, and a code fence opened at the end of
/// its longest line, which is the middle of a word (seen in
/// `iris/run-headless.sh phone`, 2026-09-08).
///
/// One method with the axis and the pin passed in, rather than the
/// three named variants this used to be (Iris, 2026-09-08: "can we
/// make both scroll methods become `.scrollable`, and it takes an axis
/// and a pin instead of having two?"). A code fence pans across its
/// own long lines exactly the way a transcript pans down its rows, so
/// the two are one mechanism with the direction passed in --
/// `DragArbiter::on` is the other half.
///
/// A [`LazySpan`] has an inherent `scrollable` of its own that this
/// does not reach: it owns a controller already and must not be
/// wrapped in an area that would slide it about as a lump.
fn scrollable(self, axis: Axis, pin: Pin) -> impl WidgetIdFn<Rsc, ScrollArea> where Rsc: HasEvents {
move |state| {
Scroll::new(self.add_strong(state), Axis::Y)
.on(CursorSense::Scroll, |ctx, rsc| {
let delta = ctx.data.scroll_delta.y * 50.0;
ctx.widget(rsc).scroll(delta);
})
.add(state)
let area = ScrollArea::new(self.add_strong(state), axis, pin);
scroll_senses(area, axis)(state)
}
}
fn masked(self) -> impl WidgetFn<Rsc, Masked> {
move |state| Masked {
shape: None,
inner: self.add_strong(state),
}
}
/// Clip to `shape` rather than to a plain box: `shape` is drawn
/// behind this widget, filling the same region, and what clips is the
/// primitive it drew -- so a rounded background and the corner its
/// content is cut to are one rect, with no radius passed twice.
/// Replaces `.masked().background(w)`, which drew the two but clipped
/// to the box.
fn masked_by<T>(self, shape: impl WidgetLike<Rsc, T>) -> impl WidgetFn<Rsc, Masked> {
move |state| Masked {
shape: Some(shape.add_strong(state)),
inner: self.add_strong(state),
}
}