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
|
card.onHover = { [weak self, weak card] selected, mouseLocation in
|
||||||
guard let self else { return false }
|
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
|
return false
|
||||||
}
|
}
|
||||||
self.viewModel.selectItem(at: selected, mode: .hover)
|
self.viewModel.selectItem(at: hoverIndex, mode: .hover)
|
||||||
if !self.isSearchFieldEditing, let card {
|
if !self.isSearchFieldEditing {
|
||||||
self.window?.makeFirstResponder(card)
|
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
|
card.onHoverExit = { [weak self] index in
|
||||||
self?.handleCardHoverExit(at: index)
|
self?.handleCardHoverExit(at: index)
|
||||||
@@ -3821,6 +3824,14 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|||||||
cardView(at: index)?.debugSetHovered(true)
|
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) {
|
func debugUnhoverCard(at index: Int) {
|
||||||
guard index >= 0, index < cardItemCount else { return }
|
guard index >= 0, index < cardItemCount else { return }
|
||||||
renderCardIfNeeded(at: index)
|
renderCardIfNeeded(at: index)
|
||||||
@@ -4367,22 +4378,61 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|||||||
clearCardHoverStates()
|
clearCardHoverStates()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func claimMouseHoverSelection(at index: Int, mouseLocation: NSPoint?) -> Bool {
|
private func claimMouseHoverSelection(at candidateIndex: Int, mouseLocation: NSPoint?) -> Int? {
|
||||||
if hoverSelectionRequiresFreshMouseMovement {
|
if hoverSelectionRequiresFreshMouseMovement {
|
||||||
guard let location = mouseLocation else { return false }
|
guard let location = mouseLocation else { return nil }
|
||||||
if let barrier = hoverSelectionKeyboardBarrierLocation,
|
if let barrier = hoverSelectionKeyboardBarrierLocation,
|
||||||
abs(location.x - barrier.x) < 0.5,
|
abs(location.x - barrier.x) < 0.5,
|
||||||
abs(location.y - barrier.y) < 0.5 {
|
abs(location.y - barrier.y) < 0.5 {
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
hoverSelectionRequiresFreshMouseMovement = false
|
hoverSelectionRequiresFreshMouseMovement = false
|
||||||
hoverSelectionKeyboardBarrierLocation = nil
|
hoverSelectionKeyboardBarrierLocation = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let index = visualHoverIndex(candidateIndex: candidateIndex, mouseLocation: mouseLocation) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
cardSelectionInputSource = .mouse
|
cardSelectionInputSource = .mouse
|
||||||
selectionScrollSuppressionCount = 3
|
selectionScrollSuppressionCount = 3
|
||||||
hoveredCardIndex = index
|
hoveredCardIndex = index
|
||||||
clearCardHoverStates(except: 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) {
|
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
|
#if DEBUG
|
||||||
|
var debugPresentationFrame: NSRect {
|
||||||
|
visualFrame
|
||||||
|
}
|
||||||
|
|
||||||
var debugCardTopOffset: CGFloat {
|
var debugCardTopOffset: CGFloat {
|
||||||
topConstraint?.constant ?? inactiveTopOffset
|
topConstraint?.constant ?? inactiveTopOffset
|
||||||
}
|
}
|
||||||
@@ -5707,9 +5769,6 @@ private final class ClipboardItemCardSlotView: NSView {
|
|||||||
CGFloat(layer?.zPosition ?? 0)
|
CGFloat(layer?.zPosition ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugPresentationFrame: NSRect {
|
|
||||||
layer?.presentation()?.frame ?? frame
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6612,8 +6671,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func debugSetHovered(_ hovered: Bool) {
|
func debugSetHovered(_ hovered: Bool, mouseLocation: NSPoint? = nil) {
|
||||||
setHoverState(hovered, notifySelection: hovered)
|
setHoverState(hovered, notifySelection: hovered, mouseLocation: mouseLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugBorderWidth: CGFloat {
|
var debugBorderWidth: CGFloat {
|
||||||
|
|||||||
@@ -518,6 +518,34 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
XCTAssertEqual(fixture.view.debugCardExpandedDetailFramesInPanel[1].height, 0, accuracy: 0.5)
|
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() {
|
func testToolbarControlsExposeVoiceOverActionHints() {
|
||||||
let fixture = makePanelFixture()
|
let fixture = makePanelFixture()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user