Bonsplit is a custom tab bar and layout split library for macOS apps. Enjoy out of the box 120fps animations, drag-and-drop reordering, SwiftUI support & keyboard navigation.
.package(url: "https://github.com/almonk/bonsplit.git", from: "1.1.1")
### Features
Create tabs with optional icons and dirty indicators. Target specific panes or use the focused pane.
let tabId = controller.createTab(title: "Document.swift",icon: "swift",isDirty: false,inPane: paneId)
Create tabs with optional icons and dirty indicators. Target specific panes or use the focused pane.
let tabId = controller.createTab(title: "Document.swift",icon: "swift",isDirty: false,inPane: paneId)
Split any pane horizontally or vertically. New panes are empty by default, giving you full control.
// Split focused pane horizontallylet newPaneId = controller.splitPane(orientation: .horizontal)// Split with a tab already in the new panecontroller.splitPane(orientation: .vertical,withTab: Tab(title: "New", icon: "doc"))
Split any pane horizontally or vertically. New panes are empty by default, giving you full control.
// Split focused pane horizontallylet newPaneId = controller.splitPane(orientation: .horizontal)// Split with a tab already in the new panecontroller.splitPane(orientation: .vertical,withTab: Tab(title: "New", icon: "doc"))
Update tab properties at any time. Changes animate smoothly.
// Mark document as modifiedcontroller.updateTab(tabId, isDirty: true)// Rename tabcontroller.updateTab(tabId, title: "NewName.swift")// Change iconcontroller.updateTab(tabId, icon: "doc.text")
Update tab properties at any time. Changes animate smoothly.
// Mark document as modifiedcontroller.updateTab(tabId, isDirty: true)// Rename tabcontroller.updateTab(tabId, title: "NewName.swift")// Change iconcontroller.updateTab(tabId, icon: "doc.text")
Programmatically navigate between panes using directional navigation.
// Move focus between panescontroller.navigateFocus(direction: .left)controller.navigateFocus(direction: .right)controller.navigateFocus(direction: .up)controller.navigateFocus(direction: .down)// Or focus a specific panecontroller.focusPane(paneId)
Programmatically navigate between panes using directional navigation.
// Move focus between panescontroller.navigateFocus(direction: .left)controller.navigateFocus(direction: .right)controller.navigateFocus(direction: .up)controller.navigateFocus(direction: .down)// Or focus a specific panecontroller.focusPane(paneId)
Two-way synchronization with external programs. Query geometry, receive change notifications, and update dividers programmatically.
// Query current geometry with pixel coordinateslet snapshot = controller.layoutSnapshot()for pane in snapshot.panes {print(pane.frame.width, pane.frame.height)}// Set divider position from external sourcecontroller.setDividerPosition(0.3, forSplit: splitId, fromExternal: true)
Two-way synchronization with external programs. Query geometry, receive change notifications, and update dividers programmatically.
// Query current geometry with pixel coordinateslet snapshot = controller.layoutSnapshot()for pane in snapshot.panes {print(pane.frame.width, pane.frame.height)}// Set divider position from external sourcecontroller.setDividerPosition(0.3, forSplit: splitId, fromExternal: true)
### Read this, agents...
Complete reference for all Bonsplit classes, methods, and configuration options.
The main controller for managing tabs and panes. Create an instance and pass it to BonsplitView.
Implement this protocol to receive callbacks about tab bar events. All methods have default implementations and are optional.
Types used for geometry queries and external synchronization. All types are Codable and Sendable for easy serialization and thread safety.
A complete snapshot of the current layout with pixel coordinates.
public struct LayoutSnapshot: Codable, Sendable {public let containerFrame: PixelRect // Container bounds in screen coordspublic let panes: [PaneGeometry] // All pane geometriespublic let focusedPaneId: String? // Currently focused panepublic let timestamp: TimeInterval // Snapshot time}
Geometry information for a single pane.
public struct PaneGeometry: Codable, Sendable {public let paneId: String // Pane UUID stringpublic let frame: PixelRect // Pixel coordinatespublic let selectedTabId: String? // Selected tab UUIDpublic let tabIds: [String] // All tab UUIDs}
A rectangle in pixel coordinates for external consumption.
public struct PixelRect: Codable, Sendable {public let x: Doublepublic let y: Doublepublic let width: Doublepublic let height: Double}
Recursive tree representation of the split hierarchy.
public enum ExternalTreeNode: Codable, Sendable {case pane(ExternalPaneNode) // Leaf node: a panecase split(ExternalSplitNode) // Branch node: a split with two children}
A split node in the tree with orientation and divider position.
public struct ExternalSplitNode: Codable, Sendable {public let id: String // Split UUID stringpublic let orientation: String // "horizontal" or "vertical"public let dividerPosition: Double // 0.0-1.0public let first: ExternalTreeNode // First childpublic let second: ExternalTreeNode // Second child}
A pane node in the tree with frame and tab information.
public struct ExternalPaneNode: Codable, Sendable {public let id: String // Pane UUID stringpublic let frame: PixelRect // Pixel coordinatespublic let tabs: [ExternalTab] // Tab infopublic let selectedTabId: String? // Selected tab UUID}
Tab information for external consumption.
public struct ExternalTab: Codable, Sendable {public let id: String // Tab UUID stringpublic let title: String // Tab title}
Configure behavior and appearance. Pass to BonsplitController on initialization.
allowSplitsBoolEnable split buttons and drag-to-split
Default: true
allowCloseTabsBoolShow close buttons on tabs
Default: true
allowCloseLastPaneBoolAllow closing the last remaining pane
Default: false
allowTabReorderingBoolEnable drag-to-reorder tabs within a pane
Default: true
allowCrossPaneTabMoveBoolEnable moving tabs between panes via drag
Default: true
autoCloseEmptyPanesBoolAutomatically close panes when their last tab is closed
Default: true
contentViewLifecycleContentViewLifecycleHow tab content views are managed when switching tabs
Default: .recreateOnSwitch
newTabPositionNewTabPositionWhere new tabs are inserted in the tab list
Default: .current
let config = BonsplitConfiguration(allowSplits: true,allowCloseTabs: true,allowCloseLastPane: false,autoCloseEmptyPanes: true,contentViewLifecycle: .keepAllAlive,newTabPosition: .current)let controller = BonsplitController(configuration: config)
Controls how tab content views are managed when switching between tabs.
| Mode | Memory | State | Use Case |
|---|---|---|---|
.recreateOnSwitch | Low | None | Simple content |
.keepAllAlive | Higher | Full | Complex views, forms |
Controls where new tabs are inserted in the tab list.
| Mode | Behavior |
|---|---|
.current | Insert after currently focused tab, or at end if none |
.end | Always insert at the end of the tab list |
tabBarHeightCGFloatHeight of the tab bar
Default: 33
tabMinWidthCGFloatMinimum width of a tab
Default: 140
tabMaxWidthCGFloatMaximum width of a tab
Default: 220
tabSpacingCGFloatSpacing between tabs
Default: 0
minimumPaneWidthCGFloatMinimum width of a pane
Default: 100
minimumPaneHeightCGFloatMinimum height of a pane
Default: 100
showSplitButtonsBoolShow split buttons in the tab bar
Default: true
animationDurationDoubleDuration of animations in seconds
Default: 0.15
enableAnimationsBoolEnable or disable all animations
Default: true
.defaultBonsplitConfigurationDefault configuration with all features enabled
.singlePaneBonsplitConfigurationSingle pane mode with splits disabled
.readOnlyBonsplitConfigurationRead-only mode with all modifications disabled