# Panel docking `craft_ui::docking` lets the user arrange an app's panels: tab groups, docks with columns, splits, floating windows and iconic columns. craft-ui owns the layout model, the renderer and the gestures. The app owns panel contents, commands, document state and where layouts are saved. Office apps that draw their own document with a few task panes beside it use [`craft_ui::panes`](ui-panes.md) instead. ## Arrangements One model serves every app. What differs between apps is the default layout and a few `DockStyle` settings. An app builds its style with `DockStyle::from_ui`, `from_visuals` or `default` and then sets fields; new fields can be added, so the struct has no literal form outside the crate. | Arrangement | Layout | Style settings | Apps | |---|---|---|---| | Docks beside a canvas (the defaults) | `Node::Dock`s on the window's start and end edges. A dock has columns; a column has stacked groups and can collapse to icons. Frames (tab groups and splits) fill the space between the docks. | `new_dock_zones` on | Creative apps with a canvas and panel docks | | A split tree of tab groups | `Split`s and `Tabs` only. | `side_edge_zones`, `maximize_item`, `close_others_item`, `command_press_floats`, `float_hint`, `wheel_selects_tabs`, `middle_click_closes_tabs` on, `new_dock_zones`, `float_on_empty_drop` and `title_bar_docks` off; menu labels such as "Undock Panel" set on `float_label`, `float_group_label` and `close_group_label` | Apps whose whole window is frames | Sides are logical: `Side::Start` is the left edge, or the right edge when `DockStyle::right_to_left` is set. egui has no app-wide direction, so the app sets it from its locale. Right to left, tab strips also mirror: the first tab is rightmost, the group header and drag bar are at the left, and drop insertion slots follow. Accordion headers and floating title bars (`chrome::TitleBar`, through `ChromeStyle::right_to_left`) mirror too. Frame splits keep their sides. ## Model `Layout
` holds an optional root `Node
` and a list of `Floating` windows. | Node | Holds | |---|---| | `Tabs` | A tab group: panels and the active index. | | `Split` | Two children, horizontal or vertical, and a size: a ratio, a fixed first size or a fixed second size. A moved divider keeps the mode: a fixed side stores its new size in points, a ratio its new ratio. | | `Stack` | Accordion entries, each open or closed, with an optional height. | | `Dock` | A side and its columns. A `Column` has `Group`s, a width, and `iconic`. | A `Floating` window holds a tree like the docked layout's (`root`: splits, tab groups and accordions, no docks), a rect, an optional native-window rect with the monitor it is on, and `minimized`. Its `id` names the window across panel moves (see [Native windows](#native-windows)). `Floating::single` makes a window with one group, `Floating::new` one with a tree, and `Floating::stack` one with groups stacked top to bottom (a vertical split sized by the groups' heights, as an app's own stacked layouts migrate); `panels()` and `contains()` read it. Each of these calls makes a new window with a fresh `id`. Build a layout once and keep it, or give its windows ids with `with_id`: a layout built anew each frame, as an unsaved default layout might be, otherwise opens new OS windows each frame and loses their drags and places. Debug builds log a warning when native windows are replaced by new ones with the same panels three frames running. Saved layouts keep loading: a window stored as one group (v1) or as a stack of groups loads as that group or as a vertical split of the stacked groups, sized by their heights. `P` is the app's panel id: stable, not localized, `Clone + Eq + Hash`. The renderer also needs `Debug + Send + Sync + 'static`. A panel occurs at most once in a layout. Change a layout only through `Action`s: ```rust use craft_ui::docking::{Action, Floating, Layout, Node, Placement}; let mut layout = Layout { root: Some(Node::Tabs { panels: vec!["layers", "properties"], active: 0 }), floating: vec![Floating::single(vec!["brushes"], 0, [40.0, 80.0, 240.0, 300.0]).with_id(1)], }; layout.apply(Action::Move { panel: "brushes", anchor: "layers", placement: Placement::Tab { before: Some("properties") }, })?; # Ok::<(), craft_ui::docking::DockError>(()) ``` - `Move`, `MovePanel`, `Float` and `Close` act on one panel. `MoveGroup`, `MoveColumn` and `MoveWindow` move a group, a dock column or a floating window, named by any panel in it. - `Open` reveals an absent panel as the last tab of its `anchor`'s group. Without an anchor it becomes the root of an empty layout, else joins the first dock group that accepts tabs and is shown, not minimized or in an iconic column (the end-side dock's first), else gets a new frame a quarter wide at the right edge of the docked area. `OpenAt` takes any placement. - A `Target` is `At { anchor, placement }`, `Float { rect }` or `NewDock { side }`. - A `Placement` is a tab position, a split zone, a new group above or below (with an optional preferred `height`), a new column, or a new frame on an outer edge. - Containers take different placements: a dock column takes groups and columns, not splits. `Placement::for_zone(layout, anchor, zone, right_to_left)` maps a left, right, top, bottom or center zone onto what the anchor's container takes: a split in the frame tree, a new column on that side or a new group above or below in a dock. A floating window takes splits as the docked layout does; `Placement::Group` on a floating anchor splits its group above or below. - A side split zone puts what moves in a new half-size split of the anchor's frame, and no other frame changes size. The new split has its own divider. `Placement::SplitSized` sizes that split instead (`size`) and resizes the enclosing split whose divider runs along the anchor frame's side (`outer`), so the frames beyond that divider give the new frame its room. - Other actions select, minimize, collapse to icons, resize splits (`ResizeSplit` in the docked layout, `ResizeWindowSplit` in the floating window holding a panel), columns and groups, and move a floating window to its own OS window (`SetNative`, and `SetMonitor` for the monitor it is on, feature `native-windows`). - A move between windows (docked to floating, floating to floating) is one action, checked as a whole like any other. Every action checks the whole result before it commits. A bad target, a duplicate id, a stale split path or invalid geometry returns a `DockError` and leaves the layout unchanged. The source panel is never lost. `apply`, `apply_with_permissions` and `apply_all` return the panels the actions closed, so the app can release their state; `closed_by` lists them before applying. `Permissions` limit what may happen to a panel: `movable`, `floatable`, `closable`, `accepts_tabs` and `accepts_splits`. `apply_with_permissions` enforces them for every action, whoever issues it. `Permissions::PROTECTED` keeps a canvas docked and open and takes neither tabs nor side splits; a group dropped on it floats there. An app that opens panels beside its canvas uses `Permissions { accepts_splits: true, ..Permissions::PROTECTED }`. ## Workspace `Workspace
` is what an app saves: the layout, the hidden panels with where each one goes back, an optional saved arrangement for Reset, and `locked`. | Method | Does | |---|---| | `apply`, `apply_all` | Apply actions all or none, with the app's permissions. Closed panels are recorded in `hidden`. While `locked`, moving, floating and closing are refused; `SetNative`, which records the OS windows, still applies. | | `set_visible(panel, visible, fallback, permissions)` | Hide a panel that may close, or show it at its recorded place, else as `Open` beside `fallback`, or where `Open` without an anchor puts it. A group or window closed one panel at a time comes back as one group as its panels are shown again. | | `reconcile(registered, permissions, default)` | Fit a loaded workspace to the panels the app has now. Unknown panels move to `hidden`, so they come back when registered again. A registered panel that cannot close, such as the canvas, is always shown. | | `reset(default)` | Return to the saved arrangement, or to `default` for a built-in workspace. | | `validate` | Check a loaded workspace before use. | With the `serde` feature, `Workspace`, `Layout` and `Action` serialize. Files without a version are v1 and load unchanged. craft-ui does not choose where files go; the app does. ## Rendering `DockArea::new(id).show_content(ui, &layout, &style, &mut content)` draws a layout snapshot and returns a `DockOutput`. It does not change the layout. The app applies `output.actions` through its own command path, so pointer, keyboard and control commands share one path. `DockContent
` is the app's adapter. It draws each panel body and gives its label, icon, permissions, size limits, dock column heights, preferred floating size, panel menu and group header. The group header sits at the end of the tab strip; `group_header_width` gets the strip width, so a header that draws its own tabs can size them to the room left. An icon is an image (`icon`), or drawn in code by `paint_icon`, which gets a painter, the icon rect and the tint color and returns true when it painted. Panel bodies keep a stable egui id when they move, so widget memory survives. `DockOutput` also has: - `focused`: the panel the user focused this frame: a clicked tab, a drop, or a press of any pointer button in a panel body (protected panels included) or in the group header, which focuses the group's shown panel. Commands, `raise_panel` and workspace resets never set it; a host that tracks the last focused panel clears it when a reset or command hides, closes or moves that panel. - `events`: requests outside the layout, such as `DockEvent::ToggleMaximize` (`maximize_item`) and `DockEvent::ToggleAutoCollapse` and `DockEvent::FlyoutChanged { panel }` (docks): the flyout opened, switched or closed (`None`). It comes once per change, whatever made it: an icon, Escape, a press outside, a raised panel, `open_flyout` or `close_flyout`. - `error`: a rejected gesture. Builder options on `DockArea`: | Option | Use | |---|---| | `maximized(panel)` | Maximize Frame: that frame fills the area. | | `hide_panels(HidePanels)` | Hide panels for Tab and Shift+Tab. Protected panels stay. | | `auto_collapse_iconic(on)` | Close a flyout on Escape or a click outside (default on). | | `raise_panel(panel)` | Raise a panel's floating window, or open its flyout, this frame. It moves no keyboard focus and does not set `DockOutput::focused`. | | `locked(on)` | Draw `Workspace::locked`: every panel gets `Permissions::locked`, so nothing starts a move, float or close the workspace would refuse. | | `blocked_rects(rects)` | App overlays that docking gestures must ignore. | | `float_bounds(rect)` | Where floating windows, flyouts and drag ghosts may go (default: the whole viewport). Pass the viewport below a title bar drawn under a transparent OS title bar, so no floating window sits where a press moves the app window. | `DockStyle::close_menu_items` and `DockStyle::close_button` hide the built-in Close menu items and the floating window ×. They hide controls only: a panel that stays `closable` still closes by the app's own Close items and commands. `DockArea::chrome_has_focus` tells whether keyboard focus is on docking chrome (any control a `Hit` names) rather than on a panel body. Focus follows only the user's input: a tab keeps keyboard focus until a command or a workspace reset hides, closes or moves its panel, and then gives it up. A divider or a floating window's resize grip holds focus only while the pointer drags it, so the app's shortcuts and canvas arrow keys work right after; one reached with Tab or F6 keeps its focus, and arrow keys resize it. Hovering focuses nothing. `begin_drag` hands a drag that started in the app's own chrome to the area. `docking::chrome` has the floating frame, the title bar and `drop_preview` (the drag ghost and region preview docking and `craft_ui::panes` both draw) for apps that draw their own windows. ## Tab strips Tabs start at their natural width, wide enough for the panel's name. When the strip is too narrow, the widest tabs shrink first, to a common width; tabs whose names fit that width keep their natural width. A tab narrower than its name cuts the name with "…" and shows it in full as a tooltip. Shrunk tabs keep their hit areas, drag and keyboard behaviour, and accessibility labels with the full names. No tab shrinks below `DockStyle::min_tab_width`: by default about three characters and the "…" plus the tab's padding, 54 pt at the 12 pt font. Only when the tabs at that width do not all fit do the rest go to the "…" button at the end of the strip, whose menu lists the hidden panels; choosing one makes it active. The active tab is always shown, in its logical place, and the others take the remaining room in their order. An active tab with less room than its minimum width, or a lone tab wider than the strip, is cut the same way. The drag bar always keeps its reserved width: tabs shrink, then overflow, before it narrows, and only a strip narrower than the group header plus the bar shrinks it. A tab dropped on the "…" button goes to the end of the group. ## Floating size A panel or group floated by a drag, a menu or a command without a rect takes its content height: the title bar, the tab strip and the height the active panel's content used when it was last drawn (the content ui's `min_rect`), at least 120 points. The most it takes is the height of `DockContent::floating_size`, or 320 points for a panel leaving a dock or frame and a group leaving a dock. A short panel therefore floats as a short window. Content that reaches the bottom of its body (a panel that allocates its whole rect) has no height of its own: it floats at the `floating_size` height, else at 320 points. A panel's width is its `floating_size` width, else its docked width; a group keeps its docked width. A flyout fits its content the same way, without the title bar. - A panel the area has not drawn floats at the `floating_size` height, else the docked height capped at 320, or at a `FloatSpace::with_panel_size` size for commands. - A group from a frame without a `floating_size` keeps its frame size. A dock column of several groups floats at the tallest `floating_size` height, else its column height capped at 320. - A rect from a command and a window the user resized keep their size; floating windows are not refitted while open. - Docked layout does not change: a column's last group still fills the column. ## Gestures - A tab drags one panel. A group's drag bar (the space beside its tabs) or its top border drags the whole group. A column bar drags a column. A floating window's title bar drags the window; with `title_bar_docks` a release over a target docks the whole window there, else it only moves. While the drag highlights a drop target, the window is drawn at 75% opacity so the target shows through it; over empty space, and once the drag ends, it is opaque. A native window moved by its only tab or tab row fades the same way. - Inside a floating window, tabs and drag bars drag and dock as they do in the docked layout, into any window. A drag bar or border of a window's only group takes the whole window along, so, like the title bar, it looks through the window: the groups, docks and windows behind it take the drop while the pointer is still inside it. In a window of several groups, the other groups stay targets. Its groups split, join and resize like frames, with dividers and four-way handles (`Divider::WindowSplit` and `Divider::WindowIntersection`, named by the window's first panel). Double-clicking the title bar minimizes the window to its title bar, or to the tab strip of its only group; so does double-clicking that group's strip. The group menu's float item floats one group of a window that has several. Closing the window closes its panels; each reopens where it was while the window or its neighbour in it is open, else in a window at the window's last rect. - A movable floating window resizes from every edge and corner. The top edge and corners are a 4 pt band above the title bar (`chrome::RESIZE_BAND`), so they take no moves and no clicks on the title bar's buttons. A resize keeps the opposite edges in place and stays within the size limits and the screen. A focused resize handle moves its edge 8 pt per arrow key. - Over a group (a frame, a dock column's group or a floating group), the pointer's place decides: - The tab strip joins the group's tabs at the insertion slot under the pointer. - In the body below the strip, the outer `DockStyle::side_zone_fraction` (one third) of its width from the left or right edge, or of its height from the top or bottom edge, docks beside the group on that side. Where two sides meet, at a corner, the side the pointer is relatively nearer wins. - The middle third joins the group's tabs. - A group that takes no tabs, such as a protected canvas, takes nothing on its strip, and its body has no middle: every point docks beside it on the relatively nearest side. - In the docking profile (`new_dock_zones` without `side_edge_zones`), the body of a frame or floating group that takes no tabs takes nothing, whatever its `accepts_splits`: a release there is a release over no target, so a floating window stays floating and a docked panel or group floats as `float_on_empty_drop` says. Only the bands along borders (below) dock beside it, as in Adobe apps, whose document area is no drop zone. A dock column's group keeps its sides. - Beside a frame is a split of that frame. Beside a group in a dock column, the top and bottom stack a new group above or below it, and the left and right make a new column on that side. A group dragged to an edge of its own column, such as the window edge of the column it heads, makes a new column there, as one of its tabs would; when it is the column's only group, nothing changes. - A frame too narrow or short for two halves of `DockStyle::min_pane` still splits when the frames beyond it can give the room, as they do for a dragged divider. When they cannot, the frame keeps its size and the new frame takes the width (or height) the moved panels have now, at least its minimum, from the frames beyond the frame's divider on that side (`Placement::SplitSized`). When those have no room either, that side joins the frame's tabs instead, or takes nothing when the frame takes no tabs. The gap between two frames is no target. - Bands along borders come before the group zones. Each is centred on its border where the pointer can reach both sides: | Band | Drop | Field (default) | |---|---|---| | The seam between two stacked groups of a dock column | A new group between them | `seam_band` (28 pt) | | Each edge of a dock column | A new column on that side | `column_edge_band` (28 pt) | | The docked area's left or right edge without a dock, and an empty dock | A new dock | `new_dock_band` (28 pt) | | The edge area's top and bottom in `drop_edges` (all four sides with `side_edge_zones`) | A new frame along that whole side, a quarter of the edge area | `edge_band` (28 pt, 8 pt with `side_edge_zones`; at most a quarter of the edge area) | Where a band overlaps a group's outer third, both mean the same side. Without `side_edge_zones`, a band reaches only 4 pt past its line into a tab strip that takes tabs; the rest of the strip joins. The outer edge band is offered only when every frame keeps its minimum. - The edge area is the root, or the part between the docks. Without `side_edge_zones` it also narrows to the canvas column: where neither side of a side-by-side split is a dock and only one side holds a protected panel, that side. The top (bottom) band also narrows a split of rows to its first (last) row when only that row holds the canvas, so a full-width row below the canvas keeps the bottom band. Side panels in split frames then get no band across the whole window, and the drop wraps only the canvas column. A drop on a narrowed band anchors on the canvas, so `Placement::Edge` narrows the same way when applied; on a band that is not narrowed it anchors on a panel that is not protected. The renderer tries the drop with its own permissions and offers the band only when the drop wraps the panels the band shows. `window.panel.dock` to an outer edge picks its anchor the same way, from the permissions `apply` (or `to_actions`) gets and the area's `FloatSpace` (`with_side_edge_zones` before it has drawn). - `DockStyle::drop_edges` (a `Sides`, all four by default) lists which sides of the layout's outer edge, and of a group that takes no tabs (a canvas) without `new_dock_zones`, take a drop. There a canvas's whole body goes to the nearest listed side; with none listed it takes nothing. In the docking profile the canvas body takes no drop and the outer left and right edges are new docks, so only the outer top and bottom apply. Groups that take tabs keep all four sides. A dock-column app whose palettes dock only at the left and right, as in AutoCAD, sets `Sides::NONE` with `new_dock_zones` on. Not used with `side_edge_zones`. - With `side_edge_zones` (the split-tree arrangement), a frame's zones are a picture frame instead: trapezoids along the four edges of the body below the tab strip, all four the same depth, `docking::SIDE_EDGE_ZONE_DEPTH` (40 pt), or a quarter of the body's smaller side when that is less, so every frame keeps a center. A dock column's groups take the same depth. `drop_zone` and `resolve_drop` give these zones to hosts that draw their own frames. - A host that resolves drops over its own groups calls `resolve_drop_by` with `DockStyle::zone_rule()`, so its zones and previews match the dock area's in either profile. `resolve_drop` always gives the split-tree zones. - A dragged divider grows the frames that touch it on one side, up to their maximums. On the other side the frame touching it shrinks to its minimum, then the next frame along the same axis, and so on: the divider stops when every frame on that side is at its minimum. Frames side by side across the axis shrink together, and the next frame gives way once one of them is at its minimum. The line of frames ends at a split across the axis or at a dock; every other frame keeps its size. Dividers between stacked panels and between the groups of a dock column work the same way. Where a divider ends on another divider, a four-way handle (`Divider::Intersection`) moves both. - Every frame has the same minimum, `DockStyle::min_pane` (plus its tab strip), unless its panels ask for more through `PanelLimits::min`. Frames have no maximum unless every panel in them asks for one through `PanelLimits::max`: a panel without a maximum lifts the maximum of the panels it joins. - A dock column stacks its groups top to bottom. A minimized group shows only its tab strip, and a group the user resized keeps that `Group::height`. A group the user never resized (`height` is `None`) takes the default height its panels ask for through `DockContent::dock_heights`. Groups whose panels ask for a `fill` height share the rest of the column, and groups whose panels ask for no heights take their minimum. When no group asks for a fill, the groups without heights share the rest, and when every open group has a height, the last one fills the column. In a short column the groups at their default give way first, down to their `compact` heights, so the filling group gets its `fill` height. When the column is still too short, every group gives way down to its minimum, and past that the column scrolls. Defaults give way nearest the filling group first: one is at its compact height before the next gives way. Towards the minimums, groups give way in proportion to their room. A group takes the largest default, compact and fill height of its panels. All three are body heights, without the tab strip, like `PanelLimits`. - Drop previews have four shapes, the same for tab and group drags: - A drop beside something shows the rect the dragged panel, group or column will take, at its real position and size, as a 2 pt accent outline with a light fill. The renderer applies the drop to a copy of the layout and lays it out with the code that draws it, so the rect is the one the panel's group gets on release: the new frame for a split (including `Placement::SplitSized`), the new group for a stack above or below a dock group or on a seam, and the whole column, bar included, for a new column between columns or beside a dock group's side. - A drop on a band along the layout's outer edge shows a `DockStyle::drop_line_width` (4 pt) accent line along that edge: the new-dock band, the window edge of a dock's outermost column, and the outer edge band (`Placement::Edge`), whose line spans the edge area it wraps. A drop between the icon groups of an iconic column shows the same line there. - A drop that joins a group shows the same outline and fill around the whole group, and on its strip the 2 pt insertion marker. - A drop that floats shows the floating rect. A drop that would leave every panel where it is, changing only sizes, shows nothing and changes nothing: a lone group dropped beside the group it already borders, or a frame's tab on the layout edge the frame already holds. Split sizes, column widths, group heights and window rectangles are not compared, and a run of splits along one axis is the same however it nests. A group drag also shows its ghost at the pointer. With `side_edge_zones`, a drag over a group instead outlines each of its zones that a drop would change something in (the side trapezoids, the center rectangle and the tab strip; `zone_shape` gives the outlines) and fills the zone under the pointer. Sides the group does not take, by its permissions or because a split leaves no room, are not drawn; nor is the center of the group a tab comes from. A drop there that the zones do not show, such as the outer edge band, fills the region it takes, and dock drops show the line along the edge they dock to. Escape or losing window focus cancels a drag. - A drag in progress (a tab, group, column, window bar, native window bar or divider, in any of the app's windows) also ends when the layout passed to the area is not the one it drew last frame, with or without its returned actions applied: the app switched workspaces, ran Reset or changed the layout by command. Sizes and rectangles are not compared, so an app may clamp a moved window or adjust a split during the drag. Nothing is restored from before the drag; windows go where the new layout has them. `cancel_drag` ends the area's drags at any other time. A native window moved by the compositor (`ViewportCommand::StartDrag`, as on Wayland) is not the area's drag: it ends when the user releases the button. - With `float_on_empty_drop`, a release over no target floats what moves at the pointer, such as a panel dropped on a protected canvas. Without it, that release cancels the drag and nothing moves: the gap between two groups, a menu bar or a status bar is no target. A release outside the app window floats in both cases. - Ctrl (⌘ on macOS) held at the drop floats it wherever it lands. With `command_press_floats`, the key held when a tab drag of a frame starts is enough. With `float_hint`, a hint under the pointer names the modifier while a drag offers a docking target: `float_hint_label`, where `{key}` becomes ⌘ or Ctrl. - Double-click a tab or drag bar to minimize a group to its strip. - Right-click a tab for its menu, or a drag bar for the group menu; Enter or Space on a focused drag bar opens it too. A left click on the drag bar only focuses it. - With `DockStyle::wheel_selects_tabs`, the mouse wheel over a tab strip selects the next or previous tab. With `middle_click_closes_tabs`, a middle click on a closable panel's tab closes it. - A column's »/« button collapses it to icons, one icon per panel with a separator between groups. Widen an iconic column to show panel names. Click an icon to open its group as a flyout on the canvas side. Drag an icon to move its panel, or an icon group's top edge (the separator line above it) to move the group. ## Commands With the `serde` feature, `DockCommand
` is the shared control vocabulary. The app puts the
command id under `"command"` in the params and deserializes them. craft-ui has no `serde_json`
dependency.
```rust,ignore
let mut params = params.clone();
params["command"] = json!(id);
let command: DockCommand