Files
craft-libs/crates/ui/src/docking/dock_model.rs
T
aiongg d30c207c8d docking: one model; profile differences are style data
Remove `Profile`. What it switched is now a `DockStyle` setting with the
dock-and-canvas behaviour as the default: `side_edge_zones`,
`new_dock_zones`, `maximize_item`, `close_others_item`,
`command_press_floats`, `float_hint`, and plain bools for
`wheel_selects_tabs` and `middle_click_closes_tabs`. The undock and
Close Panel Group labels go; an app sets `float_label`,
`float_group_label` and `close_group_label` instead.

Remove the size machinery that made frames stick: `DockContent::bare`,
`FixedSlot` and the split that took a parent's or sibling's half beside
a fixed-size frame, and the gap snapping to the nearest frame. A tab
group takes the largest maximum of its panels, so a panel without one
lifts the cap of the group it joins. A dragged divider stores a ratio
whatever mode it was saved with.

Sizes follow the reference rules: a divider resizes only the groups
that touch it and stops at their minimums; a four-way handle where a
divider ends on another moves both (`Divider::Intersection`); a split or
outer-edge drop that would leave a group without `min_pane` is not
offered, and a group too small to halve takes the drop as a tab.

Tests: one per rule, the stuck cases from the FilmCraft map, and a
property test over random drops checking minimums and divider travel.
2026-10-22 20:01:31 +00:00

710 lines
32 KiB
Rust

