From c065214a07ef525dffc7aa8af06689b71235e8d0 Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Sun, 31 May 2026 02:08:51 -0500 Subject: [PATCH 1/2] Fix clipped rightmost titlebar shortcut hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The titlebar shortcut-hint pills (⌘B, ⌘N, ⌘[, ⌘], etc.) live in an NSTitlebarAccessoryViewController whose width AppKit allocates from preferredContentSize and clips to. The rightmost pill was being cut off on its right edge. Measured root cause (window coordinates): the accessory ended at x=224 but the rightmost pill rendered to x=230, a 6pt overflow that AppKit clipped. The reason the reservation fell short is that the hint reservation and the actual hint layout were two independent computations. The reservation was a fixed inset past the button row, but the real layout runs through ShortcutHintHorizontalPlanner, which shifts the whole pill row right by ~20pt to keep the leftmost pill from crossing the leading edge (the pills are wider than the button pitch). The fixed inset never accounted for that shift. Fix: derive the accessory's reserved width from the planner's actual rightmost hint edge via a shared titlebarHintLayoutRightmostExtent(), so contentSize reserves max(buttonReservation, leadingPad + rightmostHintExtent + shadowMargin). The pill-width measurement (SF Rounded, matching ShortcutHintPill) is extracted to a shared titlebarHintPillWidth() used by both the view and the reservation, so the two can't drift. After the fix the accessory ends at x=234 with the pill at x=230 (4pt clearance), and the pills keep their positions under the buttons. No automated test: this is a pixel-level titlebar layout fix verified by measuring the rendered frames; a unit test would only re-derive the same geometry. Verified with the always-show-hints test path. --- Sources/Update/UpdateTitlebarAccessory.swift | 91 +++++++++++++++++--- 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index 49fcdd2971..358acfc626 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -401,6 +401,56 @@ func titlebarShortcutHintHeight(for config: TitlebarControlsStyleConfig) -> CGFl max(14, config.iconSize + 1) } +/// Width of a titlebar shortcut-hint pill, measured with the same font `ShortcutHintPill` +/// renders with (SF Rounded at the pill's font size). Measuring with the default +/// (non-rounded) system font underestimated command-symbol glyphs and let the pill +/// overflow its reserved slot. The `+ 12` matches the pill's 6pt horizontal padding per side. +func titlebarHintPillWidth(for shortcut: StoredShortcut, config: TitlebarControlsStyleConfig) -> CGFloat { + let pillFontSize = max(8, config.iconSize - 5) + let baseFont = NSFont.systemFont(ofSize: pillFontSize, weight: .semibold) + let pillFont = baseFont.fontDescriptor.withDesign(.rounded) + .flatMap { NSFont(descriptor: $0, size: pillFontSize) } ?? baseFont + let textWidth = (shortcut.displayString as NSString).size(withAttributes: [.font: pillFont]).width + return ceil(textWidth) + 12 +} + +/// The rightmost edge the shortcut-hint pills occupy, in the controls' content +/// coordinate space (measured from the leading edge of the button row), after the +/// horizontal planner resolves overlaps. +/// +/// This mirrors `TitlebarControlsView.titlebarHintIntervals` and the +/// `ShortcutHintHorizontalPlanner` so the accessory reserves exactly enough width for +/// the real layout. It is computed unconditionally for every command-bound slot (not +/// gated on modifier state) so the reserved width stays stable whether or not the hints +/// are currently visible. Returns 0 when no slot would show a hint. +func titlebarHintLayoutRightmostExtent( + config: TitlebarControlsStyleConfig, + titlebarShortcutHintXOffset: Double = ShortcutHintDebugSettings.defaultTitlebarHintX +) -> CGFloat { + let xOffset = CGFloat(ShortcutHintDebugSettings.clamped(titlebarShortcutHintXOffset)) + var intervals: [ClosedRange] = [] + for slot in TitlebarShortcutHintActionSlot.allCases { + 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) + } + guard !intervals.isEmpty else { return 0 } + let assignedRightEdges = ShortcutHintHorizontalPlanner.assignRightEdges( + for: intervals, + minSpacing: 6, + minLeadingEdge: config.groupPadding.leading + ) + return assignedRightEdges.max() ?? 0 +} + enum TitlebarShortcutHintMetrics { static let verticalGap: CGFloat = -3 } @@ -438,6 +488,15 @@ enum TitlebarControlsLayoutMetrics { 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 + /// 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)) @@ -455,12 +514,24 @@ enum TitlebarControlsLayoutMetrics { config: TitlebarControlsStyleConfig, titlebarShortcutHintXOffset: Double = ShortcutHintDebugSettings.defaultTitlebarHintX ) -> NSSize { - NSSize( - width: outerLeadingPadding - + config.groupPadding.leading - + buttonRowWidth(config: config) - + config.groupPadding.trailing - + hintTrailingInset(titlebarShortcutHintXOffset: titlebarShortcutHintXOffset), + // Two width requirements; reserve the larger so neither the buttons nor the + // shortcut hints are clipped by the accessory's allocated frame. + let buttonReservation = outerLeadingPadding + + config.groupPadding.leading + + buttonRowWidth(config: config) + + config.groupPadding.trailing + + hintTrailingInset(titlebarShortcutHintXOffset: titlebarShortcutHintXOffset) + // Drive the reservation from the planner's actual rightmost hint edge so the + // overlap-shift the planner applies (which the fixed inset above ignores) is + // always covered. This is what prevents the rightmost pill from clipping. + let hintReservation = hintLeadingPadding + + titlebarHintLayoutRightmostExtent( + config: config, + titlebarShortcutHintXOffset: titlebarShortcutHintXOffset + ) + + hintShadowMargin + return NSSize( + width: max(buttonReservation, hintReservation), height: max( WindowChromeMetrics.appTitlebarHeight, config.groupPadding.top + config.buttonSize + config.groupPadding.bottom @@ -751,7 +822,7 @@ struct TitlebarControlsView: View { private let titlebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultTitlebarHintX private let titlebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultTitlebarHintY private let alwaysShowShortcutHints = ShortcutHintDebugSettings.alwaysShowHints() - private let titlebarHintBaseXShift: CGFloat = -10 + private let titlebarHintBaseXShift: CGFloat = TitlebarControlsLayoutMetrics.hintBaseXShift private struct TitlebarHintLayoutItem: Identifiable { let action: KeyboardShortcutSettings.Action @@ -790,7 +861,7 @@ struct TitlebarControlsView: View { controlsGroup(config: config, foregroundColor: foregroundColor) .padding(.top, -1) .padding(.bottom, 1) - .padding(.leading, 4) + .padding(.leading, TitlebarControlsLayoutMetrics.hintLeadingPadding) .padding(.trailing, titlebarHintTrailingInset) .frame(width: contentSize.width, height: contentSize.height, alignment: .leading) .fixedSize() @@ -1047,9 +1118,7 @@ struct TitlebarControlsView: View { } private func titlebarHintWidth(for shortcut: StoredShortcut, config: TitlebarControlsStyleConfig) -> CGFloat { - let font = NSFont.systemFont(ofSize: max(8, config.iconSize - 4), weight: .semibold) - let textWidth = (shortcut.displayString as NSString).size(withAttributes: [.font: font]).width - return ceil(textWidth) + 12 + titlebarHintPillWidth(for: shortcut, config: config) } private func titlebarButtonRightEdge(for slot: TitlebarShortcutHintActionSlot, config: TitlebarControlsStyleConfig) -> CGFloat { From b5113e7b7d3f4c045b0f910e7bad9cfa4bec4fe8 Mon Sep 17 00:00:00 2001 From: Aziz Albahar Date: Sun, 31 May 2026 11:42:28 -0500 Subject: [PATCH 2/2] Floor sidebar width to the leading titlebar accessory + tooltip extent The default minimum sidebar width (216) is narrower than the rightmost titlebar shortcut-hint tooltip's right edge (~234 in window coordinates), so shrinking the sidebar to its minimum let the last tooltip spill past the divider into the content area. Make minimumSidebarWidth take the max of the user's setting and a tooltip-derived floor: titlebarLeadingInset + a small clearance gap. titlebarLeadingInset is already measured at runtime (TitlebarLeadingInsetReader) as the traffic-light inset plus the width of every leading titlebar accessory, and the controls accessory's width now reserves the shortcut-hint tooltip extent (from the prior commit), so this reads "everything in the accessory area plus the tooltip" with no new measurement. Falls back to the user floor when the inset isn't measured yet. --- Sources/ContentView.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index c0a03908d9..439c0bf544 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -1581,8 +1581,21 @@ struct ContentView: View { private static let maximumRightSidebarWidth: CGFloat = 1200 private static let minimumTerminalWidthWithRightSidebar: CGFloat = 360 + /// Gap kept between the rightmost titlebar shortcut-hint tooltip and the sidebar's + /// trailing edge, so the last tooltip never sits flush against (or spills over) the + /// sidebar divider. + private static let sidebarTooltipClearance: CGFloat = 8 + private var minimumSidebarWidth: CGFloat { - CGFloat(SessionPersistencePolicy.sanitizedMinimumSidebarWidth(sidebarMinimumWidthSetting)) + let userFloor = CGFloat(SessionPersistencePolicy.sanitizedMinimumSidebarWidth(sidebarMinimumWidthSetting)) + // The sidebar must be at least as wide as everything in the leading titlebar + // accessory area (traffic lights + controls + the shortcut-hint tooltips) plus a + // small gap, so the rightmost tooltip stays within the sidebar column instead of + // spilling past the divider. `titlebarLeadingInset` is measured at runtime as the + // traffic-light inset plus every leading accessory's width, and the accessory's + // width now reserves the tooltip extent (see TitlebarControlsLayoutMetrics.contentSize). + let tooltipFloor = titlebarLeadingInset + Self.sidebarTooltipClearance + return max(userFloor, tooltipFloor) } private enum SidebarResizerHandle: Hashable {