From d5cf4d94edbe72d53fb417bf32105c2258bce924 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 14 Jun 2026 16:02:13 +0800
Subject: [PATCH] Fix horizontal App hover linkage

---
 C3.tools/round1-app-shelf-fixture-qa.sh             |    1 
 C3.tools/round1-horizontal-waterfall-fixture-qa.sh  |   35 +++++++++++++++++
 C1.source/Sources/Aligner/QuickSwitchRootView.swift |   46 +++++++++++++++++++----
 C1.source/Resources/Aligner-Info.plist              |    4 +-
 C3.tools/round1-three-zone-linkage-fixture-qa.sh    |    1 
 5 files changed, 77 insertions(+), 10 deletions(-)

diff --git a/C1.source/Resources/Aligner-Info.plist b/C1.source/Resources/Aligner-Info.plist
index 03cfa10..5c260c2 100644
--- a/C1.source/Resources/Aligner-Info.plist
+++ b/C1.source/Resources/Aligner-Info.plist
@@ -17,9 +17,9 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-    <string>0.0.66</string>
+    <string>0.0.67</string>
 	<key>CFBundleVersion</key>
-    <string>20260613.2043</string>
+    <string>20260614.1559</string>
 	<key>LSMinimumSystemVersion</key>
 	<string>26.0</string>
 	<key>NSHighResolutionCapable</key>
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index c04faf7..40611ae 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -3340,6 +3340,14 @@
         )
     }
 
+    private var appIconHoverLinkedAppGroupIndex: Int? {
+        if let debugHoveredAppGroupIndex {
+            return debugHoveredAppGroupIndex
+        }
+        guard case .app(let appGroupIndex) = hoverTarget else { return nil }
+        return appGroupIndex
+    }
+
     private var appHoverAssociatedSpaceLabels: [String] {
         guard !appHoverAssociatedSpaceIDs.isEmpty else { return [] }
         return currentViewModel?.displays
@@ -3767,10 +3775,13 @@
         let bounds = card.containerLayer.bounds
         let isSelected = card.item.window.id == effectiveSelection?.windowID
         let isHovered = card.item.window.id == hoveredWindowID
+        let isAppLinked = isHorizontalMasonryMode
+            && card.item.appGroupIndex == appIconHoverLinkedAppGroupIndex
         applyWaterfallCardVisualState(
             card,
             selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered),
-            hovered: isHovered
+            hovered: isHovered,
+            appLinked: isAppLinked
         )
 
         let outerInset: CGFloat = 0
@@ -3833,12 +3844,19 @@
         )
     }
 
