Skip to content

Latest commit

 

History

History
760 lines (591 loc) · 26 KB

File metadata and controls

760 lines (591 loc) · 26 KB

API Reference

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.


dock_types.h

PanelId

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.

PanelDef

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 });

DropZone

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.

Dir

enum class Dir { Horizontal, Vertical };

The orientation of a split node. Horizontal lays children out left-to-right.

Rect, Point

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.

Metrics

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.


dock_manager.h

kMainWindow

constexpr int kMainWindow = 0;   // floating windows get ids >= 1

Window ids. The main window is always 0; each float gets a unique id from 1 upwards.

class DockManager

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.

Construction

DockManager();
~DockManager();

Panel registry

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();                        // mutable

add_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.

Programmatic layout operations

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.

Window plumbing

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 default

Input events

All 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.

Render state

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;
};

Persistence

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.

Notifications

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.


slint_bridge.h

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

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_main

If 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.

Debug tracing

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.


The .slint module (@pbslintdock)

Import from ui/pbslintdock.slint, registered as the library path pbslintdock in CMake.

Components

DockPanel inherits Rectangle

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.

DockHost inherits Rectangle

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.

DragGhostWindow inherits Window

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.

FloatWindowFrame inherits Window

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.

View-model structs

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; }

global PbDock

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();

Theming: PbDockTheme

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.


serialization.h

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);

Layout JSON schema

{
  "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 splitd 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_layout returns false).
  • Slots appearing more than once are de-duplicated.

layout_tree.h

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.


layout_solver.h

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.


dock_platform.h

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.


Quick reference

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.