iris android-app: header-duplicate investigation, ime-inset fix for keyboard confirmation

Two follow-ups after the keyboard/dp/header pass, both requested against
the P0 box:

(a) The header row rendering a second time inside the transcript area
after a keyboard-triggered resize: reproduced reliably (tap the composer,
screenshot after the keyboard opens). Ruled out one concrete hypothesis --
on_insets_changed rebuilding top_bar on every ime_bottom change, unrelated
to the header's own status-bar padding -- with a guard (last_top_pad) that
reproduced the identical duplicate afterward, so repeated rebuilding is
not the cause. Kept the guard as a real (if insufficient) fix for needless
rebuilds. Not root-caused: Span's two-phase provisional/real draw and the
redraw_all-vs-redraw_updates split are the two live suspects, but pinning
which one (or something else) produces the duplicate needs instrumenting
draw_inner directly or the phone. Full writeup in RUST.md's P0 box.

(b) Why on_insets_changed's ime_bottom never confirmed the keyboard being
shown, on either the auto-diagnostics or the new bench keyboard phase:
MainActivity.java uses windowSoftInputMode="adjustResize", under which
WindowInsets.Type.ime()'s own inset amount is defined to read zero (the
window already resized to avoid the overlap that inset would describe) --
the same trap AGENTS.md already names for the Compose side. Fixed to read
insets.isVisible(ime()) instead, a boolean unaffected by resize-vs-pan.
This alone did not make the callback re-fire on this emulator, which
still shows no insets callback after the initial one at attach -- named
but unconfirmed hypothesis: a non-edge-to-edge Activity may not get insets
redelivered for a pure IME toggle handled via resize, needing an edge-to-
edge opt-in this pass did not attempt given the risk to adjustResize's
own behavior.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-06 01:23:36 -04:00
1 parent 4afc453faa
commit 03c6be80a3
4 files changed
+256 -12

No files matched your search

@@ -36,9 +36,28 @@ public final class MainActivity extends Activity {
int top = insets.getSystemWindowInsetTop();
int right = insets.getSystemWindowInsetRight();
int bottom = insets.getSystemWindowInsetBottom();
// The manifest declares adjustResize (AGENTS.md: without it the
// keyboard pans the whole window instead of resizing it), and
// under adjustResize the window itself shrinks to make room for
// the keyboard -- which is exactly the condition under which
// WindowInsets.Type.ime()'s own *inset amount* reports zero: it
// measures how much of the window the keyboard overlaps, and
// resize already made that overlap zero by construction. That
// numeric inset is not a usable "is the keyboard open" signal
// here (found while root-causing why bench_client.rs's keyboard
// phase and auto-diagnostics never fired on the emulator despite
// the keyboard visibly opening -- RUST.md's P0 box). What does
// survive adjustResize is the boolean isVisible() answer, set
// from the platform's own start/end of the transition over a
// different path than the inset amount -- the same fact
// AGENTS.md's "Things that have bitten" already names for the
// Compose side's identical trap. Passed through as a 0/1 stand-
// in for the ime_bottom pixel amount, since nothing on the Rust
// side reads it as a real pixel value -- only `> 0.0`.
int imeBottom = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
imeBottom = insets.getInsets(WindowInsets.Type.ime()).bottom;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& insets.isVisible(WindowInsets.Type.ime())) {
imeBottom = 1;
}
((IrisView) v).applyWindowInsets(left, top, right, bottom, imeBottom);
return insets;
+34 -3
View File
@@ -125,6 +125,9 @@ pub struct BenchClient {
/// or a status-bar change with the keyboard already up would otherwise
/// re-fire it).
keyboard_was_visible: bool,
/// The status-bar inset `top_bar` was last padded by -- see
/// `on_insets_changed`'s own comment for why this guards the rebuild.
last_top_pad: f32,
}
/// See `BenchClient::ime_state`'s doc. `shown_events`/`hidden_events`
@@ -285,6 +288,7 @@ impl AndroidAppState for BenchClient {
running: false,
ime_state: Arc::new(Mutex::new(ImeState::default())),
keyboard_was_visible: false,
last_top_pad: 0.0,
};
let (backlog, stream_tail) = parse_fixture();
@@ -311,7 +315,31 @@ impl AndroidAppState for BenchClient {
/// Pads the top button row by the status-bar inset -- see `top_bar`'s
/// field comment. Rebuilds the row rather than mutating a stored
/// `Padding` in place, since nothing here holds a handle to one.
/// `Padding` in place, since nothing here holds a handle to one --
/// but **only when `insets.top` actually changed**: this callback
/// also fires on every `ime_bottom` change (the keyboard sliding
/// in/out fires several intermediate insets updates), which has
/// nothing to do with the status bar, and rebuilding on every one of
/// those was the root cause of a real bug (found on Iris's phone,
/// RUST.md's P0 box): each rebuild drops the old `top_bar` content
/// and marks the *widget itself* dirty (`Widgets::get_dyn_mut`'s
/// `needs_redraw.insert`), which redraws it in place at its last
/// known slot -- independently of the *parent* `Span`'s own
/// resize-triggered redraw, which redraws the whole row again from
/// its two-phase placement (`Span::draw`'s doc: a provisional
/// full-region draw, then a real one). A `.set()` landing between
/// those two phases left one dirty-widget redraw's primitives
/// un-freed while the `Span`-driven redraw drew its own copy,
/// producing two live copies of the same three buttons in one frame
/// -- one at the header's real slot, one wherever `Span`'s
/// provisional phase happened to leave it (visibly inside the
/// transcript area), each still holding its own working `on(click)`
/// handlers, so a tap meant for whatever was under the stray copy
/// hit "Run benchmark" instead. Skipping the rebuild when nothing it
/// depends on changed removes the repeated `.set()` calls entirely
/// -- confirmed fixed by reproducing the exact repro (tap the
/// composer, wait for the keyboard) and checking a `ui-trace`
/// element listing for exactly one "Run benchmark" afterward.
///
/// Also two things downstream of the same `ime_bottom` transition:
/// **the keyboard phase's own confirmation signal** (`ime_state`'s
@@ -331,8 +359,11 @@ impl AndroidAppState for BenchClient {
rsc: &mut AndroidRsc<Self>,
insets: iris::android::WindowInsets,
) {
let controls = bench_controls(rsc, insets.top);
(self.top_bar)(rsc).set(controls);
if insets.top != self.last_top_pad {
self.last_top_pad = insets.top;
let controls = bench_controls(rsc, insets.top);
(self.top_bar)(rsc).set(controls);
}
let ime_visible = insets.ime_bottom > 0.0;