oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1import SwiftUI
2@MainActor struct EditorToolbar: View {
3 @Bindable var model: DocsModel
4 let content: NSAttributedString
5 @State private var settledSelection = DocumentToolbarSelection()
6 private var selection: DocumentToolbarSelection { currentSelection ?? settledSelection }
7 private var currentSelection: DocumentToolbarSelection? { DocumentToolbarSelection(commands: actions) }
8 var body: some View {
9 let selection = self.selection
10 ScrollView(.horizontal) {
11 HStack(spacing: 12) {
12 command("docs.editor.toolbar.undo", icon: "arrow.uturn.backward", op: "undo", enabled: selection.canUndo)
13 command("docs.editor.toolbar.redo", icon: "arrow.uturn.forward", op: "redo", enabled: selection.canRedo)
14 Divider().frame(height: 24)
15 Menu {
16 Button { block("paragraph") } label: { Text("docs.editor.toolbar.block.paragraph", bundle: DocsResources.bundle) }
17 ForEach(1...3, id: \.self) { level in
18 Button { block("heading", level: level) } label: { Text(LocalizedStringKey("docs.editor.toolbar.block.heading\(level)"), bundle: DocsResources.bundle) }
19 }
20 } label: { Text(selection.styleLabel).frame(minWidth: 100, alignment: .leading) }.accessibilityLabel(Text("native.style", bundle: DocsResources.bundle))
21 Menu {
22 ForEach(["sans", "serif", "mono"], id: \.self) { family in
23 Button { format("font_" + family, exclusive: ["font_sans", "font_serif", "font_mono"]) } label: { Text(LocalizedStringKey("docs.editor.toolbar.font." + family), bundle: DocsResources.bundle) }
24 }
25 } label: { Text(LocalizedStringKey("docs.editor.toolbar.font." + selection.font), bundle: DocsResources.bundle).frame(minWidth: 80, alignment: .leading) }.accessibilityLabel(Text("native.font", bundle: DocsResources.bundle))
26 Menu {
27 ForEach([12, 8, 9, 10, 11, 14, 18, 24, 36, 48, 72], id: \.self) { size in
28 Button(size.formatted()) { format("size_\(size)", exclusive: [8,9,10,11,12,14,18,24,36,48,72].map { "size_\($0)" } + ["size_sm", "size_lg", "size_xl"]) }
29 }
30 } label: { Text(selection.size.formatted()).frame(minWidth: 32) }.accessibilityLabel(Text("native.size", bundle: DocsResources.bundle))
31 Divider().frame(height: 24)
32 mark("docs.editor.toolbar.bold", icon: "bold", name: "strong", active: selection.marks.contains("strong"))
33 mark("docs.editor.toolbar.italic", icon: "italic", name: "em", active: selection.marks.contains("em"))
34 mark("docs.editor.toolbar.code", icon: "chevron.left.forwardslash.chevron.right", name: "code", active: selection.marks.contains("code"))
35 Divider().frame(height: 24)
36 ForEach(["left", "center", "right"], id: \.self) { alignment in
37 Button { blockAttribute("align", value: alignment) } label: { Image(systemName: "text.align" + alignment) }
38 .accessibilityLabel(Text(LocalizedStringKey("docs.editor.toolbar.align." + alignment), bundle: DocsResources.bundle))
39 .foregroundStyle(selection.alignment == alignment ? Color.accentColor : Color.primary)
40 .accessibilityAddTraits(selection.alignment == alignment ? .isSelected : [])
41 }
42 Divider().frame(height: 24)
43 ForEach(["bullet_list", "ordered_list", "blockquote"], id: \.self) { type in
44 Button { block(type) } label: { Image(systemName: type == "bullet_list" ? "list.bullet" : type == "ordered_list" ? "list.number" : "text.quote") }
45 .accessibilityLabel(Text(LocalizedStringKey("native." + type), bundle: DocsResources.bundle))
46 .foregroundStyle(selection.blocks.contains(type) ? Color.accentColor : Color.primary)
47 .accessibilityAddTraits(selection.blocks.contains(type) ? .isSelected : [])
48 }
49 command("docs.editor.menu.format.indent", icon: "increase.indent", op: "indent", enabled: true)
50 command("docs.editor.menu.format.outdent", icon: "decrease.indent", op: "outdent", enabled: true)
51 command("docs.editor.menu.format.clear", icon: "eraser", op: "clear-format", enabled: selection.hasSelection)
52 }.buttonStyle(.borderless).padding(.horizontal, 12).padding(.vertical, 8)
53 }
54 .scrollIndicators(.hidden)
55 .fixedSize(horizontal: false, vertical: true)
56 .accessibilityIdentifier("docs.formatting-toolbar")
57 // Replica work briefly gates commands on every keystroke. Keep that
58 // synchronization gate out of disabled styling so the bar does not blink.
59 .disabled(!actions.isAvailable)
60 .allowsHitTesting(actions.isEnabled)
61 .onChange(of: currentSelection, initial: true) { _, value in
62 if let value { settledSelection = value }
63 }
64 }
65 private func command(_ key: String, icon: String, op: String, enabled: Bool) -> some View {
66 Button { actions.perform(op) } label: { Image(systemName: icon) }
67 .accessibilityLabel(Text(LocalizedStringKey(key), bundle: DocsResources.bundle)).disabled(!enabled)
68 }
69 private func mark(_ key: String, icon: String, name: String, active: Bool) -> some View {
70 Button { format(name) } label: { Image(systemName: icon) }.accessibilityLabel(Text(LocalizedStringKey(key), bundle: DocsResources.bundle))
71 .foregroundStyle(active ? Color.accentColor : Color.primary)
72 .accessibilityAddTraits(active ? .isSelected : [])
73 }
74 private var actions: DocumentCommands { DocumentCommands(model: model, content: content) }
75 private func format(_ name: String, exclusive: [String] = []) { actions.format(name, exclusive: exclusive) }
76 private func block(_ type: String, level: Int = 1) { actions.block(type, level: level) }
77 private func blockAttribute(_ name: String, value: String) { actions.blockAttribute(name, value: value == "left" ? nil : value) }
78}
79