From a15ba2915312f25ffa52f2e92e593a02c8a7d27a Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 24 May 2026 00:55:07 +0900 Subject: [PATCH] Move the tab close button to the leading edge Align the terminal tab close affordance with native macOS tabs (Safari/Finder), where the close button sits on the left. Move the close button, its notification dot, and the shortcut-hint overlay into the leading slot, and mirror the rename-field reservation and the icon-picker double-click hit zone to the trailing placeholder so they stay clear of it. --- .../TabBar/Views/TerminalTabLabelView.swift | 3 +- .../TabBar/Views/TerminalTabView.swift | 28 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/supacode/Features/Terminal/TabBar/Views/TerminalTabLabelView.swift b/supacode/Features/Terminal/TabBar/Views/TerminalTabLabelView.swift index 7b4122d8..683cbc20 100644 --- a/supacode/Features/Terminal/TabBar/Views/TerminalTabLabelView.swift +++ b/supacode/Features/Terminal/TabBar/Views/TerminalTabLabelView.swift @@ -30,7 +30,8 @@ struct TerminalTabLabelView: View { .frame(width: TerminalTabBarMetrics.closeButtonSize) } .frame(maxHeight: .infinity) - .overlay(alignment: .trailing) { + // Leading, sharing the close button's slot (they are mutually exclusive). + .overlay(alignment: .leading) { if showsShortcutHint, let shortcutHint { ShortcutHintView(text: shortcutHint, color: TerminalTabBarColors.inactiveText) } diff --git a/supacode/Features/Terminal/TabBar/Views/TerminalTabView.swift b/supacode/Features/Terminal/TabBar/Views/TerminalTabView.swift index dccd6b7a..501b7883 100644 --- a/supacode/Features/Terminal/TabBar/Views/TerminalTabView.swift +++ b/supacode/Features/Terminal/TabBar/Views/TerminalTabView.swift @@ -23,11 +23,12 @@ struct TerminalTabView: View { @State private var editingTitle = "" @State private var initialEditingTitle = "" @State private var cancelOnExit = false + @State private var tabWidth: CGFloat = 0 @Environment(CommandKeyObserver.self) private var commandKeyObserver @Environment(\.resolvedKeybindings) private var resolvedKeybindings var body: some View { - ZStack(alignment: .trailing) { + ZStack(alignment: .leading) { Button(action: onSelect) { TerminalTabLabelView( tab: tab, @@ -67,7 +68,7 @@ struct TerminalTabView: View { } .animation(.easeInOut(duration: TerminalTabBarMetrics.hoverAnimationDuration), value: isHovering) .animation(.easeInOut(duration: 0.2), value: hasNotification) - .padding(.trailing, TerminalTabBarMetrics.tabHorizontalPadding) + .padding(.leading, TerminalTabBarMetrics.tabHorizontalPadding) .opacity(isEditing ? 0 : 1) .allowsHitTesting(!isEditing) } @@ -102,8 +103,8 @@ struct TerminalTabView: View { ) .accessibilityLabel("Rename tab") } - .padding(.leading, TerminalTabBarMetrics.tabHorizontalPadding) - .padding(.trailing, TerminalTabBarMetrics.closeButtonSize + TerminalTabBarMetrics.contentSpacing) + .padding(.leading, TerminalTabBarMetrics.closeButtonSize + TerminalTabBarMetrics.contentSpacing) + .padding(.trailing, TerminalTabBarMetrics.tabHorizontalPadding) .padding(.vertical, TerminalTabBarMetrics.renameFieldInset) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) } @@ -122,6 +123,11 @@ struct TerminalTabView: View { .onHover { hovering in isHovering = hovering } + .onGeometryChange(for: CGFloat.self) { proxy in + proxy.size.width + } action: { width in + tabWidth = width + } .simultaneousGesture( SpatialTapGesture(count: 2, coordinateSpace: .local).onEnded { value in if isInIconHitArea(value.location) { @@ -160,14 +166,16 @@ struct TerminalTabView: View { hasNotification && !isHovering && !isHoveringClose && !isDragging && !showsShortcutHint } - /// Hit zone for the icon in tab-local coordinates. Covers the leading - /// padding plus the icon column, so a double-click anywhere on the icon - /// (or the empty strip just to its left) opens the icon picker. The rest - /// of the tab routes to inline rename. + /// Hit zone for the icon-picker double-click, in tab-local coordinates. + /// The close button now lives in the leading slot, so this mirrors to the + /// trailing placeholder (the opposite slot) to stay clear of it: a + /// double-click there opens the icon picker, while the rest of the tab + /// routes to inline rename. private func isInIconHitArea(_ point: CGPoint) -> Bool { guard tab.isDirty || tab.icon != nil else { return false } - let maxX = TerminalTabBarMetrics.tabHorizontalPadding + TerminalTabBarMetrics.closeButtonSize - return point.x >= 0 && point.x <= maxX + guard tabWidth > 0 else { return false } + let minX = tabWidth - TerminalTabBarMetrics.tabHorizontalPadding - TerminalTabBarMetrics.closeButtonSize + return point.x >= minX } } -- 2.51.2