diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift index fc30e3d4..3b1caa4d 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadHomeView.swift @@ -42,6 +42,9 @@ private struct HomeTile: Identifiable { /// The profile this tile connects with — shown as a tinted chip. Set on a pinned card; nil on /// a plain host tile unless the host is bound to one (then it answers "what will A do?"). var profile: StreamProfile? + /// This tile IS a pinned host+profile card, not a host wearing its binding's chip — the chip + /// reads loud on the former (it is the reason the tile exists) and quiet on the latter. + var isPinnedCard = false var isOnline = false var isPaired = false var isConnecting = false @@ -273,6 +276,7 @@ struct GamepadHomeView: View { title: host.displayName, subtitle: "\(host.address):\(String(host.port))", profile: profile ?? bound, + isPinnedCard: profile != nil, isOnline: online, isPaired: host.pinnedSHA256 != nil, isConnecting: connecting, @@ -374,7 +378,8 @@ private struct GamepadHostTile: View { .lineLimit(1) .minimumScaleFactor(0.7) if let profile = tile.profile { - ProfileChip(profile: profile, size: Self.statusFont, prominent: true) + ProfileChip( + profile: profile, size: Self.statusFont, prominent: tile.isPinnedCard) .padding(.top, 4) } Text(tile.subtitle) diff --git a/clients/apple/Sources/PunktfunkClient/Home/HostCards.swift b/clients/apple/Sources/PunktfunkClient/Home/HostCards.swift index b35ba9a5..119a3f15 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/HostCards.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/HostCards.swift @@ -135,12 +135,23 @@ struct HostCardView: View { HStack(spacing: m.spacing) { monogramTile(monogram(host.displayName), m: m, connecting: isConnecting, filled: true) VStack(alignment: .leading, spacing: 4) { - Text(host.displayName) - .font(.geist(m.name, .bold, relativeTo: .title3)) - .foregroundStyle(.primary) - .lineLimit(1) - if let profile = shownProfile { - ProfileChip(profile: profile, size: m.status, prominent: pinnedProfile != nil) + // The chip rides the TITLE line rather than a line of its own: a card with a + // profile and one without have to be the same height, or a pinned card sticks + // out of its row in the grid. Beside the name is also where it reads as "this + // card connects with that", which is what it means. + HStack(spacing: 6) { + Text(host.displayName) + .font(.geist(m.name, .bold, relativeTo: .title3)) + .foregroundStyle(.primary) + .lineLimit(1) + if let profile = shownProfile { + ProfileChip( + profile: profile, size: m.status, + prominent: pinnedProfile != nil) + // The chip is the shorter, more compressible of the two; let the + // host name give up its width first only after the chip has. + .layoutPriority(1) + } } Text("\(host.address):\(String(host.port))") .font(.geist(m.meta, relativeTo: .caption)) @@ -301,6 +312,9 @@ struct HostCardView: View { /// The profile a card connects with, as a tinted pill. Quiet on a bound primary card (it only /// answers "what will a click do?"); prominent on a pinned card, where the profile IS the reason /// the card exists — which is where the catalog's `accent` earns its keep. +/// +/// Prominence is fill and weight only, never TYPE SIZE: the chip sits on the card's title line, +/// and a chip taller than the name would make pinned cards taller than their host's. struct ProfileChip: View { let profile: StreamProfile let size: CGFloat @@ -314,13 +328,13 @@ struct ProfileChip: View { .frame(width: size * 0.55, height: size * 0.55) .accessibilityHidden(true) // the name is right there Text(profile.name) - .font(.geist(prominent ? size + 2 : size, .semibold, relativeTo: .caption2)) + .font(.geist(size, prominent ? .bold : .semibold, relativeTo: .caption2)) .lineLimit(1) } .foregroundStyle(tint) .padding(.horizontal, 7) .padding(.vertical, 2) - .background(Capsule().fill(tint.opacity(prominent ? 0.22 : 0.13))) + .background(Capsule().fill(tint.opacity(prominent ? 0.24 : 0.12))) .accessibilityLabel("Profile \(profile.name)") } } diff --git a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Scope.swift b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Scope.swift index 687ca5e0..fdce67ff 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Scope.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Scope.swift @@ -244,9 +244,13 @@ extension SettingsView { profiles.clearOverride(id, field: name) } - /// The accent dot + "Reset" affordance a row carries while it overrides the defaults. Rendered + /// The accent dot + Reset affordance a row carries while it overrides the defaults. Rendered /// inside `described`'s caption line, so it sits with the row it belongs to instead of in some /// separate list of exceptions. + /// + /// Reset is a BORDERED control pushed to the far edge, not tinted text next to the label: as a + /// plain button it read as a third word of the notice, and the one action that can undo an + /// override must not look like prose. @ViewBuilder func overrideMarker(_ name: String) -> some View { if isOverridden(name) { @@ -259,12 +263,19 @@ extension SettingsView { Text("Overrides Default settings") .font(.geist(12, .medium, relativeTo: .caption2)) .foregroundStyle(Color.brand) - Button("Reset") { resetOverride(name) } - .buttonStyle(.plain) - .font(.geist(12, .medium, relativeTo: .caption2)) - .foregroundStyle(.tint) - .accessibilityLabel("Reset to Default settings") + Spacer(minLength: 12) + Button { + resetOverride(name) + } label: { + Label("Reset", systemImage: "arrow.uturn.backward") + .font(.geist(11, .medium, relativeTo: .caption2)) + } + .buttonStyle(.bordered) + .controlSize(.small) + .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.swift b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView.swift index 572624e8..a0375838 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView.swift @@ -29,6 +29,9 @@ struct SettingsView: View { @State var nameAction: ProfileNameAction? @State var nameDraft = "" @State var profilePendingDelete: StreamProfile? + #if os(macOS) + @State private var macTab: MacTab = .general + #endif @AppStorage(DefaultsKey.streamWidth) var width = 1920 @AppStorage(DefaultsKey.streamHeight) var height = 1080 @AppStorage(DefaultsKey.streamHz) var hz = 60 @@ -133,14 +136,24 @@ struct SettingsView: View { // MARK: - macOS: tabbed preferences #if os(macOS) + /// The preferences tabs, tagged so the scope control can sit out the one page that isn't a + /// settings layer at all. + private enum MacTab: Hashable { + case general, display, input, audio, controllers, about + } + private var macBody: some View { // The scope control heads the window — above the tabs, because it is about which layer - // every tab is editing, not about any one of them. + // every tab is editing, not about any one of them. About is the exception: it edits + // nothing, and directly above the acknowledgements the switcher read as belonging to + // them. VStack(alignment: .leading, spacing: 0) { - scopeSwitcher - .padding(.horizontal, 20) - .padding(.top, 14) - .padding(.bottom, 10) + if macTab != .about { + scopeSwitcher + .padding(.horizontal, 20) + .padding(.top, 14) + .padding(.bottom, 10) + } macTabs } .frame(width: 500, height: 580) @@ -149,7 +162,7 @@ struct SettingsView: View { private var macTabs: some View { // Tab map mirrors SettingsCategory: General = session/app behavior, Display = the whole // picture (resolution lives here), Input = keyboard & mouse. - TabView { + TabView(selection: $macTab) { Form { sessionSection overlaySection @@ -157,6 +170,7 @@ struct SettingsView: View { } .formStyle(.grouped) .tabItem { Label("General", systemImage: "gearshape") } + .tag(MacTab.general) Form { resolutionSection @@ -166,12 +180,14 @@ struct SettingsView: View { } .formStyle(.grouped) .tabItem { Label("Display", systemImage: "display") } + .tag(MacTab.display) Form { inputSection } .formStyle(.grouped) .tabItem { Label("Input", systemImage: "keyboard") } + .tag(MacTab.input) Form { audioSection @@ -188,6 +204,7 @@ struct SettingsView: View { if micChannel > micChannelCount { micChannel = 0 } } .tabItem { Label("Audio", systemImage: "speaker.wave.2") } + .tag(MacTab.audio) Form { controllersSection @@ -199,9 +216,11 @@ struct SettingsView: View { } .onDisappear { gamepads.stopDiscovery() } .tabItem { Label("Controllers", systemImage: "gamecontroller") } + .tag(MacTab.controllers) AcknowledgementsView() .tabItem { Label("About", systemImage: "info.circle") } + .tag(MacTab.about) } } #endif