From 93a2d8490108bf157aaebe8f2442a95a5d00cc2b Mon Sep 17 00:00:00 2001 From: arkleselect Date: Wed, 2 Sep 2026 17:59:40 +0800 Subject: [PATCH 1/2] fix(macos): resolve frontend style priority conflicts --- .../AppModel/AppModel+Notifications.swift | 3 - .../Lithe/Models/AppModel/AppModel.swift | 1 - macos/Sources/Lithe/Theme/LitheTheme.swift | 2 +- .../Views/App/PluginManagementView.swift | 1 - .../Lithe/Views/Editor/CodeEditorView.swift | 6 +- .../Lithe/Views/Editor/EditorAreaView.swift | 3 +- .../Lithe/Views/Workbench/WorkbenchView.swift | 138 ++++++++---------- .../JavaTestDebugLaunchServiceTests.swift | 2 +- .../WorkbenchNotificationTests.swift | 2 +- 9 files changed, 68 insertions(+), 90 deletions(-) diff --git a/macos/Sources/Lithe/Models/AppModel/AppModel+Notifications.swift b/macos/Sources/Lithe/Models/AppModel/AppModel+Notifications.swift index 8fc874b20..0f2e5f6c0 100644 --- a/macos/Sources/Lithe/Models/AppModel/AppModel+Notifications.swift +++ b/macos/Sources/Lithe/Models/AppModel/AppModel+Notifications.swift @@ -18,7 +18,6 @@ extension AppModel { let removed = activeNotifications.removeFirst() cancelNotificationDismissal(for: removed.id) } - notificationMessage = message if areNotificationsHovered { notificationRemainingDurations[notification.id] = WorkbenchNotificationTiming.displayDuration } else { @@ -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() { @@ -75,7 +73,6 @@ extension AppModel { notificationDismissalDeadlines.removeAll() notificationRemainingDurations.removeAll() areNotificationsHovered = false - notificationMessage = nil } private func scheduleNotificationDismissal( diff --git a/macos/Sources/Lithe/Models/AppModel/AppModel.swift b/macos/Sources/Lithe/Models/AppModel/AppModel.swift index 3514c3a68..26d6d7921 100644 --- a/macos/Sources/Lithe/Models/AppModel/AppModel.swift +++ b/macos/Sources/Lithe/Models/AppModel/AppModel.swift @@ -97,7 +97,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] = [:] diff --git a/macos/Sources/Lithe/Theme/LitheTheme.swift b/macos/Sources/Lithe/Theme/LitheTheme.swift index 274c15feb..3bcbabea7 100644 --- a/macos/Sources/Lithe/Theme/LitheTheme.swift +++ b/macos/Sources/Lithe/Theme/LitheTheme.swift @@ -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( diff --git a/macos/Sources/Lithe/Views/App/PluginManagementView.swift b/macos/Sources/Lithe/Views/App/PluginManagementView.swift index 8f55bf909..985864b99 100644 --- a/macos/Sources/Lithe/Views/App/PluginManagementView.swift +++ b/macos/Sources/Lithe/Views/App/PluginManagementView.swift @@ -214,7 +214,6 @@ struct PluginManagementView: View { hoveredPluginID = nil } } - .animation(.easeOut(duration: 0.12), value: isHovered) .accessibilityElement(children: .combine) .accessibilityAddTraits(.isButton) .lithePointer() diff --git a/macos/Sources/Lithe/Views/Editor/CodeEditorView.swift b/macos/Sources/Lithe/Views/Editor/CodeEditorView.swift index 2d5d84ef6..9eed35da7 100644 --- a/macos/Sources/Lithe/Views/Editor/CodeEditorView.swift +++ b/macos/Sources/Lithe/Views/Editor/CodeEditorView.swift @@ -3590,7 +3590,11 @@ final class CodeTextView: NSTextView, NSLayoutManagerDelegate { scrollView.documentView = textView scrollView.hasVerticalScroller = true scrollView.drawsBackground = true - scrollView.backgroundColor = NSColor(red: 0.105, green: 0.11, blue: 0.12, alpha: 1) + let isDark = effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua + scrollView.backgroundColor = LitheTheme.nsColor( + .editor, + isDark: isDark + ) let controller = NSViewController() controller.view = scrollView controller.preferredContentSize = NSSize(width: 480, height: 220) diff --git a/macos/Sources/Lithe/Views/Editor/EditorAreaView.swift b/macos/Sources/Lithe/Views/Editor/EditorAreaView.swift index ed077f5d5..5936a1061 100644 --- a/macos/Sources/Lithe/Views/Editor/EditorAreaView.swift +++ b/macos/Sources/Lithe/Views/Editor/EditorAreaView.swift @@ -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], @@ -939,7 +939,6 @@ struct EditorAreaView: View { } .frame(width: 104, height: 26) .padding(.horizontal, 7) - .animation(.easeOut(duration: 0.12), value: hoveredMarkdownMode) } private var selectedMarkdownMode: MarkdownViewMode { diff --git a/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift b/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift index 05e88ef64..e2b88ef02 100644 --- a/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift +++ b/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift @@ -4,7 +4,6 @@ import LitheGitModule enum WorkbenchLayoutMetrics { static let rightActivityBarWidth: CGFloat = 40 - static let rightActivityBarDividerWidth: CGFloat = 1 static let workspaceTrailingInset = rightActivityBarWidth } @@ -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 } @@ -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) } @@ -114,7 +108,6 @@ struct WorkbenchView: View { rightHoverRegion } - Rectangle().fill(LitheTheme.divider).frame(height: 1) statusBar } .background { @@ -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 { @@ -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) } @@ -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) } @@ -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, @@ -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 @@ -906,20 +882,27 @@ 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)) } .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, alignment: .leading) + .frame(height: 30) + .contentShape(Rectangle()) } .menuStyle(.borderlessButton) + .menuIndicator(.visible) .fixedSize(horizontal: true, vertical: false) + .frame(minWidth: 160, 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") @@ -1180,9 +1163,6 @@ struct WorkbenchView: View { } } } - Rectangle() - .fill(LitheTheme.divider) - .frame(width: 1) pluginActivityBar } .fixedSize(horizontal: true, vertical: false) diff --git a/macos/Tests/LitheTests/JavaTestDebugLaunchServiceTests.swift b/macos/Tests/LitheTests/JavaTestDebugLaunchServiceTests.swift index 6cda94cb5..0c64240f5 100644 --- a/macos/Tests/LitheTests/JavaTestDebugLaunchServiceTests.swift +++ b/macos/Tests/LitheTests/JavaTestDebugLaunchServiceTests.swift @@ -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) } diff --git a/macos/Tests/LitheTests/WorkbenchNotificationTests.swift b/macos/Tests/LitheTests/WorkbenchNotificationTests.swift index c50427b48..2e2869c6a 100644 --- a/macos/Tests/LitheTests/WorkbenchNotificationTests.swift +++ b/macos/Tests/LitheTests/WorkbenchNotificationTests.swift @@ -39,7 +39,7 @@ struct WorkbenchNotificationTests { model.clearNotifications() #expect(model.notifications.isEmpty) - #expect(model.notificationMessage == nil) + #expect(model.activeNotifications.isEmpty) } @Test From de4208d59b6576720da28a9050653c387c0aef6d Mon Sep 17 00:00:00 2001 From: arkleselect Date: Thu, 3 Sep 2026 16:55:55 +0800 Subject: [PATCH 2/2] update --- .../Lithe/Views/Editor/CodeEditorView.swift | 27 ++++++++++++++----- .../Lithe/Views/Workbench/WorkbenchView.swift | 7 ++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Lithe/Views/Editor/CodeEditorView.swift b/macos/Sources/Lithe/Views/Editor/CodeEditorView.swift index 9eed35da7..b7c6281fc 100644 --- a/macos/Sources/Lithe/Views/Editor/CodeEditorView.swift +++ b/macos/Sources/Lithe/Views/Editor/CodeEditorView.swift @@ -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 @@ -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)? @@ -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) @@ -3583,18 +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 - let isDark = effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua - scrollView.backgroundColor = LitheTheme.nsColor( - .editor, - isDark: isDark - ) + languageHoverTextView = textView + languageHoverScrollView = scrollView + refreshLanguageHoverAppearance() let controller = NSViewController() controller.view = scrollView controller.preferredContentSize = NSSize(width: 480, height: 220) @@ -3606,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 } diff --git a/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift b/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift index e2b88ef02..25475c6b6 100644 --- a/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift +++ b/macos/Sources/Lithe/Views/Workbench/WorkbenchView.swift @@ -890,17 +890,18 @@ struct WorkbenchView: View { Text(model.runFeatureIfActive?.selectedConfiguration?.name ?? "Current File") .font(.system(size: 11.5, weight: .medium)) .lineLimit(1) + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) } .foregroundStyle(LitheTheme.primaryText) .padding(.horizontal, 4) - .frame(minWidth: 160, alignment: .leading) + .frame(minWidth: 160, maxWidth: 190, alignment: .leading) .frame(height: 30) .contentShape(Rectangle()) } .menuStyle(.borderlessButton) .menuIndicator(.visible) - .fixedSize(horizontal: true, vertical: false) - .frame(minWidth: 160, alignment: .leading) + .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")