diff --git a/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift b/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift index ef8ad7b..f72cc0d 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/AppKit/MenuBarRenderer.swift @@ -294,20 +294,37 @@ enum MenuBarRenderer { return image } - /// 设置页预览:按指定外观绘制到不透明底色上 + /// 设置页预览:按指定外观绘制到不透明底色上。 + /// 绘制回调型图像每次布局或重绘都会重跑文字排版,因此栅格化一次,SwiftUI 之后只复用位图。 static func preview(_ image: NSImage, dark: Bool) -> NSImage { let appearance = NSAppearance(named: dark ? .darkAqua : .aqua) - return NSImage(size: image.size, flipped: false) { rect in - let draw = { + let draw = { (rect: NSRect) in + let content = { image.draw(in: rect) if image.isTemplate { (dark ? NSColor.white : NSColor.black).set() rect.fill(using: .sourceAtop) } } - if let appearance { appearance.performAsCurrentDrawingAppearance(draw) } else { draw() } - return true + if let appearance { appearance.performAsCurrentDrawingAppearance(content) } else { content() } } + let scale = 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 { + return NSImage(size: image.size, flipped: false) { draw($0); return true } + } + rep.size = image.size + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.current = context + draw(NSRect(origin: .zero, size: image.size)) + NSGraphicsContext.restoreGraphicsState() + let result = NSImage(size: image.size) + result.addRepresentation(rep) + return result } // MARK: 各指标 diff --git a/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift b/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift index 63e7396..513fbff 100644 --- a/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift +++ b/Packages/XStatsKit/Tests/XStatsUITests/XStatsUITests.swift @@ -202,6 +202,31 @@ private func isolatedDefaults() -> UserDefaults { @MainActor @Suite struct SettingsTests { + @Test(arguments: [false, true]) + func settingsPreviewRasterizesOnceAndKeepsAppearanceColors(dark: Bool) throws { + var draws = 0 + let source = NSImage(size: NSSize(width: 40, height: 18), flipped: false) { rect in + draws += 1 + NSColor.black.setFill() + rect.fill() + return true + } + source.isTemplate = true + let preview = MenuBarRenderer.preview(source, dark: dark) + let drawsAfterRender = draws + #expect(drawsAfterRender >= 1) + // 反复读取预览(布局、重绘)不能再触发源图的绘制回调。 + for _ in 0..<5 { _ = preview.tiffRepresentation } + #expect(draws == drawsAfterRender) + #expect(preview.size == source.size) + #expect(preview.representations.contains { $0 is NSBitmapImageRep }) + // 模板图按外观着色:浅色外观为黑,深色外观为白。 + let rep = try #require(preview.representations.compactMap { $0 as? NSBitmapImageRep }.first) + 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) + } + @Test func stackedCenteredStyleRendersDifferentPlacementAtSameWidth() { #expect(MenuBarStyle.options(for: .fan).contains(.stackedCenter)) var reading = MenuBarReading.sample