Skip to content

Commit 1ce519e

Browse files
Merge pull request #13 from ayangweb/feat/issue-8-scale-toast-notifications
feat(popup): scale toast notifications with popupScale preference
2 parents b35c6e1 + 31adba6 commit 1ce519e

2 files changed

Lines changed: 55 additions & 10 deletions

File tree

Sources/OpenClip/UI/Popup/ToastView.swift

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,16 @@ struct ToastView: View {
1111

1212
@AppStorage(SettingKey.popupTheme.name) private var selectedTheme: String = SettingKey.popupTheme.defaultValue
1313
@AppStorage(SettingKey.popupThemeColor.name) private var themeColor: String = SettingKey.popupThemeColor.defaultValue
14+
@AppStorage(SettingKey.popupScale.name) private var popupScale: Int = SettingKey.popupScale.defaultValue
1415
@Environment(\.colorScheme) private var colorScheme
1516

17+
/// Visual multiplier derived from the user's Popup Scale level (1...5) so the toast keeps pace
18+
/// with the popup bar it attaches to — same scale factor `PopupView` applies to the bar.
19+
private var scale: CGFloat { PopupMetrics.scaleMultiplier(for: popupScale) }
20+
21+
/// Corner radius for the toast bubble, scaled with the popup scale.
22+
private var cornerRadius: CGFloat { PopupMetrics.toastCornerRadius * scale }
23+
1624
private var isGlass: Bool {
1725
PopupThemeModel.category(fromStored: selectedTheme) == .glass
1826
}
@@ -48,43 +56,47 @@ struct ToastView: View {
4856
}
4957

5058
var body: some View {
51-
let content = HStack(spacing: 6) {
59+
let content = HStack(spacing: 6 * scale) {
5260
if feedback.isLoading {
61+
// macOS `.small` spinner is 16pt; scale its frame AND rendering so both the panel
62+
// sizing (hostingView.fittingSize) and the visible spinner track the popup scale.
5363
ProgressView()
5464
.controlSize(.small)
65+
.scaleEffect(scale)
66+
.frame(width: 16 * scale, height: 16 * scale)
5567
} else if let symbol = feedback.symbolName {
5668
Image(systemName: symbol)
57-
.font(.system(size: 10, weight: .medium))
69+
.font(.system(size: 10 * scale, weight: .medium))
5870
.foregroundColor(feedback.style == .error ? Color.red : (feedback.style == .success ? Color.accentColor : textColor))
5971
}
6072
Text(feedback.message)
61-
.font(.system(size: 11, weight: .regular))
73+
.font(.system(size: 11 * scale, weight: .regular))
6274
.lineLimit(1)
6375
.truncationMode(.tail)
6476
}
6577
.foregroundColor(textColor)
66-
.padding(.horizontal, 11)
67-
.padding(.vertical, 5)
78+
.padding(.horizontal, 11 * scale)
79+
.padding(.vertical, 5 * scale)
6880

6981
Group {
7082
if isGlass {
7183
content
7284
.background(
73-
RoundedRectangle(cornerRadius: PopupMetrics.toastCornerRadius, style: .continuous)
85+
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
7486
.fill(.ultraThinMaterial)
7587
)
76-
.clipShape(RoundedRectangle(cornerRadius: PopupMetrics.toastCornerRadius, style: .continuous))
88+
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
7789
.overlay(
78-
RoundedRectangle(cornerRadius: PopupMetrics.toastCornerRadius, style: .continuous)
90+
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
7991
.stroke(glassBorderColor, lineWidth: 1.0)
8092
)
8193
.shadow(color: Color.black.opacity(0.15), radius: 4, x: 0, y: 1)
8294
} else {
8395
content
8496
.background(opaqueBackground)
85-
.clipShape(RoundedRectangle(cornerRadius: PopupMetrics.toastCornerRadius, style: .continuous))
97+
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
8698
.overlay(
87-
RoundedRectangle(cornerRadius: PopupMetrics.toastCornerRadius, style: .continuous)
99+
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
88100
.stroke(opaqueBorder, lineWidth: 1.0)
89101
)
90102
.shadow(color: Color.black.opacity(effectiveTheme == "light" ? 0.10 : 0.20), radius: 4, x: 0, y: 1)

Tests/OpenClipTests/ToastPanelControllerTests.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,39 @@ final class ToastPanelControllerTests: XCTestCase {
120120
XCTAssertFalse(controller.isShowing, "info toast should auto-dismiss")
121121
}
122122

123+
/// Regression: the toast must scale with the user's Popup Scale preference, matching the popup
124+
/// bar it attaches to. `ToastView` reads `SettingKey.popupScale` via @AppStorage; a larger scale
125+
/// must produce a larger bubble (and vice-versa). The default level 3 must stay byte-identical
126+
/// to the legacy fixed layout (baseline 77×24), so only the relative growth is asserted.
127+
@MainActor
128+
func testToastScalesWithPopupScale() {
129+
let defaults = UserDefaults.standard
130+
let key = SettingKey.popupScale.name
131+
let original = defaults.object(forKey: key)
132+
defer {
133+
if let original {
134+
defaults.set(original, forKey: key)
135+
} else {
136+
defaults.removeObject(forKey: key)
137+
}
138+
}
139+
140+
let controller = ToastPanelController()
141+
142+
defaults.set(5, forKey: key)
143+
controller.show(StatusFeedback(message: "Copied", style: .success, symbolName: "checkmark"))
144+
let largeSize = controller.panelFrame.size
145+
controller.hide()
146+
147+
defaults.set(1, forKey: key)
148+
controller.show(StatusFeedback(message: "Copied", style: .success, symbolName: "checkmark"))
149+
let smallSize = controller.panelFrame.size
150+
controller.hide()
151+
152+
XCTAssertGreaterThan(largeSize.height, smallSize.height, "toast panel height must grow with popupScale")
153+
XCTAssertGreaterThan(largeSize.width, smallSize.width, "toast panel width must grow with popupScale")
154+
}
155+
123156
@MainActor
124157
func testLoadingToastHasNoTimer() async throws {
125158
let controller = ToastPanelController(autoDismissNanoseconds: 5_000_000)

0 commit comments

Comments
 (0)