motionlab/native/Sources/MotionLab/Panel.swift
0xrsydn 6591138bbf Python/OpenCV proof of concept plus first native Swift port
Snapshot before the Python POC is removed. The POC (main.py, effects.py,
panel.py, shaders.py, effects/*.glsl, flake.nix) is the reference for the
native app under native/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:26:47 +07:00

92 lines
3.9 KiB
Swift

import AppKit
private final class TopAlignedView: NSView {
override var isFlipped: Bool { true } // so the scroll view starts at the top
}
/// Side panel: every effect with a checkbox, and the sliders of the ones that are switched on.
final class Panel: NSObject {
static let width: CGFloat = 230
let view = NSScrollView()
private let stack = NSStackView()
private let statusLabel = NSTextField(labelWithString: "")
private let renderer: Renderer
init(renderer: Renderer) {
self.renderer = renderer
super.init()
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 4
stack.translatesAutoresizingMaskIntoConstraints = false
let document = TopAlignedView()
document.translatesAutoresizingMaskIntoConstraints = false
document.addSubview(stack)
view.documentView = document
view.hasVerticalScroller = true
view.drawsBackground = false
NSLayoutConstraint.activate([
view.widthAnchor.constraint(equalToConstant: Panel.width),
document.widthAnchor.constraint(equalTo: view.contentView.widthAnchor),
stack.topAnchor.constraint(equalTo: document.topAnchor, constant: 14),
stack.leadingAnchor.constraint(equalTo: document.leadingAnchor, constant: 14),
stack.trailingAnchor.constraint(equalTo: document.trailingAnchor, constant: -14),
stack.bottomAnchor.constraint(equalTo: document.bottomAnchor, constant: -14),
])
statusLabel.font = .boldSystemFont(ofSize: 11)
statusLabel.textColor = .systemRed
rebuild()
}
private func label(_ text: String, dim: Bool = true) -> NSTextField {
let field = NSTextField(labelWithString: text)
field.font = .systemFont(ofSize: 11)
field.textColor = dim ? .secondaryLabelColor : .labelColor
return field
}
func rebuild() {
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
statusLabel.stringValue = renderer.status
stack.addArrangedSubview(statusLabel)
for (layerIndex, layer) in renderer.layers.enumerated() {
let title = layer.error == nil ? layer.name : "\(layer.name) (shader error)"
let checkbox = NSButton(checkboxWithTitle: title, target: self, action: #selector(toggled(_:)))
checkbox.state = layer.enabled ? .on : .off
checkbox.tag = layerIndex
checkbox.font = .systemFont(ofSize: 13, weight: .medium)
stack.addArrangedSubview(checkbox)
guard layer.enabled else { continue }
for (paramIndex, param) in layer.params.enumerated() {
let slider = NSSlider(value: Double(param.value), minValue: Double(param.min), maxValue: Double(param.max),
target: self, action: #selector(sliderMoved(_:)))
slider.tag = layerIndex * maxParams + paramIndex
slider.controlSize = .small
stack.addArrangedSubview(label(param.name))
stack.addArrangedSubview(slider)
slider.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
}
stack.setCustomSpacing(12, after: stack.arrangedSubviews.last!)
}
if renderer.layers.isEmpty { stack.addArrangedSubview(label("no .metal files found")) }
stack.setCustomSpacing(16, after: stack.arrangedSubviews.last!)
stack.addArrangedSubview(label("s screenshot\nr record\nq quit"))
}
func refreshStatus() {
statusLabel.stringValue = renderer.status
}
@objc private func toggled(_ checkbox: NSButton) {
renderer.setEnabled(checkbox.tag, checkbox.state == .on)
}
@objc private func sliderMoved(_ slider: NSSlider) {
renderer.setParam(layer: slider.tag / maxParams, param: slider.tag % maxParams, value: Float(slider.doubleValue))
}
}