From adc28fcf2fdff27115a9ed3e8c12c6d83a56e77f Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 24 Dec 2025 23:56:08 +0530 Subject: [PATCH] Fix Scroll Effect on the notch notes and clipboard --- DynamicIsland/ContentView.swift | 6 +- DynamicIsland/DynamicIslandApp.swift | 10 ++- .../DynamicIslandViewCoordinator.swift | 1 + .../components/Notch/NotchNotesView.swift | 69 +++++++++++++++++++ DynamicIsland/enums/generic.swift | 18 +++++ .../models/DynamicIslandViewModel.swift | 56 +++++++++++++-- 6 files changed, 152 insertions(+), 8 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 09f52ee..f7e268c 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -50,7 +50,9 @@ struct ContentView: View { } if coordinator.currentView == .notes || coordinator.currentView == .clipboard { - return CGSize(width: baseSize.width, height: 230) // Consistent height for split views + let preferredHeight = coordinator.notesLayoutState.preferredHeight + let resolvedHeight = max(baseSize.height, preferredHeight) + return CGSize(width: baseSize.width, height: resolvedHeight) } guard coordinator.currentView == .stats else { @@ -905,7 +907,7 @@ struct ContentView: View { } private func handleUpGesture(translation: CGFloat, phase: NSEvent.Phase) { - if vm.notchState == .open && !vm.isHoveringCalendar { + if vm.notchState == .open && !vm.isHoveringCalendar && !vm.isScrollGestureActive { withAnimation(.smooth) { gestureProgress = (translation / Defaults[.gestureSensitivity]) * -20 } diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index cfec521..5a0ed51 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -284,7 +284,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { if coordinator.currentView == .timer { baseSize.height = 250 // Extra space for timer presets } else if coordinator.currentView == .notes || coordinator.currentView == .clipboard { - baseSize.height = 230 + let preferredHeight = coordinator.notesLayoutState.preferredHeight + baseSize.height = max(baseSize.height, preferredHeight) } let adjustedContentSize = statsAdjustedNotchSize( @@ -378,6 +379,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { coordinator.$currentView.sink { [weak self] newView in self?.updateWindowSizeIfNeeded() }.store(in: &cancellables) + + coordinator.$notesLayoutState + .removeDuplicates() + .sink { [weak self] _ in + self?.updateWindowSizeIfNeeded() + } + .store(in: &cancellables) // Observe stats settings changes - use debounced updates Defaults.publisher(.enableStatsFeature, options: []).sink { [weak self] _ in diff --git a/DynamicIsland/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift index 462ef7b..2bc22e6 100644 --- a/DynamicIsland/DynamicIslandViewCoordinator.swift +++ b/DynamicIsland/DynamicIslandViewCoordinator.swift @@ -63,6 +63,7 @@ class DynamicIslandViewCoordinator: ObservableObject { private var statsSecondRowWorkItem: DispatchWorkItem? private let statsSecondRowRevealDelay: TimeInterval = 0.5 private let statsSecondRowAnimationDuration: TimeInterval = 0.3 + @Published var notesLayoutState: NotesLayoutState = .list @AppStorage("firstLaunch") var firstLaunch: Bool = true diff --git a/DynamicIsland/components/Notch/NotchNotesView.swift b/DynamicIsland/components/Notch/NotchNotesView.swift index a26c9c8..b823c7a 100644 --- a/DynamicIsland/components/Notch/NotchNotesView.swift +++ b/DynamicIsland/components/Notch/NotchNotesView.swift @@ -9,6 +9,8 @@ import SwiftUI import Defaults struct NotchNotesView: View { + @EnvironmentObject var vm: DynamicIslandViewModel + @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @FocusState private var isFocused: Bool @Default(.savedNotes) var savedNotes @Default(.clipboardDisplayMode) var clipboardDisplayMode @@ -85,6 +87,27 @@ struct NotchNotesView: View { } } .frame(maxHeight: .infinity) + .onAppear { + updateLayoutState() + } + .onDisappear { + coordinator.notesLayoutState = .list + } + .onChange(of: isEditingNewNote) { _, _ in + updateLayoutState() + } + .onChange(of: selectedNoteId) { _, _ in + updateLayoutState() + } + .onChange(of: enableClipboardManager) { _, _ in + updateLayoutState() + } + .onChange(of: clipboardDisplayMode) { _, _ in + updateLayoutState() + } + .onChange(of: enableNotes) { _, _ in + updateLayoutState() + } } // MARK: - Actions @@ -300,15 +323,34 @@ struct NotchNotesView: View { editorTitle = "" editorContent = "" } + updateLayoutState() + } + + private func updateLayoutState() { + let newState: NotesLayoutState + if enableNotes && (isEditingNewNote || selectedNoteId != nil) { + newState = .editor + } else if showSplitView { + newState = .split + } else { + newState = .list + } + + if coordinator.notesLayoutState != newState { + coordinator.notesLayoutState = newState + } } } // MARK: - Subviews struct NotchClipboardList: View { + @EnvironmentObject var vm: DynamicIslandViewModel @ObservedObject var clipboardManager = ClipboardManager.shared @State private var hoveredItemId: UUID? @State private var justCopiedId: UUID? + @State private var suppressionToken = UUID() + @State private var isSuppressing = false var body: some View { VStack(spacing: 0) { @@ -384,6 +426,18 @@ struct NotchClipboardList: View { } } .frame(maxHeight: .infinity) + .onHover { hovering in + updateSuppression(for: hovering) + } + .onDisappear { + updateSuppression(for: false) + } + } + + private func updateSuppression(for hovering: Bool) { + guard hovering != isSuppressing else { return } + isSuppressing = hovering + vm.setScrollGestureSuppression(hovering, token: suppressionToken) } } @@ -440,6 +494,7 @@ struct NotchClipboardItemRow: View { struct NoteListView: View { + @EnvironmentObject var vm: DynamicIslandViewModel let notes: [NoteItem] let onSelect: (NoteItem) -> Void let onCreate: () -> Void @@ -456,6 +511,8 @@ struct NoteListView: View { @State private var searchText = "" @State private var selectedColorFilter: Int? = nil @State private var isSearchExpanded = false + @State private var suppressionToken = UUID() + @State private var isSuppressing = false var sortedNotes: [NoteItem] { var filtered = searchText.isEmpty ? notes : notes.filter { @@ -647,6 +704,18 @@ struct NoteListView: View { } } .frame(maxHeight: .infinity) + .onHover { hovering in + updateSuppression(for: hovering) + } + .onDisappear { + updateSuppression(for: false) + } + } + + private func updateSuppression(for hovering: Bool) { + guard hovering != isSuppressing else { return } + isSuppressing = hovering + vm.setScrollGestureSuppression(hovering, token: suppressionToken) } } diff --git a/DynamicIsland/enums/generic.swift b/DynamicIsland/enums/generic.swift index 14080ea..9656bc1 100644 --- a/DynamicIsland/enums/generic.swift +++ b/DynamicIsland/enums/generic.swift @@ -7,6 +7,7 @@ import Foundation import Defaults +import CoreGraphics public enum Style { case notch @@ -34,6 +35,23 @@ public enum NotchViews { case clipboard } +enum NotesLayoutState: Equatable { + case list + case split + case editor + + var preferredHeight: CGFloat { + switch self { + case .list: + return 240 + case .split: + return 260 + case .editor: + return 320 + } + } +} + enum SettingsEnum { case general case about diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift index 5b64b7e..6d7f5cc 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -36,6 +36,26 @@ class DynamicIslandViewModel: NSObject, ObservableObject { @Published var isMediaOutputPopoverActive: Bool = false @Published var isTimerPopoverActive: Bool = false @Published var shouldRecheckHover: Bool = false + @Published var isScrollGestureActive: Bool = false + private var scrollGestureSuppressionTokens: Set = [] + + func setScrollGestureSuppression(_ active: Bool, token: UUID) { + if active { + let inserted = scrollGestureSuppressionTokens.insert(token).inserted + if inserted { + isScrollGestureActive = true + } + } else { + if scrollGestureSuppressionTokens.remove(token) != nil { + isScrollGestureActive = !scrollGestureSuppressionTokens.isEmpty + } + } + } + + private func resetScrollGestureSuppression() { + scrollGestureSuppressionTokens.removeAll() + isScrollGestureActive = false + } let webcamManager = WebcamManager.shared @Published var isCameraExpanded: Bool = false @@ -150,6 +170,27 @@ class DynamicIslandViewModel: NSObject, ObservableObject { } .store(in: &cancellables) + coordinator.$notesLayoutState + .removeDuplicates() + .receive(on: RunLoop.main) + .sink { [weak self] _ in + guard let self else { return } + guard self.notchState == .open else { return } + let updatedTarget = self.calculateDynamicNotchSize() + guard self.notchSize != updatedTarget else { return } + withAnimation(.easeInOut(duration: 0.25)) { + self.notchSize = updatedTarget + } + if let delegate = AppDelegate.shared { + delegate.ensureWindowSize( + addShadowPadding(to: updatedTarget, isMinimalistic: Defaults[.enableMinimalisticUI]), + animated: true, + force: false + ) + } + } + .store(in: &cancellables) + Defaults.publisher(.openNotchWidth, options: []) .map { $0.newValue } .removeDuplicates() @@ -267,15 +308,18 @@ class DynamicIslandViewModel: NSObject, ObservableObject { } private func calculateDynamicNotchSize() -> CGSize { - // Use minimalistic size if minimalistic UI is enabled let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize - if DynamicIslandViewCoordinator.shared.currentView == .notes { - return CGSize(width: baseSize.width, height: 360) + var adjustedSize = baseSize + + if coordinator.currentView == .notes || coordinator.currentView == .clipboard { + let preferred = coordinator.notesLayoutState.preferredHeight + adjustedSize.height = max(adjustedSize.height, preferred) + return adjustedSize } return statsAdjustedNotchSize( - from: baseSize, - isStatsTabActive: DynamicIslandViewCoordinator.shared.currentView == .stats, + from: adjustedSize, + isStatsTabActive: coordinator.currentView == .stats, secondRowProgress: coordinator.statsSecondRowExpansion ) } @@ -285,6 +329,7 @@ class DynamicIslandViewModel: NSObject, ObservableObject { notchSize = targetSize closedNotchSize = targetSize notchState = .closed + resetScrollGestureSuppression() // Set the current view to shelf if it contains files and the user enables openShelfByDefault // Otherwise, if the user has not enabled openLastShelfByDefault, set the view to home @@ -301,6 +346,7 @@ class DynamicIslandViewModel: NSObject, ObservableObject { notchSize = targetSize closedNotchSize = targetSize notchState = .closed + resetScrollGestureSuppression() } }