//! Docks, columns, groups and floating stacks: their types and the structural edits behind
//! group, column and window moves.
//!
//! A group, column or floating window is named by a panel it contains. Edits run on a proposed
//! copy of the layout, so a helper that fails part-way leaves nothing behind.
use std::{collections::HashMap, hash::Hash};
use super::model::{DockError, Floating, Layout, MAX_DEPTH, MAX_NODES, Node, Placement, Zone, valid_height, valid_rect, valid_width};
use crate::layout::{SplitAxis, SplitSize};
/// Width of a new column when nothing better is known.
pub(super) const DEFAULT_COLUMN_WIDTH: f32 = 240.0;
/// Rect of a new floating window when the caller gives none and the source has no size.
const DEFAULT_FLOAT: [f32; 4] = [48.0, 48.0, 260.0, 320.0];
/// The window edge a [`Node::Dock`] sits on, in reading order so right-to-left interfaces
/// mirror. `Start` is the edge of a horizontal split's first child (left in a left-to-right
/// layout); the renderer maps it to a physical edge from the layout direction.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Side {
Start,
End,
}
/// Tabs that move, minimize and size together, in a dock column or a floating stack.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(bound(deserialize = "P: serde::Deserialize<'de>")))]
pub struct Group<P> {
pub panels: Vec<P>,
pub active: usize,
/// Only the tab strip shows. `height` is kept for when the group expands again.
#[cfg_attr(feature = "serde", serde(default))]
pub minimized: bool,
/// Preferred height in points; `None` shares the free height of the column or stack.
#[cfg_attr(feature = "serde", serde(default))]
pub height: Option<f32>,
}
impl<P> Group<P> {
pub fn new(panels: Vec<P>, active: usize) -> Self {
Self { panels, active, minimized: false, height: None }
}
pub fn active_panel(&self) -> Option<&P> {
self.panels.get(self.active)
}
}
/// One column of a dock: groups from top to bottom.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(bound(deserialize = "P: serde::Deserialize<'de>")))]
pub struct Column<P> {
pub groups: Vec<Group<P>>,
/// Expanded width in points.
pub width: f32,
/// Collapsed to one icon per panel.
#[cfg_attr(feature = "serde", serde(default))]
pub iconic: bool,
/// Width while iconic; labels show beside the icons when it is wide enough.
#[cfg_attr(feature = "serde", serde(default))]
pub iconic_width: Option<f32>,
}
impl<P> Column<P> {
pub fn new(groups: Vec<Group<P>>, width: f32) -> Self {
Self { groups, width, iconic: false, iconic_width: None }
}
}
/// Where a group, column or floating window goes.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Target<P> {
/// Relative to the panel `anchor`, which must not be part of what moves.
At { anchor: P, placement: Placement<P> },
/// A new dock on an edge that has none (or an empty one). The root becomes the other side
/// of a horizontal split.
NewDock { side: Side },
/// A new floating window. `None` keeps the source's size at a default position.
Float { rect: Option<[f32; 4]> },
}
impl<P: Clone + Eq + Hash> Placement<P> {
/// The placement for a drop zone on `anchor`'s group, in the vocabulary its container
/// accepts. `zone` is physical: `Left` is the left half on screen in either direction.
///
/// | Anchor in | `Center` | `Top`, `Bottom` | `Left`, `Right` |
/// |---|---|---|---|
/// | the split tree | [`Placement::Split`] | `Split` | `Split` |
/// | a dock column | `Split(Center)` (join as tabs) | [`Placement::Group`] above or below | [`Placement::Column`] on that side |
/// | a floating window | `Split(Center)` | `Group` above or below | [`DockError::InvalidPlacement`] |
///
/// A dock's `Column { after }` counts from its window edge, which depends on the dock's
/// side and `right_to_left` (see [`Side`]); this picks `after` so the new column lands on
/// the zone's side of the anchor's column.
pub fn for_zone(layout: &Layout<P>, anchor: &P, zone: Zone, right_to_left: bool) -> Result<Self, DockError> {
let place = layout.place(anchor).ok_or(DockError::MissingTarget)?;
Ok(match (place, zone) {
(Place::Node(_), zone) | (_, zone @ Zone::Center) => Placement::Split(zone),
(Place::Dock { .. } | Place::Floating { .. }, Zone::Top | Zone::Bottom) => Placement::Group { below: zone == Zone::Bottom, height: None },
(Place::Floating { .. }, _) => return Err(DockError::InvalidPlacement),
(Place::Dock { path, .. }, _) => {
let side = match layout.root.as_ref().and_then(|root| node_at(root, &path)) {
Some(Node::Dock { side, .. }) => *side,
_ => return Err(DockError::MissingTarget),
};
// The physical edge of the dock: Start is left in a left-to-right layout.
let left_edge = (side == Side::Start) != right_to_left;
// `after` is away from the window edge: rightwards for a left dock.
Placement::Column { after: (zone == Zone::Right) == left_edge }
}
})
}
}
/// What the Window menu shows for a panel. The model does not track the z-order of floating
/// windows, so `Front` means "showing": the active tab of an expanded group in an expanded column.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PanelState {
Hidden,
Open,
Front,
}
/// The group a closed panel was in, when that group was in a dock or a floating stack.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GroupLocation {
pub minimized: bool,
pub height: Option<f32>,
/// The column, when the group was docked.
pub column: Option<ColumnLocation>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ColumnLocation {
pub side: Side,
pub width: f32,
pub iconic: bool,
pub iconic_width: Option<f32>,
}
/// Where a panel is. Paths lead from the root: false is a split's first child, true its second.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum Place {
/// A `Tabs` or `Stack` node.
Node(Vec<bool>),
Dock {
path: Vec<bool>,
column: usize,
group: usize,
},
Floating {
window: usize,
group: usize,
},
}
/// Which container a whole-container action names.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Unit {
Group,
Column,
Window,
}
/// Groups in transit, with the size and state of the container they came from.
pub(super) struct Moving<P> {
pub groups: Vec<Group<P>>,
pub width: Option<f32>,
pub iconic: bool,
pub iconic_width: Option<f32>,
pub rect: Option<[f32; 4]>,
pub native: Option<[f32; 4]>,
}
impl<P> Moving<P> {
pub fn group(group: Group<P>) -> Self {
Self { groups: vec![group], width: None, iconic: false, iconic_width: None, rect: None, native: None }
}
fn into_column(self, width: f32) -> Column<P> {
Column { groups: self.groups, width: self.width.unwrap_or(width), iconic: self.iconic, iconic_width: self.iconic_width }
}
}
pub(super) fn node_at<'a, P>(mut node: &'a Node<P>, path: &[bool]) -> Option<&'a Node<P>> {
for second_child in path {
let Node::Split { first, second, .. } = node else { return None };
node = if *second_child { second } else { first };
}
Some(node)
}
pub(super) fn node_at_mut<'a, P>(mut node: &'a mut Node<P>, path: &[bool]) -> Option<&'a mut Node<P>> {
for second_child in path {
let Node::Split { first, second, .. } = node else { return None };
node = if *second_child { second } else { first };
}
Some(node)
}
/// Put `new` on the `zone` side of the frame at `path`: the frame splits into two halves and
/// every other frame keeps its size.
pub(super) fn split_frame<P>(root: &mut Node<P>, path: &[bool], new: Node<P>, zone: Zone) -> bool {
let axis = if matches!(zone, Zone::Top | Zone::Bottom) { SplitAxis::Vertical } else { SplitAxis::Horizontal };
let new_first = matches!(zone, Zone::Top | Zone::Left);
let Some(node) = node_at_mut(root, path) else { return false };
let old = std::mem::replace(node, Node::Tabs { panels: Vec::new(), active: 0 });
let (first, second) = if new_first { (new, old) } else { (old, new) };
*node = Node::Split { axis, size: SplitSize::Ratio(0.5), first: Box::new(first), second: Box::new(second) };
true
}
/// Insert `new` at `index` and select the member at `index + selected`.
fn splice<P>(panels: &mut Vec<P>, active: &mut usize, index: usize, new: Vec<P>, selected: usize) {
let index = index.min(panels.len());
let count = new.len();
panels.splice(index..index, new);
*active = index + selected.min(count.saturating_sub(1));
}
impl<P: Clone + Eq + Hash> Layout<P> {
pub(super) fn place(&self, panel: &P) -> Option<Place> {
for (w, window) in self.floating.iter().enumerate() {
if let Some(g) = window.groups.iter().position(|group| group.panels.contains(panel)) {
return Some(Place::Floating { window: w, group: g });
}
}
let mut visit: Vec<(&Node<P>, Vec<bool>)> = self.root.iter().map(|node| (node, Vec::new())).collect();
let mut visited = 0usize;
while let Some((node, path)) = visit.pop() {
visited = visited.saturating_add(1);
if visited > MAX_NODES {
return None;
}
match node {
Node::Split { first, second, .. } => {
let mut second_path = path.clone();
second_path.push(true);
let mut first_path = path;
first_path.push(false);
visit.push((second, second_path));
visit.push((first, first_path));
}
Node::Tabs { panels, .. } if panels.contains(panel) => return Some(Place::Node(path)),
Node::Stack { entries } if entries.iter().any(|e| &e.panel == panel) => return Some(Place::Node(path)),
Node::Dock { columns, .. } => {
for (c, column) in columns.iter().enumerate() {
if let Some(g) = column.groups.iter().position(|group| group.panels.contains(panel)) {
return Some(Place::Dock { path, column: c, group: g });
}
}
}
Node::Tabs { .. } | Node::Stack { .. } => {}
}
}
None
}
/// [`Self::place`] of every panel, from one traversal: the renderer looks up many panels a
/// frame.
pub(super) fn places(&self) -> HashMap<P, Place> {
let mut places = HashMap::new();
for (w, window) in self.floating.iter().enumerate() {
for (g, group) in window.groups.iter().enumerate() {
for panel in &group.panels {
places.entry(panel.clone()).or_insert(Place::Floating { window: w, group: g });
}
}
}
let mut visit: Vec<(&Node<P>, Vec<bool>)> = self.root.iter().map(|node| (node, Vec::new())).collect();
let mut visited = 0usize;
while let Some((node, path)) = visit.pop() {
visited = visited.saturating_add(1);
if visited > MAX_NODES {
break;
}
match node {
Node::Split { first, second, .. } => {
let mut second_path = path.clone();
second_path.push(true);
let mut first_path = path;
first_path.push(false);
visit.push((second, second_path));
visit.push((first, first_path));
}
Node::Tabs { panels, .. } => {
for panel in panels {
places.entry(panel.clone()).or_insert_with(|| Place::Node(path.clone()));
}
}
Node::Stack { entries } => {
for entry in entries {
places.entry(entry.panel.clone()).or_insert_with(|| Place::Node(path.clone()));
}
}
Node::Dock { columns, .. } => {
for (c, column) in columns.iter().enumerate() {
for (g, group) in column.groups.iter().enumerate() {
for panel in &group.panels {
places.entry(panel.clone()).or_insert_with(|| Place::Dock { path: path.clone(), column: c, group: g });
}
}
}
}
}
}
places
}
pub(super) fn dock_path(&self, side: Side) -> Option<Vec<bool>> {
let mut visit: Vec<(&Node<P>, Vec<bool>)> = self.root.iter().map(|node| (node, Vec::new())).collect();
let mut visited = 0usize;
while let Some((node, path)) = visit.pop() {
visited = visited.saturating_add(1);
if visited > MAX_NODES {
return None;
}
match node {
Node::Split { first, second, .. } => {
let mut second_path = path.clone();
second_path.push(true);
let mut first_path = path;
first_path.push(false);
visit.push((second, second_path));
visit.push((first, first_path));
}
Node::Dock { side: s, .. } if *s == side => return Some(path),
_ => {}
}
}
None
}
/// The node an edge split wraps: the root, or the part between the docks.
pub(in crate::docking) fn edge_path(&self) -> Vec<bool> {
let mut path = Vec::new();
let mut node = self.root.as_ref();
while let Some(Node::Split { axis: SplitAxis::Horizontal, first, second, .. }) = node {
if path.len() > MAX_DEPTH {
break;
}
let second_side = match (&**first, &**second) {
(Node::Dock { .. }, _) => true,
(_, Node::Dock { .. }) => false,
_ => break,
};
path.push(second_side);
node = Some(if second_side { second } else { first });
}
path
}
pub(super) fn columns_mut(&mut self, path: &[bool]) -> Option<&mut Vec<Column<P>>> {
match self.root.as_mut().and_then(|root| node_at_mut(root, path)) {
Some(Node::Dock { columns, .. }) => Some(columns),
_ => None,
}
}
pub(super) fn column(&self, place: &Place) -> Option<&Column<P>> {
let Place::Dock { path, column, .. } = place else { return None };
match self.root.as_ref().and_then(|root| node_at(root, path)) {
Some(Node::Dock { columns, .. }) => columns.get(*column),
_ => None,
}
}
pub(super) fn column_mut(&mut self, place: &Place) -> Option<&mut Column<P>> {
let Place::Dock { path, column, .. } = place else { return None };
self.columns_mut(path)?.get_mut(*column)
}
/// The dock or floating group at `place`; `None` for a `Tabs` or `Stack` node.
pub(super) fn group(&self, place: &Place) -> Option<&Group<P>> {
match place {
Place::Node(_) => None,
Place::Dock { group, .. } => self.column(place)?.groups.get(*group),
Place::Floating { window, group } => self.floating.get(*window)?.groups.get(*group),
}
}
pub(super) fn group_mut(&mut self, place: &Place) -> Option<&mut Group<P>> {
match place {
Place::Node(_) => None,
Place::Dock { group, .. } => self.column_mut(place)?.groups.get_mut(*group),
Place::Floating { window, group } => self.floating.get_mut(*window)?.groups.get_mut(*group),
}
}
/// Remove the node at `path`; its parent split collapses into the sibling.
pub(super) fn take_node(&mut self, path: &[bool]) -> Option<Node<P>> {
let Some((second_child, parent_path)) = path.split_last() else { return self.root.take() };
let parent = self.root.as_mut().and_then(|root| node_at_mut(root, parent_path))?;
if !matches!(parent, Node::Split { .. }) {
return None;
}
match std::mem::replace(parent, Node::Tabs { panels: Vec::new(), active: 0 }) {
Node::Split { first, second, .. } => {
let (taken, kept) = if *second_child { (second, first) } else { (first, second) };
*parent = *kept;
Some(*taken)
}
other => {
*parent = other;
None
}
}
}
/// The members of the group, column or floating window containing `panel`, in order.
/// An accordion entry is a group of one.
pub(super) fn members(&self, panel: &P, unit: Unit) -> Result<Vec<P>, DockError> {
let place = self.place(panel).ok_or(DockError::MissingPanel)?;
match (unit, &place) {
(Unit::Group, Place::Node(path)) => match self.root.as_ref().and_then(|root| node_at(root, path)) {
Some(Node::Tabs { panels, .. }) => Ok(panels.clone()),
Some(Node::Stack { .. }) => Ok(vec![panel.clone()]),
_ => Err(DockError::MissingPanel),
},
(Unit::Group, _) => self.group(&place).map(|g| g.panels.clone()).ok_or(DockError::MissingPanel),
(Unit::Column, Place::Dock { .. }) => {
let column = self.column(&place).ok_or(DockError::MissingPanel)?;
Ok(column.groups.iter().flat_map(|g| g.panels.iter().cloned()).collect())
}
(Unit::Window, Place::Floating { window, .. }) => {
let window = self.floating.get(*window).ok_or(DockError::MissingPanel)?;
Ok(window.panels().cloned().collect())
}
(Unit::Column | Unit::Window, _) => Err(DockError::InvalidPlacement),
}
}
pub(super) fn take_unit(&mut self, panel: &P, unit: Unit) -> Result<Moving<P>, DockError> {
match unit {
Unit::Group => self.take_group(panel),
Unit::Column => self.take_column(panel),
Unit::Window => self.take_window(panel),
}
}
fn take_group(&mut self, panel: &P) -> Result<Moving<P>, DockError> {
let place = self.place(panel).ok_or(DockError::MissingPanel)?;
match place {
Place::Node(path) => {
if let Some(Node::Stack { entries }) = self.root.as_mut().and_then(|root| node_at_mut(root, &path))
&& entries.len() > 1
&& let Some(index) = entries.iter().position(|e| &e.panel == panel)
{
let entry = entries.remove(index);
let group = Group { panels: vec![entry.panel], active: 0, minimized: !entry.open, height: entry.height };
return Ok(Moving::group(group));
}
match self.take_node(&path) {
Some(Node::Tabs { panels, active }) => Ok(Moving::group(Group::new(panels, active))),
Some(Node::Stack { mut entries }) if entries.len() == 1 => {
let entry = entries.remove(0);
Ok(Moving::group(Group { panels: vec![entry.panel], active: 0, minimized: !entry.open, height: entry.height }))
}
_ => Err(DockError::MissingPanel),
}
}
Place::Dock { path, column, group } => {
let columns = self.columns_mut(&path).ok_or(DockError::MissingPanel)?;
let source = columns.get_mut(column).ok_or(DockError::MissingPanel)?;
if group >= source.groups.len() {
return Err(DockError::MissingPanel);
}
let taken = source.groups.remove(group);
let width = source.width;
if source.groups.is_empty() {
columns.remove(column);
}
if columns.is_empty() {
self.take_node(&path);
}
Ok(Moving { width: Some(width), ..Moving::group(taken) })
}
Place::Floating { window, group } => {
let source = self.floating.get_mut(window).ok_or(DockError::MissingPanel)?;
if group >= source.groups.len() {
return Err(DockError::MissingPanel);
}
let taken = source.groups.remove(group);
let rect = source.rect;
let native = source.native;
let whole = source.groups.is_empty();
if whole {
self.floating.remove(window);
}
let width = rect.get(2).copied();
Ok(Moving { width, rect: whole.then_some(rect), native: native.filter(|_| whole), ..Moving::group(taken) })
}
}
}
fn take_column(&mut self, panel: &P) -> Result<Moving<P>, DockError> {
let Some(Place::Dock { path, column, .. }) = self.place(panel) else { return Err(DockError::InvalidPlacement) };
let columns = self.columns_mut(&path).ok_or(DockError::MissingPanel)?;
if column >= columns.len() {
return Err(DockError::MissingPanel);
}
let taken = columns.remove(column);
if columns.is_empty() {
self.take_node(&path);
}
Ok(Moving {
groups: taken.groups,
width: Some(taken.width),
iconic: taken.iconic,
iconic_width: taken.iconic_width,
rect: None,
native: None,
})
}
fn take_window(&mut self, panel: &P) -> Result<Moving<P>, DockError> {
let Some(Place::Floating { window, .. }) = self.place(panel) else { return Err(DockError::InvalidPlacement) };
if window >= self.floating.len() {
return Err(DockError::MissingPanel);
}
let taken = self.floating.remove(window);
Ok(Moving {
groups: taken.groups,
width: taken.rect.get(2).copied(),
iconic: false,
iconic_width: None,
rect: Some(taken.rect),
native: taken.native,
})
}
/// Insert groups taken by [`Self::take_unit`] (or a new single-panel group).
pub(super) fn insert_moving(&mut self, moving: Moving<P>, to: Target<P>) -> Result<(), DockError> {
if moving.groups.is_empty() || moving.groups.iter().any(|g| g.panels.is_empty()) {
return Err(DockError::MissingPanel);
}
match to {
Target::Float { rect } => {
let native = if rect.is_none() { moving.native } else { None };
let rect = rect.or(moving.rect).unwrap_or_else(|| {
let [x, y, width, height] = DEFAULT_FLOAT;
[x, y, moving.width.unwrap_or(width), height]
});
if !valid_rect(rect) {
return Err(DockError::InvalidGeometry);
}
self.floating.push(Floating { groups: moving.groups, rect, native });
Ok(())
}
Target::NewDock { side } => {
if let Some(path) = self.dock_path(side) {
let columns = self.columns_mut(&path).ok_or(DockError::MissingTarget)?;
if !columns.is_empty() {
return Err(DockError::InvalidPlacement);
}
columns.push(moving.into_column(DEFAULT_COLUMN_WIDTH));
return Ok(());
}
let column = moving.into_column(DEFAULT_COLUMN_WIDTH);
self.add_dock(side, column);
Ok(())
}
Target::At { anchor, placement } => {
let place = self.place(&anchor).ok_or(DockError::MissingTarget)?;
match placement {
Placement::Tab { before } => {
let index = match &before {
Some(before) => {
if self.place(before).as_ref() != Some(&place) {
return Err(DockError::MissingTarget);
}
self.index_in(&place, before)
}
None => None,
};
self.merge(&place, index, moving)
}
Placement::Split(Zone::Center) => {
let index = self.index_in(&place, &anchor).map(|i| i + 1);
self.merge(&place, index, moving)
}
Placement::Split(zone) => {
let Place::Node(path) = place else { return Err(DockError::InvalidPlacement) };
let (panels, active) = flatten(moving.groups);
let root = self.root.as_mut().ok_or(DockError::MissingTarget)?;
if split_frame(root, &path, Node::Tabs { panels, active }, zone) { Ok(()) } else { Err(DockError::MissingTarget) }
}
Placement::Group { below, height } => {
if !valid_height(height) {
return Err(DockError::InvalidGeometry);
}
let mut moving = moving;
if height.is_some() {
moving.groups.iter_mut().for_each(|group| group.height = height);
}
let (groups, index) = match &place {
Place::Dock { group, .. } => (&mut self.column_mut(&place).ok_or(DockError::MissingTarget)?.groups, *group),
Place::Floating { window, group } => {
(&mut self.floating.get_mut(*window).ok_or(DockError::MissingTarget)?.groups, *group)
}
Place::Node(_) => return Err(DockError::InvalidPlacement),
};
let index = (index + usize::from(below)).min(groups.len());
groups.splice(index..index, moving.groups);
Ok(())
}
Placement::Edge(zone) => {
if zone == Zone::Center || matches!(place, Place::Floating { .. }) {
return Err(DockError::InvalidPlacement);
}
let path = self.edge_path();
let node = self.root.as_mut().and_then(|root| node_at_mut(root, &path)).ok_or(DockError::MissingTarget)?;
if matches!(node, Node::Dock { .. }) {
return Err(DockError::InvalidPlacement);
}
let (panels, active) = flatten(moving.groups);
let new = Node::Tabs { panels, active };
let old = std::mem::replace(node, Node::Tabs { panels: Vec::new(), active: 0 });
let axis = if matches!(zone, Zone::Top | Zone::Bottom) { SplitAxis::Vertical } else { SplitAxis::Horizontal };
let (first, second, size) = if matches!(zone, Zone::Top | Zone::Left) { (new, old, 0.25) } else { (old, new, 0.75) };
*node = Node::Split { axis, size: SplitSize::Ratio(size), first: Box::new(first), second: Box::new(second) };
Ok(())
}
Placement::Column { after } => {
let Place::Dock { path, column, .. } = &place else { return Err(DockError::InvalidPlacement) };
let columns = self.columns_mut(path).ok_or(DockError::MissingTarget)?;
let width = columns.get(*column).map_or(DEFAULT_COLUMN_WIDTH, |c| c.width);
let index = (column + usize::from(after)).min(columns.len());
columns.insert(index, moving.into_column(width));
Ok(())
}
}
}
}
}
/// Wrap the root in a horizontal split with a new dock on `side`. The split's fixed size is
/// the column width.
pub(super) fn add_dock(&mut self, side: Side, column: Column<P>) {
let width = column.width;
let dock = Node::Dock { side, columns: vec![column] };
self.root = Some(match self.root.take() {
None => dock,
Some(root) => {
let (first, second, size) = match side {
Side::Start => (dock, root, SplitSize::FixedFirst(width)),
Side::End => (root, dock, SplitSize::FixedSecond(width)),
};
Node::Split { axis: SplitAxis::Horizontal, size, first: Box::new(first), second: Box::new(second) }
}
});
}
fn index_in(&self, place: &Place, panel: &P) -> Option<usize> {
match place {
Place::Node(path) => match self.root.as_ref().and_then(|root| node_at(root, path))? {
Node::Tabs { panels, .. } => panels.iter().position(|p| p == panel),
Node::Stack { entries } => entries.iter().position(|e| &e.panel == panel),
_ => None,
},
_ => self.group(place)?.panels.iter().position(|p| p == panel),
}
}
/// Join every moving panel to the group at `place`, before `index` or at the end. The first
/// moving group's active panel becomes active.
fn merge(&mut self, place: &Place, index: Option<usize>, moving: Moving<P>) -> Result<(), DockError> {
let (panels, selected) = flatten(moving.groups);
match place {
Place::Node(path) => match self.root.as_mut().and_then(|root| node_at_mut(root, path)) {
Some(Node::Tabs { panels: target, active }) => {
splice(target, active, index.unwrap_or(target.len()), panels, selected);
Ok(())
}
Some(Node::Stack { entries }) => {
let index = index.unwrap_or(entries.len()).min(entries.len());
entries.splice(index..index, panels.into_iter().map(|panel| super::StackEntry { panel, open: true, height: None }));
Ok(())
}
_ => Err(DockError::MissingTarget),
},
_ => {
let group = self.group_mut(place).ok_or(DockError::MissingTarget)?;
let end = group.panels.len();
splice(&mut group.panels, &mut group.active, index.unwrap_or(end), panels, selected);
Ok(())
}
}
}
}
/// All panels in order, and the index of the first group's active panel among them.
fn flatten<P>(groups: Vec<Group<P>>) -> (Vec<P>, usize) {
let selected = groups.first().map_or(0, |g| g.active.min(g.panels.len().saturating_sub(1)));
(groups.into_iter().flat_map(|g| g.panels).collect(), selected)
}
pub(super) fn valid_column<P>(column: &Column<P>) -> bool {
valid_width(column.width) && column.iconic_width.is_none_or(valid_width)
}