diff --git a/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift b/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift index f72cc0d..67da64e 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift @@ -294,9 +294,9 @@ enum MenuBarRenderer { return image } - /// 设置页预览:按指定外观绘制到不透明底色上。 + /// 设置页预览:按指定外观栅格化,透明区域由界面叠加底色。 /// 绘制回调型图像每次布局或重绘都会重跑文字排版,因此栅格化一次,SwiftUI 之后只复用位图。 - static func preview(_ image: NSImage, dark: Bool) -> NSImage { + static func preview(_ image: NSImage, dark: Bool, backingScale: CGFloat? = nil) -> NSImage { let appearance = NSAppearance(named: dark ? .darkAqua : .aqua) let draw = { (rect: NSRect) in let content = { @@ -308,16 +308,19 @@ enum MenuBarRenderer { } if let appearance { appearance.performAsCurrentDrawingAppearance(content) } else { content() } } - let scale = NSScreen.main?.backingScaleFactor ?? 2 + let scale = backingScale ?? NSScreen.main?.backingScaleFactor ?? 2 guard let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(ceil(image.size.width * scale)), pixelsHigh: Int(ceil(image.size.height * scale)), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, - colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0), - let context = NSGraphicsContext(bitmapImageRep: rep) else { + colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0) else { return NSImage(size: image.size, flipped: false) { draw($0); return true } } + // 逻辑尺寸决定上下文的点到像素缩放,必须在创建上下文前设置。 rep.size = image.size + guard let context = NSGraphicsContext(bitmapImageRep: rep) else { + return NSImage(size: image.size, flipped: false) { draw($0); return true } + } NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = context draw(NSRect(origin: .zero, size: image.size)) diff --git a/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift b/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift index 513fbff..90b7b66 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift @@ -202,8 +202,8 @@ private func isolatedDefaults() -> UserDefaults { @MainActor @Suite struct SettingsTests { - @Test(arguments: [false, true]) - func settingsPreviewRasterizesOnceAndKeepsAppearanceColors(dark: Bool) throws { + @Test(arguments: [false, true], [CGFloat(1), CGFloat(2)]) + func settingsPreviewRasterizesOnceAndKeepsAppearanceColors(dark: Bool, scale: CGFloat) throws { var draws = 0 let source = NSImage(size: NSSize(width: 40, height: 18), flipped: false) { rect in draws += 1 @@ -212,7 +212,7 @@ private func isolatedDefaults() -> UserDefaults { return true } source.isTemplate = true - let preview = MenuBarRenderer.preview(source, dark: dark) + let preview = MenuBarRenderer.preview(source, dark: dark, backingScale: scale) let drawsAfterRender = draws #expect(drawsAfterRender >= 1) // 反复读取预览(布局、重绘)不能再触发源图的绘制回调。 @@ -222,9 +222,16 @@ private func isolatedDefaults() -> UserDefaults { #expect(preview.representations.contains { $0 is NSBitmapImageRep }) // 模板图按外观着色:浅色外观为黑,深色外观为白。 let rep = try #require(preview.representations.compactMap { $0 as? NSBitmapImageRep }.first) + #expect(rep.pixelsWide == Int(source.size.width * scale)) + #expect(rep.pixelsHigh == Int(source.size.height * scale)) let center = try #require(rep.colorAt(x: rep.pixelsWide / 2, y: rep.pixelsHigh / 2)?.usingColorSpace(.deviceRGB)) #expect(center.alphaComponent > 0.99) #expect((center.redComponent > 0.5) == dark) + for (x, y) in [(0, 0), (rep.pixelsWide - 1, 0), (0, rep.pixelsHigh - 1), (rep.pixelsWide - 1, rep.pixelsHigh - 1)] { + let corner = try #require(rep.colorAt(x: x, y: y)?.usingColorSpace(.deviceRGB)) + #expect(corner.alphaComponent > 0.99) + #expect((corner.redComponent > 0.5) == dark) + } } @Test func stackedCenteredStyleRendersDifferentPlacementAtSameWidth() {