Skip to content

Commit c5bfb8a

Browse files
author
evan.zhao
committed
Keep rich text backgrounds inside cards
1 parent f8d7883 commit c5bfb8a

8 files changed

Lines changed: 332 additions & 29 deletions

‎Sources/ClipDock/AppCommands.swift‎

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,9 @@ enum PinboardRealQACommand {
11641164
)
11651165
let githubAssets = try PanelQASamples.prepareRealGitHubSampleAssets(appSupportURL: appSupportURL)
11661166
let sourceIconPaths = try PanelQASamples.makeSourceAppIconPaths(outputDirectory: appSupportURL)
1167+
let terminalRichTextPreviewURL = try PanelQASamples.makeTerminalRichTextPreviewURL(
1168+
outputDirectory: appSupportURL
1169+
)
11671170
PanelCardAssetResolver.primePreviewImageCacheForSmoke(paths: [
11681171
imagePreviewURL.path,
11691172
filePreviewURL.path,
@@ -1174,7 +1177,8 @@ enum PinboardRealQACommand {
11741177
imagePayloadPath: imageURL.path,
11751178
filePreviewPath: filePreviewURL.path,
11761179
linkMetadata: githubAssets.linkMetadata,
1177-
sourceIconPaths: sourceIconPaths
1180+
sourceIconPaths: sourceIconPaths,
1181+
terminalRichTextPreviewPath: terminalRichTextPreviewURL.path
11781182
)
11791183
let frame = CommandLineWindowPlacement.bottomPanelFrame(
11801184
arguments: arguments,
@@ -1210,7 +1214,7 @@ enum PinboardRealQACommand {
12101214

12111215
let mode = mode(arguments: arguments)
12121216
let targetPinboardID = mode == "ai" ? "ai" : "untitled-new"
1213-
if mode == "overview" || mode == "preview" {
1217+
if mode == "overview" || mode == "preview" || mode == "verify-rich-text" {
12141218
contentView.updateListState(
12151219
.success(RustCoreListResult(
12161220
items: sampleItems,
@@ -1242,6 +1246,11 @@ enum PinboardRealQACommand {
12421246
break
12431247
}
12441248

1249+
if mode == "verify-rich-text" {
1250+
try verifyRichTextCard(contentView: contentView)
1251+
return
1252+
}
1253+
12451254
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
12461255
switch mode {
12471256
case "menu":
@@ -1269,6 +1278,97 @@ enum PinboardRealQACommand {
12691278
CommandLineArgumentReader.value(after: flag, in: arguments) ?? "toolbar"
12701279
}
12711280

1281+
@MainActor
1282+
private static func verifyRichTextCard(contentView: FloatingPanelContentView) throws {
1283+
let itemID = "panel-smoke-text"
1284+
RunLoop.main.run(until: Date().addingTimeInterval(0.25))
1285+
contentView.layoutSubtreeIfNeeded()
1286+
1287+
let theme = ClipDockTheme.current(for: contentView)
1288+
guard let card = contentView.smokeCardBoxes().first(where: { $0.itemID == itemID }) else {
1289+
throw CommandLineQAError(message: "rich text QA card is not visible")
1290+
}
1291+
guard colorAndAlphaDistance(card.fillColor, theme.card.textItemBackgroundColor) < 0.01 else {
1292+
throw CommandLineQAError(message: "rich text QA card fill does not use themed text surface")
1293+
}
1294+
guard let attributed = contentView.smokeBodyAttributedStrings(itemID: itemID).first else {
1295+
throw CommandLineQAError(message: "rich text QA body attributed string is missing")
1296+
}
1297+
let gitRange = (attributed.string as NSString).range(of: "git")
1298+
guard gitRange.location != NSNotFound else {
1299+
throw CommandLineQAError(message: "rich text QA body does not contain terminal sample text")
1300+
}
1301+
let expectedTerminalBackground = NSColor(deviceWhite: 0.24, alpha: 1)
1302+
guard let renderedBackground = attributed.attribute(
1303+
.backgroundColor,
1304+
at: gitRange.location,
1305+
effectiveRange: nil
1306+
) as? NSColor else {
1307+
throw CommandLineQAError(message: "rich text QA body did not preserve terminal background")
1308+
}
1309+
guard colorAndAlphaDistance(renderedBackground, expectedTerminalBackground) < 0.01 else {
1310+
throw CommandLineQAError(message: "rich text QA body terminal background changed")
1311+
}
1312+
guard let fadeView = descendant(in: card, identifier: "TextBodyBottomFade"),
1313+
let fadeColors = fadeView as? PanelTextBodyFadeColorProviding
1314+
else {
1315+
throw CommandLineQAError(message: "rich text QA bottom fade is missing")
1316+
}
1317+
guard colorAndAlphaDistance(
1318+
fadeColors.smokeFadeBottomColor,
1319+
theme.card.textBodyFadeBottomColor
1320+
) < 0.01 else {
1321+
throw CommandLineQAError(message: "rich text QA fade does not use themed bottom color")
1322+
}
1323+
1324+
print("rich_text_card_verify=passed")
1325+
print("card_fill=\(rgbaDescription(card.fillColor))")
1326+
print("theme_text_surface=\(rgbaDescription(theme.card.textItemBackgroundColor))")
1327+
print("content_background=\(rgbaDescription(renderedBackground))")
1328+
print("expected_terminal_background=\(rgbaDescription(expectedTerminalBackground))")
1329+
print("fade_bottom=\(rgbaDescription(fadeColors.smokeFadeBottomColor))")
1330+
print("theme_fade_bottom=\(rgbaDescription(theme.card.textBodyFadeBottomColor))")
1331+
}
1332+
1333+
@MainActor
1334+
private static func descendant(in view: NSView, identifier: String) -> NSView? {
1335+
if view.identifier?.rawValue == identifier {
1336+
return view
1337+
}
1338+
for subview in view.subviews {
1339+
if let found = descendant(in: subview, identifier: identifier) {
1340+
return found
1341+
}
1342+
}
1343+
return nil
1344+
}
1345+
1346+
private static func colorAndAlphaDistance(_ lhs: NSColor, _ rhs: NSColor) -> CGFloat {
1347+
guard let lhs = lhs.usingColorSpace(.sRGB),
1348+
let rhs = rhs.usingColorSpace(.sRGB)
1349+
else {
1350+
return .greatestFiniteMagnitude
1351+
}
1352+
1353+
return abs(lhs.redComponent - rhs.redComponent)
1354+
+ abs(lhs.greenComponent - rhs.greenComponent)
1355+
+ abs(lhs.blueComponent - rhs.blueComponent)
1356+
+ abs(lhs.alphaComponent - rhs.alphaComponent)
1357+
}
1358+
1359+
private static func rgbaDescription(_ color: NSColor) -> String {
1360+
guard let rgb = color.usingColorSpace(.sRGB) else {
1361+
return "unavailable"
1362+
}
1363+
return String(
1364+
format: "%.3f,%.3f,%.3f,%.3f",
1365+
rgb.redComponent,
1366+
rgb.greenComponent,
1367+
rgb.blueComponent,
1368+
rgb.alphaComponent
1369+
)
1370+
}
1371+
12721372
@MainActor
12731373
private static func installCleanDesktopBackdropIfNeeded(arguments: [String]) {
12741374
guard arguments.contains(cleanDesktopFlag) else {

‎Sources/ClipDock/AppRuntime.swift‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3391,6 +3391,10 @@ extension FloatingPanelContentView {
33913391
activeListPage.visibleCards()
33923392
}
33933393

3394+
func smokeBodyAttributedStrings(itemID: String) -> [NSAttributedString] {
3395+
activeListPage.visibleBodyAttributedStrings(for: itemID)
3396+
}
3397+
33943398
func smokeOrderedCardBoxes() -> [ClipboardItemCardBox] {
33953399
activeListPage.visibleCards()
33963400
}

‎Sources/ClipDock/PanelItemCardRenderer.swift‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ private struct RichTextCardBodyPreview {
179179
let promotedBackgroundColor: NSColor?
180180
}
181181

182+
@MainActor
183+
protocol PanelTextBodyFadeColorProviding {
184+
var smokeFadeBottomColor: NSColor { get }
185+
}
186+
182187
@MainActor
183188
final class PanelItemCardRenderer {
184189
private let cardAssetResolver: PanelCardAssetResolver
@@ -210,8 +215,7 @@ final class PanelItemCardRenderer {
210215
let richTextBodyPreviewPlan = isTextLikeItem
211216
? richTextBodyPreview(
212217
fallbackText: state.summaryText,
213-
assetRequest: state.assetRequest,
214-
defaultSurfaceColor: defaultCardBackgroundColor
218+
assetRequest: state.assetRequest
215219
)
216220
: nil
217221
let textSurfaceStyle = textCardSurfaceStyle(
@@ -696,8 +700,7 @@ final class PanelItemCardRenderer {
696700

697701
private func richTextBodyPreview(
698702
fallbackText: String,
699-
assetRequest: PanelCardAssetRequest,
700-
defaultSurfaceColor: NSColor
703+
assetRequest: PanelCardAssetRequest
701704
) -> RichTextCardBodyPreview? {
702705
guard let attributed = cardAssetResolver.richTextPreviewAttributedString(for: assetRequest),
703706
attributed.length > 0,
@@ -714,16 +717,12 @@ final class PanelItemCardRenderer {
714717

715718
let preview = NSMutableAttributedString(string: "\u{200E}")
716719
preview.append(attributed.attributedSubstring(from: NSRange(location: 0, length: boundedLength)))
717-
let promotedBackgroundColor = ClipboardRichTextPreviewStyler.promotedContentBackgroundColor(
718-
preview,
719-
surfaceColor: defaultSurfaceColor
720-
)
721-
let surfaceStyle = textCardSurfaceStyle(promotedBackgroundColor: promotedBackgroundColor)
720+
let surfaceStyle = textCardSurfaceStyle(promotedBackgroundColor: nil)
722721
let displayPlan = ClipboardRichTextPreviewStyler.inlineSurfaceDisplayPlan(
723722
preview,
724723
bodyColor: surfaceStyle.bodyTextColor,
725724
surfaceColor: surfaceStyle.backgroundColor,
726-
promotedBackgroundColor: promotedBackgroundColor
725+
promotesBackgroundToSurface: false
727726
)
728727
return RichTextCardBodyPreview(
729728
attributedString: displayPlan.attributedString,
@@ -1396,7 +1395,7 @@ final class PanelItemCardBodyTextView: NSView {
13961395
}
13971396

13981397
@MainActor
1399-
private final class PanelTextBodyFadeView: NSView {
1398+
private final class PanelTextBodyFadeView: NSView, PanelTextBodyFadeColorProviding {
14001399
private let topColor: NSColor
14011400
private let middleColor: NSColor
14021401
private let footerColor: NSColor
@@ -1420,6 +1419,10 @@ private final class PanelTextBodyFadeView: NSView {
14201419
nil
14211420
}
14221421

1422+
var smokeFadeBottomColor: NSColor {
1423+
bottomColor
1424+
}
1425+
14231426
override func draw(_ dirtyRect: NSRect) {
14241427
guard !bounds.isEmpty else { return }
14251428
let colors = [

‎Sources/ClipDock/PanelItemCollectionSurface.swift‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ final class PanelItemCollectionCell: NSCollectionViewItem {
163163
view.needsLayout = true
164164
}
165165

166+
func bodyAttributedStringsForSmoke() -> [NSAttributedString] {
167+
artifacts?.bodyLabels.map(\.attributedStringForTesting) ?? []
168+
}
169+
166170
func cleanupForRemoval() {
167171
cleanupHostedCard()
168172
}
@@ -484,6 +488,18 @@ final class PanelItemCollectionSurface: NSObject,
484488
visibleCells().compactMap(\.hostedCard)
485489
}
486490

491+
func visibleBodyAttributedStrings(for itemID: String) -> [NSAttributedString] {
492+
guard let index = indexByID[itemID] else { return [] }
493+
let indexPath = IndexPath(item: index, section: 0)
494+
guard collectionView.indexPathsForVisibleItems().contains(indexPath),
495+
let cell = collectionView.item(at: indexPath) as? PanelItemCollectionCell,
496+
cell.itemID == itemID
497+
else {
498+
return []
499+
}
500+
return cell.bodyAttributedStringsForSmoke()
501+
}
502+
487503
func collectionDocumentWidth() -> CGFloat {
488504
updateCollectionFrameWidth()
489505
return collectionView.frame.width

‎Sources/ClipDock/PanelPreviewUI.swift‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,18 @@ enum ClipboardRichTextPreviewStyler {
4444
bodyColor: NSColor,
4545
surfaceColor: NSColor,
4646
linkColor: NSColor = .linkColor,
47-
promotedBackgroundColor: NSColor? = nil
47+
promotedBackgroundColor: NSColor? = nil,
48+
promotesBackgroundToSurface: Bool = true
4849
) -> DisplayPlan {
4950
let mutable = NSMutableAttributedString(attributedString: source)
5051
let fullRange = NSRange(location: 0, length: mutable.length)
5152
guard fullRange.length > 0 else {
5253
return DisplayPlan(attributedString: mutable, promotedBackgroundColor: nil)
5354
}
5455

55-
let promotedBackgroundColor = promotedBackgroundColor
56-
?? contentBackgroundColor(in: source, surfaceColor: surfaceColor)
56+
let promotedBackgroundColor = promotesBackgroundToSurface
57+
? (promotedBackgroundColor ?? contentBackgroundColor(in: source, surfaceColor: surfaceColor))
58+
: nil
5759
mutable.enumerateAttributes(in: fullRange, options: []) { attributes, range, _ in
5860
let originalForeground = attributes[.foregroundColor] as? NSColor
5961
let preferredFallback = attributes[.link] == nil ? bodyColor : linkColor

‎Sources/ClipDock/QASupport.swift‎

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ enum PanelQASamples {
1919
let imageURL: URL
2020
}
2121

22+
private static let terminalRichTextSampleText = """
23+
Last login: Sat May 23
24+
16:07:44 on ttys006
25+
~/IdeaProjects
26+
git clone https://github.com/appdev/siyuan-unlock.git
27+
Cloning into 'siyuan-unlock'...
28+
"""
29+
2230
private static let sourceAppIconFixtures: [SourceAppIconFixture] = [
2331
SourceAppIconFixture(
2432
key: "Chrome",
@@ -559,6 +567,37 @@ enum PanelQASamples {
559567
return url
560568
}
561569

570+
static func makeTerminalRichTextPreviewURL(outputDirectory: URL) throws -> URL {
571+
let attributed = NSMutableAttributedString(
572+
string: terminalRichTextSampleText,
573+
attributes: [
574+
.font: NSFont.monospacedSystemFont(ofSize: 13, weight: .regular),
575+
.foregroundColor: NSColor.white,
576+
.backgroundColor: NSColor(deviceWhite: 0.24, alpha: 1)
577+
]
578+
)
579+
attributed.addAttribute(
580+
.foregroundColor,
581+
value: NSColor.systemGreen,
582+
range: (terminalRichTextSampleText as NSString).range(of: "git")
583+
)
584+
attributed.addAttribute(
585+
.foregroundColor,
586+
value: NSColor.systemBlue,
587+
range: (terminalRichTextSampleText as NSString).range(of: "~/IdeaProjects")
588+
)
589+
590+
let data = try attributed.data(
591+
from: NSRange(location: 0, length: attributed.length),
592+
documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf]
593+
)
594+
let directory = outputDirectory.appendingPathComponent("styled-text", isDirectory: true)
595+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
596+
let url = directory.appendingPathComponent("terminal-paste.rtf")
597+
try data.write(to: url, options: .atomic)
598+
return url
599+
}
600+
562601
@MainActor
563602
static func makePanelInteractionSmokeImageURL(outputDirectory: URL) throws -> URL {
564603
let image = NSImage(size: NSSize(width: 360, height: 220))
@@ -588,20 +627,27 @@ enum PanelQASamples {
588627
imagePayloadPath: String? = nil,
589628
filePreviewPath: String? = nil,
590629
linkMetadata: RustLinkMetadataSummary? = nil,
591-
sourceIconPaths: [String: String] = [:]
630+
sourceIconPaths: [String: String] = [:],
631+
terminalRichTextPreviewPath: String? = nil
592632
) -> [RustClipboardItemSummary] {
593633
let now = Int64(Date().timeIntervalSince1970 * 1000)
634+
let firstText = terminalRichTextPreviewPath == nil
635+
? "ClipDock 提供本地剪贴板历史、快速预览与 Pinboard 分类管理,适合高频跨应用工作流。"
636+
: terminalRichTextSampleText
594637
var items = [
595638
makeItem(
596639
id: "panel-smoke-text",
597640
itemType: "text",
598-
summary: "ClipDock 提供本地剪贴板历史、快速预览与 Pinboard 分类管理,适合高频跨应用工作流。",
599-
primaryText: "ClipDock 提供本地剪贴板历史、快速预览与 Pinboard 分类管理,适合高频跨应用工作流。",
600-
sourceAppName: "备忘录",
641+
summary: firstText,
642+
primaryText: firstText,
643+
sourceAppName: terminalRichTextPreviewPath == nil ? "备忘录" : "终端",
601644
timestamp: now,
602645
contentHash: "panel-smoke-panel-smoke-text",
603-
sourceAppIconPath: sourceIconPaths["Notes"],
604-
sizeBytes: 112
646+
sourceAppIconPath: terminalRichTextPreviewPath == nil
647+
? sourceIconPaths["Notes"]
648+
: sourceIconPaths["Terminal"],
649+
previewAssetPath: terminalRichTextPreviewPath,
650+
sizeBytes: Int64(firstText.utf8.count)
605651
),
606652
makeItem(
607653
id: "panel-smoke-image",

‎Tests/ClipboardPanelAppTests/PanelPreviewRichTextStylerTests.swift‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,31 @@ struct PanelPreviewRichTextStylerTests {
6868
#expect(source.attribute(.backgroundColor, at: 0, effectiveRange: nil) != nil)
6969
}
7070

71+
@Test
72+
func inlineSurfaceCanPreserveContentBackgroundWithoutPromotingIt() throws {
73+
let terminalBackground = NSColor(calibratedWhite: 0.24, alpha: 1)
74+
let source = NSAttributedString(
75+
string: "git clone https://github.com/appdev/siyuan-unlock.git",
76+
attributes: [
77+
.foregroundColor: NSColor.white,
78+
.backgroundColor: terminalBackground
79+
]
80+
)
81+
82+
let plan = ClipboardRichTextPreviewStyler.inlineSurfaceDisplayPlan(
83+
source,
84+
bodyColor: .black,
85+
surfaceColor: .white,
86+
promotesBackgroundToSurface: false
87+
)
88+
let background = try #require(
89+
plan.attributedString.attribute(.backgroundColor, at: 0, effectiveRange: nil) as? NSColor
90+
)
91+
92+
#expect(plan.promotedBackgroundColor == nil)
93+
#expect(colorAndAlphaDistance(background, terminalBackground) < 0.01)
94+
}
95+
7196
@Test
7297
func suppressingDefaultBackgroundPreservesSyntaxColorsAndMapsDefaultLinks() {
7398
let bodyColor = NSColor(calibratedWhite: 0.88, alpha: 1)

0 commit comments

Comments
 (0)