Skip to content
Draft
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
87 changes: 87 additions & 0 deletions Sources/ClickLight/ClickLightSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,55 @@ struct ClickLightSettingsView: View {
}
}

SettingsCard(title: "Geometry", subtitle: "Choose the marker shape used for each click type.") {
HStack {
Spacer()
Button {
viewModel.resetGeometryToDefaults()
} label: {
Label("Reset Geometry", systemImage: "arrow.counterclockwise")
}
.controlSize(.small)
}
VStack(spacing: 10) {
geometryShapeRow(
title: "Left Press",
subtitle: "Shape for left-button press pulses.",
shape: geometryBinding(\.leftPressShape)
)
geometryShapeRow(
title: "Left Release",
subtitle: "Shape for left-button release pulses.",
shape: geometryBinding(\.leftReleaseShape)
)
geometryShapeRow(
title: "Right Press",
subtitle: "Shape for right-button press pulses.",
shape: geometryBinding(\.rightPressShape)
)
geometryShapeRow(
title: "Right Release",
subtitle: "Shape for right-button release pulses.",
shape: geometryBinding(\.rightReleaseShape)
)
geometryShapeRow(
title: "Middle Press",
subtitle: "Shape for middle-button press pulses.",
shape: geometryBinding(\.middlePressShape)
)
geometryShapeRow(
title: "Middle Release",
subtitle: "Shape for middle-button release pulses.",
shape: geometryBinding(\.middleReleaseShape)
)
geometryShapeRow(
title: "Drag Trail",
subtitle: "Shape used for drag pulses when the normal trail is active.",
shape: geometryBinding(\.dragShape)
)
}
}

