Bonsplit - Native macOS Tab Bar with Split Panes

5 min read Original article ↗

### Features

Configurable & Observable

Create Tabs

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

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 Panes

Split any pane horizontally or vertically. New panes are empty by default, giving you full control.

// Split focused pane horizontally

let newPaneId = controller.splitPane(

orientation: .horizontal

)

// Split with a tab already in the new pane

controller.splitPane(

orientation: .vertical,

withTab: Tab(title: "New", icon: "doc")

)

Split Panes

Split any pane horizontally or vertically. New panes are empty by default, giving you full control.

// Split focused pane horizontally

let newPaneId = controller.splitPane(

orientation: .horizontal

)

// Split with a tab already in the new pane

controller.splitPane(

orientation: .vertical,

withTab: Tab(title: "New", icon: "doc")

)

Update Tab State

Update tab properties at any time. Changes animate smoothly.

// Mark document as modified

controller.updateTab(tabId, isDirty: true)

// Rename tab

controller.updateTab(tabId, title: "NewName.swift")

// Change icon

controller.updateTab(tabId, icon: "doc.text")

Update Tab State

Update tab properties at any time. Changes animate smoothly.

// Mark document as modified

controller.updateTab(tabId, isDirty: true)

// Rename tab

controller.updateTab(tabId, title: "NewName.swift")

// Change icon

controller.updateTab(tabId, icon: "doc.text")

Navigate Focus

Programmatically navigate between panes using directional navigation.

// Move focus between panes

controller.navigateFocus(direction: .left)

controller.navigateFocus(direction: .right)

controller.navigateFocus(direction: .up)

controller.navigateFocus(direction: .down)

// Or focus a specific pane

controller.focusPane(paneId)

Navigate Focus

Programmatically navigate between panes using directional navigation.

// Move focus between panes

controller.navigateFocus(direction: .left)

controller.navigateFocus(direction: .right)

controller.navigateFocus(direction: .up)

controller.navigateFocus(direction: .down)

// Or focus a specific pane

controller.focusPane(paneId)

Geometry Sync

Two-way synchronization with external programs. Query geometry, receive change notifications, and update dividers programmatically.

// Query current geometry with pixel coordinates

let snapshot = controller.layoutSnapshot()

for pane in snapshot.panes {

print(pane.frame.width, pane.frame.height)

}

// Set divider position from external source

controller.setDividerPosition(0.3, forSplit: splitId, fromExternal: true)

Geometry Sync

Two-way synchronization with external programs. Query geometry, receive change notifications, and update dividers programmatically.

// Query current geometry with pixel coordinates

let snapshot = controller.layoutSnapshot()

for pane in snapshot.panes {

print(pane.frame.width, pane.frame.height)

}

// Set divider position from external source

controller.setDividerPosition(0.3, forSplit: splitId, fromExternal: true)

### Read this, agents...

API Reference

Complete reference for all Bonsplit classes, methods, and configuration options.

BonsplitController

The main controller for managing tabs and panes. Create an instance and pass it to BonsplitView.

Tab Operations

Split Operations

Focus Management

Query Methods

Geometry & Synchronization

BonsplitDelegate

Implement this protocol to receive callbacks about tab bar events. All methods have default implementations and are optional.

Tab Callbacks

Pane Callbacks

Geometry Callbacks

Geometry Types

Types used for geometry queries and external synchronization. All types are Codable and Sendable for easy serialization and thread safety.

LayoutSnapshot

A complete snapshot of the current layout with pixel coordinates.

public struct LayoutSnapshot: Codable, Sendable {

public let containerFrame: PixelRect // Container bounds in screen coords

public let panes: [PaneGeometry] // All pane geometries

public let focusedPaneId: String? // Currently focused pane

public let timestamp: TimeInterval // Snapshot time

}

PaneGeometry

Geometry information for a single pane.

