
IDE-style window docking: splits, tab groups, drag-to-dock with handles and translucent previews, floating windows, maximize, tab reordering, anchors, JSON layout persistence, pluggable renderer.
IDE-style window docking for Compose Multiplatform: splits, tab groups, drag-to-dock with docking handles, floating windows, maximize, and layout persistence.
A port of the concepts and algorithms of ModernDocking (Java Swing, MIT) to an idiomatic Compose API.
Status: early development. Published, but expect breaking API changes between 0.x releases.
| Artifact | Description | Targets |
|---|---|---|
com.seanproctor:compose-docking-core |
Layout engine, state model, docking machinery, unstyled renderer | JVM (desktop), Android, iOS, wasmJs |
com.seanproctor:compose-docking-material3 |
Material 3 renderer | same as core |
com.seanproctor:compose-docking-jewel |
Jewel (IntelliJ look-and-feel) renderer | JVM only |
Floating OS windows and cross-window drag are desktop (JVM) features. Everything else — splits, tabs, in-window drag-to-dock, tab reordering, maximize, persistence — is common code and works on every target.
Depend on a renderer; it exposes the core as an api dependency, so you do not need to
declare compose-docking-core yourself.
repositories {
// Compose itself resolves from Google's repository, so mavenCentral() alone
// is not enough - the transitive androidx artifacts will not be found.
google()
mavenCentral()
}
dependencies {
implementation("com.seanproctor:compose-docking-material3:0.2.0")
// ...or, for the IntelliJ look on desktop:
// implementation("com.seanproctor:compose-docking-jewel:0.2.0")
}In a multiplatform build these go in commonMain, except compose-docking-jewel, which
is JVM only and belongs in jvmMain (or desktopMain).
fun main() = application {
val state = rememberDockState(
initialLayout = {
dockLayout {
mainWindow {
dock("project")
dock("editor", target = "project", region = DockRegion.East, proportion = 0.75f)
dock("terminal", target = "editor", region = DockRegion.South, proportion = 0.3f)
dock("problems", target = "terminal", region = DockRegion.Center)
display("terminal")
}
}
},
) {
dockable("project", title = { "Project" }, options = DockableOptions(closable = false)) {
ProjectTree()
}
dockable("editor", title = { "Editor" }) { EditorPane() }
dockable("terminal", title = { "Terminal" }) { TerminalPane() }
dockable("problems", title = { "Problems" }) { ProblemsPane() }
dockable("todo", title = { "TODO" }) { TodoPane() }
}
// Restore on launch + debounced auto-save on every layout change.
rememberAutoPersist(state, FileLayoutStorage(File(configDir, "layout.json")))
Window(onCloseRequest = ::exitApplication, title = "My App") {
MaterialTheme {
Material3Docking {
registerDockingWindow(state) // desktop: cross-window drag + focus handling
DockArea(state, modifier = Modifier.fillMaxSize())
}
}
}
// One OS window per floating entry, plus the drag preview window.
MaterialTheme {
Material3Docking { FloatingDockWindows(state) }
}
}On web/mobile, drop the two desktop calls and just use DockArea inside Material3Docking.
IntUiTheme(isDark = true) {
JewelDocking {
registerDockingWindow(state)
DockArea(state, Modifier.fillMaxSize())
}
}The Jewel adapter only reads from JewelTheme, so it works under both the standalone
IntUiTheme and the IDE SwingBridgeTheme. Jewel 0.39+ ships Java 25 bytecode — run on
a JDK 25+ (JetBrains Runtime recommended).
DockState — the single source of truth. Holds an immutable layout value
(windows → tree of splits / tab groups / dockables), the dockable registry, and every
operation: dock, undock, close (with veto), maximize, moveToNewWindow,
show, … Observe with snapshotFlow { state.layout } or the
state.events flow.title/icon lambdas, per-dockable
DockableOptions (closable, floatable, docking style, anchor, …), optional
saveState/restoreState for persisted per-panel state, and the content composable.
Content keeps all internal state when re-docked within a window; hoist what must
survive a move between windows into rememberSaveable.DockingSettings(collapsedAnchorThickness = 24.dp) to draw an empty one as a strip
instead of a full-size pane, so the area gives its space back while it is empty and
still takes a drop, and add emptyAnchorVisibility = EmptyAnchorVisibility.WhileDragging
to hide the strip entirely until a drag starts - an empty area then costs nothing at all
until there is something to drop into it.kotlinx.serialization) with a version field and migration
hooks; unknown dockables in a saved layout are kept as placeholders and fill in when
registered (or are created on demand via a DockableResolver). Named layout snapshots
live on state.layouts.DockingRenderer is a slot interface; core builds all models and
gesture modifiers, adapters only draw. Implement it to match any design system, and
provide it via LocalDockingRenderer + LocalDockingTheme../gradlew :demo:demo-material3:run # Material 3, desktop
./gradlew :demo:demo-material3:wasmJsBrowserDevelopmentRun # Material 3, browser
./gradlew :demo:demo-jewel:run # IntelliJ look (needs JDK 25 toolchain)The docking model — drop-target precedence, region sensitivity, anchors, and layout persistence semantics — is ported from ModernDocking by Andrew Auclair (MIT).
IDE-style window docking for Compose Multiplatform: splits, tab groups, drag-to-dock with docking handles, floating windows, maximize, and layout persistence.
A port of the concepts and algorithms of ModernDocking (Java Swing, MIT) to an idiomatic Compose API.
Status: early development. Published, but expect breaking API changes between 0.x releases.
| Artifact | Description | Targets |
|---|---|---|
com.seanproctor:compose-docking-core |
Layout engine, state model, docking machinery, unstyled renderer | JVM (desktop), Android, iOS, wasmJs |
com.seanproctor:compose-docking-material3 |
Material 3 renderer | same as core |
com.seanproctor:compose-docking-jewel |
Jewel (IntelliJ look-and-feel) renderer | JVM only |
Floating OS windows and cross-window drag are desktop (JVM) features. Everything else — splits, tabs, in-window drag-to-dock, tab reordering, maximize, persistence — is common code and works on every target.
Depend on a renderer; it exposes the core as an api dependency, so you do not need to
declare compose-docking-core yourself.
repositories {
// Compose itself resolves from Google's repository, so mavenCentral() alone
// is not enough - the transitive androidx artifacts will not be found.
google()
mavenCentral()
}
dependencies {
implementation("com.seanproctor:compose-docking-material3:0.2.0")
// ...or, for the IntelliJ look on desktop:
// implementation("com.seanproctor:compose-docking-jewel:0.2.0")
}In a multiplatform build these go in commonMain, except compose-docking-jewel, which
is JVM only and belongs in jvmMain (or desktopMain).
fun main() = application {
val state = rememberDockState(
initialLayout = {
dockLayout {
mainWindow {
dock("project")
dock("editor", target = "project", region = DockRegion.East, proportion = 0.75f)
dock("terminal", target = "editor", region = DockRegion.South, proportion = 0.3f)
dock("problems", target = "terminal", region = DockRegion.Center)
display("terminal")
}
}
},
) {
dockable("project", title = { "Project" }, options = DockableOptions(closable = false)) {
ProjectTree()
}
dockable("editor", title = { "Editor" }) { EditorPane() }
dockable("terminal", title = { "Terminal" }) { TerminalPane() }
dockable("problems", title = { "Problems" }) { ProblemsPane() }
dockable("todo", title = { "TODO" }) { TodoPane() }
}
// Restore on launch + debounced auto-save on every layout change.
rememberAutoPersist(state, FileLayoutStorage(File(configDir, "layout.json")))
Window(onCloseRequest = ::exitApplication, title = "My App") {
MaterialTheme {
Material3Docking {
registerDockingWindow(state) // desktop: cross-window drag + focus handling
DockArea(state, modifier = Modifier.fillMaxSize())
}
}
}
// One OS window per floating entry, plus the drag preview window.
MaterialTheme {
Material3Docking { FloatingDockWindows(state) }
}
}On web/mobile, drop the two desktop calls and just use DockArea inside Material3Docking.
IntUiTheme(isDark = true) {
JewelDocking {
registerDockingWindow(state)
DockArea(state, Modifier.fillMaxSize())
}
}The Jewel adapter only reads from JewelTheme, so it works under both the standalone
IntUiTheme and the IDE SwingBridgeTheme. Jewel 0.39+ ships Java 25 bytecode — run on
a JDK 25+ (JetBrains Runtime recommended).
DockState — the single source of truth. Holds an immutable layout value
(windows → tree of splits / tab groups / dockables), the dockable registry, and every
operation: dock, undock, close (with veto), maximize, moveToNewWindow,
show, … Observe with snapshotFlow { state.layout } or the
state.events flow.title/icon lambdas, per-dockable
DockableOptions (closable, floatable, docking style, anchor, …), optional
saveState/restoreState for persisted per-panel state, and the content composable.
Content keeps all internal state when re-docked within a window; hoist what must
survive a move between windows into rememberSaveable.DockingSettings(collapsedAnchorThickness = 24.dp) to draw an empty one as a strip
instead of a full-size pane, so the area gives its space back while it is empty and
still takes a drop, and add emptyAnchorVisibility = EmptyAnchorVisibility.WhileDragging
to hide the strip entirely until a drag starts - an empty area then costs nothing at all
until there is something to drop into it.kotlinx.serialization) with a version field and migration
hooks; unknown dockables in a saved layout are kept as placeholders and fill in when
registered (or are created on demand via a DockableResolver). Named layout snapshots
live on state.layouts.DockingRenderer is a slot interface; core builds all models and
gesture modifiers, adapters only draw. Implement it to match any design system, and
provide it via LocalDockingRenderer + LocalDockingTheme../gradlew :demo:demo-material3:run # Material 3, desktop
./gradlew :demo:demo-material3:wasmJsBrowserDevelopmentRun # Material 3, browser
./gradlew :demo:demo-jewel:run # IntelliJ look (needs JDK 25 toolchain)The docking model — drop-target precedence, region sensitivity, anchors, and layout persistence semantics — is ported from ModernDocking by Andrew Auclair (MIT).