fix(engine): fill tableau fan to viewport on all aspect ratios
On a near-square viewport (e.g. an unfolded Galaxy Fold) a fresh deal left the bottom ~40% of the screen empty: the dynamic fan (update_tableau_fan_frac) measured only face-up column depth and returned early at a fresh deal (face-up depth 1), so it never spread the tableau, and the deep face-down stacks were ignored. The cold-start deal also never fired StateChangedEvent, and on Android the safe-area-inset resize (frames 1-3) reset the fan to compute_layout's sparse worst-case value, so even the post-move fill was wiped. Move the fill into layout::apply_dynamic_tableau_fan, driven by each column's TOTAL weighted depth (face-down cards count, scaled by the face-down/face-up step ratio) so the deepest column fills the available height. Run it in three places so every path stays filled: PostStartup (cold-start deal), on StateChangedEvent (moves), and inside on_window_resized after compute_layout (safe-area resize + fold/unfold). MAX_DYNAMIC_FAN_FRAC caps the spread so a near-empty column keeps readable overlap; TABLEAU_FAN_FRAC floors it. Deeper columns drive the fraction down so everything still fits — no overflow. card_position/card_positions read the same fractions, so hit-testing stays in sync. Adds regression tests: cold-start deal fills the fan, and a resize re-fills it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,9 @@ use solitaire_core::Suit;
|
||||
|
||||
use crate::events::{HintVisualEvent, StateChangedEvent};
|
||||
use crate::hud_plugin::HudVisibility;
|
||||
use crate::layout::{Layout, LayoutResource, LayoutSystem, TABLE_COLOUR, compute_layout};
|
||||
use crate::layout::{
|
||||
Layout, LayoutResource, LayoutSystem, TABLE_COLOUR, apply_dynamic_tableau_fan, compute_layout,
|
||||
};
|
||||
use crate::resources::GameStateResource;
|
||||
use crate::safe_area::SafeAreaInsets;
|
||||
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
|
||||
@@ -348,12 +350,13 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||
fn on_window_resized(
|
||||
mut events: MessageReader<WindowResized>,
|
||||
safe_area: Option<Res<SafeAreaInsets>>,
|
||||
windows: Query<&Window>,
|
||||
hud_vis: Option<Res<HudVisibility>>,
|
||||
game: Option<Res<GameStateResource>>,
|
||||
mut layout_res: Option<ResMut<LayoutResource>>,
|
||||
mut backgrounds: Query<
|
||||
(&mut Sprite, &mut Transform),
|
||||
@@ -370,7 +373,16 @@ fn on_window_resized(
|
||||
let safe_area_top = insets.top / scale;
|
||||
let safe_area_bottom = insets.bottom / scale;
|
||||
let hud_visible = hud_vis.as_deref().copied().unwrap_or_default() == HudVisibility::Visible;
|
||||
let new_layout = compute_layout(window_size, safe_area_top, safe_area_bottom, hud_visible);
|
||||
let mut new_layout = compute_layout(window_size, safe_area_top, safe_area_bottom, hud_visible);
|
||||
|
||||
// compute_layout sizes the fan for a worst-case column; refine it to the
|
||||
// current deal so a resize (incl. the Android safe-area-inset resize that
|
||||
// fires in the first few frames, and fold/unfold on foldables) keeps the
|
||||
// tableau filling the viewport instead of snapping back to the sparse
|
||||
// worst-case fan.
|
||||
if let Some(game) = game.as_ref() {
|
||||
apply_dynamic_tableau_fan(&game.0, &mut new_layout);
|
||||
}
|
||||
|
||||
if let Some(layout_res) = layout_res.as_deref_mut() {
|
||||
layout_res.0 = new_layout.clone();
|
||||
|
||||
Reference in New Issue
Block a user