Complete C++ public surface of pbSlintDock. Rust and Python expose the same
engine operations; see the language bindings guide and
include/pbslintdock/c_api.h.
日本語版はこちら / Japanese · Integration Guide · ← README
Everything lives in namespace pbdock. Headers are under
include/pbslintdock/. The engine headers have no Slint dependency; only
slint_bridge.h does.
| Header | Contents |
|---|---|
dock_types.h |
Rect, Point, PanelDef, Metrics, DropZone, Dir |
dock_manager.h |
DockManager — the main API |
slint_bridge.h |
SlintBridge — Slint glue (header-only) |
serialization.h |
layout save/restore primitives |
layout_tree.h |
the split tree (advanced) |
layout_solver.h |
the geometry solver (advanced) |
dock_platform.h |
cursor and work-area queries |
Most applications only need DockManager, SlintBridge and PanelDef.
using PanelId = std::string;Your stable, human-chosen key for a panel. Used in .slint
(DockPanel { panel-id: … }), in every DockManager call, and in saved
layouts.
struct PanelDef {
PanelId id;
std::string title;
bool closable = true;
float min_w = 100;
float min_h = 80;
};| Field | Meaning |
|---|---|
id |
must match the panel-id in your .slint; exact, case-sensitive |
title |
shown on the tab and on a float window's title bar |
closable |
false hides the tab's close button (the panel can still be closed from code) |
min_w, min_h |
real layout constraints, propagated through the split tree and enforced by the solver and by splitter dragging |
Aggregate-initialized in practice:
dock.add_panel({ "editor", "Editor", true, 200, 120 });enum class DropZone {
None, Center, Left, Right, Top, Bottom,
RootLeft, RootRight, RootTop, RootBottom, TabBar,
};| Value | Effect when docking |
|---|---|
Center |
add as a tab in the target's group |
Left / Right / Top / Bottom |
split the target group, inserting on that side |
RootLeft / RootRight / RootTop / RootBottom |
split the whole window, inserting at that edge |
TabBar |
insert at an exact tab position (used by drag & drop) |
None |
no drop target |
The integer values reported to .slint through PbDock.overlay-zone are, in
order: 0 none, 1 center, 2 left, 3 right, 4 top, 5 bottom,
6 root-left, 7 root-right, 8 root-top, 9 root-bottom, 10 tabbar.
enum class Dir { Horizontal, Vertical };The orientation of a split node. Horizontal lays children out left-to-right.
struct Rect {
float x = 0, y = 0, w = 0, h = 0;
bool contains(float px, float py) const;
float right() const; // x + w
float bottom() const; // y + h
};
struct Point { float x = 0, y = 0; };Rectangles are window-local unless a function explicitly says screen.
Layout sizes, distinct from colours (which live in PbDockTheme). Reachable
as a mutable reference via DockManager::metrics().
struct Metrics {
float splitter_thickness = 2;
float splitter_grab_pad = 4; // hit area widened on each side
float tabbar_height = 28;
bool float_hides_single_tabbar = true;
bool hide_single_tabbar = false; // internal, set per context
float tab_min_width = 64;
float tab_max_width = 220;
float tab_base_width = 42; // padding + close button allowance
float tab_char_width = 8.5f; // approx. latin char at 13px
float tab_cjk_width = 15.0f;
float border = 1; // currently unused
};| Field | Meaning |
|---|---|
splitter_thickness |
drawn width of a splitter. The default 2 is a hairline |
splitter_grab_pad |
the splitter's hit area is widened by this much on each side, so a 2 px hairline is still a comfortable ~10 px grab target. Pushed to .slint as PbDock.splitter-grab-pad on every model push |
tabbar_height |
height of a tab bar. The default 28 matches the float-window title bar |
float_hides_single_tabbar |
when true (default), a floating window whose group holds a single tab draws no tab bar — the title bar already names that panel. The main window is never affected: it keeps its tab bar even with one tab. Set to false to always draw the tab bar |
hide_single_tabbar |
internal. DockManager::resolve sets it per context from float_hides_single_tabbar; do not assign it yourself |
border |
not referenced by the current implementation |
Because a single-tab floating window has no tab bar, it also offers no tab-bar drop zone. Dropping onto its centre still merges the dragged panel into that group as a tab.
Tab widths are computed from the title text: latin characters count
tab_char_width, CJK characters count tab_cjk_width, plus tab_base_width,
then clamped to [tab_min_width, tab_max_width] with eliding. If you change
PbDockTheme.font-size, scale tab_char_width and tab_cjk_width to match.
constexpr int kMainWindow = 0; // floating windows get ids >= 1Window ids. The main window is always 0; each float gets a unique id from 1
upwards.
The engine. Owns the split tree for every window, runs the solver, drives the drag state machine, and produces the flat rectangles the view renders.
DockManager();
~DockManager();int add_panel(const PanelDef &def); // returns the slot index
int slot_of(const PanelId &id) const; // -1 if unknown
const PanelDef &panel(int slot) const;
int panel_count() const;
Metrics &metrics(); // mutableadd_panel returns an internal slot index. Panels start closed — a
registered panel is invisible until docked. Prefer string ids over slots in
your own code; slots are an implementation detail that saved layouts
deliberately avoid.
void dock_panel(const PanelId &id, const PanelId &target, DropZone zone,
float ratio = 0.5f);
void dock_panel_root(const PanelId &id, DropZone root_zone,
float ratio = 0.25f);
void close_panel(const PanelId &id);
void open_panel(const PanelId &id);
bool is_open(const PanelId &id) const;
void activate_panel(const PanelId &id);
void float_panel(const PanelId &id, float sx, float sy,
float w = 320, float h = 240);| Call | Behaviour |
|---|---|
dock_panel |
dock id relative to target. ratio is the fraction given to the newly inserted panel |
dock_panel_root |
dock id at a window edge; pass a Root* zone. On an empty layout the panel simply becomes the root |
close_panel |
remove from the layout, remembering its neighbour |
open_panel |
re-dock next to the neighbour it last had |
is_open |
currently in a layout (main window or a float) |
activate_panel |
select the panel's tab within its group |
float_panel |
detach into a floating window at screen position sx, sy with size w × h |
Empty groups and degenerate single-child splits are pruned automatically, and same-direction nested splits are merged, so the tree never accumulates artefacts from repeated operations.
Normally driven by SlintBridge; call these directly only if you are writing
your own view layer.
void set_view_size(int window, float w, float h);
void set_window_origin(int window, float sx, float sy);
const LayoutOutput &output(int window) const;
std::vector<int> float_ids() const;
Rect float_geometry(int fid) const; // screen coords, incl. titlebar
float float_titlebar_height() const; // 28 by defaultAll coordinates are window-local. SlintBridge routes these for you.
void on_tab_pressed(int window, int tab_index, float x, float y);
void on_group_bar_pressed(int window, int group_index, float x, float y);
void on_pointer_moved(int window, float x, float y);
void on_pointer_released(int window, float x, float y);
void on_tab_close(int window, int tab_index);
void on_splitter_pressed(int window, int splitter_index, float pos_px);
void on_splitter_dragged(int window, int splitter_index, float pos_px);
void on_float_window_drag(int fid, float dx, float dy, float sx, float sy);
void on_float_window_drag_end(int fid);
void on_float_window_close(int fid);
void on_float_window_resize(int fid, int edge_x, int edge_y,
float dx, float dy);on_group_bar_pressed starts a drag of the whole group (grabbing the empty
part of a tab bar). on_float_window_close also closes the panels the float
contained. In on_float_window_resize, edge_x and edge_y are each -1,
0 or +1, selecting which edge the grip belongs to — that is how all eight
grips map onto one call.
const OverlayState &overlay() const;
const DragVisual &drag_visual() const;struct OverlayState {
bool active = false;
int window = kMainWindow;
DropZone zone = DropZone::None;
Rect preview; // drop preview rectangle (window-local)
Rect group_rect; // hovered group rect — the indicator anchor
int tab_index = -1;
};
struct DragVisual {
bool active = false;
int window = kMainWindow;
float x = 0, y = 0; // window-local ghost position
float sx = 0, sy = 0; // the same point in logical screen coordinates,
// for a ghost that leaves the source window
std::string title;
};std::string save_layout() const;
bool restore_layout(std::string_view json);restore_layout cancels any drag in flight, destroys existing floats, rebuilds
the main tree and all floats, and de-duplicates any slot that appears more than
once. It returns false on malformed input, leaving the current layout
untouched — so always keep a default-layout fallback. See
Layout JSON schema.
void set_on_layout_changed(std::function<void()> f);
void set_on_float_created(std::function<void(int fid)> f);
void set_on_float_destroyed(std::function<void(int fid)> f);
void set_on_float_geometry_changed(std::function<void(int fid)> f);
void set_work_area_provider(
std::function<bool(float sx, float sy, Rect &out)> f);SlintBridge::attach_main installs its own handlers for the first four, so set
yours before attaching only if you intend to replace the bridge's behaviour;
otherwise use them in a custom view layer.
set_work_area_provider overrides how the monitor work area is determined for
float-window clamping. Return false to indicate "unknown". The default uses
Win32 on Windows and falls back gracefully elsewhere — supply your own if you
need custom multi-monitor behaviour.
Header-only. Include it after the slint-generated header — it is templated over your generated component types.
#include "app.h" // slint-generated
#include "pbslintdock/slint_bridge.h"class SlintBridge {
public:
explicit SlintBridge(DockManager &dm);
template <typename App>
void attach_main(slint::ComponentHandle<App> app);
template <typename FloatComponent>
void set_float_factory();
template <typename FloatComponent, typename Fn>
void set_float_factory(Fn customize); // customize(handle, int fid)
template <typename GhostComponent>
void set_ghost_window();
void push_all();
};| Method | Behaviour |
|---|---|
attach_main(app) |
installs every PbDock callback, registers the layout-changed and float-lifecycle hooks on the DockManager, materializes floats that already exist in the layout, and pushes the models once |
set_float_factory<T>() |
registers T as the float-window type. Must be called before attach_main |
set_float_factory<T>(fn) |
same, plus fn(handle, fid) is invoked once per created window before it is shown — use it for per-window wiring such as theming or state sync |
set_ghost_window<T>() |
routes the drag ghost into its own top-level window of type T, so it is not clipped by the drag source window. Must be called before attach_main |
push_all() |
force a full model refresh. Rarely needed; the bridge pushes automatically on layout change |
FloatComponent must inherit FloatWindowFrame and contain your DockPanel
set.
GhostComponent must inherit DragGhostWindow, and your .slint must import
and re-export DragGhostWindow so the generated header declares it:
bridge.set_ghost_window<DragGhostWindow>(); // before attach_mainIf you never call set_ghost_window, the ghost is drawn inside the DockHost
of the drag source window, as before, and is clipped at that window's edge.
Installing a ghost window takes over: PbDock.ghost-active then stays false
in every window, and the ghost window is positioned from DragVisual::sx/sy.
inline void bridge_debug(const char *fmt, ...);Active only when the environment variable PBSLINTDOCK_DEBUG is set to a
writable file path; the bridge then appends a trace of the events it routes.
Useful when input is not reaching the engine.
Import from ui/pbslintdock.slint, registered as the library path
pbslintdock in CMake.
DockPanel { panel-id: "explorer"; MyExplorerView { } }| Property | Meaning |
|---|---|
in property <string> panel-id |
the only property you set; must match a registered PanelDef::id |
Resolves its slot through PbDock.slot-of() and binds x, y, width,
height and visible from PbDock.panels[slot]. Clips its content and paints
PbDockTheme.group-background behind it. Your content goes inside as children.
No properties to set. Renders group frames, tab bars, splitters, the drop
overlay, the 5-way indicator and — unless a DragGhostWindow is installed —
the drag ghost; reports its absolute screen
position (host-abs-x/y) and view size on init and on every resize. Put your
panel set inside it as children.
export component MyGhostWindow inherits DragGhostWindow { }| Property | Meaning |
|---|---|
in property <string> ghost-title |
the dragged panel's title; set by the bridge |
A frameless, always-on-top, transparent-background Window that draws the drag
ghost chip (painted with PbDockTheme.ghost-window-background). Register it
with SlintBridge::set_ghost_window<T>(); you can use DragGhostWindow
directly, or inherit from it. It only has to be imported and re-exported from
your .slint.
A frameless (no-frame: true, always-on-top: true) window with a 28 px title
bar, close button, 8 resize grips and an internal DockHost wrapping
@children. Inherit from it for your float window type.
Its width and height are bound to PbDock.float-frame-w / float-frame-h.
Do not call set_size on a float window — Slint's auto-sizing wins over
set_size, which is exactly why the binding exists.
Supplied by the bridge; you only touch these if you write your own view.
struct PanelVm { x, y, w, h: length; visible: bool; }
struct TabVm { x, y, w, h: length; title: string; active: bool; closable: bool; }
struct GroupVm { x, y, w, h, bar-h: length; }
struct SplitterVm { x, y, w, h: length; vertical: bool; }The full bridge interface. attach_main wires all of it; you normally never
touch it directly.
in property <int> epoch: -1;
in property <[PanelVm]> panels;
in property <[TabVm]> tabs;
in property <[GroupVm]> groups;
in property <[SplitterVm]> splitters;
in property <length> splitter-grab-pad: 4px; // from Metrics::splitter_grab_pad
in property <bool> overlay-active: false;
in property <int> overlay-zone: 0;
in property <length> ov-x; ov-y; ov-w; ov-h; // drop preview rect
in property <length> gr-x; gr-y; gr-w; gr-h; // hovered group rect
in property <bool> ghost-active: false; // false when a ghost window is used
in property <length> ghost-x; ghost-y;
in property <string> ghost-title;
pure callback slot-of(string) -> int;
in-out property <length> host-abs-x: 0;
in-out property <length> host-abs-y: 0;
callback view-resized(length, length);
callback tab-pressed(int, length, length);
callback tab-close(int);
callback group-bar-pressed(int, length, length);
callback pointer-moved(length, length);
callback pointer-released(length, length);
callback splitter-pressed(int, length);
callback splitter-dragged(int, length);
in property <string> float-title: "";
in-out property <length> float-frame-w: 360px;
in-out property <length> float-frame-h: 260px;
callback float-title-pressed(length, length);
callback float-title-moved(length, length);
callback float-title-released();
callback float-close-clicked();
callback float-grip-pressed(int, int);
callback float-grip-moved();
callback float-grip-released();Every colour and the base font size. All are in property, settable from
.slint or from C++ via app->global<PbDockTheme>().set_*(…).
| Property | Type | Default |
|---|---|---|
host-background |
brush | #1e1f22 |
group-background |
brush | #26282c |
tabbar-background |
brush | #2b2d31 |
tab-active-background |
brush | #26282c |
tab-inactive-background |
brush | #2b2d31 |
tab-hover-background |
brush | #33363b |
tab-active-text |
color | #e8e8e8 |
tab-inactive-text |
color | #9da0a6 |
accent |
color | #4a9eff |
splitter-background |
brush | #1e1f22 |
splitter-hover |
brush | #4a9eff |
group-border |
color | #17181a |
overlay-fill |
brush | #4a9eff33 |
overlay-border |
color | #4a9eff |
indicator-background |
brush | #2b2d31ee |
indicator-border |
color | #55585e |
ghost-background |
brush | #33363bdd |
ghost-window-background |
brush | #33363b |
float-titlebar |
brush | #2b2d31 |
float-title-text |
color | #cfd2d8 |
font-size |
length | 13px |
ghost-background is the in-host ghost chip and is semi-transparent because it
blends over the panels behind it; ghost-window-background is used by
DragGhostWindow, which has no opaque surface behind it and therefore needs an
opaque colour. Keep them in sync if you retheme.
Apply your theme to every window, floats included — the float factory's customization callback is the natural place.
Lower-level persistence primitives. DockManager::save_layout /
restore_layout wrap these and are what you normally want.
struct FloatState {
float x = 0, y = 0, w = 0, h = 0; // screen coordinates
std::unique_ptr<Node> tree;
};
struct LayoutState {
std::unique_ptr<Node> main;
std::vector<FloatState> floats;
std::vector<PanelId> closed;
};
using SlotToId = std::function<PanelId(int)>;
using IdToSlot = std::function<int(const PanelId &)>;
std::string serialize_layout(const LayoutState &state, const SlotToId &id_of);
std::optional<LayoutState> deserialize_layout(std::string_view json,
const IdToSlot &slot_of);{
"version": 1,
"main": <node>,
"floats": [ { "x": 0, "y": 0, "w": 0, "h": 0, "tree": <node> } ],
"closed": [ "panel-id", "…" ]
}<node> is one of:
{ "t": "g", "tabs": ["id1", "id2"], "active": 0 }a group — a tab group holding panel ids, with the index of the active tab;
{ "t": "s", "d": "h", "r": [0.3, 0.7], "c": [ <node>, <node> ] }a split — d is "h" (horizontal) or "v" (vertical), r are the child
ratios (same length as c, summing to 1), c are the child nodes;
or null for an empty root.
Guarantees you can rely on when migrating saved files:
- Panels are referenced by string id, never by slot index, so layouts survive changes to registration order.
- Unknown ids are skipped on load; groups left empty are pruned.
- Malformed input yields
nullopt(restore_layoutreturnsfalse). - Slots appearing more than once are de-duplicated.
The split tree. Advanced — only needed if you manipulate layouts below the
DockManager level or write custom serialization.
struct Node {
enum class Kind { Split, Group };
Kind kind = Kind::Group;
Dir dir = Dir::Horizontal; // Split only
std::vector<std::unique_ptr<Node>> children; // Split only
std::vector<float> ratios; // same size as children, sums to 1
std::vector<int> tabs; // Group only: panel slots
int active = 0;
static std::unique_ptr<Node> make_group(int slot);
static std::unique_ptr<Node> make_split(Dir d);
bool is_group() const;
bool is_split() const;
int active_slot() const; // -1 when not a non-empty group
};Node *find_group_of(Node *root, int slot);
bool tree_contains(const Node *root, const Node *node);
void for_each_group(Node *root, const std::function<void(Node *)> &fn);
Node *first_group(Node *root);
bool remove_panel(std::unique_ptr<Node> &root, int slot);
void insert_panel(std::unique_ptr<Node> &root, int slot, Node *target,
DropZone zone, int tab_index = -1, float edge_ratio = 0.5f);
void insert_subtree(std::unique_ptr<Node> &root, std::unique_ptr<Node> sub,
Node *target, DropZone zone, int tab_index = -1,
float edge_ratio = 0.5f);
std::vector<int> collect_tabs(const Node *root);
void reorder_tab(Node *group, int from, int to);
void drag_ratio(Node *split, int index, float delta_ratio,
const std::vector<float> &min_ratios);
void normalize_ratios(Node *split);
std::unique_ptr<Node> clone_tree(const Node *root);Splits are n-ary — a split can hold any number of children, not just two.
insert_panel and remove_panel keep the tree normalized: degenerate
single-child splits collapse and nested same-direction splits merge.
Turns a tree plus a viewport into the flat rectangles the view renders, and performs drop hit-testing. Advanced.
struct TabOut { Rect rect; int slot = -1; int group = -1; bool active = false; };
struct GroupOut { Rect rect; Rect tabbar; Rect content;
Node *node = nullptr;
int first_tab = 0, tab_count = 0; int active_slot = -1; };
struct SplitterOut { Rect rect; Node *split = nullptr; int index = 0;
bool vertical = false; float avail = 1; };
struct PanelPlace { Rect rect; bool visible = false; };
struct LayoutOutput {
std::vector<GroupOut> groups;
std::vector<TabOut> tabs;
std::vector<SplitterOut> splitters;
std::vector<PanelPlace> panels; // indexed by slot
};
struct PanelInfoSource {
std::function<std::string(int slot)> title;
std::function<float(int slot)> min_w;
std::function<float(int slot)> min_h;
int panel_count = 0;
};
void solve(Node *root, const Rect &view, const Metrics &m,
const PanelInfoSource &info, LayoutOutput &out);
float min_extent(const Node *node, Dir axis, const Metrics &m,
const PanelInfoSource &info);
float tab_width_for(const std::string &utf8_title, const Metrics &m);struct DropHit {
DropZone zone = DropZone::None;
Node *group = nullptr;
int tab_index = -1;
Rect preview;
Rect group_rect;
};
DropHit hit_test_drop(const LayoutOutput &out, const Rect &view,
const Metrics &m, float px, float py,
float root_margin = 24);root_margin is the width of the window-edge band that produces the Root*
zones instead of a group-relative zone.
LayoutOutput::panels is indexed by slot, so
out.panels[dock.slot_of("editor")] gives that panel's placement directly.
Two platform queries, with Win32 implementations and graceful fallbacks elsewhere.
bool cursor_screen_pos(float &x, float &y); // physical pixels
bool work_area_at(float px, float py, float &x, float &y, float &w, float &h);Both return false when the information is unavailable. work_area_at returns
the work area (screen minus taskbar) of the monitor containing the given point;
the manager uses it to clamp new floating windows so they never appear
off-screen or under the taskbar. Override it through
DockManager::set_work_area_provider if you need custom behaviour.
The calls a typical application actually uses:
// setup
pbdock::DockManager dock;
dock.add_panel({ "id", "Title", true, min_w, min_h });
dock.dock_panel_root("id", pbdock::DropZone::RootLeft, 0.25f);
dock.dock_panel("id", "target", pbdock::DropZone::Bottom, 0.3f);
dock.activate_panel("id");
dock.metrics().tabbar_height = 32; // default 28
// bridge
pbdock::SlintBridge bridge(dock);
auto app = MainWindow::create();
bridge.set_float_factory<MyFloatWindow>(); // before attach_main
bridge.set_ghost_window<DragGhostWindow>(); // optional, before attach_main
bridge.attach_main(app);
// runtime
dock.is_open("id");
dock.open_panel("id"); dock.close_panel("id");
dock.float_panel("id", 300, 300, 400, 300);
std::string json = dock.save_layout();
dock.restore_layout(json);
// theming
app->global<PbDockTheme>().set_accent(slint::Color::from_rgb_uint8(255, 153, 0));See the Integration Guide for a step-by-step walkthrough with a complete working example.