From e8eb803a3f1ac1ca12940d518154da6b18238800 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 29 Jul 2026 14:33:25 +0200 Subject: [PATCH] feat(client/windows): 4:4:4 reaches Windows, and the Overridden marker stops moving the page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GTK client grew its "Full chroma (4:4:4)" switch in the same commit that fixed its override marker; Windows got neither until now. The row lands in the Quality group under HDR with the GTK wording verbatim (same setting, same constraints), a full overridable profile row — the core overlay and the session binary already spoke `enable_444`, so the shell was the only gap. OverrideFlags grows the matching flag, and a unit test now pins the whole overlay -> row-flag mapping (tri-state resolution included): a field that records without marking its row is the original Overridden-row bug wearing a new face. And the marker obeys the GTK rule learned the hard way there: nothing that appears on an edit may move what was edited. Both states of a profile-scope row reserve the same caption-line height, so the marker + Reset materialise in place instead of shoving every row below them down by a button's height. Scope-blind audit (the bitrate-box failure class): clean. Decoder and GPU are hidden in profile scope as device facts (GTK parity), the forwarded controller stays gated, and every visible profile row commits through `commit`. Co-Authored-By: Claude Fable 5 --- clients/windows/src/app/settings.rs | 89 ++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/clients/windows/src/app/settings.rs b/clients/windows/src/app/settings.rs index 30950952..d7617cb4 100644 --- a/clients/windows/src/app/settings.rs +++ b/clients/windows/src/app/settings.rs @@ -393,6 +393,7 @@ struct OverrideFlags { bitrate_kbps: bool, codec: bool, hdr_enabled: bool, + enable_444: bool, compositor: bool, audio_channels: bool, mic_enabled: bool, @@ -419,6 +420,7 @@ impl OverrideFlags { bitrate_kbps: o.bitrate_kbps.is_some(), codec: o.codec.is_some(), hdr_enabled: o.hdr_enabled.is_some(), + enable_444: o.enable_444.is_some(), compositor: o.compositor.is_some(), audio_channels: o.audio_channels.is_some(), mic_enabled: o.mic_enabled.is_some(), @@ -511,9 +513,30 @@ fn described_overridable( control: impl Into, caption: &str, ) -> Element { - if scope.is_empty() || !overridden { + if scope.is_empty() { return described(control, caption); } + // Nothing that appears on an edit may move what was edited (the GTK client's rule, + // learned there the hard way): both states of a profile-scope row reserve the same + // caption-line height, so the marker + Reset materialise IN PLACE instead of shoving + // every row below them down by a button's height. + const CAPTION_MIN_H: f64 = 34.0; + if !overridden { + return vstack(( + control.into(), + hstack(vec![text_block(caption) + .font_size(12.0) + .foreground(ThemeRef::SecondaryText) + .wrap() + .max_width(420.0) + .horizontal_alignment(HorizontalAlignment::Left) + .vertical_alignment(VerticalAlignment::Center) + .into()]) + .min_height(CAPTION_MIN_H), + )) + .spacing(5.0) + .into(); + } let (rev, set_rev) = (rev.0, rev.1.clone()); let scope = scope.to_string(); vstack(( @@ -524,7 +547,8 @@ fn described_overridable( .foreground(ThemeRef::AccentText) .wrap() .max_width(360.0) - .horizontal_alignment(HorizontalAlignment::Left), + .horizontal_alignment(HorizontalAlignment::Left) + .vertical_alignment(VerticalAlignment::Center), button("Reset").on_click(move || { let mut catalog = ProfilesFile::load(); if let Some(p) = catalog.profiles.iter_mut().find(|p| p.id == scope) { @@ -539,7 +563,8 @@ fn described_overridable( set_rev.call(rev + 1); }), )) - .spacing(8.0), + .spacing(8.0) + .min_height(CAPTION_MIN_H), )) .spacing(5.0) .into() @@ -827,6 +852,14 @@ pub(crate) fn settings_page( s.hdr_enabled, |s, on| s.hdr_enabled = on, ); + let chroma_toggle = setting_toggle( + ctx, + scope, + (rev, set_rev), + "Full chroma (4:4:4)", + s.enable_444, + |s, on| s.enable_444 = on, + ); // --- Input ----------------------------------------------------------------------------- // Controller forwarding: Automatic forwards EVERY real controller, each as its own pad; @@ -1058,6 +1091,17 @@ pub(crate) fn settings_page( "HDR10, when the host has HDR content and this display supports it. \ HEVC only; otherwise the stream stays SDR.", ), + // Wording shared with the GTK client (its chroma_row) — same setting, + // same constraints. + described_overridable( + (rev, set_rev), + scope, + "enable_444", + over.enable_444, + chroma_toggle, + "Full-colour video: crisp small text and thin lines, at more \ + bandwidth. HEVC only, and only where the host can encode it.", + ), ], None, )); @@ -1581,3 +1625,42 @@ pub(crate) fn settings_page( } grid(layers).into() } + +#[cfg(test)] +mod tests { + use super::*; + use pf_client_core::profiles::SettingsOverlay; + + /// Every overlay field maps to its row flag — including the tri-state resolution + /// (any of width/height/match_window marks the one Resolution row) and the 4:4:4 + /// switch added for GTK parity. A field that records without marking its row is the + /// original Overridden-row bug wearing a new face. + #[test] + fn override_flags_mirror_the_overlay() { + let none = OverrideFlags::of(None); + assert!(!none.resolution && !none.enable_444 && !none.codec); + + let mut p = StreamProfile::new("t".to_string()); + p.overrides = SettingsOverlay { + match_window: Some(true), + enable_444: Some(true), + codec: Some("hevc".into()), + bitrate_kbps: Some(20000), + ..Default::default() + }; + let f = OverrideFlags::of(Some(&p)); + assert!(f.resolution, "match_window alone marks the Resolution row"); + assert!(f.enable_444); + assert!(f.codec); + assert!(f.bitrate_kbps); + assert!(!f.hdr_enabled && !f.compositor && !f.render_scale); + + let mut p2 = StreamProfile::new("t2".to_string()); + p2.overrides = SettingsOverlay { + width: Some(3840), + height: Some(2160), + ..Default::default() + }; + assert!(OverrideFlags::of(Some(&p2)).resolution); + } +}