Bug 1 (card_plugin): waste Draw-Three fan step was a fixed 0.28×card_width,
chosen for the desktop gap ratio (H_GAP_DIVISOR=4). On Android
(H_GAP_DIVISOR=32) the column spacing is only 1.031×card_width, so the same
fraction pushed the top fanned card's centre past the waste column's right
edge. Fix: derive fan_step from column spacing × 0.224 — preserves 0.28×cw
on desktop while reducing to ≈0.231×cw on Android, keeping fanned cards
within their column footprint. Adds regression test on 900×2000 portrait window.
Bug 2 (safe_area): refresh_insets stored its retry counter as Local<u32>,
making it impossible to re-arm after a background/foreground cycle. On resume
the counter was already saturated so JNI was never re-queried; layouts
computed with stale (zero) insets pushed the top card row up under the HUD.
Fix: convert tries to SafeAreaPollTries Resource; add android::rearm_on_resumed
which resets both counter and SafeAreaInsets on AppLifecycle::WillResume so
the poller re-fires; add on_app_resumed (all platforms) which emits a synthetic
WindowResized on WillResume to immediately trigger layout recomputation. Adds
pure-function regression test in layout.rs pinning the suspend→resume invariant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug A: Replace U+21C4 (tofu on FiraMono) with plain ASCII "M" on the
Modes action button.
Bug B: HudAvatar disc was invisible against BG_HUD_BAND (same dark
grey). Switch background to ACCENT_PRIMARY and text to TEXT_PRIMARY so
the disc is clearly visible.
Bug C/D: toggle_hud_on_tap improvements:
- Drain buffered TouchInput events in the early-return path (scrim
present or paused) so the modal-dismiss frame does not replay the
button tap's Started+Ended pair as a spurious toggle.
- Stop clearing start_pos on TouchPhase::Moved — Android fires Moved
even for clean taps (jitter), and the distance check at Ended already
rejects real drags via drag.is_idle(). Clearing it silently swallowed
toggle attempts on physical devices.
- Increase HUD_TAP_SLOP_PX from 15 → 25 for better tap recognition.
Also reduces Android HUD_BAND_HEIGHT from 128 → 80 px now that action
buttons live in the bottom bar rather than the top band.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UX-1 (safe_area.rs): apply_safe_area_to_modal_scrims pads ModalScrim
bottom by insets.bottom / scale_factor so Done buttons clear the
gesture bar; fires on inset change + Added<ModalScrim>
- UX-5b (home_plugin.rs): replace Geometric Shapes (U+25xx, missing
from FiraMono) with card suits U+2660/2665/2666
- UX-7 (help_plugin.rs): shorten Android ≡ button description to
"Open menu (Stats, Settings, Profile...)" — fits one line at 360 dp
- BUG-3 (hud_plugin.rs): guard spawn_menu_popover with
scrims.is_empty() so tapping ≡ while a modal is open is a no-op
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`WindowInsets.getInsets(systemBars())` returns physical pixels (e.g. 84 px
on a 2.625× Pixel 7) but both Bevy's `Val::Px` (UI layer) and the world-
space layout coordinate system use logical pixels. Dividing by
`window.scale_factor()` before applying gives the correct 32 dp offset.
- `safe_area.rs::apply_safe_area_anchors`: query `Window`, divide `insets.top`
by `scale_factor()` before writing `Val::Px(base_top + top_logical)`.
- `layout.rs::compute_layout`: new `safe_area_top: f32` parameter (logical px)
subtracts from the vertical budget (`card_width_height_based`) and from
`top_y` so both card sizing and pile positioning honour the status-bar band.
- `table_plugin.rs`: `setup_table` and `on_window_resized` now read
`SafeAreaInsets` and divide by scale before passing `safe_area_top` to
`compute_layout`. New `on_safe_area_changed` system fires a synthetic
`WindowResized` when insets arrive (~frame 2-3 on Android) so the full
resize pipeline (layout → pile markers → card snap) re-runs automatically.
- All test call-sites updated with `, 0.0` safe_area_top (desktop/no inset).
- Two regression tests added: shift amount equals `safe_area_top` exactly;
horizontal layout is unaffected by vertical inset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SafeAreaInsets resource + SafeAreaInsetsPlugin that report the
OS-reserved regions (status bar, gesture/nav bar, display cutout)
around the playable surface. Desktop reports all zeros; Android
queries WindowInsets.getInsets(systemBars()) via JNI on the decor
view, polling for up to 120 frames since getRootWindowInsets()
returns null until the view is laid out.
UI that should respect the top inset carries a SafeAreaAnchoredTop
{ base_top } marker. A change-detection system re-applies
`base_top + insets.top` whenever the resource changes, so late
inset arrival (frame 1-3 on Android) and future orientation
changes flow through without re-spawning entities.
Wires the three top-anchored HUD spawn sites — hud_band, hud
column, action button row — to the new pattern. Spawn systems
take Option<Res<SafeAreaInsets>> so HudPlugin still works
standalone in unit tests (mirrors the existing FontResource
pattern).
Closes P0 #1 of docs/android/PLAYABILITY_TODO.md. Resolves the
status-bar/HUD collision visible in the v0.22.3 hardware
screenshot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>