SettingsCard(title: "Color", subtitle: "Tint applied to every pulse.") {
VStack(alignment: .leading, spacing: 14) {
HStack(spacing: 10) {
Expand Down Expand Up @@ -812,6 +861,44 @@ struct ClickLightSettingsView: View {
)
}

private func geometryBinding(_ keyPath: WritableKeyPath<ClickSettings, ClickGeometryShape>) -> Binding<ClickGeometryShape> {
Binding(
get: { viewModel.settings[keyPath: keyPath] },
set: { newValue in
viewModel.update { $0[keyPath: keyPath] = newValue }
}
)
}

private func geometryShapeRow(title: String, subtitle: String, shape: Binding<ClickGeometryShape>) -> some View {
ModernRow(title: title, subtitle: subtitle) {
HStack(spacing: 8) {
Text(shape.wrappedValue.glyph)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(.primary)
.frame(width: 28, height: 28)
.background(Capsule().fill(Color.accentColor.opacity(0.14)))
.accessibilityHidden(true)

Picker("Shape", selection: shape) {
ForEach(ClickGeometryShape.allCases, id: \ .self) { option in
Label {
Text(option.title)
} icon: {
Text(option.glyph)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(.primary)
}
.tag(option)
}
}
.labelsHidden()
.pickerStyle(.menu)
.accessibilityLabel(title)
}
}
}

private func saveCurrentProfile() {
guard let profile = profileStore.saveProfile(named: profileName, from: viewModel.settings) else { return }
profileName = ""
Expand Down
112 changes: 97 additions & 15 deletions Sources/ClickLight/ClickOverlayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ final class ClickOverlayView: NSView {
context.setLineCap(.round)
context.setLineJoin(.round)

let shape = settings.geometryShape(for: pulse.kind)

switch pulse.kind {
case .leftDown:
drawGlowIfNeeded(
Expand All @@ -276,15 +278,15 @@ final class ClickOverlayView: NSView {
color: pulse.color,
alpha: fade * visualIntensity
)
drawRing(
drawShape(
context: context,
shape: shape,
point: pulse.point,
radius: pulse.baseSize * (0.18 + 0.62 * eased),
lineWidth: lineWidth,
color: pulse.color,
alpha: alpha
)
drawDot(context: context, point: pulse.point, radius: pulse.baseSize * 0.085, color: pulse.color, alpha: alpha * 0.75)
case .leftUp:
let releaseRadius = pulse.baseSize * (0.76 - 0.42 * eased)
let releaseAlpha = alpha * 0.55
Expand All @@ -295,15 +297,15 @@ final class ClickOverlayView: NSView {
color: pulse.color,
alpha: fade * visualIntensity * 0.45
)
drawRing(
drawShape(
context: context,
shape: shape,
point: pulse.point,
radius: releaseRadius,
lineWidth: lineWidth * 0.55,
color: pulse.color,
alpha: releaseAlpha
)
drawDot(context: context, point: pulse.point, radius: pulse.baseSize * 0.055, color: pulse.color, alpha: releaseAlpha * 0.6)
case .rightDown:
drawGlowIfNeeded(
context: context,
Expand All @@ -312,15 +314,15 @@ final class ClickOverlayView: NSView {
color: pulse.color,
alpha: fade * visualIntensity
)
drawRing(
drawShape(
context: context,
shape: shape,
point: pulse.point,
radius: pulse.baseSize * (0.18 + 0.54 * eased),
lineWidth: lineWidth,
color: pulse.color,
alpha: alpha
)
drawCrosshair(context: context, point: pulse.point, size: pulse.baseSize * 0.28, color: pulse.color, alpha: alpha * 0.85)
case .rightUp:
let releaseRadius = pulse.baseSize * (0.68 - 0.36 * eased)
let releaseAlpha = alpha * 0.5
Expand All @@ -331,15 +333,15 @@ final class ClickOverlayView: NSView {
color: pulse.color,
alpha: fade * visualIntensity * 0.4
)
drawRing(
drawShape(
context: context,
shape: shape,
point: pulse.point,
radius: releaseRadius,
lineWidth: lineWidth * 0.55,
color: pulse.color,
alpha: releaseAlpha
)
drawCrosshair(context: context, point: pulse.point, size: pulse.baseSize * (0.16 + 0.08 * fade), color: pulse.color, alpha: releaseAlpha * 0.7)
case .middleDown:
drawGlowIfNeeded(
context: context,
Expand All @@ -348,17 +350,17 @@ final class ClickOverlayView: NSView {
color: pulse.color,
alpha: fade * visualIntensity
)
drawRing(
drawShape(
context: context,
shape: shape,
point: pulse.point,
radius: pulse.baseSize * (0.16 + 0.52 * eased),
lineWidth: lineWidth,
color: pulse.color,
alpha: alpha
)
drawDiamond(context: context, point: pulse.point, size: pulse.baseSize * 0.24, color: pulse.color, alpha: alpha * 0.86)
case .middleUp:
let releaseSize = pulse.baseSize * (0.32 - 0.12 * eased)
let releaseRadius = pulse.baseSize * (0.32 - 0.12 * eased)
let releaseAlpha = alpha * 0.5
drawGlowIfNeeded(
context: context,
Expand All @@ -367,14 +369,24 @@ final class ClickOverlayView: NSView {
color: pulse.color,
alpha: fade * visualIntensity * 0.38
)
drawDiamond(context: context, point: pulse.point, size: releaseSize, color: pulse.color, alpha: releaseAlpha * 0.82)
drawShape(
context: context,
shape: shape,
point: pulse.point,
radius: releaseRadius,
lineWidth: lineWidth * 0.55,
color: pulse.color,
alpha: releaseAlpha
)
case .drag:
drawDot(
drawShape(
context: context,
shape: shape,
point: pulse.point,
radius: pulse.baseSize * (0.08 + 0.065 * visualIntensity),
radius: pulse.baseSize * (0.18 + 0.08 * visualIntensity),
lineWidth: lineWidth * 0.8,
color: pulse.color,
alpha: alpha * 0.78
alpha: alpha * 0.86
)
case .move:
break
Expand All @@ -401,6 +413,35 @@ final class ClickOverlayView: NSView {
))
}

private func drawShape(
context: CGContext,
shape: ClickGeometryShape,
point: CGPoint,
radius: CGFloat,
lineWidth: CGFloat,
color: NSColor,
alpha: CGFloat
) {
switch shape {
case .dot:
drawDot(context: context, point: point, radius: max(2, radius * 0.42), color: color, alpha: alpha)
case .ring:
drawRing(context: context, point: point, radius: radius, lineWidth: max(2, lineWidth), color: color, alpha: alpha)
case .diamond:
drawDiamond(context: context, point: point, size: max(4, radius * 0.62), color: color, alpha: alpha)
case .cross:
drawCrosshair(context: context, point: point, size: max(4, radius * 0.72), color: color, alpha: alpha)
case .square:
drawSquare(context: context, point: point, size: max(4, radius * 0.72), color: color, alpha: alpha)
case .triangle:
drawTriangle(context: context, point: point, size: max(4, radius * 0.82), color: color, alpha: alpha)
case .hexagon:
drawHexagon(context: context, point: point, size: max(4, radius * 0.82), color: color, alpha: alpha)
case .capsule:
drawCapsule(context: context, point: point, size: max(4, radius * 0.9), color: color, alpha: alpha)
}
}

private func drawRing(
context: CGContext,
point: CGPoint,
Expand Down Expand Up @@ -439,6 +480,47 @@ final class ClickOverlayView: NSView {
context.strokePath()
}

private func drawSquare(context: CGContext, point: CGPoint, size: CGFloat, color: NSColor, alpha: CGFloat) {
context.setFillColor(color.withAlphaComponent(alpha).cgColor)
let rect = CGRect(x: point.x - size, y: point.y - size, width: size * 2, height: size * 2)
context.addPath(CGPath(roundedRect: rect, cornerWidth: size * 0.18, cornerHeight: size * 0.18, transform: nil))
context.fillPath()
}

private func drawTriangle(context: CGContext, point: CGPoint, size: CGFloat, color: NSColor, alpha: CGFloat) {
context.setFillColor(color.withAlphaComponent(alpha).cgColor)
let path = CGMutablePath()
path.move(to: CGPoint(x: point.x, y: point.y + size))
path.addLine(to: CGPoint(x: point.x + size * 0.95, y: point.y - size * 0.55))
path.addLine(to: CGPoint(x: point.x - size * 0.95, y: point.y - size * 0.55))
path.closeSubpath()
context.addPath(path)
context.fillPath()
}

private func drawHexagon(context: CGContext, point: CGPoint, size: CGFloat, color: NSColor, alpha: CGFloat) {
context.setFillColor(color.withAlphaComponent(alpha).cgColor)
let path = CGMutablePath()
let cos60 = CGFloat(0.5)
let sin60 = CGFloat(0.8660254)
path.move(to: CGPoint(x: point.x + size, y: point.y))
path.addLine(to: CGPoint(x: point.x + size * cos60, y: point.y + size * sin60))
path.addLine(to: CGPoint(x: point.x - size * cos60, y: point.y + size * sin60))
path.addLine(to: CGPoint(x: point.x - size, y: point.y))
path.addLine(to: CGPoint(x: point.x - size * cos60, y: point.y - size * sin60))
path.addLine(to: CGPoint(x: point.x + size * cos60, y: point.y - size * sin60))
path.closeSubpath()
context.addPath(path)
context.fillPath()
}

private func drawCapsule(context: CGContext, point: CGPoint, size: CGFloat, color: NSColor, alpha: CGFloat) {
context.setFillColor(color.withAlphaComponent(alpha).cgColor)
let rect = CGRect(x: point.x - size * 0.92, y: point.y - size * 0.42, width: size * 1.84, height: size * 0.84)
context.addPath(CGPath(roundedRect: rect, cornerWidth: size * 0.42, cornerHeight: size * 0.42, transform: nil))
context.fillPath()
}

private func drawDiamond(context: CGContext, point: CGPoint, size: CGFloat, color: NSColor, alpha: CGFloat) {
context.setFillColor(color.withAlphaComponent(alpha).cgColor)
context.move(to: CGPoint(x: point.x, y: point.y + size))
Expand Down
21 changes: 21 additions & 0 deletions Sources/ClickLight/ClickProfileStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ struct ClickProfileSettings: Codable, Equatable {
var size: CGFloat
var intensity: CGFloat
var duration: TimeInterval
var leftPressShape: ClickGeometryShape
var leftReleaseShape: ClickGeometryShape
var rightPressShape: ClickGeometryShape
var rightReleaseShape: ClickGeometryShape
var middlePressShape: ClickGeometryShape
var middleReleaseShape: ClickGeometryShape
var dragShape: ClickGeometryShape
var colorPreset: ClickColorPreset
var customColorMode: CustomClickColorMode
var customColorRed: CGFloat
Expand Down Expand Up @@ -91,6 +98,13 @@ struct ClickProfileSettings: Codable, Equatable {
self.size = settings.size
self.intensity = settings.intensity
self.duration = settings.duration
self.leftPressShape = settings.leftPressShape
self.leftReleaseShape = settings.leftReleaseShape
self.rightPressShape = settings.rightPressShape
self.rightReleaseShape = settings.rightReleaseShape
self.middlePressShape = settings.middlePressShape
self.middleReleaseShape = settings.middleReleaseShape
self.dragShape = settings.dragShape
self.colorPreset = settings.colorPreset
self.customColorMode = settings.customColorMode
self.customColorRed = settings.customColorRed
Expand Down Expand Up @@ -129,6 +143,13 @@ struct ClickProfileSettings: Codable, Equatable {
settings.size = size
settings.intensity = intensity
settings.duration = duration
settings.leftPressShape = leftPressShape
settings.leftReleaseShape = leftReleaseShape
settings.rightPressShape = rightPressShape
settings.rightReleaseShape = rightReleaseShape
settings.middlePressShape = middlePressShape
settings.middleReleaseShape = middleReleaseShape
settings.dragShape = dragShape
settings.colorPreset = colorPreset
settings.customColorMode = customColorMode
settings.customColorRed = customColorRed
Expand Down
Loading