Skip to content

Instantly share code, notes, and snippets.

@mitchellh
Created August 10, 2023 19:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mitchellh/4613cf383454968b98801d55a7a92a9b to your computer and use it in GitHub Desktop.
Save mitchellh/4613cf383454968b98801d55a7a92a9b to your computer and use it in GitHub Desktop.
/// 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