mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
922 lines
35 KiB
Swift
922 lines
35 KiB
Swift
import SwiftUI
|
||
#if os(macOS)
|
||
import AppKit
|
||
#elseif os(iOS)
|
||
import UIKit
|
||
#endif
|
||
|
||
extension ContentView {
|
||
private var compactActiveProviderName: String {
|
||
activeProviderName.components(separatedBy: " (").first ?? activeProviderName
|
||
}
|
||
|
||
#if os(iOS)
|
||
private var iOSToolbarChromeStyle: GlassChromeStyle { .single }
|
||
private var iOSToolbarTintColor: Color {
|
||
if toolbarIconsBlueIOS {
|
||
return NeonUIStyle.accentBlue
|
||
}
|
||
return colorScheme == .dark ? Color.white.opacity(0.95) : Color.primary.opacity(0.92)
|
||
}
|
||
|
||
private var isIPadToolbarLayout: Bool {
|
||
guard UIDevice.current.userInterfaceIdiom == .pad else { return false }
|
||
// During first render on iOS, horizontalSizeClass can transiently be nil.
|
||
// Treat nil as regular so the full iPad toolbar appears immediately.
|
||
if horizontalSizeClass == .compact { return false }
|
||
return true
|
||
}
|
||
|
||
private var iPhoneToolbarWidth: CGFloat {
|
||
UIApplication.shared.connectedScenes
|
||
.compactMap { $0 as? UIWindowScene }
|
||
.first(where: { $0.activationState == .foregroundActive })?
|
||
.screen.bounds.width ?? 390
|
||
}
|
||
|
||
private var iPhonePromotedActionsCount: Int {
|
||
switch iPhoneToolbarWidth {
|
||
case 430...: return 4
|
||
case 395...: return 3
|
||
default: return 2
|
||
}
|
||
}
|
||
|
||
private var iPhoneLanguagePickerWidth: CGFloat {
|
||
switch iPhoneToolbarWidth {
|
||
case 430...: return 74
|
||
case 395...: return 68
|
||
default: return 62
|
||
}
|
||
}
|
||
|
||
private var iPadToolbarMaxWidth: CGFloat {
|
||
let screenWidth = UIApplication.shared.connectedScenes
|
||
.compactMap { $0 as? UIWindowScene }
|
||
.first(where: { $0.activationState == .foregroundActive })?
|
||
.screen.bounds.width ?? 1024
|
||
let target = screenWidth * 0.96
|
||
return min(max(target, 760), 1320)
|
||
}
|
||
|
||
|
||
private enum IPadToolbarAction: String, CaseIterable, Hashable {
|
||
case openFile
|
||
case newTab
|
||
case saveFile
|
||
case toggleSidebar
|
||
case toggleProjectSidebar
|
||
case findReplace
|
||
case settings
|
||
case codeCompletion
|
||
case performanceMode
|
||
case lineWrap
|
||
case keyboardAccessory
|
||
case clearEditor
|
||
case insertTemplate
|
||
case brainDump
|
||
case welcomeTour
|
||
case translucentWindow
|
||
}
|
||
|
||
private var iPadActionPriority: [IPadToolbarAction] {
|
||
[
|
||
.openFile,
|
||
.newTab,
|
||
.saveFile,
|
||
.toggleSidebar,
|
||
.toggleProjectSidebar,
|
||
.findReplace,
|
||
.settings,
|
||
.codeCompletion,
|
||
.lineWrap,
|
||
.keyboardAccessory,
|
||
.clearEditor,
|
||
.insertTemplate,
|
||
.performanceMode,
|
||
.brainDump,
|
||
.welcomeTour,
|
||
.translucentWindow
|
||
]
|
||
}
|
||
|
||
private func toggleKeyboardAccessoryBar() {
|
||
showKeyboardAccessoryBarIOS.toggle()
|
||
NotificationCenter.default.post(
|
||
name: .keyboardAccessoryBarVisibilityChanged,
|
||
object: showKeyboardAccessoryBarIOS
|
||
)
|
||
}
|
||
|
||
private var iPadPinnedOverflowActions: Set<IPadToolbarAction> {
|
||
[
|
||
.performanceMode,
|
||
.brainDump,
|
||
.welcomeTour,
|
||
.translucentWindow
|
||
]
|
||
}
|
||
|
||
private var iPadPromotedActionSlotCount: Int {
|
||
switch iPadToolbarMaxWidth {
|
||
case 1160...: return 11
|
||
case 1060...: return 10
|
||
case 980...: return 10
|
||
case 900...: return 9
|
||
case 820...: return 8
|
||
case 760...: return 8
|
||
default: return 7
|
||
}
|
||
}
|
||
|
||
private var iPadPromotedActions: [IPadToolbarAction] {
|
||
let eligible = iPadActionPriority.filter { !iPadPinnedOverflowActions.contains($0) }
|
||
return Array(eligible.prefix(iPadPromotedActionSlotCount))
|
||
}
|
||
|
||
private var iPadOverflowActions: [IPadToolbarAction] {
|
||
iPadActionPriority.filter { iPadPinnedOverflowActions.contains($0) || !iPadPromotedActions.contains($0) }
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var newTabControl: some View {
|
||
Button(action: { viewModel.addNewTab() }) {
|
||
Image(systemName: "plus.square.on.square")
|
||
}
|
||
.help("New Tab (Cmd+T)")
|
||
.keyboardShortcut("t", modifiers: .command)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var settingsControl: some View {
|
||
Button(action: { showSettingsSheet = true }) {
|
||
Image(systemName: "gearshape")
|
||
}
|
||
.help("Settings (Cmd+,)")
|
||
.keyboardShortcut(",", modifiers: .command)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var languagePickerControl: some View {
|
||
Picker("Language", selection: currentLanguagePickerBinding) {
|
||
ForEach(["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "cobol", "dotenv", "proto", "graphql", "rst", "nginx", "sql", "html", "expressionengine", "css", "c", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "vim", "log", "ipynb", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in
|
||
Text(iOSToolbarLanguageLabel(lang)).tag(lang)
|
||
}
|
||
}
|
||
.labelsHidden()
|
||
.help("Language")
|
||
.frame(width: isIPadToolbarLayout ? 100 : iPhoneLanguagePickerWidth)
|
||
.tint(iOSToolbarTintColor)
|
||
}
|
||
|
||
private func iOSToolbarLanguageLabel(_ lang: String) -> String {
|
||
switch lang {
|
||
case "swift": return "Sw"
|
||
case "python": return "Py"
|
||
case "javascript": return "JS"
|
||
case "typescript": return "TS"
|
||
case "php": return "PHP"
|
||
case "java": return "Jv"
|
||
case "kotlin": return "Kt"
|
||
case "go": return "Go"
|
||
case "ruby": return "Rb"
|
||
case "rust": return "Rs"
|
||
case "cobol": return "Cob"
|
||
case "dotenv": return "Env"
|
||
case "proto": return "Prt"
|
||
case "graphql": return "GQL"
|
||
case "rst": return "RST"
|
||
case "nginx": return "Ngnx"
|
||
case "sql": return "SQL"
|
||
case "html": return "HTML"
|
||
case "expressionengine": return "EE"
|
||
case "css": return "CSS"
|
||
case "c": return "C"
|
||
case "cpp": return "C++"
|
||
case "csharp": return "C#"
|
||
case "objective-c": return "ObjC"
|
||
case "json": return "JSON"
|
||
case "xml": return "XML"
|
||
case "yaml": return "YML"
|
||
case "toml": return "TML"
|
||
case "csv": return "CSV"
|
||
case "ini": return "INI"
|
||
case "vim": return "Vim"
|
||
case "log": return "Log"
|
||
case "ipynb": return "JNB"
|
||
case "markdown": return "MD"
|
||
case "bash": return "Sh"
|
||
case "zsh": return "zsh"
|
||
case "powershell": return "PS"
|
||
case "standard": return "Std"
|
||
case "plain": return "Txt"
|
||
default: return lang.capitalized
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var activeProviderBadgeControl: some View {
|
||
Text(compactActiveProviderName)
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
.lineLimit(1)
|
||
.truncationMode(.tail)
|
||
.minimumScaleFactor(0.9)
|
||
.fixedSize(horizontal: true, vertical: false)
|
||
.padding(.horizontal, 6)
|
||
.padding(.vertical, 2)
|
||
.background(Color.secondary.opacity(0.12), in: Capsule())
|
||
.help("Active provider")
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var clearEditorControl: some View {
|
||
Button(action: {
|
||
requestClearEditorContent()
|
||
}) {
|
||
Image(systemName: "trash")
|
||
}
|
||
.help("Clear Editor")
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var insertTemplateControl: some View {
|
||
Button(action: { insertTemplateForCurrentLanguage() }) {
|
||
Image(systemName: "doc.badge.plus")
|
||
}
|
||
.help("Insert Template for Current Language")
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var lineWrapControl: some View {
|
||
Button(action: {
|
||
viewModel.isLineWrapEnabled.toggle()
|
||
}) {
|
||
Image(systemName: "text.justify")
|
||
}
|
||
.help("Enable Wrap / Disable Wrap (Cmd+Opt+L)")
|
||
.keyboardShortcut("l", modifiers: [.command, .option])
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var openFileControl: some View {
|
||
Button(action: { openFileFromToolbar() }) {
|
||
Image(systemName: "folder")
|
||
}
|
||
.help("Open File… (Cmd+O)")
|
||
.keyboardShortcut("o", modifiers: .command)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var saveFileControl: some View {
|
||
Button(action: { saveCurrentTabFromToolbar() }) {
|
||
Image(systemName: "square.and.arrow.down")
|
||
}
|
||
.disabled(viewModel.selectedTab == nil)
|
||
.help("Save File (Cmd+S)")
|
||
.keyboardShortcut("s", modifiers: .command)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var toggleSidebarControl: some View {
|
||
Button(action: { toggleSidebarFromToolbar() }) {
|
||
Image(systemName: "sidebar.left")
|
||
}
|
||
.help("Toggle Sidebar (Cmd+Opt+S)")
|
||
.keyboardShortcut("s", modifiers: [.command, .option])
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var toggleProjectSidebarControl: some View {
|
||
Button(action: { toggleProjectSidebarFromToolbar() }) {
|
||
Image(systemName: "sidebar.right")
|
||
}
|
||
.help("Toggle Project Structure Sidebar")
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var findReplaceControl: some View {
|
||
Button(action: { showFindReplace = true }) {
|
||
Image(systemName: "magnifyingglass")
|
||
}
|
||
.help("Find & Replace (Cmd+F)")
|
||
.keyboardShortcut("f", modifiers: .command)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var brainDumpControl: some View {
|
||
Button(action: {
|
||
viewModel.isBrainDumpMode.toggle()
|
||
UserDefaults.standard.set(viewModel.isBrainDumpMode, forKey: "BrainDumpModeEnabled")
|
||
}) {
|
||
Image(systemName: "note.text")
|
||
.symbolVariant(viewModel.isBrainDumpMode ? .fill : .none)
|
||
}
|
||
.help("Brain Dump Mode")
|
||
.accessibilityLabel("Brain Dump Mode")
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var codeCompletionControl: some View {
|
||
Button(action: {
|
||
toggleAutoCompletion()
|
||
}) {
|
||
Image(systemName: "bolt.horizontal.circle")
|
||
.symbolVariant(isAutoCompletionEnabled ? .fill : .none)
|
||
}
|
||
.help(isAutoCompletionEnabled ? "Disable Code Completion" : "Enable Code Completion")
|
||
.accessibilityLabel("Code Completion")
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var performanceModeControl: some View {
|
||
Button(action: {
|
||
forceLargeFileMode.toggle()
|
||
updateLargeFileMode(for: currentContentBinding.wrappedValue)
|
||
recordDiagnostic("Toolbar toggled performance mode: \(forceLargeFileMode ? "on" : "off")")
|
||
}) {
|
||
Image(systemName: forceLargeFileMode ? "speedometer" : "speedometer")
|
||
.symbolVariant(forceLargeFileMode ? .fill : .none)
|
||
}
|
||
.help("Performance Mode")
|
||
.accessibilityLabel("Performance Mode")
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var keyboardAccessoryControl: some View {
|
||
Button(action: {
|
||
toggleKeyboardAccessoryBar()
|
||
}) {
|
||
Image(systemName: showKeyboardAccessoryBarIOS ? "keyboard.chevron.compact.down.fill" : "keyboard.chevron.compact.down")
|
||
}
|
||
.help(showKeyboardAccessoryBarIOS ? "Hide Keyboard Snippet Bar" : "Show Keyboard Snippet Bar")
|
||
.accessibilityLabel("Keyboard Snippet Bar")
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var welcomeTourControl: some View {
|
||
Button(action: {
|
||
showWelcomeTour = true
|
||
}) {
|
||
Image(systemName: "sparkles.rectangle.stack")
|
||
}
|
||
.help("Welcome Tour")
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var translucentWindowControl: some View {
|
||
Button(action: {
|
||
enableTranslucentWindow.toggle()
|
||
UserDefaults.standard.set(enableTranslucentWindow, forKey: "EnableTranslucentWindow")
|
||
NotificationCenter.default.post(name: .toggleTranslucencyRequested, object: enableTranslucentWindow)
|
||
}) {
|
||
Image(systemName: enableTranslucentWindow ? "rectangle.fill" : "rectangle")
|
||
}
|
||
.help("Toggle Translucent Window Background")
|
||
.accessibilityLabel("Translucent Window Background")
|
||
}
|
||
|
||
@ViewBuilder
|
||
private func iPadToolbarActionControl(_ action: IPadToolbarAction) -> some View {
|
||
switch action {
|
||
case .openFile: openFileControl
|
||
case .newTab: newTabControl
|
||
case .saveFile: saveFileControl
|
||
case .toggleSidebar: toggleSidebarControl
|
||
case .toggleProjectSidebar: toggleProjectSidebarControl
|
||
case .findReplace: findReplaceControl
|
||
case .settings: settingsControl
|
||
case .codeCompletion: codeCompletionControl
|
||
case .performanceMode: performanceModeControl
|
||
case .lineWrap: lineWrapControl
|
||
case .keyboardAccessory: keyboardAccessoryControl
|
||
case .clearEditor: clearEditorControl
|
||
case .insertTemplate: insertTemplateControl
|
||
case .brainDump: brainDumpControl
|
||
case .welcomeTour: welcomeTourControl
|
||
case .translucentWindow: translucentWindowControl
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var iPadOverflowMenuControl: some View {
|
||
if !iPadOverflowActions.isEmpty {
|
||
Menu {
|
||
ForEach(iPadOverflowActions, id: \.self) { action in
|
||
switch action {
|
||
case .openFile:
|
||
Button(action: { openFileFromToolbar() }) {
|
||
Label("Open File…", systemImage: "folder")
|
||
}
|
||
case .newTab:
|
||
Button(action: { viewModel.addNewTab() }) {
|
||
Label("New Tab", systemImage: "plus.square.on.square")
|
||
}
|
||
case .saveFile:
|
||
Button(action: { saveCurrentTabFromToolbar() }) {
|
||
Label("Save File", systemImage: "square.and.arrow.down")
|
||
}
|
||
.disabled(viewModel.selectedTab == nil)
|
||
case .toggleSidebar:
|
||
Button(action: { toggleSidebarFromToolbar() }) {
|
||
Label("Toggle Sidebar", systemImage: "sidebar.left")
|
||
}
|
||
case .toggleProjectSidebar:
|
||
Button(action: { toggleProjectSidebarFromToolbar() }) {
|
||
Label("Toggle Project Structure Sidebar", systemImage: "sidebar.right")
|
||
}
|
||
case .findReplace:
|
||
Button(action: { showFindReplace = true }) {
|
||
Label("Find & Replace", systemImage: "magnifyingglass")
|
||
}
|
||
case .settings:
|
||
Button(action: { showSettingsSheet = true }) {
|
||
Label("Settings", systemImage: "gearshape")
|
||
}
|
||
case .codeCompletion:
|
||
Button(action: { toggleAutoCompletion() }) {
|
||
Label(isAutoCompletionEnabled ? "Disable Code Completion" : "Enable Code Completion", systemImage: "text.badge.plus")
|
||
}
|
||
case .performanceMode:
|
||
Button(action: {
|
||
forceLargeFileMode.toggle()
|
||
updateLargeFileMode(for: currentContentBinding.wrappedValue)
|
||
}) {
|
||
Label(forceLargeFileMode ? "Disable Performance Mode" : "Enable Performance Mode", systemImage: "speedometer")
|
||
}
|
||
case .lineWrap:
|
||
Button(action: { viewModel.isLineWrapEnabled.toggle() }) {
|
||
Label("Enable Wrap / Disable Wrap", systemImage: "text.justify")
|
||
}
|
||
case .keyboardAccessory:
|
||
Button(action: { toggleKeyboardAccessoryBar() }) {
|
||
Label(
|
||
showKeyboardAccessoryBarIOS ? "Hide Keyboard Snippet Bar" : "Show Keyboard Snippet Bar",
|
||
systemImage: showKeyboardAccessoryBarIOS ? "keyboard.chevron.compact.down.fill" : "keyboard.chevron.compact.down"
|
||
)
|
||
}
|
||
case .clearEditor:
|
||
Button(action: { requestClearEditorContent() }) {
|
||
Label("Clear Editor", systemImage: "trash")
|
||
}
|
||
case .insertTemplate:
|
||
Button(action: { insertTemplateForCurrentLanguage() }) {
|
||
Label("Insert Template", systemImage: "doc.badge.plus")
|
||
}
|
||
case .brainDump:
|
||
Button(action: {
|
||
viewModel.isBrainDumpMode.toggle()
|
||
UserDefaults.standard.set(viewModel.isBrainDumpMode, forKey: "BrainDumpModeEnabled")
|
||
}) {
|
||
Label("Brain Dump Mode", systemImage: "note.text")
|
||
}
|
||
case .welcomeTour:
|
||
Button(action: { showWelcomeTour = true }) {
|
||
Label("Welcome Tour", systemImage: "sparkles.rectangle.stack")
|
||
}
|
||
case .translucentWindow:
|
||
Button(action: {
|
||
enableTranslucentWindow.toggle()
|
||
UserDefaults.standard.set(enableTranslucentWindow, forKey: "EnableTranslucentWindow")
|
||
NotificationCenter.default.post(name: .toggleTranslucencyRequested, object: enableTranslucentWindow)
|
||
}) {
|
||
Label("Translucent Window Background", systemImage: enableTranslucentWindow ? "rectangle.fill" : "rectangle")
|
||
}
|
||
}
|
||
}
|
||
|
||
Button(action: { toolbarIconsBlueIOS.toggle() }) {
|
||
Label("Blue Toolbar Icons", systemImage: toolbarIconsBlueIOS ? "checkmark.circle.fill" : "circle")
|
||
}
|
||
} label: {
|
||
Image(systemName: "ellipsis.circle")
|
||
}
|
||
.help("More Actions")
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var moreActionsControl: some View {
|
||
Menu {
|
||
Button(action: {
|
||
showSettingsSheet = true
|
||
}) {
|
||
Label("Settings", systemImage: "gearshape")
|
||
}
|
||
|
||
Button(action: {
|
||
requestClearEditorContent()
|
||
}) {
|
||
Label("Clear Editor", systemImage: "trash")
|
||
}
|
||
|
||
Button(action: { insertTemplateForCurrentLanguage() }) {
|
||
Label("Insert Template", systemImage: "doc.badge.plus")
|
||
}
|
||
|
||
Button(action: { openFileFromToolbar() }) {
|
||
Label("Open File…", systemImage: "folder")
|
||
}
|
||
.keyboardShortcut("o", modifiers: .command)
|
||
|
||
Button(action: { saveCurrentTabFromToolbar() }) {
|
||
Label("Save File", systemImage: "square.and.arrow.down")
|
||
}
|
||
.disabled(viewModel.selectedTab == nil)
|
||
.keyboardShortcut("s", modifiers: .command)
|
||
|
||
Button(action: { toggleSidebarFromToolbar() }) {
|
||
Label("Toggle Sidebar", systemImage: "sidebar.left")
|
||
}
|
||
.keyboardShortcut("s", modifiers: [.command, .option])
|
||
|
||
Button(action: { toggleProjectSidebarFromToolbar() }) {
|
||
Label("Toggle Project Structure Sidebar", systemImage: "sidebar.right")
|
||
}
|
||
|
||
Button(action: { showFindReplace = true }) {
|
||
Label("Find & Replace", systemImage: "magnifyingglass")
|
||
}
|
||
.keyboardShortcut("f", modifiers: .command)
|
||
|
||
Button(action: { viewModel.isLineWrapEnabled.toggle() }) {
|
||
Label("Enable Wrap / Disable Wrap", systemImage: "text.justify")
|
||
}
|
||
.keyboardShortcut("l", modifiers: [.command, .option])
|
||
|
||
Button(action: { toggleAutoCompletion() }) {
|
||
Label(isAutoCompletionEnabled ? "Disable Code Completion" : "Enable Code Completion", systemImage: "text.badge.plus")
|
||
}
|
||
|
||
Button(action: {
|
||
toggleKeyboardAccessoryBar()
|
||
}) {
|
||
Label(
|
||
showKeyboardAccessoryBarIOS ? "Hide Keyboard Snippet Bar" : "Show Keyboard Snippet Bar",
|
||
systemImage: showKeyboardAccessoryBarIOS ? "keyboard.chevron.compact.down.fill" : "keyboard.chevron.compact.down"
|
||
)
|
||
}
|
||
|
||
Button(action: {
|
||
forceLargeFileMode.toggle()
|
||
updateLargeFileMode(for: currentContentBinding.wrappedValue)
|
||
}) {
|
||
Label(forceLargeFileMode ? "Disable Performance Mode" : "Enable Performance Mode", systemImage: "speedometer")
|
||
}
|
||
|
||
Button(action: {
|
||
viewModel.isBrainDumpMode.toggle()
|
||
UserDefaults.standard.set(viewModel.isBrainDumpMode, forKey: "BrainDumpModeEnabled")
|
||
}) {
|
||
Label("Brain Dump Mode", systemImage: "note.text")
|
||
}
|
||
|
||
Button(action: {
|
||
showWelcomeTour = true
|
||
}) {
|
||
Label("Welcome Tour", systemImage: "sparkles.rectangle.stack")
|
||
}
|
||
|
||
Button(action: {
|
||
enableTranslucentWindow.toggle()
|
||
UserDefaults.standard.set(enableTranslucentWindow, forKey: "EnableTranslucentWindow")
|
||
NotificationCenter.default.post(name: .toggleTranslucencyRequested, object: enableTranslucentWindow)
|
||
}) {
|
||
Label("Translucent Window Background", systemImage: enableTranslucentWindow ? "rectangle.fill" : "rectangle")
|
||
}
|
||
|
||
Button(action: {
|
||
toolbarIconsBlueIOS.toggle()
|
||
}) {
|
||
Label("Blue Toolbar Icons", systemImage: toolbarIconsBlueIOS ? "checkmark.circle.fill" : "circle")
|
||
}
|
||
|
||
} label: {
|
||
Image(systemName: "ellipsis.circle")
|
||
}
|
||
.help("More Actions")
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var iOSToolbarControls: some View {
|
||
openFileControl
|
||
if iPhonePromotedActionsCount >= 2 { newTabControl }
|
||
if iPhonePromotedActionsCount >= 3 { saveFileControl }
|
||
if iPhonePromotedActionsCount >= 4 { findReplaceControl }
|
||
keyboardAccessoryControl
|
||
Divider()
|
||
.frame(height: 18)
|
||
moreActionsControl
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var iPhonePrimaryToolbarCluster: some View {
|
||
GlassSurface(
|
||
enabled: shouldUseLiquidGlass,
|
||
material: primaryGlassMaterial,
|
||
fallbackColor: toolbarFallbackColor,
|
||
shape: .capsule,
|
||
chromeStyle: iOSToolbarChromeStyle
|
||
) {
|
||
HStack(spacing: 12) {
|
||
languagePickerControl
|
||
iOSToolbarControls
|
||
}
|
||
.padding(.horizontal, 12)
|
||
.padding(.vertical, 8)
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var iPadDistributedToolbarControls: some View {
|
||
languagePickerControl
|
||
ForEach(iPadPromotedActions, id: \.self) { action in
|
||
iPadToolbarActionControl(action)
|
||
}
|
||
if !iPadOverflowActions.isEmpty {
|
||
Divider()
|
||
.frame(height: 18)
|
||
.padding(.horizontal, 2)
|
||
iPadOverflowMenuControl
|
||
}
|
||
}
|
||
#endif
|
||
@ToolbarContentBuilder
|
||
var editorToolbarContent: some ToolbarContent {
|
||
#if os(iOS)
|
||
if isIPadToolbarLayout {
|
||
if #available(iOS 26.0, *) {
|
||
ToolbarItem(placement: .topBarTrailing) {
|
||
GlassSurface(
|
||
enabled: shouldUseLiquidGlass,
|
||
material: primaryGlassMaterial,
|
||
fallbackColor: toolbarFallbackColor,
|
||
shape: .capsule,
|
||
chromeStyle: iOSToolbarChromeStyle
|
||
) {
|
||
HStack(spacing: 6) {
|
||
iPadDistributedToolbarControls
|
||
}
|
||
.padding(.horizontal, 8)
|
||
.padding(.vertical, 8)
|
||
.frame(maxWidth: iPadToolbarMaxWidth, alignment: .leading)
|
||
}
|
||
.scaleEffect(toolbarDensityScale, anchor: .trailing)
|
||
.opacity(toolbarDensityOpacity)
|
||
.animation(.easeOut(duration: 0.18), value: toolbarDensityScale)
|
||
.animation(.easeOut(duration: 0.18), value: toolbarDensityOpacity)
|
||
.tint(iOSToolbarTintColor)
|
||
}
|
||
.sharedBackgroundVisibility(.hidden)
|
||
} else {
|
||
ToolbarItem(placement: .topBarTrailing) {
|
||
GlassSurface(
|
||
enabled: shouldUseLiquidGlass,
|
||
material: primaryGlassMaterial,
|
||
fallbackColor: toolbarFallbackColor,
|
||
shape: .capsule,
|
||
chromeStyle: iOSToolbarChromeStyle
|
||
) {
|
||
HStack(spacing: 6) {
|
||
iPadDistributedToolbarControls
|
||
}
|
||
.padding(.horizontal, 8)
|
||
.padding(.vertical, 8)
|
||
.frame(maxWidth: iPadToolbarMaxWidth, alignment: .leading)
|
||
}
|
||
.scaleEffect(toolbarDensityScale, anchor: .trailing)
|
||
.opacity(toolbarDensityOpacity)
|
||
.animation(.easeOut(duration: 0.18), value: toolbarDensityScale)
|
||
.animation(.easeOut(duration: 0.18), value: toolbarDensityOpacity)
|
||
.tint(iOSToolbarTintColor)
|
||
}
|
||
}
|
||
} else {
|
||
if #available(iOS 26.0, *) {
|
||
ToolbarItem(placement: .principal) {
|
||
iPhonePrimaryToolbarCluster
|
||
.frame(maxWidth: .infinity, alignment: .center)
|
||
.scaleEffect(toolbarDensityScale)
|
||
.opacity(toolbarDensityOpacity)
|
||
.animation(.easeOut(duration: 0.18), value: toolbarDensityScale)
|
||
.animation(.easeOut(duration: 0.18), value: toolbarDensityOpacity)
|
||
.tint(iOSToolbarTintColor)
|
||
}
|
||
.sharedBackgroundVisibility(.hidden)
|
||
} else {
|
||
ToolbarItem(placement: .principal) {
|
||
iPhonePrimaryToolbarCluster
|
||
.frame(maxWidth: .infinity, alignment: .center)
|
||
.scaleEffect(toolbarDensityScale)
|
||
.opacity(toolbarDensityOpacity)
|
||
.animation(.easeOut(duration: 0.18), value: toolbarDensityScale)
|
||
.animation(.easeOut(duration: 0.18), value: toolbarDensityOpacity)
|
||
.tint(iOSToolbarTintColor)
|
||
}
|
||
}
|
||
}
|
||
#else
|
||
ToolbarItemGroup(placement: .automatic) {
|
||
Picker("Language", selection: currentLanguagePickerBinding) {
|
||
ForEach(["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "cobol", "dotenv", "proto", "graphql", "rst", "nginx", "sql", "html", "expressionengine", "css", "c", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "vim", "log", "ipynb", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in
|
||
let label: String = {
|
||
switch lang {
|
||
case "php": return "PHP"
|
||
case "cobol": return "COBOL"
|
||
case "dotenv": return "Dotenv"
|
||
case "proto": return "Proto"
|
||
case "graphql": return "GraphQL"
|
||
case "rst": return "reStructuredText"
|
||
case "nginx": return "Nginx"
|
||
case "objective-c": return "Objective‑C"
|
||
case "csharp": return "C#"
|
||
case "c": return "C"
|
||
case "cpp": return "C++"
|
||
case "json": return "JSON"
|
||
case "xml": return "XML"
|
||
case "yaml": return "YAML"
|
||
case "toml": return "TOML"
|
||
case "csv": return "CSV"
|
||
case "ini": return "INI"
|
||
case "sql": return "SQL"
|
||
case "vim": return "Vim"
|
||
case "log": return "Log"
|
||
case "ipynb": return "Jupyter Notebook"
|
||
case "html": return "HTML"
|
||
case "expressionengine": return "ExpressionEngine"
|
||
case "css": return "CSS"
|
||
case "standard": return "Standard"
|
||
default: return lang.capitalized
|
||
}
|
||
}()
|
||
Text(label).tag(lang)
|
||
}
|
||
}
|
||
.labelsHidden()
|
||
.help("Language")
|
||
.controlSize(.large)
|
||
.frame(width: 140)
|
||
.padding(.vertical, 2)
|
||
|
||
Text(compactActiveProviderName)
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
.lineLimit(1)
|
||
.truncationMode(.tail)
|
||
.minimumScaleFactor(0.9)
|
||
.fixedSize(horizontal: true, vertical: false)
|
||
.padding(.horizontal, 6)
|
||
.padding(.vertical, 2)
|
||
.background(Color.secondary.opacity(0.12), in: Capsule())
|
||
.padding(.leading, 6)
|
||
.help("Active provider")
|
||
|
||
Button(action: {
|
||
openSettings()
|
||
}) {
|
||
Image(systemName: "gearshape")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.help("Settings")
|
||
|
||
if ReleaseRuntimePolicy.isUpdaterEnabledForCurrentDistribution {
|
||
Button(action: {
|
||
showUpdaterDialog(checkNow: true)
|
||
}) {
|
||
Image(systemName: "arrow.triangle.2.circlepath.circle")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.help("Check for Updates")
|
||
}
|
||
|
||
Button(action: { adjustEditorFontSize(-1) }) {
|
||
Image(systemName: "textformat.size.smaller")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.help("Decrease Font Size")
|
||
|
||
Button(action: { adjustEditorFontSize(1) }) {
|
||
Image(systemName: "textformat.size.larger")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.help("Increase Font Size")
|
||
|
||
Button(action: {
|
||
requestClearEditorContent()
|
||
}) {
|
||
Image(systemName: "trash")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.help("Clear Editor")
|
||
|
||
Button(action: {
|
||
insertTemplateForCurrentLanguage()
|
||
}) {
|
||
Image(systemName: "doc.badge.plus")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.help("Insert Template for Current Language")
|
||
|
||
Button(action: { openFileFromToolbar() }) {
|
||
Image(systemName: "folder")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.help("Open File… (Cmd+O)")
|
||
|
||
Button(action: { viewModel.addNewTab() }) {
|
||
Image(systemName: "plus.square.on.square")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.help("New Tab (Cmd+T)")
|
||
|
||
#if os(macOS)
|
||
Button(action: {
|
||
openWindow(id: "blank-window")
|
||
}) {
|
||
Image(systemName: "macwindow.badge.plus")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.help("New Window (Cmd+N)")
|
||
#endif
|
||
|
||
Button(action: {
|
||
saveCurrentTabFromToolbar()
|
||
}) {
|
||
Image(systemName: "square.and.arrow.down")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.disabled(viewModel.selectedTab == nil)
|
||
.help("Save File (Cmd+S)")
|
||
|
||
Button(action: {
|
||
toggleSidebarFromToolbar()
|
||
}) {
|
||
Image(systemName: "sidebar.left")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
.symbolVariant(viewModel.showSidebar ? .fill : .none)
|
||
}
|
||
.help("Toggle Sidebar (Cmd+Opt+S)")
|
||
|
||
Button(action: {
|
||
showProjectStructureSidebar.toggle()
|
||
}) {
|
||
Image(systemName: "sidebar.right")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
.symbolVariant(showProjectStructureSidebar ? .fill : .none)
|
||
}
|
||
.help("Toggle Project Structure Sidebar")
|
||
|
||
Button(action: {
|
||
showFindReplace = true
|
||
}) {
|
||
Image(systemName: "magnifyingglass")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.help("Find & Replace (Cmd+F)")
|
||
|
||
Button(action: {
|
||
toggleAutoCompletion()
|
||
}) {
|
||
Image(systemName: "bolt.horizontal.circle")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
.symbolVariant(isAutoCompletionEnabled ? .fill : .none)
|
||
}
|
||
.help(isAutoCompletionEnabled ? "Disable Code Completion" : "Enable Code Completion")
|
||
.accessibilityLabel("Code Completion")
|
||
|
||
Button(action: {
|
||
showBracketHelperBarMac.toggle()
|
||
}) {
|
||
Image(systemName: "chevron.left.chevron.right")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
.symbolVariant(showBracketHelperBarMac ? .fill : .none)
|
||
}
|
||
.help(showBracketHelperBarMac ? "Hide Bracket Helper Bar" : "Show Bracket Helper Bar")
|
||
.accessibilityLabel("Bracket Helper Bar")
|
||
|
||
Button(action: {
|
||
viewModel.isBrainDumpMode.toggle()
|
||
UserDefaults.standard.set(viewModel.isBrainDumpMode, forKey: "BrainDumpModeEnabled")
|
||
}) {
|
||
Image(systemName: viewModel.isBrainDumpMode ? "note.text" : "note.text")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
.symbolVariant(viewModel.isBrainDumpMode ? .fill : .none)
|
||
}
|
||
.help("Brain Dump Mode")
|
||
.accessibilityLabel("Brain Dump Mode")
|
||
|
||
Button(action: {
|
||
enableTranslucentWindow.toggle()
|
||
UserDefaults.standard.set(enableTranslucentWindow, forKey: "EnableTranslucentWindow")
|
||
NotificationCenter.default.post(name: .toggleTranslucencyRequested, object: enableTranslucentWindow)
|
||
}) {
|
||
Image(systemName: enableTranslucentWindow ? "rectangle.fill" : "rectangle")
|
||
.foregroundStyle(NeonUIStyle.accentBlue)
|
||
}
|
||
.help("Toggle Translucent Window Background")
|
||
.accessibilityLabel("Translucent Window Background")
|
||
|
||
}
|
||
#endif
|
||
}
|
||
}
|