Add support for persistent storage of panel layouts, sizes, and active tabs (#4017)
* Add persistence to panel layouts * Fix and persist the Window > Focus Document mode * Add a Window > Reset Workspace action * workspace_layout.json -> workspace_layout.ron * Fix native app hole punch * Cleanup review pass
This commit is contained in:
parent
b099e2faca
commit
b100892bfa
|
|
@ -21,6 +21,7 @@ use crate::persist::PersistentData;
|
|||
use crate::preferences;
|
||||
use crate::render::{RenderError, RenderState};
|
||||
use crate::window::Window;
|
||||
use crate::workspace_layout;
|
||||
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
|
||||
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||
|
||||
|
|
@ -304,6 +305,15 @@ impl App {
|
|||
let message = DesktopWrapperMessage::LoadPreferences { preferences };
|
||||
responses.push(message);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceWriteWorkspaceLayout { workspace_layout: layout } => {
|
||||
workspace_layout::write(&layout);
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceLoadWorkspaceLayout => {
|
||||
if let Some(workspace_layout) = workspace_layout::read() {
|
||||
let message = DesktopWrapperMessage::LoadWorkspaceLayout { workspace_layout };
|
||||
responses.push(message);
|
||||
}
|
||||
}
|
||||
DesktopFrontendMessage::PersistenceLoadCurrentDocument => {
|
||||
if let Some((id, document)) = self.persistent_data.current_document() {
|
||||
let message = DesktopWrapperMessage::LoadDocument {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ pub(crate) const APP_DIRECTORY_NAME: &str = "Graphite";
|
|||
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
|
||||
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
|
||||
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
|
||||
pub(crate) const APP_WORKSPACE_LAYOUT_FILE_NAME: &str = "workspace_layout.ron";
|
||||
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
|
||||
|
||||
// CEF configuration constants
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ mod persist;
|
|||
mod preferences;
|
||||
mod render;
|
||||
mod window;
|
||||
mod workspace_layout;
|
||||
|
||||
pub(crate) mod consts;
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ impl NativeWindowHandle {
|
|||
|
||||
// Subclass the main window.
|
||||
// https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setwindowlongptra
|
||||
let prev_window_message_handler = unsafe { SetWindowLongPtrW(main, GWLP_WNDPROC, main_window_handle_message as isize) };
|
||||
let prev_window_message_handler = unsafe { SetWindowLongPtrW(main, GWLP_WNDPROC, main_window_handle_message as *const () as isize) };
|
||||
if prev_window_message_handler == 0 {
|
||||
let _ = unsafe { DestroyWindow(helper) };
|
||||
panic!("SetWindowLongPtrW failed");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
pub(crate) fn write(workspace_layout: &str) {
|
||||
std::fs::write(file_path(), workspace_layout).unwrap_or_else(|e| {
|
||||
tracing::error!("Failed to write workspace layout to disk: {e}");
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn read() -> Option<String> {
|
||||
std::fs::read_to_string(file_path()).ok()
|
||||
}
|
||||
|
||||
fn file_path() -> std::path::PathBuf {
|
||||
let mut path = crate::dirs::app_data_dir();
|
||||
path.push(crate::consts::APP_WORKSPACE_LAYOUT_FILE_NAME);
|
||||
path
|
||||
}
|
||||
|
|
@ -75,6 +75,15 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
|
|||
let message = PreferencesMessage::Load { preferences };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
DesktopWrapperMessage::LoadWorkspaceLayout { workspace_layout } => match ron::from_str(&workspace_layout) {
|
||||
Ok(layout) => {
|
||||
let message = PortfolioMessage::LoadWorkspaceLayout { layout };
|
||||
dispatcher.queue_editor_message(message);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to deserialize workspace layout: {e}");
|
||||
}
|
||||
},
|
||||
#[cfg(target_os = "macos")]
|
||||
DesktopWrapperMessage::MenuEvent { id } => {
|
||||
if let Some(message) = crate::utils::menu::parse_item_path(id) {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,16 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
|||
FrontendMessage::TriggerLoadPreferences => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences);
|
||||
}
|
||||
FrontendMessage::TriggerSaveWorkspaceLayout { workspace_layout } => {
|
||||
let Ok(workspace_layout) = ron::ser::to_string_pretty(&workspace_layout, ron::ser::PrettyConfig::default()) else {
|
||||
tracing::error!("Failed to serialize workspace layout");
|
||||
return None;
|
||||
};
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceWriteWorkspaceLayout { workspace_layout });
|
||||
}
|
||||
FrontendMessage::TriggerLoadWorkspaceLayout => {
|
||||
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadWorkspaceLayout);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
FrontendMessage::UpdateLayout {
|
||||
layout_target: LayoutTarget::MenuBar,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ pub enum DesktopFrontendMessage {
|
|||
preferences: Preferences,
|
||||
},
|
||||
PersistenceLoadPreferences,
|
||||
PersistenceWriteWorkspaceLayout {
|
||||
workspace_layout: String,
|
||||
},
|
||||
PersistenceLoadWorkspaceLayout,
|
||||
UpdateMenu {
|
||||
entries: Vec<MenuItem>,
|
||||
},
|
||||
|
|
@ -117,6 +121,9 @@ pub enum DesktopWrapperMessage {
|
|||
LoadPreferences {
|
||||
preferences: Preferences,
|
||||
},
|
||||
LoadWorkspaceLayout {
|
||||
workspace_layout: String,
|
||||
},
|
||||
MenuEvent {
|
||||
id: String,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ impl Dispatcher {
|
|||
Message::MenuBar(message) => {
|
||||
let menu_bar_message_handler = &mut self.message_handlers.menu_bar_message_handler;
|
||||
|
||||
menu_bar_message_handler.focus_document = self.message_handlers.portfolio_message_handler.focus_document;
|
||||
menu_bar_message_handler.focus_document = self.message_handlers.portfolio_message_handler.workspace_panel_layout.focus_document;
|
||||
let layout = &self.message_handlers.portfolio_message_handler.workspace_panel_layout;
|
||||
menu_bar_message_handler.data_panel_open = layout.is_panel_present(PanelType::Data);
|
||||
menu_bar_message_handler.layers_panel_open = layout.is_panel_present(PanelType::Layers);
|
||||
|
|
|
|||
|
|
@ -127,12 +127,17 @@ pub enum FrontendMessage {
|
|||
TriggerLoadRestAutoSaveDocuments,
|
||||
TriggerOpenLaunchDocuments,
|
||||
TriggerLoadPreferences,
|
||||
TriggerLoadWorkspaceLayout,
|
||||
TriggerOpen,
|
||||
TriggerImport,
|
||||
TriggerSavePreferences {
|
||||
#[tsify(type = "unknown")]
|
||||
preferences: PreferencesMessageHandler,
|
||||
},
|
||||
TriggerSaveWorkspaceLayout {
|
||||
#[serde(rename = "workspaceLayout")]
|
||||
workspace_layout: WorkspacePanelLayout,
|
||||
},
|
||||
TriggerSaveActiveDocument {
|
||||
#[serde(rename = "documentId")]
|
||||
document_id: DocumentId,
|
||||
|
|
|
|||
|
|
@ -647,6 +647,12 @@ impl LayoutHolder for MenuBarMessageHandler {
|
|||
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleFocusDocument))
|
||||
.on_commit(|_| PortfolioMessage::ToggleFocusDocument.into()),
|
||||
],
|
||||
vec![
|
||||
MenuListEntry::new("Reset Workspace")
|
||||
.label("Reset Workspace")
|
||||
.icon("Reset")
|
||||
.on_commit(|_| PortfolioMessage::ResetWorkspaceLayout.into()),
|
||||
],
|
||||
vec![
|
||||
MenuListEntry::new("Properties")
|
||||
.label("Properties")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType};
|
||||
use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType, WorkspacePanelLayout};
|
||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
use crate::messages::portfolio::utility_types::FontCatalog;
|
||||
|
|
@ -61,6 +61,9 @@ pub enum PortfolioMessage {
|
|||
LoadDocumentResources {
|
||||
document_id: DocumentId,
|
||||
},
|
||||
LoadWorkspaceLayout {
|
||||
layout: WorkspacePanelLayout,
|
||||
},
|
||||
MoveAllPanelTabs {
|
||||
source_group: PanelGroupId,
|
||||
target_group: PanelGroupId,
|
||||
|
|
@ -184,4 +187,16 @@ pub enum PortfolioMessage {
|
|||
UpdateDocumentWidgets,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdateWorkspacePanelLayout,
|
||||
SaveWorkspaceLayout,
|
||||
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<usize>,
|
||||
},
|
||||
SetPanelGroupSizes {
|
||||
/// Path of child indices from the root to the split node whose children's sizes are being set.
|
||||
split_path: Vec<usize>,
|
||||
/// New sizes for the children at that split node.
|
||||
sizes: Vec<f64>,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use super::document::utility_types::network_interface;
|
||||
use super::utility_types::{PanelType, PersistentData, WorkspacePanelLayout};
|
||||
use super::utility_types::{PanelLayoutSubdivision, PanelType, PersistentData, WorkspacePanelLayout};
|
||||
use crate::application::{Editor, generate_uuid};
|
||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
|
||||
use crate::messages::animation::TimingInformation;
|
||||
|
|
@ -57,7 +57,6 @@ pub struct PortfolioMessageHandler {
|
|||
pub executor: NodeGraphExecutor,
|
||||
pub selection_mode: SelectionMode,
|
||||
pub reset_node_definitions_on_open: bool,
|
||||
pub focus_document: bool,
|
||||
pub workspace_panel_layout: WorkspacePanelLayout,
|
||||
}
|
||||
|
||||
|
|
@ -88,9 +87,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
current_tool,
|
||||
preferences,
|
||||
viewport,
|
||||
data_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Data) && !self.focus_document,
|
||||
layers_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.focus_document,
|
||||
properties_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Properties) && !self.focus_document,
|
||||
data_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Data) && !self.workspace_panel_layout.focus_document,
|
||||
layers_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document,
|
||||
properties_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Properties) && !self.workspace_panel_layout.focus_document,
|
||||
};
|
||||
document.process_message(message, responses, document_inputs)
|
||||
}
|
||||
|
|
@ -105,6 +104,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
|
||||
// Tell frontend to load persistent preferences
|
||||
responses.add(FrontendMessage::TriggerLoadPreferences);
|
||||
responses.add(FrontendMessage::TriggerLoadWorkspaceLayout);
|
||||
|
||||
// Before loading any documents, initially prepare the welcome screen buttons layout
|
||||
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);
|
||||
|
|
@ -155,9 +155,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
current_tool,
|
||||
preferences,
|
||||
viewport,
|
||||
data_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Data) && !self.focus_document,
|
||||
layers_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.focus_document,
|
||||
properties_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Properties) && !self.focus_document,
|
||||
data_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Data) && !self.workspace_panel_layout.focus_document,
|
||||
layers_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document,
|
||||
properties_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Properties) && !self.workspace_panel_layout.focus_document,
|
||||
};
|
||||
document.process_message(message, responses, document_inputs)
|
||||
}
|
||||
|
|
@ -445,6 +445,17 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
document.load_layer_resources(responses);
|
||||
}
|
||||
}
|
||||
PortfolioMessage::LoadWorkspaceLayout { layout } => {
|
||||
self.workspace_panel_layout = layout;
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
|
||||
// Refresh all visible panels since the layout may have changed
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
PortfolioMessage::NewDocumentWithName { name } => {
|
||||
let mut new_document = DocumentMessageHandler::default();
|
||||
new_document.name = name;
|
||||
|
|
@ -507,6 +518,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
|
||||
// Refresh the new active tab
|
||||
if let Some(panel_type) = self.workspace_panel_layout.panel_group(target_group).and_then(|g| g.active_panel_type()) {
|
||||
|
|
@ -556,6 +568,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
|
||||
// Refresh the moved panel's content in its new location
|
||||
self.refresh_panel_content(panel_type, responses);
|
||||
|
|
@ -1190,6 +1203,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
}
|
||||
}
|
||||
PortfolioMessage::RequestWelcomeScreenButtonsLayout => {
|
||||
|
|
@ -1269,6 +1283,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
|
||||
// Send the layout update first so the frontend mounts the new panel component before it receives content
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
|
||||
if let Some(panel_type) = self.workspace_panel_layout.panel_group(group).and_then(|g| g.active_panel_type()) {
|
||||
self.refresh_panel_content(panel_type, responses);
|
||||
|
|
@ -1303,6 +1318,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
|
||||
// Refresh the new panel group's active tab
|
||||
if let Some(panel_type) = self.workspace_panel_layout.panel_group(new_id).and_then(|g| g.active_panel_type()) {
|
||||
|
|
@ -1465,43 +1481,25 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
}
|
||||
PortfolioMessage::ToggleFocusDocument => {
|
||||
self.focus_document = !self.focus_document;
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
self.workspace_panel_layout.focus_document = !self.workspace_panel_layout.focus_document;
|
||||
|
||||
let properties_present = self.workspace_panel_layout.is_panel_present(PanelType::Properties);
|
||||
let layers_present = self.workspace_panel_layout.is_panel_present(PanelType::Layers);
|
||||
let data_present = self.workspace_panel_layout.is_panel_present(PanelType::Data);
|
||||
|
||||
if self.focus_document {
|
||||
if properties_present {
|
||||
Self::destroy_panel_layouts(PanelType::Properties, responses);
|
||||
}
|
||||
if layers_present {
|
||||
Self::destroy_panel_layouts(PanelType::Layers, responses);
|
||||
}
|
||||
if data_present {
|
||||
Self::destroy_panel_layouts(PanelType::Data, responses);
|
||||
}
|
||||
} else {
|
||||
// Run the graph to grab the data
|
||||
if properties_present || layers_present || data_present {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
if properties_present {
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
if layers_present && self.active_document_id.is_some() {
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![NodeGraphMessage::UpdateLayerPanel.into(), DocumentMessage::DocumentStructureChanged.into()],
|
||||
});
|
||||
// Destroy or refresh non-document panel layouts based on focus mode
|
||||
for &panel_type in PanelType::non_document_panels() {
|
||||
if self.workspace_panel_layout.is_panel_present(panel_type) {
|
||||
if self.workspace_panel_layout.focus_document {
|
||||
Self::destroy_panel_layouts(panel_type, responses);
|
||||
} else {
|
||||
self.refresh_panel_content(panel_type, responses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
}
|
||||
PortfolioMessage::TogglePropertiesPanelOpen => {
|
||||
if self.focus_document {
|
||||
if self.workspace_panel_layout.focus_document {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1509,7 +1507,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
self.toggle_dockable_panel(panel_type, responses);
|
||||
}
|
||||
PortfolioMessage::ToggleLayersPanelOpen => {
|
||||
if self.focus_document {
|
||||
if self.workspace_panel_layout.focus_document {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1517,7 +1515,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
self.toggle_dockable_panel(panel_type, responses);
|
||||
}
|
||||
PortfolioMessage::ToggleDataPanelOpen => {
|
||||
if self.focus_document {
|
||||
if self.workspace_panel_layout.focus_document {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1538,11 +1536,72 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
}
|
||||
PortfolioMessage::UpdateWorkspacePanelLayout => {
|
||||
let panel_layout = match self.workspace_panel_layout.focus_document {
|
||||
true => self.workspace_panel_layout.document_only_layout(),
|
||||
false => self.workspace_panel_layout.clone(),
|
||||
};
|
||||
responses.add(FrontendMessage::UpdateWorkspacePanelLayout { panel_layout });
|
||||
}
|
||||
PortfolioMessage::SaveWorkspaceLayout => {
|
||||
responses.add(FrontendMessage::TriggerSaveWorkspaceLayout {
|
||||
workspace_layout: self.workspace_panel_layout.clone(),
|
||||
});
|
||||
}
|
||||
PortfolioMessage::ResetWorkspaceLayout => {
|
||||
// Destroy layouts for all currently visible non-document panels
|
||||
for &panel_type in PanelType::non_document_panels() {
|
||||
if self.workspace_panel_layout.is_panel_present(panel_type) {
|
||||
Self::destroy_panel_layouts(panel_type, responses);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace layout with the default and recalculate sizes
|
||||
self.workspace_panel_layout = WorkspacePanelLayout::default();
|
||||
self.workspace_panel_layout.recalculate_default_sizes();
|
||||
|
||||
responses.add(FrontendMessage::UpdateWorkspacePanelLayout {
|
||||
panel_layout: self.workspace_panel_layout.clone(),
|
||||
});
|
||||
// Refresh all visible panels since the layout has been completely replaced
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
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);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
// Apply the new sizes to the split's children
|
||||
if let PanelLayoutSubdivision::Split { children } = node {
|
||||
for (child, &size) in children.iter_mut().zip(sizes.iter()) {
|
||||
child.size = size;
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
}
|
||||
PortfolioMessage::UpdateOpenDocumentsList => {
|
||||
// Send the list of document tab names
|
||||
|
|
@ -1602,7 +1661,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||
}
|
||||
|
||||
// Extend with actions that are disabled when focusing the document
|
||||
if !self.focus_document {
|
||||
if !self.workspace_panel_layout.focus_document {
|
||||
common.extend(actions!(PortfolioMessageDiscriminant;
|
||||
TogglePropertiesPanelOpen,
|
||||
ToggleLayersPanelOpen,
|
||||
|
|
@ -1696,8 +1755,14 @@ impl PortfolioMessageHandler {
|
|||
} else {
|
||||
self.document_ids.push_back(document_id);
|
||||
}
|
||||
new_document.update_layers_panel_control_bar_widgets(self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.focus_document, responses);
|
||||
new_document.update_layers_panel_bottom_bar_widgets(self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.focus_document, responses);
|
||||
new_document.update_layers_panel_control_bar_widgets(
|
||||
self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document,
|
||||
responses,
|
||||
);
|
||||
new_document.update_layers_panel_bottom_bar_widgets(
|
||||
self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document,
|
||||
responses,
|
||||
);
|
||||
|
||||
self.documents.insert(document_id, new_document);
|
||||
|
||||
|
|
@ -1744,7 +1809,7 @@ impl PortfolioMessageHandler {
|
|||
/// Get the ID of the selected node that should be used as the current source for the Data panel.
|
||||
pub fn node_to_inspect(&self) -> Option<NodeId> {
|
||||
// Skip if the Data panel is not open
|
||||
if !self.workspace_panel_layout.is_panel_visible(PanelType::Data) || self.focus_document {
|
||||
if !self.workspace_panel_layout.is_panel_visible(PanelType::Data) || self.workspace_panel_layout.focus_document {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
@ -1794,6 +1859,7 @@ impl PortfolioMessageHandler {
|
|||
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||
}
|
||||
|
||||
/// Destroy the stored layout for a panel that is no longer the active tab.
|
||||
|
|
|
|||
|
|
@ -106,6 +106,12 @@ impl From<String> for PanelType {
|
|||
}
|
||||
}
|
||||
|
||||
impl PanelType {
|
||||
pub fn non_document_panels() -> &'static [PanelType] {
|
||||
&[PanelType::Layers, PanelType::Properties, PanelType::Data]
|
||||
}
|
||||
}
|
||||
|
||||
/// Unique identifier for a panel group (a leaf subdivision in the layout tree that holds tabs).
|
||||
#[repr(transparent)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
|
||||
|
|
@ -127,7 +133,6 @@ pub enum DockingSplitDirection {
|
|||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PanelGroupState {
|
||||
pub tabs: Vec<PanelType>,
|
||||
#[serde(rename = "activeTabIndex")]
|
||||
pub active_tab_index: usize,
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +161,12 @@ pub enum PanelLayoutSubdivision {
|
|||
Split { children: Vec<SplitChild> },
|
||||
}
|
||||
|
||||
impl Default for PanelLayoutSubdivision {
|
||||
fn default() -> Self {
|
||||
PanelLayoutSubdivision::Split { children: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A child within a split container, with a proportional size weight.
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
|
|
@ -170,13 +181,17 @@ pub struct SplitChild {
|
|||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct WorkspacePanelLayout {
|
||||
#[serde(default)]
|
||||
pub root: PanelLayoutSubdivision,
|
||||
/// Counter for generating unique panel group IDs.
|
||||
#[serde(rename = "nextGroupId")]
|
||||
#[serde(default)]
|
||||
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.
|
||||
#[serde(default, rename = "savedPositions")]
|
||||
#[serde(default)]
|
||||
saved_positions: Vec<(PanelType, PanelGroupId, usize)>,
|
||||
/// Whether Focus Document mode is active, hiding all non-document panels.
|
||||
#[serde(default)]
|
||||
pub focus_document: bool,
|
||||
}
|
||||
|
||||
impl WorkspacePanelLayout {
|
||||
|
|
@ -217,6 +232,14 @@ impl WorkspacePanelLayout {
|
|||
self.root.prune();
|
||||
}
|
||||
|
||||
/// Produce a filtered copy of this layout containing only the document panel, for use in Focus Document mode.
|
||||
pub fn document_only_layout(&self) -> WorkspacePanelLayout {
|
||||
let mut layout = self.clone();
|
||||
layout.root.retain_only_document_panels();
|
||||
layout.root.prune();
|
||||
layout
|
||||
}
|
||||
|
||||
/// Split a panel group by inserting a new panel group adjacent to it.
|
||||
/// 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.
|
||||
|
|
@ -399,6 +422,7 @@ impl Default for WorkspacePanelLayout {
|
|||
},
|
||||
next_group_id: PanelGroupId(3),
|
||||
saved_positions: Vec::new(),
|
||||
focus_document: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -455,6 +479,19 @@ impl PanelLayoutSubdivision {
|
|||
children.retain(|child| !matches!(&child.subdivision, PanelLayoutSubdivision::Split { children } if children.is_empty()));
|
||||
}
|
||||
|
||||
/// Remove all non-document/non-welcome tabs from panel groups, leaving only document-related panels.
|
||||
pub fn retain_only_document_panels(&mut self) {
|
||||
match self {
|
||||
PanelLayoutSubdivision::PanelGroup { state, .. } => {
|
||||
state.tabs.retain(|t| matches!(t, PanelType::Document | PanelType::Welcome));
|
||||
state.active_tab_index = state.active_tab_index.min(state.tabs.len().saturating_sub(1));
|
||||
}
|
||||
PanelLayoutSubdivision::Split { children } => {
|
||||
children.iter_mut().for_each(|child| child.subdivision.retain_only_document_panels());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this subtree contains a panel group with the given ID.
|
||||
pub fn contains_group(&self, target_id: PanelGroupId) -> bool {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -51,39 +51,25 @@
|
|||
height: 100%;
|
||||
overflow: auto;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
position: relative;
|
||||
}
|
||||
.release-candidate-expiry {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: var(--color-e-nearwhite);
|
||||
color: var(--color-2-mildblack);
|
||||
opacity: 0.9;
|
||||
pointer-events: none;
|
||||
padding: 12px 40px;
|
||||
border-radius: 4px;
|
||||
text-align-last: justify;
|
||||
font-size: 18px;
|
||||
z-index: 1000;
|
||||
|
||||
// Needed for the viewport hole punch on desktop
|
||||
.viewport-hole-punch .workspace .workspace-grid-subdivision:has(.panel.document-panel)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 6px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 0 calc(100vw + 100vh) var(--color-2-mildblack);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.release-candidate-expiry {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: var(--color-e-nearwhite);
|
||||
color: var(--color-2-mildblack);
|
||||
opacity: 0.9;
|
||||
pointer-events: none;
|
||||
padding: 12px 40px;
|
||||
border-radius: 4px;
|
||||
text-align-last: justify;
|
||||
font-size: 18px;
|
||||
z-index: 1000;
|
||||
|
||||
.text-label {
|
||||
line-height: 1.5;
|
||||
}
|
||||
.text-label {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@
|
|||
const editor = getContext<EditorWrapper>("editor");
|
||||
const portfolio = getContext<PortfolioStore>("portfolio");
|
||||
|
||||
export let subdivision: PanelLayoutSubdivision;
|
||||
export let subdivision: PanelLayoutSubdivision | undefined;
|
||||
export let depth: number;
|
||||
export let splitPath: number[] = [];
|
||||
|
||||
// Local size overrides for gutter resizing (keyed by child index)
|
||||
let sizeOverrides: Record<number, number> = {};
|
||||
|
|
@ -29,7 +30,7 @@
|
|||
// Reset overrides when the subdivision changes (e.g., backend sends a new layout)
|
||||
$: if (subdivision) sizeOverrides = {};
|
||||
// Reactive array of resolved sizes (merging backend defaults with local overrides)
|
||||
$: resolvedSizes = "Split" in subdivision ? subdivision.Split.children.map((child, index) => sizeOverrides[index] ?? child.size) : [];
|
||||
$: resolvedSizes = subdivision && "Split" in subdivision ? subdivision.Split.children.map((child, index) => sizeOverrides[index] ?? child.size) : [];
|
||||
$: documentTabLabels = $portfolio.documents.map((doc: OpenDocument) => {
|
||||
const name = doc.details.name;
|
||||
const unsaved = !doc.details.isSaved;
|
||||
|
|
@ -44,7 +45,7 @@
|
|||
});
|
||||
|
||||
function resizePanel(e: PointerEvent, prevIndex: number, nextIndex: number) {
|
||||
if (!("Split" in subdivision)) return;
|
||||
if (!(subdivision && "Split" in subdivision)) return;
|
||||
|
||||
const gutter = e.target;
|
||||
if (!(gutter instanceof HTMLDivElement)) return;
|
||||
|
|
@ -55,13 +56,13 @@
|
|||
if (!(nextSibling instanceof HTMLDivElement) || !(prevSibling instanceof HTMLDivElement) || !(parentElement instanceof HTMLDivElement)) return;
|
||||
|
||||
// Double-click resets both adjacent panels to their default sizes
|
||||
const children = subdivision.Split.children;
|
||||
const now = Date.now();
|
||||
const isDoubleClick = now - lastGutterClickTime < DOUBLE_CLICK_MILLISECONDS && lastGutterClickTarget === gutter;
|
||||
lastGutterClickTime = now;
|
||||
lastGutterClickTarget = gutter;
|
||||
if (isDoubleClick) {
|
||||
sizeOverrides = { ...sizeOverrides, [prevIndex]: children[prevIndex].size, [nextIndex]: children[nextIndex].size };
|
||||
sizeOverrides = {};
|
||||
editor.resetPanelGroupSizes(splitPath);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +114,12 @@
|
|||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||
removeListeners();
|
||||
activeResizeCleanup = undefined;
|
||||
|
||||
// Persist the resized sizes to the backend
|
||||
if ("Split" in subdivision) {
|
||||
const allSizes = subdivision.Split.children.map((child, i) => sizeOverrides[i] ?? child.size);
|
||||
editor.setPanelGroupSizes(splitPath, allSizes);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
|
|
@ -159,7 +166,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#if "PanelGroup" in subdivision}
|
||||
{#if subdivision && "PanelGroup" in subdivision}
|
||||
{@const group = subdivision.PanelGroup}
|
||||
{#if isDocumentGroup(group.state)}
|
||||
<Panel
|
||||
|
|
@ -182,7 +189,7 @@
|
|||
panelId={String(group.id)}
|
||||
panelTypes={group.state.tabs}
|
||||
tabLabels={group.state.tabs.map((name) => ({ name }))}
|
||||
tabActiveIndex={Number(group.state.activeTabIndex)}
|
||||
tabActiveIndex={Number(group.state.active_tab_index)}
|
||||
clickAction={(tabIndex) => editor.setPanelGroupActiveTab(group.id, tabIndex)}
|
||||
reorderAction={(oldIndex, newIndex) => editor.reorderPanelGroupTab(group.id, oldIndex, newIndex)}
|
||||
crossPanelDropAction={crossPanelDrop}
|
||||
|
|
@ -190,7 +197,7 @@
|
|||
splitDropAction={splitDrop}
|
||||
/>
|
||||
{/if}
|
||||
{:else if "Split" in subdivision}
|
||||
{:else if subdivision && "Split" in subdivision}
|
||||
{#each subdivision.Split.children as child, index}
|
||||
{#if index > 0}
|
||||
{#if horizontal}
|
||||
|
|
@ -201,28 +208,17 @@
|
|||
{/if}
|
||||
{#if horizontal}
|
||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": resolvedSizes[index] }}>
|
||||
<svelte:self subdivision={child.subdivision} depth={depth + 1} />
|
||||
<svelte:self subdivision={child.subdivision} depth={depth + 1} splitPath={[...splitPath, index]} />
|
||||
</LayoutCol>
|
||||
{:else}
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": resolvedSizes[index] }}>
|
||||
<svelte:self subdivision={child.subdivision} depth={depth + 1} />
|
||||
<svelte:self subdivision={child.subdivision} depth={depth + 1} splitPath={[...splitPath, index]} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.workspace-grid-subdivision {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-height: 28px;
|
||||
|
||||
&.folded {
|
||||
flex-grow: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-grid-resize-gutter {
|
||||
flex: 0 0 4px;
|
||||
border-radius: 2px;
|
||||
|
|
@ -241,4 +237,25 @@
|
|||
transition: background 0.2s 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-grid-subdivision {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-height: 28px;
|
||||
|
||||
&.folded {
|
||||
flex-grow: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Needed for the viewport hole punch on desktop
|
||||
.viewport-hole-punch .workspace-grid-subdivision:has(> .panel.document-panel)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: 6px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 0 calc(100vw + 100vh) var(--color-2-mildblack);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
import type { PortfolioStore } from "/src/stores/portfolio";
|
||||
import type { SubscriptionsRouter } from "/src/subscriptions-router";
|
||||
import { saveEditorPreferences, loadEditorPreferences, storeDocument, removeDocument, loadFirstDocument, loadRestDocuments, saveActiveDocument } from "/src/utility-functions/persistence";
|
||||
import {
|
||||
saveEditorPreferences,
|
||||
loadEditorPreferences,
|
||||
saveWorkspaceLayout,
|
||||
loadWorkspaceLayout,
|
||||
storeDocument,
|
||||
removeDocument,
|
||||
loadFirstDocument,
|
||||
loadRestDocuments,
|
||||
saveActiveDocument,
|
||||
} from "/src/utility-functions/persistence";
|
||||
import type { EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
|
||||
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
|
||||
|
|
@ -22,6 +32,14 @@ export function createPersistenceManager(subscriptions: SubscriptionsRouter, edi
|
|||
await loadEditorPreferences(editor);
|
||||
});
|
||||
|
||||
subscriptions.subscribeFrontendMessage("TriggerSaveWorkspaceLayout", async (data) => {
|
||||
await saveWorkspaceLayout(data.workspaceLayout);
|
||||
});
|
||||
|
||||
subscriptions.subscribeFrontendMessage("TriggerLoadWorkspaceLayout", async () => {
|
||||
await loadWorkspaceLayout(editor);
|
||||
});
|
||||
|
||||
subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteDocument", async (data) => {
|
||||
await storeDocument(data, portfolio);
|
||||
});
|
||||
|
|
@ -53,6 +71,8 @@ export function destroyPersistenceManager() {
|
|||
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerSavePreferences");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerLoadPreferences");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerSaveWorkspaceLayout");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerLoadWorkspaceLayout");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteDocument");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceRemoveDocument");
|
||||
subscriptions.unsubscribeFrontendMessage("TriggerLoadFirstAutoSaveDocument");
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const initialState: PortfolioStoreState = {
|
|||
unsaved: false,
|
||||
documents: [],
|
||||
activeDocumentIndex: 0,
|
||||
panelLayout: { root: { Split: { children: [] } }, nextGroupId: 0n },
|
||||
panelLayout: {},
|
||||
};
|
||||
|
||||
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
|
||||
|
|
|
|||
|
|
@ -161,6 +161,15 @@ export async function loadEditorPreferences(editor: EditorWrapper) {
|
|||
editor.loadPreferences(preferences ? JSON.stringify(preferences) : undefined);
|
||||
}
|
||||
|
||||
export async function saveWorkspaceLayout(layout: unknown) {
|
||||
await databaseSet("workspace_layout", layout);
|
||||
}
|
||||
|
||||
export async function loadWorkspaceLayout(editor: EditorWrapper) {
|
||||
const layout = await databaseGet<Record<string, unknown>>("workspace_layout");
|
||||
if (layout) editor.loadWorkspaceLayout(layout);
|
||||
}
|
||||
|
||||
export async function wipeDocuments() {
|
||||
await databaseDelete("documents_tab_order");
|
||||
await databaseDelete("current_document_id");
|
||||
|
|
|
|||
|
|
@ -379,6 +379,16 @@ impl EditorWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = loadWorkspaceLayout)]
|
||||
pub fn load_workspace_layout(&self, layout: JsValue) {
|
||||
let Ok(layout) = serde_wasm_bindgen::from_value(layout) else {
|
||||
log::error!("Failed to deserialize workspace layout");
|
||||
return;
|
||||
};
|
||||
let message = PortfolioMessage::LoadWorkspaceLayout { layout };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = selectDocument)]
|
||||
pub fn select_document(&self, document_id: u64) {
|
||||
let document_id = DocumentId(document_id);
|
||||
|
|
@ -486,6 +496,21 @@ impl EditorWrapper {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = resetPanelGroupSizes)]
|
||||
pub fn reset_panel_group_sizes(&self, split_path: JsValue) {
|
||||
let split_path: Vec<usize> = 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<usize> = serde_wasm_bindgen::from_value(split_path).unwrap();
|
||||
let sizes: Vec<f64> = serde_wasm_bindgen::from_value(sizes).unwrap();
|
||||
let message = PortfolioMessage::SetPanelGroupSizes { split_path, sizes };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = closeDocumentWithConfirmation)]
|
||||
pub fn close_document_with_confirmation(&self, document_id: u64) {
|
||||
let document_id = DocumentId(document_id);
|
||||
|
|
|
|||
Loading…
Reference in New Issue