-    private func applyWaterfallCardVisualState(_ card: WaterfallCardLayers, selected: Bool, hovered: Bool) {
+    private func applyWaterfallCardVisualState(
+        _ card: WaterfallCardLayers,
+        selected: Bool,
+        hovered: Bool,
+        appLinked: Bool
+    ) {
         let isSpaceFocused = isWaterfallCardSpaceFocused(card)
         card.containerLayer.backgroundColor = (selected
             ? NSColor.windowBackgroundColor.withAlphaComponent(0.94)
             : hovered
                 ? accentTintedWindowBackground(fraction: 0.09, alpha: 0.95)
+                : appLinked
+                    ? accentTintedWindowBackground(fraction: 0.08, alpha: 0.93)
                 : isSpaceFocused
                     ? accentTintedWindowBackground(fraction: 0.10, alpha: 0.94)
                 : NSColor.windowBackgroundColor.withAlphaComponent(0.82)
@@ -3847,27 +3865,31 @@
             ? NSColor.controlAccentColor.withAlphaComponent(0.52)
             : hovered
                 ? NSColor.controlAccentColor.withAlphaComponent(0.44)
+                : appLinked
+                    ? NSColor.controlAccentColor.withAlphaComponent(0.38)
                 : isSpaceFocused
                     ? NSColor.controlAccentColor.withAlphaComponent(0.42)
                 : NSColor.separatorColor.withAlphaComponent(0.28)
         ).cgColor
-        card.containerLayer.borderWidth = (selected || hovered || isSpaceFocused) ? 1.5 : 1
+        card.containerLayer.borderWidth = (selected || hovered || appLinked || isSpaceFocused) ? 1.5 : 1
         card.containerLayer.shadowColor = NSColor.black.cgColor
-        card.containerLayer.shadowOpacity = selected ? 0.24 : hovered ? 0.14 : isSpaceFocused ? 0.20 : 0
-        card.containerLayer.shadowRadius = selected ? 22 : hovered ? 17 : isSpaceFocused ? 24 : 0
+        card.containerLayer.shadowOpacity = selected ? 0.24 : hovered ? 0.14 : appLinked ? 0.16 : isSpaceFocused ? 0.20 : 0
+        card.containerLayer.shadowRadius = selected ? 22 : hovered ? 17 : appLinked ? 18 : isSpaceFocused ? 24 : 0
         card.containerLayer.shadowOffset = selected
             ? CGSize(width: 0, height: -6)
             : hovered
                 ? CGSize(width: 0, height: -2.5)
+                : appLinked
+                    ? CGSize(width: 0, height: -3)
                 : isSpaceFocused
                     ? CGSize(width: 0, height: -5)
                     : .zero
-        card.containerLayer.zPosition = selected ? 20 : hovered ? 10 : isSpaceFocused ? 8 : 0
+        card.containerLayer.zPosition = selected ? 20 : hovered ? 12 : appLinked ? 10 : isSpaceFocused ? 8 : 0
         let focusedScale: CGFloat = selected ? 1.015 : isSpaceFocused ? 1.012 : 1.008
         card.containerLayer.transform = (selected || hovered || isSpaceFocused)
             ? CATransform3DMakeScale(focusedScale, focusedScale, 1)
             : CATransform3DIdentity
-        card.containerLayer.shadowPath = (selected || hovered || isSpaceFocused)
+        card.containerLayer.shadowPath = (selected || hovered || appLinked || isSpaceFocused)
             ? CGPath(
                 roundedRect: card.containerLayer.bounds,
                 cornerWidth: card.containerLayer.cornerRadius,
@@ -3875,7 +3897,9 @@
                 transform: nil
             )
             : nil
-        card.titleBarLayer.backgroundColor = NSColor.clear.cgColor
+        card.titleBarLayer.backgroundColor = appLinked
+            ? spaceLaneOccupiedColor.withAlphaComponent(0.18).cgColor
+            : NSColor.clear.cgColor
         card.titleBarLayer.borderColor = NSColor.clear.cgColor
         card.titleBarLayer.borderWidth = 0
         card.shineLayer.opacity = selected ? 1 : 0
@@ -5002,6 +5026,7 @@
                 "shadowRadius": Double(card.containerLayer.shadowRadius),
                 "thumbnailFrame": dictionary(from: card.thumbnailLayer.frame),
                 "titleBarFrame": dictionary(from: card.titleBarLayer.frame),
+                "titleBarBackgroundColor": colorDictionary(from: card.titleBarLayer.backgroundColor),
                 "titleBarBorderWidth": Double(card.titleBarLayer.borderWidth),
                 "titleFrame": dictionary(from: card.titleLayer.frame),
                 "metaFrame": dictionary(from: card.metaLayer.frame),
@@ -5109,6 +5134,8 @@
         var states: [String] = []
         let isSelected = card.item.window.id == effectiveSelection?.windowID
         let isHovered = card.item.window.id == hoveredWindowID
+        let isAppLinked = isHorizontalMasonryMode
+            && card.item.appGroupIndex == appIconHoverLinkedAppGroupIndex
         if shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) {
             states.append("selected")
         } else if isSelected {
@@ -5117,6 +5144,9 @@
         if isHovered {
             states.append("hover")
         }
+        if isAppLinked {
+            states.append("appLinked")
+        }
         if isWaterfallCardSpaceFocused(card) {
             states.append("spaceFocused")
         }
diff --git a/C3.tools/round1-app-shelf-fixture-qa.sh b/C3.tools/round1-app-shelf-fixture-qa.sh
index bd4716f..59446ee 100755
--- a/C3.tools/round1-app-shelf-fixture-qa.sh
+++ b/C3.tools/round1-app-shelf-fixture-qa.sh
@@ -214,6 +214,7 @@
     --round01-open-quick-switch
     --round01-fixture-app-count="$app_count"
     --round01-disable-screenshot-refresh
+    --round01-waterfall-view-mode=vertical
     --round01-debug-hover-app-index="$hover_index"
     --round01-quick-switch-report="$report"
   )
diff --git a/C3.tools/round1-horizontal-waterfall-fixture-qa.sh b/C3.tools/round1-horizontal-waterfall-fixture-qa.sh
index a441016..5463421 100755
--- a/C3.tools/round1-horizontal-waterfall-fixture-qa.sh
+++ b/C3.tools/round1-horizontal-waterfall-fixture-qa.sh
@@ -122,6 +122,7 @@
 require(root.get("horizontalMasonryMaxScrollOffset", 0) > 0, "horizontal masonry must expose positive vertical max scroll offset")
 require(root.get("horizontalMasonryScrollOffset", 0) > 0, "hovering a later App must anchor masonry vertically")
 require(root.get("appShelfHoveredIndex") == hover_app_index, "debug hover must mark the requested App Shelf item")
+require(root.get("hoveredAppGroupIndex") == hover_app_index, "horizontal masonry must keep the hovered App group available to Waterfall visuals")
 
 require(cards, "horizontal masonry fixture must expose cards")
 widths = [round(card.get("frame", {}).get("width", 0), 1) for card in cards]
@@ -161,6 +162,37 @@
 hover_visible = hover_first_card.get("visibleFrame", {})
 require(hover_visible.get("y", -9999) < waterfall_height + 1, "hovered App first card should be brought into vertical viewport")
 require(hover_visible.get("y", 0) + hover_visible.get("height", 0) > -1, "hovered App first card should not be fully below viewport")
+hover_cards = hover_column.get("cards", [])
+non_hover_cards = [
+    card
+    for column in columns
+    if column.get("appGroupIndex") != hover_app_index
+    for card in column.get("cards", [])
+]
+blue_reference = next(
+    (
+        color
+        for segment in root.get("spaceLaneSegments", [])
+        for color in segment.get("windowBlockColors", [])
+        if color.get("alpha", 0) > 0
+    ),
+    None
+)
+require(blue_reference is not None, "fixture must expose a Space Lane blue reference color")
+require(all("appLinked" in card.get("visualStates", []) for card in hover_cards), "all cards for the hovered App must expose appLinked visual state")
+require(all("appLinked" not in card.get("visualStates", []) for card in non_hover_cards), "non-hovered App cards must not expose appLinked visual state")
+require(all(card.get("shadowOpacity", 0) >= 0.15 for card in hover_cards), "hovered App cards must float with visible shadow opacity")
+require(all(card.get("shadowRadius", 0) >= 18 for card in hover_cards), "hovered App cards must float with visible shadow radius")
+require(all(card.get("shadowOpacity", 1) <= 0.01 for card in non_hover_cards if "spaceFocused" not in card.get("visualStates", []) and "hover" not in card.get("visualStates", []) and "selected" not in card.get("visualStates", [])), "normal non-hovered cards must not inherit App hover shadow")
+for card in hover_cards:
+    title_fill = card.get("titleBarBackgroundColor", {})
+    require(title_fill.get("alpha", 0) >= 0.16, "hovered App card title bar must get a visible blue fill")
+    for channel in ("red", "green", "blue"):
+        require(
+            abs(title_fill.get(channel, -1) - blue_reference.get(channel, -2)) <= 0.002,
+            f"hovered App card title bar must use the Space Lane blue reference for {channel}"
+        )
+require(all(card.get("titleBarBackgroundColor", {}).get("alpha", 1) <= 0.01 for card in non_hover_cards), "non-hovered App card title bars must stay clear")
 
 print(json.dumps({
     "mode": root.get("waterfallViewMode"),
@@ -170,6 +202,9 @@
     "cardHeight": heights[0],
     "thumbnailHeight": thumbnail_heights[0],
     "cardStep": expected_card_step,
+    "hoveredAppGroupIndex": root.get("hoveredAppGroupIndex"),
+    "hoveredAppLinkedCardCount": len(hover_cards),
+    "hoveredAppTitleBarFill": hover_cards[0].get("titleBarBackgroundColor"),
     "horizontalMasonryScrollOffset": root.get("horizontalMasonryScrollOffset"),
     "horizontalMasonryMaxScrollOffset": root.get("horizontalMasonryMaxScrollOffset")
 }, indent=2, ensure_ascii=False))
diff --git a/C3.tools/round1-three-zone-linkage-fixture-qa.sh b/C3.tools/round1-three-zone-linkage-fixture-qa.sh
index 8486406..af79f00 100755
--- a/C3.tools/round1-three-zone-linkage-fixture-qa.sh
+++ b/C3.tools/round1-three-zone-linkage-fixture-qa.sh
@@ -131,6 +131,7 @@
     --round01-debug-overlay-width="$OVERLAY_WIDTH"
     --round01-disable-screenshot-refresh
     --round01-debug-window-activation
+    --round01-waterfall-view-mode=vertical
     --round01-debug-mouse-sequence="$sequence"
     --round01-quick-switch-report="$report"
   )

--
Gitblit v1.9.3