Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions _release-content/release-notes/dock_tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Dock tree
authors: ["@jbuehler23"]
pull_requests: [26034]
---

`bevy_ui_widgets` can now describe dockable layouts as plain data. A `DockTree` component holds tab groups and the splits between them, with operations to add, move, split and close tabs. Empty groups are removed and nested splits are merged as the tree changes.

Split sizes are flex weights, the same as `Pane::size` in the split pane widget. Each tab names its content with a `DockPanelKey` that the app maps to its own UI, and with the `serialize` feature a layout can be saved and restored.

```rust
let mut tree = DockTree::new();
let center = tree.root();
tree.add_tab(center, "viewport")?;
tree.split(center, DockEdge::Left, "outliner")?;
tree.split(center, DockEdge::Bottom, "assets")?;
```

The dock tree holds the data only. Building UI from the tree and dragging tabs between groups come in later releases.
1 change: 1 addition & 0 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ serialize = [
"bevy_time/serialize",
"bevy_transform/serialize",
"bevy_ui?/serialize",
"bevy_ui_widgets?/serialize",
"bevy_utils/serialize",
"bevy_window?/serialize",
"bevy_winit?/serialize",
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_ui_widgets/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,21 @@ bevy_window = { path = "../bevy_window", version = "0.20.0-dev" }
# other
accesskit = "0.25"
parley = { version = "0.11.0", default-features = false }
serde = { version = "1", features = ["derive"], optional = true }
smol_str = "0.2"
thiserror = { version = "2", default-features = false }

# The shortcut layout (Cmd vs Ctrl) depends on the HOST os, which on wasm is
# only knowable at runtime -- see `mac_host` in text_input.
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3", features = ["Window", "Navigator"] }

[dev-dependencies]
ron = "0.12"

[features]
default = []
serialize = ["dep:serde"]

[lints]
workspace = true
Expand Down
51 changes: 51 additions & 0 deletions crates/bevy_ui_widgets/src/dock/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! Dock layouts for Bevy UI, described as plain data.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this meant to be a representational source of truth, or a wire format derived from the scene?

Reading further down, I see that it is a component. What entity owns it?

I'm a little fuzzy on the lifecycle - if this is derived from the scene, it makes sense, since extracting this would make it easier to serialize. If OTOH the scene is derived from this, I'm not as clear why the scene isn't the source of truth (I'm sure there's a reason, I just don't know it yet.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! The DockTree is the source of truth, and the UI is derived from it. It's a component on the dock's root entity, and the follow-up PR adds a reconciler that builds the split panes, tab lists and panel content as children of that entity, and keeps them in sync whenever the tree changes. Tabs and splitters only propose changes (same controlled pattern as the other widgets), and those get applied to the tree.

The reason it isn't the scene itself is that the layout operations are tree operations on plain data. Splitting a group at an edge, moving a tab between groups and collapsing empty groups are much simpler and easier to test there than as entity hierarchy surgery. It also makes layouts easy to save and restore, and lets the UI entities be rebuilt or reused freely without losing the layout. Happy to talk through it more!

@viridia viridia Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get a comment that explains this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added it to the module docs and the DockTree docs, explaining that the tree is the source of truth and the UI is derived from it.

//!
//! A [`DockTree`] describes how a region of the screen is divided into tab groups. It holds
//! no entities and draws nothing; UI is built from it and kept in sync by other code. Layout
//! changes such as moving, splitting or closing tabs are made on the tree.
//!
//! The tree has two kinds of node:
//!
//! - a leaf, [`DockLeaf`], is a tab group: an ordered list of [`DockTab`]s and the one that
//! is active
//! - a split, [`DockSplit`], lays out two or more children along a [`ControlOrientation`],
//! each with a flex weight that matches [`Pane::size`]
//!
//! Each tab names its content with a [`DockPanelKey`]. The app decides what content a key
//! stands for, so the same tree can be saved and loaded across runs.
//!
//! [`DockNodeId`]s of leaves and [`DockTabId`]s are stable across edits and never reused.
//! Split nodes are created and removed as the tree is simplified: empty leaves are removed,
//! splits with a single child are replaced by that child, and a split nested in a split of
//! the same orientation is merged into its parent.
//!
//! [`DockTree`] is a component, so an app can have several docks, one per root entity. With
//! the `serialize` feature it can be saved and loaded with serde.
//!
//! The tree is the source of truth for the layout, and the dock's UI entities are derived from
//! it. Widgets such as tabs and splitters only propose changes, which are applied to the tree,
//! and the UI is then brought back in line with it. Keeping the layout as plain data makes
//! operations like splitting at an edge or collapsing empty groups simple to write and test,
//! makes layouts easy to save and restore, and lets the UI entities be rebuilt without losing
//! the layout.
//!
//! ```
//! use bevy_ui_widgets::{DockEdge, DockTree};
//!
//! let mut tree = DockTree::new();
//! let center = tree.root();
//! tree.add_tab(center, "viewport").unwrap();
//! tree.split(center, DockEdge::Left, "outliner").unwrap();
//! let (right, _) = tree.split(center, DockEdge::Right, "inspector").unwrap();
//! tree.add_tab(right, "settings").unwrap();
//! tree.split(center, DockEdge::Bottom, "assets").unwrap();
//!
//! assert_eq!(tree.leaves().count(), 4);
//! ```
//!
//! [`ControlOrientation`]: crate::ControlOrientation
//! [`Pane::size`]: crate::Pane::size

mod tree;

pub use tree::*;
Loading
Loading