From d89bc29425e45ee6af2bd6a3d5d38450ce98243d Mon Sep 17 00:00:00 2001 From: lwouis Date: Tue, 10 Feb 2026 11:38:50 +0100 Subject: [PATCH] perf: do all mouse interactions globally instead of local tracking --- src/logic/events/CursorEvents.swift | 7 +- src/logic/events/MouseEvents.swift | 66 ++++++++++++++++++- .../TrafficLightButton.swift | 11 ---- src/ui/main-window/TileOverView.swift | 47 ++++++++++++- src/ui/main-window/TileTitleView.swift | 4 -- src/ui/main-window/TileView.swift | 38 +---------- 6 files changed, 116 insertions(+), 57 deletions(-) diff --git a/src/logic/events/CursorEvents.swift b/src/logic/events/CursorEvents.swift index 35cb3335..1d4601a3 100644 --- a/src/logic/events/CursorEvents.swift +++ b/src/logic/events/CursorEvents.swift @@ -45,6 +45,9 @@ class CursorEvents { private static let handleEvent: CGEventTapCallBack = { _, type, cgEvent, _ in if type == .mouseMoved { updateDeadzoneSituation(cgEvent) + if isAllowedToMouseHover { + App.app.thumbnailsPanel.tilesView.thumbnailOverView.updateHover() + } } else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) && shouldBeEnabled { CGEvent.tapEnable(tap: eventTap!, enable: true) } @@ -64,8 +67,6 @@ class CursorEvents { let deltaY = event.locationInWindow.y - deadZoneInitialPosition.y let d = hypot(deltaX, deltaY) isAllowedToMouseHover = d > 25 - if isAllowedToMouseHover { - App.app.thumbnailsPanel.tilesView.thumbnailOverView.updateHover() - } + } } diff --git a/src/logic/events/MouseEvents.swift b/src/logic/events/MouseEvents.swift index 9074960b..27e6f29e 100644 --- a/src/logic/events/MouseEvents.swift +++ b/src/logic/events/MouseEvents.swift @@ -4,6 +4,8 @@ import Carbon.HIToolbox.Events class MouseEvents { private static var eventTap: CFMachPort! private static var shouldBeEnabled: Bool! + private static var mouseDownTileView: TileView? + private static var mouseDownButton: TrafficLightButton? static func observe() { observe_() @@ -18,7 +20,7 @@ class MouseEvents { } private static func observe_() { - let eventMask = [CGEventType.leftMouseDown, CGEventType.leftMouseUp].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) }) + let eventMask = [CGEventType.leftMouseDown, CGEventType.leftMouseUp, CGEventType.otherMouseUp].reduce(CGEventMask(0), { $0 | (1 << $1.rawValue) }) // CGEvent.tapCreate returns nil if ensureAccessibilityCheckboxIsChecked() didn't pass eventTap = CGEvent.tapCreate( tap: .cgSessionEventTap, @@ -42,11 +44,41 @@ class MouseEvents { if !isPointerInsideUi() { return nil // focused app won't receive the event } + if let button = findButtonUnderPointer() { + mouseDownButton = button + button.isHighlighted = true + button.setNeedsDisplay() + return nil // swallow - we handle button clicks centrally + } + mouseDownTileView = findTileViewUnderPointer() + return nil // swallow - we handle tile clicks centrally } else if type == .leftMouseUp && cgEvent.getIntegerValueField(.mouseEventClickState) >= 1 { if !isPointerInsideUi() { App.app.hideUi() return nil // focused app won't receive the event } + if let button = mouseDownButton { + mouseDownButton = nil + button.isHighlighted = false + button.setNeedsDisplay() + if findButtonUnderPointer() === button { + button.onClick() + } + return nil + } + if let target = mouseDownTileView { + mouseDownTileView = nil + if isPointerOverTileView(target) { + target.mouseUpCallback() + } + return nil + } + } else if type == .otherMouseUp { + guard isPointerInsideUi() else { return Unmanaged.passUnretained(cgEvent) } + if cgEvent.getIntegerValueField(.mouseEventButtonNumber) == 2 { + handleMiddleClick() + return nil + } } else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) && shouldBeEnabled { CGEvent.tapEnable(tap: eventTap!, enable: true) } @@ -56,4 +88,36 @@ class MouseEvents { private static func isPointerInsideUi() -> Bool { return App.app.thumbnailsPanel.contentLayoutRect.contains(App.app.thumbnailsPanel.mouseLocationOutsideOfEventStream) } + + private static func findButtonUnderPointer() -> TrafficLightButton? { + let panel = App.app.thumbnailsPanel! + let overlay = panel.tilesView.thumbnailOverView + let windowLocation = panel.mouseLocationOutsideOfEventStream + let overlayLocation = overlay.convert(windowLocation, from: nil) + return overlay.findButton(overlayLocation) + } + + private static func findTileViewUnderPointer() -> TileView? { + let panel = App.app.thumbnailsPanel! + let overlay = panel.tilesView.thumbnailOverView + let windowLocation = panel.mouseLocationOutsideOfEventStream + let overlayLocation = overlay.convert(windowLocation, from: nil) + return overlay.findTarget(overlayLocation) + } + + private static func isPointerOverTileView(_ tileView: TileView) -> Bool { + let panel = App.app.thumbnailsPanel! + let windowLocation = panel.mouseLocationOutsideOfEventStream + let localPoint = tileView.convert(windowLocation, from: nil) + return tileView.bounds.contains(localPoint) + } + + private static func handleMiddleClick() { + guard let target = findTileViewUnderPointer(), let window = target.window_ else { return } + if window.isWindowlessApp { + window.application.quit() + } else { + window.close() + } + } } diff --git a/src/ui/generic-components/TrafficLightButton.swift b/src/ui/generic-components/TrafficLightButton.swift index df00ca2e..f5b8d06c 100644 --- a/src/ui/generic-components/TrafficLightButton.swift +++ b/src/ui/generic-components/TrafficLightButton.swift @@ -13,7 +13,6 @@ class TrafficLightButton: NSButton { self.type = type target = self action = #selector(onClick) - addTrackingArea(NSTrackingArea(rect: .zero, options: [.mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect], owner: self, userInfo: nil)) toolTip = tooltip appearance = NSAppearance(named: .aqua) } @@ -34,16 +33,6 @@ class TrafficLightButton: NSButton { } } - override func mouseEntered(with event: NSEvent) { - isMouseOver = true - setNeedsDisplay() - } - - override func mouseExited(with event: NSEvent) { - isMouseOver = false - setNeedsDisplay() - } - override func draw(_ dirtyRect: NSRect) { let (diskBackgroundColor, diskStrokeColor, symbolColor) = colors() let disk = drawDisk(diskBackgroundColor, diskStrokeColor) diff --git a/src/ui/main-window/TileOverView.swift b/src/ui/main-window/TileOverView.swift index df3483d1..9e89feaa 100644 --- a/src/ui/main-window/TileOverView.swift +++ b/src/ui/main-window/TileOverView.swift @@ -8,6 +8,7 @@ class TileOverView: FlippedView { var isShowingWindowControls = false weak var scrollView: ScrollView? var previousTarget: TileView? + private var previousHoveredButton: TrafficLightButton? var windowControlButtons: [TrafficLightButton] { [quitButton, closeButton, minimizeButton, maximizeButton] } @@ -37,6 +38,7 @@ class TileOverView: FlippedView { func updateHover() { guard let scrollView, !scrollView.isCurrentlyScrolling else { return } let location = convert(App.app.thumbnailsPanel.mouseLocationOutsideOfEventStream, from: nil) + updateButtonHover(location) let newTarget = findTarget(location) guard newTarget !== previousTarget else { return } caTransaction { @@ -44,6 +46,10 @@ class TileOverView: FlippedView { hideWindowControls() newTarget.mouseMoved() showWindowControls(for: newTarget) + let statusFrame = newTarget.convert(newTarget.statusIcons.frame, to: superview) + if statusFrame.contains(location) { + newTarget.statusIcons.ensureTooltipsInstalled() + } } else { resetHoveredWindow() } @@ -51,7 +57,42 @@ class TileOverView: FlippedView { } } - private func findTarget(_ location: NSPoint) -> TileView? { + /// Find the TrafficLightButton at the given point (in TileOverView's coordinate space) + func findButton(_ location: NSPoint) -> TrafficLightButton? { + guard isShowingWindowControls else { return nil } + for button in windowControlButtons where !button.isHidden { + let buttonPoint = button.convert(location, from: self) + if button.bounds.contains(buttonPoint) { + return button + } + } + return nil + } + + private func updateButtonHover(_ location: NSPoint) { + var newHoveredButton: TrafficLightButton? + if isShowingWindowControls { + for button in windowControlButtons where !button.isHidden { + let buttonPoint = button.convert(location, from: self) + if button.bounds.contains(buttonPoint) { + newHoveredButton = button + break + } + } + } + guard newHoveredButton !== previousHoveredButton else { return } + if let old = previousHoveredButton { + old.isMouseOver = false + old.setNeedsDisplay() + } + if let new = newHoveredButton { + new.isMouseOver = true + new.setNeedsDisplay() + } + previousHoveredButton = newHoveredButton + } + + func findTarget(_ location: NSPoint) -> TileView? { guard let documentView = superview else { return nil } for case let view as TileView in documentView.subviews { let frame = view.frame @@ -107,6 +148,10 @@ class TileOverView: FlippedView { func hideWindowControls() { guard isShowingWindowControls else { return } isShowingWindowControls = false + if let old = previousHoveredButton { + old.isMouseOver = false + previousHoveredButton = nil + } for button in windowControlButtons { button.isHidden = true } diff --git a/src/ui/main-window/TileTitleView.swift b/src/ui/main-window/TileTitleView.swift index 49f4e70f..94a908a2 100644 --- a/src/ui/main-window/TileTitleView.swift +++ b/src/ui/main-window/TileTitleView.swift @@ -31,10 +31,6 @@ class TileTitleView: NSTextField { frame.size.width = width } - override func mouseMoved(with event: NSEvent) { - // no-op here prevents tooltips from disappearing on mouseMoved - } - func updateTruncationModeIfNeeded() { let newLineBreakMode = getTruncationMode() if lineBreakMode != newLineBreakMode { diff --git a/src/ui/main-window/TileView.swift b/src/ui/main-window/TileView.swift index dc34f163..fefd87ba 100644 --- a/src/ui/main-window/TileView.swift +++ b/src/ui/main-window/TileView.swift @@ -25,8 +25,6 @@ class TileView: FlippedView { var indexInRow = 0 var numberOfViewsInRow = 0 - var receivedMouseDown = false - // for VoiceOver cursor override var canBecomeKeyView: Bool { true } override var acceptsFirstResponder: Bool { true } @@ -72,32 +70,6 @@ class TileView: FlippedView { return open != nil } - override func mouseDown(with event: NSEvent) { - receivedMouseDown = true - } - - override func mouseUp(with event: NSEvent) { - if receivedMouseDown { - if bounds.contains(convert(event.locationInWindow, from: nil)) { - mouseUpCallback() - } - receivedMouseDown = false - } - } - - override func otherMouseUp(with event: NSEvent) { - // middle-click - if event.buttonNumber == 2 { - if let window_ { - if window_.isWindowlessApp { - window_.application.quit() - } else { - window_.close() - } - } - } - } - func mouseMoved() { updateLabelTooltipIfNeeded() mouseMovedCallback() @@ -582,7 +554,6 @@ class StatusIconsView: FlippedView { let measure = NSAttributedString(string: Symbols.circledNumber0.rawValue, attributes: attrs) iconCellSize = measure.size() super.init(frame: frame) - addTrackingArea(NSTrackingArea(rect: .zero, options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], owner: self, userInfo: nil)) } required init?(coder: NSCoder) { fatalError() } @@ -623,11 +594,7 @@ class StatusIconsView: FlippedView { needsDisplay = true } - override func mouseEntered(with event: NSEvent) { - ensureTooltipsInstalled() - } - - private func ensureTooltipsInstalled() { + func ensureTooltipsInstalled() { guard tooltipsDirty else { return } tooltipsDirty = false removeAllToolTips() @@ -661,7 +628,4 @@ class StatusIconsView: FlippedView { } } - override func mouseMoved(with event: NSEvent) { - // no-op prevents tooltips from disappearing on mouseMoved - } }