fix(android): responsive HUD typography + portrait orientation lock

Closes the final two P2 Android playability items:

1. HUD typography — new `update_hud_typography` system fires on
   `WindowResized` and adjusts Tier-1 font sizes: below 480 logical px
   Score drops HEADLINE(26)→BODY_LG(18) and Moves/Timer drop
   BODY_LG(18)→CAPTION(11), so all three fit in the 180dp HUD column
   on a 360dp phone without wrapping.

2. Orientation lock — `[package.metadata.android.application.activity]`
   with `orientation = "portrait"` in solitaire_app/Cargo.toml; cargo-apk
   maps this to `android:screenOrientation="portrait"` in the generated
   AndroidManifest.xml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-11 13:44:26 -07:00
parent 948864e653
commit 016fb7214d
3 changed files with 68 additions and 4 deletions
+15 -4
View File
@@ -135,10 +135,21 @@ rewrites required.
the 0.5 s threshold fires). the 0.5 s threshold fires).
Hardware verification needed: confirm the 0.5 s hold feel, verify Hardware verification needed: confirm the 0.5 s hold feel, verify
sliding to a destination and lifting confirms the move. sliding to a destination and lifting confirms the move.
- [ ] **HUD typography.** Reduce text sizes for `Score:`, `Moves:`, - [x] **HUD typography.** *Closed 2026-05-11.* New system
timer so they fit cleanly in one row. `update_hud_typography` fires on `WindowResized` and adjusts Tier-1
- [ ] **Orientation lock.** Set `android:screenOrientation="portrait"` font sizes based on viewport width. Below 480 logical px: Score
in cargo-apk manifest (or design a landscape layout). `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::<WindowResized>()` 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 ## P3 — Asset density
+6
View File
@@ -82,3 +82,9 @@ label = "Solitaire Quest"
# `debuggable` defaults to false on release builds; cargo-apk flips it # `debuggable` defaults to false on release builds; cargo-apk flips it
# automatically for debug profiles. Leaving the field unset keeps the # automatically for debug profiles. Leaving the field unset keeps the
# default behaviour. # 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"
+47
View File
@@ -7,6 +7,7 @@
//! without a separate tick system. //! without a separate tick system.
use bevy::prelude::*; use bevy::prelude::*;
use bevy::window::WindowResized;
use solitaire_core::card::Suit; use solitaire_core::card::Suit;
use solitaire_core::game_state::{DrawMode, GameMode}; use solitaire_core::game_state::{DrawMode, GameMode};
use solitaire_core::pile::PileType; use solitaire_core::pile::PileType;
@@ -324,11 +325,15 @@ impl Plugin for HudPlugin {
.add_message::<WinStreakMilestoneEvent>() .add_message::<WinStreakMilestoneEvent>()
.init_resource::<PreviousScore>() .init_resource::<PreviousScore>()
.init_resource::<HudActionFade>() .init_resource::<HudActionFade>()
// WindowResized is registered by table_plugin; re-register
// defensively so the HUD plugin works standalone in tests.
.add_message::<WindowResized>()
.add_systems(Startup, (spawn_hud_band, spawn_hud, spawn_action_buttons)) .add_systems(Startup, (spawn_hud_band, spawn_hud, spawn_action_buttons))
.add_systems(Update, update_hud.after(GameMutation)) .add_systems(Update, update_hud.after(GameMutation))
.add_systems(Update, update_won_previously.after(GameMutation)) .add_systems(Update, update_won_previously.after(GameMutation))
.add_systems(Update, announce_auto_complete.after(GameMutation)) .add_systems(Update, announce_auto_complete.after(GameMutation))
.add_systems(Update, update_selection_hud) .add_systems(Update, update_selection_hud)
.add_systems(Update, update_hud_typography)
.add_systems( .add_systems(
Update, 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<WindowResized>,
mut score_q: Query<
&mut TextFont,
(With<HudScore>, Without<HudMoves>, Without<HudTime>),
>,
mut moves_q: Query<
&mut TextFont,
(With<HudMoves>, Without<HudScore>, Without<HudTime>),
>,
mut time_q: Query<
&mut TextFont,
(With<HudTime>, Without<HudScore>, Without<HudMoves>),
>,
) {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;