fix(client/windows): left-align setting captions, drop the width cap, add auto-wake

Three follow-ups to the parity sweep:

- Captions read centred: a TextBlock defaults to Stretch alignment, so the
  MaxWidth cap left it floating in the middle of the leftover width instead
  of sitting under its control. Pin them (and the footers) Left.
- The settings column kept the shared page() 640px cap, which -- next to the
  NavigationView pane spending the left third -- squeezed the cards into a
  narrow ribbon. Drop the cap here.
- Auto-wake on connect (the Apple client's toggle) is now a setting rather
  than unconditional behavior. Default on, which is what it did before.
  Gating the flow's ENTRY, not its packet sends: with it off a failed dial
  just fails, instead of showing a "Waking..." wait that would never send a
  packet to end itself. The explicit "Wake" host-card menu action is
  deliberately NOT gated -- that is the user asking directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 17:07:31 +02:00
parent 72583b272e
commit 7a1809547b
3 changed files with 48 additions and 11 deletions
+9 -3
View File
@@ -36,7 +36,9 @@ pub(crate) fn initiate_waking(
set_screen: &AsyncSetState<Screen>,
set_status: &AsyncSetState<String>,
) {
crate::wol::wake(&target.mac, target.addr.parse().ok());
if ctx.settings.lock().unwrap().auto_wake {
crate::wol::wake(&target.mac, target.addr.parse().ok());
}
initiate_opts(ctx, target, set_screen, set_status, true)
}
@@ -291,9 +293,13 @@ fn connect_spawn(
*shared.target.lock().unwrap() = target.clone();
ss.call(Screen::Pair);
}
Some((_, false)) if wake_on_fail => {
Some((_, false))
if wake_on_fail && ctx2.settings.lock().unwrap().auto_wake =>
{
// The dial-first attempt to a non-advertising host failed — it
// may genuinely be asleep. NOW wake and wait.
// may genuinely be asleep. NOW wake and wait. Skipped entirely
// when auto-wake is off: the wait is only worth showing if we
// are actually sending magic packets to end it.
wake_and_connect(&ctx2, target.clone(), &ss, &st);
}
Some((msg, false)) => {
+31 -8
View File
@@ -162,7 +162,10 @@ fn described(control: impl Into<Element>, caption: &str) -> Element {
.font_size(12.0)
.foreground(ThemeRef::SecondaryText)
.wrap()
.max_width(420.0),
.max_width(420.0)
// Stretch (the TextBlock default) CENTRES a MaxWidth-capped block in the leftover
// width — the caption must be pinned left or it drifts away from its control.
.horizontal_alignment(HorizontalAlignment::Left),
))
.spacing(5.0)
.into()
@@ -182,6 +185,7 @@ fn group(header: Option<&str>, fields: Vec<Element>, footer: Option<&str>) -> Ve
.font_size(12.0)
.foreground(ThemeRef::SecondaryText)
.wrap()
.horizontal_alignment(HorizontalAlignment::Left)
.margin(edges(2.0, 6.0, 0.0, 0.0))
.into(),
);
@@ -267,6 +271,9 @@ pub(crate) fn settings_page(
let comp_combo = setting_combo(ctx, "Host compositor", comp_names, comp_i, |s, i| {
s.compositor = COMPOSITORS[i].0.to_string();
});
let auto_wake_toggle = setting_toggle(ctx, "Auto-wake on connect", s.auto_wake, |s, on| {
s.auto_wake = on
});
let fullscreen_toggle = setting_toggle(
ctx,
"Start streams fullscreen",
@@ -577,10 +584,19 @@ pub(crate) fn settings_page(
_ => {
let mut out = group(
Some("Session"),
vec![described(
fullscreen_toggle,
"Go fullscreen when a session starts; F11 or Alt+Enter switches back live.",
)],
vec![
described(
fullscreen_toggle,
"Go fullscreen when a session starts; F11 or Alt+Enter switches back \
live.",
),
described(
auto_wake_toggle,
"Connecting to a saved host that\u{2019}s offline sends Wake-on-LAN and \
waits for it to boot. Turn off if hosts behind a VPN look offline when \
they aren\u{2019}t.",
),
],
None,
);
out.extend(group(
@@ -634,9 +650,16 @@ pub(crate) fn settings_page(
//
// The content column (not the NavigationView — the sidebar must stay put) carries the
// section-switch entrance: fade + slide-up from the root-driven tween.
let content = page(vec![vstack(groups).spacing(10.0).with_key(section).into()])
.opacity(progress)
.margin(edges(0.0, (1.0 - progress) * 22.0, 0.0, 0.0));
// No max-width cap here (unlike the other pages): the NavigationView already spends the
// left third on its pane, so a 640-wide column left the cards as a narrow ribbon.
let content = scroll_view(
vstack(groups)
.spacing(10.0)
.margin(edges(24.0, 20.0, 28.0, 40.0))
.with_key(section),
)
.opacity(progress)
.margin(edges(0.0, (1.0 - progress) * 22.0, 0.0, 0.0));
NavigationView::new(items, content)
.pane_title("Settings")
.header(title)
+8
View File
@@ -526,6 +526,13 @@ pub struct Settings {
/// Experimental: the game-library browser ("Browse library…" on saved cards) —
/// mirrors the Apple client's "Show game library" toggle, default off.
pub library_enabled: bool,
/// Send Wake-on-LAN before connecting to a saved host and wait for it to boot (the
/// Apple client's "Auto-wake on connect"). Default ON — that was the unconditional
/// behavior before this became a setting. Off is for hosts reached over a VPN, where
/// an offline-looking host is really just unreachable by broadcast and the wake +
/// wait only adds a delay.
#[serde(default = "default_true")]
pub auto_wake: bool,
/// Match-window resolution policy (design/midstream-resolution-resize.md D1): the
/// stream mode follows the session window — the connect asks for the window's pixel
/// size and a mid-session resize renegotiates the host's virtual display + encoder
@@ -614,6 +621,7 @@ impl Default for Settings {
stats_verbosity: None,
fullscreen_on_stream: true,
library_enabled: false,
auto_wake: true,
match_window: false,
last_window_w: 0,
last_window_h: 0,