Skip to content

Commit f6a2097

Browse files
authored
Merge pull request #3 from iSapozhnik/codex/textdiff-styling-ergonomics
Redesign styling API with additions/removals styles
2 parents c6d5bf2 + 3d5ecd1 commit f6a2097

11 files changed

Lines changed: 224 additions & 75 deletions

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,19 @@ import SwiftUI
8787
import TextDiff
8888

8989
let customStyle = TextDiffStyle(
90-
additionFillColor: NSColor.systemGreen.withAlphaComponent(0.28),
91-
additionStrokeColor: NSColor.systemGreen.withAlphaComponent(0.75),
92-
deletionFillColor: NSColor.systemRed.withAlphaComponent(0.24),
93-
deletionStrokeColor: NSColor.systemRed.withAlphaComponent(0.75),
94-
unchangedTextColor: .labelColor,
90+
additionsStyle: TextDiffChangeStyle(
91+
fillColor: NSColor.systemGreen.withAlphaComponent(0.28),
92+
strokeColor: NSColor.systemGreen.withAlphaComponent(0.75)
93+
),
94+
removalsStyle: TextDiffChangeStyle(
95+
fillColor: NSColor.systemRed.withAlphaComponent(0.24),
96+
strokeColor: NSColor.systemRed.withAlphaComponent(0.75),
97+
strikethrough: true
98+
),
99+
textColor: .labelColor,
95100
font: .monospacedSystemFont(ofSize: 15, weight: .regular),
96101
chipCornerRadius: 5,
97102
chipInsets: NSEdgeInsets(top: 1, left: 3, bottom: 1, right: 3),
98-
deletionStrikethrough: true,
99103
interChipSpacing: 4,
100104
lineSpacing: 2
101105
)
@@ -111,6 +115,8 @@ struct StyledDemoView: View {
111115
}
112116
```
113117

118+
Change-specific colors and text treatment live under `additionsStyle` and `removalsStyle`. Shared layout and typography stay on `TextDiffStyle` (`font`, `chipInsets`, `interChipSpacing`, `lineSpacing`, etc.).
119+
114120
## Behavior Notes
115121

116122
- Tokenization uses `NLTokenizer` (`.word`) and reconstructs punctuation/whitespace by filling range gaps.

Sources/TextDiff/AppKit/DiffTokenLayouter.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ enum DiffTokenLayouter {
139139
.foregroundColor: textColor(for: segment, style: style)
140140
]
141141

142-
if style.deletionStrikethrough, segment.kind == .delete, segment.tokenKind != .whitespace {
142+
if style.removalsStyle.strikethrough, segment.kind == .delete, segment.tokenKind != .whitespace {
143143
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
144144
}
145145

@@ -163,26 +163,26 @@ enum DiffTokenLayouter {
163163
private static func textColor(for segment: DiffSegment, style: TextDiffStyle) -> NSColor {
164164
switch segment.kind {
165165
case .equal:
166-
return style.unchangedTextColor
166+
return style.textColor
167167
case .delete:
168-
if let override = style.deletionTextColorOverride {
168+
if let override = style.removalsStyle.textColorOverride {
169169
return override
170170
}
171-
return adaptiveChipTextColor(for: style.deletionFillColor)
171+
return adaptiveChipTextColor(for: style.removalsStyle.fillColor)
172172
case .insert:
173-
if let override = style.additionTextColorOverride {
173+
if let override = style.additionsStyle.textColorOverride {
174174
return override
175175
}
176-
return adaptiveChipTextColor(for: style.additionFillColor)
176+
return adaptiveChipTextColor(for: style.additionsStyle.fillColor)
177177
}
178178
}
179179

180180
private static func chipFillColorForOperation(_ kind: DiffOperationKind, style: TextDiffStyle) -> NSColor? {
181181
switch kind {
182182
case .delete:
183-
return style.deletionFillColor
183+
return style.removalsStyle.fillColor
184184
case .insert:
185-
return style.additionFillColor
185+
return style.additionsStyle.fillColor
186186
case .equal:
187187
return nil
188188
}
@@ -191,9 +191,9 @@ enum DiffTokenLayouter {
191191
private static func chipStrokeColorForOperation(_ kind: DiffOperationKind, style: TextDiffStyle) -> NSColor? {
192192
switch kind {
193193
case .delete:
194-
return style.deletionStrokeColor
194+
return style.removalsStyle.strokeColor
195195
case .insert:
196-
return style.additionStrokeColor
196+
return style.additionsStyle.strokeColor
197197
case .equal:
198198
return nil
199199
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import AppKit
2+
import Foundation
3+
4+
/// Concrete change style used for additions and removals.
5+
public struct TextDiffChangeStyle: TextDiffStyling, @unchecked Sendable {
6+
public var fillColor: NSColor
7+
public var strokeColor: NSColor
8+
public var textColorOverride: NSColor?
9+
public var strikethrough: Bool
10+
11+
public init(
12+
fillColor: NSColor,
13+
strokeColor: NSColor,
14+
textColorOverride: NSColor? = nil,
15+
strikethrough: Bool = false
16+
) {
17+
self.fillColor = fillColor
18+
self.strokeColor = strokeColor
19+
self.textColorOverride = textColorOverride
20+
self.strikethrough = strikethrough
21+
}
22+
23+
public init(_ styling: some TextDiffStyling) {
24+
self.fillColor = styling.fillColor
25+
self.strokeColor = styling.strokeColor
26+
self.textColorOverride = styling.textColorOverride
27+
self.strikethrough = styling.strikethrough
28+
}
29+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import AppKit
2+
3+
public extension TextDiffChangeStyle {
4+
static let defaultAddition = TextDiffChangeStyle(
5+
fillColor: NSColor.systemGreen.withAlphaComponent(0.22),
6+
strokeColor: NSColor.systemGreen.withAlphaComponent(0.65),
7+
textColorOverride: nil,
8+
strikethrough: false
9+
)
10+
11+
static let defaultRemoval = TextDiffChangeStyle(
12+
fillColor: NSColor.systemRed.withAlphaComponent(0.22),
13+
strokeColor: NSColor.systemRed.withAlphaComponent(0.65),
14+
textColorOverride: nil,
15+
strikethrough: false
16+
)
17+
}

Sources/TextDiff/TextDiffStyle.swift

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,19 @@ import Foundation
33

44
/// Visual configuration for rendering text diff segments.
55
public struct TextDiffStyle: @unchecked Sendable {
6-
/// Fill color used for inserted token chips.
7-
public var additionFillColor: NSColor
8-
/// Stroke color used for inserted token chips.
9-
public var additionStrokeColor: NSColor
10-
/// Optional text color override for inserted tokens.
11-
public var additionTextColorOverride: NSColor?
12-
13-
/// Fill color used for deleted token chips.
14-
public var deletionFillColor: NSColor
15-
/// Stroke color used for deleted token chips.
16-
public var deletionStrokeColor: NSColor
17-
/// Optional text color override for deleted tokens.
18-
public var deletionTextColorOverride: NSColor?
6+
/// Visual style used for inserted token chips.
7+
public var additionsStyle: TextDiffChangeStyle
8+
/// Visual style used for deleted token chips.
9+
public var removalsStyle: TextDiffChangeStyle
1910

2011
/// Text color used for unchanged tokens.
21-
public var unchangedTextColor: NSColor
12+
public var textColor: NSColor
2213
/// Font used for all rendered tokens.
2314
public var font: NSFont
2415
/// Corner radius applied to changed-token chips.
2516
public var chipCornerRadius: CGFloat
2617
/// Insets used to draw changed-token chips. Horizontal insets are floored to 3 points by the renderer.
2718
public var chipInsets: NSEdgeInsets
28-
/// Controls whether deleted lexical tokens are drawn with a strikethrough.
29-
public var deletionStrikethrough: Bool
3019
/// Minimum visual gap between adjacent changed lexical chips.
3120
public var interChipSpacing: CGFloat
3221
/// Additional vertical spacing between wrapped lines.
@@ -35,54 +24,67 @@ public struct TextDiffStyle: @unchecked Sendable {
3524
/// Creates a style for rendering text diffs.
3625
///
3726
/// - Parameters:
38-
/// - additionFillColor: Fill color used for inserted token chips.
39-
/// - additionStrokeColor: Stroke color used for inserted token chips.
40-
/// - additionTextColorOverride: Optional text color override for inserted tokens.
41-
/// - deletionFillColor: Fill color used for deleted token chips.
42-
/// - deletionStrokeColor: Stroke color used for deleted token chips.
43-
/// - deletionTextColorOverride: Optional text color override for deleted tokens.
44-
/// - unchangedTextColor: Text color used for unchanged tokens.
27+
/// - additionsStyle: Change style used for inserted token chips.
28+
/// - removalsStyle: Change style used for deleted token chips.
29+
/// - textColor: Text color used for unchanged tokens.
4530
/// - font: Font used for all rendered tokens.
4631
/// - chipCornerRadius: Corner radius applied to changed-token chips.
4732
/// - chipInsets: Insets applied around changed-token text when drawing chips.
48-
/// - deletionStrikethrough: Whether deleted lexical tokens use a strikethrough.
4933
/// - interChipSpacing: Gap between adjacent changed lexical chips.
5034
/// - lineSpacing: Additional vertical spacing between wrapped lines.
5135
public init(
52-
additionFillColor: NSColor,
53-
additionStrokeColor: NSColor,
54-
additionTextColorOverride: NSColor? = nil,
55-
deletionFillColor: NSColor,
56-
deletionStrokeColor: NSColor,
57-
deletionTextColorOverride: NSColor? = nil,
58-
unchangedTextColor: NSColor = .labelColor,
36+
additionsStyle: TextDiffChangeStyle = .defaultAddition,
37+
removalsStyle: TextDiffChangeStyle = .defaultRemoval,
38+
textColor: NSColor = .labelColor,
5939
font: NSFont = .monospacedSystemFont(ofSize: 14, weight: .regular),
6040
chipCornerRadius: CGFloat = 4,
6141
chipInsets: NSEdgeInsets = NSEdgeInsets(top: 1, left: 3, bottom: 1, right: 3),
62-
deletionStrikethrough: Bool = false,
6342
interChipSpacing: CGFloat = 0,
6443
lineSpacing: CGFloat = 2
6544
) {
66-
self.additionFillColor = additionFillColor
67-
self.additionStrokeColor = additionStrokeColor
68-
self.additionTextColorOverride = additionTextColorOverride
69-
self.deletionFillColor = deletionFillColor
70-
self.deletionStrokeColor = deletionStrokeColor
71-
self.deletionTextColorOverride = deletionTextColorOverride
72-
self.unchangedTextColor = unchangedTextColor
45+
self.additionsStyle = additionsStyle
46+
self.removalsStyle = removalsStyle
47+
self.textColor = textColor
7348
self.font = font
7449
self.chipCornerRadius = chipCornerRadius
7550
self.chipInsets = chipInsets
76-
self.deletionStrikethrough = deletionStrikethrough
7751
self.interChipSpacing = interChipSpacing
7852
self.lineSpacing = lineSpacing
7953
}
8054

55+
/// Creates a style by converting protocol-based operation styles to concrete change styles.
56+
///
57+
/// - Parameters:
58+
/// - additionsStyle: Protocol-based style for inserted token chips.
59+
/// - removalsStyle: Protocol-based style for deleted token chips.
60+
/// - textColor: Text color used for unchanged tokens.
61+
/// - font: Font used for all rendered tokens.
62+
/// - chipCornerRadius: Corner radius applied to changed-token chips.
63+
/// - chipInsets: Insets applied around changed-token text when drawing chips.
64+
/// - interChipSpacing: Gap between adjacent changed lexical chips.
65+
/// - lineSpacing: Additional vertical spacing between wrapped lines.
66+
public init(
67+
additionsStyle: some TextDiffStyling,
68+
removalsStyle: some TextDiffStyling,
69+
textColor: NSColor = .labelColor,
70+
font: NSFont = .monospacedSystemFont(ofSize: 14, weight: .regular),
71+
chipCornerRadius: CGFloat = 4,
72+
chipInsets: NSEdgeInsets = NSEdgeInsets(top: 1, left: 3, bottom: 1, right: 3),
73+
interChipSpacing: CGFloat = 0,
74+
lineSpacing: CGFloat = 2
75+
) {
76+
self.init(
77+
additionsStyle: TextDiffChangeStyle(additionsStyle),
78+
removalsStyle: TextDiffChangeStyle(removalsStyle),
79+
textColor: textColor,
80+
font: font,
81+
chipCornerRadius: chipCornerRadius,
82+
chipInsets: chipInsets,
83+
interChipSpacing: interChipSpacing,
84+
lineSpacing: lineSpacing
85+
)
86+
}
87+
8188
/// The default style tuned for system green insertions and system red deletions.
82-
public static let `default` = TextDiffStyle(
83-
additionFillColor: NSColor.systemGreen.withAlphaComponent(0.22),
84-
additionStrokeColor: NSColor.systemGreen.withAlphaComponent(0.65),
85-
deletionFillColor: NSColor.systemRed.withAlphaComponent(0.22),
86-
deletionStrokeColor: NSColor.systemRed.withAlphaComponent(0.65)
87-
)
89+
public static let `default` = TextDiffStyle()
8890
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import AppKit
2+
3+
/// Change-specific visual configuration used for addition/removal rendering.
4+
public protocol TextDiffStyling {
5+
/// Fill color used for chip backgrounds.
6+
var fillColor: NSColor { get }
7+
/// Stroke color used for chip outlines.
8+
var strokeColor: NSColor { get }
9+
/// Optional text color override for chip text.
10+
var textColorOverride: NSColor? { get }
11+
/// Whether changed lexical content should render with a strikethrough.
12+
var strikethrough: Bool { get }
13+
}

Sources/TextDiff/TextDiffView.swift

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,21 @@ public struct TextDiffView: View {
5050

5151
#Preview("TextDiffView") {
5252
let style = TextDiffStyle(
53-
additionFillColor: .systemGreen.withAlphaComponent(0.28),
54-
additionStrokeColor: .systemGreen.withAlphaComponent(0.75),
55-
additionTextColorOverride: .labelColor,
56-
deletionFillColor: .systemRed.withAlphaComponent(0.24),
57-
deletionStrokeColor: .systemRed.withAlphaComponent(0.75),
58-
deletionTextColorOverride: .secondaryLabelColor,
59-
unchangedTextColor: .labelColor,
53+
additionsStyle: TextDiffChangeStyle(
54+
fillColor: .systemGreen.withAlphaComponent(0.28),
55+
strokeColor: .systemGreen.withAlphaComponent(0.75),
56+
textColorOverride: .labelColor
57+
),
58+
removalsStyle: TextDiffChangeStyle(
59+
fillColor: .systemRed.withAlphaComponent(0.24),
60+
strokeColor: .systemRed.withAlphaComponent(0.75),
61+
textColorOverride: .secondaryLabelColor,
62+
strikethrough: true
63+
),
64+
textColor: .labelColor,
6065
font: .systemFont(ofSize: 16, weight: .regular),
6166
chipCornerRadius: 3,
6267
chipInsets: NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0),
63-
deletionStrikethrough: true,
6468
interChipSpacing: 1,
6569
lineSpacing: 2
6670
)

Tests/TextDiffTests/NSTextDiffSnapshotTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ struct NSTextDiffSnapshotTests {
4949
@Test
5050
func custom_style_spacing_strikethrough() {
5151
var style = TextDiffStyle.default
52-
style.deletionStrikethrough = true
52+
style.removalsStyle.strikethrough = true
5353
style.interChipSpacing = 1
5454

5555
assertNSTextDiffSnapshot(

Tests/TextDiffTests/NSTextDiffViewTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func nsTextDiffViewStyleChangeDoesNotRecomputeDiff() {
8686
}
8787

8888
var style = TextDiffStyle.default
89-
style.deletionStrikethrough = true
89+
style.removalsStyle.strikethrough = true
9090
view.style = style
9191

9292
#expect(callCount == 1)
@@ -106,7 +106,7 @@ func nsTextDiffViewSetContentBatchesRecompute() {
106106
}
107107

108108
var style = TextDiffStyle.default
109-
style.deletionStrikethrough = true
109+
style.removalsStyle.strikethrough = true
110110
view.setContent(
111111
original: "old-2",
112112
updated: "new-2",
@@ -131,7 +131,7 @@ func nsTextDiffViewSetContentStyleOnlyDoesNotRecomputeDiff() {
131131
}
132132

133133
var style = TextDiffStyle.default
134-
style.deletionStrikethrough = true
134+
style.removalsStyle.strikethrough = true
135135
view.setContent(
136136
original: "old",
137137
updated: "new",

0 commit comments

Comments
 (0)