mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 15:49:54 +00:00
docking: docks, columns, groups and floating stacks in the layout model
Node::Dock holds columns of groups on a logical window edge (Side::Start
or Side::End). Floating windows hold a stack of groups and an optional
native rect; the v1 {panels, active, rect} shape still parses, and a
window with one plain group writes it. Placement gains Group and Column,
Target names where a group, column or window goes, and the new actions
move, close, minimize, collapse and resize groups, columns and windows
atomically. Location records group and column state so a panel closed
from a dock or floating stack restores to the same group, seam, column
or dock. Layout gains apply_all, closed_by and panel_state; Permissions
gains locked. Node, Action, Placement and DockError are non_exhaustive.
The renderer draws each floating window's groups stacked and a dock's
columns side by side until the dock and floating-stack profiles land.
This commit is contained in:
1 parent
c4b007706d
commit
0ae7cddbb5
6 files changed
+1140
-84
No files matched your search
@@ -0,0 +1,573 @@
|
||||
//! 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::hash::Hash;
|
||||
|
||||
use super::model::{DockError, Floating, Layout, MAX_NODES, Node, Placement, Zone, 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))]
|
||||
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))]
|
||||
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]> },
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 node = self.root.as_mut().and_then(|root| node_at_mut(root, &path)).ok_or(DockError::MissingTarget)?;
|
||||
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) = if matches!(zone, Zone::Top | Zone::Left) { (new, old) } else { (old, new) };
|
||||
*node = Node::Split { axis, size: SplitSize::Ratio(0.5), first: Box::new(first), second: Box::new(second) };
|
||||
Ok(())
|
||||
}
|
||||
Placement::Group { below } => {
|
||||
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::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)
|
||||
}
|
||||
@@ -5,10 +5,12 @@
|
||||
//! Validate deserialized layouts before rendering them. Every transaction checks its complete
|
||||
//! input and commits only a valid result; a rejected move cannot lose the source panel.
|
||||
|
||||
mod dock_model;
|
||||
mod model;
|
||||
mod surface;
|
||||
mod targets;
|
||||
|
||||
pub use dock_model::{Column, ColumnLocation, Group, GroupLocation, PanelState, Side, Target};
|
||||
pub use model::{Action, DockError, Floating, Layout, Location, MAX_DEPTH, MAX_PANELS, Node, Permissions, Placement, StackEntry, Zone};
|
||||
pub use surface::{DockArea, DockContent, DockOutput, DockStyle, PanelLimits, begin_drag, cancel_drag};
|
||||
pub use targets::{DropProposal, DropTarget, drop_zone, resolve_drop, zone_rect};
|
||||
|
||||
+514
-58
@@ -1,11 +1,12 @@
|
||||
use std::{collections::HashSet, fmt, hash::Hash};
|
||||
|
||||
use super::dock_model::{Column, ColumnLocation, Group, GroupLocation, Moving, PanelState, Place, Side, Target, Unit, node_at, valid_column};
|
||||
use crate::layout::{SplitAxis, SplitSize};
|
||||
|
||||
/// Limits are checked before cloning or recursively modifying an application layout.
|
||||
pub const MAX_DEPTH: usize = 64;
|
||||
pub const MAX_PANELS: usize = 4096;
|
||||
const MAX_NODES: usize = MAX_PANELS * 2;
|
||||
pub(super) const MAX_NODES: usize = MAX_PANELS * 2;
|
||||
const MAX_EXTENT: f32 = 1_000_000.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -20,6 +21,7 @@ pub enum Zone {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
pub enum Node<P> {
|
||||
Split {
|
||||
axis: SplitAxis,
|
||||
@@ -35,6 +37,13 @@ pub enum Node<P> {
|
||||
Stack {
|
||||
entries: Vec<StackEntry<P>>,
|
||||
},
|
||||
/// One window edge's dock, columns from the window edge inwards. A dock is the root or,
|
||||
/// following only same-side children of horizontal splits, at its window edge. At most one
|
||||
/// per side. An empty dock (no columns) is valid and accepts [`Target::NewDock`].
|
||||
Dock {
|
||||
side: Side,
|
||||
columns: Vec<Column<P>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -45,13 +54,93 @@ pub struct StackEntry<P> {
|
||||
pub height: Option<f32>,
|
||||
}
|
||||
|
||||
/// A floating window: a stack of groups, top to bottom.
|
||||
///
|
||||
/// Serde reads the v1 shape `{panels, active, rect}` as one group. A window with one plain group
|
||||
/// (not minimized, no height, no native rect) writes that shape, so older builds can read files
|
||||
/// that use no newer feature.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Floating<P> {
|
||||
pub panels: Vec<P>,
|
||||
pub active: usize,
|
||||
pub groups: Vec<Group<P>>,
|
||||
/// `[x, y, width, height]` in logical points, in the application's egui viewport.
|
||||
pub rect: [f32; 4],
|
||||
/// Desktop rect while the window is in its own OS window.
|
||||
pub native: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
impl<P> Floating<P> {
|
||||
pub fn single(panels: Vec<P>, active: usize, rect: [f32; 4]) -> Self {
|
||||
Self { groups: vec![Group::new(panels, active)], rect, native: None }
|
||||
}
|
||||
|
||||
pub fn panels(&self) -> impl Iterator<Item = &P> {
|
||||
self.groups.iter().flat_map(|group| group.panels.iter())
|
||||
}
|
||||
|
||||
pub fn contains(&self, panel: &P) -> bool
|
||||
where
|
||||
P: PartialEq,
|
||||
{
|
||||
self.groups.iter().any(|group| group.panels.contains(panel))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod floating_serde {
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use super::{Floating, Group};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct V1<'a, P> {
|
||||
panels: &'a [P],
|
||||
active: usize,
|
||||
rect: [f32; 4],
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct V2<'a, P> {
|
||||
groups: &'a [Group<P>],
|
||||
rect: [f32; 4],
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
native: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
impl<P: Serialize> Serialize for Floating<P> {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match self.groups.as_slice() {
|
||||
[only] if !only.minimized && only.height.is_none() && self.native.is_none() => {
|
||||
V1 { panels: &only.panels, active: only.active, rect: self.rect }.serialize(serializer)
|
||||
}
|
||||
groups => V2 { groups, rect: self.rect, native: self.native }.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(bound(deserialize = "P: Deserialize<'de>"))]
|
||||
struct Repr<P> {
|
||||
groups: Option<Vec<Group<P>>>,
|
||||
panels: Option<Vec<P>>,
|
||||
#[serde(default)]
|
||||
active: usize,
|
||||
rect: [f32; 4],
|
||||
#[serde(default)]
|
||||
native: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
impl<'de, P: Deserialize<'de>> Deserialize<'de> for Floating<P> {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let repr = Repr::<P>::deserialize(deserializer)?;
|
||||
let groups = match (repr.groups, repr.panels) {
|
||||
(Some(groups), None) => groups,
|
||||
(None, Some(panels)) => vec![Group::new(panels, repr.active)],
|
||||
(None, None) => return Err(serde::de::Error::missing_field("groups")),
|
||||
(Some(_), Some(_)) => return Err(serde::de::Error::duplicate_field("panels")),
|
||||
};
|
||||
Ok(Floating { groups, rect: repr.rect, native: repr.native })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -69,11 +158,17 @@ impl<P> Default for Layout<P> {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
pub enum Placement<P> {
|
||||
/// Insert before this existing destination member, or append. Also reorders a group.
|
||||
Tab { before: Option<P> },
|
||||
/// Center inserts after the anchor; sides create a new half-size split.
|
||||
/// Center inserts after the anchor; sides create a new half-size split. Sides apply to
|
||||
/// `Tabs` and `Stack` nodes only.
|
||||
Split(Zone),
|
||||
/// A new group above or below the anchor's group, in a dock column or a floating stack.
|
||||
Group { below: bool },
|
||||
/// A new column before (towards the window edge) or after the anchor's column.
|
||||
Column { after: bool },
|
||||
}
|
||||
|
||||
/// A closed panel's former placement. Applications may retain this beside visibility flags.
|
||||
@@ -92,10 +187,16 @@ pub struct Location<P> {
|
||||
/// rather than accidentally inserting beside only its first leaf when restoring.
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub sibling: Vec<P>,
|
||||
/// Group and column state when the panel was in a dock or a floating stack. Applies when
|
||||
/// restoring creates a new group, column or dock for the panel.
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub group: Option<GroupLocation>,
|
||||
}
|
||||
|
||||
/// Groups, columns and floating windows are named by a panel they contain.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
pub enum Action<P> {
|
||||
Move {
|
||||
panel: P,
|
||||
@@ -141,6 +242,62 @@ pub enum Action<P> {
|
||||
panel: P,
|
||||
height: Option<f32>,
|
||||
},
|
||||
/// Move the group containing `panel` with its order, active tab and minimized state.
|
||||
/// `Target::At` with `Tab` or `Split(Center)` merges every member. An accordion entry is a
|
||||
/// group of one. A target inside the group is a no-op.
|
||||
MoveGroup {
|
||||
panel: P,
|
||||
to: Target<P>,
|
||||
},
|
||||
/// Move the dock column containing `panel`. `Float` makes a window of its groups.
|
||||
MoveColumn {
|
||||
panel: P,
|
||||
to: Target<P>,
|
||||
},
|
||||
/// Dock or merge the floating window containing `panel`, every group included.
|
||||
MoveWindow {
|
||||
panel: P,
|
||||
to: Target<P>,
|
||||
},
|
||||
/// Close Tab Group.
|
||||
CloseGroup {
|
||||
panel: P,
|
||||
},
|
||||
/// × on a floating window: closes all its panels.
|
||||
CloseWindow {
|
||||
panel: P,
|
||||
},
|
||||
/// Close Other Panels in Group.
|
||||
CloseOthers {
|
||||
panel: P,
|
||||
},
|
||||
/// Minimize a dock or floating group to its tab strip. On an accordion entry, closes it.
|
||||
SetMinimized {
|
||||
panel: P,
|
||||
minimized: bool,
|
||||
},
|
||||
SetIconic {
|
||||
panel: P,
|
||||
iconic: bool,
|
||||
},
|
||||
ResizeColumn {
|
||||
panel: P,
|
||||
width: f32,
|
||||
},
|
||||
/// Preferred height of a dock or floating group, or of an accordion entry.
|
||||
ResizeGroup {
|
||||
panel: P,
|
||||
height: Option<f32>,
|
||||
},
|
||||
ResizeIconic {
|
||||
panel: P,
|
||||
width: Option<f32>,
|
||||
},
|
||||
/// Put the floating window in its own OS window at `rect`, or back in the app window.
|
||||
SetNative {
|
||||
panel: P,
|
||||
rect: Option<[f32; 4]>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Rendering and command adapters can protect a document canvas while leaving panels movable.
|
||||
@@ -160,9 +317,17 @@ impl Default for Permissions {
|
||||
|
||||
impl Permissions {
|
||||
pub const PROTECTED: Self = Self { movable: false, floatable: false, closable: false, accepts_tabs: false };
|
||||
|
||||
/// Lock Workspace: nothing moves, floats or closes; tab drops keep their policy.
|
||||
/// [`super::Workspace::apply`] applies this when the workspace is locked; an application
|
||||
/// with a global lock preference applies it in its own permissions callback.
|
||||
pub fn locked(self) -> Self {
|
||||
Self { movable: false, floatable: false, closable: false, accepts_tabs: self.accepts_tabs }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum DockError {
|
||||
LimitExceeded,
|
||||
DuplicatePanel,
|
||||
@@ -172,6 +337,8 @@ pub enum DockError {
|
||||
InvalidGeometry,
|
||||
InvalidPlacement,
|
||||
NotAllowed,
|
||||
/// Saved by a newer version of the docking format.
|
||||
UnsupportedVersion,
|
||||
}
|
||||
|
||||
impl fmt::Display for DockError {
|
||||
@@ -185,6 +352,7 @@ impl fmt::Display for DockError {
|
||||
Self::InvalidGeometry => "docking geometry is not finite and bounded",
|
||||
Self::InvalidPlacement => "docking destination does not support this placement",
|
||||
Self::NotAllowed => "this panel does not allow the requested docking operation",
|
||||
Self::UnsupportedVersion => "docking data was saved by a newer version",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -195,6 +363,10 @@ pub(super) fn valid_rect(rect: [f32; 4]) -> bool {
|
||||
rect.iter().all(|v| v.is_finite() && v.abs() <= MAX_EXTENT) && rect.get(2).is_some_and(|v| *v > 0.0) && rect.get(3).is_some_and(|v| *v > 0.0)
|
||||
}
|
||||
|
||||
pub(super) fn valid_width(width: f32) -> bool {
|
||||
width.is_finite() && width > 0.0 && width <= MAX_EXTENT
|
||||
}
|
||||
|
||||
fn valid_size(size: SplitSize) -> bool {
|
||||
match size {
|
||||
SplitSize::Ratio(r) => r.is_finite() && (0.0..=1.0).contains(&r),
|
||||
@@ -219,19 +391,47 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
}
|
||||
let mut seen = HashSet::new();
|
||||
let mut count = 0usize;
|
||||
let mut visit = self.root.as_ref().map(|n| vec![(n, 1usize)]).unwrap_or_default();
|
||||
while let Some((node, depth)) = visit.pop() {
|
||||
let mut docks = (false, false);
|
||||
// Each node carries whether it touches the window's start and end edges.
|
||||
let mut visit = self.root.as_ref().map(|n| vec![(n, 1usize, true, true)]).unwrap_or_default();
|
||||
while let Some((node, depth, start, end)) = visit.pop() {
|
||||
count = count.saturating_add(1);
|
||||
if depth > MAX_DEPTH || count > MAX_NODES {
|
||||
return Err(DockError::LimitExceeded);
|
||||
}
|
||||
match node {
|
||||
Node::Split { size, first, second, .. } => {
|
||||
Node::Split { axis, size, first, second } => {
|
||||
if !valid_size(*size) {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
visit.push((second, depth + 1));
|
||||
visit.push((first, depth + 1));
|
||||
let horizontal = *axis == SplitAxis::Horizontal;
|
||||
visit.push((second, depth + 1, false, end && horizontal));
|
||||
visit.push((first, depth + 1, start && horizontal, false));
|
||||
}
|
||||
Node::Dock { side, columns } => {
|
||||
let (at_edge, taken) = match side {
|
||||
Side::Start => (start, &mut docks.0),
|
||||
Side::End => (end, &mut docks.1),
|
||||
};
|
||||
if !at_edge || *taken {
|
||||
return Err(DockError::InvalidPlacement);
|
||||
}
|
||||
*taken = true;
|
||||
if columns.len() > MAX_PANELS {
|
||||
return Err(DockError::LimitExceeded);
|
||||
}
|
||||
for column in columns {
|
||||
if !valid_column(column) || column.groups.is_empty() {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
count = count.saturating_add(column.groups.len().saturating_add(1));
|
||||
if count > MAX_NODES {
|
||||
return Err(DockError::LimitExceeded);
|
||||
}
|
||||
for group in &column.groups {
|
||||
check_group(group, &mut seen)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::Tabs { panels, active } => {
|
||||
check_tabs(panels, *active, &mut seen)?;
|
||||
@@ -250,10 +450,16 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
}
|
||||
}
|
||||
for floating in &self.floating {
|
||||
if !valid_rect(floating.rect) || floating.panels.is_empty() {
|
||||
if !valid_rect(floating.rect) || floating.groups.is_empty() || floating.native.is_some_and(|r| !valid_rect(r)) {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
check_tabs(&floating.panels, floating.active, &mut seen)?;
|
||||
count = count.saturating_add(floating.groups.len());
|
||||
if count > MAX_NODES {
|
||||
return Err(DockError::LimitExceeded);
|
||||
}
|
||||
for group in &floating.groups {
|
||||
check_group(group, &mut seen)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -275,10 +481,14 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
}
|
||||
Node::Tabs { panels: group, .. } => panels.extend(group.iter().take(MAX_PANELS - panels.len())),
|
||||
Node::Stack { entries } => panels.extend(entries.iter().map(|e| &e.panel).take(MAX_PANELS - panels.len())),
|
||||
Node::Dock { columns, .. } => {
|
||||
let members = columns.iter().flat_map(|c| c.groups.iter()).flat_map(|g| g.panels.iter());
|
||||
panels.extend(members.take(MAX_PANELS - panels.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for group in self.floating.iter().take(MAX_PANELS) {
|
||||
panels.extend(group.panels.iter().take(MAX_PANELS - panels.len()));
|
||||
for window in self.floating.iter().take(MAX_PANELS) {
|
||||
panels.extend(window.panels().take(MAX_PANELS - panels.len()));
|
||||
}
|
||||
panels
|
||||
}
|
||||
@@ -296,12 +506,69 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
/// Capture a panel's nearest surviving group/split neighbor before hiding it.
|
||||
pub fn location(&self, panel: &P) -> Result<Location<P>, DockError> {
|
||||
self.validate()?;
|
||||
if let Some(group) = self.floating.iter().find(|g| g.panels.contains(panel)) {
|
||||
let mut location = group_location(&group.panels, panel).ok_or(DockError::MissingPanel)?;
|
||||
location.floating = Some(group.rect);
|
||||
let place = self.place(panel).ok_or(DockError::MissingPanel)?;
|
||||
let (groups, group, floating, column) = match &place {
|
||||
Place::Node(_) => return self.root.as_ref().and_then(|node| node_location(node, panel)).ok_or(DockError::MissingPanel),
|
||||
Place::Floating { window, group } => {
|
||||
let window = self.floating.get(*window).ok_or(DockError::MissingPanel)?;
|
||||
(&window.groups, *group, Some(window.rect), None)
|
||||
}
|
||||
Place::Dock { path, group, .. } => {
|
||||
let side = match self.root.as_ref().and_then(|root| node_at(root, path)) {
|
||||
Some(Node::Dock { side, .. }) => *side,
|
||||
_ => return Err(DockError::MissingPanel),
|
||||
};
|
||||
let column = self.column(&place).ok_or(DockError::MissingPanel)?;
|
||||
let state = ColumnLocation { side, width: column.width, iconic: column.iconic, iconic_width: column.iconic_width };
|
||||
(&column.groups, *group, None, Some(state))
|
||||
}
|
||||
};
|
||||
let own = groups.get(group).ok_or(DockError::MissingPanel)?;
|
||||
let mut location = group_location(&own.panels, panel).ok_or(DockError::MissingPanel)?;
|
||||
location.floating = floating;
|
||||
location.group = Some(GroupLocation { minimized: own.minimized, height: own.height, column });
|
||||
if location.anchor.is_some() {
|
||||
return Ok(location);
|
||||
}
|
||||
self.root.as_ref().and_then(|node| node_location(node, panel)).ok_or(DockError::MissingPanel)
|
||||
// Alone in its group: anchor to the neighbouring group, then the neighbouring column.
|
||||
let first = |g: Option<&Group<P>>| g.and_then(|g| g.panels.first()).cloned();
|
||||
if let Some(anchor) = first(groups.get(group + 1)) {
|
||||
location.anchor = Some(anchor);
|
||||
location.placement = Placement::Group { below: false };
|
||||
} else if let Some(anchor) = first(group.checked_sub(1).and_then(|i| groups.get(i))) {
|
||||
location.anchor = Some(anchor);
|
||||
location.placement = Placement::Group { below: true };
|
||||
} else if groups.len() == 1
|
||||
&& let Place::Dock { path, column, .. } = &place
|
||||
&& let Some(Node::Dock { columns, .. }) = self.root.as_ref().and_then(|root| node_at(root, path))
|
||||
{
|
||||
let first_of = |c: Option<&Column<P>>| first(c.and_then(|c| c.groups.first()));
|
||||
if let Some(anchor) = first_of(columns.get(column + 1)) {
|
||||
location.anchor = Some(anchor);
|
||||
location.placement = Placement::Column { after: false };
|
||||
} else if let Some(anchor) = first_of(column.checked_sub(1).and_then(|i| columns.get(i))) {
|
||||
location.anchor = Some(anchor);
|
||||
location.placement = Placement::Column { after: true };
|
||||
}
|
||||
}
|
||||
Ok(location)
|
||||
}
|
||||
|
||||
/// Hidden, open behind another tab or in a collapsed container, or showing.
|
||||
pub fn panel_state(&self, panel: &P) -> PanelState {
|
||||
let showing = match self.place(panel) {
|
||||
None => return PanelState::Hidden,
|
||||
Some(Place::Node(path)) => match self.root.as_ref().and_then(|root| node_at(root, &path)) {
|
||||
Some(Node::Tabs { panels, active }) => panels.get(*active) == Some(panel),
|
||||
Some(Node::Stack { entries }) => entries.iter().any(|e| &e.panel == panel && e.open),
|
||||
_ => false,
|
||||
},
|
||||
Some(place) => {
|
||||
let iconic = self.column(&place).is_some_and(|c| c.iconic);
|
||||
self.group(&place).is_some_and(|g| !g.minimized && g.active_panel() == Some(panel)) && !iconic
|
||||
}
|
||||
};
|
||||
if showing { PanelState::Front } else { PanelState::Open }
|
||||
}
|
||||
|
||||
/// Restore an absent panel at a captured location. Missing anchors are reported without
|
||||
@@ -314,18 +581,57 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
if location.size.is_some_and(|s| !valid_size(s)) || location.stack.is_some_and(|(_, h)| !valid_height(h)) {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
let state = location.group.as_ref();
|
||||
if state.is_some_and(|s| {
|
||||
!valid_height(s.height) || s.column.is_some_and(|c| !valid_width(c.width) || c.iconic_width.is_some_and(|w| !valid_width(w)))
|
||||
}) {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
// A restore that creates a group, column or dock gives it the saved state.
|
||||
let moving = || {
|
||||
let group =
|
||||
Group { panels: vec![panel.clone()], active: 0, minimized: state.is_some_and(|s| s.minimized), height: state.and_then(|s| s.height) };
|
||||
let column = state.and_then(|s| s.column);
|
||||
Moving {
|
||||
width: column.map(|c| c.width),
|
||||
iconic: column.is_some_and(|c| c.iconic),
|
||||
iconic_width: column.and_then(|c| c.iconic_width),
|
||||
..Moving::group(group)
|
||||
}
|
||||
};
|
||||
let mut proposed = self.clone();
|
||||
if let Some(rect) = location.floating {
|
||||
if !valid_rect(rect) {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
if let Some(anchor) = &location.anchor {
|
||||
if !proposed.floating.iter().any(|g| g.panels.contains(anchor)) {
|
||||
if !proposed.floating.iter().any(|g| g.contains(anchor)) {
|
||||
return Err(DockError::MissingTarget);
|
||||
}
|
||||
proposed.apply_inner(Action::OpenAt { panel: panel.clone(), anchor: anchor.clone(), placement: location.placement.clone() })?;
|
||||
if let Placement::Group { .. } = location.placement {
|
||||
proposed.insert_moving(moving(), Target::At { anchor: anchor.clone(), placement: location.placement.clone() })?;
|
||||
} else {
|
||||
proposed.apply_inner(Action::OpenAt { panel: panel.clone(), anchor: anchor.clone(), placement: location.placement.clone() })?;
|
||||
}
|
||||
} else {
|
||||
proposed.floating.push(Floating { panels: vec![panel.clone()], active: 0, rect });
|
||||
proposed.insert_moving(moving(), Target::Float { rect: Some(rect) })?;
|
||||
}
|
||||
} else if let Some(anchor) = &location.anchor
|
||||
&& matches!(location.placement, Placement::Group { .. } | Placement::Column { .. })
|
||||
{
|
||||
if !proposed.contains(anchor) {
|
||||
return Err(DockError::MissingTarget);
|
||||
}
|
||||
proposed.insert_moving(moving(), Target::At { anchor: anchor.clone(), placement: location.placement.clone() })?;
|
||||
} else if let Some(column) = state.and_then(|s| s.column).filter(|_| location.anchor.is_none()) {
|
||||
// Alone in its dock: back at that edge, as the outermost column if a dock exists.
|
||||
match proposed.dock_path(column.side) {
|
||||
Some(path) => {
|
||||
let columns = proposed.columns_mut(&path).ok_or(DockError::MissingTarget)?;
|
||||
let Moving { groups, .. } = moving();
|
||||
columns.insert(0, Column { groups, width: column.width, iconic: column.iconic, iconic_width: column.iconic_width });
|
||||
}
|
||||
None => proposed.insert_moving(moving(), Target::NewDock { side: column.side })?,
|
||||
}
|
||||
} else if let Some(anchor) = &location.anchor {
|
||||
if !proposed.contains(anchor) {
|
||||
@@ -397,6 +703,31 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
Action::Close { panel } if !permissions(panel).closable => return Err(DockError::NotAllowed),
|
||||
Action::MoveFloating { panel, .. } if !permissions(panel).movable => return Err(DockError::NotAllowed),
|
||||
Action::Open { anchor: Some(anchor), .. } if !permissions(anchor).accepts_tabs => return Err(DockError::NotAllowed),
|
||||
Action::MoveGroup { panel, to } | Action::MoveColumn { panel, to } | Action::MoveWindow { panel, to } => {
|
||||
let members = self.members(panel, unit_of(&action))?;
|
||||
if members.iter().any(|p| !permissions(p).movable) {
|
||||
return Err(DockError::NotAllowed);
|
||||
}
|
||||
match to {
|
||||
Target::Float { .. } if members.iter().any(|p| !permissions(p).floatable) => return Err(DockError::NotAllowed),
|
||||
Target::At { anchor, placement: Placement::Tab { .. } | Placement::Split(Zone::Center) }
|
||||
if !members.contains(anchor) && !permissions(anchor).accepts_tabs =>
|
||||
{
|
||||
return Err(DockError::NotAllowed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Action::CloseGroup { panel } | Action::CloseWindow { panel } | Action::CloseOthers { panel } => {
|
||||
let others = matches!(action, Action::CloseOthers { .. });
|
||||
let members = self.members(panel, unit_of(&action))?;
|
||||
if members.iter().filter(|p| !others || *p != panel).any(|p| !permissions(p).closable) {
|
||||
return Err(DockError::NotAllowed);
|
||||
}
|
||||
}
|
||||
Action::SetNative { panel, rect: Some(_) } if self.members(panel, Unit::Window)?.iter().any(|p| !permissions(p).floatable) => {
|
||||
return Err(DockError::NotAllowed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let mut proposed = self.clone();
|
||||
@@ -406,6 +737,28 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply every action in order, or none: the first rejection leaves the layout unchanged.
|
||||
pub fn apply_all(&mut self, actions: impl IntoIterator<Item = Action<P>>, permissions: impl Fn(&P) -> Permissions) -> Result<(), DockError> {
|
||||
let mut proposed = self.clone();
|
||||
for action in actions {
|
||||
proposed.apply_with_permissions(action, &permissions)?;
|
||||
}
|
||||
*self = proposed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The panels a close action (`Close`, `CloseGroup`, `CloseWindow`, `CloseOthers`) would
|
||||
/// remove, in group order, so the application can release their state once the action is
|
||||
/// applied. Empty for other actions.
|
||||
pub fn closed_by(&self, action: &Action<P>) -> Result<Vec<P>, DockError> {
|
||||
Ok(match action {
|
||||
Action::Close { panel } => vec![panel.clone()],
|
||||
Action::CloseGroup { panel } | Action::CloseWindow { panel } => self.members(panel, unit_of(action))?,
|
||||
Action::CloseOthers { panel } => self.members(panel, Unit::Group)?.into_iter().filter(|p| p != panel).collect(),
|
||||
_ => Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_inner(&mut self, action: Action<P>) -> Result<(), DockError> {
|
||||
match action {
|
||||
Action::Move { panel, anchor, placement } => {
|
||||
@@ -432,19 +785,20 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
let entry = self.take(&panel).ok_or(DockError::MissingPanel)?;
|
||||
self.floating.push(Floating { panels: vec![entry.panel], active: 0, rect });
|
||||
self.floating.push(Floating::single(vec![entry.panel], 0, rect));
|
||||
Ok(())
|
||||
}
|
||||
Action::Close { panel } => self.take(&panel).map(|_| ()).ok_or(DockError::MissingPanel),
|
||||
Action::Activate { panel } => {
|
||||
if self.root.as_mut().is_some_and(|node| activate(node, &panel)) {
|
||||
let place = self.place(&panel).ok_or(DockError::MissingPanel)?;
|
||||
if let Some(group) = self.group_mut(&place) {
|
||||
// Selecting a panel shows it: a minimized group expands.
|
||||
group.active = group.panels.iter().position(|p| p == &panel).ok_or(DockError::MissingPanel)?;
|
||||
group.minimized = false;
|
||||
return Ok(());
|
||||
}
|
||||
for group in &mut self.floating {
|
||||
if let Some(i) = group.panels.iter().position(|p| p == &panel) {
|
||||
group.active = i;
|
||||
return Ok(());
|
||||
}
|
||||
if self.root.as_mut().is_some_and(|node| activate(node, &panel)) {
|
||||
return Ok(());
|
||||
}
|
||||
Err(DockError::MissingPanel)
|
||||
}
|
||||
@@ -477,8 +831,8 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
if !valid_rect(rect) {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
let group = self.floating.iter_mut().find(|g| g.panels.contains(&panel)).ok_or(DockError::MissingPanel)?;
|
||||
group.rect = rect;
|
||||
let window = self.floating.iter_mut().find(|g| g.contains(&panel)).ok_or(DockError::MissingPanel)?;
|
||||
window.rect = rect;
|
||||
Ok(())
|
||||
}
|
||||
Action::ResizeSplit { path, size } => {
|
||||
@@ -510,47 +864,115 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
entry.height = height;
|
||||
Ok(())
|
||||
}
|
||||
Action::MoveGroup { ref panel, ref to } | Action::MoveColumn { ref panel, ref to } | Action::MoveWindow { ref panel, ref to } => {
|
||||
let unit = unit_of(&action);
|
||||
if let Target::At { anchor, .. } = to
|
||||
&& self.members(panel, unit)?.contains(anchor)
|
||||
{
|
||||
// Dropped on itself.
|
||||
return Ok(());
|
||||
}
|
||||
let moving = self.take_unit(panel, unit)?;
|
||||
self.insert_moving(moving, to.clone())
|
||||
}
|
||||
Action::CloseGroup { ref panel } | Action::CloseWindow { ref panel } => self.take_unit(panel, unit_of(&action)).map(|_| ()),
|
||||
Action::CloseOthers { panel } => {
|
||||
for other in self.members(&panel, Unit::Group)? {
|
||||
if other != panel {
|
||||
self.take(&other).ok_or(DockError::MissingPanel)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Action::SetMinimized { panel, minimized } => {
|
||||
let place = self.place(&panel).ok_or(DockError::MissingPanel)?;
|
||||
if let Some(group) = self.group_mut(&place) {
|
||||
group.minimized = minimized;
|
||||
return Ok(());
|
||||
}
|
||||
let entry = self.root.as_mut().and_then(|n| stack_entry_mut(n, &panel)).ok_or(DockError::InvalidPlacement)?;
|
||||
entry.open = !minimized;
|
||||
Ok(())
|
||||
}
|
||||
Action::ResizeGroup { panel, height } => {
|
||||
if !valid_height(height) {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
let place = self.place(&panel).ok_or(DockError::MissingPanel)?;
|
||||
if let Some(group) = self.group_mut(&place) {
|
||||
group.height = height;
|
||||
return Ok(());
|
||||
}
|
||||
let entry = self.root.as_mut().and_then(|n| stack_entry_mut(n, &panel)).ok_or(DockError::InvalidPlacement)?;
|
||||
entry.height = height;
|
||||
Ok(())
|
||||
}
|
||||
Action::SetIconic { panel, iconic } => {
|
||||
self.column_of(&panel)?.iconic = iconic;
|
||||
Ok(())
|
||||
}
|
||||
Action::ResizeColumn { panel, width } => {
|
||||
if !valid_width(width) {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
self.column_of(&panel)?.width = width;
|
||||
Ok(())
|
||||
}
|
||||
Action::ResizeIconic { panel, width } => {
|
||||
if width.is_some_and(|w| !valid_width(w)) {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
self.column_of(&panel)?.iconic_width = width;
|
||||
Ok(())
|
||||
}
|
||||
Action::SetNative { panel, rect } => {
|
||||
if rect.is_some_and(|r| !valid_rect(r)) {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
let window = self.floating.iter_mut().find(|g| g.contains(&panel)).ok_or(DockError::InvalidPlacement)?;
|
||||
window.native = rect;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn column_of(&mut self, panel: &P) -> Result<&mut Column<P>, DockError> {
|
||||
let place = self.place(panel).ok_or(DockError::MissingPanel)?;
|
||||
self.column_mut(&place).ok_or(DockError::InvalidPlacement)
|
||||
}
|
||||
|
||||
fn same_group(&self, panel: &P, anchor: &P) -> bool {
|
||||
fn in_node<P: Eq>(node: &Node<P>, panel: &P, anchor: &P) -> bool {
|
||||
match node {
|
||||
Node::Tabs { panels, .. } => panels.contains(panel) && panels.contains(anchor),
|
||||
Node::Stack { entries } => entries.iter().any(|e| &e.panel == panel) && entries.iter().any(|e| &e.panel == anchor),
|
||||
Node::Split { first, second, .. } => in_node(first, panel, anchor) || in_node(second, panel, anchor),
|
||||
}
|
||||
}
|
||||
self.root.as_ref().is_some_and(|n| in_node(n, panel, anchor))
|
||||
|| self.floating.iter().any(|g| g.panels.contains(panel) && g.panels.contains(anchor))
|
||||
let place = self.place(panel);
|
||||
place.is_some() && place == self.place(anchor)
|
||||
}
|
||||
|
||||
fn take(&mut self, panel: &P) -> Option<StackEntry<P>> {
|
||||
pub(super) fn take(&mut self, panel: &P) -> Option<StackEntry<P>> {
|
||||
let mut removed = None;
|
||||
self.root = self.root.take().and_then(|n| remove_node(n, panel, &mut removed));
|
||||
if removed.is_some() {
|
||||
return removed;
|
||||
}
|
||||
for group in &mut self.floating {
|
||||
if let Some(p) = remove_tab(&mut group.panels, &mut group.active, panel) {
|
||||
removed = Some(StackEntry { panel: p, open: true, height: None });
|
||||
for window in &mut self.floating {
|
||||
for group in &mut window.groups {
|
||||
if let Some(p) = remove_tab(&mut group.panels, &mut group.active, panel) {
|
||||
removed = Some(StackEntry { panel: p, open: true, height: None });
|
||||
break;
|
||||
}
|
||||
}
|
||||
if removed.is_some() {
|
||||
window.groups.retain(|g| !g.panels.is_empty());
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.floating.retain(|g| !g.panels.is_empty());
|
||||
self.floating.retain(|g| !g.groups.is_empty());
|
||||
removed
|
||||
}
|
||||
|
||||
fn insert(&mut self, entry: StackEntry<P>, anchor: &P, placement: Placement<P>) -> Result<(), DockError> {
|
||||
if let Some(group) = self.floating.iter_mut().find(|g| g.panels.contains(anchor)) {
|
||||
let index = match placement {
|
||||
Placement::Tab { before } => before.as_ref().and_then(|p| group.panels.iter().position(|q| q == p)).unwrap_or(group.panels.len()),
|
||||
Placement::Split(Zone::Center) => group.panels.iter().position(|p| p == anchor).map_or(group.panels.len(), |i| i + 1),
|
||||
Placement::Split(_) => return Err(DockError::InvalidPlacement),
|
||||
};
|
||||
group.panels.insert(index, entry.panel);
|
||||
group.active = index;
|
||||
return Ok(());
|
||||
let place = self.place(anchor).ok_or(DockError::MissingTarget)?;
|
||||
if matches!(place, Place::Dock { .. } | Place::Floating { .. }) || matches!(placement, Placement::Group { .. } | Placement::Column { .. }) {
|
||||
let group = Group::new(vec![entry.panel], 0);
|
||||
return self.insert_moving(Moving::group(group), Target::At { anchor: anchor.clone(), placement });
|
||||
}
|
||||
if let Some(root) = self.root.as_mut()
|
||||
&& insert_node(root, &entry, anchor, &placement)
|
||||
@@ -561,6 +983,14 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
}
|
||||
}
|
||||
|
||||
fn unit_of<P>(action: &Action<P>) -> Unit {
|
||||
match action {
|
||||
Action::MoveColumn { .. } => Unit::Column,
|
||||
Action::MoveWindow { .. } | Action::CloseWindow { .. } => Unit::Window,
|
||||
_ => Unit::Group,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_panel<'a, P: Eq + Hash>(panel: &'a P, seen: &mut HashSet<&'a P>) -> Result<(), DockError> {
|
||||
if seen.len() >= MAX_PANELS {
|
||||
return Err(DockError::LimitExceeded);
|
||||
@@ -571,6 +1001,13 @@ fn check_panel<'a, P: Eq + Hash>(panel: &'a P, seen: &mut HashSet<&'a P>) -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_group<'a, P: Eq + Hash>(group: &'a Group<P>, seen: &mut HashSet<&'a P>) -> Result<(), DockError> {
|
||||
if group.panels.is_empty() || !valid_height(group.height) {
|
||||
return Err(DockError::InvalidGeometry);
|
||||
}
|
||||
check_tabs(&group.panels, group.active, seen)
|
||||
}
|
||||
|
||||
fn check_tabs<'a, P: Eq + Hash>(panels: &'a [P], active: usize, seen: &mut HashSet<&'a P>) -> Result<(), DockError> {
|
||||
if panels.len() > MAX_PANELS {
|
||||
return Err(DockError::LimitExceeded);
|
||||
@@ -614,6 +1051,20 @@ fn remove_node<P: Clone + Eq>(node: Node<P>, panel: &P, removed: &mut Option<Sta
|
||||
(node, None) | (None, node) => node,
|
||||
}
|
||||
}
|
||||
Node::Dock { side, mut columns } => {
|
||||
// A dock that was already empty stays; one emptied by this removal collapses.
|
||||
let had_columns = !columns.is_empty();
|
||||
for column in &mut columns {
|
||||
for group in &mut column.groups {
|
||||
if let Some(panel) = remove_tab(&mut group.panels, &mut group.active, panel) {
|
||||
*removed = Some(StackEntry { panel, open: true, height: None });
|
||||
}
|
||||
}
|
||||
column.groups.retain(|g| !g.panels.is_empty());
|
||||
}
|
||||
columns.retain(|c| !c.groups.is_empty());
|
||||
(!had_columns || !columns.is_empty()).then_some(Node::Dock { side, columns })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,6 +1087,7 @@ fn activate<P: Eq>(node: &mut Node<P>, panel: &P) -> bool {
|
||||
}
|
||||
}
|
||||
Node::Split { first, second, .. } => activate(first, panel) || activate(second, panel),
|
||||
Node::Dock { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,7 +1095,7 @@ fn stack_entry_mut<'a, P: Eq>(node: &'a mut Node<P>, panel: &P) -> Option<&'a mu
|
||||
match node {
|
||||
Node::Stack { entries } => entries.iter_mut().find(|e| &e.panel == panel),
|
||||
Node::Split { first, second, .. } => stack_entry_mut(first, panel).or_else(|| stack_entry_mut(second, panel)),
|
||||
Node::Tabs { .. } => None,
|
||||
Node::Tabs { .. } | Node::Dock { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,6 +1104,7 @@ fn insert_node<P: Clone + Eq>(node: &mut Node<P>, entry: &StackEntry<P>, anchor:
|
||||
Node::Split { first, second, .. } => return insert_node(first, entry, anchor, placement) || insert_node(second, entry, anchor, placement),
|
||||
Node::Tabs { panels, .. } => panels.contains(anchor),
|
||||
Node::Stack { entries } => entries.iter().any(|e| &e.panel == anchor),
|
||||
Node::Dock { .. } => false,
|
||||
};
|
||||
if !here {
|
||||
return false;
|
||||
@@ -668,7 +1121,7 @@ fn insert_node<P: Clone + Eq>(node: &mut Node<P>, entry: &StackEntry<P>, anchor:
|
||||
Node::Tabs { panels, active } => {
|
||||
let index = match placement {
|
||||
Placement::Tab { before } => before.as_ref().and_then(|p| panels.iter().position(|q| q == p)).unwrap_or(panels.len()),
|
||||
Placement::Split(_) => panels.iter().position(|p| p == anchor).map_or(panels.len(), |i| i + 1),
|
||||
_ => panels.iter().position(|p| p == anchor).map_or(panels.len(), |i| i + 1),
|
||||
};
|
||||
panels.insert(index, entry.panel.clone());
|
||||
*active = index;
|
||||
@@ -676,11 +1129,11 @@ fn insert_node<P: Clone + Eq>(node: &mut Node<P>, entry: &StackEntry<P>, anchor:
|
||||
Node::Stack { entries } => {
|
||||
let index = match placement {
|
||||
Placement::Tab { before } => before.as_ref().and_then(|p| entries.iter().position(|e| &e.panel == p)).unwrap_or(entries.len()),
|
||||
Placement::Split(_) => entries.iter().position(|e| &e.panel == anchor).map_or(entries.len(), |i| i + 1),
|
||||
_ => entries.iter().position(|e| &e.panel == anchor).map_or(entries.len(), |i| i + 1),
|
||||
};
|
||||
entries.insert(index, entry.clone());
|
||||
}
|
||||
Node::Split { .. } => return false,
|
||||
Node::Split { .. } | Node::Dock { .. } => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -689,7 +1142,7 @@ fn group_location<P: Clone + Eq>(panels: &[P], panel: &P) -> Option<Location<P>>
|
||||
let index = panels.iter().position(|p| p == panel)?;
|
||||
let anchor = panels.iter().find(|p| *p != panel).cloned();
|
||||
let before = panels.get(index + 1).cloned();
|
||||
Some(Location { floating: None, anchor, placement: Placement::Tab { before }, size: None, stack: None, sibling: Vec::new() })
|
||||
Some(Location { floating: None, anchor, placement: Placement::Tab { before }, size: None, stack: None, sibling: Vec::new(), group: None })
|
||||
}
|
||||
|
||||
fn first_panel<P>(node: &Node<P>) -> Option<&P> {
|
||||
@@ -697,6 +1150,7 @@ fn first_panel<P>(node: &Node<P>) -> Option<&P> {
|
||||
Node::Tabs { panels, .. } => panels.first(),
|
||||
Node::Stack { entries } => entries.first().map(|e| &e.panel),
|
||||
Node::Split { first, second, .. } => first_panel(first).or_else(|| first_panel(second)),
|
||||
Node::Dock { columns, .. } => columns.iter().flat_map(|c| c.groups.iter()).find_map(|g| g.panels.first()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,6 +1187,7 @@ fn node_location<P: Clone + Eq>(node: &Node<P>, panel: &P) -> Option<Location<P>
|
||||
}
|
||||
Some(location)
|
||||
}
|
||||
Node::Dock { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,6 +1228,7 @@ fn node_members<'a, P>(node: &'a Node<P>, members: &mut Vec<&'a P>) {
|
||||
node_members(first, members);
|
||||
node_members(second, members);
|
||||
}
|
||||
Node::Dock { columns, .. } => members.extend(columns.iter().flat_map(|c| c.groups.iter()).flat_map(|g| g.panels.iter())),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -208,7 +208,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<P> {
|
||||
|| matches(self.id.with(("move-group", panels.first())))
|
||||
};
|
||||
for floating in &layout.floating {
|
||||
if group(&floating.panels) || floating.panels.first().is_some_and(|first| matches(self.id.with(("floating", first)).with("resize"))) {
|
||||
if floating.groups.iter().any(|g| group(&g.panels))
|
||||
|| floating.panels().next().is_some_and(|first| matches(self.id.with(("floating", first)).with("resize")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -216,6 +218,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<P> {
|
||||
while let Some((node, path)) = nodes.pop() {
|
||||
match node {
|
||||
Node::Tabs { panels, .. } if group(panels) => return true,
|
||||
Node::Dock { columns, .. } if columns.iter().flat_map(|c| c.groups.iter()).any(|g| group(&g.panels)) => return true,
|
||||
Node::Stack { entries } => {
|
||||
if entries.iter().any(|entry| {
|
||||
matches(self.id.with(("tab", &entry.panel)))
|
||||
@@ -401,6 +404,17 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
}
|
||||
Node::Tabs { panels, active } => self.group(ui, panels, *active, rect, false),
|
||||
Node::Dock { columns, .. } => {
|
||||
// Interim rendering: columns side by side, groups stacked evenly.
|
||||
let total: f32 = columns.iter().map(|c| c.width).sum();
|
||||
let mut left = rect.left();
|
||||
for column in columns {
|
||||
let width = if total > 0.0 { rect.width() * column.width / total } else { 0.0 };
|
||||
let column_rect = Rect::from_min_max(pos2(left, rect.top()), pos2(left + width, rect.bottom()));
|
||||
left += width;
|
||||
self.stacked_groups(ui, &column.groups, column_rect, false);
|
||||
}
|
||||
}
|
||||
Node::Stack { entries } => {
|
||||
let total_header = (entries.len() as f32 * self.style.tab_height).min(rect.height());
|
||||
let remaining = (rect.height() - total_header).max(0.0);
|
||||
@@ -487,8 +501,19 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Groups stacked top to bottom in equal shares of `rect`.
|
||||
fn stacked_groups(&mut self, ui: &mut Ui, groups: &[super::Group<P>], rect: Rect, floating: bool) {
|
||||
let share = rect.height() / groups.len().max(1) as f32;
|
||||
for (index, group) in groups.iter().enumerate() {
|
||||
let top = rect.top() + share * index as f32;
|
||||
let group_rect = Rect::from_min_max(pos2(rect.left(), top), pos2(rect.right(), top + share));
|
||||
self.group(ui, &group.panels, group.active, group_rect, floating);
|
||||
}
|
||||
}
|
||||
|
||||
fn node_limits(&self, node: &Node<P>) -> PanelLimits {
|
||||
match node {
|
||||
Node::Dock { .. } => PanelLimits { min: Vec2::splat(self.style.min_pane), ..PanelLimits::default() },
|
||||
Node::Tabs { panels, .. } => {
|
||||
let mut limits = PanelLimits::default();
|
||||
for panel in panels {
|
||||
@@ -750,17 +775,17 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let layers = ctx.memory(|memory| memory.layer_ids().collect::<Vec<_>>());
|
||||
let mut groups: Vec<_> = self.layout.floating.iter().collect();
|
||||
groups.sort_by_key(|group| {
|
||||
let id = group.panels.first().map(|panel| self.id.with(("floating", panel)));
|
||||
let id = group.panels().next().map(|panel| self.id.with(("floating", panel)));
|
||||
layers.iter().position(|layer| Some(layer.id) == id).unwrap_or(usize::MAX)
|
||||
});
|
||||
for group in groups {
|
||||
let Some(first) = group.panels.first() else { continue };
|
||||
let Some(first) = group.panels().next() else { continue };
|
||||
let [x, y, width, height] = group.rect;
|
||||
let limits = self.node_limits(&Node::Tabs { panels: group.panels.clone(), active: group.active });
|
||||
let limits = self.node_limits(&Node::Tabs { panels: group.panels().cloned().collect(), active: 0 });
|
||||
let size = vec2(width, height).max(limits.min).min(limits.max);
|
||||
let rect = clamp_float(Rect::from_min_size(pos2(x, y), size), screen);
|
||||
let id = self.id.with(("floating", first));
|
||||
if self.activation.as_ref().is_some_and(|panel| group.panels.contains(panel)) {
|
||||
if self.activation.as_ref().is_some_and(|panel| group.contains(panel)) {
|
||||
ctx.move_to_top(egui::LayerId::new(egui::Order::Middle, id));
|
||||
ctx.memory_mut(|memory| memory.request_focus(self.id.with(("tab", self.activation.as_ref().unwrap_or(first)))));
|
||||
self.output.focused = self.activation.clone();
|
||||
@@ -769,7 +794,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
ui.set_min_size(rect.size());
|
||||
let rect = Rect::from_min_size(ui.min_rect().min, rect.size());
|
||||
ui.allocate_rect(rect, Sense::hover());
|
||||
self.group(ui, &group.panels, group.active, rect, true);
|
||||
self.stacked_groups(ui, &group.groups, rect, true);
|
||||
let corner = Rect::from_min_max((rect.max - vec2(12.0, 12.0)).max(rect.min), rect.max);
|
||||
let response = ui.interact(corner, id.with("resize"), Sense::drag()).on_hover_cursor(egui::CursorIcon::ResizeNwSe);
|
||||
self.register_interaction(ui.ctx(), response.id);
|
||||
@@ -813,7 +838,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
|
||||
fn finish_group_drag(&mut self, ctx: &Context) {
|
||||
let Some(drag) = ctx.data(|d| d.get_temp::<GroupDrag<P>>(self.id.with("group-drag"))) else { return };
|
||||
if !self.layout.floating.iter().any(|g| g.panels.contains(&drag.panel) && g.panels.iter().all(|p| (self.permissions)(p).movable)) {
|
||||
if !self.layout.floating.iter().any(|g| g.contains(&drag.panel) && g.panels().all(|p| (self.permissions)(p).movable)) {
|
||||
ctx.data_mut(|d| d.remove::<GroupDrag<P>>(self.id.with("group-drag")));
|
||||
return;
|
||||
}
|
||||
@@ -873,7 +898,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let action = if let Some(proposal) = proposal.filter(|_| !gone) {
|
||||
Some(Action::Move { panel: drag.panel.clone(), anchor: proposal.anchor, placement: proposal.placement })
|
||||
} else if (self.permissions)(&drag.panel).floatable
|
||||
&& (gone || !drag.source.contains(pointer) || self.layout.floating.iter().any(|g| g.panels.contains(&drag.panel)))
|
||||
&& (gone || !drag.source.contains(pointer) || self.layout.floating.iter().any(|g| g.contains(&drag.panel)))
|
||||
{
|
||||
let rect = clamp_float(Rect::from_min_size(pointer - drag.grab, drag.source.size()), ctx.content_rect());
|
||||
Some(Action::Float { panel: drag.panel.clone(), rect: [rect.left(), rect.top(), rect.width(), rect.height()] })
|
||||
|
||||
@@ -14,7 +14,7 @@ fn sample() -> Layout<u32> {
|
||||
first: Box::new(tabs(&[1], 0)),
|
||||
second: Box::new(tabs(&[2, 3, 4], 1)),
|
||||
}),
|
||||
floating: vec![Floating { panels: vec![5, 6], active: 1, rect: [80.0, 70.0, 300.0, 220.0] }],
|
||||
floating: vec![Floating::single(vec![5, 6], 1, [80.0, 70.0, 300.0, 220.0])],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ fn floating_brush_panel_joins_main_tabs_without_losing_other_panels() {
|
||||
let Node::Split { size, second, .. } = layout.root.as_ref().unwrap() else { panic!() };
|
||||
assert_eq!(*size, SplitSize::FixedSecond(240.0));
|
||||
assert_eq!(**second, tabs(&[2, 3, 5, 4], 2));
|
||||
assert_eq!(layout.floating[0].panels, vec![6]);
|
||||
assert_eq!(layout.floating[0].active, 0);
|
||||
assert_eq!(layout.floating[0].groups[0].panels, vec![6]);
|
||||
assert_eq!(layout.floating[0].groups[0].active, 0);
|
||||
layout.validate().unwrap();
|
||||
assert_eq!(layout.panels().len(), 6);
|
||||
}
|
||||
@@ -43,8 +43,8 @@ fn detach_redock_merge_and_collapse_empty_source_containers() {
|
||||
layout.apply(move_to(1, 2, Zone::Left)).unwrap();
|
||||
assert_eq!(layout.floating.len(), 1);
|
||||
layout.apply(move_to(5, 6, Zone::Center)).unwrap();
|
||||
assert_eq!(layout.floating[0].panels, vec![6, 5]);
|
||||
assert_eq!(layout.floating[0].active, 1);
|
||||
assert_eq!(layout.floating[0].groups[0].panels, vec![6, 5]);
|
||||
assert_eq!(layout.floating[0].groups[0].active, 1);
|
||||
layout.apply(move_to(6, 3, Zone::Center)).unwrap();
|
||||
layout.apply(move_to(5, 3, Zone::Center)).unwrap();
|
||||
assert!(layout.floating.is_empty());
|
||||
@@ -154,7 +154,7 @@ fn close_and_restore_preserve_fixed_side_and_floating_geometry() {
|
||||
first: Box::new(tabs(&[1], 0)),
|
||||
second: Box::new(tabs(&[2], 0)),
|
||||
}),
|
||||
floating: vec![Floating { panels: vec![3], active: 0, rect: [41.0, 52.0, 233.0, 244.0] }],
|
||||
floating: vec![Floating::single(vec![3], 0, [41.0, 52.0, 233.0, 244.0])],
|
||||
};
|
||||
let saved = layout.clone();
|
||||
let first = layout.location(&1).unwrap();
|
||||
@@ -197,12 +197,12 @@ fn closing_and_reopening_an_entire_accordion_restores_its_kind_and_heights() {
|
||||
#[test]
|
||||
fn invalid_saved_layouts_reject_duplicates_selection_depth_and_nonfinite_geometry() {
|
||||
let mut duplicate = sample();
|
||||
duplicate.floating[0].panels.push(1);
|
||||
duplicate.floating[0].groups[0].panels.push(1);
|
||||
assert_eq!(duplicate.validate(), Err(DockError::DuplicatePanel));
|
||||
let mut invalid = sample();
|
||||
invalid.floating[0].active = usize::MAX;
|
||||
invalid.floating[0].groups[0].active = usize::MAX;
|
||||
assert_eq!(invalid.validate(), Err(DockError::InvalidSelection));
|
||||
invalid.floating[0].active = 0;
|
||||
invalid.floating[0].groups[0].active = 0;
|
||||
invalid.floating[0].rect[3] = f32::INFINITY;
|
||||
assert_eq!(invalid.validate(), Err(DockError::InvalidGeometry));
|
||||
let mut deep = tabs(&[0], 0);
|
||||
@@ -404,7 +404,7 @@ fn actual_pointer_drag_docks_a_floating_panel_into_the_main_tab_strip() {
|
||||
step(&ctx, &mut layout, vec![Event::PointerMoved(target)]);
|
||||
let actions = step(&ctx, &mut layout, vec![pointer_button(target, false)]);
|
||||
assert!(actions.iter().any(|a| matches!(a, Action::Move { panel: 5, .. })), "{actions:?}");
|
||||
assert!(!layout.floating.iter().any(|g| g.panels.contains(&5)));
|
||||
assert!(!layout.floating.iter().any(|g| g.contains(&5)));
|
||||
assert_eq!(layout.panels().len(), 6);
|
||||
let Node::Split { second, .. } = layout.root.as_ref().unwrap() else { panic!() };
|
||||
let Node::Tabs { panels, active } = second.as_ref() else { panic!() };
|
||||
@@ -429,7 +429,7 @@ fn tab_dragged_out_of_the_window_floats_where_it_left() {
|
||||
step(&ctx, &mut layout, vec![Event::PointerMoved(exit)]);
|
||||
let actions = step(&ctx, &mut layout, vec![Event::PointerGone]);
|
||||
assert!(actions.iter().any(|a| matches!(a, Action::Float { panel: 3, .. })), "{actions:?}");
|
||||
assert!(layout.floating.iter().any(|g| g.panels == [3]), "{layout:?}");
|
||||
assert!(layout.floating.iter().any(|g| g.panels().eq([3].iter())), "{layout:?}");
|
||||
let back = pos2(400.0, 300.0);
|
||||
assert!(step(&ctx, &mut layout, vec![Event::PointerMoved(back)]).is_empty());
|
||||
assert!(step(&ctx, &mut layout, vec![pointer_button(back, false)]).is_empty(), "the drag ended when the pointer left");
|
||||
@@ -634,7 +634,7 @@ fn restoring_a_panel_beside_a_nested_subtree_preserves_the_complete_workspace()
|
||||
#[test]
|
||||
fn floating_only_surface_does_not_allocate_the_callers_document_region() {
|
||||
let ctx = Context::default();
|
||||
let layout = Layout { root: None, floating: vec![Floating { panels: vec![1u32], active: 0, rect: [30.0, 40.0, 200.0, 200.0] }] };
|
||||
let layout = Layout { root: None, floating: vec![Floating::single(vec![1u32], 0, [30.0, 40.0, 200.0, 200.0])] };
|
||||
ctx.run_ui(RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0))), ..Default::default() }, |ui| {
|
||||
let before = ui.available_rect_before_wrap();
|
||||
let out = DockArea::new(Id::new("float-only")).show(
|
||||
@@ -666,8 +666,8 @@ fn floating_group_grip_moves_every_member_while_tab_drag_detaches_one() {
|
||||
step(&ctx, &mut layout, vec![Event::PointerMoved(start), pointer_button(start, true)]);
|
||||
let actions = step(&ctx, &mut layout, vec![Event::PointerMoved(start + vec2(50.0, 40.0))]);
|
||||
assert!(actions.iter().any(|a| matches!(a, Action::MoveFloating { .. })));
|
||||
assert_eq!(layout.floating[0].panels, original.panels);
|
||||
assert_eq!(layout.floating[0].active, original.active);
|
||||
assert_eq!(layout.floating[0].groups[0].panels, original.groups[0].panels);
|
||||
assert_eq!(layout.floating[0].groups[0].active, original.groups[0].active);
|
||||
assert_ne!(layout.floating[0].rect, original.rect);
|
||||
step(&ctx, &mut layout, vec![pointer_button(start + vec2(50.0, 40.0), false)]);
|
||||
for _ in 0..2 {
|
||||
@@ -680,7 +680,7 @@ fn floating_group_grip_moves_every_member_while_tab_drag_detaches_one() {
|
||||
let detached = step(&ctx, &mut layout, vec![pointer_button(tab + vec2(100.0, 100.0), false)]);
|
||||
assert!(detached.iter().any(|a| matches!(a, Action::Float { panel: 5, .. })));
|
||||
assert_eq!(layout.floating.len(), 2);
|
||||
assert_eq!(layout.floating[0].panels, vec![6]);
|
||||
assert_eq!(layout.floating[0].groups[0].panels, vec![6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1026,7 +1026,7 @@ fn chrome_focus_classifies_rendered_tab_but_excludes_body_text_field_and_invalid
|
||||
assert!(!DockArea::new(Id::new("focus-area")).chrome_has_focus(&ctx, &layout));
|
||||
paint(tab);
|
||||
let mut invalid = layout.clone();
|
||||
invalid.floating.push(Floating { panels: vec![3], active: 0, rect: [10.0, 10.0, 100.0, 100.0] });
|
||||
invalid.floating.push(Floating::single(vec![3], 0, [10.0, 10.0, 100.0, 100.0]));
|
||||
assert!(!DockArea::new(Id::new("focus-area")).chrome_has_focus(&ctx, &invalid));
|
||||
assert!(!DockArea::<u32>::new(Id::new("focus-area")).chrome_has_focus(&ctx, &Layout::default()));
|
||||
ctx.memory_mut(|memory| memory.request_focus(Id::new("unrendered")));
|
||||
|
||||
@@ -55,7 +55,7 @@ impl Harness {
|
||||
first: Box::new(Node::Tabs { panels: vec![CANVAS], active: 0 }),
|
||||
second: Box::new(Node::Tabs { panels: vec![LAYERS, PROPERTIES], active: 0 }),
|
||||
}),
|
||||
floating: vec![Floating { panels: vec![BRUSHES], active: 0, rect: [38.0, 76.0, 210.0, 250.0] }],
|
||||
floating: vec![Floating::single(vec![BRUSHES], 0, [38.0, 76.0, 210.0, 250.0])],
|
||||
};
|
||||
let mut harness = Self {
|
||||
ctx,
|
||||
@@ -235,7 +235,7 @@ fn floating_brushes_merge_detach_and_redock_through_real_pointer_input() {
|
||||
assert_eq!(h.body_ids[&BRUSHES], brush_id);
|
||||
h.capture(&directory, "02-merged-main-tabs");
|
||||
h.drag(h.tab(BRUSHES).center(), pos2((width - 240.0) * 0.5, 270.0));
|
||||
assert!(h.layout.floating.iter().any(|g| g.panels.contains(&BRUSHES)), "{name}: {:?}", h.applied);
|
||||
assert!(h.layout.floating.iter().any(|g| g.contains(&BRUSHES)), "{name}: {:?}", h.applied);
|
||||
h.capture(&directory, "03-detached-again");
|
||||
h.drag(h.tab(BRUSHES).center(), h.tab(LAYERS).left_center() + vec2(4.0, 0.0));
|
||||
assert!(h.layout.floating.is_empty(), "{name}");
|
||||
|
||||
Reference in new issue
Block a user