| | |
| | | var sourceTag: String? = nil |
| | | var dragModeActive: Bool = false |
| | | var onDragModeChange: ((Bool) -> Void)? = nil |
| | | var onBubbleHover: ((AppInfo, CGRect, Bool) -> Void)? = nil |
| | | var onBubbleHover: ((AppInfo, CGRect, AppBubbleHoverEvent) -> Void)? = nil |
| | | var onEditNote: ((AppInfo, CGRect) -> Void)? = nil |
| | | var bubbleDisabled: Bool = false |
| | | var itemID: String |
| | | var dragResetToken: Int = 0 |
| | | @Binding var hoveredAppItemID: String? |
| | | let onSelect: () -> Void |
| | | |
| | | @State private var wiggle = false |
| | | @State private var interactionFrame: CGRect = .zero |
| | | @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles |
| | | |
| | |
| | | .background( |
| | | AppGridItemHoverTracker { hovering, frame in |
| | | interactionFrame = frame |
| | | if bubbleDisabled || dragModeActive { |
| | | setHoverState(false) |
| | | onBubbleHover?(app, frame, .exited) |
| | | return |
| | | } |
| | | if hovering { |
| | | setHoverState(true) |
| | | guard shouldShowAppBubble else { return } |
| | | onBubbleHover?(app, frame, hovering) |
| | | onBubbleHover?(app, frame, .entered(canShowBubble: shouldShowAppBubble)) |
| | | } else if isHovered { |
| | | setHoverState(false) |
| | | onBubbleHover?(app, frame, hovering) |
| | | onBubbleHover?(app, frame, .exited) |
| | | } else { |
| | | setHoverState(false) |
| | | } |
| | |
| | | onEditNote?(app, interactionFrame) |
| | | } |
| | | } |
| | | .rotationEffect(.degrees(dragModeActive ? (wiggle ? 2.0 : -2.0) : 0)) |
| | | .animation( |
| | | dragModeActive |
| | | ? .easeInOut(duration: 0.12).repeatForever(autoreverses: true) |
| | | : .default, |
| | | value: wiggle |
| | | ) |
| | | .opacity(dragModeActive ? 0.92 : 1) |
| | | .animation(.easeOut(duration: 0.08), value: dragModeActive) |
| | | .id("\(itemID)|reset-\(dragResetToken)") |
| | | .onChange(of: dragModeActive) { _, active in |
| | | wiggle = active |
| | | if active { |
| | | setHoverState(false) |
| | | onBubbleHover?(app, interactionFrame, .exited) |
| | | } |
| | | } |
| | | .onChange(of: bubbleDisabled) { _, disabled in |
| | | if disabled { |
| | | setHoverState(false) |
| | | onBubbleHover?(app, interactionFrame, .exited) |
| | | } |
| | | } |
| | | .onDisappear { |
| | | if hoveredAppItemID == itemID { |
| | | hoveredAppItemID = nil |
| | | onBubbleHover?(app, interactionFrame, false) |
| | | onBubbleHover?(app, interactionFrame, .exited) |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | enum AppBubbleHoverEvent { |
| | | case entered(canShowBubble: Bool) |
| | | case exited |
| | | } |
| | | |
| | | private struct AppGridItemHoverTracker: NSViewRepresentable { |
| | |
| | | } |
| | | |
| | | func updateNSView(_ view: DragIconNSView, context: Context) { |
| | | let imageChanged = view.image !== icon |
| | | let sizeChanged = view.iconSize != iconSize |
| | | view.image = icon |
| | | view.iconSize = iconSize |
| | | view.payload = payload |
| | | view.onLongPress = onLongPress |
| | | view.onDragEnd = onDragEnd |
| | | view.onClick = onClick |
| | | view.needsDisplay = true |
| | | if imageChanged || sizeChanged { |
| | | view.needsDisplay = true |
| | | } |
| | | } |
| | | } |
| | | |