diff options
Diffstat (limited to 'Sources/NorgKeyboardToolbar/NorgKeyboardToolbar.swift')
| -rw-r--r-- | Sources/NorgKeyboardToolbar/NorgKeyboardToolbar.swift | 59 |
1 files changed, 59 insertions, 0 deletions
diff --git a/Sources/NorgKeyboardToolbar/NorgKeyboardToolbar.swift b/Sources/NorgKeyboardToolbar/NorgKeyboardToolbar.swift new file mode 100644 index 0000000..fdd114b --- /dev/null +++ b/Sources/NorgKeyboardToolbar/NorgKeyboardToolbar.swift @@ -0,0 +1,59 @@ +#if canImport(SwiftUI) + import SwiftUI + + /// `ToolbarContent` that provides one button per `EditorSnippet`. + /// + /// It takes the same `text` and `selection` bindings used with `TextEditor`. + /// + /// ```swift + /// TextEditor(text: $draft, selection: $selection) + /// .toolbar { NorgKeyboardToolbar(text: $draft, selection: $selection) } + /// ``` + /// + /// Tapping a button applies the corresponding snippet to `text` and moves the + /// insertion point to the snippet's cursor offset. + @available(iOS 18.0, macOS 15.0, visionOS 2.0, *) + public struct NorgKeyboardToolbar: ToolbarContent { + @Binding private var text: String + @Binding private var selection: TextSelection? + + public init(text: Binding<String>, selection: Binding<TextSelection?>) { + self._text = text + self._selection = selection + } + + public var body: some ToolbarContent { + ToolbarItemGroup(placement: .keyboard) { + ForEach(EditorSnippet.allCases) { snippet in + Button(snippet.label, systemImage: snippet.systemImage) { insert(snippet) } + .labelStyle(.iconOnly) + } + } + } + + private func insert(_ snippet: EditorSnippet) { + let result = snippet.apply(to: text, cursor: cursorOffset()) + text = result.text + + // Apply the new cursor after the text change commits, otherwise the + // editor resets the insertion point to the end. + DispatchQueue.main.async { + let clamped = min(max(result.cursor, 0), text.count) + selection = TextSelection(insertionPoint: text.index(text.startIndex, offsetBy: clamped)) + } + } + + private func cursorOffset() -> Int { + guard let selection else { return text.count } + switch selection.indices { + case .selection(let range): + return text.distance(from: text.startIndex, to: range.lowerBound) + case .multiSelection(let rangeSet): + guard let first = rangeSet.ranges.first else { return text.count } + return text.distance(from: text.startIndex, to: first.lowerBound) + @unknown default: + return text.count + } + } + } +#endif |