From 6316da0b05cf5c00e89d39a41c9efcfe4eb1bbba Mon Sep 17 00:00:00 2001 From: Thanh Tran Date: Sat, 3 Oct 2026 17:39:02 +0700 Subject: [PATCH 1/4] feat: adjust font size app --- macgit/App/AppState.swift | 9 ++++ macgit/App/macgitApp.swift | 4 ++ macgit/Models/AppTextSize.swift | 41 +++++++++++++++++++ .../Views/Common/AppearanceSettingsView.swift | 22 +++++++++- macgit/Views/Common/BadgeToolbarButton.swift | 3 +- macgit/Views/Common/ToolbarButton.swift | 5 ++- macgit/Views/MainWindow/BranchRowView.swift | 15 ++++--- macgit/Views/MainWindow/MainWindowView.swift | 2 +- .../Sidebar/GitFlowBranchRoleBadge.swift | 3 +- .../Sidebar/GitFlowRecoveryCard.swift | 3 +- .../Sidebar/SidebarBranchesSection.swift | 7 ++-- .../MainWindow/Sidebar/SidebarRemoteRow.swift | 3 +- .../Sidebar/SidebarRemotesSection.swift | 3 +- .../Sidebar/SidebarSectionHeader.swift | 5 ++- .../MainWindow/Sidebar/SidebarStashRow.swift | 3 +- .../Sidebar/SidebarStashesSection.swift | 3 +- .../Sidebar/SidebarSubmodulesSection.swift | 3 +- .../Sidebar/SidebarSubtreesSection.swift | 3 +- .../MainWindow/Sidebar/SidebarTagRow.swift | 3 +- .../Sidebar/SidebarTagsSection.swift | 3 +- .../Sidebar/SidebarWorkspaceSection.swift | 6 ++- .../Sidebar/SidebarWorktreeRow.swift | 9 ++-- .../Sidebar/SidebarWorktreesSection.swift | 3 +- .../MainWindow/SidebarSubmoduleRow.swift | 8 ++-- .../Views/MainWindow/SidebarSubtreeRow.swift | 8 ++-- macgitTests/AppSettingsSnapshotTests.swift | 15 +++++++ 26 files changed, 155 insertions(+), 37 deletions(-) create mode 100644 macgit/Models/AppTextSize.swift diff --git a/macgit/App/AppState.swift b/macgit/App/AppState.swift index ecee4c92..561dfa6f 100644 --- a/macgit/App/AppState.swift +++ b/macgit/App/AppState.swift @@ -21,6 +21,7 @@ import Combine final class AppState: ObservableObject { static let shared = AppState() private static let appearanceKey = "appearance" + private static let textSizeKey = "textSize" private static let showToolbarButtonTextKey = "showToolbarButtonText" private static let showGitFlowKey = "showGitFlow" private static let showWorkspaceReflogKey = "showWorkspaceReflog" @@ -61,6 +62,11 @@ final class AppState: ObservableObject { } } } + @Published var textSize: AppTextSize { + didSet { + userDefaults.set(textSize.rawValue, forKey: Self.textSizeKey) + } + } @Published var showToolbarButtonText: Bool { didSet { userDefaults.set(showToolbarButtonText, forKey: Self.showToolbarButtonTextKey) @@ -275,6 +281,8 @@ final class AppState: ObservableObject { self.userDefaults = userDefaults let appearance = userDefaults.string(forKey: Self.appearanceKey) .flatMap(AppAppearance.init(rawValue:)) ?? .system + let textSize = userDefaults.string(forKey: Self.textSizeKey) + .flatMap(AppTextSize.init(rawValue:)) ?? .default let showToolbarButtonText = userDefaults.object(forKey: Self.showToolbarButtonTextKey) as? Bool ?? true let showGitFlow = userDefaults.object(forKey: Self.showGitFlowKey) as? Bool ?? true let showWorkspaceReflog = userDefaults.object(forKey: Self.showWorkspaceReflogKey) as? Bool ?? true @@ -334,6 +342,7 @@ final class AppState: ObservableObject { ).normalizedRepositoryToolbarShortcuts() self.appearance = appearance + self.textSize = textSize self.showToolbarButtonText = showToolbarButtonText self.showGitFlow = showGitFlow self.showWorkspaceReflog = showWorkspaceReflog diff --git a/macgit/App/macgitApp.swift b/macgit/App/macgitApp.swift index fc550613..595e0f71 100644 --- a/macgit/App/macgitApp.swift +++ b/macgit/App/macgitApp.swift @@ -240,6 +240,8 @@ struct macgitApp: App { .environmentObject(repositoryBookmarkController) .environmentObject(gitFlowConfigurationSyncController) .preferredColorScheme(appState.appearance.colorScheme) + .font(appState.textSize.font) + .environment(\.appTextScale, appState.textSize.scale) .task { appUpdateController.start() } @@ -459,6 +461,8 @@ struct macgitApp: App { ) .environmentObject(featureAccessController) .preferredColorScheme(appState.appearance.colorScheme) + .font(appState.textSize.font) + .environment(\.appTextScale, appState.textSize.scale) } .defaultSize(width: 920, height: 640) .defaultLaunchBehavior(.suppressed) diff --git a/macgit/Models/AppTextSize.swift b/macgit/Models/AppTextSize.swift new file mode 100644 index 00000000..9d007631 --- /dev/null +++ b/macgit/Models/AppTextSize.swift @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import SwiftUI + +enum AppTextSize: String, CaseIterable, Identifiable, Sendable { + case `default` + case large + case extraLarge + + var id: Self { self } + + var title: String { + switch self { + case .default: "Default" + case .large: "Large" + case .extraLarge: "Extra Large" + } + } + + var scale: CGFloat { + switch self { + case .default: 1 + case .large: 1.15 + case .extraLarge: 1.3 + } + } + + var font: Font { + .body.scaled(by: scale) + } +} + +private struct AppTextScaleKey: EnvironmentKey { + static let defaultValue: CGFloat = 1 +} + +extension EnvironmentValues { + var appTextScale: CGFloat { + get { self[AppTextScaleKey.self] } + set { self[AppTextScaleKey.self] = newValue } + } +} diff --git a/macgit/Views/Common/AppearanceSettingsView.swift b/macgit/Views/Common/AppearanceSettingsView.swift index 0cae688c..95935741 100644 --- a/macgit/Views/Common/AppearanceSettingsView.swift +++ b/macgit/Views/Common/AppearanceSettingsView.swift @@ -37,6 +37,25 @@ struct AppearanceSettingsView: View { Text("System follows the current macOS appearance automatically.") } + Section { + Picker("Text size", selection: $appState.textSize) { + ForEach(AppTextSize.allCases) { textSize in + Text(textSize.title) + .tag(textSize) + } + } + .pickerStyle(.segmented) + + Text("The quick brown fox jumps over the lazy dog.") + .font(.body.scaled(by: appState.textSize.scale)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 4) + } header: { + Label("Text Size", systemImage: "textformat.size") + } footer: { + Text("Larger text is useful on high-resolution and large displays. This setting applies only to this Mac.") + } + Section { SettingsToggleRow( title: "Show button text", @@ -88,7 +107,7 @@ struct AppearanceSettingsView: View { Button("Restore Defaults", role: .destructive, action: restoreDefaults) Button("Cancel", role: .cancel) {} } message: { - Text("Theme, toolbar, and header preferences on this page will be reset.") + Text("Theme, text size, toolbar, and header preferences on this page will be reset.") } } @@ -98,6 +117,7 @@ struct AppearanceSettingsView: View { private func restoreDefaults() { appState.appearance = .system + appState.textSize = .default appState.showToolbarButtonText = true appState.showHeaderBranchButton = true appState.showHeaderMergeButton = true diff --git a/macgit/Views/Common/BadgeToolbarButton.swift b/macgit/Views/Common/BadgeToolbarButton.swift index f12ad297..0f8bbac2 100644 --- a/macgit/Views/Common/BadgeToolbarButton.swift +++ b/macgit/Views/Common/BadgeToolbarButton.swift @@ -30,6 +30,7 @@ struct BadgeToolbarButton: View { let disabled: Bool var showText: Bool = true let action: () -> Void + @Environment(\.appTextScale) private var textScale private var badgeText: String { return String(badgeCount) @@ -43,7 +44,7 @@ struct BadgeToolbarButton: View { if badgeCount > 0 && !isLoading { Text(badgeText) - .font(.system(size: 9, weight: .bold)) + .font(.system(size: 9, weight: .bold).scaled(by: textScale)) .foregroundColor(.white) .padding(.horizontal, 3) .padding(.vertical, 1) diff --git a/macgit/Views/Common/ToolbarButton.swift b/macgit/Views/Common/ToolbarButton.swift index e7e2df51..a4ae5c97 100644 --- a/macgit/Views/Common/ToolbarButton.swift +++ b/macgit/Views/Common/ToolbarButton.swift @@ -26,6 +26,7 @@ struct ToolbarButtonLabel: View { let icon: String let label: String var showText: Bool = true + @Environment(\.appTextScale) private var textScale var body: some View { VStack(spacing: 1) { @@ -33,10 +34,10 @@ struct ToolbarButtonLabel: View { .font(.system(size: 13, weight: .medium)) if showText { Text(label) - .font(.system(size: 9)) + .font(.system(size: 9).scaled(by: textScale)) } } - .frame(width: 40, height: 28) + .frame(width: 40 * textScale, height: 28) } } diff --git a/macgit/Views/MainWindow/BranchRowView.swift b/macgit/Views/MainWindow/BranchRowView.swift index 73b00b88..32668e8f 100644 --- a/macgit/Views/MainWindow/BranchRowView.swift +++ b/macgit/Views/MainWindow/BranchRowView.swift @@ -38,6 +38,7 @@ struct BranchRowContent: View, Equatable { let folderIsExpanded: Bool let isCurrentBranchPrefix: Bool let gitFlowRole: GitFlowBranchRole? + @Environment(\.appTextScale) private var textScale static func == (lhs: BranchRowContent, rhs: BranchRowContent) -> Bool { lhs.row == rhs.row @@ -66,7 +67,7 @@ struct BranchRowContent: View, Equatable { leadingIcon Text(row.name) - .font(.system(size: 12)) + .font(.system(size: 12).scaled(by: textScale)) .fontWeight(isCurrentBranch && !row.isFolder || isCurrentBranchPrefix ? .semibold : .regular) .foregroundStyle(isCurrentBranchPrefix ? Color.accentColor : .primary) .lineLimit(1) @@ -127,10 +128,11 @@ struct BranchRowContent: View, Equatable { struct BranchDropLabel: View { let text: String + @Environment(\.appTextScale) private var textScale var body: some View { Text(text) - .font(.system(size: 10, weight: .semibold)) + .font(.system(size: 10, weight: .semibold).scaled(by: textScale)) .foregroundStyle(Color.accentColor) .padding(.horizontal, 6) .padding(.vertical, 2) @@ -139,9 +141,11 @@ struct BranchDropLabel: View { } private struct BranchHeadBadge: View { + @Environment(\.appTextScale) private var textScale + var body: some View { Text("HEAD") - .font(.system(size: 10, weight: .semibold)) + .font(.system(size: 10, weight: .semibold).scaled(by: textScale)) .foregroundStyle(.secondary) .padding(.horizontal, 6) .padding(.vertical, 2) @@ -152,6 +156,7 @@ private struct BranchHeadBadge: View { private struct BranchSyncBadge: View { let isSyncing: Bool let status: BranchSyncStatus? + @Environment(\.appTextScale) private var textScale var body: some View { if isSyncing { @@ -171,7 +176,7 @@ private struct BranchSyncBadge: View { Text("\(status.ahead)") Text("\u{2191}") } - .font(.system(size: 10, weight: .medium)) + .font(.system(size: 10, weight: .medium).scaled(by: textScale)) .foregroundColor(.white) .padding(.horizontal, 5) .padding(.vertical, 1) @@ -184,7 +189,7 @@ private struct BranchSyncBadge: View { Text("\(status.behind)") Text("\u{2193}") } - .font(.system(size: 10, weight: .medium)) + .font(.system(size: 10, weight: .medium).scaled(by: textScale)) .foregroundColor(.white) .padding(.horizontal, 5) .padding(.vertical, 1) diff --git a/macgit/Views/MainWindow/MainWindowView.swift b/macgit/Views/MainWindow/MainWindowView.swift index ee5790f8..d9ffbd80 100644 --- a/macgit/Views/MainWindow/MainWindowView.swift +++ b/macgit/Views/MainWindow/MainWindowView.swift @@ -1478,7 +1478,7 @@ struct MainWindowView: View { .aspectRatio(contentMode: .fit) .frame(width: 18, height: 18) Text(repositoryURL.lastPathComponent) - .font(.headline) + .font(.headline.scaled(by: appState.textSize.scale)) } .padding(.horizontal, 12) } diff --git a/macgit/Views/MainWindow/Sidebar/GitFlowBranchRoleBadge.swift b/macgit/Views/MainWindow/Sidebar/GitFlowBranchRoleBadge.swift index 41c881b2..9441413a 100644 --- a/macgit/Views/MainWindow/Sidebar/GitFlowBranchRoleBadge.swift +++ b/macgit/Views/MainWindow/Sidebar/GitFlowBranchRoleBadge.swift @@ -20,10 +20,11 @@ import SwiftUI struct GitFlowBranchRoleBadge: View { let role: GitFlowBranchRole + @Environment(\.appTextScale) private var textScale var body: some View { Text(role.rawValue) - .font(.caption) + .font(.caption.scaled(by: textScale)) .bold() .foregroundStyle(.secondary) .padding(.horizontal, 4) diff --git a/macgit/Views/MainWindow/Sidebar/GitFlowRecoveryCard.swift b/macgit/Views/MainWindow/Sidebar/GitFlowRecoveryCard.swift index e52e23cd..b432606a 100644 --- a/macgit/Views/MainWindow/Sidebar/GitFlowRecoveryCard.swift +++ b/macgit/Views/MainWindow/Sidebar/GitFlowRecoveryCard.swift @@ -19,6 +19,7 @@ import SwiftUI struct GitFlowRecoveryCard: View { + @Environment(\.appTextScale) private var textScale let checkpoint: GitFlowFinishCheckpoint? let issue: GitFlowLocalStateIssue? let actionsEnabled: Bool @@ -52,7 +53,7 @@ struct GitFlowRecoveryCard: View { .foregroundStyle(.secondary) } } - .font(.subheadline) + .font(.subheadline.scaled(by: textScale)) .padding(8) .frame(maxWidth: .infinity, alignment: .leading) .background(.quaternary, in: RoundedRectangle(cornerRadius: 7)) diff --git a/macgit/Views/MainWindow/Sidebar/SidebarBranchesSection.swift b/macgit/Views/MainWindow/Sidebar/SidebarBranchesSection.swift index 83418568..248b7b8e 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarBranchesSection.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarBranchesSection.swift @@ -19,6 +19,7 @@ import SwiftUI import UniformTypeIdentifiers struct SidebarBranchesSection: View { + @Environment(\.appTextScale) private var textScale let rows: [BranchRowItem] let isExpanded: Bool let isLoading: Bool @@ -55,7 +56,7 @@ struct SidebarBranchesSection: View { .padding(.leading, 4) } else if rows.isEmpty { Text("No branches") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) } else { if currentBranch.isEmpty && !headHash.isEmpty { @@ -71,12 +72,12 @@ struct SidebarBranchesSection: View { .frame(width: 16, alignment: .center) Text("HEAD") - .font(.system(size: 12, weight: .bold)) + .font(.system(size: 12, weight: .bold).scaled(by: textScale)) .lineLimit(1) if !headHash.isEmpty { Text(headHash) - .font(.system(size: 11)) + .font(.system(size: 11).scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) } diff --git a/macgit/Views/MainWindow/Sidebar/SidebarRemoteRow.swift b/macgit/Views/MainWindow/Sidebar/SidebarRemoteRow.swift index 8f87b339..bdba719b 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarRemoteRow.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarRemoteRow.swift @@ -18,6 +18,7 @@ import SwiftUI struct SidebarRemoteRow: View { + @Environment(\.appTextScale) private var textScale let row: BranchRowItem let currentBranch: String let expandedFolders: Set @@ -105,7 +106,7 @@ struct SidebarRemoteRow: View { } Text(row.name) - .font(.system(size: 12)) + .font(.system(size: 12).scaled(by: textScale)) .lineLimit(1) } .padding(.vertical, 2) diff --git a/macgit/Views/MainWindow/Sidebar/SidebarRemotesSection.swift b/macgit/Views/MainWindow/Sidebar/SidebarRemotesSection.swift index 5bb45180..1cc9cc67 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarRemotesSection.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarRemotesSection.swift @@ -19,6 +19,7 @@ import SwiftUI import UniformTypeIdentifiers struct SidebarRemotesSection: View { + @Environment(\.appTextScale) private var textScale let rows: [BranchRowItem] let isExpanded: Bool let isLoading: Bool @@ -40,7 +41,7 @@ struct SidebarRemotesSection: View { .padding(.leading, 4) } else if rows.isEmpty { Text("No remotes") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) } else { ForEach(rows) { row in diff --git a/macgit/Views/MainWindow/Sidebar/SidebarSectionHeader.swift b/macgit/Views/MainWindow/Sidebar/SidebarSectionHeader.swift index 256effdf..3b60dceb 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarSectionHeader.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarSectionHeader.swift @@ -24,6 +24,7 @@ struct SidebarSectionHeader: View { let activeDropLabel: String? let onToggle: () -> Void @ViewBuilder let trailing: Trailing + @Environment(\.appTextScale) private var textScale var body: some View { HStack { @@ -33,7 +34,7 @@ struct SidebarSectionHeader: View { .foregroundStyle(section.iconColor) Text(section.rawValue) - .font(.system(size: 11)) + .font(.system(size: 11).scaled(by: textScale)) .bold() } .foregroundStyle(.secondary) @@ -42,7 +43,7 @@ struct SidebarSectionHeader: View { Spacer() if let activeDropLabel { Text(activeDropLabel) - .font(.system(size: 10, weight: .semibold)) + .font(.system(size: 10, weight: .semibold).scaled(by: textScale)) .foregroundStyle(Color.accentColor) .padding(.horizontal, 6) .padding(.vertical, 2) diff --git a/macgit/Views/MainWindow/Sidebar/SidebarStashRow.swift b/macgit/Views/MainWindow/Sidebar/SidebarStashRow.swift index fe608b45..7a41fbd0 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarStashRow.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarStashRow.swift @@ -18,6 +18,7 @@ import SwiftUI struct SidebarStashRow: View { + @Environment(\.appTextScale) private var textScale let stash: StashEntry let actions: SidebarStashSectionActions @@ -55,7 +56,7 @@ struct SidebarStashRow: View { .frame(width: 16, alignment: .center) Text(stash.displayTitle) - .font(.system(size: 12)) + .font(.system(size: 12).scaled(by: textScale)) .lineLimit(1) Spacer() diff --git a/macgit/Views/MainWindow/Sidebar/SidebarStashesSection.swift b/macgit/Views/MainWindow/Sidebar/SidebarStashesSection.swift index bf29004a..f6748e12 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarStashesSection.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarStashesSection.swift @@ -19,6 +19,7 @@ import SwiftUI import UniformTypeIdentifiers struct SidebarStashesSection: View { + @Environment(\.appTextScale) private var textScale let stashes: [StashEntry] let isExpanded: Bool let isLoading: Bool @@ -38,7 +39,7 @@ struct SidebarStashesSection: View { .padding(.leading, 4) } else if stashes.isEmpty { Text("No stashes") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) } else { ForEach(stashes) { stash in diff --git a/macgit/Views/MainWindow/Sidebar/SidebarSubmodulesSection.swift b/macgit/Views/MainWindow/Sidebar/SidebarSubmodulesSection.swift index 8ed2a00e..65aa0bd1 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarSubmodulesSection.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarSubmodulesSection.swift @@ -18,6 +18,7 @@ import SwiftUI struct SidebarSubmodulesSection: View { + @Environment(\.appTextScale) private var textScale let repositoryURL: URL let rows: [BranchRowItem] let entriesByPath: [String: GitSubmoduleEntry] @@ -39,7 +40,7 @@ struct SidebarSubmodulesSection: View { .padding(.leading, 4) } else if rows.isEmpty { Text("No submodules") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) } else { ForEach(rows) { row in diff --git a/macgit/Views/MainWindow/Sidebar/SidebarSubtreesSection.swift b/macgit/Views/MainWindow/Sidebar/SidebarSubtreesSection.swift index dde025f3..248bf570 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarSubtreesSection.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarSubtreesSection.swift @@ -18,6 +18,7 @@ import SwiftUI struct SidebarSubtreesSection: View { + @Environment(\.appTextScale) private var textScale let repositoryURL: URL let entries: [GitSubtreeEntry] let isExpanded: Bool @@ -37,7 +38,7 @@ struct SidebarSubtreesSection: View { .padding(.leading, 4) } else if entries.isEmpty { Text("No subtrees") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) } else { ForEach(entries) { entry in diff --git a/macgit/Views/MainWindow/Sidebar/SidebarTagRow.swift b/macgit/Views/MainWindow/Sidebar/SidebarTagRow.swift index 9b64a7d5..33313e78 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarTagRow.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarTagRow.swift @@ -19,6 +19,7 @@ import SwiftUI import UniformTypeIdentifiers struct SidebarTagRow: View { + @Environment(\.appTextScale) private var textScale let row: BranchRowItem let expandedFolders: Set let remoteNames: [String] @@ -101,7 +102,7 @@ struct SidebarTagRow: View { } Text(row.name) - .font(.system(size: 12)) + .font(.system(size: 12).scaled(by: textScale)) .lineLimit(1) Spacer() diff --git a/macgit/Views/MainWindow/Sidebar/SidebarTagsSection.swift b/macgit/Views/MainWindow/Sidebar/SidebarTagsSection.swift index 65128519..694ef82a 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarTagsSection.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarTagsSection.swift @@ -19,6 +19,7 @@ import SwiftUI import UniformTypeIdentifiers struct SidebarTagsSection: View { + @Environment(\.appTextScale) private var textScale let rows: [BranchRowItem] let isExpanded: Bool let isLoading: Bool @@ -40,7 +41,7 @@ struct SidebarTagsSection: View { .padding(.leading, 4) } else if rows.isEmpty { Text("No tags") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) } else { ForEach(rows) { row in diff --git a/macgit/Views/MainWindow/Sidebar/SidebarWorkspaceSection.swift b/macgit/Views/MainWindow/Sidebar/SidebarWorkspaceSection.swift index db6e1091..e00b9a5c 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarWorkspaceSection.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarWorkspaceSection.swift @@ -19,6 +19,7 @@ import SwiftUI struct SidebarWorkspaceSection: View { + @Environment(\.appTextScale) private var textScale @ObservedObject var appState: AppState let onRequestSearch: () -> Void let onRequestCreatePullRequest: () -> Void @@ -35,7 +36,7 @@ struct SidebarWorkspaceSection: View { .foregroundStyle(SidebarSection.workspace.iconColor) Text(SidebarSection.workspace.rawValue) - .font(.system(size: 11)) + .font(.system(size: 11).scaled(by: textScale)) .bold() } .foregroundStyle(.secondary) @@ -62,6 +63,7 @@ struct SidebarWorkspaceSection: View { ForEach(SidebarSection.workspace.items) { item in if isVisible(item), item == .search { Label(item.rawValue, systemImage: item.icon) + .font(.body.scaled(by: textScale)) .padding(.leading, 6) .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) @@ -71,6 +73,7 @@ struct SidebarWorkspaceSection: View { .sidebarPointingHandCursor() } else if isVisible(item) { Label(item.rawValue, systemImage: item.icon) + .font(.body.scaled(by: textScale)) .padding(.leading, 6) .tag(SidebarSelection.item(item)) .contextMenu { @@ -84,6 +87,7 @@ struct SidebarWorkspaceSection: View { if appState.showGitFlow { Label(SidebarItem.gitFlow.rawValue, systemImage: SidebarItem.gitFlow.icon) + .font(.body.scaled(by: textScale)) .padding(.leading, 6) .tag(SidebarSelection.item(.gitFlow)) .contextMenu { diff --git a/macgit/Views/MainWindow/Sidebar/SidebarWorktreeRow.swift b/macgit/Views/MainWindow/Sidebar/SidebarWorktreeRow.swift index e5ac4691..7f6906df 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarWorktreeRow.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarWorktreeRow.swift @@ -18,6 +18,7 @@ import SwiftUI struct SidebarWorktreeRow: View { + @Environment(\.appTextScale) private var textScale let entry: WorktreeEntry let isCurrentRepositoryWorktree: Bool let onSelect: () -> Void @@ -40,14 +41,14 @@ struct SidebarWorktreeRow: View { .frame(width: 16, alignment: .center) Text(entry.displayTitle) - .font(.system(size: 12)) + .font(.system(size: 12).scaled(by: textScale)) .fontWeight(isCurrentRepositoryWorktree ? .bold : .regular) .italic(isCurrentRepositoryWorktree) .lineLimit(1) if isCurrentRepositoryWorktree { Text("(this)") - .font(.system(size: 10)) + .font(.system(size: 10).scaled(by: textScale)) .foregroundStyle(.secondary) } @@ -55,7 +56,7 @@ struct SidebarWorktreeRow: View { if !isCurrentRepositoryWorktree, entry.dirtyCount > 0 { Text("\(entry.dirtyCount)") - .font(.system(size: 10, weight: .medium)) + .font(.system(size: 10, weight: .medium).scaled(by: textScale)) .foregroundColor(.white) .padding(.horizontal, 5) .padding(.vertical, 1) @@ -63,7 +64,7 @@ struct SidebarWorktreeRow: View { .cornerRadius(4) } else if !isCurrentRepositoryWorktree, entry.dirtyCount < 0 { Text("?") - .font(.system(size: 10, weight: .medium)) + .font(.system(size: 10, weight: .medium).scaled(by: textScale)) .foregroundStyle(.secondary) } } diff --git a/macgit/Views/MainWindow/Sidebar/SidebarWorktreesSection.swift b/macgit/Views/MainWindow/Sidebar/SidebarWorktreesSection.swift index 43d5e0ff..cfc7c56a 100644 --- a/macgit/Views/MainWindow/Sidebar/SidebarWorktreesSection.swift +++ b/macgit/Views/MainWindow/Sidebar/SidebarWorktreesSection.swift @@ -18,6 +18,7 @@ import SwiftUI struct SidebarWorktreesSection: View { + @Environment(\.appTextScale) private var textScale let currentRepositoryURL: URL let entries: [WorktreeEntry] let isExpanded: Bool @@ -37,7 +38,7 @@ struct SidebarWorktreesSection: View { .padding(.leading, 4) } else if entries.isEmpty { Text("No worktrees") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) } else { ForEach(entries) { entry in diff --git a/macgit/Views/MainWindow/SidebarSubmoduleRow.swift b/macgit/Views/MainWindow/SidebarSubmoduleRow.swift index f812af3c..01ea15c5 100644 --- a/macgit/Views/MainWindow/SidebarSubmoduleRow.swift +++ b/macgit/Views/MainWindow/SidebarSubmoduleRow.swift @@ -18,6 +18,7 @@ import SwiftUI struct SidebarSubmoduleRow: View { + @Environment(\.appTextScale) private var textScale let entry: GitSubmoduleEntry let onOpen: () -> Void let onShowInFinder: () -> Void @@ -42,9 +43,10 @@ struct SidebarSubmoduleRow: View { VStack(alignment: .leading, spacing: 1) { Text(displayName) + .font(.body.scaled(by: textScale)) .lineLimit(1) Text(entry.path) - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) .truncationMode(.middle) @@ -54,13 +56,13 @@ struct SidebarSubmoduleRow: View { if let branch = entry.branch { Text(branch) - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) } Text(entry.state.title) - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(entry.state.tint) .lineLimit(1) } diff --git a/macgit/Views/MainWindow/SidebarSubtreeRow.swift b/macgit/Views/MainWindow/SidebarSubtreeRow.swift index fe7f92e5..2ea7091a 100644 --- a/macgit/Views/MainWindow/SidebarSubtreeRow.swift +++ b/macgit/Views/MainWindow/SidebarSubtreeRow.swift @@ -18,6 +18,7 @@ import SwiftUI struct SidebarSubtreeRow: View { + @Environment(\.appTextScale) private var textScale let entry: GitSubtreeEntry let onShowInFinder: () -> Void let onOpenInTerminal: () -> Void @@ -38,9 +39,10 @@ struct SidebarSubtreeRow: View { VStack(alignment: .leading, spacing: 1) { Text(entry.name) + .font(.body.scaled(by: textScale)) .lineLimit(1) Text(entry.path) - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) .truncationMode(.middle) @@ -50,14 +52,14 @@ struct SidebarSubtreeRow: View { if entry.squash { Text("Squashed") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) } if !entry.folderExists { Text("Missing folder") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.red) .lineLimit(1) } diff --git a/macgitTests/AppSettingsSnapshotTests.swift b/macgitTests/AppSettingsSnapshotTests.swift index be0b4beb..afa2143d 100644 --- a/macgitTests/AppSettingsSnapshotTests.swift +++ b/macgitTests/AppSettingsSnapshotTests.swift @@ -208,6 +208,21 @@ final class AppSettingsSnapshotTests: XCTestCase { XCTAssertEqual(defaults.object(forKey: "settingsSyncEnabled") as? Bool, true) } + func testTextSizeDefaultsAndPersistsLocally() { + let suiteName = "AppSettingsSnapshotTests.\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defer { defaults.removePersistentDomain(forName: suiteName) } + + let state = AppState(userDefaults: defaults) + XCTAssertEqual(state.textSize, .default) + + state.textSize = .extraLarge + + XCTAssertEqual(AppState(userDefaults: defaults).textSize, .extraLarge) + XCTAssertEqual(defaults.string(forKey: "textSize"), AppTextSize.extraLarge.rawValue) + XCTAssertEqual(state.snapshot.appearance, .system) + } + func testGitFlowVisibilityDefaultsOnAndPersistsLocally() { let suiteName = "AppSettingsSnapshotTests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suiteName)! From a5f81de9eaca8b90f00a34822d31f3ebe44785f1 Mon Sep 17 00:00:00 2001 From: Thanh Tran Date: Sat, 3 Oct 2026 17:53:18 +0700 Subject: [PATCH 2/4] feat: Scale welcome and repo picker fonts with app text scale Apply appTextScale to typography across RepoPickerView and welcome dashboard views so text respects the user's font size preference. --- macgit/Views/MainWindow/RepoPickerView.swift | 40 +++++++++++-------- .../MainWindow/WelcomeActivityGrid.swift | 9 +++-- .../MainWindow/WelcomeActivityView.swift | 8 ++-- .../MainWindow/WelcomeAttentionView.swift | 17 ++++---- .../MainWindow/WelcomeDashboardContent.swift | 6 ++- .../MainWindow/WelcomeOverviewCard.swift | 7 ++-- 6 files changed, 51 insertions(+), 36 deletions(-) diff --git a/macgit/Views/MainWindow/RepoPickerView.swift b/macgit/Views/MainWindow/RepoPickerView.swift index 22440686..bb991eba 100644 --- a/macgit/Views/MainWindow/RepoPickerView.swift +++ b/macgit/Views/MainWindow/RepoPickerView.swift @@ -62,6 +62,7 @@ enum RepoPickerFilterType: String, CaseIterable, Identifiable { struct RepoPickerView: View { @Environment(\.colorScheme) private var colorScheme + @Environment(\.appTextScale) private var textScale @EnvironmentObject private var bookmarkController: RepositoryBookmarkController @ObservedObject private var store = RecentRepositoriesStore.shared @State private var showingCloneSheet = false @@ -180,7 +181,7 @@ struct RepoPickerView: View { Text(bookmarkController.canSyncPendingChanges ? "Bookmark changes are saved locally and waiting to sync." : "Bookmark changes are saved locally. Sign in to sync them.") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) Spacer() if bookmarkController.canSyncPendingChanges { @@ -333,7 +334,7 @@ struct RepoPickerView: View { .accessibilityHidden(true) } Text("Commit+") - .font(.title2.bold()) + .font(.title2.bold().scaled(by: textScale)) .foregroundStyle(.blue) } .padding(.bottom, 8) @@ -353,6 +354,7 @@ struct RepoPickerView: View { private var dashboardActionButtons: some View { Button(action: openExistingRepository) { Label("Open", systemImage: "folder") + .font(.body.scaled(by: textScale)) .fixedSize() .frame(maxWidth: .infinity) } @@ -361,6 +363,7 @@ struct RepoPickerView: View { .help("Open Repository") Button { showingCloneSheet = true } label: { Label("Clone", systemImage: "arrow.down.circle") + .font(.body.scaled(by: textScale)) .fixedSize() .frame(maxWidth: .infinity) } @@ -369,6 +372,7 @@ struct RepoPickerView: View { .help("Clone Repository") Button(action: createRepository) { Label("Create Repository", systemImage: "plus.rectangle.on.folder") + .font(.body.scaled(by: textScale)) .fixedSize() .frame(maxWidth: .infinity) } @@ -413,10 +417,10 @@ struct RepoPickerView: View { VStack(alignment: .leading, spacing: 6) { Text(title) - .font(.largeTitle) + .font(.largeTitle.scaled(by: textScale)) .fontWeight(.semibold) Text("Open an existing repository or clone a new one") - .font(.title3) + .font(.title3.scaled(by: textScale)) .foregroundStyle(.secondary) } } @@ -425,7 +429,7 @@ struct RepoPickerView: View { HStack(spacing: 12) { Button(action: openExistingRepository) { Label("Open Repository", systemImage: "folder") - .font(.headline) + .font(.headline.scaled(by: textScale)) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) @@ -435,7 +439,7 @@ struct RepoPickerView: View { Button(action: { showingCloneSheet = true }) { Label("Clone Repository", systemImage: "arrow.down.circle") - .font(.headline) + .font(.headline.scaled(by: textScale)) .frame(maxWidth: .infinity) } .buttonStyle(.bordered) @@ -450,6 +454,7 @@ struct RepoPickerView: View { private var controlBar: some View { HStack(spacing: 12) { TextField("Filter repositories", text: $searchText) + .font(.body.scaled(by: textScale)) .textFieldStyle(.roundedBorder) .disabled(store.repositories.isEmpty && bookmarkController.bookmarks.isEmpty) @@ -507,7 +512,7 @@ struct RepoPickerView: View { private var recentRepositoriesSection: some View { VStack(alignment: .leading, spacing: 14) { Text("Repositories") - .font(.headline) + .font(.headline.scaled(by: textScale)) if store.repositories.isEmpty && visibleUnlinkedBookmarks.isEmpty { ContentUnavailableView( @@ -590,7 +595,7 @@ struct RepoPickerView: View { } .buttonStyle(.borderless) } - .font(.caption) + .font(.caption.scaled(by: textScale)) .padding(.leading, 46) .padding(.bottom, 8) } @@ -670,9 +675,9 @@ struct RepoPickerView: View { VStack(alignment: .leading, spacing: 3) { Text(bookmark.name) - .font(.body.weight(.medium)) + .font(.body.weight(.medium).scaled(by: textScale)) Text(bookmark.remoteURL.absoluteString) - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) } @@ -708,7 +713,7 @@ struct RepoPickerView: View { .buttonStyle(.borderless) .sidebarPointingHandCursor() } - .font(.caption) + .font(.caption.scaled(by: textScale)) .padding(.leading, 46) } .padding(.vertical, 12) @@ -763,7 +768,7 @@ struct RepoPickerView: View { VStack(alignment: .leading, spacing: 5) { HStack(spacing: 6) { Text(repo.name) - .font(.body.weight(.medium)) + .font(.body.weight(.medium).scaled(by: textScale)) .lineLimit(1) .layoutPriority(1) @@ -774,7 +779,7 @@ struct RepoPickerView: View { HStack(spacing: 8) { Text(repo.url.path) - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) .layoutPriority(1) @@ -782,7 +787,7 @@ struct RepoPickerView: View { Spacer(minLength: 0) Text(timeAgoString(from: repo.lastOpened)) - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) .fixedSize() } @@ -800,7 +805,7 @@ struct RepoPickerView: View { if rowState?.isMissing == true { Text("Repository moved or deleted") - .font(.caption.weight(.semibold)) + .font(.caption.weight(.semibold).scaled(by: textScale)) .foregroundStyle(.white) .padding(.horizontal, 8) .padding(.vertical, 4) @@ -809,7 +814,7 @@ struct RepoPickerView: View { HStack(spacing: 6) { if let branch = rowState.currentBranch, !branch.isEmpty { Label(branch, systemImage: "arrow.triangle.branch") - .font(.caption.weight(.medium)) + .font(.caption.weight(.medium).scaled(by: textScale)) .lineLimit(1) .truncationMode(.middle) .padding(.horizontal, 8) @@ -1051,6 +1056,7 @@ struct RepoPickerView: View { } private struct RepoPickerCountBadge: View { + @Environment(\.appTextScale) private var textScale let icon: String let label: String let count: Int @@ -1060,7 +1066,7 @@ private struct RepoPickerCountBadge: View { Group { if count > 0 { Label(String(count), systemImage: icon) - .font(.system(size: 10, weight: .semibold)) + .font(.system(size: 10, weight: .semibold).scaled(by: textScale)) .foregroundStyle(.primary) .padding(.horizontal, 5) .padding(.vertical, 3) diff --git a/macgit/Views/MainWindow/WelcomeActivityGrid.swift b/macgit/Views/MainWindow/WelcomeActivityGrid.swift index 0fd38a91..52d2923c 100644 --- a/macgit/Views/MainWindow/WelcomeActivityGrid.swift +++ b/macgit/Views/MainWindow/WelcomeActivityGrid.swift @@ -19,6 +19,7 @@ import SwiftUI /// Explicit rows keep every repository's cells together, including repeated day IDs. struct WelcomeActivityGrid: View { + @Environment(\.appTextScale) private var textScale let snapshot: WelcomeDashboardSnapshot let onRepositoryOpened: (URL) -> Void @@ -45,7 +46,7 @@ struct WelcomeActivityGrid: View { } Text("Total").frame(width: 32, alignment: .trailing) } - .font(.system(size: 10)) + .font(.system(size: 10).scaled(by: textScale)) .foregroundStyle(.secondary) .padding(.bottom, 4) @@ -53,7 +54,7 @@ struct WelcomeActivityGrid: View { HStack(spacing: 12) { Button { onRepositoryOpened(repository.url) } label: { Text(repository.name) - .font(.system(size: 10, weight: .medium)) + .font(.system(size: 10, weight: .medium).scaled(by: textScale)) .lineLimit(1) .frame(width: nameWidth, alignment: .leading) .contentShape(Rectangle()) @@ -67,7 +68,7 @@ struct WelcomeActivityGrid: View { .frame(width: cellSize, height: cellSize) .overlay { if repository.activityNote != nil && count == 0 { - Text("–").font(.system(size: 8)).foregroundStyle(.secondary) + Text("–").font(.system(size: 8).scaled(by: textScale)).foregroundStyle(.secondary) } } .help(cellDescription(repository: repository, index: index)) @@ -75,7 +76,7 @@ struct WelcomeActivityGrid: View { } } Text(repository.activityNote != nil && repository.commitCount == 0 ? "–" : "\(repository.commitCount)") - .font(.system(size: 10).monospacedDigit()) + .font(.system(size: 10).monospacedDigit().scaled(by: textScale)) .foregroundStyle(.secondary) .frame(width: 32, alignment: .trailing) } diff --git a/macgit/Views/MainWindow/WelcomeActivityView.swift b/macgit/Views/MainWindow/WelcomeActivityView.swift index dc8288f7..ba744aea 100644 --- a/macgit/Views/MainWindow/WelcomeActivityView.swift +++ b/macgit/Views/MainWindow/WelcomeActivityView.swift @@ -19,6 +19,7 @@ import SwiftUI struct WelcomeActivityView: View { @Environment(\.colorScheme) private var colorScheme + @Environment(\.appTextScale) private var textScale let snapshot: WelcomeDashboardSnapshot let isLoading: Bool let onRepositoryOpened: (URL) -> Void @@ -27,9 +28,9 @@ struct WelcomeActivityView: View { VStack(alignment: .leading, spacing: 18) { HStack { VStack(alignment: .leading, spacing: 5) { - Text("Your commit activity").font(.title3.bold()) + Text("Your commit activity").font(.title3.bold().scaled(by: textScale)) Text("Last 30 days · 7 most recent repositories") - .font(.caption).foregroundStyle(.secondary) + .font(.caption.scaled(by: textScale)).foregroundStyle(.secondary) } Spacer() Image(systemName: "lock.shield") @@ -38,6 +39,7 @@ struct WelcomeActivityView: View { } if isLoading && snapshot.repositories.isEmpty { ProgressView("Reading local history…") + .font(.body.scaled(by: textScale)) .frame(maxWidth: .infinity, minHeight: 120) } else if snapshot.repositories.isEmpty { ContentUnavailableView("Your activity starts here", systemImage: "square.grid.3x3", @@ -62,7 +64,7 @@ struct WelcomeActivityView: View { Spacer() Text("Local only").foregroundStyle(.secondary) } - .font(.caption2) + .font(.caption2.scaled(by: textScale)) } .padding(20) .background(.background, in: RoundedRectangle(cornerRadius: 18)) diff --git a/macgit/Views/MainWindow/WelcomeAttentionView.swift b/macgit/Views/MainWindow/WelcomeAttentionView.swift index 086d712f..dbabf70e 100644 --- a/macgit/Views/MainWindow/WelcomeAttentionView.swift +++ b/macgit/Views/MainWindow/WelcomeAttentionView.swift @@ -19,6 +19,7 @@ import SwiftUI struct WelcomeAttentionView: View { @Environment(\.colorScheme) private var colorScheme + @Environment(\.appTextScale) private var textScale let repositories: [WelcomeRepositoryAttention] let isLoading: Bool let hasRepositories: Bool @@ -28,36 +29,38 @@ struct WelcomeAttentionView: View { var body: some View { VStack(alignment: .leading, spacing: 14) { HStack { - Text("Needs your attention").font(.title3.bold()) + Text("Needs your attention").font(.title3.bold().scaled(by: textScale)) Spacer() if isLoading { ProgressView().controlSize(.small) } } if repositories.isEmpty { Label(isLoading ? "Checking local repositories…" : hasRepositories ? "All caught up" : "Open a repository to see tasks here", systemImage: isLoading ? "clock" : "checkmark.circle") + .font(.body.scaled(by: textScale)) .foregroundStyle(.secondary) } else { Text("Current branches · Ahead / behind reflects the last fetch. No automatic fetch.") - .font(.caption).foregroundStyle(.secondary) + .font(.caption.scaled(by: textScale)).foregroundStyle(.secondary) ForEach(repositories) { repository in HStack(alignment: .center, spacing: 12) { Image(systemName: repository.priority == 0 ? "exclamationmark.triangle.fill" : "arrow.triangle.branch") .foregroundStyle(repository.priority == 0 ? Color.orange : Color.accentColor) .frame(width: 22) VStack(alignment: .leading, spacing: 5) { - Text(repository.name).font(.subheadline.bold()) + Text(repository.name).font(.subheadline.bold().scaled(by: textScale)) if !repository.unavailable { - Text(repository.branch).font(.caption).foregroundStyle(.secondary) + Text(repository.branch).font(.caption.scaled(by: textScale)).foregroundStyle(.secondary) } - Text(repository.summary).font(.caption).foregroundStyle(.secondary) + Text(repository.summary).font(.caption.scaled(by: textScale)).foregroundStyle(.secondary) if let error = repository.error, repository.priority < 5 { - Text(error).font(.caption).foregroundStyle(.secondary) + Text(error).font(.caption.scaled(by: textScale)).foregroundStyle(.secondary) } } .frame(maxWidth: .infinity, alignment: .leading) Button(repository.unavailable ? "Locate Folder" : repository.showsHistory ? "View History" : "Review") { onReview(repository) } + .font(.body.scaled(by: textScale)) .buttonStyle(.bordered) } .padding(12) @@ -66,7 +69,7 @@ struct WelcomeAttentionView: View { } if let updatedAt, !repositories.isEmpty { Text("Checked \(updatedAt.formatted(date: .omitted, time: .shortened))") - .font(.caption2).foregroundStyle(.secondary) + .font(.caption2.scaled(by: textScale)).foregroundStyle(.secondary) } } .padding(20) diff --git a/macgit/Views/MainWindow/WelcomeDashboardContent.swift b/macgit/Views/MainWindow/WelcomeDashboardContent.swift index df740d21..24149f54 100644 --- a/macgit/Views/MainWindow/WelcomeDashboardContent.swift +++ b/macgit/Views/MainWindow/WelcomeDashboardContent.swift @@ -19,6 +19,7 @@ import SwiftUI struct WelcomeDashboardContent: View { @Environment(\.colorScheme) private var colorScheme + @Environment(\.appTextScale) private var textScale let model: WelcomeDashboardModel let accountDisplayName: String? let repositoryCount: Int @@ -34,8 +35,9 @@ struct WelcomeDashboardContent: View { HStack(alignment: .top) { VStack(alignment: .leading, spacing: 6) { Text(accountDisplayName.map { "Welcome back, 👋 \($0)" } ?? "Welcome back") - .font(.largeTitle.bold()) + .font(.largeTitle.bold().scaled(by: textScale)) Text("A little perspective on your local work.") + .font(.body.scaled(by: textScale)) .foregroundStyle(.secondary) } Spacer() @@ -57,7 +59,7 @@ struct WelcomeDashboardContent: View { WelcomeAttentionView(repositories: model.attention, isLoading: model.isCheckingAttention, hasRepositories: repositoryCount > 0, updatedAt: model.attentionUpdatedAt, onReview: onReviewAttention) if let date = model.updatedAt { Text("Updated \(date.formatted(date: .omitted, time: .shortened)) · Dashboard reads local Git only") - .font(.caption2).foregroundStyle(.secondary) + .font(.caption2.scaled(by: textScale)).foregroundStyle(.secondary) } } .padding(24) diff --git a/macgit/Views/MainWindow/WelcomeOverviewCard.swift b/macgit/Views/MainWindow/WelcomeOverviewCard.swift index 113d35b5..d10af1bd 100644 --- a/macgit/Views/MainWindow/WelcomeOverviewCard.swift +++ b/macgit/Views/MainWindow/WelcomeOverviewCard.swift @@ -19,6 +19,7 @@ import SwiftUI struct WelcomeOverviewCard: View { @Environment(\.colorScheme) private var colorScheme + @Environment(\.appTextScale) private var textScale let title: String let value: String let detail: String @@ -28,14 +29,14 @@ struct WelcomeOverviewCard: View { var body: some View { VStack(alignment: .leading, spacing: 12) { Label(title, systemImage: icon) - .font(.subheadline.weight(.medium)) + .font(.subheadline.weight(.medium).scaled(by: textScale)) .foregroundStyle(tint) .lineLimit(1) Text(value) - .font(.system(size: 30, weight: .semibold, design: .rounded)) + .font(.system(size: 30, weight: .semibold, design: .rounded).scaled(by: textScale)) .monospacedDigit() Text(detail) - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) From 764ad21fed5aa72cf415f860a98dd21ecc9d00d9 Mon Sep 17 00:00:00 2001 From: Thanh Tran Date: Sat, 3 Oct 2026 18:35:31 +0700 Subject: [PATCH 3/4] feat: Scale diff and history views with app text scale --- .../Views/Common/DiffLineHighlightCache.swift | 4 +- macgit/Views/Common/DiffRenderBlock.swift | 9 ++-- macgit/Views/Common/DiffView.swift | 44 +++++++++++-------- macgit/Views/History/CommitFileListView.swift | 11 ++--- .../Views/History/CommitInfoPopoverView.swift | 16 ++++--- .../History/HistoryCommitMessageCell.swift | 7 +-- macgit/Views/History/HistoryView.swift | 29 ++++++------ macgit/Views/History/RefLabel.swift | 3 +- 8 files changed, 70 insertions(+), 53 deletions(-) diff --git a/macgit/Views/Common/DiffLineHighlightCache.swift b/macgit/Views/Common/DiffLineHighlightCache.swift index c83a5fa5..1a0cc640 100644 --- a/macgit/Views/Common/DiffLineHighlightCache.swift +++ b/macgit/Views/Common/DiffLineHighlightCache.swift @@ -16,11 +16,11 @@ final class DiffLineHighlightCache { entries[lineID] } - func text(for line: DiffLine, fileExtension: String) -> AttributedString { + func text(for line: DiffLine, fileExtension: String, fontSize: CGFloat = 12) -> AttributedString { if let cached = entries[line.id] { return cached } let highlighted = SyntaxHighlighter(fileExtension: fileExtension) - .attributedString(for: line.text, fontSize: 12) + .attributedString(for: line.text, fontSize: fontSize) if insertionOrder.count == capacity, let oldestLineID = insertionOrder.first { entries.removeValue(forKey: oldestLineID) insertionOrder.removeFirst() diff --git a/macgit/Views/Common/DiffRenderBlock.swift b/macgit/Views/Common/DiffRenderBlock.swift index 567b7750..ac7a1849 100644 --- a/macgit/Views/Common/DiffRenderBlock.swift +++ b/macgit/Views/Common/DiffRenderBlock.swift @@ -15,18 +15,19 @@ nonisolated struct DiffRenderBlock: Identifiable { let hunk: DiffHunk let lineRange: Range let offset: CGFloat + let scale: CGFloat var id: String { "\(hunk.id)-\(lineRange.lowerBound)" } var height: CGFloat { - Self.headerHeight + CGFloat(lineRange.count) * Self.rowHeight + Self.scrollerHeight + (Self.headerHeight + CGFloat(lineRange.count) * Self.rowHeight + Self.scrollerHeight) * scale } - var endOffset: CGFloat { offset + height + Self.spacing } + var endOffset: CGFloat { offset + height + Self.spacing * scale } - static func layout(hunks: [DiffHunk]) -> [Self] { + static func layout(hunks: [DiffHunk], scale: CGFloat = 1) -> [Self] { var offset: CGFloat = 0 return hunks.flatMap { hunk in DiffRenderBatch.ranges(lineCount: hunk.lines.count).map { range in - let block = Self(hunk: hunk, lineRange: range, offset: offset) + let block = Self(hunk: hunk, lineRange: range, offset: offset, scale: scale) offset = block.endOffset return block } diff --git a/macgit/Views/Common/DiffView.swift b/macgit/Views/Common/DiffView.swift index 618ecd8c..dd58b3e5 100644 --- a/macgit/Views/Common/DiffView.swift +++ b/macgit/Views/Common/DiffView.swift @@ -27,6 +27,7 @@ private func isChangedDiffLine(_ line: DiffLine) -> Bool { } struct DiffView: View { + @Environment(\.appTextScale) private var textScale let hunks: [DiffHunk] let file: StatusFile? let repositoryURL: URL? @@ -90,7 +91,7 @@ struct DiffView: View { } else if hunks.isEmpty { EmptyStateView(message: "No diff to display", detail: "Select a file to see changes") } else { - let blocks = DiffRenderBlock.layout(hunks: hunks) + let blocks = DiffRenderBlock.layout(hunks: hunks, scale: textScale) let range = renderedBlockRange.clamped(to: blocks.indices) ScrollView { VStack(alignment: .leading, spacing: 0) { @@ -125,7 +126,7 @@ struct DiffView: View { commitPatchDisabledReason: commitPatchDisabledReason ) .frame(height: block.height) - .padding(.bottom, DiffRenderBlock.spacing) + .padding(.bottom, DiffRenderBlock.spacing * textScale) } if let last = range.last, let end = blocks.last { Color.clear.frame(height: end.endOffset - blocks[last].endOffset) @@ -148,6 +149,10 @@ struct DiffView: View { highlightCache.removeAll() renderedBlockRange = 0.. let file: StatusFile? @@ -251,13 +257,13 @@ struct HunkView: View { // Hunk header HStack(spacing: 10) { Text(hunk.header) - .font(.system(size: 11, weight: .medium, design: .monospaced)) + .font(.system(size: 11, weight: .medium, design: .monospaced).scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) if hunk.lines.count > DiffRenderBatch.lineLimit { Text("\(lineRange.lowerBound + 1)–\(lineRange.upperBound) of \(hunk.lines.count) diff lines") - .font(.caption2) + .font(.caption2.scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) } @@ -275,26 +281,26 @@ struct HunkView: View { Button("Unstage") { unstageHunk() } - .buttonStyle(GlassButtonStyle(tint: .yellow, fontSize: 10)) + .buttonStyle(GlassButtonStyle(tint: .yellow, fontSize: 10 * textScale)) .pointingHandCursor() } else { Button("Stage") { stageHunk() } - .buttonStyle(GlassButtonStyle(tint: .accentColor, fontSize: 10)) + .buttonStyle(GlassButtonStyle(tint: .accentColor, fontSize: 10 * textScale)) .pointingHandCursor() Button("Discard") { let patch = DiffPatchBuilder.patchString(for: hunk, filePath: file!.path) performPatchAction(label: "Discard hunk in \(file!.displayName)", patch: patch, cached: false, reverse: true) } - .buttonStyle(GlassButtonStyle(tint: .red, fontSize: 10)) + .buttonStyle(GlassButtonStyle(tint: .red, fontSize: 10 * textScale)) .pointingHandCursor() } } } .padding(.horizontal, 10) - .frame(height: DiffRenderBlock.headerHeight) + .frame(height: DiffRenderBlock.headerHeight * textScale) .background(.secondary.opacity(0.06)) .overlay(alignment: .bottom) { Rectangle() @@ -317,7 +323,7 @@ struct HunkView: View { highlightCache: highlightCache, horizontalViewport: horizontalViewport ) - .frame(height: DiffRenderBlock.rowHeight) + .frame(height: DiffRenderBlock.rowHeight * textScale) .frame(minWidth: availableWidth, alignment: .leading) .onTapGesture { handleLineTap(at: index) @@ -339,7 +345,7 @@ struct HunkView: View { } action: { _, viewport in horizontalViewport = viewport } - .frame(height: CGFloat(lineRange.count) * DiffRenderBlock.rowHeight + DiffRenderBlock.scrollerHeight) + .frame(height: (CGFloat(lineRange.count) * DiffRenderBlock.rowHeight + DiffRenderBlock.scrollerHeight) * textScale) .frame(maxWidth: .infinity, alignment: .leading) .background { GeometryReader { geometry in @@ -628,6 +634,7 @@ struct HunkView: View { } struct DiffLineView: View { + @Environment(\.appTextScale) private var textScale let line: DiffLine let fileExtension: String let isSelected: Bool @@ -684,7 +691,7 @@ struct DiffLineView: View { HStack(spacing: 0) { if showsDiffGutter { Text(line.oldLineNumber.map(String.init) ?? "") - .font(.system(size: 10, design: .monospaced)) + .font(.system(size: 10, design: .monospaced).scaled(by: textScale)) .foregroundStyle(.tertiary) .frame(width: 36, alignment: .trailing) .padding(.trailing, 6) @@ -692,7 +699,7 @@ struct DiffLineView: View { // New line number Text(line.newLineNumber.map(String.init) ?? "") - .font(.system(size: 10, design: .monospaced)) + .font(.system(size: 10, design: .monospaced).scaled(by: textScale)) .foregroundStyle(.tertiary) .frame(width: 36, alignment: .trailing) .padding(.trailing, 6) @@ -700,7 +707,7 @@ struct DiffLineView: View { // Prefix if showsDiffGutter && !prefix.isEmpty { Text(prefix) - .font(.system(size: 11, weight: .semibold, design: .monospaced)) + .font(.system(size: 11, weight: .semibold, design: .monospaced).scaled(by: textScale)) .foregroundStyle(textColor.opacity(0.7)) .frame(width: 14, alignment: .center) } @@ -710,13 +717,14 @@ struct DiffLineView: View { DiffLongLineContent( lineID: line.id, text: line.text, - viewport: horizontalViewport.offsetBy(dx: -contentLeadingInset, dy: 0) + viewport: horizontalViewport.offsetBy(dx: -contentLeadingInset, dy: 0), + fontSize: 12 * textScale ) } else { Text(highlightedText) .lineLimit(1) .fixedSize(horizontal: true, vertical: false) - .task(id: line.id) { + .task(id: "\(line.id)-\(textScale)") { await loadHighlightedTextIfNeeded() } } @@ -740,11 +748,11 @@ struct DiffLineView: View { attributed = deferredHighlightedText } else if highlightCache != nil { attributed = AttributedString(line.text) - attributed.font = Font(NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)) + attributed.font = Font(NSFont.monospacedSystemFont(ofSize: 12 * textScale, weight: .regular)) attributed.foregroundColor = .primary } else { attributed = SyntaxHighlighter(fileExtension: fileExtension) - .attributedString(for: line.text, fontSize: 12) + .attributedString(for: line.text, fontSize: 12 * textScale) } // Keep diff metadata readable while allowing syntax colors in the code. @@ -768,7 +776,7 @@ struct DiffLineView: View { // Let the plain monospaced row reach the first frame before regex work. await Task.yield() guard !Task.isCancelled else { return } - let highlighted = highlightCache.text(for: line, fileExtension: fileExtension) + let highlighted = highlightCache.text(for: line, fileExtension: fileExtension, fontSize: 12 * textScale) guard !Task.isCancelled else { return } deferredHighlightedText = highlighted } diff --git a/macgit/Views/History/CommitFileListView.swift b/macgit/Views/History/CommitFileListView.swift index a2624956..c13d1524 100644 --- a/macgit/Views/History/CommitFileListView.swift +++ b/macgit/Views/History/CommitFileListView.swift @@ -23,6 +23,7 @@ import SwiftUI struct CommitFileListView: View { + @Environment(\.appTextScale) private var textScale let changes: [CommitFileChange] @Binding var selectedFile: CommitFileChange? var onPreview: ((CommitFileChange) -> Void)? = nil @@ -79,15 +80,15 @@ struct CommitFileListView: View { VStack(alignment: .leading, spacing: 1) { Text(fileName(from: change.path)) - .font(.system(size: 12, weight: .medium)) + .font(.system(size: 12, weight: .medium).scaled(by: textScale)) .lineLimit(1) Text(directory(from: change.path)) - .font(.system(size: 10)) + .font(.system(size: 10).scaled(by: textScale)) .foregroundStyle(.tertiary) .lineLimit(1) if let oldPath = change.oldPath { Text("From: \(oldPath)") - .font(.caption2) + .font(.caption2.scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) .help(oldPath) @@ -112,7 +113,7 @@ struct CommitFileListView: View { } } else { Text(change.status.displayText) - .font(.system(size: 10, weight: .medium)) + .font(.system(size: 10, weight: .medium).scaled(by: textScale)) .foregroundStyle(.secondary) .padding(.horizontal, 5) .padding(.vertical, 1) @@ -134,7 +135,7 @@ struct CommitFileListView: View { } if visibleFileCount < changes.count { Text("Loading more files… (\(visibleFileCount) of \(changes.count))") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) .selectionDisabled() .id(visibleFileCount) diff --git a/macgit/Views/History/CommitInfoPopoverView.swift b/macgit/Views/History/CommitInfoPopoverView.swift index 3a0f9bc2..8a867c52 100644 --- a/macgit/Views/History/CommitInfoPopoverView.swift +++ b/macgit/Views/History/CommitInfoPopoverView.swift @@ -28,6 +28,8 @@ struct CommitInfoPopoverView: View { case hash } + @Environment(\.appTextScale) private var textScale + let commit: Commit let fullMessage: String? let isLoadingMessage: Bool @@ -42,10 +44,10 @@ struct CommitInfoPopoverView: View { Image(systemName: "info.circle.fill") .foregroundStyle(.tint) Text("Commit details") - .font(.headline) + .font(.headline.scaled(by: textScale)) Spacer() Text(commit.shortHash) - .font(.system(.caption, design: .monospaced)) + .font(.system(.caption, design: .monospaced).scaled(by: textScale)) .foregroundStyle(.secondary) } @@ -61,14 +63,14 @@ struct CommitInfoPopoverView: View { LabeledContent("Commit") { Text(commit.hash) - .font(.system(.body, design: .monospaced)) + .font(.system(.body, design: .monospaced).scaled(by: textScale)) .textSelection(.enabled) } if !commit.parents.isEmpty { LabeledContent(commit.parents.count == 1 ? "Parent" : "Parents") { Text(commit.parents.map { String($0.prefix(12)) }.joined(separator: ", ")) - .font(.system(.body, design: .monospaced)) + .font(.system(.body, design: .monospaced).scaled(by: textScale)) .textSelection(.enabled) } } @@ -81,13 +83,14 @@ struct CommitInfoPopoverView: View { } } } + .font(.body.scaled(by: textScale)) Divider() VStack(alignment: .leading, spacing: 6) { HStack { Text("Commit message") - .font(.subheadline.weight(.semibold)) + .font(.subheadline.weight(.semibold).scaled(by: textScale)) Spacer() if isLoadingMessage { ProgressView() @@ -111,7 +114,7 @@ struct CommitInfoPopoverView: View { .frame(maxWidth: .infinity, minHeight: 90, alignment: .center) } } - .font(.body) + .font(.body.scaled(by: textScale)) .padding(10) .background(.quaternary.opacity(0.45), in: RoundedRectangle(cornerRadius: 8)) .frame(maxWidth: .infinity, minHeight: 90, maxHeight: 220) @@ -137,6 +140,7 @@ struct CommitInfoPopoverView: View { .foregroundStyle(copiedTarget == .hash ? .green : .primary) .onContinuousHover(perform: updateCopyCursor) } + .font(.body.scaled(by: textScale)) .buttonStyle(.borderless) } .padding(16) diff --git a/macgit/Views/History/HistoryCommitMessageCell.swift b/macgit/Views/History/HistoryCommitMessageCell.swift index 7cf7954b..dc357fb5 100644 --- a/macgit/Views/History/HistoryCommitMessageCell.swift +++ b/macgit/Views/History/HistoryCommitMessageCell.swift @@ -19,6 +19,7 @@ import SwiftUI struct HistoryCommitMessageCell: View { private static let emptyMessagePlaceholder = "" + @Environment(\.appTextScale) private var textScale let commit: Commit let graphModel: CommitGraphModel @@ -44,7 +45,7 @@ struct HistoryCommitMessageCell: View { if commit.refs.count > 3 { Text("+\(commit.refs.count - 3)") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) .help(commit.refs.dropFirst(3).joined(separator: "\n")) } @@ -53,13 +54,13 @@ struct HistoryCommitMessageCell: View { } Text(displayMessage) - .font(.callout) + .font(.callout.scaled(by: textScale)) .lineLimit(1) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) .help(displayMessage) } - .frame(height: 16) + .frame(height: 16 * textScale) .opacity(isDragActive ? 0.4 : 1) .background { HistoryTableIntrospectionView( diff --git a/macgit/Views/History/HistoryView.swift b/macgit/Views/History/HistoryView.swift index 4fde74c6..882979f5 100644 --- a/macgit/Views/History/HistoryView.swift +++ b/macgit/Views/History/HistoryView.swift @@ -24,6 +24,7 @@ import Combine import SwiftUI struct HistoryView: View { + @Environment(\.appTextScale) private var textScale private enum HistoryWindowTrimEdge { case newer case older @@ -637,7 +638,7 @@ struct HistoryView: View { TableColumn("Author") { commit in commitInteractionCell(for: commit) { Text("\(commit.author) <\(commit.email)>") - .font(.callout) + .font(.callout.scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) .help("\(commit.author) <\(commit.email)>") @@ -661,7 +662,7 @@ struct HistoryView: View { .month(.abbreviated) .year() ) - .font(.callout) + .font(.callout.scaled(by: textScale)) .foregroundStyle(.secondary) .monospacedDigit() .lineLimit(1) @@ -678,7 +679,7 @@ struct HistoryView: View { TableColumn("Commit") { commit in commitInteractionCell(for: commit) { Text(commit.shortHash) - .font(.callout.monospaced()) + .font(.callout.monospaced().scaled(by: textScale)) .foregroundStyle(.tertiary) .lineLimit(1) .help(commit.hash) @@ -723,7 +724,7 @@ struct HistoryView: View { if paging.isLoadingMore { ProgressView("Loading older commits…") - .font(.caption) + .font(.caption.scaled(by: textScale)) .padding(.horizontal, 12) .padding(.vertical, 6) .background(.regularMaterial, in: Capsule()) @@ -745,7 +746,7 @@ struct HistoryView: View { if commitPatchController.isPreparing { HStack(spacing: 8) { ProgressView().controlSize(.small) - Text("Checking and merging selected changes…").font(.callout) + Text("Checking and merging selected changes…").font(.callout.scaled(by: textScale)) Spacer() Button("Cancel") { commitPatchController.cancelPreparation() } } @@ -792,32 +793,32 @@ struct HistoryView: View { VStack(alignment: .leading, spacing: 1) { Text(displayCommitMessage(commit.message)) - .font(.system(size: 13, weight: .semibold)) + .font(.system(size: 13, weight: .semibold).scaled(by: textScale)) .lineLimit(1) HStack(spacing: 8) { Text(commit.author) - .font(.system(size: 11)) + .font(.system(size: 11).scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) Text("•") - .font(.system(size: 11)) + .font(.system(size: 11).scaled(by: textScale)) .foregroundStyle(.tertiary) Text(commit.email) - .font(.system(size: 11)) + .font(.system(size: 11).scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) Text("•") - .font(.system(size: 11)) + .font(.system(size: 11).scaled(by: textScale)) .foregroundStyle(.tertiary) Text(commit.date, format: .dateTime.year().month().day().hour().minute()) - .font(.system(size: 11)) + .font(.system(size: 11).scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) Text("•") - .font(.system(size: 11)) + .font(.system(size: 11).scaled(by: textScale)) .foregroundStyle(.tertiary) Text(commit.hash) - .font(.system(size: 11, design: .monospaced)) + .font(.system(size: 11, design: .monospaced).scaled(by: textScale)) .foregroundStyle(.tertiary) .lineLimit(1) } @@ -892,7 +893,7 @@ struct HistoryView: View { .foregroundStyle(.primary) .font(.system(size: 14, weight: .medium)) Text(file.path) - .font(.system(size: 13, weight: .semibold)) + .font(.system(size: 13, weight: .semibold).scaled(by: textScale)) .lineLimit(1) Spacer() } diff --git a/macgit/Views/History/RefLabel.swift b/macgit/Views/History/RefLabel.swift index 492996dc..968176c8 100644 --- a/macgit/Views/History/RefLabel.swift +++ b/macgit/Views/History/RefLabel.swift @@ -21,6 +21,7 @@ struct RefLabel: View { let text: String let graphColorIndex: Int? @Environment(\.backgroundProminence) private var backgroundProminence + @Environment(\.appTextScale) private var textScale init(text: String, graphColorIndex: Int? = nil) { self.text = text @@ -67,7 +68,7 @@ struct RefLabel: View { .font(.system(size: 10, weight: .semibold)) Text(displayText) - .font(.system(size: 11, weight: .semibold)) + .font(.system(size: 11, weight: .semibold).scaled(by: textScale)) } .lineLimit(1) .foregroundStyle( From 165ed71f4c96a3ee9de93d5c6fe5790240767d11 Mon Sep 17 00:00:00 2001 From: Thanh Tran Date: Sat, 3 Oct 2026 20:12:05 +0700 Subject: [PATCH 4/4] feat: Scale pull request view fonts with app text scale Apply appTextScale to pull request list, detail, changes, conversation, metadata, and commit sheet views. --- macgit/Views/FileStatus/CommitSheetView.swift | 14 ++++-- .../PullRequests/PullRequestChangesView.swift | 15 +++--- .../PullRequestConversationBlock.swift | 4 +- .../PullRequestDetailHeader.swift | 19 ++++---- .../PullRequestDetailTabBar.swift | 2 + .../PullRequests/PullRequestListView.swift | 47 ++++++++++++------- .../PullRequestMetadataSidebar.swift | 7 +-- 7 files changed, 66 insertions(+), 42 deletions(-) diff --git a/macgit/Views/FileStatus/CommitSheetView.swift b/macgit/Views/FileStatus/CommitSheetView.swift index acdef0fa..591ab22f 100644 --- a/macgit/Views/FileStatus/CommitSheetView.swift +++ b/macgit/Views/FileStatus/CommitSheetView.swift @@ -24,6 +24,7 @@ import SwiftUI struct CommitSheetView: View { @Environment(\.dismiss) private var dismiss + @Environment(\.appTextScale) private var textScale @AppStorage("commit.allChanges") private var commitAllChanges = false @ObservedObject var aiProviderController: AIProviderController @Binding var message: String @@ -38,14 +39,14 @@ struct CommitSheetView: View { var body: some View { VStack(spacing: 20) { Text("Commit Changes") - .font(.title2) + .font(.title2.scaled(by: textScale)) .fontWeight(.semibold) VStack(alignment: .leading, spacing: 8) { Text("Commit Message") - .font(.headline) + .font(.headline.scaled(by: textScale)) Text("Leave this empty to create a commit without a message.") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) ZStack(alignment: .topTrailing) { TextField( @@ -54,6 +55,7 @@ struct CommitSheetView: View { selection: $messageSelection, axis: .vertical ) + .font(.body.scaled(by: textScale)) .textFieldStyle(.roundedBorder) .frame(width: 400) .lineLimit(3...6) @@ -71,12 +73,12 @@ struct CommitSheetView: View { if !hasStagedChanges { HStack(spacing: 8) { Label("No files staged", systemImage: "exclamationmark.triangle") - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.orange) Spacer() Toggle("Commit all changes", isOn: $commitAllChanges) .toggleStyle(.checkbox) - .font(.system(size: 12)) + .font(.system(size: 12).scaled(by: textScale)) } .frame(width: 400, alignment: .leading) } @@ -86,12 +88,14 @@ struct CommitSheetView: View { Button("Cancel", role: .cancel) { dismiss() } + .font(.body.scaled(by: textScale)) .keyboardShortcut(.cancelAction) Button(message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "Commit Without Message" : "Commit") { onCommit(message, !hasStagedChanges && commitAllChanges) dismiss() } + .font(.body.scaled(by: textScale)) .keyboardShortcut(.defaultAction) .disabled(!hasStagedChanges && !commitAllChanges) } diff --git a/macgit/Views/PullRequests/PullRequestChangesView.swift b/macgit/Views/PullRequests/PullRequestChangesView.swift index 0709c04e..b7cccab8 100644 --- a/macgit/Views/PullRequests/PullRequestChangesView.swift +++ b/macgit/Views/PullRequests/PullRequestChangesView.swift @@ -19,6 +19,7 @@ import SwiftUI struct PullRequestChangesView: View { + @Environment(\.appTextScale) private var textScale let files: [PullRequestChangedFile] let isLoading: Bool let errorMessage: String? @@ -31,6 +32,7 @@ struct PullRequestChangesView: View { Group { if isLoading && files.isEmpty { ProgressView("Loading changes…") + .font(.body.scaled(by: textScale)) .frame(maxWidth: .infinity, maxHeight: .infinity) } else if let errorMessage, files.isEmpty { unavailableState( @@ -86,7 +88,7 @@ struct PullRequestChangesView: View { VStack(spacing: 0) { HStack(spacing: 8) { Text("\(files.count) file\(files.count == 1 ? "" : "s")") - .font(.subheadline) + .font(.subheadline.scaled(by: textScale)) .bold() Spacer() Button("Refresh changes", systemImage: "arrow.clockwise", action: onRefresh) @@ -111,10 +113,10 @@ struct PullRequestChangesView: View { VStack(alignment: .leading, spacing: 2) { Text(URL(fileURLWithPath: file.path).lastPathComponent) - .font(.system(size: 12, weight: .medium)) + .font(.system(size: 12, weight: .medium).scaled(by: textScale)) .lineLimit(1) Text(directory(for: file.path)) - .font(.system(size: 10)) + .font(.system(size: 10).scaled(by: textScale)) .foregroundStyle(.tertiary) .lineLimit(1) } @@ -124,7 +126,7 @@ struct PullRequestChangesView: View { if let additions = file.additions, let deletions = file.deletions { Text("+\(additions) −\(deletions)") - .font(.system(size: 10, design: .monospaced)) + .font(.system(size: 10, design: .monospaced).scaled(by: textScale)) .foregroundStyle(.secondary) } } @@ -142,12 +144,12 @@ struct PullRequestChangesView: View { VStack(spacing: 0) { HStack(spacing: 8) { Text(selectedFile.path) - .font(.subheadline.monospaced()) + .font(.subheadline.monospaced().scaled(by: textScale)) .lineLimit(1) .truncationMode(.middle) Spacer() Text(selectedFile.status.displayText) - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.secondary) } .padding(.horizontal, 12) @@ -194,6 +196,7 @@ struct PullRequestChangesView: View { HStack(spacing: 8) { if showsRefresh { Button("Try Again", systemImage: "arrow.clockwise", action: onRefresh) + .font(.body.scaled(by: textScale)) } } } diff --git a/macgit/Views/PullRequests/PullRequestConversationBlock.swift b/macgit/Views/PullRequests/PullRequestConversationBlock.swift index 5e7f082c..6ee81be2 100644 --- a/macgit/Views/PullRequests/PullRequestConversationBlock.swift +++ b/macgit/Views/PullRequests/PullRequestConversationBlock.swift @@ -19,6 +19,7 @@ import SwiftUI struct PullRequestConversationBlock: View { + @Environment(\.appTextScale) private var textScale let author: PullRequestAuthor let date: Date let action: String @@ -36,7 +37,7 @@ struct PullRequestConversationBlock: View { .foregroundStyle(.secondary) Spacer() } - .font(.subheadline) + .font(.subheadline.scaled(by: textScale)) .padding(.horizontal, 12) .padding(.vertical, 10) .background(Color(nsColor: .controlBackgroundColor)) @@ -44,6 +45,7 @@ struct PullRequestConversationBlock: View { Divider() content + .font(.body.scaled(by: textScale)) .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background(Color(nsColor: .textBackgroundColor)) diff --git a/macgit/Views/PullRequests/PullRequestDetailHeader.swift b/macgit/Views/PullRequests/PullRequestDetailHeader.swift index d65f0011..132d7e02 100644 --- a/macgit/Views/PullRequests/PullRequestDetailHeader.swift +++ b/macgit/Views/PullRequests/PullRequestDetailHeader.swift @@ -19,6 +19,7 @@ import SwiftUI struct PullRequestDetailHeader: View { + @Environment(\.appTextScale) private var textScale let summary: PullRequestSummary let onClose: () -> Void @@ -26,11 +27,11 @@ struct PullRequestDetailHeader: View { VStack(alignment: .leading, spacing: 12) { HStack(alignment: .firstTextBaseline, spacing: 8) { Text(summary.title) - .font(.title2) + .font(.title2.scaled(by: textScale)) .bold() .lineLimit(2) Text("#\(summary.number)") - .font(.title2) + .font(.title2.scaled(by: textScale)) .foregroundStyle(.secondary) Spacer(minLength: 12) Button("Close detail", systemImage: "xmark", action: onClose) @@ -41,7 +42,7 @@ struct PullRequestDetailHeader: View { HStack(spacing: 8) { Label(stateTitle, systemImage: stateIcon) - .font(.subheadline) + .font(.subheadline.scaled(by: textScale)) .bold() .foregroundStyle(.white) .padding(.horizontal, 10) @@ -50,23 +51,23 @@ struct PullRequestDetailHeader: View { PullRequestAuthorAvatar(author: summary.author) Text(summary.author.username) - .font(.subheadline) + .font(.subheadline.scaled(by: textScale)) .bold() Text("opened this pull request on \(summary.createdAt.formatted(date: .abbreviated, time: .omitted))") - .font(.subheadline) + .font(.subheadline.scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) } HStack(spacing: 7) { Text(summary.source.ref) - .branchBadge() + .branchBadge(scale: textScale) Image(systemName: "arrow.right") .font(.caption) .foregroundStyle(.secondary) .accessibilityLabel("into") Text(summary.target.ref) - .branchBadge() + .branchBadge(scale: textScale) } .accessibilityElement(children: .combine) .accessibilityLabel("From \(summary.source.ref) into \(summary.target.ref)") @@ -105,8 +106,8 @@ struct PullRequestDetailHeader: View { } private extension View { - func branchBadge() -> some View { - font(.subheadline.monospaced()) + func branchBadge(scale: CGFloat) -> some View { + font(.subheadline.monospaced().scaled(by: scale)) .foregroundStyle(.blue) .padding(.horizontal, 7) .padding(.vertical, 3) diff --git a/macgit/Views/PullRequests/PullRequestDetailTabBar.swift b/macgit/Views/PullRequests/PullRequestDetailTabBar.swift index b0f51077..3e128783 100644 --- a/macgit/Views/PullRequests/PullRequestDetailTabBar.swift +++ b/macgit/Views/PullRequests/PullRequestDetailTabBar.swift @@ -19,6 +19,7 @@ import SwiftUI struct PullRequestDetailTabBar: View { + @Environment(\.appTextScale) private var textScale @Binding var selection: PullRequestDetailTab var body: some View { @@ -28,6 +29,7 @@ struct PullRequestDetailTabBar: View { selection = tab } label: { Label(tab.rawValue, systemImage: tab.systemImage) + .font(.body.scaled(by: textScale)) .padding(.horizontal, 12) .padding(.vertical, 9) .contentShape(Rectangle()) diff --git a/macgit/Views/PullRequests/PullRequestListView.swift b/macgit/Views/PullRequests/PullRequestListView.swift index 0d4648b9..d51c12c6 100644 --- a/macgit/Views/PullRequests/PullRequestListView.swift +++ b/macgit/Views/PullRequests/PullRequestListView.swift @@ -20,6 +20,7 @@ import MarkdownUI import SwiftUI struct PullRequestListView: View { + @Environment(\.appTextScale) private var textScale @ObservedObject var controller: PullRequestController let repositoryURL: URL var accountConnectionErrorMessage: String? = nil @@ -78,10 +79,10 @@ struct PullRequestListView: View { } else if let errorMessage = controller.errorMessage { VStack(spacing: 12) { Text(errorMessage) - .font(.headline) + .font(.headline.scaled(by: textScale)) if controller.needsAccountConnectionAction { Text("Pull requests require an OAuth account over HTTPS. SSH keys are only used for Git fetch and push.") - .font(.callout) + .font(.callout.scaled(by: textScale)) .multilineTextAlignment(.center) .foregroundStyle(.tertiary) .frame(maxWidth: 440) @@ -96,9 +97,10 @@ struct PullRequestListView: View { } .disabled(controller.isLoading) } + .font(.body.scaled(by: textScale)) if let accountConnectionErrorMessage { Text(accountConnectionErrorMessage) - .font(.callout) + .font(.callout.scaled(by: textScale)) .multilineTextAlignment(.center) } } @@ -107,6 +109,7 @@ struct PullRequestListView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } else if controller.visibleItems.isEmpty { Text(emptyStateMessage) + .font(.body.scaled(by: textScale)) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity) } else if selectedPullRequestID != nil { @@ -225,6 +228,7 @@ struct PullRequestListView: View { if controller.isLoadingDetail, controller.selectedDetail?.id != selectedPullRequestID { ProgressView("Loading pull request…") + .font(.body.scaled(by: textScale)) .frame(maxWidth: .infinity, maxHeight: .infinity) } else if let detail = controller.selectedDetail, detail.id == selectedPullRequestID { @@ -276,6 +280,7 @@ struct PullRequestListView: View { ) } else { ProgressView("Loading pull request…") + .font(.body.scaled(by: textScale)) .frame(maxWidth: .infinity, maxHeight: .infinity) } } @@ -299,18 +304,21 @@ struct PullRequestListView: View { private var header: some View { HStack { Text("Pull Requests") - .font(.headline) + .font(.headline.scaled(by: textScale)) Spacer() Picker("Filter", selection: $controller.stateFilter) { ForEach(PullRequestListFilter.allCases) { filter in Text(filter.rawValue).tag(filter) } } + .font(.body.scaled(by: textScale)) .pickerStyle(.menu) - .frame(width: 110) + .frame(width: 110 * textScale) Toggle("Created by me", isOn: $controller.createdByMeOnly) + .font(.body.scaled(by: textScale)) .disabled(controller.selectedProviderAccountUsername == nil) Button("Create Pull Request", action: onRequestCreatePullRequest) + .font(.body.scaled(by: textScale)) .disabled(controller.isLoading || controller.errorMessage != nil) Button("Refresh pull requests", systemImage: "arrow.clockwise") { Task { @@ -342,7 +350,7 @@ struct PullRequestListView: View { .help("Previous page") Text("Page \(controller.currentPage)") - .font(.caption.monospacedDigit()) + .font(.caption.monospacedDigit().scaled(by: textScale)) .foregroundStyle(.secondary) .frame(minWidth: 54) @@ -381,6 +389,7 @@ struct PullRequestListView: View { } private struct PullRequestRow: View { + @Environment(\.appTextScale) private var textScale let summary: PullRequestSummary let isBusy: Bool let onOpen: () -> Void @@ -389,11 +398,6 @@ private struct PullRequestRow: View { var body: some View { HStack(alignment: .top, spacing: 6) { - Text("#\(summary.number)") - .font(.subheadline.monospacedDigit()) - .foregroundStyle(.secondary) - .frame(width: 18, alignment: .leading) - Image(systemName: pullRequestIcon) .font(.system(size: 15, weight: .semibold)) .foregroundStyle(pullRequestTint) @@ -403,7 +407,7 @@ private struct PullRequestRow: View { VStack(alignment: .leading, spacing: 5) { HStack(alignment: .firstTextBaseline, spacing: 8) { Text(summary.title) - .font(.headline) + .font(.headline.scaled(by: textScale)) .lineLimit(1) .truncationMode(.tail) .layoutPriority(1) @@ -415,7 +419,7 @@ private struct PullRequestRow: View { } Text("\(summary.source.ref) -> \(summary.target.ref)") - .font(.subheadline) + .font(.subheadline.scaled(by: textScale)) .foregroundStyle(.secondary) .lineLimit(1) .truncationMode(.middle) @@ -427,7 +431,7 @@ private struct PullRequestRow: View { Text("merged \(relativeString(for: mergedAt))") } } - .font(.caption) + .font(.caption.scaled(by: textScale)) .foregroundStyle(.tertiary) .lineLimit(1) } @@ -536,6 +540,7 @@ private struct PullRequestRow: View { } private struct PullRequestDetailPane: View { + @Environment(\.appTextScale) private var textScale let detail: PullRequestDetail let onClose: () -> Void let onOpenPullRequest: () -> Void @@ -615,11 +620,13 @@ private struct PullRequestDetailPane: View { ) { if detail.body.isEmpty { Text("No description provided.") + .font(.body.scaled(by: textScale)) .italic() .foregroundStyle(.secondary) } else { Markdown(detail.body) .markdownTheme(.gitHub) + .markdownTextStyle { FontSize(.em(textScale)) } .textSelection(.enabled) } } @@ -632,13 +639,14 @@ private struct PullRequestDetailPane: View { ) { Markdown(comment.body) .markdownTheme(.gitHub) + .markdownTextStyle { FontSize(.em(textScale)) } .textSelection(.enabled) } } if detail.comments.isEmpty { Text("No comments yet") - .font(.subheadline) + .font(.subheadline.scaled(by: textScale)) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, 8) @@ -653,6 +661,7 @@ private struct PullRequestDetailPane: View { } } .buttonStyle(.borderless) + .font(.body.scaled(by: textScale)) .foregroundStyle(.secondary) .disabled(isRefreshingDetail) .frame(maxWidth: .infinity, alignment: .center) @@ -690,6 +699,7 @@ private struct PullRequestDetailPane: View { Button("Close", action: onClose) .keyboardShortcut(.cancelAction) } + .font(.body.scaled(by: textScale)) .padding(16) .background(Color(nsColor: .controlBackgroundColor)) .overlay(alignment: .top) { @@ -743,7 +753,7 @@ private struct PullRequestDetailPane: View { TextField("Add a comment", text: $commentText) .textFieldStyle(.plain) - .font(.system(size: 13)) + .font(.system(size: 13).scaled(by: textScale)) .disabled(true) Spacer() @@ -777,14 +787,14 @@ private struct PullRequestDetailPane: View { .foregroundStyle(.secondary) Text("Comment on #\(detail.summary.number)") - .font(.system(size: 12, weight: .semibold)) + .font(.system(size: 12, weight: .semibold).scaled(by: textScale)) Spacer() } TextEditor(text: $commentText) .focused($isCommentFocused) - .font(.system(size: 13)) + .font(.system(size: 13).scaled(by: textScale)) .lineSpacing(2) .frame(minHeight: 48, maxHeight: 100) .padding(6) @@ -820,6 +830,7 @@ private struct PullRequestDetailPane: View { || commentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ) } + .font(.body.scaled(by: textScale)) } .padding(.horizontal, 16) .padding(.vertical, 10) diff --git a/macgit/Views/PullRequests/PullRequestMetadataSidebar.swift b/macgit/Views/PullRequests/PullRequestMetadataSidebar.swift index adfbfd15..4a7bfc30 100644 --- a/macgit/Views/PullRequests/PullRequestMetadataSidebar.swift +++ b/macgit/Views/PullRequests/PullRequestMetadataSidebar.swift @@ -19,6 +19,7 @@ import SwiftUI struct PullRequestMetadataSidebar: View { + @Environment(\.appTextScale) private var textScale let reviewers: [PullRequestAuthor] let assignees: [PullRequestAuthor] @@ -40,12 +41,12 @@ struct PullRequestMetadataSidebar: View { ) -> some View { VStack(alignment: .leading, spacing: 10) { Text(title) - .font(.subheadline) + .font(.subheadline.scaled(by: textScale)) .bold() if people.isEmpty { Text("None") - .font(.subheadline) + .font(.subheadline.scaled(by: textScale)) .foregroundStyle(.secondary) } else { ForEach(people, id: \.username) { person in @@ -53,7 +54,7 @@ struct PullRequestMetadataSidebar: View { PullRequestAuthorAvatar(author: person, size: 22) Text(person.username) } - .font(.subheadline) + .font(.subheadline.scaled(by: textScale)) .lineLimit(1) } }