From ed4ebcfa763f66ecd69cae57df2c949a7a9a8a45 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 13 Jun 2026 20:23:29 +0800
Subject: [PATCH] Prepare 7.9.1 usage tips release

---
 src/Apptag/AppGridCollectionView.swift |   97 +++++++++++++++++++++++++++++-------------------
 1 files changed, 59 insertions(+), 38 deletions(-)

diff --git a/src/Apptag/AppGridCollectionView.swift b/src/Apptag/AppGridCollectionView.swift
index e5df3a5..ae2714e 100644
--- a/src/Apptag/AppGridCollectionView.swift
+++ b/src/Apptag/AppGridCollectionView.swift
@@ -38,8 +38,8 @@
 }
 
 enum AppGridUsageTipsMetrics {
-    static let barHeight: CGFloat = 84
-    static let reservedHeight: CGFloat = 128
+    static let barHeight: CGFloat = 136
+    static let reservedHeight: CGFloat = 176
     static let bottomMargin: CGFloat = 20
     static let horizontalInset: CGFloat = 24
     static let minWidth: CGFloat = 640
@@ -943,44 +943,42 @@
         let paddingRight: CGFloat = 16
         let iconSize: CGFloat = 24
         let iconTextGap: CGFloat = 10
-        let titleDetailGap: CGFloat = 22
-        let detailControlsGap: CGFloat = 24
+        let controlZoneWidth: CGFloat = 176
         let buttonGap: CGFloat = 6
         let buttonSize: CGFloat = 48
+        let buttonDotsGap: CGFloat = 6
+        let dotsHeight: CGFloat = 10
         let dotsWidth = dotsView.preferredWidth
         let fullTitleWidth = ceil(titleLabel.attributedStringValue.size().width)
 
-        let centerY = visualFrame.midY
+        let titleX = visualFrame.minX + paddingLeft + iconSize + iconTextGap
+        let buttonsWidth = buttonSize * 2 + buttonGap
+        let buttonsGroupMinX = visualFrame.maxX - paddingRight - buttonsWidth
+        let textRight = visualFrame.maxX - controlZoneWidth
+        let availableTextWidth = max(1, textRight - titleX)
+        let titleWidth = min(fullTitleWidth, availableTextWidth)
+        let titleHeight: CGFloat = 32
+        let detailHeight: CGFloat = 68
+        let lineGap: CGFloat = 8
+        let textBlockHeight = titleHeight + lineGap + detailHeight
+        let textBlockY = visualFrame.minY + max(0, (visualFrame.height - textBlockHeight) / 2)
+
         iconView.frame = NSRect(
             x: visualFrame.minX + paddingLeft,
-            y: centerY - iconSize / 2,
+            y: visualFrame.midY - iconSize / 2,
             width: iconSize,
             height: iconSize
         )
 
-        let titleX = iconView.frame.maxX + iconTextGap
-        let buttonsWidth = buttonSize * 2 + buttonGap
-        let trailingWidth = paddingRight + buttonsWidth + detailControlsGap
-        let availableTextWidth = max(1, visualFrame.maxX - titleX - titleDetailGap - trailingWidth)
-        let minimumReadableDetailWidth: CGFloat = 220
-        let titleWidth: CGFloat
-        if availableTextWidth >= fullTitleWidth + minimumReadableDetailWidth {
-            titleWidth = fullTitleWidth
-        } else if availableTextWidth >= fullTitleWidth {
-            titleWidth = fullTitleWidth
-        } else {
-            titleWidth = max(1, availableTextWidth)
-        }
-
         titleLabel.frame = NSRect(
             x: titleX,
-            y: visualFrame.minY,
+            y: textBlockY,
             width: titleWidth,
-            height: visualFrame.height
+            height: titleHeight
         )
 
-        let buttonY = visualFrame.minY + 10
-        let buttonsGroupMinX = visualFrame.maxX - paddingRight - buttonsWidth
+        let controlGroupHeight = buttonSize + buttonDotsGap + dotsHeight
+        let buttonY = visualFrame.minY + max(0, (visualFrame.height - controlGroupHeight) / 2)
         nextButton.frame = NSRect(
             x: buttonsGroupMinX + buttonSize + buttonGap,
             y: buttonY,
@@ -995,18 +993,18 @@
         )
         dotsView.frame = NSRect(
             x: buttonsGroupMinX + (buttonsWidth - dotsWidth) / 2,
-            y: previousButton.frame.maxY + 6,
+            y: previousButton.frame.maxY + buttonDotsGap,
             width: dotsWidth,
-            height: 10
+            height: dotsHeight
         )
 
-        let detailX = titleLabel.frame.maxX + titleDetailGap
-        let detailRight = previousButton.frame.minX - detailControlsGap
+        let detailX = titleX
+        let detailRight = textRight
         detailScrollView.frame = NSRect(
             x: detailX,
-            y: visualFrame.minY,
+            y: titleLabel.frame.maxY + lineGap,
             width: max(1, detailRight - detailX),
-            height: visualFrame.height
+            height: detailHeight
         )
         layoutDetailLabel()
     }
