fix(client/settings): captions ran under the switch on iPhone
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) <noreply@anthropic.com>
This commit is contained in:
@@ -275,7 +275,6 @@ extension SettingsView {
|
|||||||
.accessibilityLabel("Reset to Default settings")
|
.accessibilityLabel("Reset to Default settings")
|
||||||
.help("Stop overriding this — follow Default settings again")
|
.help("Stop overriding this — follow Default settings again")
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 360, alignment: .leading) // the described-row caption's line cap
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ extension SettingsView {
|
|||||||
.font(.geist(13, relativeTo: .footnote))
|
.font(.geist(13, relativeTo: .footnote))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.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()
|
.labelsHidden()
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
overrideMarker("refresh_hz")
|
overrideMarker("refresh_hz")
|
||||||
|
.modifier(CaptionWidth())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// A device with a single supported rate (e.g. 60 Hz) has nothing to pick.
|
// A device with a single supported rate (e.g. 60 Hz) has nothing to pick.
|
||||||
|
|||||||
@@ -8,6 +8,33 @@ import AppKit
|
|||||||
import PunktfunkKit
|
import PunktfunkKit
|
||||||
import SwiftUI
|
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 {
|
extension SettingsView {
|
||||||
// MARK: - Described rows (the 2026-07 revamp's field idiom)
|
// MARK: - Described rows (the 2026-07 revamp's field idiom)
|
||||||
|
|
||||||
@@ -25,17 +52,17 @@ extension SettingsView {
|
|||||||
) -> some View {
|
) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
content()
|
content()
|
||||||
Text(caption)
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
.font(.geist(13, relativeTo: .footnote))
|
Text(caption)
|
||||||
.foregroundStyle(.secondary)
|
.font(.geist(13, relativeTo: .footnote))
|
||||||
.fixedSize(horizontal: false, vertical: true) // wrap, never truncate, in Form cells
|
.foregroundStyle(.secondary)
|
||||||
// Cap the caption's line length well short of the cell: a full-width caption runs
|
// Wrap, never truncate, in Form cells.
|
||||||
// its text right up to the control column (toggles especially), reading as one
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
// colliding block. ~46 chars/line also just measures better.
|
if let field {
|
||||||
.frame(maxWidth: 360, alignment: .leading)
|
overrideMarker(field)
|
||||||
if let field {
|
}
|
||||||
overrideMarker(field)
|
|
||||||
}
|
}
|
||||||
|
.modifier(CaptionWidth())
|
||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user