项目文件夹

文件
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

169 行
8.3 KiB
Swift

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
import Foundation
/// Settings layout-preview geometry (design §4.1): a PURE derivation from the appearance
/// preferences to scaled preview rects — the view only draws, never computes.
///
/// Every capacity/geometry number flows through the SAME `LaunchpadLayoutMath` pipeline
/// the live overlay runs (`resolve → compactFrame → gridViewport → pageGrid → slot math`),
/// so WYSIWYG is a structural guarantee, not a convention. The model owns no persistence
/// and performs no IO: it projects the preference values it is handed and never reads the
/// app catalog (ruling P2 — the preview shows page CAPACITY, not the current census).
struct LaunchpadLayoutPreviewModel: Equatable {
struct Tile: Equatable {
var iconRect: CGRect
/// nil when app names are hidden (feature 7) — the view drops the label bar.
var labelRect: CGRect?
}
/// The scaled "screen" frame (origin .zero in the preview's coordinate space, top-left).
var screenSize: CGSize
/// The scaled launcher window. Fullscreen: == the whole screen frame. Compact: the
/// centred panel drawn as a SECOND layer over the screen frame (ruling P1 — two
/// layers, which also visualises the window-size slider for free).
var windowRect: CGRect
/// Scaled search-bar placeholder, positioned from `Chrome` — the same constants the
/// live view reads, so the preview cannot drift from the real chrome.
var searchBarRect: CGRect
/// One FULL page of placeholders, rows × columns, row-major.
var tiles: [Tile]
var columns: Int
var rows: Int
/// Scaled centre of the page-indicator dot row (inside `Chrome.pageIndicatorReserve`).
var pageDotsCenter: CGPoint
/// Preview points per screen point — uniform on both axes (aspect-fit).
var scale: CGFloat
/// The caption's "K per page" — and the A4 clamp's user-facing signal: when a fixed
/// column count cannot fit (e.g. 12 × 96pt icons), `pageGrid` clamps silently and
/// these numbers are how the user learns the effective layout.
var perPage: Int { columns * rows }
/// - Parameters:
/// - appearance: the normalized appearance snapshot (`LaunchpadPreferences.appearance`).
/// - fixedColumns: nil = auto columns; otherwise the user's fixed count (pre-clamp).
/// - compactScalePercent: the normalized window-size percentage; ignored in fullscreen.
/// - screenFrame: the representative screen's PHYSICAL frame size (`NSScreen.frame`)
/// — what the real fullscreen overlay covers, and the drawn screen canvas.
/// - visibleFrame: the screen's visible area (menu bar/Dock removed) expressed in
/// this model's TOP-LEFT space relative to `screenFrame` — what the real compact
/// panel centres on. The two sources mirror `LaunchpadOverlayController.targetFrame(on:)`
/// exactly; feeding one size to both modes made the fullscreen caption drop a
/// row/column vs the real launchpad on the very same screen.
/// - canvas: the available preview area; the screen frame aspect-fits into it.
static func make(
appearance: LaunchpadAppearance,
mode: LaunchpadPreferences.WindowMode,
fixedColumns: Int?,
compactScalePercent: Int,
screenFrame: CGSize,
visibleFrame: CGRect,
canvas: CGSize
) -> LaunchpadLayoutPreviewModel {
// Degenerate inputs (a zero-size GeometryReader pass, a missing screen) fall back
// to safe positives so nothing below divides by zero or returns NaN rects.
let screen = CGSize(width: max(screenFrame.width, 320), height: max(screenFrame.height, 200))
let screenRect = CGRect(origin: .zero, size: screen)
// The visible area must be a positive rect inside the (possibly clamped) screen
// frame; anything else — a zero rect from a headless host, a stale value after
// the frame fallback kicked in — degrades to the whole frame.
let visible = visibleFrame.width > 0 && visibleFrame.height > 0
&& screenRect.contains(visibleFrame) ? visibleFrame : screenRect
let scale = max(0.001, min(canvas.width / screen.width, canvas.height / screen.height))
// The production pipeline, verbatim (design §4.1):
// resolve → (compactFrame) → gridViewport → pageGrid.
let metrics = LaunchpadGridMetrics.resolve(appearance)
let window: CGRect
switch mode {
case .fullscreen:
// Frame parity with `targetFrame(on:)`: the real fullscreen overlay covers
// `screen.frame` (menu bar and Dock included), NOT the visible frame.
window = screenRect
case .compact:
// Frame parity again: the real compact panel centres `compactFrame` on
// `screen.visibleFrame`. `visible` is already in this top-left space, and
// centring survives the coordinate flip (centres map to centres), so the
// returned rect reads directly here.
window = LaunchpadLayoutMath.compactFrame(
visible: visible,
scalePercent: compactScalePercent,
metrics: metrics,
legacyCap: false
)
}
let chrome = LaunchpadLayoutMath.Chrome.standard(isCompact: mode == .compact)
let viewport = LaunchpadLayoutMath.gridViewport(mode: mode, windowSize: window.size)
let grid = LaunchpadLayoutMath.pageGrid(
viewport: viewport,
metrics: metrics,
fixedColumns: fixedColumns
)
// Grid viewport origin inside the window — mirrors `LaunchpadGridView.body`'s
// VStack: top padding, then the search bar, then the stack spacing.
let viewportOrigin = CGPoint(
x: window.minX + chrome.horizontalPadding,
y: window.minY + chrome.topPadding + chrome.searchBarHeight + chrome.stackSpacing
)
// Tile geometry comes from the SAME functions the live grid lays out with —
// `LaunchpadLayoutMath.slotRect` (incl. the floored centring inset) and the
// cell-local `iconFrameInCell` / `labelFrameInCell`. Shared source, not
// mirrored formulas: a cell-layout change moves both surfaces together.
var tiles: [Tile] = []
tiles.reserveCapacity(grid.columns * grid.rows)
for index in 0..<(grid.columns * grid.rows) {
let slot = LaunchpadLayoutMath.slotRect(
index: index,
columns: grid.columns,
containerWidth: viewport.width,
metrics: metrics
).offsetBy(dx: viewportOrigin.x, dy: viewportOrigin.y)
let icon = metrics.iconFrameInCell.offsetBy(dx: slot.minX, dy: slot.minY)
let label: CGRect? = metrics.showsLabels
? metrics.labelFrameInCell.offsetBy(dx: slot.minX, dy: slot.minY)
: nil
tiles.append(Tile(
iconRect: icon.scaled(by: scale),
labelRect: label.map { $0.scaled(by: scale) }
))
}
let searchBar = CGRect(
x: window.midX - chrome.searchBarWidth / 2,
y: window.minY + chrome.topPadding,
width: chrome.searchBarWidth,
height: chrome.searchBarHeight
)
// The dot row sits at the bottom of the grid viewport, inside the 26pt reserve
// `pageGrid` subtracts — i.e. just above the window's bottom padding.
let pageDotsCenter = CGPoint(
x: window.midX,
y: window.maxY - chrome.bottomPadding
- LaunchpadLayoutMath.Chrome.pageIndicatorReserve / 2
)
return LaunchpadLayoutPreviewModel(
screenSize: CGSize(width: screen.width * scale, height: screen.height * scale),
windowRect: window.scaled(by: scale),
searchBarRect: searchBar.scaled(by: scale),
tiles: tiles,
columns: grid.columns,
rows: grid.rows,
pageDotsCenter: pageDotsCenter.scaled(by: scale),
scale: scale
)
}
}
private extension CGRect {
func scaled(by factor: CGFloat) -> CGRect {
CGRect(x: minX * factor, y: minY * factor, width: width * factor, height: height * factor)
}
}
private extension CGPoint {
func scaled(by factor: CGFloat) -> CGPoint {
CGPoint(x: x * factor, y: y * factor)
}
}