diff --git a/Clipy/Sources/Managers/MenuManager.swift b/Clipy/Sources/Managers/MenuManager.swift index 3f00cfb..7e2d75d 100644 --- a/Clipy/Sources/Managers/MenuManager.swift +++ b/Clipy/Sources/Managers/MenuManager.swift @@ -1569,7 +1569,28 @@ final class BoardManHeaderSegmentedControl: NSSegmentedControl { } } -private final class BoardManHistoryCellView: NSTableCellView { +final class BoardManSearchFieldCell: NSSearchFieldCell { + private func verticallyCentered(_ contentRect: NSRect, in bounds: NSRect) -> NSRect { + guard contentRect.height > 0 else { return contentRect } + var centered = contentRect + centered.origin.y = floor(bounds.midY - (contentRect.height / 2)) + return centered + } + + override func searchTextRect(forBounds rect: NSRect) -> NSRect { + return verticallyCentered(super.searchTextRect(forBounds: rect), in: rect) + } + + override func searchButtonRect(forBounds rect: NSRect) -> NSRect { + return verticallyCentered(super.searchButtonRect(forBounds: rect), in: rect) + } + + override func cancelButtonRect(forBounds rect: NSRect) -> NSRect { + return verticallyCentered(super.cancelButtonRect(forBounds: rect), in: rect) + } +} + +final class BoardManHistoryCellView: NSTableCellView { private let primaryLabel = NSTextField(labelWithString: "") private let metadataLabel = NSTextField(labelWithString: "") private let countBadge = NSTextField(labelWithString: "") @@ -1602,6 +1623,8 @@ private final class BoardManHistoryCellView: NSTableCellView { countBadge.alignment = .center countBadge.lineBreakMode = .byTruncatingTail countBadge.maximumNumberOfLines = 1 + countBadge.cell?.usesSingleLineMode = true + countBadge.cell?.wraps = false countBadge.isBordered = false countBadge.isEditable = false countBadge.wantsLayer = true @@ -1628,7 +1651,7 @@ private final class BoardManHistoryCellView: NSTableCellView { addSubview(countBadge) } - func configure(item: BoardManHistoryItem, + fileprivate func configure(item: BoardManHistoryItem, isSelected: Bool, usageStyle: String, useLiquidGlass: Bool, @@ -1664,22 +1687,35 @@ private final class BoardManHistoryCellView: NSTableCellView { needsLayout = true } + static func usageBadgeFrame(in bounds: NSRect, intrinsicWidth: CGFloat) -> NSRect { + let horizontalInset: CGFloat = 16 + let trailingInset: CGFloat = 22 + let badgeHeight: CGFloat = 20 + let maxContentWidth = max(0, bounds.width - horizontalInset - trailingInset) + let badgeWidth = min(maxContentWidth, min(76, max(38, ceil(intrinsicWidth) + 20))) + return NSIntegralRect(NSRect( + x: max(horizontalInset, floor(bounds.maxX - trailingInset - badgeWidth)), + y: floor(bounds.midY - (badgeHeight / 2)), + width: badgeWidth, + height: badgeHeight + )) + } + override func layout() { super.layout() let horizontalInset: CGFloat = 16 - let trailingInset: CGFloat = 14 + let trailingInset: CGFloat = 22 let accessoryGap: CGFloat = 10 let titleHeight: CGFloat = 18 let metadataHeight: CGFloat = 15 let textGap: CGFloat = 4 let textBlockHeight = titleHeight + textGap + metadataHeight let textBottom = floor((bounds.height - textBlockHeight) / 2) - let maxContentWidth = max(0, bounds.width - horizontalInset - trailingInset) - let badgeHeight: CGFloat = 20 - let badgeWidth: CGFloat = countBadge.isHidden - ? 0 - : min(maxContentWidth, min(72, max(34, countBadge.intrinsicContentSize.width + 16))) - let badgeX = max(horizontalInset, floor(bounds.width - trailingInset - badgeWidth)) + let badgeFrame = countBadge.isHidden + ? .zero + : Self.usageBadgeFrame(in: bounds, intrinsicWidth: countBadge.intrinsicContentSize.width) + let badgeWidth = badgeFrame.width + let badgeX = badgeFrame.minX let imageSize: CGFloat = inlineImageView.isHidden ? 0 : 32 let imageX = imageSize > 0 ? max(horizontalInset, badgeX - imageSize - accessoryGap) : 0 let textRightLimit = imageSize > 0 @@ -1708,12 +1744,7 @@ private final class BoardManHistoryCellView: NSTableCellView { )) } if !countBadge.isHidden { - countBadge.frame = NSIntegralRect(NSRect( - x: badgeX, - y: floor((bounds.height - badgeHeight) / 2), - width: badgeWidth, - height: badgeHeight - )) + countBadge.frame = badgeFrame } } } @@ -2239,6 +2270,7 @@ class BoardManPanel: NSPanel { setupGlassBackgroundIfNeeded() let search = NSSearchField(frame: .zero) + search.cell = BoardManSearchFieldCell(textCell: "") search.placeholderString = "Search clipboard history and snippets" search.target = self search.action = #selector(searchTextChanged(_:)) diff --git a/ClipyTests/EntitlementGateTests.swift b/ClipyTests/EntitlementGateTests.swift index 200e666..8262bef 100644 --- a/ClipyTests/EntitlementGateTests.swift +++ b/ClipyTests/EntitlementGateTests.swift @@ -515,13 +515,21 @@ final class BoardManPanelLayoutTests { let search = descendants.compactMap { $0 as? NSSearchField }.first #expect((search != nil) == expectsSearch || search?.isHidden == !expectsSearch) if expectsSearch, let search { - #expect(search.cell is NSSearchFieldCell, - "Search field is not using the native AppKit search cell.") + guard let searchCell = search.cell as? BoardManSearchFieldCell else { + Issue.record("Search field is not using the vertically centered Board-Man search cell.") + return + } #expect((search.layer?.borderWidth ?? 0) == 0, "Search field has a second custom layer border.") #expect(abs(search.frame.midY - tabs.frame.midY) <= 0.5, "Search input is not vertically centered with the header tabs.") #expect(search.frame.height == tabs.frame.height) + let textRect = searchCell.searchTextRect(forBounds: search.bounds) + let buttonRect = searchCell.searchButtonRect(forBounds: search.bounds) + #expect(abs(textRect.midY - search.bounds.midY) <= 0.5, + "Search text is not vertically centered inside its border.") + #expect(abs(buttonRect.midY - search.bounds.midY) <= 0.5, + "Search icon is not vertically centered inside its border.") } } @@ -534,6 +542,15 @@ final class BoardManPanelLayoutTests { #expect(table.rowHeight == panel.tableView(table, heightOfRow: 0), "Configured and delegated history row heights diverge.") #expect(table.rowHeight == 62) + + let rowBounds = NSRect(x: 0, y: 0, width: max(240, table.bounds.width), height: table.rowHeight) + let badgeFrame = BoardManHistoryCellView.usageBadgeFrame(in: rowBounds, intrinsicWidth: 16) + #expect(badgeFrame.maxX <= rowBounds.maxX - 21, + "Usage badge does not reserve enough trailing space inside the rounded row.") + #expect(abs(badgeFrame.midY - rowBounds.midY) <= 0.5, + "Usage badge is not vertically centered in the row.") + #expect(badgeFrame.width >= 38, + "Usage badge is too narrow and may clip its text.") } private func allSubviews(of view: NSView) -> [NSView] {