import SwiftUI import Runestone final class EditorKeyboardBridge: ObservableObject { weak var activeTextView: Runestone.TextView? } struct EditorKeyboardToolbar: View { @ObservedObject var bridge: EditorKeyboardBridge @State private var showFindBar = false @State private var searchQuery = "" @State private var isRegex = false var body: some View { VStack(spacing: 0) { if showFindBar { InlineFindBar(query: $searchQuery, isRegex: $isRegex, onNext: findNext, onPrev: findPrev, onDismiss: { withAnimation { showFindBar = false } }) .transition(.move(edge: .top).combined(with: .opacity)) Divider() } ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 4) { KeyToolbarButton(icon: "increase.indent", label: "Indent") { bridge.activeTextView?.shiftRight() } KeyToolbarButton(icon: "decrease.indent", label: "Unindent") { bridge.activeTextView?.shiftLeft() } KeyToolbarDivider() KeyToolbarButton(icon: "text.badge.slash", label: "Comment") { toggleComment() } KeyToolbarDivider() KeyToolbarText("(") { insertPair("(", ")") } KeyToolbarText("[") { insertPair("[", "]") } KeyToolbarText("{") { insertPair("{", "}") } KeyToolbarDivider() KeyToolbarText("->") { insert(" -> ") } KeyToolbarText("??") { insert(" ?? ") } KeyToolbarText("$0") { insert("$0") } KeyToolbarDivider() KeyToolbarButton(icon: "arrow.left", label: "Left") { moveCursor(-1) } KeyToolbarButton(icon: "arrow.right", label: "Right") { moveCursor(+1) } KeyToolbarDivider() KeyToolbarButton(icon: "magnifyingglass", label: "Find") { withAnimation { showFindBar.toggle() } } Spacer() KeyToolbarButton(icon: "keyboard.chevron.compact.down", label: "Done") { bridge.activeTextView?.resignFirstResponder() } } .padding(.horizontal, 8).padding(.vertical, 6) } .background(.regularMaterial) } } private func insert(_ t: String) { bridge.activeTextView?.insertText(t) } private func insertPair(_ l: String, _ r: String) { bridge.activeTextView?.insertText(l+r); moveCursor(-1) } private func moveCursor(_ offset: Int) { guard let tv = bridge.activeTextView, let sel = tv.selectedRange else { return } tv.selectedRange = NSRange(location: max(0, sel.location + offset), length: 0) } private func toggleComment() { guard let tv = bridge.activeTextView, let sel = tv.selectedRange else { return } let text = tv.text as NSString let lr = text.lineRange(for: sel) let line = text.substring(with: lr) let trimmed = line.trimmingCharacters(in: .whitespaces) let indent = String(line.prefix(while: { $0 == " " || $0 == "\t" })) let newLine: String if trimmed.hasPrefix("// ") { newLine = line.replacingFirstOccurrence(of: "// ", with: "") } else if trimmed.hasPrefix("//") { newLine = line.replacingFirstOccurrence(of: "//", with: "") } else { newLine = indent + "// " + line.dropFirst(indent.count) } tv.replace(lr, withText: newLine) } private func findNext() { guard let tv = bridge.activeTextView, !searchQuery.isEmpty else { return } tv.selectNextOccurrence(matching: SearchOptions(query: searchQuery)) } private func findPrev() { guard let tv = bridge.activeTextView, !searchQuery.isEmpty else { return } tv.selectPreviousOccurrence(matching: SearchOptions(query: searchQuery)) } } struct InlineFindBar: View { @Binding var query: String; @Binding var isRegex: Bool var onNext: () -> Void; var onPrev: () -> Void; var onDismiss: () -> Void var body: some View { HStack(spacing: 8) { Image(systemName: "magnifyingglass").foregroundStyle(.secondary) TextField("Find…", text: $query).textFieldStyle(.plain) .font(.system(.body, design: .monospaced)).autocorrectionDisabled().autocapitalization(.none) .onSubmit(onNext) Toggle(isOn: $isRegex) { Image(systemName: "ellipsis.curlybraces") } .toggleStyle(.button).buttonStyle(.bordered).tint(isRegex ? .accentColor : .secondary) Button(action: onPrev) { Image(systemName: "chevron.up") }.buttonStyle(.bordered).disabled(query.isEmpty) Button(action: onNext) { Image(systemName: "chevron.down") }.buttonStyle(.bordered).disabled(query.isEmpty) Button(action: onDismiss) { Image(systemName: "xmark") }.buttonStyle(.bordered) } .padding(.horizontal, 12).padding(.vertical, 6) } } struct KeyToolbarButton: View { let icon: String; let label: String; let action: () -> Void var body: some View { Button(action: action) { Image(systemName: icon).frame(minWidth: 32, minHeight: 32).contentShape(Rectangle()) } .buttonStyle(.plain).foregroundStyle(.primary).help(label) } } struct KeyToolbarText: View { let text: String; let action: () -> Void init(_ text: String, action: @escaping () -> Void) { self.text = text; self.action = action } var body: some View { Button(action: action) { Text(text).font(.system(.callout, design: .monospaced).bold()) .frame(minWidth: 32, minHeight: 32).contentShape(Rectangle()) } .buttonStyle(.plain).foregroundStyle(.primary) } } struct KeyToolbarDivider: View { var body: some View { Divider().frame(height: 20).padding(.horizontal, 2) } }