From 6f2c5fdba0039e8cf63a2317062013bff0732927 Mon Sep 17 00:00:00 2001 From: Daniel Lyons <72824209+DandyLyons@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:04:14 -0600 Subject: [PATCH] Implement OnKeyPress --- Package.swift | 3 + Sources/SwiftTUI/Controls/Control.swift | 16 +++ Sources/SwiftTUI/RunLoop/Application.swift | 8 ++ .../SwiftTUI/Views/Modifiers/OnKeyPress.swift | 109 ++++++++++++++++++ Sources/SwiftTUIExample/ExampleApp.swift | 17 +++ 5 files changed, 153 insertions(+) create mode 100644 Sources/SwiftTUI/Views/Modifiers/OnKeyPress.swift create mode 100644 Sources/SwiftTUIExample/ExampleApp.swift diff --git a/Package.swift b/Package.swift index a6b9c079..21b5b3f1 100644 --- a/Package.swift +++ b/Package.swift @@ -22,5 +22,8 @@ let package = Package( .testTarget( name: "SwiftTUITests", dependencies: ["SwiftTUI"]), + .executableTarget( + name: "SwiftTUIExample", + dependencies: ["SwiftTUI"]), ] ) diff --git a/Sources/SwiftTUI/Controls/Control.swift b/Sources/SwiftTUI/Controls/Control.swift index 4fff1e48..ee41a2cb 100644 --- a/Sources/SwiftTUI/Controls/Control.swift +++ b/Sources/SwiftTUI/Controls/Control.swift @@ -121,3 +121,19 @@ class Control: LayerDrawing { } } + +// Extension to flatten nested categories +extension Array where Element == Control { + /// Filter out all ``Control``s except ``OnKeyPressControl`` and recursively flatten into one array. + func flattenAndKeepOnlyOnKeyPressControl() -> [OnKeyPressControl] { + return self.flatMap { child in + let current: [OnKeyPressControl] + if child is OnKeyPressControl { + current = [child as! OnKeyPressControl] + } else { + current = [] + } + return current + child.children.flattenAndKeepOnlyOnKeyPressControl() + } + } +} diff --git a/Sources/SwiftTUI/RunLoop/Application.swift b/Sources/SwiftTUI/RunLoop/Application.swift index 2972d72a..4bd20990 100644 --- a/Sources/SwiftTUI/RunLoop/Application.swift +++ b/Sources/SwiftTUI/RunLoop/Application.swift @@ -129,6 +129,14 @@ public class Application { stop() } else { window.firstResponder?.handleEvent(char) + + // handle input for `onKeyPress` View modifier + let onKeyPressControls = window.controls.flattenAndKeepOnlyOnKeyPressControl() + for control in onKeyPressControls { + if control.keyPress == char { + control.action() + } + } } } } diff --git a/Sources/SwiftTUI/Views/Modifiers/OnKeyPress.swift b/Sources/SwiftTUI/Views/Modifiers/OnKeyPress.swift new file mode 100644 index 00000000..dc949780 --- /dev/null +++ b/Sources/SwiftTUI/Views/Modifiers/OnKeyPress.swift @@ -0,0 +1,109 @@ +import Foundation + +public extension View { + /// Use this modifier to provide a closure to be executed when the user presses one or more keys, while the view has the focus. + func onKeyPress(_ char: Character, _ action: @escaping () -> Void) -> some View { + log("inside onKeyPress modifier") + return OnKeyPress( + content: self, + keyPress: char, + action: action + ) + } +} + +struct OnKeyPress: View, PrimitiveView, ModifierView { + let content: Content + let keyPress: Character + let action: () -> Void + + static var size: Int? { Content.size } + + func buildNode(_ node: Node) { + node.addNode(at: 0, Node(view: content.view)) + } + + func updateNode(_ node: Node) { + node.view = self + node.children[0].update(using: content.view) + } + + func passControl(_ control: Control, node: Node) -> Control { + // I'm not sure if parent views or child views should take precedence for key press handling + // or perhaps they both handle it, with no precedence. +// if let onKeyPressControl = control.parent { return onKeyPressControl } + + let onKeyPressControl = OnKeyPressControl(keyPress: keyPress, action: action) + onKeyPressControl.addSubview(control, at: 0) + + return onKeyPressControl + } + +} + +class OnKeyPressControl: Control { + let keyPress: Character + let action: () -> Void + + init(keyPress: Character, action: @escaping () -> Void) { + log("inside OnKeyPressControl init") + self.keyPress = keyPress + self.action = action + } + + override func size(proposedSize: Size) -> Size { + log("inside OnKeyPressControl size(proposedSize:)") + return children[0].size(proposedSize: proposedSize) + } + + override func layout(size: Size) { + log("inside OnKeyPressControl layout(size:)") + super.layout(size: size) + children[0].layout(size: size) + + } + + override func handleEvent(_ char: Character) { + log("inside OnKeyPressControl handleEvent(_:\(char))") + super.handleEvent(char) // let children handle the event + if char == keyPress { + // self.root.window?.runKeyPressAction(char: char, actionLabel: actionLabel) + } + } +} + +// MARK: Example Usage +public struct ExampleOnKeyPressView: View { + @State var isASelected: Bool + @State var isBSelected: Bool + + public init(isASelected: Bool = false, isBSelected: Bool = false) { + self.isASelected = isASelected + self.isBSelected = isBSelected + } + + public var body: some View { + let _ = log("inside ExampleOnKeyPressView body") + + return VStack { + Text("a (onKeyPress modifier is APPLIED)") + .padding() + .border() + .background(isASelected ? .green : .default) + .onKeyPress("a") { + log("isASelected.toggle()") + isASelected.toggle() + } + + Text("b (onKeyPress modifier is NOT APPLIED)") + .padding() + .border() + .background(isBSelected ? .cyan : .default) + // .onKeyPress("b") { + // log("isBSelected.toggle()") + // isBSelected.toggle() + // } + + } + } +} diff --git a/Sources/SwiftTUIExample/ExampleApp.swift b/Sources/SwiftTUIExample/ExampleApp.swift new file mode 100644 index 00000000..2e395434 --- /dev/null +++ b/Sources/SwiftTUIExample/ExampleApp.swift @@ -0,0 +1,17 @@ + +import SwiftTUI + +@main struct ExampleApp { + static func main() { + if #available(macOS 14.0, *) { + Application( + rootView: ExampleOnKeyPressView(), + runLoopType: .cocoa + ) + .start() + } else { + // Fallback on earlier versions + log("This ExampleApp requires macOS 14.0 or newer") + } + } +}