A full-featured docking (dock panel) library for Slint — C++, Rust, and Python
日本語 README はこちら / Japanese README
Slint has no built-in docking system. pbSlintDock fills that gap with a complete,
IDE-grade docking experience: nested splits, tab groups, drag & drop re-docking
with drop indicators, native floating windows, and layout persistence — all
driven by a GUI-independent C++ layout engine and rendered by plain .slint
components.
Real capture of the demo: tab merge → split → new column → splitter resize → tab reorder. Recorded frame-by-frame from the running app.
Drag & drop with drop-zone overlay, 5-way indicator and drag ghost:
New to the library? Start with the Integration Guide — a step-by-step walkthrough of adding pbSlintDock to your own Slint application, followed by the API Reference.
- Arbitrary nested splits — horizontal / vertical splits of any depth, n-ary (a split can hold any number of children), automatically normalized (no degenerate single-child splits, same-direction splits merge).
- Tab groups — every leaf is a tab group. Active-tab highlight, per-tab close buttons, hover states, CJK-aware tab widths with eliding.
- Drag & drop re-docking
- Drag a tab (or a whole group by its empty tab-bar area) anywhere.
- 5-way drop indicator (center / left / right / top / bottom) centred on the hovered group, plus window-edge root zones.
- Live drop preview rectangle and a drag ghost chip that follows the cursor — optionally as its own top-level window, so the ghost can leave the source window and travel across the desktop.
- Drop on a tab bar to insert at an exact tab position.
- Tab reordering — drag a tab along its own tab bar to reorder in place.
- Floating windows
- Drag a tab out of the window → it detaches into a real, frameless OS window (multi-monitor friendly, clamped to the monitor work area).
- Floating windows are full dock hosts: they have their own tab bar and can hold several panels. A float holding a single panel shows only its title bar — the redundant one-tab tab bar is hidden (configurable).
- Drag a floating window by its title bar back over the main window to re-dock it — with the same overlay/zone UX.
- 8-direction resize grips, close button, automatic window title.
- Splitter resizing — hover-highlighted splitters with min-size constraints propagated through the tree; ratios are preserved on resize.
- Layout persistence — one-call JSON save/restore including floating windows. Panels are referenced by string ids, so layouts survive registration-order changes; unknown ids are dropped gracefully.
- Programmatic API — dock / float / close / open / activate panels from C++, Rust, or Python at any time. All bindings use the same engine and JSON persistence format.
- Theming — every color lives in the
PbDockThemeglobal (dark theme by default). - Engine is GUI-independent — the layout tree, solver, hit-testing and
serialization compile without Slint and are covered by unit tests
(
tests/test_layout.cpp). The demo has a built-in UI self-test (--auto) that drives the real UI with synthetic pointer events.
Slint is declarative: components cannot recurse, cannot be re-parented at
runtime, and z must be a literal. A docking system needs none of those if
you flip the design around:
┌───────────────┐ flat, absolutely-positioned view-models ┌──────────────┐
│ C++ engine │ ───────────────────────────────────────────▶│ .slint view │
│ (layout tree, │ [GroupVm] [TabVm] [SplitterVm] [PanelVm] │ (for-loops, │
│ solver, drag │ ◀─────────────────────────────────────────── │ TouchAreas) │
│ state machine)│ raw input events (press/move/…) └──────────────┘
└───────────────┘
The C++ side owns the split tree and computes flat rectangles for every
group, tab, splitter and panel. The .slint side renders those rectangles
with for loops and reports raw pointer input back. Because geometry flows
through reactive Slint models, everything re-layouts live — and because your
panels are declared once and only moved by geometry, panel state (scroll
positions, text, widgets) survives every dock operation inside a window.
| Language | Package / entry point | UI integration |
|---|---|---|
| C++ | pbslintdock::core, SlintBridge |
Full native bridge and demo |
| Rust | bindings/rust crate |
Safe engine API and full Slint demo including float windows and drag ghost |
| Python | bindings/python wheel |
DockManager + SlintBridge, including native float-window support on Windows |
See the Rust and Python guide for build, packaging, API mapping, and complete examples.
- Slint 1.17+ C++ SDK (
find_package(Slint)) - CMake 3.21+, a C++20 compiler (verified with MSVC 2022 on Windows 11)
- Rust 1.85+ for the Rust crate
- Python 3.12+ and
slint==1.17.1b2for the Python package - Windows is the primary target (global cursor tracking and work-area clamping use Win32; both have graceful fallbacks on other platforms)
third_party/ is not committed. Fetch the official binary SDK once:
- Download
Slint-cpp-<version>-win64-MSVC-AMD64.exefrom the Slint releases page. - Extract/install it to
third_party/slint(silent install works:Slint-cpp-….exe /S /D=<repo>\third_party\slint).
Any other install location works too — pass it via CMAKE_PREFIX_PATH.
cmake -S . -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
build\bin\Release\pbslintdock_demo.exeDemo flags:
| flag | effect |
|---|---|
--shot <file.bmp> |
render, save a screenshot of the main window, quit |
--auto |
run the built-in UI self-test (synthetic pointer events), write %TEMP%\autotest_result.txt, exit code 0 on success |
--record <dir> |
play the scripted docking showcase and write one BMP frame per tick into <dir> (this is how docs/docking.gif was made) |
Engine unit tests: build\Release\pbslintdock_tests.exe.
add_subdirectory(pbSlintDock) # provides pbslintdock::core
add_executable(my_app WIN32 main.cpp)
slint_target_sources(my_app app.slint
LIBRARY_PATHS pbslintdock=${PBSLINTDOCK_UI_DIR}/pbslintdock.slint)
target_link_libraries(my_app PRIVATE pbslintdock::core Slint::Slint)import { DockHost, DockPanel, FloatWindowFrame, DragGhostWindow,
PbDock, PbDockTheme, PanelVm, TabVm, GroupVm, SplitterVm }
from "@pbslintdock";
// re-export so the generated C++ header exposes the bridge types
export { PbDock, PbDockTheme, PanelVm, TabVm, GroupVm, SplitterVm,
DragGhostWindow }
// declare your panels once; the library drives their geometry
component PanelSet inherits Rectangle {
background: transparent;
DockPanel { panel-id: "explorer"; MyExplorerView { } }
DockPanel { panel-id: "console"; MyConsoleView { } }
}
export component MainWindow inherits Window {
DockHost { PanelSet { } }
}
// floating windows embed the same panel set
export component MyFloatWindow inherits FloatWindowFrame {
PanelSet { }
}#include "app.h" // slint-generated
#include "pbslintdock/slint_bridge.h" // include AFTER the generated header
int main() {
pbdock::DockManager dock;
dock.add_panel({ "explorer", "Explorer" });
dock.add_panel({ "console", "Console" });
dock.dock_panel_root("explorer", pbdock::DropZone::RootLeft, 0.25f);
dock.dock_panel("console", "explorer", pbdock::DropZone::Bottom);
pbdock::SlintBridge bridge(dock);
bridge.set_float_factory<MyFloatWindow>(); // before attach_main
bridge.set_ghost_window<DragGhostWindow>(); // before attach_main
auto app = MainWindow::create();
bridge.attach_main(app);
app->run();
}That is the whole integration: everything else (drag & drop, floating windows, overlays, splitters) is handled by the library.
set_ghost_window<DragGhostWindow>() is optional: it promotes the drag ghost
to its own top-level window so it can follow the cursor outside the source
window. Omit it and the ghost is drawn inside the DockHost as before,
clipped at the window edge.
For the long form — required call order, initial layout recipes, float factories, state sharing across windows, and troubleshooting — see the Integration Guide.
| call | effect |
|---|---|
add_panel({id, title, closable, min_w, min_h}) |
register a panel (starts closed) |
dock_panel(id, target_id, DropZone, ratio) |
dock relative to another panel |
dock_panel_root(id, DropZone::Root*, ratio) |
dock at a window edge |
float_panel(id, x, y, w, h) |
detach into a floating window |
close_panel(id) / open_panel(id) / is_open(id) |
visibility control (reopen remembers the last neighbour) |
activate_panel(id) |
select the panel's tab |
save_layout() → std::string |
serialize everything (JSON) |
restore_layout(json) |
restore, recreating floating windows |
metrics() |
tune tab bar height, splitter thickness and grab padding, tab widths, … |
DropZone: Center, Left, Right, Top, Bottom, RootLeft, RootRight, RootTop, RootBottom, TabBar.
Override any color/metric of the PbDockTheme global from .slint or C++:
app->global<PbDockTheme>().set_accent(slint::Color::from_rgb_uint8(0xff, 0x99, 0x00));Slint cannot move a live component between windows. When a panel floats (or
re-docks from a float), its content component in the target window is a fresh
instance. Keep the state you care about in Slint globals and mirror it in
C++ — the demo (demo/main.cpp, DemoState) shows a compact pattern that
syncs state across every window, so text typed into a panel survives
float/dock round-trips.
include/pbslintdock/ public headers (engine + header-only Slint bridge)
src/ engine implementation (no Slint dependency)
ui/pbslintdock.slint the view layer (import as @pbslintdock)
demo/ demo application (also the UI self-test)
tests/ engine unit tests
docs/ integration guide, API reference, screenshots
| Document | Contents |
|---|---|
| Integration Guide (日本語) | step-by-step setup, from CMake to a complete working example |
| API Reference (日本語) | every public type and method, the PbDockTheme / Metrics field lists, the layout JSON schema |
MIT — see LICENSE.


