From 7d318d6efcc7f7573a2803f5c1bd78d156f2714e Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 16 Dec 2025 20:42:39 -0800 Subject: [PATCH] chore: simplify UI to only single button --- ScreenShader.xcodeproj/project.pbxproj | 4 - ScreenShader/config.swift | 245 +++--------------------- ScreenShader/config_view.swift | 248 ++++++++++++------------- ScreenShader/effect_view.swift | 147 --------------- ScreenShader/main.swift | 8 +- ScreenShader/overlay.swift | 7 +- 6 files changed, 144 insertions(+), 515 deletions(-) delete mode 100644 ScreenShader/effect_view.swift diff --git a/ScreenShader.xcodeproj/project.pbxproj b/ScreenShader.xcodeproj/project.pbxproj index d3564ec..b78a8fa 100644 --- a/ScreenShader.xcodeproj/project.pbxproj +++ b/ScreenShader.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 416975FC2D497E21004C8E93 /* overlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 416975F22D497E21004C8E93 /* overlay.swift */; }; 416975FD2D497E21004C8E93 /* error_message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 416975F32D497E21004C8E93 /* error_message.swift */; }; 416975FE2D497E21004C8E93 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 416975F42D497E21004C8E93 /* main.swift */; }; - 416975FF2D497E21004C8E93 /* effect_view.swift in Sources */ = {isa = PBXBuildFile; fileRef = 416975F52D497E21004C8E93 /* effect_view.swift */; }; 41C290E42D4B04A100675317 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 41C290E32D4B04A100675317 /* Sparkle */; }; 9736AA942EF210C1002DDC37 /* slang_compiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9736AA932EF210C1002DDC37 /* slang_compiler.swift */; }; /* End PBXBuildFile section */ @@ -33,7 +32,6 @@ 416975F22D497E21004C8E93 /* overlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = overlay.swift; sourceTree = ""; }; 416975F32D497E21004C8E93 /* error_message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = error_message.swift; sourceTree = ""; }; 416975F42D497E21004C8E93 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; - 416975F52D497E21004C8E93 /* effect_view.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = effect_view.swift; sourceTree = ""; }; 4176335D2D4DB1EB00FCC2AF /* waves.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = waves.gif; sourceTree = ""; }; 41C290CD2D49862B00675317 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 41C290CE2D4ABD2000675317 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -84,7 +82,6 @@ 4159FE662D497D4600B79670 /* ScreenShader.entitlements */, 416975F02D497E21004C8E93 /* config_view.swift */, 416975ED2D497E21004C8E93 /* config.swift */, - 416975F52D497E21004C8E93 /* effect_view.swift */, 416975F32D497E21004C8E93 /* error_message.swift */, 416975F42D497E21004C8E93 /* main.swift */, 416975EC2D497E21004C8E93 /* metrics.swift */, @@ -192,7 +189,6 @@ 9736AA942EF210C1002DDC37 /* slang_compiler.swift in Sources */, 416975FA2D497E21004C8E93 /* config_view.swift in Sources */, 416975F62D497E21004C8E93 /* metrics.swift in Sources */, - 416975FF2D497E21004C8E93 /* effect_view.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ScreenShader/config.swift b/ScreenShader/config.swift index bb8d3ca..245061a 100644 --- a/ScreenShader/config.swift +++ b/ScreenShader/config.swift @@ -43,72 +43,10 @@ let defaultShaderSource: String = """ } """ -let predefinedShaders: [(String, String)] = [ - ( - "Swap red-blue channels", - """ - float4 shaderFunction(ShaderInput input) { - float4 inputColor = sampleInput(input.texCoord); - return float4(inputColor.b, inputColor.g, inputColor.r, inputColor.a); - } - """ - ), - ( - "Grey scale", - """ - float4 shaderFunction(ShaderInput input) { - float4 inputColor = sampleInput(input.texCoord); - float grey = dot(inputColor.rgb, float3(0.299, 0.587, 0.114)); - return float4(grey, grey, grey, inputColor.a); - } - """ - ), - ( - "Color Invert", - """ - float4 shaderFunction(ShaderInput input) { - float4 inputColor = sampleInput(input.texCoord); - return float4(1.0 - inputColor.rgb, inputColor.a); - } - """ - ), - ( - "Sepia Tone", - """ - float4 shaderFunction(ShaderInput input) { - float4 inputColor = sampleInput(input.texCoord); - - float3 sepia; - sepia.r = dot(inputColor.rgb, float3(0.393, 0.769, 0.189)); - sepia.g = dot(inputColor.rgb, float3(0.349, 0.686, 0.168)); - sepia.b = dot(inputColor.rgb, float3(0.272, 0.534, 0.131)); - - return float4(sepia, inputColor.a); - } - """ - ), - ( - "Vignette", - """ - float4 shaderFunction(ShaderInput input) { - float4 inputColor = sampleInput(input.texCoord); - - // Calculate distance from center - float2 center = float2(0.5, 0.5); - float dist = distance(input.texCoord, center); - - // Create vignette effect - float vignette = 1.0 - smoothstep(0.3, 0.8, dist); - - return float4(inputColor.rgb * vignette, inputColor.a); - } - """ - ), -] - class Config: Codable { - var configVersion: Int = 1 - var effects: Effects = Effects() + var configVersion: Int = 2 + var shaderPath: String? = nil + var active: Bool = false var targetFPS: Int = 60 static func getFileURL() -> URL { @@ -151,177 +89,38 @@ class Config: Codable { config = try decoder.decode(Config.self, from: data) print("Loaded config from \(fileURL)") } catch { - fatalError("Failed to load config: \(error)") + print("Failed to load config, creating new: \(error)") + config = Config() } } - if config.effects.effectList().isEmpty { - // Create a default empty effect for the user to configure with a shader file - let _ = config.effects.new() - } - return config } -} - -class Effects: Codable { - private var nextEffectNumber: Int = 1 - private var effects: [UUID] = [] - private var deletedEffects: [UUID] = [] - private var effectToName: [UUID: String] = [:] - private var effectToShader: [UUID: String] = [:] - private var effectToShaderPath: [UUID: String] = [:] - private var effectToActive: [UUID: Bool] = [:] - private var mostRecentActiveEffect: UUID? = nil - - // Custom CodingKeys to handle optional effectToShaderPath - private enum CodingKeys: String, CodingKey { - case nextEffectNumber - case effects - case deletedEffects - case effectToName - case effectToShader - case effectToShaderPath - case effectToActive - case mostRecentActiveEffect - } - - // Default initializer - init() {} - // Custom decoder to handle missing effectToLanguage in old config files - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - nextEffectNumber = try container.decode(Int.self, forKey: .nextEffectNumber) - effects = try container.decode([UUID].self, forKey: .effects) - deletedEffects = try container.decode([UUID].self, forKey: .deletedEffects) - effectToName = try container.decode([UUID: String].self, forKey: .effectToName) - effectToShader = try container.decode([UUID: String].self, forKey: .effectToShader) - effectToShaderPath = try container.decodeIfPresent([UUID: String].self, forKey: .effectToShaderPath) ?? [:] - effectToActive = try container.decode([UUID: Bool].self, forKey: .effectToActive) - mostRecentActiveEffect = try container.decodeIfPresent(UUID.self, forKey: .mostRecentActiveEffect) - } - - func effectList() -> [UUID] { - return self.effects - } - - func new() -> UUID { - let newEffectID = UUID() - let newEffectName = "Effect \(self.nextEffectNumber)" - self.nextEffectNumber += 1 - - self.effects.append(newEffectID) - self.effectToName[newEffectID] = newEffectName - self.effectToShader[newEffectID] = "" // Empty - will be loaded from file - self.effectToActive[newEffectID] = false - - return newEffectID - } - - func delete(effect: UUID) { - if let index = self.effects.firstIndex(of: effect) { - self.effects.remove(at: index) - self.deletedEffects.append(effect) - if self.mostRecentActiveEffect == effect { - self.mostRecentActiveEffect = nil - } - } - } - - func getName(effect: UUID) -> String { - return self.effectToName[effect]! - } - - func setName(effect: UUID, newName: String) { - self.effectToName[effect] = newName - } - - func isActive(effect: UUID) -> Bool { - return self.effectToActive[effect]! - } - - func setActive(effect: UUID, active: Bool) { - if active { - for otherEffect in self.effects { - self.effectToActive[otherEffect] = false - } - self.mostRecentActiveEffect = effect - } - self.effectToActive[effect] = active - } - - func toggleActive(effect: UUID) { - let active = self.isActive(effect: effect) - self.setActive(effect: effect, active: !active) - } - - func getShader(effect: UUID) -> String { - // If a shader path is set, read from file - if let path = effectToShaderPath[effect], !path.isEmpty { - do { - let shaderSource = try String(contentsOfFile: path, encoding: .utf8) - return shaderSource - } catch { - print("Failed to read shader from file: \(error)") - // Fall back to cached shader if file read fails - return self.effectToShader[effect] ?? "" - } - } - return self.effectToShader[effect] ?? "" - } - - func setShader(effect: UUID, shader: String) { - self.effectToShader[effect] = shader - } - - func getShaderPath(effect: UUID) -> String? { - return self.effectToShaderPath[effect] - } - - func setShaderPath(effect: UUID, path: String?) { - if let path = path, !path.isEmpty { - self.effectToShaderPath[effect] = path - } else { - self.effectToShaderPath.removeValue(forKey: effect) - } - } - - func hasShaderPath(effect: UUID) -> Bool { - if let path = effectToShaderPath[effect] { + func hasShaderPath() -> Bool { + if let path = shaderPath { return !path.isEmpty } return false } - - func getActiveEffect() -> UUID? { - for effect in self.effects { - if self.isActive(effect: effect) { - return effect - } + + func getShader() -> String? { + guard let path = shaderPath, !path.isEmpty else { + return nil } - return nil - } - - func getMostRecentActiveEffect() -> UUID? { - return self.mostRecentActiveEffect - } - - func anyEffectActive() -> Bool { - return self.getActiveEffect() != nil - } - - func deactivateAll() { - for effect in self.effects { - self.setActive(effect: effect, active: false) + do { + return try String(contentsOfFile: path, encoding: .utf8) + } catch { + print("Failed to read shader from file: \(error)") + return nil } } - - func activateDefault() { - if let mostRecentActiveEffect = self.mostRecentActiveEffect { - self.setActive(effect: mostRecentActiveEffect, active: true) - } else if self.effects.count > 0 { - self.setActive(effect: self.effects.first!, active: true) + + func toggleActive() { + if hasShaderPath() { + active = !active + } else { + active = false } } } diff --git a/ScreenShader/config_view.swift b/ScreenShader/config_view.swift index 3e803b4..2737a52 100644 --- a/ScreenShader/config_view.swift +++ b/ScreenShader/config_view.swift @@ -1,45 +1,88 @@ import AppKit -class ConfigViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource { +class ConfigViewController: NSViewController { var config: Config! = nil - var effects: Effects { - return self.config.effects - } var onConfigUpdate: () -> Void = {} var errorMessage: ErrorMessage! = nil - private var tableView: NSTableView! = nil + private var stackView: NSStackView! = nil + private var shaderPathField: NSTextField! = nil + private var browseButton: NSButton! = nil + private var activateButton: NSButton! = nil + private var reloadButton: NSButton! = nil private var errorMessageField: NSTextField! = nil - private var newEffectButton: NSButton! = nil - private var contentPane: NSView! = nil - private var effectToController: [UUID: EffectViewController] = [:] override func loadView() { - let splitView = NSSplitView() - splitView.dividerStyle = .thin - splitView.isVertical = true - splitView.translatesAutoresizingMaskIntoConstraints = false - - let tabsPane = NSStackView() - tabsPane.orientation = .vertical - tabsPane.spacing = 10 - tabsPane.translatesAutoresizingMaskIntoConstraints = false - - self.tableView = NSTableView() - self.tableView.delegate = self - self.tableView.dataSource = self - self.tableView.headerView = nil - self.tableView.focusRingType = .none - - let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("Tabs")) - column.title = "Tabs" - self.tableView.addTableColumn(column) - - let scrollView = NSScrollView() - scrollView.documentView = self.tableView - scrollView.hasVerticalScroller = true - scrollView.translatesAutoresizingMaskIntoConstraints = false - + self.view = NSView() + self.view.translatesAutoresizingMaskIntoConstraints = false + + self.stackView = NSStackView() + self.stackView.orientation = .vertical + self.stackView.spacing = 16 + self.stackView.alignment = .leading + self.stackView.translatesAutoresizingMaskIntoConstraints = false + self.view.addSubview(self.stackView) + + // Title + let titleLabel = NSTextField(labelWithString: "ScreenShader") + titleLabel.font = NSFont.boldSystemFont(ofSize: 18) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + self.stackView.addArrangedSubview(titleLabel) + + // Shader file path section + let shaderPathStack = NSStackView() + shaderPathStack.orientation = .horizontal + shaderPathStack.spacing = 8 + shaderPathStack.alignment = .centerY + shaderPathStack.translatesAutoresizingMaskIntoConstraints = false + + let shaderPathLabel = NSTextField(labelWithString: "Shader File:") + shaderPathLabel.translatesAutoresizingMaskIntoConstraints = false + shaderPathStack.addArrangedSubview(shaderPathLabel) + + self.shaderPathField = NSTextField() + self.shaderPathField.placeholderString = "Select a .slang file..." + self.shaderPathField.isEditable = false + self.shaderPathField.focusRingType = .none + self.shaderPathField.translatesAutoresizingMaskIntoConstraints = false + self.shaderPathField.stringValue = self.config.shaderPath ?? "" + shaderPathStack.addArrangedSubview(self.shaderPathField) + + self.browseButton = NSButton(title: "Browse...", target: self, action: #selector(self.browseForShader)) + self.browseButton.translatesAutoresizingMaskIntoConstraints = false + shaderPathStack.addArrangedSubview(self.browseButton) + + self.stackView.addArrangedSubview(shaderPathStack) + + // Button row + let buttonStack = NSStackView() + buttonStack.orientation = .horizontal + buttonStack.spacing = 12 + buttonStack.alignment = .centerY + buttonStack.translatesAutoresizingMaskIntoConstraints = false + + self.activateButton = NSButton(title: self.getActivateButtonTitle(), target: self, action: #selector(self.toggleActive)) + self.activateButton.translatesAutoresizingMaskIntoConstraints = false + self.activateButton.bezelStyle = .rounded + self.activateButton.isEnabled = self.config.hasShaderPath() + buttonStack.addArrangedSubview(self.activateButton) + + self.reloadButton = NSButton(title: "Reload Shader", target: self, action: #selector(self.reloadShader)) + self.reloadButton.translatesAutoresizingMaskIntoConstraints = false + self.reloadButton.isEnabled = self.config.hasShaderPath() + buttonStack.addArrangedSubview(self.reloadButton) + + self.stackView.addArrangedSubview(buttonStack) + + // Add Slang availability indicator + if !SlangCompiler.isAvailable { + let warningLabel = NSTextField(labelWithString: "⚠️ slangc not found - Slang shaders won't compile") + warningLabel.textColor = .systemOrange + warningLabel.translatesAutoresizingMaskIntoConstraints = false + self.stackView.addArrangedSubview(warningLabel) + } + + // Error message field self.errorMessageField = NSTextField() self.errorMessageField.isEditable = false self.errorMessageField.drawsBackground = false @@ -49,6 +92,7 @@ class ConfigViewController: NSViewController, NSTableViewDelegate, NSTableViewDa self.errorMessageField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) self.errorMessageField.setContentCompressionResistancePriority(.defaultLow, for: .vertical) self.errorMessageField.translatesAutoresizingMaskIntoConstraints = false + self.stackView.addArrangedSubview(self.errorMessageField) self.errorMessage.onMessageChanged = { self.errorMessageField.stringValue = self.errorMessage.get() ?? "" @@ -56,111 +100,55 @@ class ConfigViewController: NSViewController, NSTableViewDelegate, NSTableViewDa } self.errorMessage.onMessageChanged?() - self.newEffectButton = NSButton( - title: "New Effect", target: self, action: #selector(self.newEffect)) - self.newEffectButton.translatesAutoresizingMaskIntoConstraints = false - - tabsPane.addArrangedSubview(scrollView) - tabsPane.addArrangedSubview(self.errorMessageField) - tabsPane.addArrangedSubview(self.newEffectButton) - - self.contentPane = NSView() - - splitView.addArrangedSubview(tabsPane) - splitView.addArrangedSubview(self.contentPane) - NSLayoutConstraint.activate([ - scrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200), - - self.errorMessageField.leftAnchor.constraint(equalTo: tabsPane.leftAnchor, constant: 10), - self.errorMessageField.rightAnchor.constraint(equalTo: tabsPane.rightAnchor, constant: -10), - - self.newEffectButton.heightAnchor.constraint(equalToConstant: 30), - self.newEffectButton.bottomAnchor.constraint(equalTo: tabsPane.bottomAnchor, constant: -10), - - self.contentPane.topAnchor.constraint(equalTo: splitView.topAnchor), - self.contentPane.bottomAnchor.constraint(equalTo: splitView.bottomAnchor), - - tabsPane.widthAnchor.constraint(equalTo: splitView.widthAnchor, multiplier: 0.3), + self.stackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20), + self.stackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20), + self.stackView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 20), + + self.shaderPathField.widthAnchor.constraint(greaterThanOrEqualToConstant: 300), ]) - - self.view = splitView - if self.effects.effectList().count > 0 { - self.selectTab(index: 0) - } - } - - private func selectTab(index: Int) { - self.tableView.selectRowIndexes(IndexSet(integer: index), byExtendingSelection: false) - self.tableView.scrollRowToVisible(index) } - - @objc private func newEffect() { - let _ = self.effects.new() - self.tableView.reloadData() - self.selectTab(index: self.effects.effectList().count - 1) - self.onConfigUpdate() - } - - private func onUpdateEffect(effect: UUID) { - self.tableView.reloadData() - self.onConfigUpdate() + + private func getActivateButtonTitle() -> String { + return self.config.active ? "Deactivate" : "Activate" } - - private func onDeleteEffect(effect: UUID) { - self.tableView.reloadData() - self.tableView.deselectAll(nil) - self.effectToController.removeValue(forKey: effect) + + @objc func toggleActive() { + self.config.toggleActive() + self.activateButton.title = self.getActivateButtonTitle() self.onConfigUpdate() } - - func numberOfRows(in tableView: NSTableView) -> Int { - return self.effects.effectList().count - } - - func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) - -> Any? - { - let effect = self.effects.effectList()[row] - let name = self.effects.getName(effect: effect) - let isActive = self.effects.isActive(effect: effect) - return isActive ? "\(name) (active)" : name - } - - func tableViewSelectionDidChange(_ notification: Notification) { - let selectedRow = self.tableView.selectedRow - if selectedRow >= 0 && selectedRow < self.effects.effectList().count { - let selectedEffect = self.effects.effectList()[selectedRow] - - if !self.effectToController.keys.contains(selectedEffect) { - let controller = EffectViewController() - controller.effects = self.effects - controller.effect = selectedEffect - controller.onUpdate = { self.onUpdateEffect(effect: selectedEffect) } - controller.onDelete = { self.onDeleteEffect(effect: selectedEffect) } - self.effectToController[selectedEffect] = controller + + @objc func browseForShader() { + let openPanel = NSOpenPanel() + openPanel.title = "Select Shader File" + openPanel.allowedContentTypes = [.init(filenameExtension: "slang")!] + openPanel.allowsMultipleSelection = false + openPanel.canChooseDirectories = false + openPanel.canChooseFiles = true + + openPanel.begin { [weak self] result in + guard let self = self else { return } + if result == .OK, let url = openPanel.url { + let path = url.path + self.config.shaderPath = path + self.shaderPathField.stringValue = path + self.activateButton.isEnabled = true + self.reloadButton.isEnabled = true + + self.onConfigUpdate() } - - let controller = self.effectToController[selectedEffect]! - self.contentPane.subviews = [controller.view] - - NSLayoutConstraint.activate([ - controller.view.leadingAnchor.constraint(equalTo: self.contentPane.leadingAnchor), - controller.view.trailingAnchor.constraint(equalTo: self.contentPane.trailingAnchor), - controller.view.topAnchor.constraint(equalTo: self.contentPane.topAnchor), - controller.view.bottomAnchor.constraint(equalTo: self.contentPane.bottomAnchor), - ]) - } else { - self.contentPane.subviews = [] } - self.tableView.reloadData() } - - func refreshActiveEffects() { - self.tableView.reloadData() - for controller in self.effectToController.values { - controller.refreshActiveCheckbox() - } + + @objc func reloadShader() { + // Force re-read of the shader file by triggering an update + self.onConfigUpdate() + } + + func refreshUI() { + self.activateButton.title = self.getActivateButtonTitle() + self.activateButton.isEnabled = self.config.hasShaderPath() } } @@ -193,6 +181,6 @@ class ConfigWindowController: NSWindowController { } func refreshActiveEffects() { - self.configViewController.refreshActiveEffects() + self.configViewController.refreshUI() } } diff --git a/ScreenShader/effect_view.swift b/ScreenShader/effect_view.swift deleted file mode 100644 index f5e3a79..0000000 --- a/ScreenShader/effect_view.swift +++ /dev/null @@ -1,147 +0,0 @@ -import AppKit - -class EffectViewController: NSViewController, NSTextFieldDelegate { - var effects: Effects! = nil - var effect: UUID! = nil - var onUpdate: () -> Void = {} - var onDelete: () -> Void = {} - - private var stackView: NSStackView! = nil - private var nameField: NSTextField! = nil - private var activeButton: NSButton! = nil - private var deleteButton: NSButton! = nil - private var shaderPathLabel: NSTextField! = nil - private var shaderPathField: NSTextField! = nil - private var browseButton: NSButton! = nil - private var reloadButton: NSButton! = nil - - override func loadView() { - self.view = NSView() - self.view.translatesAutoresizingMaskIntoConstraints = false - - self.stackView = NSStackView() - self.stackView.orientation = .vertical - self.stackView.spacing = 10 - self.stackView.alignment = .leading - self.stackView.translatesAutoresizingMaskIntoConstraints = false - self.view.addSubview(self.stackView) - - self.nameField = NSTextField() - self.nameField.placeholderString = "Effect Name" - self.nameField.focusRingType = .none - self.nameField.delegate = self - self.nameField.translatesAutoresizingMaskIntoConstraints = false - self.nameField.stringValue = self.effects.getName(effect: self.effect) - self.stackView.addArrangedSubview(self.nameField) - - self.activeButton = NSButton( - checkboxWithTitle: "Active (only one effect can be active at a time)", - target: self, - action: #selector(self.toggleActive)) - self.activeButton.translatesAutoresizingMaskIntoConstraints = false - self.activeButton.state = self.effects.isActive(effect: self.effect) ? .on : .off - self.stackView.addArrangedSubview(self.activeButton) - - // Shader file path section - let shaderPathStack = NSStackView() - shaderPathStack.orientation = .horizontal - shaderPathStack.spacing = 8 - shaderPathStack.alignment = .centerY - shaderPathStack.translatesAutoresizingMaskIntoConstraints = false - - self.shaderPathLabel = NSTextField(labelWithString: "Shader File:") - self.shaderPathLabel.translatesAutoresizingMaskIntoConstraints = false - shaderPathStack.addArrangedSubview(self.shaderPathLabel) - - self.shaderPathField = NSTextField() - self.shaderPathField.placeholderString = "Select a .slang file..." - self.shaderPathField.isEditable = false - self.shaderPathField.focusRingType = .none - self.shaderPathField.translatesAutoresizingMaskIntoConstraints = false - self.shaderPathField.stringValue = self.effects.getShaderPath(effect: self.effect) ?? "" - shaderPathStack.addArrangedSubview(self.shaderPathField) - - self.browseButton = NSButton(title: "Browse...", target: self, action: #selector(self.browseForShader)) - self.browseButton.translatesAutoresizingMaskIntoConstraints = false - shaderPathStack.addArrangedSubview(self.browseButton) - - self.reloadButton = NSButton(title: "Reload", target: self, action: #selector(self.reloadShader)) - self.reloadButton.translatesAutoresizingMaskIntoConstraints = false - self.reloadButton.isEnabled = self.effects.hasShaderPath(effect: self.effect) - shaderPathStack.addArrangedSubview(self.reloadButton) - - self.stackView.addArrangedSubview(shaderPathStack) - - // Add Slang availability indicator - if !SlangCompiler.isAvailable { - let warningLabel = NSTextField(labelWithString: "⚠️ slangc not found - Slang shaders won't compile") - warningLabel.textColor = .systemOrange - warningLabel.translatesAutoresizingMaskIntoConstraints = false - self.stackView.addArrangedSubview(warningLabel) - } - - self.deleteButton = NSButton( - title: "Delete effect", - target: self, - action: #selector(self.deleteEffect)) - self.deleteButton.translatesAutoresizingMaskIntoConstraints = false - self.stackView.addArrangedSubview(self.deleteButton) - - NSLayoutConstraint.activate([ - self.stackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10), - self.stackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10), - self.stackView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 10), - self.stackView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -10), - - self.shaderPathField.widthAnchor.constraint(greaterThanOrEqualToConstant: 300), - ]) - } - - @objc func toggleActive() { - self.effects.toggleActive(effect: self.effect) - self.onUpdate() - } - - @objc func deleteEffect() { - self.effects.delete(effect: self.effect) - self.onDelete() - } - - @objc func browseForShader() { - let openPanel = NSOpenPanel() - openPanel.title = "Select Shader File" - openPanel.allowedContentTypes = [.init(filenameExtension: "slang")!] - openPanel.allowsMultipleSelection = false - openPanel.canChooseDirectories = false - openPanel.canChooseFiles = true - - openPanel.begin { [weak self] result in - guard let self = self else { return } - if result == .OK, let url = openPanel.url { - let path = url.path - self.effects.setShaderPath(effect: self.effect, path: path) - self.shaderPathField.stringValue = path - self.reloadButton.isEnabled = true - - self.onUpdate() - } - } - } - - @objc func reloadShader() { - // Force re-read of the shader file by triggering an update - self.onUpdate() - } - - func controlTextDidChange(_ obj: Notification) { - let newName = self.nameField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) - if !newName.isEmpty { - self.effects.setName(effect: self.effect, newName: self.nameField.stringValue) - self.onUpdate() - } - } - - func refreshActiveCheckbox() { - self.activeButton.state = self.effects.isActive(effect: self.effect) ? .on : .off - } -} diff --git a/ScreenShader/main.swift b/ScreenShader/main.swift index 449bb3b..f130a17 100644 --- a/ScreenShader/main.swift +++ b/ScreenShader/main.swift @@ -121,18 +121,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { } private func getMenuBarIcon() -> NSImage { - let active = self.config.effects.anyEffectActive() + let active = self.config.active let systemSymbolName = active ? "paintbrush.fill" : "paintbrush" return NSImage( systemSymbolName: systemSymbolName, accessibilityDescription: "ScreenShader")! } @objc private func toggleEffect() { - if self.config.effects.anyEffectActive() { - self.config.effects.deactivateAll() - } else { - self.config.effects.activateDefault() - } + self.config.toggleActive() self.refreshConfig() } diff --git a/ScreenShader/overlay.swift b/ScreenShader/overlay.swift index 57b200c..e01c347 100644 --- a/ScreenShader/overlay.swift +++ b/ScreenShader/overlay.swift @@ -85,13 +85,10 @@ class OverlayController: NSObject, MTKViewDelegate { } func refreshConfig() { - let activeEffect = self.config.effects.getActiveEffect() - let active = activeEffect != nil - - let activeEffectShader = active ? self.config.effects.getShader(effect: activeEffect!) : nil + let activeShader = self.config.active ? self.config.getShader() : nil do { - try self.renderer.setEffectSource(activeEffectShader) + try self.renderer.setEffectSource(activeShader) self.errorMessage.clear() } catch { print("Effect shader error: \(error.localizedDescription)") -- 2.51.2