|
|
|
@@ -2,6 +2,11 @@ use graphene_std::Color;
|
|
|
|
|
use graphene_std::raster::Image;
|
|
|
|
|
use graphene_std::text::{Font, FontCache};
|
|
|
|
|
|
|
|
|
|
/// Proportional share (0-1) for the document panel's side when splitting adjacent to non-document panels.
|
|
|
|
|
const DOCUMENT_PANEL_SHARE: f64 = 0.8;
|
|
|
|
|
/// Proportional share for each side when neither (or both) contain the document panel.
|
|
|
|
|
const EQUAL_PANEL_SHARE: f64 = 0.5;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Default)]
|
|
|
|
|
pub struct CachedData {
|
|
|
|
|
pub font_cache: FontCache,
|
|
|
|
@@ -244,25 +249,54 @@ impl WorkspacePanelLayout {
|
|
|
|
|
/// The direction determines where the new group goes relative to the target.
|
|
|
|
|
/// Left/Right creates a horizontal (row) split, Top/Bottom creates a vertical (column) split.
|
|
|
|
|
/// Returns the ID of the newly created panel group, or `None` if insertion failed.
|
|
|
|
|
pub fn split_panel_group(&mut self, target_group_id: PanelGroupId, direction: DockingSplitDirection, tabs: Vec<PanelType>, active_tab_index: usize) -> Option<PanelGroupId> {
|
|
|
|
|
///
|
|
|
|
|
/// `source_slot_size` overrides the new panel's size (for moves where the old slot will be pruned).
|
|
|
|
|
/// If `None`, target's slot is split in the default ratio.
|
|
|
|
|
pub fn split_panel_group(
|
|
|
|
|
&mut self,
|
|
|
|
|
target_group_id: PanelGroupId,
|
|
|
|
|
direction: DockingSplitDirection,
|
|
|
|
|
tabs: Vec<PanelType>,
|
|
|
|
|
active_tab_index: usize,
|
|
|
|
|
source_slot_size: Option<f64>,
|
|
|
|
|
) -> Option<PanelGroupId> {
|
|
|
|
|
let new_id = self.next_id();
|
|
|
|
|
let new_group = SplitChild {
|
|
|
|
|
subdivision: PanelLayoutSubdivision::PanelGroup {
|
|
|
|
|
id: new_id,
|
|
|
|
|
state: PanelGroupState { tabs, active_tab_index },
|
|
|
|
|
},
|
|
|
|
|
size: 50.,
|
|
|
|
|
size: source_slot_size.unwrap_or(EQUAL_PANEL_SHARE),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let insert_before = matches!(direction, DockingSplitDirection::Left | DockingSplitDirection::Top);
|
|
|
|
|
let needs_horizontal = matches!(direction, DockingSplitDirection::Left | DockingSplitDirection::Right);
|
|
|
|
|
|
|
|
|
|
self.root.insert_split_adjacent(target_group_id, new_group, insert_before, needs_horizontal, 0).then_some(new_id)
|
|
|
|
|
self.root
|
|
|
|
|
.insert_split_adjacent(target_group_id, new_group, insert_before, needs_horizontal, 0, source_slot_size)
|
|
|
|
|
.then_some(new_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Find the slot size of the panel group whose entire content is exactly the given tabs.
|
|
|
|
|
/// Returns `None` if the tabs span multiple groups or don't fill their group exactly.
|
|
|
|
|
pub fn find_source_slot_size(&self, tabs: &[PanelType]) -> Option<f64> {
|
|
|
|
|
if tabs.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let group_id = self.find_panel(tabs[0])?;
|
|
|
|
|
if !tabs.iter().all(|&t| self.find_panel(t) == Some(group_id)) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let group = self.panel_group(group_id)?;
|
|
|
|
|
if group.tabs.len() != tabs.len() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
self.root.find_slot_size_by_group_id(group_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Recalculate the default sizes for all splits in the tree based on document panel proximity.
|
|
|
|
|
pub fn recalculate_default_sizes(&mut self) {
|
|
|
|
|
self.root.recalculate_default_sizes();
|
|
|
|
|
self.root.recalculate_default_sizes_recursive();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Remember which panel group and tab index a panel was in before removal, so it can be restored there later.
|
|
|
|
@@ -311,10 +345,10 @@ impl WorkspacePanelLayout {
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
size: match panel_type {
|
|
|
|
|
PanelType::Data => 30.,
|
|
|
|
|
PanelType::Properties => 45.,
|
|
|
|
|
PanelType::Layers => 55.,
|
|
|
|
|
_ => 50.,
|
|
|
|
|
PanelType::Data => 0.3,
|
|
|
|
|
PanelType::Properties => 0.45,
|
|
|
|
|
PanelType::Layers => 0.55,
|
|
|
|
|
_ => EQUAL_PANEL_SHARE,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -330,7 +364,10 @@ impl WorkspacePanelLayout {
|
|
|
|
|
if !matches!(&self.root, PanelLayoutSubdivision::Split { .. }) {
|
|
|
|
|
let old_root = std::mem::replace(&mut self.root, PanelLayoutSubdivision::Split { children: vec![] });
|
|
|
|
|
if let PanelLayoutSubdivision::Split { children } = &mut self.root {
|
|
|
|
|
children.push(SplitChild { subdivision: old_root, size: 80. });
|
|
|
|
|
children.push(SplitChild {
|
|
|
|
|
subdivision: old_root,
|
|
|
|
|
size: DOCUMENT_PANEL_SHARE,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -340,7 +377,7 @@ impl WorkspacePanelLayout {
|
|
|
|
|
while root_children.len() <= root_child_index {
|
|
|
|
|
root_children.push(SplitChild {
|
|
|
|
|
subdivision: PanelLayoutSubdivision::Split { children: vec![] },
|
|
|
|
|
size: 20.,
|
|
|
|
|
size: (1. - DOCUMENT_PANEL_SHARE),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -351,7 +388,7 @@ impl WorkspacePanelLayout {
|
|
|
|
|
if let PanelLayoutSubdivision::Split { children } = target {
|
|
|
|
|
children.push(SplitChild {
|
|
|
|
|
subdivision: old_subdivision,
|
|
|
|
|
size: 50.,
|
|
|
|
|
size: EQUAL_PANEL_SHARE,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -386,10 +423,10 @@ impl Default for WorkspacePanelLayout {
|
|
|
|
|
active_tab_index: 0,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
size: 100.,
|
|
|
|
|
size: 1.,
|
|
|
|
|
}],
|
|
|
|
|
},
|
|
|
|
|
size: 80.,
|
|
|
|
|
size: DOCUMENT_PANEL_SHARE,
|
|
|
|
|
},
|
|
|
|
|
SplitChild {
|
|
|
|
|
subdivision: PanelLayoutSubdivision::Split {
|
|
|
|
@@ -402,7 +439,7 @@ impl Default for WorkspacePanelLayout {
|
|
|
|
|
active_tab_index: 0,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
size: 50.,
|
|
|
|
|
size: EQUAL_PANEL_SHARE,
|
|
|
|
|
},
|
|
|
|
|
SplitChild {
|
|
|
|
|
subdivision: PanelLayoutSubdivision::PanelGroup {
|
|
|
|
@@ -412,11 +449,11 @@ impl Default for WorkspacePanelLayout {
|
|
|
|
|
active_tab_index: 0,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
size: 50.,
|
|
|
|
|
size: EQUAL_PANEL_SHARE,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
size: 20.,
|
|
|
|
|
size: (1. - DOCUMENT_PANEL_SHARE),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
@@ -427,6 +464,15 @@ impl Default for WorkspacePanelLayout {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The share of the slot that should go to the old side when splitting it with a new side.
|
|
|
|
|
fn document_split_share(old_side: &PanelLayoutSubdivision, new_side: &PanelLayoutSubdivision) -> f64 {
|
|
|
|
|
match (old_side.contains_document(), new_side.contains_document()) {
|
|
|
|
|
(true, false) => DOCUMENT_PANEL_SHARE,
|
|
|
|
|
(false, true) => 1. - DOCUMENT_PANEL_SHARE,
|
|
|
|
|
_ => EQUAL_PANEL_SHARE,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PanelLayoutSubdivision {
|
|
|
|
|
/// Find the panel group state for a given ID.
|
|
|
|
|
pub fn find_group(&self, target_id: PanelGroupId) -> Option<&PanelGroupState> {
|
|
|
|
@@ -463,9 +509,10 @@ impl PanelLayoutSubdivision {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Remove empty panel groups and collapse unnecessary nesting.
|
|
|
|
|
/// Does NOT collapse single-child splits into their child, as that would change subdivision depths
|
|
|
|
|
/// and break the direction-by-depth alternation system.
|
|
|
|
|
/// Remove empty groups/splits and flatten single-child `Split`-in-`Split` nesting (which docking sequences can create).
|
|
|
|
|
///
|
|
|
|
|
/// Flattening preserves depth parity (and therefore direction). `PanelGroup`-only single-child splits are left
|
|
|
|
|
/// alone since collapsing would change the panel's depth and alter future wrap orientation.
|
|
|
|
|
pub fn prune(&mut self) {
|
|
|
|
|
let PanelLayoutSubdivision::Split { children } = self else { return };
|
|
|
|
|
|
|
|
|
@@ -477,6 +524,54 @@ impl PanelLayoutSubdivision {
|
|
|
|
|
|
|
|
|
|
// Remove empty splits (splits that lost all their children after pruning)
|
|
|
|
|
children.retain(|child| !matches!(&child.subdivision, PanelLayoutSubdivision::Split { children } if children.is_empty()));
|
|
|
|
|
|
|
|
|
|
// Flatten single-child `Split`-in-`Split` nesting, rescaling sizes to preserve visual proportions
|
|
|
|
|
let mut i = 0;
|
|
|
|
|
while i < children.len() {
|
|
|
|
|
// Must be a `Split`...
|
|
|
|
|
let PanelLayoutSubdivision::Split { children: outer } = &children[i].subdivision else {
|
|
|
|
|
i += 1;
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
// ...with exactly one child...
|
|
|
|
|
let [only_child] = outer.as_slice() else {
|
|
|
|
|
i += 1;
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
// ...that is itself a `Split`
|
|
|
|
|
let PanelLayoutSubdivision::Split { .. } = &only_child.subdivision else {
|
|
|
|
|
i += 1;
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Remove the redundant wrapper
|
|
|
|
|
let removed = children.remove(i);
|
|
|
|
|
let outer_size = removed.size;
|
|
|
|
|
|
|
|
|
|
// Extract the inner grandchildren
|
|
|
|
|
let PanelLayoutSubdivision::Split { children: mut outer_children } = removed.subdivision else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
let Some(inner_split) = outer_children.pop() else { continue };
|
|
|
|
|
let PanelLayoutSubdivision::Split { children: inner_children } = inner_split.subdivision else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Splice grandchildren in at the same position, scaling their sizes to fill the removed slot
|
|
|
|
|
let inner_total: f64 = inner_children.iter().map(|c| c.size).sum();
|
|
|
|
|
for (offset, mut grandchild) in inner_children.into_iter().enumerate() {
|
|
|
|
|
grandchild.size = if inner_total > 0. { grandchild.size / inner_total * outer_size } else { outer_size };
|
|
|
|
|
children.insert(i + offset, grandchild);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Renormalize to sum=1 since dock/prune cycles can compound shrinkage
|
|
|
|
|
let total: f64 = children.iter().map(|c| c.size).sum();
|
|
|
|
|
if total > 0. && (total - 1.).abs() > 0.001 {
|
|
|
|
|
for child in children.iter_mut() {
|
|
|
|
|
child.size /= total;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Remove all non-document/non-welcome tabs from panel groups, leaving only document-related panels.
|
|
|
|
@@ -503,7 +598,9 @@ impl PanelLayoutSubdivision {
|
|
|
|
|
/// Inserts a new split child adjacent to a target panel group and returns whether the insertion was successful.
|
|
|
|
|
/// Recurses to the deepest split closest to the target that matches the requested split direction.
|
|
|
|
|
/// If the target is a direct child of a mismatched-direction split, this wraps it in a new sub-split.
|
|
|
|
|
pub fn insert_split_adjacent(&mut self, target_id: PanelGroupId, new_child: SplitChild, insert_before: bool, needs_horizontal: bool, depth: usize) -> bool {
|
|
|
|
|
///
|
|
|
|
|
/// `source_slot_size` preserves the moved panel's visual weight. If `None`, uses the default split ratio.
|
|
|
|
|
pub fn insert_split_adjacent(&mut self, target_id: PanelGroupId, new_child: SplitChild, insert_before: bool, needs_horizontal: bool, depth: usize, source_slot_size: Option<f64>) -> bool {
|
|
|
|
|
let PanelLayoutSubdivision::Split { children } = self else { return false };
|
|
|
|
|
|
|
|
|
|
let is_horizontal = depth.is_multiple_of(2);
|
|
|
|
@@ -517,18 +614,35 @@ impl PanelLayoutSubdivision {
|
|
|
|
|
// If the target is a direct child: we can certainly insert the new split, either as a sibling (if direction matches) or wrapping the target in a new split (if direction is mismatched)
|
|
|
|
|
let target_is_direct_child = matches!(&children[containing_index].subdivision, PanelLayoutSubdivision::PanelGroup { id, .. } if *id == target_id);
|
|
|
|
|
if target_is_direct_child {
|
|
|
|
|
// Direction matches and target is right here: insert as a sibling
|
|
|
|
|
// Direction matches: insert as sibling, sizing based on whether target will be pruned, source size hint, or default ratio
|
|
|
|
|
if direction_matches {
|
|
|
|
|
let mut new_child = new_child;
|
|
|
|
|
let target_will_be_pruned = matches!(&children[containing_index].subdivision, PanelLayoutSubdivision::PanelGroup { state, .. } if state.tabs.is_empty());
|
|
|
|
|
if target_will_be_pruned {
|
|
|
|
|
new_child.size = children[containing_index].size;
|
|
|
|
|
} else if let Some(hint) = source_slot_size {
|
|
|
|
|
new_child.size = hint;
|
|
|
|
|
} else {
|
|
|
|
|
let target_share = document_split_share(&children[containing_index].subdivision, &new_child.subdivision);
|
|
|
|
|
let total = children[containing_index].size;
|
|
|
|
|
children[containing_index].size = total * target_share;
|
|
|
|
|
new_child.size = total * (1. - target_share);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let insert_index = if insert_before { containing_index } else { containing_index + 1 };
|
|
|
|
|
children.insert(insert_index, new_child);
|
|
|
|
|
}
|
|
|
|
|
// Direction mismatch: wrap the target in a new sub-split (at depth+1, which has the opposite direction of this and thus is the requested direction)
|
|
|
|
|
// Direction mismatch: wrap target in a sub-split at depth+1, sharing the slot in the default ratio
|
|
|
|
|
else {
|
|
|
|
|
let old_child_subdivision = std::mem::replace(&mut children[containing_index].subdivision, PanelLayoutSubdivision::Split { children: vec![] });
|
|
|
|
|
|
|
|
|
|
let old_share = document_split_share(&old_child_subdivision, &new_child.subdivision);
|
|
|
|
|
let old_child = SplitChild {
|
|
|
|
|
subdivision: old_child_subdivision,
|
|
|
|
|
size: 50.,
|
|
|
|
|
size: old_share,
|
|
|
|
|
};
|
|
|
|
|
let mut new_child = new_child;
|
|
|
|
|
new_child.size = 1. - old_share;
|
|
|
|
|
|
|
|
|
|
if let PanelLayoutSubdivision::Split { children: sub_children } = &mut children[containing_index].subdivision {
|
|
|
|
|
if insert_before {
|
|
|
|
@@ -547,7 +661,25 @@ impl PanelLayoutSubdivision {
|
|
|
|
|
// The target is deeper, so recurse into the containing child's subtree and return its insertion outcome
|
|
|
|
|
children[containing_index]
|
|
|
|
|
.subdivision
|
|
|
|
|
.insert_split_adjacent(target_id, new_child.clone(), insert_before, needs_horizontal, depth + 1)
|
|
|
|
|
.insert_split_adjacent(target_id, new_child.clone(), insert_before, needs_horizontal, depth + 1, source_slot_size)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Find the size of the `SplitChild` slot whose subdivision is the panel group with the given ID, if it exists.
|
|
|
|
|
pub fn find_slot_size_by_group_id(&self, group_id: PanelGroupId) -> Option<f64> {
|
|
|
|
|
let PanelLayoutSubdivision::Split { children } = self else { return None };
|
|
|
|
|
for child in children {
|
|
|
|
|
if let PanelLayoutSubdivision::PanelGroup { id, .. } = &child.subdivision
|
|
|
|
|
&& *id == group_id
|
|
|
|
|
{
|
|
|
|
|
return Some(child.size);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for child in children {
|
|
|
|
|
if let Some(size) = child.subdivision.find_slot_size_by_group_id(group_id) {
|
|
|
|
|
return Some(size);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check if this subtree contains the document panel.
|
|
|
|
@@ -558,39 +690,46 @@ impl PanelLayoutSubdivision {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Recalculate the default sizes for this subdivision's children based on proximity to the document panel.
|
|
|
|
|
/// Recalculate the default sizes for this subdivision's direct children based on proximity to the document panel.
|
|
|
|
|
/// Splits directly surrounding the document panel use 80-20 weighting.
|
|
|
|
|
/// All other splits use equal division.
|
|
|
|
|
/// Does not recurse into descendants: use [`Self::recalculate_default_sizes_recursive`] for that.
|
|
|
|
|
pub fn recalculate_default_sizes(&mut self) {
|
|
|
|
|
if let PanelLayoutSubdivision::Split { children } = self {
|
|
|
|
|
let child_count = children.len();
|
|
|
|
|
if child_count == 0 {
|
|
|
|
|
return;
|
|
|
|
|
let PanelLayoutSubdivision::Split { children } = self else { return };
|
|
|
|
|
|
|
|
|
|
let child_count = children.len();
|
|
|
|
|
if child_count == 0 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if any child directly contains (or is) the document panel
|
|
|
|
|
let document_child_index = children.iter().position(|child| child.subdivision.contains_document());
|
|
|
|
|
|
|
|
|
|
if let Some(document_index) = document_child_index {
|
|
|
|
|
// This split directly surrounds the document panel
|
|
|
|
|
let non_document_count = child_count - 1;
|
|
|
|
|
let document_share = if non_document_count > 0 { DOCUMENT_PANEL_SHARE } else { 1. };
|
|
|
|
|
let other_share = if non_document_count > 0 { (1. - DOCUMENT_PANEL_SHARE) / non_document_count as f64 } else { 0. };
|
|
|
|
|
|
|
|
|
|
for (i, child) in children.iter_mut().enumerate() {
|
|
|
|
|
child.size = if i == document_index { document_share } else { other_share };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if any child directly contains (or is) the document panel
|
|
|
|
|
let document_child_index = children.iter().position(|child| child.subdivision.contains_document());
|
|
|
|
|
|
|
|
|
|
if let Some(document_index) = document_child_index {
|
|
|
|
|
// This split directly surrounds the document panel, so use 80-20 weighting
|
|
|
|
|
let non_document_count = child_count - 1;
|
|
|
|
|
let document_share = if non_document_count > 0 { 80. } else { 100. };
|
|
|
|
|
let other_share = if non_document_count > 0 { 20. / non_document_count as f64 } else { 0. };
|
|
|
|
|
|
|
|
|
|
for (i, child) in children.iter_mut().enumerate() {
|
|
|
|
|
child.size = if i == document_index { document_share } else { other_share };
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// This split doesn't directly contain the document, use equal division
|
|
|
|
|
let equal_share = 100. / child_count as f64;
|
|
|
|
|
for child in children.iter_mut() {
|
|
|
|
|
child.size = equal_share;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Recurse into children
|
|
|
|
|
} else {
|
|
|
|
|
// This split doesn't directly contain the document, use equal division
|
|
|
|
|
let equal_share = 1. / child_count as f64;
|
|
|
|
|
for child in children.iter_mut() {
|
|
|
|
|
child.subdivision.recalculate_default_sizes();
|
|
|
|
|
child.size = equal_share;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Recalculate the default sizes for this subdivision and all its descendant splits.
|
|
|
|
|
pub fn recalculate_default_sizes_recursive(&mut self) {
|
|
|
|
|
self.recalculate_default_sizes();
|
|
|
|
|
|
|
|
|
|
if let PanelLayoutSubdivision::Split { children } = self {
|
|
|
|
|
for child in children.iter_mut() {
|
|
|
|
|
child.subdivision.recalculate_default_sizes_recursive();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|