diff --git a/docs/android/PLAYABILITY_TODO.md b/docs/android/PLAYABILITY_TODO.md index 04c6a1a..c2c5073 100644 --- a/docs/android/PLAYABILITY_TODO.md +++ b/docs/android/PLAYABILITY_TODO.md @@ -135,10 +135,21 @@ rewrites required. 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.** Reduce text sizes for `Score:`, `Moves:`, - timer so they fit cleanly in one row. -- [ ] **Orientation lock.** Set `android:screenOrientation="portrait"` - in cargo-apk manifest (or design a landscape layout). +- [x] **HUD typography.** *Closed 2026-05-11.* New system + `update_hud_typography` fires on `WindowResized` and adjusts Tier-1 + font sizes based on viewport width. Below 480 logical px: Score + `TYPE_HEADLINE` (26) → `TYPE_BODY_LG` (18), Moves/Timer + `TYPE_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::()` added defensively to `HudPlugin` + so the system works under `MinimalPlugins` in tests. +- [x] **Orientation lock.** *Closed 2026-05-11.* Added + `[package.metadata.android.application.activity]` section to + `solitaire_app/Cargo.toml` with `orientation = "portrait"`. + cargo-apk/ndk-build maps this to `android:screenOrientation="portrait"` + in the generated `AndroidManifest.xml`. Remove (or add a landscape + layout) before enabling auto-rotate. ## P3 — Asset density diff --git a/solitaire_app/Cargo.toml b/solitaire_app/Cargo.toml index 0d25fd6..d0c9c65 100644 --- a/solitaire_app/Cargo.toml +++ b/solitaire_app/Cargo.toml @@ -82,3 +82,9 @@ label = "Solitaire Quest" # `debuggable` defaults to false on release builds; cargo-apk flips it # automatically for debug profiles. Leaving the field unset keeps the # default behaviour. + +[package.metadata.android.application.activity] +# Lock to portrait — the current layout has only been designed and tested +# in portrait orientation. Remove (or add a landscape layout) before +# enabling auto-rotate. +orientation = "portrait" diff --git a/solitaire_engine/src/hud_plugin.rs b/solitaire_engine/src/hud_plugin.rs index 88459e9..7444908 100644 --- a/solitaire_engine/src/hud_plugin.rs +++ b/solitaire_engine/src/hud_plugin.rs @@ -7,6 +7,7 @@ //! without a separate tick system. use bevy::prelude::*; +use bevy::window::WindowResized; use solitaire_core::card::Suit; use solitaire_core::game_state::{DrawMode, GameMode}; use solitaire_core::pile::PileType; @@ -324,11 +325,15 @@ impl Plugin for HudPlugin { .add_message::() .init_resource::() .init_resource::() + // WindowResized is registered by table_plugin; re-register + // defensively so the HUD plugin works standalone in tests. + .add_message::() .add_systems(Startup, (spawn_hud_band, spawn_hud, spawn_action_buttons)) .add_systems(Update, update_hud.after(GameMutation)) .add_systems(Update, update_won_previously.after(GameMutation)) .add_systems(Update, announce_auto_complete.after(GameMutation)) .add_systems(Update, update_selection_hud) + .add_systems(Update, update_hud_typography) .add_systems( Update, ( @@ -2003,6 +2008,48 @@ pub fn challenge_time_color(remaining: u64) -> Color { } } +/// Scales HUD Tier-1 font sizes to fit a narrow viewport. +/// +/// Fires on every `WindowResized` event. Below 480 logical pixels wide the +/// score drops from `TYPE_HEADLINE` (26 px) to `TYPE_BODY_LG` (18 px) and the +/// Moves/Timer labels drop from `TYPE_BODY_LG` to `TYPE_CAPTION` (11 px), so +/// all three items remain on one row inside the 50 %-wide HUD column +/// (≈ 180 dp on a 360 dp phone). At ≥ 480 px the original sizes are +/// restored so desktop/tablet layouts are unaffected. +fn update_hud_typography( + mut events: MessageReader, + mut score_q: Query< + &mut TextFont, + (With, Without, Without), + >, + mut moves_q: Query< + &mut TextFont, + (With, Without, Without), + >, + mut time_q: Query< + &mut TextFont, + (With, Without, Without), + >, +) { + let Some(ev) = events.read().last() else { + return; + }; + let (score_size, secondary_size) = if ev.width < 480.0 { + (TYPE_BODY_LG, TYPE_CAPTION) + } else { + (TYPE_HEADLINE, TYPE_BODY_LG) + }; + for mut font in &mut score_q { + font.font_size = score_size; + } + for mut font in &mut moves_q { + font.font_size = secondary_size; + } + for mut font in &mut time_q { + font.font_size = secondary_size; + } +} + #[cfg(test)] mod tests { use super::*;