fix(android): lower MIN_WINDOW floor so phone viewports lay out correctly
`compute_layout` runs `window.max(MIN_WINDOW)`, which acts as a component-wise floor: any window smaller than MIN_WINDOW on either axis gets clamped up. The previous floor of 800x600 was set with desktop in mind, but on Android the OS-provided window size is the device resolution (~360 dp wide on a typical phone) and the clamp silently re-laid the board for an 800 dp width. Side effect: total grid width (9 * card_width) became ~800 px on a 360 dp viewport, so the leftmost foundation x-position fell past -180 and the rightmost tableau pile past +180 — both clipped at the visible edges, matching the v0.22.3 hardware screenshot. Lowered MIN_WINDOW to 320x400, below the smallest reasonable phone (~360x640), so every real device flows through compute_layout unclamped. The floor is preserved as a sentinel against degenerate windows (Bevy can briefly report 0-size during startup or after minimisation on some compositors). Desktop's "minimum supported playable size" is enforced separately via WindowResizeConstraints in solitaire_app. Updates `layout_below_minimum_clamps_to_minimum` to use values below the new floor, and adds a new regression test `phone_portrait_layout_fits_horizontally` that asserts all 13 piles fit inside a 360 x 800 dp viewport. Closes P0 #4 of docs/android/PLAYABILITY_TODO.md. 855 engine tests pass; clippy clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,10 +54,15 @@ rewrites required.
|
|||||||
failed silently. The face-down branch then fell through to the
|
failed silently. The face-down branch then fell through to the
|
||||||
`card_back_colour(0)` solid-red brick fallback. Gated the
|
`card_back_colour(0)` solid-red brick fallback. Gated the
|
||||||
override behind `#[cfg(not(target_os = "android"))]`.
|
override behind `#[cfg(not(target_os = "android"))]`.
|
||||||
- [ ] **Viewport overflow.** Leftmost foundation and rightmost tableau
|
- [x] **Viewport overflow.** *Closed 2026-05-10.* `compute_layout`
|
||||||
pile clipped. `LayoutResource` must recompute on Android using
|
was clamping the input window up to `MIN_WINDOW = 800 × 600`,
|
||||||
actual surface size (post-inset) instead of any desktop default
|
so a 360 dp phone got laid out as if it were 800-wide and the
|
||||||
width assumption.
|
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 test `phone_portrait_layout_fits_horizontally`
|
||||||
|
asserts all 13 piles fit a 360 × 800 viewport.
|
||||||
|
|
||||||
## P1 — Touch UX
|
## P1 — Touch UX
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,25 @@ pub enum LayoutSystem {
|
|||||||
UpdateOnResize,
|
UpdateOnResize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minimum supported window dimensions. Layout is still computed below this
|
/// Minimum window dimensions used as a layout floor.
|
||||||
/// size but cards will be small.
|
///
|
||||||
pub const MIN_WINDOW: Vec2 = Vec2::new(800.0, 600.0);
|
/// `compute_layout` runs `window.max(MIN_WINDOW)` so a window smaller than this
|
||||||
|
/// on either axis is laid out as if it were at least this size. The floor
|
||||||
|
/// exists to guard against degenerate / divide-by-zero layouts on very small
|
||||||
|
/// surfaces (Bevy can briefly report 0-size windows during startup or after
|
||||||
|
/// minimisation on some compositors); it is not a "minimum supported playable
|
||||||
|
/// size" — desktop builds enforce that via `WindowResizeConstraints` set in
|
||||||
|
/// `solitaire_app::lib`.
|
||||||
|
///
|
||||||
|
/// The previous floor of 800×600 was set with desktop in mind and produced
|
||||||
|
/// the wrong behaviour on Android: a 360 dp phone got laid out as if it were
|
||||||
|
/// 800-wide, pushing the leftmost foundation past `-180` and the rightmost
|
||||||
|
/// tableau pile past `+180`, which clipped both at the visible viewport
|
||||||
|
/// edges (visible in the v0.22.3 hardware screenshot). 320×400 is below the
|
||||||
|
/// smallest reasonable phone (≈ 360×640) so every real device flows through
|
||||||
|
/// without clamping, while still being large enough that the layout math
|
||||||
|
/// produces non-degenerate card sizes.
|
||||||
|
pub const MIN_WINDOW: Vec2 = Vec2::new(320.0, 400.0);
|
||||||
|
|
||||||
/// Aspect ratio (height / width) of a standard playing card.
|
/// Aspect ratio (height / width) of a standard playing card.
|
||||||
///
|
///
|
||||||
@@ -205,11 +221,39 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn layout_below_minimum_clamps_to_minimum() {
|
fn layout_below_minimum_clamps_to_minimum() {
|
||||||
let below = compute_layout(Vec2::new(400.0, 300.0));
|
// 200×200 sits below the floor on both axes, so the clamp pulls each
|
||||||
|
// axis up to MIN_WINDOW and the layout matches compute_layout(MIN_WINDOW).
|
||||||
|
let below = compute_layout(Vec2::new(200.0, 200.0));
|
||||||
let at_min = compute_layout(MIN_WINDOW);
|
let at_min = compute_layout(MIN_WINDOW);
|
||||||
assert_eq!(below.card_size, at_min.card_size);
|
assert_eq!(below.card_size, at_min.card_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Regression for the v0.22.3 Android viewport-overflow bug. A typical
|
||||||
|
/// portrait-phone viewport (360 dp × 800 dp) must produce a layout
|
||||||
|
/// where every pile fits horizontally — i.e. card_width is derived
|
||||||
|
/// from the actual window, not a clamped-up desktop floor.
|
||||||
|
#[test]
|
||||||
|
fn phone_portrait_layout_fits_horizontally() {
|
||||||
|
let window = Vec2::new(360.0, 800.0);
|
||||||
|
let layout = compute_layout(window);
|
||||||
|
let half_w = window.x / 2.0;
|
||||||
|
let half_card = layout.card_size.x / 2.0;
|
||||||
|
for (pile, pos) in &layout.pile_positions {
|
||||||
|
assert!(
|
||||||
|
pos.x - half_card >= -half_w - 1e-3,
|
||||||
|
"{:?} overflows left at portrait phone window {:?}",
|
||||||
|
pile,
|
||||||
|
window
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pos.x + half_card <= half_w + 1e-3,
|
||||||
|
"{:?} overflows right at portrait phone window {:?}",
|
||||||
|
pile,
|
||||||
|
window
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tableau_columns_are_sorted_left_to_right() {
|
fn tableau_columns_are_sorted_left_to_right() {
|
||||||
let layout = compute_layout(Vec2::new(1280.0, 800.0));
|
let layout = compute_layout(Vec2::new(1280.0, 800.0));
|
||||||
|
|||||||
Reference in New Issue
Block a user