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
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ extension AppModel {
let removed = activeNotifications.removeFirst()
cancelNotificationDismissal(for: removed.id)
}
notificationMessage = message
if areNotificationsHovered {
notificationRemainingDurations[notification.id] = WorkbenchNotificationTiming.displayDuration
} else {
Expand Down Expand Up @@ -56,7 +55,6 @@ extension AppModel {
guard activeNotifications.contains(where: { $0.id == id }) else { return }
activeNotifications.removeAll { $0.id == id }
cancelNotificationDismissal(for: id)
notificationMessage = activeNotifications.last?.message
}

func markAllNotificationsRead() {
Expand All @@ -75,7 +73,6 @@ extension AppModel {
notificationDismissalDeadlines.removeAll()
notificationRemainingDurations.removeAll()
areNotificationsHovered = false
notificationMessage = nil
}

private func scheduleNotificationDismissal(
Expand Down
1 change: 0 additions & 1 deletion macos/Sources/Lithe/Models/AppModel/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ final class AppModel: ObservableObject, Identifiable {
var isPerformingProjectItemOperation: Bool {
workspaceFeature.isPerformingProjectItemOperation
}
@Published var notificationMessage: String?
@Published var activeNotifications: [WorkbenchNotification] = []
@Published var notifications: [WorkbenchNotification] = []
var notificationDismissalTasks: [UUID: Task<Void, Never>] = [:]
Expand Down
2 changes: 1 addition & 1 deletion macos/Sources/Lithe/Theme/LitheTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ extension View {
cornerRadius: CGFloat = LitheTheme.Metrics.cornerRadius,
activeBackground: Color = LitheTheme.selection,
hoverBackground: Color = LitheTheme.hoverBackground,
animation: Animation? = .easeOut(duration: 0.12)
animation: Animation? = nil
) -> some View {
modifier(
LitheRowHoverModifier(
Expand Down
1 change: 0 additions & 1 deletion macos/Sources/Lithe/Views/App/PluginManagementView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ struct PluginManagementView: View {
hoveredPluginID = nil
}
}
.animation(.easeOut(duration: 0.12), value: isHovered)
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isButton)
.lithePointer()
Expand Down
23 changes: 21 additions & 2 deletions macos/Sources/Lithe/Views/Editor/CodeEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ struct CodeEditorView: NSViewRepresentable {
context.coordinator.requestInitialFocusIfNeeded()

if let codeTextView = textView as? CodeTextView {
codeTextView.refreshLanguageHoverAppearance()
let debugFeature = model.genericDebugFeatureIfActive
codeTextView.isRunToCursorEnabled = debugFeature?.state == .paused
&& debugFeature?.capabilities.supportsGotoTargetsRequest == true
Expand Down Expand Up @@ -1868,6 +1869,8 @@ final class CodeTextView: NSTextView, NSLayoutManagerDelegate {
private var lastCaretBackgroundRanges: [NSRange] = []
private var completionItemsByID: [String: LanguageServerCompletionItem] = [:]
private var languageHoverPopover: NSPopover?
private weak var languageHoverTextView: NSTextView?
private weak var languageHoverScrollView: NSScrollView?
private var debugHoverPopover: NSPopover?
private var debugHoverWorkItem: DispatchWorkItem?
private var pendingDebugHover: (expression: String, range: NSRange)?
Expand Down Expand Up @@ -1896,6 +1899,11 @@ final class CodeTextView: NSTextView, NSLayoutManagerDelegate {
private var caretVisible = true
private var caretPresentationGeneration = 0

override func viewDidChangeEffectiveAppearance() {
super.viewDidChangeEffectiveAppearance()
refreshLanguageHoverAppearance()
}

override func setFrameSize(_ newSize: NSSize) {
let previousWidth = frame.width
super.setFrameSize(newSize)
Expand Down Expand Up @@ -3583,14 +3591,15 @@ final class CodeTextView: NSTextView, NSLayoutManagerDelegate {
textView.isEditable = false
textView.isSelectable = true
textView.drawsBackground = false
textView.textColor = NSColor(white: 0.88, alpha: 1)
textView.font = .monospacedSystemFont(ofSize: 12, weight: .regular)
textView.textContainerInset = NSSize(width: 10, height: 9)
let scrollView = NSScrollView(frame: textView.frame)
scrollView.documentView = textView
scrollView.hasVerticalScroller = true
scrollView.drawsBackground = true
scrollView.backgroundColor = NSColor(red: 0.105, green: 0.11, blue: 0.12, alpha: 1)
languageHoverTextView = textView
languageHoverScrollView = scrollView
refreshLanguageHoverAppearance()
let controller = NSViewController()
controller.view = scrollView
controller.preferredContentSize = NSSize(width: 480, height: 220)
Expand All @@ -3602,6 +3611,16 @@ final class CodeTextView: NSTextView, NSLayoutManagerDelegate {
languageHoverPopover = popover
}

fileprivate func refreshLanguageHoverAppearance() {
guard let textView = languageHoverTextView,
let scrollView = languageHoverScrollView else { return }
let isDark = effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
textView.textColor = LitheTheme.nsColor(.primaryText, isDark: isDark)
scrollView.backgroundColor = LitheTheme.nsColor(.editor, isDark: isDark)
textView.needsDisplay = true
scrollView.needsDisplay = true
}

@objc private func insertLanguageCompletion(_ sender: NSMenuItem) {
guard let id = sender.representedObject as? String,
let item = completionItemsByID[id] else { return }
Expand Down
3 changes: 1 addition & 2 deletions macos/Sources/Lithe/Views/Editor/EditorAreaView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ struct EditorAreaView: View {
.background(
isTerminalTabBarDropTargeted
? LitheTheme.accent.opacity(0.08)
: (model.workbenchBackgroundFeature.hasImage ? Color.clear : LitheTheme.sidebar)
: (model.workbenchBackgroundFeature.hasImage ? Color.clear : LitheTheme.editor)
)
.onDrop(
of: [TerminalTabDragPayload.type],
Expand Down Expand Up @@ -1018,7 +1018,6 @@ struct EditorAreaView: View {
}
.frame(width: 104, height: 26)
.padding(.horizontal, 7)
.animation(.easeOut(duration: 0.12), value: hoveredMarkdownMode)
}

private var selectedMarkdownMode: MarkdownViewMode {
Expand Down
141 changes: 61 additions & 80 deletions macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import LitheGitModule

enum WorkbenchLayoutMetrics {
static let rightActivityBarWidth: CGFloat = 40
static let rightActivityBarDividerWidth: CGFloat = 1
static let workspaceTrailingInset = rightActivityBarWidth
}

Expand All @@ -19,7 +18,7 @@ private enum ActivityBarMetrics {
}

private enum WorkbenchWorkspaceMetrics {
static let paneInset: CGFloat = 6
static let paneInset: CGFloat = 0
static let paneSpacing: CGFloat = 6
static let paneCornerRadius: CGFloat = 10
}
Expand Down Expand Up @@ -94,18 +93,13 @@ struct WorkbenchView: View {
let _ = LitheSignpost.bodyEvaluated("WorkbenchView")
VStack(spacing: 0) {
topBar
Rectangle().fill(LitheTheme.divider).frame(height: 1)

if projectSessions.openProjects.count > 1 {
projectTabBar
Rectangle().fill(LitheTheme.divider).frame(height: 1)
}

HStack(spacing: 0) {
activityBar
Rectangle()
.fill(LitheTheme.divider)
.frame(width: 1)
workspaceArea
.padding(.trailing, WorkbenchLayoutMetrics.workspaceTrailingInset)
}
Expand All @@ -114,7 +108,6 @@ struct WorkbenchView: View {
rightHoverRegion
}

Rectangle().fill(LitheTheme.divider).frame(height: 1)
statusBar
}
.background {
Expand Down Expand Up @@ -330,17 +323,44 @@ struct WorkbenchView: View {
}
}
}
.overlay(alignment: .bottom) {
if let message = model.notificationMessage {
Text(LocalizedStringKey(message))
.font(.system(size: 12, weight: .medium))
.foregroundStyle(LitheTheme.primaryText)
.padding(.horizontal, 14)
.frame(height: 34)
.background(LitheTheme.raised)
.clipShape(RoundedRectangle(cornerRadius: 7))
.shadow(color: .black.opacity(0.35), radius: 12, y: 4)
.padding(.bottom, 38)
.overlay(alignment: .bottomTrailing) {
if !model.activeNotifications.isEmpty {
VStack(alignment: .trailing, spacing: 8) {
ForEach(model.activeNotifications) { notification in
HStack(alignment: .center, spacing: 10) {
Image(systemName: "info.circle.fill")
.font(.system(size: 14))
.foregroundStyle(LitheTheme.accent)
Text(LocalizedStringKey(notification.message))
.font(.system(size: 12, weight: .medium))
.foregroundStyle(LitheTheme.primaryText)
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 4)
Button {
model.dismissNotification(notification.id)
} label: {
Image(systemName: "xmark")
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(LitheTheme.tertiaryText)
}
.buttonStyle(.plain)
.frame(width: 16, height: 16)
.contentShape(Rectangle())
.litheRowHover(cornerRadius: LitheTheme.Metrics.cornerRadius, animation: nil)
.accessibilityLabel("Dismiss notification")
}
.padding(.leading, 12)
.padding(.trailing, 6)
.padding(.vertical, 10)
.frame(minWidth: 280, maxWidth: 360, alignment: .topLeading)
.background(LitheTheme.notificationBackground)
.clipShape(RoundedRectangle(cornerRadius: 7))
.contentShape(RoundedRectangle(cornerRadius: 7))
}
}
.onHover { model.setNotificationStackHovered($0) }
.padding(.trailing, WorkbenchLayoutMetrics.rightActivityBarWidth + 12)
.padding(.bottom, 38)
}
}
.overlay {
Expand Down Expand Up @@ -397,46 +417,6 @@ struct WorkbenchView: View {
}
}
}
.overlay(alignment: .bottomTrailing) {
if !model.activeNotifications.isEmpty {
VStack(alignment: .trailing, spacing: 8) {
ForEach(model.activeNotifications) { notification in
HStack(alignment: .center, spacing: 10) {
Image(systemName: "info.circle.fill")
.font(.system(size: 14))
.foregroundStyle(LitheTheme.accent)
Text(LocalizedStringKey(notification.message))
.font(.system(size: 12, weight: .medium))
.foregroundStyle(LitheTheme.primaryText)
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 4)
Button {
model.dismissNotification(notification.id)
} label: {
Image(systemName: "xmark")
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(LitheTheme.tertiaryText)
}
.buttonStyle(.plain)
.frame(width: 16, height: 16)
.contentShape(Rectangle())
.litheRowHover(cornerRadius: LitheTheme.Metrics.cornerRadius, animation: nil)
.accessibilityLabel("Dismiss notification")
}
.padding(.leading, 12)
.padding(.trailing, 6)
.padding(.vertical, 10)
.frame(minWidth: 280, maxWidth: 360, alignment: .topLeading)
.background(LitheTheme.notificationBackground)
.clipShape(RoundedRectangle(cornerRadius: 7))
.contentShape(RoundedRectangle(cornerRadius: 7))
}
}
.onHover { model.setNotificationStackHovered($0) }
.padding(.trailing, WorkbenchLayoutMetrics.rightActivityBarWidth + 12)
.padding(.bottom, 38)
}
}
.frame(height: LitheTheme.Metrics.tabHeight + 4)
.background(model.workbenchBackgroundFeature.hasImage ? Color.clear : LitheTheme.toolHeader)
}
Expand Down Expand Up @@ -514,7 +494,6 @@ struct WorkbenchView: View {
.onHover { hovering in
hoveredProjectTabID = hovering ? projectModel.id : nil
}
.animation(.easeOut(duration: 0.12), value: isHovered)
.animation(.easeOut(duration: 0.12), value: isActive)
}

Expand Down Expand Up @@ -554,11 +533,6 @@ struct WorkbenchView: View {
value: .bounds
) { $0 }

Rectangle()
.fill(LitheTheme.divider)
.frame(width: 1, height: 20)
.padding(.horizontal, 5)

Button {
updateSwitcherPresentation(
project: false,
Expand Down Expand Up @@ -600,11 +574,13 @@ struct WorkbenchView: View {

Spacer(minLength: 22)

runConfigurationPicker
runLaunchButton
debugLaunchButton
if hasActiveExecution {
stopExecutionButton
HStack(spacing: 8) {
runConfigurationPicker
runLaunchButton
debugLaunchButton
if hasActiveExecution {
stopExecutionButton
}
}

backgroundPickerButton
Expand Down Expand Up @@ -906,20 +882,28 @@ struct WorkbenchView: View {
}
}
} label: {
HStack(spacing: 5) {
HStack(spacing: 8) {
RunConfigurationIcon(
kind: model.runFeatureIfActive?.selectedConfiguration?.kind ?? .currentFile,
size: 14
)
Text(model.runFeatureIfActive?.selectedConfiguration?.name ?? "Current File")
.font(.system(size: 11.5, weight: .medium))
.lineLimit(1)
Image(systemName: "chevron.down")
.font(.system(size: 8, weight: .bold))
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
}
.foregroundStyle(LitheTheme.primaryText)
.padding(.horizontal, 8)
.frame(maxWidth: 190, minHeight: 30)
.litheRowHover(isActive: false, cornerRadius: 6, activeBackground: LitheTheme.subtleSelection)
.padding(.horizontal, 4)
.frame(minWidth: 160, maxWidth: 190, alignment: .leading)
.frame(height: 30)
.contentShape(Rectangle())
}
.menuStyle(.borderlessButton)
.fixedSize(horizontal: true, vertical: false)
.menuIndicator(.visible)
.frame(minWidth: 160, maxWidth: 190, alignment: .leading)
.frame(height: 30)
.litheRowHover(isActive: false, cornerRadius: 6, activeBackground: LitheTheme.subtleSelection)
.help("Select run configuration for Run or Debug")
.accessibilityLabel("Select run configuration for Run or Debug")
.accessibilityIdentifier("run-configuration-picker")
Expand Down Expand Up @@ -1180,9 +1164,6 @@ struct WorkbenchView: View {
}
}
}
Rectangle()
.fill(LitheTheme.divider)
.frame(width: 1)
pluginActivityBar
}
.fixedSize(horizontal: true, vertical: false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ struct JavaTestDebugLaunchServiceTests {

model.debugTest(providerID: "java", scope: .workspace)

#expect(model.notificationMessage == "Select a Java test file or test case to debug")
#expect(model.activeNotifications.last?.message == "Select a Java test file or test case to debug")
#expect(model.javaTestWorkflowState.debugLaunchTask == nil)
#expect(model.javaTestWorkflowState.debugLaunchOperationID == nil)
}
Expand Down
2 changes: 1 addition & 1 deletion macos/Tests/LitheTests/WorkbenchNotificationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ struct WorkbenchNotificationTests {

model.clearNotifications()
#expect(model.notifications.isEmpty)
#expect(model.notificationMessage == nil)
#expect(model.activeNotifications.isEmpty)
}

@Test
Expand Down
Loading