feat(settings): "Smart window size" toggle to opt out of monitor-relative

launch sizing

Players who specifically prefer the literal 1280×800 baseline on
every fresh-install launch had no way to opt out of the v0.19.0
smart-default sizer. Adds a Gameplay-section toggle (mirrors the
"Winnable deals only" pattern) so they can flip it off.

- New `Settings::disable_smart_default_size: bool` field with
  `#[serde(default)]` so legacy `settings.json` files load to the
  shipped behaviour (smart sizer enabled).
- Settings panel gains a "Smart window size" row with ON/OFF label
  inverting the negative flag, and a tooltip clarifying that saved
  window geometry always wins over both branches.
- `solitaire_app::main` reads the flag once at startup and skips
  the `apply_smart_default_window_size` registration when it's set.
  Mid-session changes apply on next launch (documented on the
  field).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-05-07 04:00:43 +00:00
parent 67c150bd7b
commit e1b8766e15
3 changed files with 83 additions and 1 deletions
+62
View File
@@ -144,6 +144,13 @@ struct ReplayMoveIntervalText;
#[derive(Component, Debug)]
struct WinnableDealsOnlyText;
/// Marks the `Text` node showing the current "Smart window size"
/// state ("ON" / "OFF") in the Gameplay section. The flag is stored
/// negatively in `Settings::disable_smart_default_size`, so the
/// label inverts: "ON" = smart sizing enabled (the default).
#[derive(Component, Debug)]
struct SmartDefaultSizeText;
/// Marks the scrollable inner card so the mouse-wheel system can target it.
#[derive(Component, Debug)]
struct SettingsPanelScrollable;
@@ -199,6 +206,13 @@ enum SettingsButton {
/// [`solitaire_core::solver::try_solve`] until one is provably
/// winnable (or the retry cap is hit). Off by default.
ToggleWinnableDealsOnly,
/// Toggle the inverse of [`Settings::disable_smart_default_size`].
/// When the visible label reads "ON", the launch-time window
/// sizer scales the window to ~70 % of the primary monitor on a
/// fresh install; "OFF" pins the literal 1280×800 baseline. The
/// flag only affects launches without saved geometry — the
/// player's last window size always wins.
ToggleSmartDefaultSize,
SyncNow,
Done,
/// Select a specific card-back by index from the picker row.
@@ -236,6 +250,8 @@ impl SettingsButton {
// sits between TimeBonusUp (48) and the Cosmetic section.
SettingsButton::ReplayMoveIntervalDown => 49,
SettingsButton::ReplayMoveIntervalUp => 49,
// Smart-default-size toggle — sits at the end of Gameplay.
SettingsButton::ToggleSmartDefaultSize => 50,
// Cosmetic section
SettingsButton::ToggleTheme => 55,
SettingsButton::ToggleColorBlind => 60,
@@ -330,6 +346,7 @@ impl Plugin for SettingsPlugin {
update_time_bonus_multiplier_text,
update_replay_move_interval_text,
update_winnable_deals_only_text,
update_smart_default_size_text,
attach_focusable_to_settings_buttons,
scroll_focus_into_view,
),
@@ -600,6 +617,21 @@ fn update_winnable_deals_only_text(
}
}
/// Refreshes the live "Smart window size" toggle value whenever
/// `SettingsResource` changes. The flag is stored negatively as
/// `disable_smart_default_size`, so the label inverts.
fn update_smart_default_size_text(
settings: Res<SettingsResource>,
mut text_nodes: Query<&mut Text, With<SmartDefaultSizeText>>,
) {
if !settings.is_changed() {
return;
}
for mut text in &mut text_nodes {
**text = smart_default_size_label(!settings.0.disable_smart_default_size);
}
}
/// Refreshes the live tooltip-delay value in the Gameplay section
/// whenever `SettingsResource` changes (slider buttons, hand-edited
/// settings.json reload, etc.).
@@ -854,6 +886,17 @@ fn handle_settings_buttons(
// The Text node is refreshed by `update_winnable_deals_only_text`
// on the next frame via `settings.is_changed()`.
}
SettingsButton::ToggleSmartDefaultSize => {
settings.0.disable_smart_default_size =
!settings.0.disable_smart_default_size;
persist(&path, &settings.0);
changed.write(SettingsChangedEvent(settings.0.clone()));
// The Text node is refreshed by
// `update_smart_default_size_text` next frame. The
// sizer system is gated only at startup, so flipping
// this mid-session takes effect on the next launch —
// documented on the field in `solitaire_data::Settings`.
}
SettingsButton::SelectCardBack(idx) => {
settings.0.selected_card_back = *idx;
persist(&path, &settings.0);
@@ -915,6 +958,14 @@ fn winnable_deals_only_label(enabled: bool) -> String {
if enabled { "ON".into() } else { "OFF".into() }
}
/// Display string for the "Smart window size" toggle. The argument
/// is the *enabled* state (i.e. the inverse of the underlying
/// `disable_smart_default_size` field) so reading the label gives
/// the player intuitive ON/OFF semantics.
fn smart_default_size_label(enabled: bool) -> String {
if enabled { "ON".into() } else { "OFF".into() }
}
/// Formats the tooltip-hover delay for display in the Settings panel.
/// `0.0` reads as `"Instant"` so the zero-delay case has a name; any
/// other value prints as `"{n:.1} s"` (e.g. `"0.5 s"`, `"1.2 s"`).
@@ -1303,6 +1354,17 @@ fn spawn_settings_panel(
settings.replay_move_interval_secs,
font_res,
);
toggle_row(
body,
"Smart window size",
SmartDefaultSizeText,
smart_default_size_label(!settings.disable_smart_default_size),
SettingsButton::ToggleSmartDefaultSize,
"When ON, fresh launches resize the window to ~70 % of the \
monitor. OFF pins the 1280\u{00D7}800 baseline. Saved \
window size always wins.",
font_res,
);
// --- Cosmetic ---
section_label(body, "Cosmetic", font_res);