diff --git a/Sources/Bonsplit/Internal/Controllers/SplitViewController.swift b/Sources/Bonsplit/Internal/Controllers/SplitViewController.swift index 7a97a37e..c747b76c 100644 --- a/Sources/Bonsplit/Internal/Controllers/SplitViewController.swift +++ b/Sources/Bonsplit/Internal/Controllers/SplitViewController.swift @@ -181,14 +181,15 @@ final class SplitViewController { } /// Split a pane with a specific tab, optionally inserting the new pane first - func splitPaneWithTab(_ paneId: PaneID, orientation: SplitOrientation, tab: TabItem, insertFirst: Bool) { + func splitPaneWithTab(_ paneId: PaneID, orientation: SplitOrientation, tab: TabItem, insertFirst: Bool, animate: Bool = true) { clearPaneZoom() rootNode = splitNodeWithTabRecursively( node: rootNode, targetPaneId: paneId, orientation: orientation, tab: tab, - insertFirst: insertFirst + insertFirst: insertFirst, + animate: animate ) } @@ -197,7 +198,8 @@ final class SplitViewController { targetPaneId: PaneID, orientation: SplitOrientation, tab: TabItem, - insertFirst: Bool + insertFirst: Bool, + animate: Bool ) -> SplitNode { switch node { case .pane(let paneState): @@ -214,7 +216,7 @@ final class SplitViewController { first: .pane(newPane), second: .pane(paneState), dividerPosition: 0.5, - animationOrigin: .fromFirst + animationOrigin: animate ? .fromFirst : nil ) } else { // New pane goes second (right or bottom). @@ -223,7 +225,7 @@ final class SplitViewController { first: .pane(paneState), second: .pane(newPane), dividerPosition: 0.5, - animationOrigin: .fromSecond + animationOrigin: animate ? .fromSecond : nil ) } @@ -240,14 +242,16 @@ final class SplitViewController { targetPaneId: targetPaneId, orientation: orientation, tab: tab, - insertFirst: insertFirst + insertFirst: insertFirst, + animate: animate ) splitState.second = splitNodeWithTabRecursively( node: splitState.second, targetPaneId: targetPaneId, orientation: orientation, tab: tab, - insertFirst: insertFirst + insertFirst: insertFirst, + animate: animate ) return .split(splitState) } diff --git a/Sources/Bonsplit/Internal/Views/TabBarView.swift b/Sources/Bonsplit/Internal/Views/TabBarView.swift index e14b6e96..4f2a5d38 100644 --- a/Sources/Bonsplit/Internal/Views/TabBarView.swift +++ b/Sources/Bonsplit/Internal/Views/TabBarView.swift @@ -249,6 +249,7 @@ struct TabBarView: View { dlog("tab.close pane=\(pane.id.id.uuidString.prefix(5)) tab=\(tab.id.uuidString.prefix(5)) title=\"\(tab.title)\"") #endif withTransaction(Transaction(animation: nil)) { + controller.onTabCloseRequest?(TabID(id: tab.id), pane.id) _ = controller.closeTab(TabID(id: tab.id), inPane: pane.id) } }, diff --git a/Sources/Bonsplit/Public/BonsplitController.swift b/Sources/Bonsplit/Public/BonsplitController.swift index a44f7688..a7abcde4 100644 --- a/Sources/Bonsplit/Public/BonsplitController.swift +++ b/Sources/Bonsplit/Public/BonsplitController.swift @@ -51,6 +51,10 @@ public final class BonsplitController { /// Return `true` when the drop has been handled by the host application. @ObservationIgnored public var onExternalTabDrop: ((ExternalTabDropRequest) -> Bool)? + /// Called when the user explicitly requests to close a tab from the tab strip UI. + /// Internal host-driven closes should not use this hook. + @ObservationIgnored public var onTabCloseRequest: ((_ tabId: TabID, _ paneId: PaneID) -> Void)? + // MARK: - Internal State internal var internalController: SplitViewController @@ -410,13 +414,15 @@ public final class BonsplitController { /// - orientation: Direction to split (horizontal = side-by-side, vertical = stacked). /// - tab: The tab to add to the new pane. /// - insertFirst: If true, insert the new pane first (left/top). Otherwise insert second (right/bottom). + /// - animate: If true (default), the new pane slides in with an entry animation. Pass false to skip animation (e.g. during session restore). /// - Returns: The new pane ID, or nil if vetoed by delegate. @discardableResult public func splitPane( _ paneId: PaneID? = nil, orientation: SplitOrientation, withTab tab: Tab, - insertFirst: Bool + insertFirst: Bool, + animate: Bool = true ) -> PaneID? { guard configuration.allowSplits else { return nil } @@ -446,7 +452,8 @@ public final class BonsplitController { PaneID(id: targetPaneId.id), orientation: orientation, tab: internalTab, - insertFirst: insertFirst + insertFirst: insertFirst, + animate: animate ) let newPaneId = focusedPaneId!