Skip to content

Commit c44d5d9

Browse files
committed
feat: click-ring controls in the sidebar
The renderer and metadata schema have supported click rings all along (effects.clickCircles), but the recorder writes effects: nil and no UI ever enabled them — the feature was invisible dead weight. New "Clicks" card: enable toggle, size (24–140px), duration (250–1200ms), and a color picker bridged to the schema's hex string. First enable materialises the effects config (trail/highlight stay disabled — schema-only); all writes route through metadata.didSet so they invalidate the snapshot and autosave like every other edit.
1 parent 06e738a commit c44d5d9

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

CineScreen/Editor/EditorViewModel.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,50 @@ final class EditorViewModel {
256256
}
257257
}
258258

259+
// MARK: - Click effects
260+
261+
/// Defaults matching the renderer's fallbacks (RenderSnapshot falls back
262+
/// to size 64 / duration 600 / white when fields are absent).
263+
private static let defaultClickCircles = MouseEffectsConfig.ClickCircles(
264+
enabled: true, size: 64, color: "#ffffff", duration: 600
265+
)
266+
267+
var clickRingsEnabled: Bool {
268+
get { metadata?.effects?.clickCircles.enabled ?? false }
269+
set { withClickCircles { $0.enabled = newValue } }
270+
}
271+
272+
var clickRingSize: Double {
273+
get { metadata?.effects?.clickCircles.size ?? Self.defaultClickCircles.size }
274+
set { withClickCircles { $0.size = newValue } }
275+
}
276+
277+
var clickRingDuration: Double {
278+
get { metadata?.effects?.clickCircles.duration ?? Self.defaultClickCircles.duration }
279+
set { withClickCircles { $0.duration = newValue } }
280+
}
281+
282+
var clickRingColorHex: String {
283+
get { metadata?.effects?.clickCircles.color ?? Self.defaultClickCircles.color }
284+
set { withClickCircles { $0.color = newValue } }
285+
}
286+
287+
/// The recorder writes `effects: nil`, so the first edit materialises the
288+
/// whole effects config (trail/highlight stay disabled — schema-only
289+
/// features). Routing the write through `self.metadata` hits the didSet
290+
/// hook (snapshot invalidation + autosave) like every other mutation.
291+
private func withClickCircles(_ mutate: (inout MouseEffectsConfig.ClickCircles) -> Void) {
292+
guard var metadata = metadata else { return }
293+
var effects = metadata.effects ?? MouseEffectsConfig(
294+
clickCircles: Self.defaultClickCircles,
295+
trail: MouseEffectsConfig.Trail(enabled: false, length: 0, fadeSpeed: 0, color: "#ffffff"),
296+
highlightRing: MouseEffectsConfig.HighlightRing(enabled: false, size: 0, color: "#ffffff", pulseSpeed: 0)
297+
)
298+
mutate(&effects.clickCircles)
299+
metadata.effects = effects
300+
self.metadata = metadata
301+
}
302+
259303
// MARK: - Webcam layout
260304

261305
/// Persist a webcam layout change into the metadata model. The drag

CineScreen/Editor/Sidebar/SidebarView.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct SidebarView: View {
1919
VStack(alignment: .leading, spacing: 14) {
2020
card(title: "Background", icon: "photo.on.rectangle.angled") { canvasControls }
2121
card(title: "Cursor", icon: "cursorarrow.rays") { cursorControls }
22+
card(title: "Clicks", icon: "hand.tap") { clickControls }
2223
card(title: "Zoom", icon: "plus.magnifyingglass") { zoomControls }
2324
if vm.webcamPlayer != nil {
2425
card(title: "Webcam", icon: "video.fill") { webcamControls }
@@ -232,6 +233,66 @@ struct SidebarView: View {
232233
}
233234

234235
@ViewBuilder
236+
private var clickControls: some View {
237+
VStack(alignment: .leading, spacing: 10) {
238+
Toggle(isOn: $vm.clickRingsEnabled) {
239+
Text("Click Rings").font(.caption).foregroundStyle(CTheme.textSecondary)
240+
}
241+
.toggleStyle(.switch)
242+
.controlSize(.small)
243+
.disabled(vm.metadata == nil)
244+
245+
if vm.clickRingsEnabled {
246+
HStack {
247+
Text("Size").font(.caption).foregroundStyle(CTheme.textSecondary)
248+
Spacer()
249+
Text("\(Int(vm.clickRingSize)) px")
250+
.font(.system(.caption, design: .monospaced))
251+
.foregroundStyle(CTheme.textSecondary)
252+
}
253+
Slider(value: $vm.clickRingSize, in: 24...140, step: 1)
254+
.tint(CTheme.accent)
255+
256+
HStack {
257+
Text("Duration").font(.caption).foregroundStyle(CTheme.textSecondary)
258+
Spacer()
259+
Text("\(Int(vm.clickRingDuration)) ms")
260+
.font(.system(.caption, design: .monospaced))
261+
.foregroundStyle(CTheme.textSecondary)
262+
}
263+
Slider(value: $vm.clickRingDuration, in: 250...1200, step: 50)
264+
.tint(CTheme.accent)
265+
266+
HStack {
267+
Text("Color").font(.caption).foregroundStyle(CTheme.textSecondary)
268+
Spacer()
269+
ColorPicker("", selection: ringColorBinding, supportsOpacity: false)
270+
.labelsHidden()
271+
.controlSize(.small)
272+
}
273+
}
274+
}
275+
}
276+
277+
/// Bridges the metadata's hex string to SwiftUI's ColorPicker.
278+
private var ringColorBinding: Binding<Color> {
279+
Binding(
280+
get: {
281+
let v = RenderSnapshot.parseHexColor(vm.clickRingColorHex)
282+
return Color(.sRGB, red: Double(v.x), green: Double(v.y), blue: Double(v.z))
283+
},
284+
set: { newColor in
285+
let ns = NSColor(newColor).usingColorSpace(.sRGB) ?? .white
286+
vm.clickRingColorHex = String(
287+
format: "#%02x%02x%02x",
288+
Int(round(ns.redComponent * 255)),
289+
Int(round(ns.greenComponent * 255)),
290+
Int(round(ns.blueComponent * 255))
291+
)
292+
}
293+
)
294+
}
295+
235296
private var zoomControls: some View {
236297
VStack(alignment: .leading, spacing: 8) {
237298
HStack {

0 commit comments

Comments
 (0)