From 83d03ad67dd21c0688fd8844be342cd85b1800b5 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 1 May 2026 04:32:11 -0700 Subject: [PATCH] Fix panel docking bugs and polish its behavior (#4087) * Fix panel docking bugs and polish its behavior * Fix bug --- .../new_document_dialog_message_handler.rs | 8 +- .../overlays/overlays_message_handler.rs | 6 + .../messages/portfolio/portfolio_message.rs | 4 - .../portfolio/portfolio_message_handler.rs | 33 +-- .../src/messages/portfolio/utility_types.rs | 243 ++++++++++++++---- .../tool/tool_messages/select_tool.rs | 12 +- frontend/src/components/panels/Data.svelte | 23 +- frontend/src/components/panels/Layers.svelte | 72 +----- .../src/components/panels/Properties.svelte | 23 +- frontend/src/components/panels/Welcome.svelte | 24 +- frontend/src/components/window/Panel.svelte | 67 ++--- .../components/window/PanelSubdivision.svelte | 58 +++-- frontend/src/stores/portfolio.ts | 92 ++++++- frontend/wrapper/src/editor_wrapper.rs | 7 - 14 files changed, 417 insertions(+), 255 deletions(-) diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index ea321efd..cbfacb17 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -58,12 +58,10 @@ impl MessageHandler for NewDocumentDialogMessageHa responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(ViewportMessage::RepropagateUpdate); + responses.add(DocumentMessage::DeselectAllLayers); + responses.add(DeferMessage::AfterNavigationReady { - messages: vec![ - DocumentMessage::ZoomCanvasToFitAll.into(), - DocumentMessage::DeselectAllLayers.into(), - PortfolioMessage::AutoSaveActiveDocument.into(), - ], + messages: vec![DocumentMessage::ZoomCanvasToFitAll.into(), PortfolioMessage::AutoSaveActiveDocument.into()], }); responses.add(DocumentMessage::MarkAsSaved); diff --git a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs index 3bf43625..c7acb50c 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -29,6 +29,12 @@ impl MessageHandler> for OverlaysMes use crate::messages::viewport::{Position, ToPhysical}; use wasm_bindgen::JsCast; + // Discard detached canvas after a panel reorganization remounts the DOM + if self.canvas.as_ref().is_some_and(|canvas| !canvas.is_connected()) { + self.canvas = None; + self.context = None; + } + let canvas = match &self.canvas { Some(canvas) => canvas, None => { diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 979fae22..e1882789 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -195,10 +195,6 @@ pub enum PortfolioMessage { UpdateOpenDocumentsList, UpdateWorkspacePanelLayout, ResetWorkspaceLayout, - ResetPanelGroupSizes { - /// Path of child indices from the root to the split node whose children's sizes should be reset to defaults. - split_path: Vec, - }, SetPanelGroupSizes { /// Path of child indices from the root to the split node whose children's sizes are being set. split_path: Vec, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index b8e3c847..95305362 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -453,6 +453,13 @@ impl MessageHandler> for Portfolio if let Some(layout) = state.workspace_layout { self.workspace_panel_layout = layout; responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); + + // Refill panels whose content was lost when the layout load remounted their frontend components + for group_id in self.workspace_panel_layout.root.all_group_ids() { + if let Some(panel_type) = self.workspace_panel_layout.panel_group(group_id).and_then(|g| g.active_panel_type()) { + self.refresh_panel_content(panel_type, responses); + } + } } let PersistedState { @@ -1330,13 +1337,16 @@ impl MessageHandler> for Portfolio Self::destroy_panel_layouts(target_active, responses); } + // Preserve the source panel's visual weight at its new location + let source_slot_size = self.workspace_panel_layout.find_source_slot_size(&tabs); + // Remove the dragged tabs from their current panel groups (without pruning, so the target group survives) for &panel_type in &tabs { self.remove_panel_from_layout(panel_type); } // Create the new panel group adjacent to the target, then prune empty groups - let Some(new_id) = self.workspace_panel_layout.split_panel_group(target_group, direction, tabs.clone(), active_tab_index) else { + let Some(new_id) = self.workspace_panel_layout.split_panel_group(target_group, direction, tabs.clone(), active_tab_index, source_slot_size) else { log::error!("Failed to insert split adjacent to panel group {target_group:?}"); return; }; @@ -1611,20 +1621,6 @@ impl MessageHandler> for Portfolio responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); responses.add(MenuBarMessage::SendLayout); } - PortfolioMessage::ResetPanelGroupSizes { split_path } => { - // Walk the tree to the target split node using the path - let mut node = &mut self.workspace_panel_layout.root; - for &index in &split_path { - let PanelLayoutSubdivision::Split { children } = node else { return }; - let Some(child) = children.get_mut(index) else { return }; - node = &mut child.subdivision; - } - - // Recalculate default sizes for this split node - node.recalculate_default_sizes(); - - responses.add(PortfolioMessage::UpdateWorkspacePanelLayout); - } PortfolioMessage::SetPanelGroupSizes { split_path, sizes } => { // Walk the tree to the target split node using the path let mut node = &mut self.workspace_panel_layout.root; @@ -1977,7 +1973,12 @@ impl PortfolioMessageHandler { PanelType::Data => { // The Data panel's content is populated automatically as a side effect of the graph run completing, so there's nothing to do here } - PanelType::Document | PanelType::Welcome => {} + PanelType::Document | PanelType::Welcome => { + // Re-send the welcome screen buttons layout to repopulate after a remount + if self.document_ids.is_empty() { + responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout); + } + } } } } diff --git a/editor/src/messages/portfolio/utility_types.rs b/editor/src/messages/portfolio/utility_types.rs index f7e0ebb3..905ed4d4 100644 --- a/editor/src/messages/portfolio/utility_types.rs +++ b/editor/src/messages/portfolio/utility_types.rs @@ -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, active_tab_index: usize) -> Option { + /// + /// `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, + active_tab_index: usize, + source_slot_size: Option, + ) -> Option { 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 { + 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) -> 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 { + 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(); } } } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 0eb1518f..4202e374 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -1430,9 +1430,15 @@ impl Fsm for SelectToolFsmState { NestedSelectionBehavior::Deepest if remove => drag_deepest_manipulation(responses, selected, tool_data, document, true), NestedSelectionBehavior::Shallowest if !deepest => drag_shallowest_manipulation(responses, selected, tool_data, document, false, true), _ => { - responses.add(DocumentMessage::DeselectAllLayers); - tool_data.layers_dragging.clear(); - drag_deepest_manipulation(responses, selected, tool_data, document, false) + // Narrow multi-selection to just the clicked layer (no-op if it's already the sole selection) + let currently_selected = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect::>(); + let already_only_selection = currently_selected.as_slice() == [intersection]; + + if !already_only_selection { + responses.add(DocumentMessage::DeselectAllLayers); + tool_data.layers_dragging.clear(); + drag_deepest_manipulation(responses, selected, tool_data, document, false) + } } } diff --git a/frontend/src/components/panels/Data.svelte b/frontend/src/components/panels/Data.svelte index b524ecef..400e556f 100644 --- a/frontend/src/components/panels/Data.svelte +++ b/frontend/src/components/panels/Data.svelte @@ -1,30 +1,15 @@ - + diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index c2ac6f93..e5d91fc8 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -1,6 +1,5 @@ (dragInPanel = false)}> - - {#if layersPanelControlBarLeftLayout?.length > 0 && layersPanelControlBarRightLayout?.length > 0} + + {#if $portfolio.layersPanelControlBarLeftLayout?.length > 0 && $portfolio.layersPanelControlBarRightLayout?.length > 0} {/if} - + - + diff --git a/frontend/src/components/panels/Properties.svelte b/frontend/src/components/panels/Properties.svelte index 31e96629..fee12bd7 100644 --- a/frontend/src/components/panels/Properties.svelte +++ b/frontend/src/components/panels/Properties.svelte @@ -1,30 +1,15 @@ - + diff --git a/frontend/src/components/panels/Welcome.svelte b/frontend/src/components/panels/Welcome.svelte index c21b57c3..a38d724e 100644 --- a/frontend/src/components/panels/Welcome.svelte +++ b/frontend/src/components/panels/Welcome.svelte @@ -1,30 +1,16 @@ {#if subdivision && "PanelGroup" in subdivision} diff --git a/frontend/src/stores/portfolio.ts b/frontend/src/stores/portfolio.ts index 8ce87951..4944d74c 100644 --- a/frontend/src/stores/portfolio.ts +++ b/frontend/src/stores/portfolio.ts @@ -1,9 +1,12 @@ +import { tick } from "svelte"; +import { SvelteMap } from "svelte/reactivity"; import { writable } from "svelte/store"; import type { Writable } from "svelte/store"; import type { SubscriptionsRouter } from "/src/subscriptions-router"; import { downloadFile, downloadFileBlob, upload } from "/src/utility-functions/files"; import { rasterizeSVG } from "/src/utility-functions/rasterization"; -import type { EditorWrapper, DocumentInfo, WorkspacePanelLayout } from "/wrapper/pkg/graphite_wasm_wrapper"; +import { patchLayout } from "/src/utility-functions/widgets"; +import type { EditorWrapper, DocumentInfo, LayerPanelEntry, LayerStructureEntry, Layout, WorkspacePanelLayout } from "/wrapper/pkg/graphite_wasm_wrapper"; export type PortfolioStore = ReturnType; @@ -12,12 +15,28 @@ type PortfolioStoreState = { documents: DocumentInfo[]; activeDocumentIndex: number; panelLayout: WorkspacePanelLayout; + welcomeScreenButtonsLayout: Layout; + propertiesPanelLayout: Layout; + dataPanelLayout: Layout; + layersPanelControlBarLeftLayout: Layout; + layersPanelControlBarRightLayout: Layout; + layersPanelBottomBarLayout: Layout; + layerCache: SvelteMap; + layerStructure: LayerStructureEntry[]; }; const initialState: PortfolioStoreState = { unsaved: false, documents: [], activeDocumentIndex: 0, panelLayout: {}, + welcomeScreenButtonsLayout: [], + propertiesPanelLayout: [], + dataPanelLayout: [], + layersPanelControlBarLeftLayout: [], + layersPanelControlBarRightLayout: [], + layersPanelBottomBarLayout: [], + layerCache: new SvelteMap(), + layerStructure: [], }; let subscriptionsRouter: SubscriptionsRouter | undefined = undefined; @@ -104,6 +123,69 @@ export function createPortfolioStore(subscriptions: SubscriptionsRouter, editor: }); }); + // All panel layouts below live in this store so panels that remount during a panel-tree change keep their contents + subscriptions.subscribeLayoutUpdate("WelcomeScreenButtons", async (data) => { + await tick(); + update((state) => { + patchLayout(state.welcomeScreenButtonsLayout, data); + return state; + }); + }); + + subscriptions.subscribeLayoutUpdate("PropertiesPanel", async (data) => { + await tick(); + update((state) => { + patchLayout(state.propertiesPanelLayout, data); + return state; + }); + }); + + subscriptions.subscribeLayoutUpdate("DataPanel", async (data) => { + await tick(); + update((state) => { + patchLayout(state.dataPanelLayout, data); + return state; + }); + }); + + subscriptions.subscribeLayoutUpdate("LayersPanelControlLeftBar", async (data) => { + await tick(); + update((state) => { + patchLayout(state.layersPanelControlBarLeftLayout, data); + return state; + }); + }); + + subscriptions.subscribeLayoutUpdate("LayersPanelControlRightBar", async (data) => { + await tick(); + update((state) => { + patchLayout(state.layersPanelControlBarRightLayout, data); + return state; + }); + }); + + subscriptions.subscribeLayoutUpdate("LayersPanelBottomBar", async (data) => { + await tick(); + update((state) => { + patchLayout(state.layersPanelBottomBarLayout, data); + return state; + }); + }); + + subscriptions.subscribeFrontendMessage("UpdateDocumentLayerStructure", (data) => { + update((state) => { + state.layerStructure = data.layerStructure; + return state; + }); + }); + + subscriptions.subscribeFrontendMessage("UpdateDocumentLayerDetails", (data) => { + update((state) => { + state.layerCache.set(String(data.data.id), data.data); + return state; + }); + }); + return { subscribe }; } @@ -120,4 +202,12 @@ export function destroyPortfolioStore() { subscriptions.unsubscribeFrontendMessage("TriggerSaveFile"); subscriptions.unsubscribeFrontendMessage("TriggerExportImage"); subscriptions.unsubscribeFrontendMessage("UpdateWorkspacePanelLayout"); + subscriptions.unsubscribeLayoutUpdate("WelcomeScreenButtons"); + subscriptions.unsubscribeLayoutUpdate("PropertiesPanel"); + subscriptions.unsubscribeLayoutUpdate("DataPanel"); + subscriptions.unsubscribeLayoutUpdate("LayersPanelControlLeftBar"); + subscriptions.unsubscribeLayoutUpdate("LayersPanelControlRightBar"); + subscriptions.unsubscribeLayoutUpdate("LayersPanelBottomBar"); + subscriptions.unsubscribeFrontendMessage("UpdateDocumentLayerStructure"); + subscriptions.unsubscribeFrontendMessage("UpdateDocumentLayerDetails"); } diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 6eb1a895..6335113d 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -498,13 +498,6 @@ impl EditorWrapper { self.dispatch(message); } - #[wasm_bindgen(js_name = resetPanelGroupSizes)] - pub fn reset_panel_group_sizes(&self, split_path: JsValue) { - let split_path: Vec = serde_wasm_bindgen::from_value(split_path).unwrap(); - let message = PortfolioMessage::ResetPanelGroupSizes { split_path }; - self.dispatch(message); - } - #[wasm_bindgen(js_name = setPanelGroupSizes)] pub fn set_panel_group_sizes(&self, split_path: JsValue, sizes: JsValue) { let split_path: Vec = serde_wasm_bindgen::from_value(split_path).unwrap();