From b57db017d34c089121b5650a7ed036d9b585cbdd Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 6 May 2026 19:49:52 -0700 Subject: [PATCH] feat(app): Wayland support + monitor-relative default window size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related platform-fit fixes for desktop launch: 1. Wayland session compatibility. The workspace Cargo.toml's Bevy feature list previously enabled only `x11`, leaving winit-on-Wayland to fall through to XWayland — the game rendered inside an X11 frame stitched into the Wayland compositor instead of as a native Wayland client. Adding the `wayland` feature lets winit prefer Wayland when WAYLAND_DISPLAY is set on the session, falling back to X11 when it isn't. Costs a few hundred KB of binary for the libwayland-client bindings; comment in Cargo.toml explains the trade. 2. Smart default window sizing. The fallback window size for first launches (no saved geometry) was a fixed 1280x800. On a 4K monitor that's a comparatively tiny window in one corner; the game's cards then occupy a small physical area even though the screen has plenty of room. New `apply_smart_default_window_size` Update system queries `Monitor` (with the `PrimaryMonitor` marker) and resizes the primary window to ~70% of the monitor's *logical* size on the first frame. Logical size already factors in the OS's HiDPI scale factor, so: - 1920x1080 / 1.0 scale → 1344x756 target - 2560x1440 / 1.0 scale → 1792x1008 target - 3840x2160 / 1.0 scale → 2688x1512 target - 2880x1800 / 2.0 scale (Retina) → 1008x630 target (same physical size as 1080p) Clamped to the existing 800x600 minimum so old systems don't get sub-minimum windows. Skipped entirely when saved geometry was applied — the player's chosen size always wins. Uses `Local` for one-shot semantics; the early- exit per tick costs nothing once `*applied` is true. Workspace: 1170 passing tests / 0 failing. cargo clippy --workspace --all-targets -- -D warnings clean. Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 247 ++++++++++++++++++++++++++++++++++++-- Cargo.toml | 7 +- solitaire_app/src/main.rs | 84 ++++++++++++- 3 files changed, 321 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e701d8..2af1fbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -719,7 +732,7 @@ dependencies = [ "cfg-if", "console_error_panic_hook", "ctrlc", - "downcast-rs", + "downcast-rs 2.0.2", "log", "thiserror 2.0.18", "variadics_please", @@ -753,7 +766,7 @@ dependencies = [ "crossbeam-channel", "derive_more", "disqualified", - "downcast-rs", + "downcast-rs 2.0.2", "either", "futures-io", "futures-lite", @@ -801,7 +814,7 @@ dependencies = [ "bevy_utils", "bevy_window", "derive_more", - "downcast-rs", + "downcast-rs 2.0.2", "serde", "smallvec", "thiserror 2.0.18", @@ -1200,7 +1213,7 @@ dependencies = [ "bevy_utils", "derive_more", "disqualified", - "downcast-rs", + "downcast-rs 2.0.2", "erased-serde", "foldhash 0.2.0", "glam 0.30.10", @@ -1259,7 +1272,7 @@ dependencies = [ "bitflags 2.11.1", "bytemuck", "derive_more", - "downcast-rs", + "downcast-rs 2.0.2", "encase", "fixedbitset", "glam 0.30.10", @@ -1854,6 +1867,18 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + [[package]] name = "cbc" version = "0.1.2" @@ -2709,6 +2734,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "downcast-rs" version = "2.0.2" @@ -5792,6 +5823,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -6165,7 +6205,7 @@ dependencies = [ "pico-args", "rgb", "svgtypes", - "tiny-skia", + "tiny-skia 0.12.0", "usvg", "zune-jpeg", ] @@ -6502,6 +6542,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia 0.11.4", +] + [[package]] name = "sec1" version = "0.7.3" @@ -6846,6 +6899,31 @@ dependencies = [ "serde", ] +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.11.1", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + [[package]] name = "smol_str" version = "0.2.2" @@ -6938,7 +7016,7 @@ dependencies = [ "solitaire_sync", "tempfile", "thiserror 2.0.18", - "tiny-skia", + "tiny-skia 0.12.0", "tokio", "usvg", "uuid", @@ -7525,7 +7603,7 @@ dependencies = [ "crc32fast", "crossbeam-channel", "datasketches", - "downcast-rs", + "downcast-rs 2.0.2", "fastdivide", "fnv", "fs4", @@ -7577,7 +7655,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c57166f5bcfd478f370ab8445afb4678dce44801fa5ce5c451aaf8595583c5dc" dependencies = [ - "downcast-rs", + "downcast-rs 2.0.2", "fastdivide", "itertools 0.14.0", "serde", @@ -7765,6 +7843,20 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path 0.11.4", +] + [[package]] name = "tiny-skia" version = "0.12.0" @@ -7777,7 +7869,18 @@ dependencies = [ "cfg-if", "log", "png 0.18.1", - "tiny-skia-path", + "tiny-skia-path 0.12.0", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", ] [[package]] @@ -8548,7 +8651,7 @@ dependencies = [ "siphasher", "strict-num", "svgtypes", - "tiny-skia-path", + "tiny-skia-path 0.12.0", "ttf-parser", "unicode-bidi", "unicode-script", @@ -8754,6 +8857,114 @@ dependencies = [ "semver", ] +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs 1.2.1", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.11.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.97" @@ -9569,6 +9780,7 @@ version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" dependencies = [ + "ahash", "android-activity", "atomic-waker", "bitflags 2.11.1", @@ -9583,6 +9795,7 @@ dependencies = [ "dpi", "js-sys", "libc", + "memmap2", "ndk", "objc2 0.5.2", "objc2-app-kit 0.2.2", @@ -9594,11 +9807,17 @@ dependencies = [ "raw-window-handle", "redox_syscall 0.4.1", "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", "smol_str", "tracing", "unicode-segmentation", "wasm-bindgen", "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", "web-sys", "web-time", "windows-sys 0.52.0", @@ -9766,6 +9985,12 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + [[package]] name = "xkbcommon-dl" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index aef152f..f29b90e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,11 +54,16 @@ bevy = { version = "0.18", default-features = false, features = [ "bevy_window", "custom_cursor", "reflect_auto_register", - # default_platform (desktop subset; no android/wayland/webgl/gilrs/sysinfo) + # default_platform (desktop subset; no android/webgl/gilrs/sysinfo) "std", "bevy_winit", "default_font", "multi_threaded", + # winit prefers Wayland when WAYLAND_DISPLAY is set on the + # session and falls through to X11 otherwise. Without `wayland`, + # winit-on-Wayland-session falls back to XWayland which renders + # the game in an X11 frame inside the Wayland compositor. + "wayland", "x11", # common_api "bevy_color", diff --git a/solitaire_app/src/main.rs b/solitaire_app/src/main.rs index 9e4cc8e..ef9bee4 100644 --- a/solitaire_app/src/main.rs +++ b/solitaire_app/src/main.rs @@ -3,7 +3,9 @@ use std::io::Write; use std::time::{SystemTime, UNIX_EPOCH}; use bevy::prelude::*; -use bevy::window::{MonitorSelection, PresentMode, WindowPosition}; +use bevy::window::{ + Monitor, MonitorSelection, PresentMode, PrimaryMonitor, PrimaryWindow, WindowPosition, +}; use solitaire_data::{load_settings_from, provider_for_backend, settings_file_path, Settings}; use solitaire_engine::{ register_theme_asset_sources, AchievementPlugin, AnimationPlugin, AssetSourcesPlugin, @@ -43,8 +45,10 @@ fn main() { // Restore the previous window geometry if the player has one saved. // Otherwise open at the platform default (1280×800, centred on the - // primary monitor). The window_geometry field is None on first run - // and after upgrading from a build that didn't persist geometry. + // primary monitor) — `apply_smart_default_window_size` will resize + // up to a monitor-relative target on the first frame so HiDPI / 4K + // sessions don't end up with a comparatively tiny window. + let had_saved_geometry = settings.window_geometry.is_some(); let (window_resolution, window_position) = match settings.window_geometry { Some(geom) => ( (geom.width, geom.height).into(), @@ -141,8 +145,78 @@ fn main() { .add_plugins(UiModalPlugin) .add_plugins(UiFocusPlugin) .add_plugins(UiTooltipPlugin) - .add_plugins(SplashPlugin) - .run(); + .add_plugins(SplashPlugin); + + // Smart default window sizing: when no saved geometry was loaded, + // resize the freshly-opened 1280×800 window to ~70 % of the primary + // monitor's logical size on the first frame. Without this, a 4K + // monitor opens the same 1280×800 window that a 1080p monitor + // does — visually tiny relative to screen. Skipped entirely when + // saved geometry was applied; the player's preference always wins. + if !had_saved_geometry { + app.add_systems(Update, apply_smart_default_window_size); + } + + app.run(); +} + +/// One-shot Update system that runs only on launches without saved +/// window geometry. Resizes the primary window to a fraction of the +/// primary monitor's *logical* size — bigger monitors get bigger +/// windows automatically. Logical size already accounts for the OS's +/// HiDPI scale factor, so a 2880×1800 Retina display reporting +/// scale_factor 2.0 yields a 1440×900 logical size and a 1008×630 +/// target window — same physical inches as a 1920×1080 monitor with +/// scale_factor 1.0 yielding 1344×756. +/// +/// Uses `Local` to make itself one-shot rather than introducing +/// a dedicated resource. The Update tick is necessary because Bevy +/// populates the `Monitor` entities asynchronously after winit's +/// Resumed event fires; they may not exist on the first Startup pass. +fn apply_smart_default_window_size( + mut applied: Local, + monitors: Query<&Monitor, With>, + mut windows: Query<&mut Window, With>, +) { + if *applied { + return; + } + let Ok(monitor) = monitors.single() else { + // Primary monitor not yet spawned by bevy_winit. Try again + // next frame; the cost is one early-exit per tick until + // monitors arrive (typically frame 1 or 2). + return; + }; + let Ok(mut window) = windows.single_mut() else { + return; + }; + + let scale = monitor.scale_factor as f32; + if scale <= 0.0 { + // Defensive: a zero or negative scale factor would NaN the + // arithmetic below. Bail and accept the default size. + *applied = true; + return; + } + let logical_w = monitor.physical_width as f32 / scale; + let logical_h = monitor.physical_height as f32 / scale; + + // Target 70 % of monitor in each dimension, clamped to the + // existing 800×600 minimum and the monitor's own logical size + // (so we never request a window larger than the screen). + let target_w = (logical_w * 0.7).clamp(800.0, logical_w); + let target_h = (logical_h * 0.7).clamp(600.0, logical_h); + + // Resize only when the change is meaningful — at exactly 1280×800 + // on a 1920×1080 monitor the new target is 1344×756 (only ~5 % + // wider), worth the resize; at the same default on an 800×600 + // monitor the clamp pins us at 800×600 and we shouldn't resize. + let curr_w = window.resolution.width(); + let curr_h = window.resolution.height(); + if (curr_w - target_w).abs() > 8.0 || (curr_h - target_h).abs() > 8.0 { + window.resolution.set(target_w, target_h); + } + *applied = true; } /// Wraps the default panic hook with one that also appends a crash log