ae7c6c97f1
P3 — App-icon density buckets:
- Created solitaire_app/res/mipmap-{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}/
ic_launcher.png from assets/icon/ (48→mdpi, 64→hdpi, 128→xhdpi,
256→xxhdpi+xxxhdpi). aapt downscales oversized buckets; no quality loss.
- Added resources = "res" to [package.metadata.android] so cargo-apk/aapt
packages the mipmap tree into the APK.
- Added icon = "@mipmap/ic_launcher" to [package.metadata.android.application]
so the launcher references the density-bucketed icon instead of the
default grey system icon.
P3 — Density-aware card scaling: investigated, no code change required.
WindowResized fires with logical pixels; 256×384 card textures are
downscaled on all current phone targets (40dp logical → 120px physical
at 3× DPI). Upscaling only occurs on tablets wider than ~765dp at 3× DPI.
P4 — B0004 hierarchy warnings: investigated, no fix required.
.despawn() is recursive in Bevy 0.18; warnings are startup timing
artifacts (UI components propagating before parent initialises), not
gameplay bugs. No crashes or defects in 2+ min AVD runtime.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
12 KiB
12 KiB
Android Playability TODO
Started: 2026-05-10 — first hardware screenshot of v0.22.3 APK running on a real device showed the desktop HUD projected onto a 360 dp portrait viewport with no mobile adaptation. This list tracks the work needed to make the APK genuinely playable, not just "boots without crashing."
Context: v0.22.3 (signed release APK) builds and launches. JNI bridges (clipboard, keystore) compile but are untested on hardware. The work below is UI/UX port work — no architectural rewrites required.
Reading from the v0.22.3 screenshot
| Region | Observation |
|---|---|
| Top ~5 % | System bar (clock, signal, battery) overlapped by game HUD — no safe-area inset |
| HUD text row | Score:0 Pause Esc Help A Modes [] New_Game N Moves:0 0:08 all overlapping — desktop layout crammed into 360 dp |
| Keyboard hints | Esc, A, [], N shown next to buttons — meaningless on touch |
| Foundations row | Leftmost foundation (♥) clipped left; rightmost tableau column (♠ 4) clipped right |
| Card backs | Face-down cards render as solid red squares, not back-art texture |
| Vertical use | Cards occupy top ~30 % only; bottom 70 % empty black — no portrait-aware layout |
| Bottom edge | No accommodation for Android gesture / home-indicator area |
P0 — Blocking playability
- Safe-area insets (top + bottom). Closed 2026-05-10 by
b9aa262.SafeAreaInsetsresource +SafeAreaInsetsPluginqueryWindowInsets.getInsets(systemBars())via JNI on Android; HUD anchors carrySafeAreaAnchoredTop { base_top }and the change-detection fix-up system re-appliesbase_top + insets.topwhenever the resource updates. Bottom inset is captured but not yet consumed (waits for bottom-anchored UI). - Mobile HUD layout. Closed 2026-05-10. Both the left HUD
column and the right action button row are now capped at
max_width: 50 %and the button row + tier-row child Nodes carryflex_wrap: Wrap. On a 360 dp viewport the 6-button row breaks to multiple lines (right-justified) and the tier rows wrap individually instead of overflowing into the action column. On desktop (≥ 1280 px) the 50 % cap is wider than any natural row width so the existing single-line layout is unchanged. - Card-back asset not rendering. Closed 2026-05-10 by
fcc7337.AssetPlugin::file_path = "../assets"was set unconditionally to fix the desktopcargo run -p solitaire_appCWD relativity, but on Android cargo-apk packages the same directory into the APK atassets/and Bevy's AndroidAssetReader is already rooted there — prepending../walked the reader out of the APK assets root and every load failed silently. The face-down branch then fell through to thecard_back_colour(0)solid-red brick fallback. Gated the override behind#[cfg(not(target_os = "android"))]. - Viewport overflow. Closed 2026-05-10.
compute_layoutwas clamping the input window up toMIN_WINDOW = 800 × 600, so a 360 dp phone got laid out as if it were 800-wide and the outer piles fell outside the actual viewport. Lowered the floor to 320 × 400 (below the smallest reasonable phone) so real Android resolutions flow through without clamping, while keeping a sentinel to guard against degenerate / startup-zero windows. New regression testphone_portrait_layout_fits_horizontallyasserts all 13 piles fit a 360 × 800 viewport.
P1 — Touch UX
- Suppress keyboard-hint labels on Android. Closed
2026-05-10.
spawn_action_buttonnow nulls thehotkeyargument on Android via a#[cfg(target_os = "android")]rebind, so the U / Esc / F1 / N chips next to the action row labels disappear on touch builds. Other hint sites (onboarding panel, pause-modalEschint, mode-card hotkey chips on the home screen, replay overlay footer, modal toggle hints in profile/stats/leaderboard/settings, help screen) survive — they live behind navigation and a touch user reaches them less often. Track as a P3 sweep when more screens are audited on hardware. - Thumb-sized hit targets. Closed 2026-05-10. Action
button Node carries
min_width: Val::Px(48.0), min_height: Val::Px(48.0)— meets Material's 48 dp baseline on touch and is a no-op for buttons whose content already exceeds 48 px in either axis. Applied universally rather than cfg-gated since Material's guideline applies to all input modes. Cards, pile markers, modal close buttons not yet audited — track as P3 if they fall below threshold on hardware. - Portrait-first card spacing. Closed 2026-05-11.
compute_layoutnow derives an adaptivetableau_fan_fracfrom the available vertical space below the tableau row. On height-limited (desktop) windows the formula returns ≈ 0.25 and the clamp keeps the existing behaviour. On width-limited (portrait phone) windows — where card size is constrained by the 9-column horizontal packing — the fan fraction expands to fill the viewport (≈ 0.84 at 360 × 800 dp).tableau_facedown_fan_fracscales proportionally. Both values live in theLayoutstruct;card_plugin::card_positionsandinput_plugin::card_position/pile_drop_rectread from the struct so rendering and hit-testing stay in sync across viewport sizes. - Double-tap auto-move visible feedback. Closed 2026-05-11.
On a recognised double-tap (priority 1 single-card or priority 2
stack move), the moved card(s) receive a 0.35 s lime flash
(
STATE_SUCCESStint +HintHighlight { remaining: 0.35 }) before the move request is written. The flash persists through the card animation and is cleaned up by the existingtick_hint_highlightsystem. Hardware trigger-verification remains a manual step — connect AVD or device and confirm two rapidTouchPhase::Endedevents within 0.5 s produce the lime flash.
P2 — Polish
- Drag responsiveness on touch. Closed 2026-05-11.
Two code-side improvements shipped; final feel confirmation still needs
hardware:
start_drag(mouse path) now bails out when a touch is just-pressed (Touches::iter_just_pressed()), ensuringtouch_start_dragalways owns the drag state on touch-screen devices — including Bevy/Winit versions that simulateMouseButton::Leftfrom the primary touch.- Mobile drag commit threshold lowered 10 px → 8 px, matching Android's
ViewConfiguration.getScaledTouchSlop()spec. Smaller threshold → smaller snap-on-commit and faster perceived response. Remaining: connect AVD or device and verify drag feels responsive with no stutter; tune threshold further if needed.
- Long-press menu. Closed 2026-05-11. New system
radial_open_on_long_pressinradial_menu.rscounts up while a touch is held (drag.active_touch_id.is_some() && !drag.committed) and opensRightClickRadialState::Activeafter 0.5 s — the same state the right-click path uses. Existing radial infrastructure then handles everything:radial_track_cursorextended to fall back to the first active touch when no cursor position is available, so sliding the held finger moves the hover ring.radial_handle_release_or_cancelextended to confirm/cancel onTouches::iter_just_released()in addition to right-mouse release.handle_double_tapskips when the radial is active (guards a narrow edge case where the finger lifts at exactly the same frame the 0.5 s threshold fires). Hardware verification needed: confirm the 0.5 s hold feel, verify sliding to a destination and lifting confirms the move.
- HUD typography. Closed 2026-05-11. New system
update_hud_typographyfires onWindowResizedand adjusts Tier-1 font sizes based on viewport width. Below 480 logical px: ScoreTYPE_HEADLINE(26) →TYPE_BODY_LG(18), Moves/TimerTYPE_BODY_LG(18) →TYPE_CAPTION(11), so all three items fit in the 180 dp HUD column on a 360 dp phone. At ≥ 480 px the original sizes are restored — desktop/tablet layout unchanged.add_message::<WindowResized>()added defensively toHudPluginso the system works underMinimalPluginsin tests. - Orientation lock. Closed 2026-05-11. Added
[package.metadata.android.application.activity]section tosolitaire_app/Cargo.tomlwithorientation = "portrait". cargo-apk/ndk-build maps this toandroid:screenOrientation="portrait"in the generatedAndroidManifest.xml. Remove (or add a landscape layout) before enabling auto-rotate.
P3 — Asset density
- Density-aware card scaling. Closed 2026-05-11 — no code change
required.
WindowResizedfires with logical pixels; sprites are sized in world units (1 world unit = 1 logical pixel); Bevy's renderer maps logical → physical viascale_factorinternally. On a 360 dp 3×-DPI phone, cards are 40 logical dp = 120 physical px. The 256 × 384 px card textures are downscaled to fit (256 → 120 px) — quality is fine. Upscaling only occurs ifcard_width × scale_factor > 256, i.e. a tablet with a logical width > 765 dp at 3× DPI — no current target device falls in that range. Revisit if the game ships on large-screen high-DPI tablets. - App-icon density buckets. Closed 2026-05-11. Created
solitaire_app/res/mipmap-{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}/ic_launcher.pngfrom the existingassets/icon/PNGs (48→mdpi, 64→hdpi, 128→xhdpi, 256→xxhdpi+xxxhdpi). Addedresources = "res"to[package.metadata.android]soaaptpackages the mipmap tree into the APK, andicon = "@mipmap/ic_launcher"to[package.metadata.android.application]so the launcher references it.
P4 — Stability / runtime
- B0004 ECS hierarchy warnings. Investigated 2026-05-11 — no
fix required. B0004 fires via Bevy's
validate_parent_has_component<C>hook when a child entity has UI componentC(e.g.Node,InheritedVisibility) but its parent doesn't yet. In Bevy 0.18,.despawn()is recursive (docs: "When a parent is despawned, all children will also be despawned"), so all.despawn()calls in the engine are safe. The warnings seen on the Pixel 7 AVD during startup are a component-propagation timing artifact — UI children reach the hook before the parent's inherited components finish initialising — not a gameplay defect.despawn_related::<Children>()incard_plugin.rsis explicit child-only teardown (parent kept alive) and is correct. No gameplay bugs attributed to these warnings over 2+ min AVD runtime. - AVD functional tests for JNI bridges. Clipboard (
2c822ba) and Keystore (f281425) shipped but never tested on real device or AVD. Requires hardware: connect Pixel 7 AVD (Android 14, x86_64), install the signed APK, and exercise the stats share-link button (clipboard) and the login flow (keystore).
Notes / decisions
- This list is screenshot-driven; expect more items to surface once P0 unblocks actually moving cards on hardware.
- The pattern across all the bugs is "no one ran the relevant code
path on Android yet." The hard work — Bevy 0.18 on Android,
JNI bridges, signed CI builds — is done. What's left is a
coordinated pass of
#[cfg(target_os = "android")]gates plus makingLayoutResourcequery the real surface size. - Where possible, prefer responsive layout (query window size) over
branching
#[cfg]blocks. Branches are fine for input methods (touch vs. mouse) but not for screen geometry — a foldable or desktop window of equivalent size should look the same.