Animation
This commit is contained in:
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test And Build
|
||||
runs-on: macos-14
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Show Swift version
|
||||
run: swift --version
|
||||
|
||||
- name: Run tests
|
||||
run: swift test -q
|
||||
|
||||
- name: Build app bundle
|
||||
run: ./scripts/build-macos-app.sh
|
||||
@@ -2094,14 +2094,17 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
}
|
||||
card.onHover = { [weak self, weak card] selected, mouseLocation in
|
||||
guard let self else { return false }
|
||||
guard self.claimMouseHoverSelection(at: selected, mouseLocation: mouseLocation) else {
|
||||
guard let hoverIndex = self.claimMouseHoverSelection(at: selected, mouseLocation: mouseLocation) else {
|
||||
return false
|
||||
}
|
||||
self.viewModel.selectItem(at: selected, mode: .hover)
|
||||
if !self.isSearchFieldEditing, let card {
|
||||
self.window?.makeFirstResponder(card)
|
||||
self.viewModel.selectItem(at: hoverIndex, mode: .hover)
|
||||
if !self.isSearchFieldEditing {
|
||||
let focusedCard = self.cardView(at: hoverIndex) ?? (hoverIndex == selected ? card : nil)
|
||||
if let focusedCard {
|
||||
self.window?.makeFirstResponder(focusedCard)
|
||||
}
|
||||
}
|
||||
return true
|
||||
return hoverIndex == selected
|
||||
}
|
||||
card.onHoverExit = { [weak self] index in
|
||||
self?.handleCardHoverExit(at: index)
|
||||
@@ -3821,6 +3824,14 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
cardView(at: index)?.debugSetHovered(true)
|
||||
}
|
||||
|
||||
func debugHoverCard(at index: Int, mouseLocationInPanel location: NSPoint) {
|
||||
guard index >= 0, index < cardItemCount else { return }
|
||||
renderCardIfNeeded(at: index)
|
||||
hoverSelectionRequiresFreshMouseMovement = false
|
||||
hoverSelectionKeyboardBarrierLocation = nil
|
||||
cardView(at: index)?.debugSetHovered(true, mouseLocation: convert(location, to: nil))
|
||||
}
|
||||
|
||||
func debugUnhoverCard(at index: Int) {
|
||||
guard index >= 0, index < cardItemCount else { return }
|
||||
renderCardIfNeeded(at: index)
|
||||
@@ -4367,22 +4378,61 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
clearCardHoverStates()
|
||||
}
|
||||
|
||||
private func claimMouseHoverSelection(at index: Int, mouseLocation: NSPoint?) -> Bool {
|
||||
private func claimMouseHoverSelection(at candidateIndex: Int, mouseLocation: NSPoint?) -> Int? {
|
||||
if hoverSelectionRequiresFreshMouseMovement {
|
||||
guard let location = mouseLocation else { return false }
|
||||
guard let location = mouseLocation else { return nil }
|
||||
if let barrier = hoverSelectionKeyboardBarrierLocation,
|
||||
abs(location.x - barrier.x) < 0.5,
|
||||
abs(location.y - barrier.y) < 0.5 {
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
hoverSelectionRequiresFreshMouseMovement = false
|
||||
hoverSelectionKeyboardBarrierLocation = nil
|
||||
}
|
||||
|
||||
guard let index = visualHoverIndex(candidateIndex: candidateIndex, mouseLocation: mouseLocation) else {
|
||||
return nil
|
||||
}
|
||||
cardSelectionInputSource = .mouse
|
||||
selectionScrollSuppressionCount = 3
|
||||
hoveredCardIndex = index
|
||||
clearCardHoverStates(except: index)
|
||||
return true
|
||||
if index != candidateIndex {
|
||||
cardView(at: index)?.setHoverState(true, notifySelection: false, mouseLocation: mouseLocation)
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
private func visualHoverIndex(candidateIndex: Int, mouseLocation: NSPoint?) -> Int? {
|
||||
guard currentPanelLayout == .vertical,
|
||||
let mouseLocation else {
|
||||
return candidateIndex
|
||||
}
|
||||
return visualCardIndex(atWindowLocation: mouseLocation)
|
||||
}
|
||||
|
||||
private func visualCardIndex(atWindowLocation location: NSPoint) -> Int? {
|
||||
let point = convert(location, from: nil)
|
||||
let hitSlop: CGFloat = 1
|
||||
let hits = cardSlots.compactMap { index, slot -> (index: Int, zPosition: CGFloat)? in
|
||||
guard slot.card != nil,
|
||||
let frame = visualSlotFrameInPanel(slot),
|
||||
frame.insetBy(dx: -hitSlop, dy: -hitSlop).contains(point) else {
|
||||
return nil
|
||||
}
|
||||
return (index, slot.visualZPosition)
|
||||
}
|
||||
return hits.sorted { lhs, rhs in
|
||||
if lhs.zPosition == rhs.zPosition {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
return lhs.zPosition > rhs.zPosition
|
||||
}.first?.index
|
||||
}
|
||||
|
||||
private func visualSlotFrameInPanel(_ slot: ClipboardItemCardSlotView) -> NSRect? {
|
||||
guard let superview = slot.superview else { return nil }
|
||||
return superview.convert(slot.visualFrame, to: self)
|
||||
}
|
||||
|
||||
private func clearCardHoverStates(except retainedIndex: Int? = nil) {
|
||||
@@ -5698,7 +5748,19 @@ private final class ClipboardItemCardSlotView: NSView {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var visualFrame: NSRect {
|
||||
layer?.presentation()?.frame ?? frame
|
||||
}
|
||||
|
||||
fileprivate var visualZPosition: CGFloat {
|
||||
CGFloat(layer?.presentation()?.zPosition ?? layer?.zPosition ?? 0)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
var debugPresentationFrame: NSRect {
|
||||
visualFrame
|
||||
}
|
||||
|
||||
var debugCardTopOffset: CGFloat {
|
||||
topConstraint?.constant ?? inactiveTopOffset
|
||||
}
|
||||
@@ -5707,9 +5769,6 @@ private final class ClipboardItemCardSlotView: NSView {
|
||||
CGFloat(layer?.zPosition ?? 0)
|
||||
}
|
||||
|
||||
var debugPresentationFrame: NSRect {
|
||||
layer?.presentation()?.frame ?? frame
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -6612,8 +6671,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
}
|
||||
}
|
||||
|
||||
func debugSetHovered(_ hovered: Bool) {
|
||||
setHoverState(hovered, notifySelection: hovered)
|
||||
func debugSetHovered(_ hovered: Bool, mouseLocation: NSPoint? = nil) {
|
||||
setHoverState(hovered, notifySelection: hovered, mouseLocation: mouseLocation)
|
||||
}
|
||||
|
||||
var debugBorderWidth: CGFloat {
|
||||
|
||||
@@ -518,6 +518,34 @@ final class ClipboardPanelViewTests: XCTestCase {
|
||||
XCTAssertEqual(fixture.view.debugCardExpandedDetailFramesInPanel[1].height, 0, accuracy: 0.5)
|
||||
}
|
||||
|
||||
func testHoverDuringCollapseUsesVisualCardUnderMouseInsteadOfCollapsedTargetSlot() {
|
||||
let fixture = makePanelFixture()
|
||||
fixture.window.setFrame(NSRect(x: 0, y: 0, width: 336, height: 760), display: true)
|
||||
for index in 0..<8 {
|
||||
fixture.store.upsert(makeTextItem("Visual hover row \(index)", store: fixture.store))
|
||||
}
|
||||
drainMainQueue()
|
||||
fixture.window.contentView?.layoutSubtreeIfNeeded()
|
||||
|
||||
fixture.view.debugHoverCard(at: 1)
|
||||
RunLoop.main.run(until: Date().addingTimeInterval(fixture.view.debugCardExpansionAnimationDuration + 0.04))
|
||||
drainMainQueue()
|
||||
fixture.window.contentView?.layoutSubtreeIfNeeded()
|
||||
XCTAssertEqual(fixture.view.debugHoveredCardIndexes, [1])
|
||||
|
||||
fixture.view.debugUnhoverCard(at: 1)
|
||||
RunLoop.main.run(until: Date().addingTimeInterval(0.06))
|
||||
let visualRowFrame = fixture.view.debugCardSlotPresentationFramesInPanel[2]
|
||||
let visualRowPoint = NSPoint(x: visualRowFrame.midX, y: visualRowFrame.midY)
|
||||
|
||||
fixture.view.debugHoverCard(at: 6, mouseLocationInPanel: visualRowPoint)
|
||||
drainMainQueue()
|
||||
fixture.window.contentView?.layoutSubtreeIfNeeded()
|
||||
|
||||
XCTAssertEqual(fixture.view.debugHoveredCardIndexes, [2])
|
||||
XCTAssertEqual(fixture.viewModel.selectedIndex, 2)
|
||||
}
|
||||
|
||||
func testToolbarControlsExposeVoiceOverActionHints() {
|
||||
let fixture = makePanelFixture()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user