From 799128b59ba2c726d8335f2e129b2a781f096414 Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Fri, 18 Sep 2026 16:58:32 +0800 Subject: [PATCH] Refine Font size UI --- Frameworks/Resources/Localizable.xcstrings | 268 +++++++----------- .../Settings/MobileTerminalSettingsView.swift | 24 +- TFITests/MobileTerminalFontTests.swift | 24 +- TFM/Features/Settings/SettingsPageView.swift | 17 +- TFMTests/TerminalSettingsRenderingTests.swift | 31 +- 5 files changed, 173 insertions(+), 191 deletions(-) diff --git a/Frameworks/Resources/Localizable.xcstrings b/Frameworks/Resources/Localizable.xcstrings index b2096a62..bd728c33 100644 --- a/Frameworks/Resources/Localizable.xcstrings +++ b/Frameworks/Resources/Localizable.xcstrings @@ -1,70 +1,6 @@ { "sourceLanguage" : "en", "strings" : { - "Invalid authentication challenge." : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "认证质询无效。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "驗證詢問無效。" - } - } - } - }, - "Invalid authentication response." : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "认证响应无效。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "驗證回應無效。" - } - } - } - }, - "This server requires interactive authentication." : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "此服务器需要交互式认证。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "此伺服器需要互動式驗證。" - } - } - } - }, - "Interactive authentication" : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "交互式认证" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "互動式驗證" - } - } - } - }, "" : { "localizations" : { "zh-Hans" : { @@ -7063,7 +6999,7 @@ } }, "Custom" : { - "extractionState" : "stale", + "extractionState" : "manual", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -7191,22 +7127,6 @@ } } }, - "Dark Configuration" : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "深色配置" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "深色設定" - } - } - } - }, "Dark Custom Configuration" : { "localizations" : { "zh-Hans" : { @@ -10660,6 +10580,7 @@ } }, "Follow Theme" : { + "extractionState" : "manual", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -10692,22 +10613,6 @@ } } }, - "Font Configuration" : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "字体配置" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "字型設定" - } - } - } - }, "Font Family" : { "extractionState" : "manual", "localizations" : { @@ -10741,23 +10646,6 @@ } } }, - "Font Size Follows Theme" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "字号跟随主题" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "字級跟隨主題" - } - } - } - }, "For secrets, use the bearer token field above." : { "localizations" : { "zh-Hans" : { @@ -12707,6 +12595,23 @@ } } }, + "Interactive authentication" : { + "extractionState" : "stale", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "交互式认证" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "互動式驗證" + } + } + } + }, "Interactive partition editor." : { "extractionState" : "stale", "localizations" : { @@ -12741,6 +12646,40 @@ } } }, + "Invalid authentication challenge." : { + "extractionState" : "stale", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "认证质询无效。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "驗證詢問無效。" + } + } + } + }, + "Invalid authentication response." : { + "extractionState" : "stale", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "认证响应无效。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "驗證回應無效。" + } + } + } + }, "Invalid SFTP copy source path." : { "extractionState" : "manual", "localizations" : { @@ -13499,22 +13438,6 @@ } } }, - "Light Configuration" : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "浅色配置" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "淺色設定" - } - } - } - }, "Light Custom Configuration" : { "localizations" : { "zh-Hans" : { @@ -20785,6 +20708,23 @@ } } }, + "Saved workspaces could not be restored. The saved data has been kept." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法恢复已保存的工作区。已保留原有保存数据。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法回復已儲存的工作區。已保留原有儲存資料。" + } + } + } + }, "scratch files" : { "extractionState" : "stale", "localizations" : { @@ -20852,23 +20792,6 @@ } } }, - "Saved workspaces could not be restored. The saved data has been kept." : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无法恢复已保存的工作区。已保留原有保存数据。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法回復已儲存的工作區。已保留原有儲存資料。" - } - } - } - }, "Search" : { "comment" : "Placeholder text for a search field.", "isCommentAutoGenerated" : true, @@ -26027,23 +25950,6 @@ } } }, - "This font is unavailable on this device. Following the theme instead." : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "此设备上无法使用该字体,已改为跟随主题。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "此裝置上無法使用此字型,已改為跟隨主題。" - } - } - } - }, "This address already has a different trusted fingerprint. Confirm adding this key." : { "extractionState" : "stale", "localizations" : { @@ -26231,6 +26137,23 @@ } } }, + "This font is unavailable on this device. Following the theme instead." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此设备上无法使用该字体,已改为跟随主题。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "此裝置上無法使用此字型,已改為跟隨主題。" + } + } + } + }, "This forwarding will start after the host is saved." : { "comment" : "A description of the state of a forwarding that is waiting for the host to be saved.", "isCommentAutoGenerated" : true, @@ -26488,6 +26411,23 @@ } } }, + "This server requires interactive authentication." : { + "extractionState" : "stale", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此服务器需要交互式认证。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "此伺服器需要互動式驗證。" + } + } + } + }, "This session is not recording history. Enable Shell Integration and use bash, zsh, or fish." : { "localizations" : { "zh-Hans" : { diff --git a/TFI/Features/Settings/MobileTerminalSettingsView.swift b/TFI/Features/Settings/MobileTerminalSettingsView.swift index 102877d6..b5dc8527 100644 --- a/TFI/Features/Settings/MobileTerminalSettingsView.swift +++ b/TFI/Features/Settings/MobileTerminalSettingsView.swift @@ -12,15 +12,25 @@ struct MobileTerminalSettingsView: View { var body: some View { Form { - Section("Font") { - TerminalFontPicker(family: $font.family, families: TerminalFontCatalog.shared.families) - Toggle("Font Size Follows Theme", isOn: $font.sizeFollowsTheme) - Stepper(value: $font.size, in: 8...36, step: 1) { - LabeledContent("Font Size", value: font.size.formatted()) - }.disabled(font.sizeFollowsTheme) - } themeSection(dark: false) themeSection(dark: true) + Section("Font") { + TerminalFontPicker(family: $font.family, families: TerminalFontCatalog.shared.families) + VStack(alignment: .leading, spacing: 8) { + Picker("Font Size", selection: $font.sizeFollowsTheme) { + Text("Follow Theme").tag(true) + Text("Custom").tag(false) + } + .pickerStyle(.menu) + if !font.sizeFollowsTheme { + Stepper(value: $font.size, in: 8...36, step: 1) { + Text(font.size, format: .number.precision(.fractionLength(0))).monospacedDigit() + } + .accessibilityLabel("Font Size") + .accessibilityValue(font.size.formatted()) + } + } + } Section("Integration") { Toggle("Shell Integration", isOn: binding(\.shellIntegration)) } if let error = model.storageError { Text(error).foregroundStyle(.red) } } diff --git a/TFITests/MobileTerminalFontTests.swift b/TFITests/MobileTerminalFontTests.swift index 4a1efb04..bc5641f0 100644 --- a/TFITests/MobileTerminalFontTests.swift +++ b/TFITests/MobileTerminalFontTests.swift @@ -29,7 +29,9 @@ struct MobileTerminalFontTests { .environment(\.locale, Locale(identifier: language)) .frame(width: 280, height: 320) .background(.background) - try await capture(view, name: "mobile-terminal-font-options-\(language)") + try await withWindow(view) { window in + try await capture(window, name: "mobile-terminal-font-options-\(language)") + } } @Test(arguments: ["en", "zh-Hans", "zh-Hant"], ["", "Menlo"]) @@ -38,16 +40,24 @@ struct MobileTerminalFontTests { let defaults = try #require(UserDefaults(suiteName: domain)) defer { defaults.removePersistentDomain(forName: domain) } defaults.set(family, forKey: SettingsKey.terminalFontFamily) - defaults.set(family.isEmpty, forKey: SettingsKey.terminalFontFollowTheme) + defaults.set(18.0, forKey: SettingsKey.terminalFontSize) let view = NavigationStack { MobileTerminalSettingsView(model: TerminalSettingsModel()) } .defaultAppStorage(defaults) .environment(\.locale, Locale(identifier: language)) - try await capture(view, name: "mobile-terminal-settings-\(language)-\(family.isEmpty ? "theme" : family)") + let name = "mobile-terminal-settings-\(language)-\(family.isEmpty ? "theme" : family)" + try await withWindow(view) { window in + for (index, followsTheme) in [false, true, false].enumerated() { + defaults.set(followsTheme, forKey: SettingsKey.terminalFontFollowTheme) + try await capture(window, name: "\(name)-\(followsTheme ? "theme-size" : "custom-size")-\(index)") + #expect(defaults.double(forKey: SettingsKey.terminalFontSize) == 18) + #expect(defaults.string(forKey: SettingsKey.terminalFontFamily) == family) + } + } } - private func capture(_ view: some View, name: String) async throws { + private func withWindow(_ view: some View, perform: (UIWindow) async throws -> Void) async throws { let scene = try #require(UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first) let previousWindow = scene.keyWindow let window = UIWindow(windowScene: scene) @@ -60,7 +70,11 @@ struct MobileTerminalFontTests { window.rootViewController = nil previousWindow?.makeKey() } - controller.view.layoutIfNeeded() + try await perform(window) + } + + private func capture(_ window: UIWindow, name: String) async throws { + window.layoutIfNeeded() try await Task.sleep(for: .milliseconds(500)) let image = UIGraphicsImageRenderer(bounds: window.bounds).image { _ in window.drawHierarchy(in: window.bounds, afterScreenUpdates: true) diff --git a/TFM/Features/Settings/SettingsPageView.swift b/TFM/Features/Settings/SettingsPageView.swift index 9973961b..dc9ede27 100644 --- a/TFM/Features/Settings/SettingsPageView.swift +++ b/TFM/Features/Settings/SettingsPageView.swift @@ -710,7 +710,7 @@ private struct TerminalSettings: View { if let error = app.terminalSettings.storageError { Text(error).foregroundStyle(.orange) } - Section("Light Configuration") { + Section("Light") { Picker("Theme", selection: binding(\.lightTheme)) { ForEach(lightThemes) { Text($0.name).tag($0.name) } } @@ -728,19 +728,24 @@ private struct TerminalSettings: View { editConfiguration(.dark) } } header: { - Text("Dark Configuration") + Text("Dark") } footer: { Text( "Termind uses [Ghostty](https://ghostty.org) as its terminal engine. Custom configuration uses [Ghostty's configuration format](https://ghostty.org/docs/config/reference)." ) } - Section("Font Configuration") { + Section("Font") { TerminalFontPicker(family: $font.family, families: TerminalFontCatalog.shared.families) - Toggle("Font Size Follows Theme", isOn: $font.sizeFollowsTheme) - if !font.sizeFollowsTheme { - LabeledContent("Font Size") { + VStack(alignment: .leading, spacing: Metrics.Space.m) { + Picker("Font Size", selection: $font.sizeFollowsTheme) { + Text("Follow Theme").tag(true) + Text("Custom").tag(false) + } + .pickerStyle(.menu) + if !font.sizeFollowsTheme { HStack { Slider(value: $font.size, in: 9...24, step: 1) + .labelsHidden().accessibilityLabel("Font Size") Text(font.size, format: .number.precision(.fractionLength(0))) .monospacedDigit().frame(width: 24, alignment: .trailing) } diff --git a/TFMTests/TerminalSettingsRenderingTests.swift b/TFMTests/TerminalSettingsRenderingTests.swift index fbd1929e..2242448f 100644 --- a/TFMTests/TerminalSettingsRenderingTests.swift +++ b/TFMTests/TerminalSettingsRenderingTests.swift @@ -14,7 +14,7 @@ struct TerminalSettingsRenderingTests { let defaults = try #require(UserDefaults(suiteName: domain)) defer { defaults.removePersistentDomain(forName: domain) } defaults.set(family, forKey: SettingsKey.terminalFontFamily) - defaults.set(family.isEmpty, forKey: SettingsKey.terminalFontFollowTheme) + defaults.set(18.0, forKey: SettingsKey.terminalFontSize) let app = AppModel(hosts: HostLibraryModel()) let model = WindowModel(workspaces: []) model.openSettings(route: .category(.terminal)) @@ -23,9 +23,16 @@ struct TerminalSettingsRenderingTests { .defaultAppStorage(defaults) .environment(\.locale, Locale(identifier: language)) .frame(width: 640, height: 1100) - try await capture( - view, name: "terminal-settings-\(language)-\(family.isEmpty ? "theme" : family)", - size: NSSize(width: 640, height: 1100)) + let name = "terminal-settings-\(language)-\(family.isEmpty ? "theme" : family)" + try await withWindow(view, size: NSSize(width: 640, height: 1100)) { hostedView in + for (index, followsTheme) in [false, true, false].enumerated() { + defaults.set(followsTheme, forKey: SettingsKey.terminalFontFollowTheme) + try await capture( + hostedView, name: "\(name)-\(followsTheme ? "theme-size" : "custom-size")-\(index)") + #expect(defaults.double(forKey: SettingsKey.terminalFontSize) == 18) + #expect(defaults.string(forKey: SettingsKey.terminalFontFamily) == family) + } + } } @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) @@ -36,10 +43,12 @@ struct TerminalSettingsRenderingTests { .environment(\.locale, Locale(identifier: language)) .frame(width: 280, height: 320) .background(.background) - try await capture(view, name: "terminal-font-options-\(language)", size: NSSize(width: 280, height: 320)) + try await withWindow(view, size: NSSize(width: 280, height: 320)) { hostedView in + try await capture(hostedView, name: "terminal-font-options-\(language)") + } } - private func capture(_ view: some View, name: String, size: NSSize) async throws { + private func withWindow(_ view: some View, size: NSSize, perform: (NSView) async throws -> Void) async rethrows { let controller = NSHostingController(rootView: view) let window = NSWindow( contentRect: NSRect(origin: .zero, size: size), @@ -52,10 +61,14 @@ struct TerminalSettingsRenderingTests { window.contentViewController = nil window.close() } + try await perform(controller.view) + } + + private func capture(_ view: NSView, name: String) async throws { try await Task.sleep(for: .milliseconds(500)) - controller.view.layoutSubtreeIfNeeded() - let bitmap = try #require(controller.view.bitmapImageRepForCachingDisplay(in: controller.view.bounds)) - controller.view.cacheDisplay(in: controller.view.bounds, to: bitmap) + view.layoutSubtreeIfNeeded() + let bitmap = try #require(view.bitmapImageRepForCachingDisplay(in: view.bounds)) + view.cacheDisplay(in: view.bounds, to: bitmap) let data = try #require(bitmap.representation(using: .png, properties: [:])) Attachment.record(data, named: "\(name).png") #expect(data.count > 5_000)