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:
1 parent
7b54aaf3c4
commit
a9312e9431
113 files changed
+23221
-2992
No files matched your search
@@ -0,0 +1,164 @@
|
||||
//! Replays a `.touch` file into the compositor as a left-button drag --
|
||||
//! see this crate's `Cargo.toml` for why it exists rather than
|
||||
//! `swaymsg seat - cursor`.
|
||||
//!
|
||||
//! WAYLAND_DISPLAY=… replay-touch WIDTH HEIGHT FILE
|
||||
//!
|
||||
//! `WIDTH`/`HEIGHT` are the output's own size, because the virtual
|
||||
//! pointer protocol positions absolutely against an extent rather than
|
||||
//! in pixels; passing the output size makes a script's coordinates mean
|
||||
//! the same pixels they mean in the headless tests.
|
||||
//!
|
||||
//! Replayed in real time (the sleeps between samples are the gaps in the
|
||||
//! file), because winit has no timestamp on a pointer event and dates
|
||||
//! each one when it arrives -- so a 20ms flick has to actually take
|
||||
//! 20ms here, unlike layer 1 where the sample carries its own time.
|
||||
|
||||
use iris::harness::{TouchAction, TouchScript};
|
||||
use std::time::Duration;
|
||||
use wayland_client::protocol::wl_pointer::ButtonState;
|
||||
use wayland_client::protocol::{wl_registry, wl_seat};
|
||||
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
|
||||
use wayland_protocols_wlr::virtual_pointer::v1::client::{
|
||||
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
|
||||
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
|
||||
};
|
||||
|
||||
/// `linux/input-event-codes.h`. The protocol takes the kernel's own
|
||||
/// button code, not a wayland enum.
|
||||
const BTN_LEFT: u32 = 0x110;
|
||||
|
||||
/// How long the pointer sits at the gesture's first position before the
|
||||
/// script starts -- see the comment at the pre-step in `main`.
|
||||
const SETTLE: Duration = Duration::from_millis(200);
|
||||
|
||||
#[derive(Default)]
|
||||
struct Globals {
|
||||
seat: Option<wl_seat::WlSeat>,
|
||||
manager: Option<ZwlrVirtualPointerManagerV1>,
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
let wl_registry::Event::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
} = event
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match interface.as_str() {
|
||||
"wl_seat" => {
|
||||
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
|
||||
}
|
||||
"zwlr_virtual_pointer_manager_v1" => {
|
||||
state.manager = Some(registry.bind(name, version.min(2), qh, ()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate_noop!(Globals: ignore wl_seat::WlSeat);
|
||||
delegate_noop!(Globals: ZwlrVirtualPointerManagerV1);
|
||||
delegate_noop!(Globals: ZwlrVirtualPointerV1);
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let [width, height, path] = args.as_slice() else {
|
||||
eprintln!("usage: replay-touch WIDTH HEIGHT FILE");
|
||||
std::process::exit(2);
|
||||
};
|
||||
let (width, height) = (parse(width, "WIDTH"), parse(height, "HEIGHT"));
|
||||
let text = std::fs::read_to_string(path)
|
||||
.unwrap_or_else(|e| fail(&format!("could not read {path}: {e}")));
|
||||
let script = TouchScript::parse(&text).unwrap_or_else(|e| fail(&e));
|
||||
|
||||
let conn = Connection::connect_to_env().unwrap_or_else(|e| {
|
||||
fail(&format!(
|
||||
"no wayland display ({e}); is WAYLAND_DISPLAY set?"
|
||||
))
|
||||
});
|
||||
let mut queue = conn.new_event_queue();
|
||||
let qh = queue.handle();
|
||||
let display = conn.display();
|
||||
display.get_registry(&qh, ());
|
||||
let mut globals = Globals::default();
|
||||
queue
|
||||
.roundtrip(&mut globals)
|
||||
.unwrap_or_else(|e| fail(&format!("wayland roundtrip failed: {e}")));
|
||||
|
||||
let manager = globals.manager.as_ref().unwrap_or_else(|| {
|
||||
fail(
|
||||
"this compositor does not offer zwlr_virtual_pointer_manager_v1, so a pointer cannot \
|
||||
be synthesised; sway and every wlroots compositor do",
|
||||
)
|
||||
});
|
||||
let pointer = manager.create_virtual_pointer(globals.seat.as_ref(), &qh, ());
|
||||
|
||||
// Put the pointer where the gesture starts and let the compositor
|
||||
// settle before anything is pressed. Without this the press is
|
||||
// dropped: sway has just learned about this pointer, and a button
|
||||
// sent in the same breath as the motion that first puts it over a
|
||||
// window arrives before there is a focused surface to send it to --
|
||||
// winit sees `CursorEntered`, the moves and the *release*, never the
|
||||
// press, so the gesture reads as a hover and nothing scrolls. Found
|
||||
// by printing winit's own events; the settle is what fixed it.
|
||||
if let Some(first) = script.samples.first() {
|
||||
pointer.motion_absolute(0, first.pos.x as u32, first.pos.y as u32, width, height);
|
||||
pointer.frame();
|
||||
conn.flush()
|
||||
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
|
||||
std::thread::sleep(SETTLE);
|
||||
}
|
||||
|
||||
let mut previous = 0;
|
||||
for sample in &script.samples {
|
||||
std::thread::sleep(Duration::from_millis(sample.t_ms - previous));
|
||||
previous = sample.t_ms;
|
||||
let t = sample.t_ms as u32;
|
||||
pointer.motion_absolute(t, sample.pos.x as u32, sample.pos.y as u32, width, height);
|
||||
// One frame per sample, so the compositor delivers them as
|
||||
// separate pointer frames rather than coalescing the whole
|
||||
// gesture -- the shape the file recorded is the point.
|
||||
pointer.frame();
|
||||
// The button goes in a frame of its own, *after* the motion has
|
||||
// been committed. Sent in the same frame as the motion that
|
||||
// first puts the pointer over the window, sway drops it: the
|
||||
// client sees `CursorEntered` and the moves but never a
|
||||
// `MouseInput { state: Pressed }`, so the whole gesture reads as
|
||||
// a hover and nothing scrolls. Found exactly that way, by
|
||||
// printing winit's events.
|
||||
let state = match sample.action {
|
||||
TouchAction::Down => Some(ButtonState::Pressed),
|
||||
TouchAction::Up | TouchAction::Cancel => Some(ButtonState::Released),
|
||||
TouchAction::Move => None,
|
||||
};
|
||||
if let Some(state) = state {
|
||||
pointer.button(t, BTN_LEFT, state);
|
||||
pointer.frame();
|
||||
}
|
||||
conn.flush()
|
||||
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
|
||||
}
|
||||
pointer.destroy();
|
||||
conn.flush().ok();
|
||||
}
|
||||
|
||||
fn parse(text: &str, what: &str) -> u32 {
|
||||
text.parse()
|
||||
.unwrap_or_else(|_| fail(&format!("{what} is not a whole number: {text:?}")))
|
||||
}
|
||||
|
||||
fn fail(message: &str) -> ! {
|
||||
eprintln!("replay-touch: {message}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Reference in new issue
Block a user