From 21e9dd7c95e3f58a19a7c7342ff3d6584eb9097e Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 14 Aug 2026 04:45:56 +0800
Subject: [PATCH] fix: reserve app shelf height for wrapped rows

---
 C3.tools/round1-app-shelf-fixture-qa.sh             |   27 +++++++++++++
 C1.source/CODEGRAPH.md                              |    1 
 C1.source/Sources/Aligner/QuickSwitchRootView.swift |   26 ++++++++----
 C1.source/Resources/Aligner-Info.plist              |    4 +-
 CHANGELOG.md                                        |    6 +++
 5 files changed, 53 insertions(+), 11 deletions(-)

diff --git a/C1.source/CODEGRAPH.md b/C1.source/CODEGRAPH.md
index ef59f7f..8f70b12 100644
--- a/C1.source/CODEGRAPH.md
+++ b/C1.source/CODEGRAPH.md
@@ -230,6 +230,7 @@
 - App Shelf 序号为 `1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ`,最多 36 个一键入口。
 - App 序号键在纵栏和横栏瀑布下一视同仁,等效于 hover 到对应 App 图标。
 - App Shelf 鼠标 hover 只作用于真实 hover App:图标本身放大并轻微上浮,但不显示蓝色矩形底或蓝色矩形边框;纵栏瀑布和横栏瀑布下都适用。
+- App Shelf 两行布局必须按真实行高预留完整 shelf 高度;首行不得被 Space Lane 裁挡,且 Waterfall 仍保留固定顶间距。
 - 鼠标停留在 App 图标上不动时,即使 view-model / 截图刷新触发布局重建,真实 hover App 必须保持放大、上浮和 Waterfall 联动;不能自行恢复普通尺寸。
 - App Shelf hover 不做邻近 App 视觉跟随;左右邻居不放大、不上浮、不暴露 `magneticNeighbor`,也不触发 hover 语义、不显示 close button、不联动 Waterfall。
 - 用户开启 Reduce Motion 时,App Shelf 不做磁性放大动效,只保留稳定状态切换。
diff --git a/C1.source/Resources/Aligner-Info.plist b/C1.source/Resources/Aligner-Info.plist
index 67da45f..00a2234 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.1.12</string>
+	<string>0.1.13</string>
 	<key>CFBundleVersion</key>
-	<string>20260625.2320</string>
+	<string>20260814.0445</string>
 	<key>LSMinimumSystemVersion</key>
 	<string>14.0</string>
 	<key>NSHighResolutionCapable</key>
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index 7bb50aa..dd36a57 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -413,6 +413,9 @@
         static let normalGap: CGFloat = 16
         static let minGap: CGFloat = 8
         static let maxRows = 2
+        static let rowHeight: CGFloat = selectedIconSize + 18
+        static let rowGap: CGFloat = 8
+        static let verticalPadding: CGFloat = 6
         static let indexFontSize: CGFloat = 16
         static let compressedIndexFontSize: CGFloat = 14
         static let indexWidth: CGFloat = 18
@@ -4505,12 +4508,17 @@
     }
 
     private func appShelfHeight(forRows rows: Int) -> CGFloat {
-        switch rows {
-        case 0, 1:
-            return 92
-        default:
-            return 148
-        }
+        appShelfRowsHeight(forRows: max(1, rows)) + AppShelfMetrics.verticalPadding
+    }
+
+    private func appShelfRowsHeight(forRows rows: Int) -> CGFloat {
+        let rowCount = max(1, rows)
+        return CGFloat(rowCount) * AppShelfMetrics.rowHeight
+            + CGFloat(rowCount - 1) * appShelfRowGap(forRows: rowCount)
+    }
+
+    private func appShelfRowGap(forRows rows: Int) -> CGFloat {
+        rows > 1 ? AppShelfMetrics.rowGap : 0
     }
 
     private var appShelfSlotPadding: CGFloat {
@@ -5052,8 +5060,8 @@
         let iconSize = appShelfLayout.iconSize
         let gap = appShelfGap(forIconSize: iconSize)
         let itemWidth = appShelfSlotWidth(forIconSize: iconSize)
-        let rowHeight: CGFloat = AppShelfMetrics.selectedIconSize + 18
-        let rowGap: CGFloat = appShelfLayout.rows > 1 ? 8 : 0
+        let rowHeight = AppShelfMetrics.rowHeight
+        let rowGap = appShelfRowGap(forRows: rows.count)
         let innerWidth = max(1, shelfLayer.bounds.width - 24)
         let maxRowWidth = rows
             .map { widthForAppShelfItems(count: $0.count, itemWidth: itemWidth, gap: gap) }
@@ -5067,7 +5075,7 @@
             height: shelfLayer.bounds.height
         )
 
