Created
August 10, 2023 19:14
-
-
Save mitchellh/4613cf383454968b98801d55a7a92a9b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// This is demonstrating a strange behavior that I can't explain. | |
/// | |
/// This is a minimal SwiftUI application that renders a NSView that only implements NSTextInputClient, allowing | |
/// it to behave like a text input field. The implementation is minimal. Any characters you type will be logged | |
/// to the console wit the codepoints that are inserted. If you type "a" you should see "97" in the logs, for example. | |
/// | |
/// 1. Launch the program with the US traditional ("US") keyboard layout. | |
/// 2. Type characters and notice they are logged. | |
/// 3. Type a control character such as enter, backspace, notice they are _not_ logged (this is fine). | |
/// 4. Switch to US international ("us-intl") keyboard layout. | |
/// 5. Repeat step 2 and 3, and notice the same behavior. | |
/// 6. Now type a dead key sequence ' followed by a, this should produce an accented character "a". | |
/// 7. Now press a control character such as enter, backspace and notice they _are now logged_. | |
/// InsertText is being called for them. Why? | |
/// 8. Switch back to US traditional ("us") keyboard layout. | |
/// 9. Press control characters such as enter, backspace, and notice they are _now logged_. The | |
/// behavior has changed! Why? | |
/// | |
/// Is this a Cocoa bug? Is this expected? Am I doing something wrong in this minimal NSTextInputClient | |
/// implementation? Should I be doing something after a dead key sequence to reset some state? | |
import SwiftUI | |
@main | |
struct InsertTextBugApp: App { | |
var body: some Scene { | |
WindowGroup { | |
ContentView() | |
} | |
} | |
} | |
struct ContentView: View { | |
var body: some View { | |
VStack { | |
Text("You can type, see log for data.") | |
MyView() | |
} | |
.padding() | |
} | |
} | |
struct MyView: NSViewRepresentable { | |
func makeNSView(context: Context) -> MyViewImpl { | |
return MyViewImpl(); | |
} | |
func updateNSView(_ view: MyViewImpl, context: Context) { | |
// Nothing | |
} | |
} | |
/// The NSView implementation for a terminal surface. | |
class MyViewImpl: NSView, NSTextInputClient, ObservableObject { | |
private var markedText: NSMutableAttributedString; | |
// We need to support being a first responder so that we can get input events | |
override var acceptsFirstResponder: Bool { return true } | |
init() { | |
self.markedText = NSMutableAttributedString() | |
super.init(frame: NSMakeRect(0, 0, 800, 600)) | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) is not supported for this view") | |
} | |
override func keyDown(with event: NSEvent) { | |
self.interpretKeyEvents([event]) | |
} | |
override func keyUp(with event: NSEvent) { | |
} | |
// MARK: NSTextInputClient | |
func hasMarkedText() -> Bool { | |
return markedText.length > 0 | |
} | |
func markedRange() -> NSRange { | |
guard markedText.length > 0 else { return NSRange() } | |
return NSRange(0...(markedText.length-1)) | |
} | |
func selectedRange() -> NSRange { | |
return NSRange() | |
} | |
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) { | |
switch string { | |
case let v as NSAttributedString: | |
self.markedText = NSMutableAttributedString(attributedString: v) | |
case let v as String: | |
self.markedText = NSMutableAttributedString(string: v) | |
default: | |
print("unknown marked text: \(string)") | |
} | |
} | |
func unmarkText() { | |
self.markedText.mutableString.setString("") | |
} | |
func validAttributesForMarkedText() -> [NSAttributedString.Key] { | |
return [] | |
} | |
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? { | |
return nil | |
} | |
func characterIndex(for point: NSPoint) -> Int { | |
return 0 | |
} | |
func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect { | |
return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) | |
} | |
func insertText(_ string: Any, replacementRange: NSRange) { | |
guard NSApp.currentEvent != nil else { return } | |
// We want the string view of the any value | |
var chars = "" | |
switch (string) { | |
case let v as NSAttributedString: | |
chars = v.string | |
case let v as String: | |
chars = v | |
default: | |
return | |
} | |
for codepoint in chars.unicodeScalars { | |
print("insertText codepoint=\(codepoint.value)") | |
} | |
} | |
override func doCommand(by selector: Selector) { | |
// Ignore, but needed for protocol. | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment