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
+6 -1
View File
@@ -153,7 +153,12 @@ fn main() {
// monitor opens the same 1280×800 window that a 1080p monitor // monitor opens the same 1280×800 window that a 1080p monitor
// does — visually tiny relative to screen. Skipped entirely when // does — visually tiny relative to screen. Skipped entirely when
// saved geometry was applied; the player's preference always wins. // saved geometry was applied; the player's preference always wins.
if !had_saved_geometry { //
// Players who specifically want the literal 1280×800 baseline on
// every fresh launch can flip `disable_smart_default_size` in
// Settings to opt out. The flag is checked once at startup; a
// mid-session change applies on the next launch.
if !had_saved_geometry && !settings.disable_smart_default_size {
app.add_systems(Update, apply_smart_default_window_size); app.add_systems(Update, apply_smart_default_window_size);
} }
+15
View File
@@ -181,6 +181,20 @@ pub struct Settings {
/// solver retry loop — see `solitaire_engine::handle_new_game`. /// solver retry loop — see `solitaire_engine::handle_new_game`.
#[serde(default)] #[serde(default)]
pub winnable_deals_only: bool, pub winnable_deals_only: bool,
/// When `true`, suppresses the launch-time
/// `apply_smart_default_window_size` system: the window opens at
/// the literal `(1280, 800)` default instead of resizing to ~70 %
/// of the primary monitor's logical size on the first frame. For
/// players who specifically prefer the 1280×800 baseline on every
/// fresh launch (i.e. installs without saved geometry).
///
/// Older `settings.json` files written before this field existed
/// deserialize cleanly to `false` via `#[serde(default)]`, which
/// preserves the smart-default behaviour shipped in v0.19.0.
/// Saved-geometry launches are unaffected by this flag — the
/// player's last window size always wins.
#[serde(default)]
pub disable_smart_default_size: bool,
/// Per-move duration during replay playback, in seconds. Range /// Per-move duration during replay playback, in seconds. Range
/// `[REPLAY_MOVE_INTERVAL_MIN_SECS, REPLAY_MOVE_INTERVAL_MAX_SECS]`; /// `[REPLAY_MOVE_INTERVAL_MIN_SECS, REPLAY_MOVE_INTERVAL_MAX_SECS]`;
/// default mirrors `solitaire_engine::replay_playback::REPLAY_MOVE_INTERVAL_SECS` /// default mirrors `solitaire_engine::replay_playback::REPLAY_MOVE_INTERVAL_SECS`
@@ -306,6 +320,7 @@ impl Default for Settings {
tooltip_delay_secs: default_tooltip_delay(), tooltip_delay_secs: default_tooltip_delay(),
time_bonus_multiplier: default_time_bonus_multiplier(), time_bonus_multiplier: default_time_bonus_multiplier(),
winnable_deals_only: false, winnable_deals_only: false,
disable_smart_default_size: false,
replay_move_interval_secs: default_replay_move_interval_secs(), replay_move_interval_secs: default_replay_move_interval_secs(),
} }
} }
+62
View File
@@ -144,6 +144,13 @@ struct ReplayMoveIntervalText;
#[derive(Component, Debug)] #[derive(Component, Debug)]
struct WinnableDealsOnlyText; 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. /// Marks the scrollable inner card so the mouse-wheel system can target it.
#[derive(Component, Debug)] #[derive(Component, Debug)]
struct SettingsPanelScrollable; struct SettingsPanelScrollable;
@@ -199,6 +206,13 @@ enum SettingsButton {
/// [`solitaire_core::solver::try_solve`] until one is provably /// [`solitaire_core::solver::try_solve`] until one is provably
/// winnable (or the retry cap is hit). Off by default. /// winnable (or the retry cap is hit). Off by default.
ToggleWinnableDealsOnly, 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, SyncNow,
Done, Done,
/// Select a specific card-back by index from the picker row. /// 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. // sits between TimeBonusUp (48) and the Cosmetic section.
SettingsButton::ReplayMoveIntervalDown => 49, SettingsButton::ReplayMoveIntervalDown => 49,
SettingsButton::ReplayMoveIntervalUp => 49, SettingsButton::ReplayMoveIntervalUp => 49,
// Smart-default-size toggle — sits at the end of Gameplay.
SettingsButton::ToggleSmartDefaultSize => 50,
// Cosmetic section // Cosmetic section
SettingsButton::ToggleTheme => 55, SettingsButton::ToggleTheme => 55,
SettingsButton::ToggleColorBlind => 60, SettingsButton::ToggleColorBlind => 60,
@@ -330,6 +346,7 @@ impl Plugin for SettingsPlugin {
update_time_bonus_multiplier_text, update_time_bonus_multiplier_text,
update_replay_move_interval_text, update_replay_move_interval_text,
update_winnable_deals_only_text, update_winnable_deals_only_text,
update_smart_default_size_text,
attach_focusable_to_settings_buttons, attach_focusable_to_settings_buttons,
scroll_focus_into_view, 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 /// Refreshes the live tooltip-delay value in the Gameplay section
/// whenever `SettingsResource` changes (slider buttons, hand-edited /// whenever `SettingsResource` changes (slider buttons, hand-edited
/// settings.json reload, etc.). /// settings.json reload, etc.).
@@ -854,6 +886,17 @@ fn handle_settings_buttons(
// The Text node is refreshed by `update_winnable_deals_only_text` // The Text node is refreshed by `update_winnable_deals_only_text`
// on the next frame via `settings.is_changed()`. // 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) => { SettingsButton::SelectCardBack(idx) => {
settings.0.selected_card_back = *idx; settings.0.selected_card_back = *idx;
persist(&path, &settings.0); persist(&path, &settings.0);
@@ -915,6 +958,14 @@ fn winnable_deals_only_label(enabled: bool) -> String {
if enabled { "ON".into() } else { "OFF".into() } 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. /// 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 /// `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"`). /// 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, settings.replay_move_interval_secs,
font_res, 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 --- // --- Cosmetic ---
section_label(body, "Cosmetic", font_res); section_label(body, "Cosmetic", font_res);