Adds --check (compile every effect without camera or window), EFFECTS.md (the contract for writing effects) and AGENTS.md. The POC stays available at the 'poc' tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
3.9 KiB
Swift
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))
|
|
}
|
|
}
|