@@ -1048,10 +1046,10 @@
 
     func preferredWidth(maxAvailableWidth: CGFloat) -> CGFloat {
         let padding: CGFloat = 22 + 16
-        let fixedWidth: CGFloat = 24 + 10 + 22 + 24 + 48 + 6 + 48
+        let fixedWidth: CGFloat = 24 + 10 + 24 + 48 + 6 + 48
         let titleWidth = ceil(titleLabel.attributedStringValue.size().width)
-        let detailWidth = ceil(detailLabel.attributedStringValue.size().width)
-        let contentWidth = padding + fixedWidth + titleWidth + detailWidth
+        let detailWidth = measuredDetailLineWidth()
+        let contentWidth = padding + fixedWidth + max(titleWidth, detailWidth)
         let preferredWidth = max(AppGridUsageTipsMetrics.minWidth, contentWidth)
         return min(maxAvailableWidth, preferredWidth)
     }
@@ -1081,7 +1079,7 @@
         addSubview(iconView)
 
         configureLabel(titleLabel, font: titleFont, lineBreakMode: .byClipping)
-        configureLabel(detailLabel, font: detailFont, lineBreakMode: .byClipping)
+        configureDetailLabel()
 
         detailScrollView.drawsBackground = false
         detailScrollView.hasVerticalScroller = false
@@ -1112,6 +1110,19 @@
         label.lineBreakMode = lineBreakMode
         label.maximumNumberOfLines = 1
         label.font = font
+    }
+
+    private func configureDetailLabel() {
+        detailLabel.cell = NSTextFieldCell(textCell: "")
+        detailLabel.isEditable = false
+        detailLabel.isSelectable = false
+        detailLabel.drawsBackground = false
+        detailLabel.isBordered = false
+        detailLabel.lineBreakMode = .byWordWrapping
+        detailLabel.maximumNumberOfLines = 2
+        detailLabel.font = detailFont
+        detailLabel.cell?.usesSingleLineMode = false
+        detailLabel.cell?.wraps = true
     }
 
     private func claimInteractionFocus() {
@@ -1180,9 +1191,17 @@
     }
 
     private func layoutDetailLabel() {
-        let textWidth = detailLabel.attributedStringValue.size().width
-        let width = max(detailScrollView.contentView.bounds.width, ceil(textWidth) + 18)
+        let width = max(1, detailScrollView.contentView.bounds.width)
+        detailLabel.preferredMaxLayoutWidth = width
         detailLabel.frame = NSRect(x: 0, y: 0, width: width, height: detailScrollView.bounds.height)
+    }
+
+    private func measuredDetailLineWidth() -> CGFloat {
+        let attributes: [NSAttributedString.Key: Any] = [.font: detailFont]
+        let lineWidths = detailLabel.stringValue
+            .components(separatedBy: .newlines)
+            .map { NSAttributedString(string: $0, attributes: attributes).size().width }
+        return ceil(lineWidths.max() ?? detailLabel.attributedStringValue.size().width)
     }
 
     private func currentVisualFrame() -> NSRect {
@@ -1198,8 +1217,10 @@
 
     private func formattedTipDetail(_ text: String) -> String {
         text
-            .replacingOccurrences(of: " > ", with: " -> ")
-            .replacingOccurrences(of: ">", with: "->")
+            .replacingOccurrences(of: "\\s*(?:-->|->|>|→|>)\\s*", with: "\n", options: .regularExpression)
+            .replacingOccurrences(of: "[ \\t]{2,}", with: " ", options: .regularExpression)
+            .replacingOccurrences(of: "\\n{2,}", with: "\n", options: .regularExpression)
+            .trimmingCharacters(in: .whitespacesAndNewlines)
     }
 
     private func updateColors() {

--
Gitblit v1.9.3