-        let totalRowsHeight = CGFloat(rows.count) * rowHeight + CGFloat(max(0, rows.count - 1)) * rowGap
+        let totalRowsHeight = appShelfRowsHeight(forRows: rows.count)
         var y = shelfLayer.bounds.midY + totalRowsHeight / 2 - rowHeight
 
         for row in rows {
diff --git a/C3.tools/round1-app-shelf-fixture-qa.sh b/C3.tools/round1-app-shelf-fixture-qa.sh
index 59281ed..7415143 100755
--- a/C3.tools/round1-app-shelf-fixture-qa.sh
+++ b/C3.tools/round1-app-shelf-fixture-qa.sh
@@ -286,6 +286,29 @@
 
 row_y_values = {round(item.get("frame", {}).get("y", 0), 2) for item in items}
 require(len(row_y_values) == expected_rows, "fixture App Shelf visible rows must match expected rows")
+
+def rect_number(rect, key):
+    value = rect.get(key, 0)
+    return value if isinstance(value, (int, float)) else 0
+
+def rect_max_y(rect):
+    return rect_number(rect, "y") + rect_number(rect, "height")
+
+space_lane_frame = root.get("spaceLaneFrame", {})
+app_shelf_frame = root.get("appShelfFrame", {})
+waterfall_frame = root.get("waterfallFrame", {})
+app_shelf_height = rect_number(app_shelf_frame, "height")
+expected_row_height = root.get("appShelfSelectedIconSize", 68) + 18
+expected_row_gap = 8 if expected_rows > 1 else 0
+expected_rows_height = expected_rows * expected_row_height + max(0, expected_rows - 1) * expected_row_gap
+epsilon = 0.5
+require(app_shelf_height + epsilon >= expected_rows_height, "App Shelf frame must reserve enough height for all visible rows")
+require(all(rect_number(item.get("frame", {}), "y") >= -epsilon for item in items), "App Shelf items must not overflow below the shelf frame")
+require(all(rect_max_y(item.get("frame", {})) <= app_shelf_height + epsilon for item in items), "App Shelf items must not overflow above the shelf frame")
+space_lane_gap = rect_number(space_lane_frame, "y") - rect_max_y(app_shelf_frame)
+waterfall_gap = rect_number(app_shelf_frame, "y") - rect_max_y(waterfall_frame)
+require(space_lane_gap >= 15.5, "App Shelf must keep the 16pt gap below Space Lane")
+require(waterfall_gap >= 13.5, "Waterfall must keep the 14pt gap below App Shelf")
 require([item.get("name") for item in items] == root.get("appShelfNames", []), "App Shelf item reports must follow App Shelf order")
 selected_z = items[selected_index].get("zPosition", 0)
 hover_z = items[hover_index].get("zPosition", 0)
@@ -299,6 +322,10 @@
     "appCount": report.get("appCount"),
     "rows": root.get("appShelfRows"),
     "iconSize": root.get("appShelfIconSize"),
+    "spaceLaneFrame": space_lane_frame,
+    "appShelfFrame": app_shelf_frame,
+    "spaceLaneGap": space_lane_gap,
+    "waterfallGap": waterfall_gap,
     "selectedIndex": selected_index,
     "selectedIcon": items[selected_index].get("iconFrame"),
     "hoveredIndex": root.get("appShelfHoveredIndex"),
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bef662f..c07ae53 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## 0.1.13 / 20260814.0445
+
+- 修复 Quick Switch 在 App Shelf 自动换成两行时,首行 App 图标被 Space Lane 裁挡的问题。
+- App Shelf 两行布局改为按真实行高预留 shelf 高度,并保持 Space Lane / App Shelf / Waterfall 的固定纵向间距。
+- 增强 App Shelf fixture QA,断言两行 App 图标必须完整落在 shelf 内,并保留 Space Lane 与 Waterfall 间距。
+
 ## 0.1.12 / 20260625.2320
 
 - 修复 App Shelf 图标点击误触关闭的问题:图标主体点击优先打开该 App 的第一个窗口,只有明确点到关闭按钮视觉区域才进入关闭确认。

--
Gitblit v1.9.3