fix(editor): activate text editor for keyboard input

This commit is contained in:
Feror 2026-05-29 19:47:40 +02:00
parent ce55b9a2ce
commit a010cb19be
2 changed files with 64 additions and 1 deletions

View file

@ -9,8 +9,16 @@ import SaplingEditor
import SaplingRenderer import SaplingRenderer
import SaplingUI import SaplingUI
#if os(macOS)
import AppKit
#endif
@main @main
struct SaplingApplication: App { struct SaplingApplication: App {
#if os(macOS)
@NSApplicationDelegateAdaptor(SaplingAppDelegate.self) private var appDelegate
#endif
@StateObject private var model = SaplingAppModel(dependencies: .live()) @StateObject private var model = SaplingAppModel(dependencies: .live())
var body: some Scene { var body: some Scene {
@ -33,6 +41,15 @@ struct SaplingApplication: App {
} }
} }
#if os(macOS)
private final class SaplingAppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
}
}
#endif
@MainActor @MainActor
private final class SaplingAppModel: ObservableObject { private final class SaplingAppModel: ObservableObject {
@Published var workspace: Workspace @Published var workspace: Workspace

View file

@ -150,7 +150,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
layoutManager.addTextContainer(textContainer) layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager) textStorage.addLayoutManager(layoutManager)
let textView = NSTextView(frame: scrollView.contentView.bounds, textContainer: textContainer) let textView = EditorTextView(frame: scrollView.contentView.bounds, textContainer: textContainer)
textView.autoresizingMask = [.width] textView.autoresizingMask = [.width]
textView.minSize = NSSize(width: 0, height: scrollView.contentSize.height) textView.minSize = NSSize(width: 0, height: scrollView.contentSize.height)
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
@ -160,6 +160,8 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
textView.delegate = context.coordinator textView.delegate = context.coordinator
textView.string = text textView.string = text
textView.isRichText = false textView.isRichText = false
textView.isEditable = true
textView.isSelectable = true
textView.isAutomaticQuoteSubstitutionEnabled = false textView.isAutomaticQuoteSubstitutionEnabled = false
textView.isAutomaticDashSubstitutionEnabled = false textView.isAutomaticDashSubstitutionEnabled = false
textView.isAutomaticTextReplacementEnabled = false textView.isAutomaticTextReplacementEnabled = false
@ -179,6 +181,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
scrollView.editorTextView = textView scrollView.editorTextView = textView
scrollView.updateEditorInsets() scrollView.updateEditorInsets()
context.coordinator.applyHybridAttributes(to: textView) context.coordinator.applyHybridAttributes(to: textView)
context.coordinator.requestInitialFocus(for: textView)
return scrollView return scrollView
} }
@ -199,6 +202,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
} }
context.coordinator.applyHybridAttributes(to: textView) context.coordinator.applyHybridAttributes(to: textView)
context.coordinator.requestInitialFocus(for: textView)
} }
final class Coordinator: NSObject, NSTextViewDelegate { final class Coordinator: NSObject, NSTextViewDelegate {
@ -206,6 +210,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
private var programmaticUpdateDepth = 0 private var programmaticUpdateDepth = 0
private var lastStyledText: String? private var lastStyledText: String?
private var lastStyledActiveLineIndex: Int? private var lastStyledActiveLineIndex: Int?
private var didFocusTextView = false
init(_ parent: NativeMarkdownTextView) { init(_ parent: NativeMarkdownTextView) {
self.parent = parent self.parent = parent
@ -263,6 +268,24 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
} }
} }
func requestInitialFocus(for textView: NSTextView) {
guard !didFocusTextView else { return }
DispatchQueue.main.async { [weak self, weak textView] in
guard let self,
let textView,
let window = textView.window,
!self.didFocusTextView
else { return }
if window.firstResponder !== textView {
window.makeFirstResponder(textView)
}
self.didFocusTextView = window.firstResponder === textView
}
}
func performProgrammaticUpdate(_ updates: () -> Void) { func performProgrammaticUpdate(_ updates: () -> Void) {
programmaticUpdateDepth += 1 programmaticUpdateDepth += 1
defer { defer {
@ -281,6 +304,12 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
} }
} }
private final class EditorTextView: NSTextView {
override var acceptsFirstResponder: Bool {
true
}
}
private final class ComfortableEditorScrollView: NSScrollView { private final class ComfortableEditorScrollView: NSScrollView {
weak var editorTextView: NSTextView? weak var editorTextView: NSTextView?
@ -323,6 +352,7 @@ private struct NativeMarkdownTextView: UIViewRepresentable {
textView.textContainerInset = UIEdgeInsets(top: 28, left: 24, bottom: 28, right: 24) textView.textContainerInset = UIEdgeInsets(top: 28, left: 24, bottom: 28, right: 24)
textView.backgroundColor = .systemBackground textView.backgroundColor = .systemBackground
context.coordinator.applyHybridAttributes(to: textView) context.coordinator.applyHybridAttributes(to: textView)
context.coordinator.requestInitialFocus(for: textView)
return textView return textView
} }
@ -334,6 +364,7 @@ private struct NativeMarkdownTextView: UIViewRepresentable {
} }
} }
context.coordinator.applyHybridAttributes(to: textView) context.coordinator.applyHybridAttributes(to: textView)
context.coordinator.requestInitialFocus(for: textView)
} }
final class Coordinator: NSObject, UITextViewDelegate { final class Coordinator: NSObject, UITextViewDelegate {
@ -341,6 +372,7 @@ private struct NativeMarkdownTextView: UIViewRepresentable {
private var programmaticUpdateDepth = 0 private var programmaticUpdateDepth = 0
private var lastStyledText: String? private var lastStyledText: String?
private var lastStyledActiveLineIndex: Int? private var lastStyledActiveLineIndex: Int?
private var didFocusTextView = false
init(_ parent: NativeMarkdownTextView) { init(_ parent: NativeMarkdownTextView) {
self.parent = parent self.parent = parent
@ -395,6 +427,20 @@ private struct NativeMarkdownTextView: UIViewRepresentable {
updates() updates()
} }
func requestInitialFocus(for textView: UITextView) {
guard !didFocusTextView else { return }
DispatchQueue.main.async { [weak self, weak textView] in
guard let self,
let textView,
textView.window != nil,
!self.didFocusTextView
else { return }
self.didFocusTextView = textView.becomeFirstResponder()
}
}
private var isPerformingProgrammaticUpdate: Bool { private var isPerformingProgrammaticUpdate: Bool {
programmaticUpdateDepth > 0 programmaticUpdateDepth > 0
} }