public struct PaneGeometry: Codable, Sendable {

public let paneId: String // Pane UUID string

public let frame: PixelRect // Pixel coordinates

public let selectedTabId: String? // Selected tab UUID

public let tabIds: [String] // All tab UUIDs

}

PixelRect

A rectangle in pixel coordinates for external consumption.

public struct PixelRect: Codable, Sendable {

public let x: Double

public let y: Double

public let width: Double

public let height: Double

}

ExternalTreeNode

Recursive tree representation of the split hierarchy.

public enum ExternalTreeNode: Codable, Sendable {

case pane(ExternalPaneNode) // Leaf node: a pane

case split(ExternalSplitNode) // Branch node: a split with two children

}

ExternalSplitNode

A split node in the tree with orientation and divider position.

public struct ExternalSplitNode: Codable, Sendable {

public let id: String // Split UUID string

public let orientation: String // "horizontal" or "vertical"

public let dividerPosition: Double // 0.0-1.0

public let first: ExternalTreeNode // First child

public let second: ExternalTreeNode // Second child

}

ExternalPaneNode

A pane node in the tree with frame and tab information.

public struct ExternalPaneNode: Codable, Sendable {

public let id: String // Pane UUID string

public let frame: PixelRect // Pixel coordinates

public let tabs: [ExternalTab] // Tab info

public let selectedTabId: String? // Selected tab UUID

}

ExternalTab

Tab information for external consumption.

public struct ExternalTab: Codable, Sendable {

public let id: String // Tab UUID string

public let title: String // Tab title

}

BonsplitConfiguration

Configure behavior and appearance. Pass to BonsplitController on initialization.

allowSplits

Bool

Enable split buttons and drag-to-split

Default: true

allowCloseTabs

Bool

Show close buttons on tabs

Default: true

allowCloseLastPane

Bool

Allow closing the last remaining pane

Default: false

allowTabReordering

Bool

Enable drag-to-reorder tabs within a pane

Default: true

allowCrossPaneTabMove

Bool

Enable moving tabs between panes via drag

Default: true

autoCloseEmptyPanes

Bool

Automatically close panes when their last tab is closed

Default: true

contentViewLifecycle

ContentViewLifecycle

How tab content views are managed when switching tabs

Default: .recreateOnSwitch

newTabPosition

NewTabPosition

Where new tabs are inserted in the tab list

Default: .current

Example

let config = BonsplitConfiguration(

allowSplits: true,

allowCloseTabs: true,

allowCloseLastPane: false,

autoCloseEmptyPanes: true,

contentViewLifecycle: .keepAllAlive,

newTabPosition: .current

)

let controller = BonsplitController(configuration: config)

Content View Lifecycle

Controls how tab content views are managed when switching between tabs.

ModeMemoryStateUse Case
.recreateOnSwitchLowNoneSimple content
.keepAllAliveHigherFullComplex views, forms

New Tab Position

Controls where new tabs are inserted in the tab list.

ModeBehavior
.currentInsert after currently focused tab, or at end if none
.endAlways insert at the end of the tab list

Appearance

tabBarHeight

CGFloat

Height of the tab bar

Default: 33

tabMinWidth

CGFloat

Minimum width of a tab

Default: 140

tabMaxWidth

CGFloat

Maximum width of a tab

Default: 220

tabSpacing

CGFloat

Spacing between tabs

Default: 0

minimumPaneWidth

CGFloat

Minimum width of a pane

Default: 100

minimumPaneHeight

CGFloat

Minimum height of a pane

Default: 100

showSplitButtons

Bool

Show split buttons in the tab bar

Default: true

animationDuration

Double

Duration of animations in seconds

Default: 0.15

enableAnimations

Bool

Enable or disable all animations

Default: true

Presets

.default

BonsplitConfiguration

Default configuration with all features enabled

.singlePane

BonsplitConfiguration

Single pane mode with splits disabled

.readOnly

BonsplitConfiguration

Read-only mode with all modifications disabled