Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions TablePro/Views/Results/Cells/DataGridBaseCellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,13 @@ class DataGridBaseCellView: NSTableCellView {
textField = cellTextField
addSubview(cellTextField)

textFieldTrailingConstraint = cellTextField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4)
textFieldTrailingConstraint = cellTextField.trailingAnchor.constraint(
equalTo: trailingAnchor,
constant: -DataGridMetrics.cellHorizontalInset
)

NSLayoutConstraint.activate([
cellTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4),
cellTextField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: DataGridMetrics.cellHorizontalInset),
textFieldTrailingConstraint,
cellTextField.centerYAnchor.constraint(equalTo: centerYAnchor),
])
Expand Down
10 changes: 8 additions & 2 deletions TablePro/Views/Results/Cells/DataGridCellRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,14 @@ final class DataGridCellRegistry {
cellView.addSubview(cell)

NSLayoutConstraint.activate([
cell.leadingAnchor.constraint(equalTo: cellView.leadingAnchor, constant: 4),
cell.trailingAnchor.constraint(equalTo: cellView.trailingAnchor, constant: -4),
cell.leadingAnchor.constraint(
equalTo: cellView.leadingAnchor,
constant: DataGridMetrics.cellHorizontalInset
),
cell.trailingAnchor.constraint(
equalTo: cellView.trailingAnchor,
constant: -DataGridMetrics.cellHorizontalInset
),
cell.centerYAnchor.constraint(equalTo: cellView.centerYAnchor),
])
}
Expand Down
7 changes: 5 additions & 2 deletions TablePro/Views/Results/Cells/DataGridChevronCellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ class DataGridChevronCellView: DataGridBaseCellView {
override func installAccessory() {
addSubview(chevronButton)
NSLayoutConstraint.activate([
chevronButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4),
chevronButton.trailingAnchor.constraint(
equalTo: trailingAnchor,
constant: -DataGridMetrics.cellHorizontalInset
),
chevronButton.centerYAnchor.constraint(equalTo: centerYAnchor),
chevronButton.widthAnchor.constraint(equalToConstant: 10),
chevronButton.heightAnchor.constraint(equalToConstant: 12),
Expand All @@ -34,7 +37,7 @@ class DataGridChevronCellView: DataGridBaseCellView {

override func textFieldTrailingInset(for content: DataGridCellContent, state: DataGridCellState) -> CGFloat {
let show = state.isEditable && !state.visualState.isDeleted
return show ? -18 : -4
return show ? -18 : -DataGridMetrics.cellHorizontalInset
}

@objc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ final class DataGridForeignKeyCellView: DataGridBaseCellView {
override func installAccessory() {
addSubview(fkButton)
NSLayoutConstraint.activate([
fkButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4),
fkButton.trailingAnchor.constraint(
equalTo: trailingAnchor,
constant: -DataGridMetrics.cellHorizontalInset
),
fkButton.centerYAnchor.constraint(equalTo: centerYAnchor),
fkButton.widthAnchor.constraint(equalToConstant: 16),
fkButton.heightAnchor.constraint(equalToConstant: 16),
Expand Down
10 changes: 10 additions & 0 deletions TablePro/Views/Results/Cells/DataGridMetrics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// DataGridMetrics.swift
// TablePro
//

import CoreGraphics

enum DataGridMetrics {
static let cellHorizontalInset: CGFloat = 4
}
31 changes: 19 additions & 12 deletions TablePro/Views/Results/DataGridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,7 @@ struct DataGridView: NSViewRepresentable {
tableView.action = #selector(TableViewCoordinator.handleClick(_:))
tableView.doubleAction = #selector(TableViewCoordinator.handleDoubleClick(_:))

let rowNumberColumn = NSTableColumn(identifier: ColumnIdentitySchema.rowNumberIdentifier)
rowNumberColumn.title = "#"
rowNumberColumn.width = 40
rowNumberColumn.minWidth = 40
rowNumberColumn.maxWidth = 60
rowNumberColumn.isEditable = false
rowNumberColumn.resizingMask = []
let rowNumberHeaderCell = SortableHeaderCell(textCell: "#")
rowNumberHeaderCell.font = rowNumberColumn.headerCell.font
rowNumberHeaderCell.alignment = .right
rowNumberColumn.headerCell = rowNumberHeaderCell
rowNumberColumn.headerCell.setAccessibilityLabel(String(localized: "Row number"))
let rowNumberColumn = Self.makeRowNumberColumn()
tableView.addTableColumn(rowNumberColumn)
rowNumberColumn.isHidden = !configuration.showRowNumbers

Expand Down Expand Up @@ -323,6 +312,24 @@ struct DataGridView: NSViewRepresentable {

// MARK: - Column Layout Helpers

@MainActor
static func makeRowNumberColumn() -> NSTableColumn {
let column = NSTableColumn(identifier: ColumnIdentitySchema.rowNumberIdentifier)
column.title = "#"
column.width = 40
column.minWidth = 40
column.maxWidth = 60
column.isEditable = false
column.resizingMask = []
let defaultHeaderFont = column.headerCell.font
let headerCell = SortableHeaderCell(textCell: "#")
headerCell.font = defaultHeaderFont
headerCell.alignment = .right
headerCell.setAccessibilityLabel(String(localized: "Row number"))
column.headerCell = headerCell
return column
}

static let firstDataTableColumnIndex: Int = 1

static func isDataTableColumn(_ tableColumnIndex: Int) -> Bool {
Expand Down
42 changes: 19 additions & 23 deletions TablePro/Views/Results/SortableHeaderCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ final class SortableHeaderCell: NSTableHeaderCell {
private static let indicatorPadding: CGFloat = 4
private static let indicatorSpacing: CGFloat = 2
private static let priorityFontSize: CGFloat = 9
private static let titleHorizontalPadding: CGFloat = 4
private static let defaultIndicatorSize = NSSize(width: 9, height: 6)

override init(textCell string: String) {
super.init(textCell: string)
Expand All @@ -30,27 +30,12 @@ final class SortableHeaderCell: NSTableHeaderCell {
}

override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
guard let direction = sortDirection else {
drawTitle(in: titleRect(forBounds: cellFrame), font: titleFont(isSorted: false))
return
}
drawTitle(in: titleRect(forBounds: cellFrame), font: titleFont(isSorted: sortDirection != nil))

let indicatorImage = Self.indicatorImage(for: direction)
let indicatorSize = indicatorImage?.size ?? NSSize(width: 9, height: 6)
let priorityText = priorityNumberString()
let priorityWidth = priorityText.map { Self.measureWidth(of: $0) } ?? 0
let reservedWidth = indicatorSize.width
+ Self.indicatorPadding * 2
+ (priorityText == nil ? 0 : priorityWidth + Self.indicatorSpacing)

let titleFrame = NSRect(
x: cellFrame.minX,
y: cellFrame.minY,
width: max(0, cellFrame.width - reservedWidth),
height: cellFrame.height
)
drawTitle(in: titleRect(forBounds: titleFrame), font: titleFont(isSorted: true))
guard let direction = sortDirection else { return }

let indicatorImage = Self.indicatorImage(for: direction)
let indicatorSize = indicatorImage?.size ?? Self.defaultIndicatorSize
let indicatorOriginX = cellFrame.maxX - Self.indicatorPadding - indicatorSize.width
let indicatorOriginY = cellFrame.midY - indicatorSize.height / 2
let indicatorRect = NSRect(
Expand All @@ -61,7 +46,8 @@ final class SortableHeaderCell: NSTableHeaderCell {
)
Self.drawIndicator(image: indicatorImage, in: indicatorRect)

if let priorityText {
if let priorityText = priorityNumberString() {
let priorityWidth = Self.measureWidth(of: priorityText)
let textOriginX = indicatorOriginX - Self.indicatorSpacing - priorityWidth
let textRect = NSRect(
x: textOriginX,
Expand All @@ -74,15 +60,25 @@ final class SortableHeaderCell: NSTableHeaderCell {
}

override func titleRect(forBounds rect: NSRect) -> NSRect {
let inset = min(Self.titleHorizontalPadding, rect.width / 2)
let inset = min(DataGridMetrics.cellHorizontalInset, rect.width / 2)
let availableWidth = max(0, rect.width - inset * 2 - reservedTrailingWidth())
return NSRect(
x: rect.minX + inset,
y: rect.minY,
width: max(0, rect.width - inset * 2),
width: availableWidth,
height: rect.height
)
}

private func reservedTrailingWidth() -> CGFloat {
guard let direction = sortDirection else { return 0 }
let indicatorWidth = Self.indicatorImage(for: direction)?.size.width
?? Self.defaultIndicatorSize.width
let priorityText = priorityNumberString()
let priorityComponent = priorityText.map { Self.measureWidth(of: $0) + Self.indicatorSpacing } ?? 0
return indicatorWidth + Self.indicatorPadding * 2 + priorityComponent
}

private func titleFont(isSorted: Bool) -> NSFont {
let baseFont = font ?? NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
guard isSorted else { return baseFont }
Expand Down
47 changes: 47 additions & 0 deletions TableProTests/Views/Results/SortableHeaderCellTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,51 @@ struct SortableHeaderCellTests {
#expect(titleRect.minX == 3)
#expect(titleRect.width == 0)
}

@Test("Sorted title rect reserves trailing space for the indicator")
func sortedTitleRectReservesTrailingSpaceForIndicator() {
let bounds = NSRect(x: 0, y: 0, width: 100, height: 24)

let unsorted = SortableHeaderCell(textCell: "id")
let sorted = SortableHeaderCell(textCell: "id")
sorted.sortDirection = .ascending

let unsortedRect = unsorted.titleRect(forBounds: bounds)
let sortedRect = sorted.titleRect(forBounds: bounds)

#expect(sortedRect.minX == unsortedRect.minX)
#expect(sortedRect.width < unsortedRect.width)
#expect(sortedRect.maxX <= bounds.maxX - DataGridMetrics.cellHorizontalInset)
}

@Test("Priority badge shrinks the sorted title rect further")
func priorityBadgeShrinksSortedTitleRectFurther() {
let bounds = NSRect(x: 0, y: 0, width: 100, height: 24)

let sorted = SortableHeaderCell(textCell: "id")
sorted.sortDirection = .ascending

let prioritized = SortableHeaderCell(textCell: "id")
prioritized.sortDirection = .ascending
prioritized.sortPriority = 2

let sortedWidth = sorted.titleRect(forBounds: bounds).width
let prioritizedWidth = prioritized.titleRect(forBounds: bounds).width

#expect(prioritizedWidth < sortedWidth)
}
}

@MainActor
@Suite("DataGridView.makeRowNumberColumn")
struct DataGridRowNumberColumnTests {
@Test("Row-number column header uses a right-aligned SortableHeaderCell")
func rowNumberHeaderIsRightAlignedSortableCell() throws {
let column = DataGridView.makeRowNumberColumn()

let headerCell = try #require(column.headerCell as? SortableHeaderCell)
#expect(headerCell.alignment == .right)
#expect(column.identifier == ColumnIdentitySchema.rowNumberIdentifier)
#expect(column.title == "#")
}
}
Loading