Ariver
2026-05-19 099cd85c325a605e0d6606817174bf99ea88c351
Apptag/AppGridItem.swift
@@ -10,12 +10,14 @@
    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 isHovered = false
    @State private var wiggle = false
    @State private var interactionFrame: CGRect = .zero
    @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
@@ -35,6 +37,7 @@
    private var iconSlotSize: CGFloat { iconSize * Self.hoverScale }
    private var labelWidth: CGFloat { iconSize + 20 }
    private var labelHeight: CGFloat { Self.labelHeight }
    private var isHovered: Bool { hoveredAppItemID == itemID }
    var body: some View {
        VStack(spacing: 6) {
@@ -62,7 +65,7 @@
                .lineLimit(1)
                .truncationMode(.tail)
                .frame(width: labelWidth, height: labelHeight)
                .opacity(showName ? 1 : (isHovered ? 0.85 : 0))
                .opacity(showName ? 1 : (shouldShowHoverName ? 0.85 : 0))
        }
        .padding(.vertical, 8)
        .padding(.horizontal, 4)
@@ -70,12 +73,19 @@
        .background(
            AppGridItemHoverTracker { hovering, frame in
                interactionFrame = frame
                setHoverState(hovering)
                if bubbleDisabled || dragModeActive {
                    setHoverState(false)
                    onBubbleHover?(app, frame, .exited)
                    return
                }
                if hovering {
                    guard shouldShowAppBubble else { return }
                    onBubbleHover?(app, frame, hovering)
                    setHoverState(true)
                    onBubbleHover?(app, frame, .entered(canShowBubble: shouldShowAppBubble))
                } else if isHovered {
                    setHoverState(false)
                    onBubbleHover?(app, frame, .exited)
                } else {
                    onBubbleHover?(app, frame, hovering)
                    setHoverState(false)
                }
            }
        )
@@ -85,15 +95,26 @@
                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, .exited)
            }
        }
    }
@@ -101,14 +122,26 @@
        !showUncommonAppBubbles || app.isUncommon
    }
    private var shouldShowHoverName: Bool {
        !showName && !shouldShowAppBubble && isHovered
    }
    private func setHoverState(_ hovering: Bool) {
        guard isHovered != hovering else { return }
        withAnimation(hovering ? Self.hoverInAnimation : Self.hoverOutAnimation) {
            isHovered = hovering
            if hovering {
                hoveredAppItemID = itemID
            } else if hoveredAppItemID == itemID {
                hoveredAppItemID = nil
            }
        }
    }
}
enum AppBubbleHoverEvent {
    case entered(canShowBubble: Bool)
    case exited
}
private struct AppGridItemHoverTracker: NSViewRepresentable {
    let onHover: (Bool, CGRect) -> Void