Fix additional panel docking and restoration bugs (#4089)
* Fix another panel docking bug where restoring would leave a blank no-panel space * Improve panel hide/show to restore position, size, and adjacency * Check full subtree for document panel and avoid double traversal in insert
This commit is contained in:
parent
0acfd3e178
commit
eddd742f9b
|
|
@ -1933,6 +1933,7 @@ impl PortfolioMessageHandler {
|
||||||
} else {
|
} else {
|
||||||
// Panel is not present, restore it to its default position in the layout tree
|
// Panel is not present, restore it to its default position in the layout tree
|
||||||
self.workspace_panel_layout.restore_panel(panel_type);
|
self.workspace_panel_layout.restore_panel(panel_type);
|
||||||
|
self.workspace_panel_layout.prune();
|
||||||
self.refresh_panel_content(panel_type, responses);
|
self.refresh_panel_content(panel_type, responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,21 @@ pub struct SplitChild {
|
||||||
pub size: f64,
|
pub size: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remembers where a panel was before it was removed, so it can be restored to the same location.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct SavedPanelPosition {
|
||||||
|
panel_type: PanelType,
|
||||||
|
/// The group the panel was a tab in.
|
||||||
|
group_id: PanelGroupId,
|
||||||
|
/// Which tab index it occupied within that group.
|
||||||
|
tab_index: usize,
|
||||||
|
/// The group's slot size at the time of removal (used to restore its visual weight via the sibling fallback).
|
||||||
|
slot_size: Option<f64>,
|
||||||
|
/// When the panel was the sole tab (so the group will be pruned), a neighboring group and
|
||||||
|
/// whether to insert before it (`true`) or after it (`false`) to recreate the original position.
|
||||||
|
sibling_fallback: Option<(PanelGroupId, bool)>,
|
||||||
|
}
|
||||||
|
|
||||||
/// The complete workspace panel layout as a tree of nested rows and columns.
|
/// The complete workspace panel layout as a tree of nested rows and columns.
|
||||||
/// The root subdivision is always a row (horizontal split). Direction alternates at each depth.
|
/// The root subdivision is always a row (horizontal split). Direction alternates at each depth.
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
|
|
@ -191,9 +206,9 @@ pub struct WorkspacePanelLayout {
|
||||||
/// Counter for generating unique panel group IDs.
|
/// Counter for generating unique panel group IDs.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
next_group_id: PanelGroupId,
|
next_group_id: PanelGroupId,
|
||||||
/// Remembers where a panel was before being removed (panel type, group ID, and tab index), so it can be restored there.
|
/// Remembers where each panel was before removal so it can be restored there.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
saved_positions: Vec<(PanelType, PanelGroupId, usize)>,
|
saved_positions: Vec<SavedPanelPosition>,
|
||||||
/// Whether Focus Document mode is active, hiding all non-document panels.
|
/// Whether Focus Document mode is active, hiding all non-document panels.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub focus_document: bool,
|
pub focus_document: bool,
|
||||||
|
|
@ -299,33 +314,69 @@ impl WorkspacePanelLayout {
|
||||||
self.root.recalculate_default_sizes_recursive();
|
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.
|
/// Remember where a panel was before removal so it can be restored there later.
|
||||||
|
/// Saves the group ID and tab index. If the panel is the sole tab (so the group will be pruned),
|
||||||
|
/// also saves a sibling group as a fallback for adjacency-based restoration.
|
||||||
pub fn save_panel_position(&mut self, panel_type: PanelType) {
|
pub fn save_panel_position(&mut self, panel_type: PanelType) {
|
||||||
if let Some(group_id) = self.find_panel(panel_type) {
|
let Some(group_id) = self.find_panel(panel_type) else { return };
|
||||||
let tab_index = self.panel_group(group_id).and_then(|g| g.tabs.iter().position(|&t| t == panel_type)).unwrap_or(0);
|
let tab_index = self.panel_group(group_id).and_then(|g| g.tabs.iter().position(|&t| t == panel_type)).unwrap_or(0);
|
||||||
|
let is_sole_tab = self.panel_group(group_id).is_some_and(|g| g.tabs.len() == 1);
|
||||||
|
|
||||||
// Replace any existing saved position for this panel type
|
// When it's the sole tab, the group will be pruned, so save a sibling and slot size as fallback
|
||||||
self.saved_positions.retain(|(pt, _, _)| *pt != panel_type);
|
let sibling_fallback = if is_sole_tab { self.root.find_sibling_group(group_id) } else { None };
|
||||||
self.saved_positions.push((panel_type, group_id, tab_index));
|
let slot_size = if is_sole_tab { self.root.find_slot_size_by_group_id(group_id) } else { None };
|
||||||
}
|
|
||||||
|
self.saved_positions.retain(|s| s.panel_type != panel_type);
|
||||||
|
self.saved_positions.push(SavedPanelPosition {
|
||||||
|
panel_type,
|
||||||
|
group_id,
|
||||||
|
tab_index,
|
||||||
|
slot_size,
|
||||||
|
sibling_fallback,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restore a panel to its previous position if available, otherwise to its default position.
|
/// Restore a panel to its previous position if available, otherwise to its default position.
|
||||||
/// If the panel was previously in a group that still exists, it's added back as a tab at its original index.
|
|
||||||
/// Otherwise, it's placed at its default structural position in the tree.
|
|
||||||
pub fn restore_panel(&mut self, panel_type: PanelType) {
|
pub fn restore_panel(&mut self, panel_type: PanelType) {
|
||||||
// Try to restore to the previously saved group and tab position
|
let saved = self.saved_positions.iter().find(|s| s.panel_type == panel_type).copied();
|
||||||
let saved = self.saved_positions.iter().find(|(pt, _, _)| *pt == panel_type).copied();
|
self.saved_positions.retain(|s| s.panel_type != panel_type);
|
||||||
if let Some((_, saved_group_id, saved_tab_index)) = saved
|
|
||||||
&& let Some(group) = self.panel_group_mut(saved_group_id)
|
let Some(saved) = saved else {
|
||||||
{
|
self.restore_panel_to_default_position(panel_type);
|
||||||
let insert_index = saved_tab_index.min(group.tabs.len());
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Primary: restore as a tab in the original group if it still exists
|
||||||
|
if let Some(group) = self.panel_group_mut(saved.group_id) {
|
||||||
|
let insert_index = saved.tab_index.min(group.tabs.len());
|
||||||
group.tabs.insert(insert_index, panel_type);
|
group.tabs.insert(insert_index, panel_type);
|
||||||
group.active_tab_index = insert_index;
|
group.active_tab_index = insert_index;
|
||||||
self.saved_positions.retain(|(pt, _, _)| *pt != panel_type);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.saved_positions.retain(|(pt, _, _)| *pt != panel_type);
|
|
||||||
|
// Fallback: the original group was pruned, but a sibling in the same parent split survives
|
||||||
|
if let Some((sibling_id, before_sibling)) = saved.sibling_fallback
|
||||||
|
&& self.root.contains_group(sibling_id)
|
||||||
|
{
|
||||||
|
let new_id = self.next_id();
|
||||||
|
let new_subdivision = PanelLayoutSubdivision::PanelGroup {
|
||||||
|
id: new_id,
|
||||||
|
state: PanelGroupState {
|
||||||
|
tabs: vec![panel_type],
|
||||||
|
active_tab_index: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_group = SplitChild {
|
||||||
|
subdivision: new_subdivision,
|
||||||
|
size: saved.slot_size.unwrap_or_else(|| {
|
||||||
|
let sibling_contains_document = self.root.find_subtree_containing_group(sibling_id).is_some_and(|s| s.contains_document());
|
||||||
|
if sibling_contains_document { 1. - DOCUMENT_PANEL_SHARE } else { EQUAL_PANEL_SHARE }
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
self.root.insert_adjacent_to_group(sibling_id, new_group, before_sibling);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.restore_panel_to_default_position(panel_type);
|
self.restore_panel_to_default_position(panel_type);
|
||||||
}
|
}
|
||||||
|
|
@ -344,12 +395,7 @@ impl WorkspacePanelLayout {
|
||||||
active_tab_index: 0,
|
active_tab_index: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
size: match panel_type {
|
size: EQUAL_PANEL_SHARE,
|
||||||
PanelType::Data => 0.3,
|
|
||||||
PanelType::Properties => 0.45,
|
|
||||||
PanelType::Layers => 0.55,
|
|
||||||
_ => EQUAL_PANEL_SHARE,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine which root child column to insert into and at which position
|
// Determine which root child column to insert into and at which position
|
||||||
|
|
@ -400,6 +446,12 @@ impl WorkspacePanelLayout {
|
||||||
children.insert(0, new_group);
|
children.insert(0, new_group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recalculate sizes within the target column to get the correct document-aware ratio
|
||||||
|
let PanelLayoutSubdivision::Split { children: root_children } = &mut self.root else { return };
|
||||||
|
if let Some(target) = root_children.get_mut(root_child_index) {
|
||||||
|
target.subdivision.recalculate_default_sizes();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -587,6 +639,80 @@ impl PanelLayoutSubdivision {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the nearest sibling panel group for the given group within the same parent split.
|
||||||
|
/// Returns the sibling's ID and whether the target was before it (`true`) or after it (`false`).
|
||||||
|
/// Prefers the immediately previous sibling (with before=false meaning "insert after it"), falling
|
||||||
|
/// back to the next sibling (with before=true) so all positions in a 3-child split are distinguishable.
|
||||||
|
pub fn find_sibling_group(&self, target_id: PanelGroupId) -> Option<(PanelGroupId, bool)> {
|
||||||
|
let PanelLayoutSubdivision::Split { children } = self else { return None };
|
||||||
|
|
||||||
|
let target_index = children
|
||||||
|
.iter()
|
||||||
|
.position(|child| matches!(&child.subdivision, PanelLayoutSubdivision::PanelGroup { id, .. } if *id == target_id));
|
||||||
|
|
||||||
|
if let Some(index) = target_index {
|
||||||
|
let previous = (0..index).rev().find_map(|i| Self::group_id_of(&children[i]).map(|id| (id, false)));
|
||||||
|
let next = ((index + 1)..children.len()).find_map(|i| Self::group_id_of(&children[i]).map(|id| (id, true)));
|
||||||
|
return previous.or(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
children.iter().find_map(|child| child.subdivision.find_sibling_group(target_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a panel group ID from a child, either directly or the first one in a subtree.
|
||||||
|
fn group_id_of(child: &SplitChild) -> Option<PanelGroupId> {
|
||||||
|
match &child.subdivision {
|
||||||
|
PanelLayoutSubdivision::PanelGroup { id, .. } => Some(*id),
|
||||||
|
sub => sub.first_group_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the first panel group ID found in this subtree.
|
||||||
|
fn first_group_id(&self) -> Option<PanelGroupId> {
|
||||||
|
match self {
|
||||||
|
PanelLayoutSubdivision::PanelGroup { id, .. } => Some(*id),
|
||||||
|
PanelLayoutSubdivision::Split { children } => children.iter().find_map(|child| child.subdivision.first_group_id()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a new split child immediately before or after the given group in its parent split,
|
||||||
|
/// scaling existing siblings down proportionally to make room for the new child's size.
|
||||||
|
/// Returns whether the insertion was performed.
|
||||||
|
pub fn insert_adjacent_to_group(&mut self, sibling_id: PanelGroupId, new_child: SplitChild, before_sibling: bool) -> bool {
|
||||||
|
let PanelLayoutSubdivision::Split { children } = self else { return false };
|
||||||
|
|
||||||
|
let sibling_index = children
|
||||||
|
.iter()
|
||||||
|
.position(|child| matches!(&child.subdivision, PanelLayoutSubdivision::PanelGroup { id, .. } if *id == sibling_id));
|
||||||
|
if let Some(index) = sibling_index {
|
||||||
|
// Shrink existing siblings proportionally to make room
|
||||||
|
let old_total: f64 = children.iter().map(|c| c.size).sum();
|
||||||
|
let scale = if old_total > 0. { (old_total - new_child.size).max(0.) / old_total } else { 1. };
|
||||||
|
for child in children.iter_mut() {
|
||||||
|
child.size *= scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
let insert_at = if before_sibling { index } else { index + 1 };
|
||||||
|
children.insert(insert_at, new_child);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
children
|
||||||
|
.iter_mut()
|
||||||
|
.any(|child| child.subdivision.insert_adjacent_to_group(sibling_id, new_child.clone(), before_sibling))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the `SplitChild` subtree that contains the panel group with the given ID.
|
||||||
|
pub fn find_subtree_containing_group(&self, target_id: PanelGroupId) -> Option<&PanelLayoutSubdivision> {
|
||||||
|
let PanelLayoutSubdivision::Split { children } = self else { return None };
|
||||||
|
for child in children {
|
||||||
|
if child.subdivision.contains_group(target_id) {
|
||||||
|
return Some(&child.subdivision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if this subtree contains a panel group with the given ID.
|
/// Check if this subtree contains a panel group with the given ID.
|
||||||
pub fn contains_group(&self, target_id: PanelGroupId) -> bool {
|
pub fn contains_group(&self, target_id: PanelGroupId) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue