Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 95 additions & 83 deletions Sources/Loci/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,100 +97,112 @@ struct LociShell: View {
@State private var isShowingCommandPalette = false

var body: some View {
ZStack {
LociColor.surface

HStack(spacing: 0) {
LociSidebar(store: store)
.frame(width: 164)

MainReferencePane(store: store, namespace: namespace)
workspace
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.animation(AppMotion.instant, value: store.mode)
.animation(AppMotion.toast, value: store.selectedItemIDs)
.animation(AppMotion.hero, value: store.focusedItemID)
.focusable()
.onKeyPress(.leftArrow) {
guard store.focusedItemID != nil else { return .ignored }
withAnimation(AppMotion.smooth) {
_ = store.focusAdjacentVisibleItem(offset: -1)
}
return .handled
}

if showsReferenceChrome {
LociTitle(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding(.top, 22)
.onKeyPress(.rightArrow) {
guard store.focusedItemID != nil else { return .ignored }
withAnimation(AppMotion.smooth) {
_ = store.focusAdjacentVisibleItem(offset: 1)
}
return .handled
}

if showsReferenceChrome {
BottomModeBar(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.padding(.bottom, 20)
.onDeleteCommand {
store.sendSelectionToTrash(undoManager: undoManager)
}

if showsReferenceChrome {
UtilityCluster(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
.padding(.trailing, 18)
.padding(.bottom, 21)
.onExitCommand {
if store.focusedItemID != nil {
store.requestFocusDismissal()
} else {
store.selectedItemIDs.removeAll()
store.searchText = ""
}
}

if showsReferenceChrome && !store.selectedItemIDs.isEmpty && store.selectedFilter != .trash {
BatchActionBar(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.padding(.bottom, 60)
.transition(AppMotion.bottomToastTransition)
.onReceive(NotificationCenter.default.publisher(for: .lociShowCommandPalette)) { _ in
withAnimation(AppMotion.quick) {
isShowingCommandPalette = true
}
}

if showsReferenceChrome && store.mode == .grid {
ZoomSlider(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
.padding(.top, 22)
.padding(.trailing, 18)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.20) {
store.warmCommonReferenceFilters()
}
startDemoAutoplayIfNeeded()
}
}

if isShowingCommandPalette {
Color.black.opacity(0.16)
.ignoresSafeArea()
.onTapGesture { isShowingCommandPalette = false }
private var workspace: some View {
ZStack {
LociColor.surface

CommandPaletteView(store: store, isPresented: $isShowingCommandPalette)
.frame(width: 560)
.transition(.opacity.combined(with: .scale(scale: 0.98)))
.zIndex(30)
}
}
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.animation(AppMotion.instant, value: store.mode)
.animation(AppMotion.toast, value: store.selectedItemIDs)
.animation(AppMotion.hero, value: store.focusedItemID)
.focusable()
.onKeyPress(.leftArrow) {
guard store.focusedItemID != nil else { return .ignored }
withAnimation(AppMotion.smooth) {
_ = store.focusAdjacentVisibleItem(offset: -1)
}
return .handled
libraryWorkspace
referenceChrome
commandPaletteOverlay
}
.onKeyPress(.rightArrow) {
guard store.focusedItemID != nil else { return .ignored }
withAnimation(AppMotion.smooth) {
_ = store.focusAdjacentVisibleItem(offset: 1)
}
return .handled
}

private var libraryWorkspace: some View {
HStack(spacing: 0) {
LociSidebar(store: store)
.frame(width: 164)

MainReferencePane(store: store, namespace: namespace)
}
.onDeleteCommand {
store.sendSelectionToTrash(undoManager: undoManager)
}

@ViewBuilder
private var referenceChrome: some View {
if showsReferenceChrome {
LociTitle(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding(.top, 22)

BottomModeBar(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.padding(.bottom, 20)

UtilityCluster(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
.padding(.trailing, 18)
.padding(.bottom, 21)
}
.onExitCommand {
if store.focusedItemID != nil {
store.requestFocusDismissal()
} else {
store.selectedItemIDs.removeAll()
store.searchText = ""
}

if showsReferenceChrome && !store.selectedItemIDs.isEmpty && store.selectedFilter != .trash {
BatchActionBar(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.padding(.bottom, 60)
.transition(AppMotion.bottomToastTransition)
}
.onReceive(NotificationCenter.default.publisher(for: .lociShowCommandPalette)) { _ in
withAnimation(AppMotion.quick) {
isShowingCommandPalette = true
}

if showsReferenceChrome && store.mode == .grid {
ZoomSlider(store: store)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
.padding(.top, 22)
.padding(.trailing, 18)
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.20) {
store.warmCommonReferenceFilters()
}
startDemoAutoplayIfNeeded()
}

@ViewBuilder
private var commandPaletteOverlay: some View {
if isShowingCommandPalette {
Color.black.opacity(0.16)
.ignoresSafeArea()
.onTapGesture { isShowingCommandPalette = false }

CommandPaletteView(store: store, isPresented: $isShowingCommandPalette)
.frame(width: 560)
.transition(.opacity.combined(with: .scale(scale: 0.98)))
.zIndex(30)
}
}

Expand Down Expand Up @@ -2168,7 +2180,7 @@ struct ZoomSlider: View {
.animation(AppMotion.quick, value: isDragging)
.offset(x: knobOffset)
}
.frame(maxHeight: .infinity)
.frame(height: 20)
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0)
Expand All @@ -2184,7 +2196,7 @@ struct ZoomSlider: View {
}
)
}
.frame(width: 82)
.frame(width: 82, height: 20)

Button {
stepZoom(by: 0.08)
Expand Down
18 changes: 15 additions & 3 deletions Sources/Loci/LociApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ final class LociAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate,
private var libraryStore: LibraryStore?

func applicationDidFinishLaunching(_ notification: Notification) {
// Loci's palette is authored against light surfaces (see LociDesign);
// pin the appearance so the titlebar and toolbar can't fall out of
// step with the content when the system is in dark mode.
NSApp.appearance = NSAppearance(named: .aqua)
LegacyAppMigration.run()
LociEnvironment.reload()
configureAppIcon()
Expand Down Expand Up @@ -214,32 +218,40 @@ final class LociAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate,
) -> NSToolbarItem? {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.target = self
item.isBordered = true

switch itemIdentifier {
case .lociCommandPalette:
item.label = "Command Palette"
item.paletteLabel = "Command Palette"
item.toolTip = "Command Palette (⌘K)"
item.image = NSImage(systemSymbolName: "command", accessibilityDescription: "Command Palette")
item.image = Self.toolbarSymbol("command", description: "Command Palette")
item.action = #selector(showCommandPalette)
case .lociNotebookInspector:
item.label = "Ask Loci"
item.paletteLabel = "Show or Hide Ask Loci"
item.toolTip = "Show or Hide Ask Loci (⌥⌘I)"
item.image = NSImage(systemSymbolName: "sidebar.right", accessibilityDescription: "Show or Hide Ask Loci")
item.image = Self.toolbarSymbol("sidebar.right", description: "Show or Hide Ask Loci")
item.action = #selector(toggleNotebookInspector)
case .lociSettings:
item.label = "Settings"
item.paletteLabel = "Settings"
item.toolTip = "Settings (⌘,)"
item.image = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings")
item.image = Self.toolbarSymbol("gearshape", description: "Settings")
item.action = #selector(openSettings)
default:
return nil
}
return item
}

/// Symbols sized for the unified-compact toolbar; without an explicit
/// configuration the raw template images render noticeably oversized.
private static func toolbarSymbol(_ name: String, description: String) -> NSImage? {
NSImage(systemSymbolName: name, accessibilityDescription: description)?
.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 13, weight: .regular))
}

@objc private func openSettings() {
if let existing = settingsWindow {
existing.makeKeyAndOrderFront(nil)
Expand Down
4 changes: 3 additions & 1 deletion Sources/Loci/LociDesign.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ private struct LociScaledFontModifier: ViewModifier {
}

func body(content: Content) -> some View {
content.lociFont(size: scaledSize, weight: weight, design: design, relativeTo: .body)
// Do not call `lociFont` here: that would reapply this modifier and
// recurse through SwiftUI's view construction until launch crashes.
content.font(.system(size: scaledSize, weight: weight, design: design))
}
}

Expand Down