From 45049e9561fe3a1b42fa77e3e849cd2dc2c67609 Mon Sep 17 00:00:00 2001 From: Jon Sterling Date: Sun, 15 Feb 2026 20:19:53 +0000 Subject: [PATCH] First cut at segmented controls --- .../AquaSegmentedCell.swift | 172 ++++++++++++++++++ .../AquaSegmentedControl.swift | 17 ++ 2 files changed, 189 insertions(+) create mode 100644 Sources/AquaKit/Controls and Views/AquaSegmentedCell.swift create mode 100644 Sources/AquaKit/Controls and Views/AquaSegmentedControl.swift diff --git a/Sources/AquaKit/Controls and Views/AquaSegmentedCell.swift b/Sources/AquaKit/Controls and Views/AquaSegmentedCell.swift new file mode 100644 index 0000000..ab29c29 --- /dev/null +++ b/Sources/AquaKit/Controls and Views/AquaSegmentedCell.swift @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: 2026 Jon Sterling +// +// SPDX-License-Identifier: MIT + +import AppKit + +open class AquaSegmentedCell: NSSegmentedCell { + func swoosh(in rect: NSRect) -> NSBezierPath { + let d: CGFloat = 20 + let r: CGFloat = 20 + + let start = NSPoint(x: rect.minX, y: rect.minY) + let end = NSPoint(x: rect.maxX, y: rect.maxY) + let firstCorner = NSPoint(x: start.x + d, y: rect.midY) + let lastCorner = NSPoint(x: end.x - d, y: rect.midY) + let horizontalEnd = NSPoint(x: lastCorner.x, y: firstCorner.y) + + let path = NSBezierPath() + path.move(to: start) + path.appendArc(from: firstCorner, to: horizontalEnd, radius: r) + path.appendArc(from: horizontalEnd, to: end, radius: r) + path.line(to: end) + + return path + } + + func segmentWidth(in cellFrame: NSRect) -> CGFloat { + cellFrame.width / CGFloat(segmentCount) + } + + func rect(forSegment segment: Int, in cellFrame: NSRect) -> NSRect { + let segmentWidth = segmentWidth(in: cellFrame) + let x = segmentWidth * CGFloat(segment) + return NSRect(x: x, y: cellFrame.minY, width: segmentWidth, height: cellFrame.height) + } + + open override func draw(withFrame cellFrame: NSRect, in controlView: NSView) { + let radius = 5.0 + let roundRect = NSBezierPath(roundedRect: cellFrame.insetBy(dx: 2, dy: 2), xRadius: radius, yRadius: radius) + NSColor.aquaSeparatorColor.setStroke() + + // Outer border + roundRect.stroke() + + NSGraphicsContext.scope { + roundRect.addClip() + + // Background + do { + let swoosh = swoosh(in: cellFrame) + + let topPath = NSBezierPath() + topPath.append(swoosh) + topPath.line(to: NSPoint(x: cellFrame.minX, y: cellFrame.maxY)) + topPath.line(to: NSPoint(x: cellFrame.minX, y: cellFrame.minY)) + topPath.close() + + let color1 = NSColor( + light: NSColor(white: 0.98, alpha: 1), + dark: NSColor(white: 0.98, alpha: 1), + ) + let color2 = NSColor( + light: NSColor(white: 0.96, alpha: 1), + dark: NSColor.graphiteColor.blended(withFraction: 0.8, of: .white)! + ) + let color3 = NSColor( + light: NSColor(white: 0.85, alpha: 1), + dark: NSColor.graphiteColor.blended(withFraction: 0.4, of: .white)! + ) + + NSGradient(colors: [color2, color3])?.draw(in: roundRect, angle: 90) + NSGradient(colors: [.white, color1])?.draw(in: topPath, angle: 270) + } + + + do { + // Segment borders + NSColor.aquaSeparatorColor.setStroke() + let segmentWidth = segmentWidth(in: cellFrame) + for segment in 1.. Int? { + for segment in 0.. Bool { + if let segment = segment(at: startPoint, in: controlView) { + savedSelectedSegment = selectedSegment + selectedSegment = segment + } + + return super.startTracking(at: startPoint, in: controlView) + } + + open override func continueTracking(last lastPoint: NSPoint, current currentPoint: NSPoint, in controlView: NSView) -> Bool { + if !controlView.bounds.contains(currentPoint), let saved = savedSelectedSegment { + selectedSegment = saved + } else if let segment = segment(at: currentPoint, in: controlView) { + selectedSegment = segment + } + + return super.continueTracking(last: lastPoint, current: currentPoint, in: controlView) + } + + open override func stopTracking(last lastPoint: NSPoint, current stopPoint: NSPoint, in controlView: NSView, mouseIsUp flag: Bool) { + savedSelectedSegment = nil + super.stopTracking(last: lastPoint, current: stopPoint, in: controlView, mouseIsUp: flag) + } +} diff --git a/Sources/AquaKit/Controls and Views/AquaSegmentedControl.swift b/Sources/AquaKit/Controls and Views/AquaSegmentedControl.swift new file mode 100644 index 0000000..de85b8a --- /dev/null +++ b/Sources/AquaKit/Controls and Views/AquaSegmentedControl.swift @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 Jon Sterling +// +// SPDX-License-Identifier: MIT + +import AppKit + +open class AquaSegmentedControl: NSSegmentedControl { + override open var intrinsicContentSize: NSSize { + return NSSize(width: 40 * segmentCount, height: 40) + } + + override open var isFlipped: Bool { false } + override open class var cellClass: AnyClass? { + get { AquaSegmentedCell.self } + set {} + } +} -- 2.51.2