-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Align titlebar accessory hints #5059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -433,22 +433,17 @@ func titlebarHintLayoutRightmostExtent( | |||||||||||||||||||||||||||||||||||||||||||
| let shortcut = KeyboardShortcutSettings.shortcut(for: slot.action) | ||||||||||||||||||||||||||||||||||||||||||||
| guard !shortcut.isUnbound, shortcut.command else { continue } | ||||||||||||||||||||||||||||||||||||||||||||
| let width = titlebarHintPillWidth(for: shortcut, config: config) | ||||||||||||||||||||||||||||||||||||||||||||
| let index = CGFloat(slot.rawValue) | ||||||||||||||||||||||||||||||||||||||||||||
| let buttonRightEdge = (index + 1) * config.buttonSize + index * config.spacing | ||||||||||||||||||||||||||||||||||||||||||||
| let rightEdge = config.groupPadding.leading | ||||||||||||||||||||||||||||||||||||||||||||
| + buttonRightEdge | ||||||||||||||||||||||||||||||||||||||||||||
| + xOffset | ||||||||||||||||||||||||||||||||||||||||||||
| + TitlebarControlsLayoutMetrics.hintRightSafetyShift | ||||||||||||||||||||||||||||||||||||||||||||
| + TitlebarControlsLayoutMetrics.hintBaseXShift | ||||||||||||||||||||||||||||||||||||||||||||
| intervals.append((rightEdge - width)...rightEdge) | ||||||||||||||||||||||||||||||||||||||||||||
| intervals.append( | ||||||||||||||||||||||||||||||||||||||||||||
| TitlebarControlsLayoutMetrics.hintInterval( | ||||||||||||||||||||||||||||||||||||||||||||
| for: slot, | ||||||||||||||||||||||||||||||||||||||||||||
| width: width, | ||||||||||||||||||||||||||||||||||||||||||||
| config: config, | ||||||||||||||||||||||||||||||||||||||||||||
| xOffset: xOffset | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| guard !intervals.isEmpty else { return 0 } | ||||||||||||||||||||||||||||||||||||||||||||
| let assignedRightEdges = ShortcutHintHorizontalPlanner.assignRightEdges( | ||||||||||||||||||||||||||||||||||||||||||||
| for: intervals, | ||||||||||||||||||||||||||||||||||||||||||||
| minSpacing: 6, | ||||||||||||||||||||||||||||||||||||||||||||
| minLeadingEdge: config.groupPadding.leading | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| return assignedRightEdges.max() ?? 0 | ||||||||||||||||||||||||||||||||||||||||||||
| return intervals.map(\.upperBound).max() ?? 0 | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| enum TitlebarShortcutHintMetrics { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -485,22 +480,16 @@ enum TitlebarShortcutHintActionSlot: Int, CaseIterable { | |||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| enum TitlebarControlsLayoutMetrics { | ||||||||||||||||||||||||||||||||||||||||||||
| static let outerLeadingPadding: CGFloat = TitlebarControlsHitRegions.outerLeadingPadding | ||||||||||||||||||||||||||||||||||||||||||||
| static let hintRightSafetyShift: CGFloat = 10 | ||||||||||||||||||||||||||||||||||||||||||||
| static let hintTrailingBaseInset: CGFloat = 8 | ||||||||||||||||||||||||||||||||||||||||||||
| static let trafficLightGap: CGFloat = 2 | ||||||||||||||||||||||||||||||||||||||||||||
| /// Constant X shift applied to every hint's right edge in the view's layout. Must | ||||||||||||||||||||||||||||||||||||||||||||
| /// match `TitlebarControlsView.titlebarHintBaseXShift` so the reserved width matches | ||||||||||||||||||||||||||||||||||||||||||||
| /// the rendered positions. | ||||||||||||||||||||||||||||||||||||||||||||
| static let hintBaseXShift: CGFloat = -10 | ||||||||||||||||||||||||||||||||||||||||||||
| /// Leading inset the controls content sits at inside the accessory; must match the | ||||||||||||||||||||||||||||||||||||||||||||
| /// `.padding(.leading, …)` applied to `controlsGroup` in the view body. | ||||||||||||||||||||||||||||||||||||||||||||
| static let hintLeadingPadding: CGFloat = 4 | ||||||||||||||||||||||||||||||||||||||||||||
| static let hintLeadingPadding: CGFloat = HeaderChromeControlMetrics.titlebarControlsLeadingPadding | ||||||||||||||||||||||||||||||||||||||||||||
| /// Extra trailing room past the rightmost pill for its capsule stroke and shadow. | ||||||||||||||||||||||||||||||||||||||||||||
| static let hintShadowMargin: CGFloat = 4 | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| static func hintTrailingInset(titlebarShortcutHintXOffset: Double = ShortcutHintDebugSettings.defaultTitlebarHintX) -> CGFloat { | ||||||||||||||||||||||||||||||||||||||||||||
| max(0, ShortcutHintDebugSettings.clamped(titlebarShortcutHintXOffset)) | ||||||||||||||||||||||||||||||||||||||||||||
| + hintRightSafetyShift | ||||||||||||||||||||||||||||||||||||||||||||
| + hintTrailingBaseInset | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -510,6 +499,26 @@ enum TitlebarControlsLayoutMetrics { | |||||||||||||||||||||||||||||||||||||||||||
| return (buttonCount * config.buttonSize) + (gapCount * config.spacing) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| static func buttonCenterX( | ||||||||||||||||||||||||||||||||||||||||||||
| for slot: TitlebarShortcutHintActionSlot, | ||||||||||||||||||||||||||||||||||||||||||||
| config: TitlebarControlsStyleConfig | ||||||||||||||||||||||||||||||||||||||||||||
| ) -> CGFloat { | ||||||||||||||||||||||||||||||||||||||||||||
| let index = CGFloat(slot.rawValue) | ||||||||||||||||||||||||||||||||||||||||||||
| return config.groupPadding.leading | ||||||||||||||||||||||||||||||||||||||||||||
| + (index * (config.buttonSize + config.spacing)) | ||||||||||||||||||||||||||||||||||||||||||||
| + (config.buttonSize / 2.0) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| static func hintInterval( | ||||||||||||||||||||||||||||||||||||||||||||
| for slot: TitlebarShortcutHintActionSlot, | ||||||||||||||||||||||||||||||||||||||||||||
| width: CGFloat, | ||||||||||||||||||||||||||||||||||||||||||||
| config: TitlebarControlsStyleConfig, | ||||||||||||||||||||||||||||||||||||||||||||
| xOffset: CGFloat | ||||||||||||||||||||||||||||||||||||||||||||
| ) -> ClosedRange<CGFloat> { | ||||||||||||||||||||||||||||||||||||||||||||
| let centerX = buttonCenterX(for: slot, config: config) + xOffset | ||||||||||||||||||||||||||||||||||||||||||||
| return (centerX - (width / 2.0))...(centerX + (width / 2.0)) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| static func contentSize( | ||||||||||||||||||||||||||||||||||||||||||||
| config: TitlebarControlsStyleConfig, | ||||||||||||||||||||||||||||||||||||||||||||
| titlebarShortcutHintXOffset: Double = ShortcutHintDebugSettings.defaultTitlebarHintX | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -544,16 +553,12 @@ enum TitlebarControlsLayoutMetrics { | |||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| static func leadingOffset( | ||||||||||||||||||||||||||||||||||||||||||||
| trafficLightFrame: NSRect?, | ||||||||||||||||||||||||||||||||||||||||||||
| trafficLightFrame _: NSRect?, | ||||||||||||||||||||||||||||||||||||||||||||
| debugSnapshot: MinimalModeTitlebarDebugSnapshot | ||||||||||||||||||||||||||||||||||||||||||||
| ) -> CGFloat { | ||||||||||||||||||||||||||||||||||||||||||||
| let debugOffset = MinimalModeTitlebarDebugSettings.leftControlsXOffset( | ||||||||||||||||||||||||||||||||||||||||||||
| MinimalModeTitlebarDebugSettings.leftControlsXOffset( | ||||||||||||||||||||||||||||||||||||||||||||
| leadingInset: debugSnapshot.leftControlsLeadingInset | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| guard let trafficLightFrame, !trafficLightFrame.isEmpty else { | ||||||||||||||||||||||||||||||||||||||||||||
| return debugOffset | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| return max(debugOffset, trafficLightFrame.maxX + trafficLightGap) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
555
to
562
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| static func yOffset( | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -822,13 +827,12 @@ struct TitlebarControlsView: View { | |||||||||||||||||||||||||||||||||||||||||||
| private let titlebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultTitlebarHintX | ||||||||||||||||||||||||||||||||||||||||||||
| private let titlebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultTitlebarHintY | ||||||||||||||||||||||||||||||||||||||||||||
| private let alwaysShowShortcutHints = ShortcutHintDebugSettings.alwaysShowHints() | ||||||||||||||||||||||||||||||||||||||||||||
| private let titlebarHintBaseXShift: CGFloat = TitlebarControlsLayoutMetrics.hintBaseXShift | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| private struct TitlebarHintLayoutItem: Identifiable { | ||||||||||||||||||||||||||||||||||||||||||||
| let action: KeyboardShortcutSettings.Action | ||||||||||||||||||||||||||||||||||||||||||||
| let shortcut: StoredShortcut | ||||||||||||||||||||||||||||||||||||||||||||
| let width: CGFloat | ||||||||||||||||||||||||||||||||||||||||||||
| let leftEdge: CGFloat | ||||||||||||||||||||||||||||||||||||||||||||
| let centerX: CGFloat | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| var id: String { action.rawValue } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1071,24 +1075,15 @@ struct TitlebarControlsView: View { | |||||||||||||||||||||||||||||||||||||||||||
| let intervals = titlebarHintIntervals(config: config, xOffset: xOffset) | ||||||||||||||||||||||||||||||||||||||||||||
| guard !intervals.isEmpty else { return [] } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // Keep all titlebar hints on the same Y lane and resolve overlaps by shifting left. | ||||||||||||||||||||||||||||||||||||||||||||
| let minimumSpacing: CGFloat = 6 | ||||||||||||||||||||||||||||||||||||||||||||
| let assignedRightEdges = ShortcutHintHorizontalPlanner.assignRightEdges( | ||||||||||||||||||||||||||||||||||||||||||||
| for: intervals.map { $0.interval }, | ||||||||||||||||||||||||||||||||||||||||||||
| minSpacing: minimumSpacing, | ||||||||||||||||||||||||||||||||||||||||||||
| minLeadingEdge: config.groupPadding.leading | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| var items: [TitlebarHintLayoutItem] = [] | ||||||||||||||||||||||||||||||||||||||||||||
| items.reserveCapacity(intervals.count) | ||||||||||||||||||||||||||||||||||||||||||||
| for (index, item) in intervals.enumerated() { | ||||||||||||||||||||||||||||||||||||||||||||
| let rightEdge = assignedRightEdges[index] | ||||||||||||||||||||||||||||||||||||||||||||
| for item in intervals { | ||||||||||||||||||||||||||||||||||||||||||||
| items.append( | ||||||||||||||||||||||||||||||||||||||||||||
| TitlebarHintLayoutItem( | ||||||||||||||||||||||||||||||||||||||||||||
| action: item.action, | ||||||||||||||||||||||||||||||||||||||||||||
| shortcut: item.shortcut, | ||||||||||||||||||||||||||||||||||||||||||||
| width: item.width, | ||||||||||||||||||||||||||||||||||||||||||||
| leftEdge: rightEdge - item.width | ||||||||||||||||||||||||||||||||||||||||||||
| centerX: (item.interval.lowerBound + item.interval.upperBound) / 2.0 | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1110,24 +1105,20 @@ struct TitlebarControlsView: View { | |||||||||||||||||||||||||||||||||||||||||||
| ) else { return nil } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let width = titlebarHintWidth(for: shortcut, config: config) | ||||||||||||||||||||||||||||||||||||||||||||
| let rightEdge = config.groupPadding.leading | ||||||||||||||||||||||||||||||||||||||||||||
| + titlebarButtonRightEdge(for: slot, config: config) | ||||||||||||||||||||||||||||||||||||||||||||
| + xOffset | ||||||||||||||||||||||||||||||||||||||||||||
| + TitlebarControlsLayoutMetrics.hintRightSafetyShift | ||||||||||||||||||||||||||||||||||||||||||||
| + titlebarHintBaseXShift | ||||||||||||||||||||||||||||||||||||||||||||
| return (slot.action, shortcut, width, (rightEdge - width)...rightEdge) | ||||||||||||||||||||||||||||||||||||||||||||
| let interval = TitlebarControlsLayoutMetrics.hintInterval( | ||||||||||||||||||||||||||||||||||||||||||||
| for: slot, | ||||||||||||||||||||||||||||||||||||||||||||
| width: width, | ||||||||||||||||||||||||||||||||||||||||||||
| config: config, | ||||||||||||||||||||||||||||||||||||||||||||
| xOffset: xOffset | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| return (slot.action, shortcut, width, interval) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| private func titlebarHintWidth(for shortcut: StoredShortcut, config: TitlebarControlsStyleConfig) -> CGFloat { | ||||||||||||||||||||||||||||||||||||||||||||
| titlebarHintPillWidth(for: shortcut, config: config) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| private func titlebarButtonRightEdge(for slot: TitlebarShortcutHintActionSlot, config: TitlebarControlsStyleConfig) -> CGFloat { | ||||||||||||||||||||||||||||||||||||||||||||
| let index = CGFloat(slot.rawValue) | ||||||||||||||||||||||||||||||||||||||||||||
| return (index + 1) * config.buttonSize + index * config.spacing | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| @ViewBuilder | ||||||||||||||||||||||||||||||||||||||||||||
| private func titlebarShortcutHintOverlay( | ||||||||||||||||||||||||||||||||||||||||||||
| items: [TitlebarHintLayoutItem], | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1138,18 +1129,17 @@ struct TitlebarControlsView: View { | |||||||||||||||||||||||||||||||||||||||||||
| + ShortcutHintDebugSettings.clamped(titlebarShortcutHintYOffset) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| ZStack(alignment: .topLeading) { | ||||||||||||||||||||||||||||||||||||||||||||
| Color.clear | ||||||||||||||||||||||||||||||||||||||||||||
| ForEach(items) { item in | ||||||||||||||||||||||||||||||||||||||||||||
| VStack(alignment: .leading, spacing: 0) { | ||||||||||||||||||||||||||||||||||||||||||||
| Color.clear.frame(height: yOffset) | ||||||||||||||||||||||||||||||||||||||||||||
| HStack(spacing: 0) { | ||||||||||||||||||||||||||||||||||||||||||||
| Color.clear.frame(width: item.leftEdge) | ||||||||||||||||||||||||||||||||||||||||||||
| titlebarShortcutHintPill(shortcut: item.shortcut, config: config) | ||||||||||||||||||||||||||||||||||||||||||||
| .accessibilityIdentifier("titlebarShortcutHint.\(item.action.rawValue)") | ||||||||||||||||||||||||||||||||||||||||||||
| .frame(width: item.width, alignment: .leading) | ||||||||||||||||||||||||||||||||||||||||||||
| .background(TitlebarChromeGeometryReporter(keyPrefix: "titlebarShortcutHint_\(item.action.rawValue)")) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| .shortcutHintTransition() | ||||||||||||||||||||||||||||||||||||||||||||
| titlebarShortcutHintPill(shortcut: item.shortcut, config: config) | ||||||||||||||||||||||||||||||||||||||||||||
| .accessibilityIdentifier("titlebarShortcutHint.\(item.action.rawValue)") | ||||||||||||||||||||||||||||||||||||||||||||
| .frame(width: item.width, alignment: .center) | ||||||||||||||||||||||||||||||||||||||||||||
| .background(TitlebarChromeGeometryReporter(keyPrefix: "titlebarShortcutHint_\(item.action.rawValue)")) | ||||||||||||||||||||||||||||||||||||||||||||
| .position( | ||||||||||||||||||||||||||||||||||||||||||||
| x: item.centerX, | ||||||||||||||||||||||||||||||||||||||||||||
| y: yOffset + titlebarShortcutHintHeight(for: config) / 2.0 | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| .shortcutHintTransition() | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| .shortcutHintVisibilityAnimation(value: shouldShowTitlebarShortcutHints) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leadingOffsetrefactortrafficLightGapis now defined but referenced nowhere in production code. The only call sites were the oldleadingOffsetbody (which now ignores the traffic-light frame entirely) and the test that was updated to expect0. Leaving an unreachable constant here risks future callers picking it up and re-introducing the double-offset that this PR is specifically removing.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!