From ebd00cdb6bb25bcbbd0689f4c93133a426052c2c Mon Sep 17 00:00:00 2001 From: onevcat Date: Fri, 26 Jun 2026 23:04:42 +0900 Subject: [PATCH] ux: minimal scroll-to-reveal instead of zoom-to-fill on card nav Replace focusViewport (which recomputes scale and centers the card) with scrollToRevealCard: keeps the current zoom level and only pans the canvas when the target card is partially outside the viewport. Cards already in view don't trigger any canvas movement at all. --- .../Features/Canvas/Views/CanvasView.swift | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/supacode/Features/Canvas/Views/CanvasView.swift b/supacode/Features/Canvas/Views/CanvasView.swift index 1df7353d..08e98dbc 100644 --- a/supacode/Features/Canvas/Views/CanvasView.swift +++ b/supacode/Features/Canvas/Views/CanvasView.swift @@ -896,7 +896,7 @@ struct CanvasView: View { let allTabIDs = collectVisibleTabIDs(from: activeStates) if let first = allTabIDs.first { focusSingleCard(first, states: activeStates) - focusViewport(on: first) + scrollToRevealCard(first) } return !allTabIDs.isEmpty } @@ -919,10 +919,57 @@ struct CanvasView: View { } focusSingleCard(targetTabID, states: activeStates) - focusViewport(on: targetTabID) + scrollToRevealCard(targetTabID) return true } + private func scrollToRevealCard(_ tabID: TerminalTabID) { + guard viewportSize.width > 0, viewportSize.height > 0 else { return } + let cardKey = tabID.rawValue.uuidString + guard let layout = layoutStore.cardLayouts[cardKey] else { return } + + let margin: CGFloat = 20 + let cardHeight = layout.size.height + titleBarHeight + let scaledHalfW = layout.size.width / 2 * canvasScale + let scaledHalfH = cardHeight / 2 * canvasScale + let screenCenter = screenPosition(for: layout.position) + + let cardMinX = screenCenter.x - scaledHalfW + let cardMaxX = screenCenter.x + scaledHalfW + let cardMinY = screenCenter.y - scaledHalfH + let cardMaxY = screenCenter.y + scaledHalfH + + let viewMinX: CGFloat = 0 + let viewMaxX = viewportSize.width + let viewMinY: CGFloat = 0 + let viewMaxY = viewportSize.height - bottomToolbarReserve + + var deltaX: CGFloat = 0 + var deltaY: CGFloat = 0 + + if cardMinX < viewMinX + margin { + deltaX = (viewMinX + margin) - cardMinX + } else if cardMaxX > viewMaxX - margin { + deltaX = (viewMaxX - margin) - cardMaxX + } + + if cardMinY < viewMinY + margin { + deltaY = (viewMinY + margin) - cardMinY + } else if cardMaxY > viewMaxY - margin { + deltaY = (viewMaxY - margin) - cardMaxY + } + + guard deltaX != 0 || deltaY != 0 else { return } + + let newOffset = CGSize( + width: canvasOffset.width + deltaX, + height: canvasOffset.height + deltaY + ) + canvasOffset = newOffset + lastCanvasOffset = newOffset + focusViewportAnimationID &+= 1 + } + private func cardEntries( from states: [WorktreeTerminalState] ) -> [CanvasSpatialNavigation.CardEntry] { -- 2.51.2