Skip to content

Commit 28218ef

Browse files
committed
refactor(datagrid): align selection visuals with native macOS patterns
Selection rendering was inverted: a 1.5pt 100% accent border was the dominant feature while the header and body fills were a barely visible 18% accent. The pattern in NSTableView row selection, Numbers, and Excel is the opposite, fill carries the meaning and borders are subtle or absent. Header now fills with NSColor.selectedContentBackgroundColor at full opacity when its column is selected, with text and sort indicator switched to alternateSelectedControlTextColor for contrast. This makes the header the strong anchor of a column selection, the way Numbers does it. Body fill bumped from 0.18 to 0.28 alpha so the selection reads as deliberate without competing with the row striping. Perimeter border now skips full-height rectangles (column selections, full-grid selectAll) because the header and fill already communicate the selection there. For arbitrary rectangles produced by drag and shift+drag, the border is now 1pt at 70% of the fill hue instead of 1.5pt at 100% accent, so it relates visually to the fill instead of overpowering it. Active-cell border is unchanged at 2pt controlAccentColor, only drawn when the selection spans more than one cell.
1 parent deba9b4 commit 28218ef

3 files changed

Lines changed: 37 additions & 20 deletions

File tree

TablePro/Views/Results/DataGridRowView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class DataGridRowView: NSTableRowView {
101101

102102
let fillColor: NSColor = isSelected
103103
? NSColor.unemphasizedSelectedContentBackgroundColor
104-
: NSColor.selectedContentBackgroundColor.withAlphaComponent(0.18)
104+
: NSColor.selectedContentBackgroundColor.withAlphaComponent(0.28)
105105
fillColor.setFill()
106106

107107
let schema = coordinator?.identitySchema

TablePro/Views/Results/Selection/GridSelectionOverlay.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ final class GridSelectionOverlay: NSView {
1212
weak var tableView: NSTableView?
1313
weak var coordinator: TableViewCoordinator?
1414

15-
private static let borderWidth: CGFloat = 1.5
15+
private static let borderWidth: CGFloat = 1.0
1616
private static let activeCellBorderWidth: CGFloat = 2.0
17+
private static let borderAlpha: CGFloat = 0.7
1718

1819
override init(frame frameRect: NSRect) {
1920
super.init(frame: frameRect)
@@ -34,11 +35,13 @@ final class GridSelectionOverlay: NSView {
3435
override func draw(_ dirtyRect: NSRect) {
3536
guard let tableView, let coordinator else { return }
3637
let schema = coordinator.identitySchema
38+
let totalRows = tableView.numberOfRows
3739

38-
NSColor.selectedContentBackgroundColor.setStroke()
40+
NSColor.selectedContentBackgroundColor.withAlphaComponent(Self.borderAlpha).setStroke()
3941
for rect in selection.rectangles {
4042
guard let frame = frame(for: rect, in: tableView, schema: schema) else { continue }
4143
guard frame.intersects(dirtyRect) else { continue }
44+
if isFullHeight(rect, totalRows: totalRows) { continue }
4245
let inset = frame.insetBy(dx: Self.borderWidth / 2, dy: Self.borderWidth / 2)
4346
let path = NSBezierPath(rect: inset)
4447
path.lineWidth = Self.borderWidth
@@ -57,6 +60,11 @@ final class GridSelectionOverlay: NSView {
5760
}
5861
}
5962

63+
private func isFullHeight(_ rect: GridRect, totalRows: Int) -> Bool {
64+
guard totalRows > 0 else { return false }
65+
return rect.rows.lowerBound <= 0 && rect.rows.upperBound >= totalRows - 1
66+
}
67+
6068
private func frame(for rect: GridRect, in tableView: NSTableView, schema: ColumnIdentitySchema) -> NSRect? {
6169
guard tableView.numberOfRows > 0, tableView.numberOfColumns > 0 else { return nil }
6270
let firstRow = max(0, rect.rows.lowerBound)

TablePro/Views/Results/SortableHeaderCell.swift

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,20 @@ final class SortableHeaderCell: NSTableHeaderCell {
3232

3333
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
3434
if isColumnSelected {
35-
NSColor.controlAccentColor.withAlphaComponent(0.18).setFill()
35+
NSColor.selectedContentBackgroundColor.setFill()
3636
cellFrame.fill()
3737
}
3838

39-
drawTitle(in: titleRect(forBounds: cellFrame), font: titleFont(isSorted: sortDirection != nil))
39+
let foreground = foregroundColor(emphasized: isColumnSelected)
40+
drawTitle(
41+
in: titleRect(forBounds: cellFrame),
42+
font: titleFont(isSorted: sortDirection != nil),
43+
color: foreground
44+
)
4045

4146
guard let direction = sortDirection else { return }
4247

43-
let indicatorImage = Self.indicatorImage(for: direction)
48+
let indicatorImage = Self.indicatorImage(for: direction, color: foreground)
4449
let indicatorSize = indicatorImage?.size ?? Self.defaultIndicatorSize
4550
let indicatorOriginX = cellFrame.maxX - Self.indicatorPadding - indicatorSize.width
4651
let indicatorOriginY = cellFrame.midY - indicatorSize.height / 2
@@ -53,15 +58,15 @@ final class SortableHeaderCell: NSTableHeaderCell {
5358
Self.drawIndicator(image: indicatorImage, in: indicatorRect)
5459

5560
if let priorityText = priorityNumberString() {
56-
let priorityWidth = Self.measureWidth(of: priorityText)
61+
let priorityWidth = Self.measureWidth(of: priorityText, color: foreground)
5762
let textOriginX = indicatorOriginX - Self.indicatorSpacing - priorityWidth
5863
let textRect = NSRect(
5964
x: textOriginX,
6065
y: cellFrame.minY,
6166
width: priorityWidth,
6267
height: cellFrame.height
6368
)
64-
Self.drawPriorityText(priorityText, in: textRect)
69+
Self.drawPriorityText(priorityText, in: textRect, color: foreground)
6570
}
6671
}
6772

@@ -78,10 +83,10 @@ final class SortableHeaderCell: NSTableHeaderCell {
7883

7984
private func reservedTrailingWidth() -> CGFloat {
8085
guard let direction = sortDirection else { return 0 }
81-
let indicatorWidth = Self.indicatorImage(for: direction)?.size.width
86+
let indicatorWidth = Self.indicatorImage(for: direction, color: .secondaryLabelColor)?.size.width
8287
?? Self.defaultIndicatorSize.width
8388
let priorityText = priorityNumberString()
84-
let priorityComponent = priorityText.map { Self.measureWidth(of: $0) + Self.indicatorSpacing } ?? 0
89+
let priorityComponent = priorityText.map { Self.measureWidth(of: $0, color: .secondaryLabelColor) + Self.indicatorSpacing } ?? 0
8590
return indicatorWidth + Self.indicatorPadding * 2 + priorityComponent
8691
}
8792

@@ -91,14 +96,18 @@ final class SortableHeaderCell: NSTableHeaderCell {
9196
return NSFontManager.shared.convert(baseFont, toHaveTrait: .boldFontMask)
9297
}
9398

94-
private func drawTitle(in rect: NSRect, font titleFont: NSFont) {
99+
private func foregroundColor(emphasized: Bool) -> NSColor {
100+
emphasized ? .alternateSelectedControlTextColor : .headerTextColor
101+
}
102+
103+
private func drawTitle(in rect: NSRect, font titleFont: NSFont, color: NSColor) {
95104
let paragraph = NSMutableParagraphStyle()
96105
paragraph.alignment = alignment
97106
paragraph.lineBreakMode = .byTruncatingTail
98107

99108
let attributes: [NSAttributedString.Key: Any] = [
100109
.font: titleFont,
101-
.foregroundColor: NSColor.headerTextColor,
110+
.foregroundColor: color,
102111
.paragraphStyle: paragraph
103112
]
104113

@@ -142,10 +151,10 @@ final class SortableHeaderCell: NSTableHeaderCell {
142151
return String(sortPriority)
143152
}
144153

145-
private static func indicatorImage(for direction: SortDirection) -> NSImage? {
154+
private static func indicatorImage(for direction: SortDirection, color: NSColor) -> NSImage? {
146155
let symbolName = direction == .ascending ? "chevron.up" : "chevron.down"
147156
let configuration = NSImage.SymbolConfiguration(pointSize: priorityFontSize, weight: .semibold)
148-
.applying(.init(hierarchicalColor: .secondaryLabelColor))
157+
.applying(.init(hierarchicalColor: color))
149158
return NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)?
150159
.withSymbolConfiguration(configuration)
151160
}
@@ -162,8 +171,8 @@ final class SortableHeaderCell: NSTableHeaderCell {
162171
)
163172
}
164173

165-
private static func drawPriorityText(_ text: String, in rect: NSRect) {
166-
let attributes = priorityAttributes()
174+
private static func drawPriorityText(_ text: String, in rect: NSRect, color: NSColor) {
175+
let attributes = priorityAttributes(color: color)
167176
let textSize = (text as NSString).size(withAttributes: attributes)
168177
let drawRect = NSRect(
169178
x: rect.minX,
@@ -174,14 +183,14 @@ final class SortableHeaderCell: NSTableHeaderCell {
174183
(text as NSString).draw(in: drawRect, withAttributes: attributes)
175184
}
176185

177-
private static func measureWidth(of text: String) -> CGFloat {
178-
(text as NSString).size(withAttributes: priorityAttributes()).width
186+
private static func measureWidth(of text: String, color: NSColor) -> CGFloat {
187+
(text as NSString).size(withAttributes: priorityAttributes(color: color)).width
179188
}
180189

181-
private static func priorityAttributes() -> [NSAttributedString.Key: Any] {
190+
private static func priorityAttributes(color: NSColor) -> [NSAttributedString.Key: Any] {
182191
[
183192
.font: NSFont.systemFont(ofSize: priorityFontSize, weight: .medium),
184-
.foregroundColor: NSColor.secondaryLabelColor
193+
.foregroundColor: color
185194
]
186195
}
187196
}

0 commit comments

Comments
 (0)