From cf55a11174e1cf0642b2a0dc83f3287d8f9da7a0 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 29 Jul 2026 09:46:22 +0200 Subject: [PATCH] fix(client/settings): captions ran under the switch on iPhone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `described` idiom capped its caption at 360pt, which was only ever about READING — past roughly 46 characters a line stops scanning well on a wide Mac window or an iPad detail pane. On an iPhone the cell is narrower than the cap in the first place, so the cap did nothing and every caption laid out to the full cell width, running its last line straight under the row's switch. `CaptionWidth` now carries both limits: the reading cap, plus an iOS trailing inset that reserves the control column (a `UISwitch` is 51pt, so 60 with room). Order matters — the inset shrinks what the text is offered, then the cap applies, so a narrow phone cell clears the switch and a wide pane still caps. One rule in one place: the override marker and the iOS resolution wheel's caption each carried their own copy of the 360, and neither would have grown the inset. Co-Authored-By: Claude Opus 5 (1M context) --- .../Settings/SettingsView+Scope.swift | 1 - .../Settings/SettingsView+Sections.swift | 3 +- .../Settings/SettingsView+Support.swift | 47 +++++++++++++++---- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Scope.swift b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Scope.swift index 7c32d5e1..dbeeb85e 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Scope.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Scope.swift @@ -275,7 +275,6 @@ extension SettingsView { .accessibilityLabel("Reset to Default settings") .help("Stop overriding this — follow Default settings again") } - .frame(maxWidth: 360, alignment: .leading) // the described-row caption's line cap } } diff --git a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift index cec9749f..dc1577fe 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift @@ -94,7 +94,7 @@ extension SettingsView { .font(.geist(13, relativeTo: .footnote)) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: 360, alignment: .leading) // match the described-row caption cap + .modifier(CaptionWidth()) // the same reading cap + control column as `described` } } @@ -134,6 +134,7 @@ extension SettingsView { .labelsHidden() .pickerStyle(.segmented) overrideMarker("refresh_hz") + .modifier(CaptionWidth()) } } else { // A device with a single supported rate (e.g. 60 Hz) has nothing to pick. diff --git a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Support.swift b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Support.swift index 20beb321..d3cb7f4c 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Support.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Support.swift @@ -8,6 +8,33 @@ import AppKit import PunktfunkKit import SwiftUI +/// How wide a `described` row's caption (and its override marker) may run. +/// +/// Two limits, because they solve different problems. The 360pt CAP is about reading: past roughly +/// 46 characters a line stops scanning well, and on a wide Mac window or an iPad detail pane an +/// uncapped caption runs the full cell. The trailing INSET is about collision: an iOS grouped row +/// puts its switch or value about 60pt in from the trailing edge, and a caption laid out to the +/// full cell width runs its last line straight under that control — which is what the cap alone +/// never fixed, because an iPhone cell is narrower than the cap in the first place. +struct CaptionWidth: ViewModifier { + #if os(iOS) + /// Reserve the control column. Sized to a `UISwitch` (51pt) plus breathing room — the widest + /// of the trailing accessories these rows use. + private static let trailingInset: CGFloat = 60 + #else + // macOS lays the control out inline and its cells are wider than the cap; nothing to clear. + private static let trailingInset: CGFloat = 0 + #endif + + func body(content: Content) -> some View { + content + // Order matters: the padding shrinks what the text is offered, THEN the cap applies — + // so a narrow phone cell reserves the control column and a wide pane still caps. + .padding(.trailing, Self.trailingInset) + .frame(maxWidth: 360, alignment: .leading) + } +} + extension SettingsView { // MARK: - Described rows (the 2026-07 revamp's field idiom) @@ -25,17 +52,17 @@ extension SettingsView { ) -> some View { VStack(alignment: .leading, spacing: 5) { content() - Text(caption) - .font(.geist(13, relativeTo: .footnote)) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) // wrap, never truncate, in Form cells - // Cap the caption's line length well short of the cell: a full-width caption runs - // its text right up to the control column (toggles especially), reading as one - // colliding block. ~46 chars/line also just measures better. - .frame(maxWidth: 360, alignment: .leading) - if let field { - overrideMarker(field) + VStack(alignment: .leading, spacing: 5) { + Text(caption) + .font(.geist(13, relativeTo: .footnote)) + .foregroundStyle(.secondary) + // Wrap, never truncate, in Form cells. + .fixedSize(horizontal: false, vertical: true) + if let field { + overrideMarker(field) + } } + .modifier(CaptionWidth()) } .padding(.vertical, 2) }