From 1190ed3ce60f97fcbd171bb980d5c7e5e76b3d7d Mon Sep 17 00:00:00 2001 From: funman300 Date: Mon, 6 Jul 2026 12:50:01 -0700 Subject: [PATCH] fix(engine): re-poll safe-area insets after app resume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refresh_insets gated its loop on insets.is_populated(), so once insets resolved at first launch, rearm_on_resumed's poll-counter reset was a no-op and JNI was never queried again. Insets that changed while the app was backgrounded (fold/unfold, rotation, gesture/3-button nav switch) stayed stale until process restart. Gate the loop on the poll counter alone and settle a cycle by exhausting it once a populated reading arrives. The cached resource is rewritten only when the value actually differs, so resumes where nothing moved trigger no change detection and no relayout — preserving the no-flash resume behaviour. Also correct the stale on_app_resumed doc that still described the old inset-zeroing approach. Closes #116 Co-Authored-By: Claude Fable 5 --- solitaire_engine/src/safe_area.rs | 40 +++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/solitaire_engine/src/safe_area.rs b/solitaire_engine/src/safe_area.rs index 6544bd5..9c0c48b 100644 --- a/solitaire_engine/src/safe_area.rs +++ b/solitaire_engine/src/safe_area.rs @@ -191,10 +191,12 @@ fn apply_safe_area_to_modal_scrims( /// whatever `SafeAreaInsets` are current at that moment. /// /// On Android the `android::rearm_on_resumed` system runs in the same frame -/// and resets both `SafeAreaPollTries` and `SafeAreaInsets` to zero, causing -/// `refresh_insets` to re-poll JNI over the next few frames. When it resolves -/// the correct values, `on_safe_area_changed` in `table_plugin` emits a second -/// synthetic `WindowResized` and the layout converges to the right position. +/// and resets `SafeAreaPollTries` (the cached `SafeAreaInsets` keep their +/// last-known values), causing `refresh_insets` to re-poll JNI over the next +/// few frames. If the insets changed while backgrounded, `on_safe_area_changed` +/// in `table_plugin` emits a second synthetic `WindowResized` and the layout +/// converges to the right position; if they didn't, nothing is rewritten and +/// the layout stays put. /// /// On non-Android targets this handler still fires — it ensures that a resume /// event always refreshes the layout (e.g., after a minimise/restore on @@ -232,9 +234,14 @@ mod android { pub(super) struct SafeAreaPollTries(pub u32); /// Polls Android for safe-area insets until we get a non-zero - /// reading, then stops. `getRootWindowInsets()` returns `null` (or - /// all-zero `Insets`) until the decor view has been laid out, which - /// is typically frame 1–3 of a fresh launch. + /// reading, then settles until [`rearm_on_resumed`] re-arms it on the + /// next foreground resume — insets can change while backgrounded + /// (rotation, fold/unfold, gesture ↔ 3-button nav). The poll counter + /// (not `insets.is_populated()`) gates the loop, so a re-armed cycle + /// re-queries JNI even though cached values are already populated. + /// `getRootWindowInsets()` returns `null` (or all-zero `Insets`) + /// until the decor view has been laid out, which is typically frame + /// 1–3 of a fresh launch. pub(super) fn refresh_insets( mut insets: ResMut, mut poll: ResMut, @@ -243,18 +250,25 @@ mod android { // devices that genuinely report zero insets. const MAX_TRIES: u32 = 120; // ~2 seconds @ 60 fps - if poll.0 >= MAX_TRIES || insets.is_populated() { + if poll.0 >= MAX_TRIES { return; } poll.0 += 1; match query_insets() { Ok(v) if v.is_populated() => { - info!( - "safe_area: insets resolved top={} bottom={} left={} right={} (after {} frames)", - v.top, v.bottom, v.left, v.right, poll.0 - ); - *insets = v; + if *insets != v { + info!( + "safe_area: insets resolved top={} bottom={} left={} right={} (after {} frames)", + v.top, v.bottom, v.left, v.right, poll.0 + ); + *insets = v; + } + // Settled for this poll cycle; `rearm_on_resumed` re-arms on + // the next resume. Writing `insets` only on an actual change + // keeps resource change detection (and the relayout it + // triggers) quiet on resumes where nothing moved. + poll.0 = MAX_TRIES; } Ok(_) => { // Layout not ready yet; try again next frame. -- 2.47.3