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::preferences;
|
||||||
use crate::render::{RenderError, RenderState};
|
use crate::render::{RenderError, RenderState};
|
||||||
use crate::window::Window;
|
use crate::window::Window;
|
||||||
|
use crate::workspace_layout;
|
||||||
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
|
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
|
||||||
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
|
||||||
|
|
||||||
|
|
@ -304,6 +305,15 @@ impl App {
|
||||||
let message = DesktopWrapperMessage::LoadPreferences { preferences };
|
let message = DesktopWrapperMessage::LoadPreferences { preferences };
|
||||||
responses.push(message);
|
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 => {
|
DesktopFrontendMessage::PersistenceLoadCurrentDocument => {
|
||||||
if let Some((id, document)) = self.persistent_data.current_document() {
|
if let Some((id, document)) = self.persistent_data.current_document() {
|
||||||
let message = DesktopWrapperMessage::LoadDocument {
|
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_LOCK_FILE_NAME: &str = "instance.lock";
|
||||||
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
|
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
|
||||||
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.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";
|
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
|
||||||
|
|
||||||
// CEF configuration constants
|
// CEF configuration constants
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ mod persist;
|
||||||
mod preferences;
|
mod preferences;
|
||||||
mod render;
|
mod render;
|
||||||
mod window;
|
mod window;
|
||||||
|
mod workspace_layout;
|
||||||
|
|
||||||
pub(crate) mod consts;
|
pub(crate) mod consts;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ impl NativeWindowHandle {
|
||||||
|
|
||||||
// Subclass the main window.
|
// Subclass the main window.
|
||||||
// https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setwindowlongptra
|
// 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 {
|
if prev_window_message_handler == 0 {
|
||||||
let _ = unsafe { DestroyWindow(helper) };
|
let _ = unsafe { DestroyWindow(helper) };
|
||||||
panic!("SetWindowLongPtrW failed");
|
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 };
|
let message = PreferencesMessage::Load { preferences };
|
||||||
dispatcher.queue_editor_message(message);
|
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")]
|
#[cfg(target_os = "macos")]
|
||||||
DesktopWrapperMessage::MenuEvent { id } => {
|
DesktopWrapperMessage::MenuEvent { id } => {
|
||||||
if let Some(message) = crate::utils::menu::parse_item_path(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 => {
|
FrontendMessage::TriggerLoadPreferences => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences);
|
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")]
|
#[cfg(target_os = "macos")]
|
||||||
FrontendMessage::UpdateLayout {
|
FrontendMessage::UpdateLayout {
|
||||||
layout_target: LayoutTarget::MenuBar,
|
layout_target: LayoutTarget::MenuBar,
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ pub enum DesktopFrontendMessage {
|
||||||
preferences: Preferences,
|
preferences: Preferences,
|
||||||
},
|
},
|
||||||
PersistenceLoadPreferences,
|
PersistenceLoadPreferences,
|
||||||
|
PersistenceWriteWorkspaceLayout {
|
||||||
|
workspace_layout: String,
|
||||||
|
},
|
||||||
|
PersistenceLoadWorkspaceLayout,
|
||||||
UpdateMenu {
|
UpdateMenu {
|
||||||
entries: Vec<MenuItem>,
|
entries: Vec<MenuItem>,
|
||||||
},
|
},
|
||||||
|
|
@ -117,6 +121,9 @@ pub enum DesktopWrapperMessage {
|
||||||
LoadPreferences {
|
LoadPreferences {
|
||||||
preferences: Preferences,
|
preferences: Preferences,
|
||||||
},
|
},
|
||||||
|
LoadWorkspaceLayout {
|
||||||
|
workspace_layout: String,
|
||||||
|
},
|
||||||
MenuEvent {
|
MenuEvent {
|
||||||
id: String,
|
id: String,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ impl Dispatcher {
|
||||||
Message::MenuBar(message) => {
|
Message::MenuBar(message) => {
|
||||||
let menu_bar_message_handler = &mut self.message_handlers.menu_bar_message_handler;
|
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;
|
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.data_panel_open = layout.is_panel_present(PanelType::Data);
|
||||||
menu_bar_message_handler.layers_panel_open = layout.is_panel_present(PanelType::Layers);
|
menu_bar_message_handler.layers_panel_open = layout.is_panel_present(PanelType::Layers);
|
||||||
|
|
|
||||||
|
|
@ -127,12 +127,17 @@ pub enum FrontendMessage {
|
||||||
TriggerLoadRestAutoSaveDocuments,
|
TriggerLoadRestAutoSaveDocuments,
|
||||||
TriggerOpenLaunchDocuments,
|
TriggerOpenLaunchDocuments,
|
||||||
TriggerLoadPreferences,
|
TriggerLoadPreferences,
|
||||||
|
TriggerLoadWorkspaceLayout,
|
||||||
TriggerOpen,
|
TriggerOpen,
|
||||||
TriggerImport,
|
TriggerImport,
|
||||||
TriggerSavePreferences {
|
TriggerSavePreferences {
|
||||||
#[tsify(type = "unknown")]
|
#[tsify(type = "unknown")]
|
||||||
preferences: PreferencesMessageHandler,
|
preferences: PreferencesMessageHandler,
|
||||||
},
|
},
|
||||||
|
TriggerSaveWorkspaceLayout {
|
||||||
|
#[serde(rename = "workspaceLayout")]
|
||||||
|
workspace_layout: WorkspacePanelLayout,
|
||||||
|
},
|
||||||
TriggerSaveActiveDocument {
|
TriggerSaveActiveDocument {
|
||||||
#[serde(rename = "documentId")]
|
#[serde(rename = "documentId")]
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
|
|
|
||||||
|
|
@ -647,6 +647,12 @@ impl LayoutHolder for MenuBarMessageHandler {
|
||||||
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleFocusDocument))
|
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleFocusDocument))
|
||||||
.on_commit(|_| PortfolioMessage::ToggleFocusDocument.into()),
|
.on_commit(|_| PortfolioMessage::ToggleFocusDocument.into()),
|
||||||
],
|
],
|
||||||
|
vec![
|
||||||
|
MenuListEntry::new("Reset Workspace")
|
||||||
|
.label("Reset Workspace")
|
||||||
|
.icon("Reset")
|
||||||
|
.on_commit(|_| PortfolioMessage::ResetWorkspaceLayout.into()),
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
MenuListEntry::new("Properties")
|
MenuListEntry::new("Properties")
|
||||||
.label("Properties")
|
.label("Properties")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
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::frontend::utility_types::{ExportBounds, FileType};
|
||||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||||
use crate::messages::portfolio::utility_types::FontCatalog;
|
use crate::messages::portfolio::utility_types::FontCatalog;
|
||||||
|
|
@ -61,6 +61,9 @@ pub enum PortfolioMessage {
|
||||||
LoadDocumentResources {
|
LoadDocumentResources {
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
},
|
},
|
||||||
|
LoadWorkspaceLayout {
|
||||||
|
layout: WorkspacePanelLayout,
|
||||||
|
},
|
||||||
MoveAllPanelTabs {
|
MoveAllPanelTabs {
|
||||||
source_group: PanelGroupId,
|
source_group: PanelGroupId,
|
||||||
target_group: PanelGroupId,
|
target_group: PanelGroupId,
|
||||||
|
|
@ -184,4 +187,16 @@ pub enum PortfolioMessage {
|
||||||
UpdateDocumentWidgets,
|
UpdateDocumentWidgets,
|
||||||
UpdateOpenDocumentsList,
|
UpdateOpenDocumentsList,
|
||||||
UpdateWorkspacePanelLayout,
|
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::document_metadata::LayerNodeIdentifier;
|
||||||
use super::document::utility_types::network_interface;
|
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::application::{Editor, generate_uuid};
|
||||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
|
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
|
||||||
use crate::messages::animation::TimingInformation;
|
use crate::messages::animation::TimingInformation;
|
||||||
|
|
@ -57,7 +57,6 @@ pub struct PortfolioMessageHandler {
|
||||||
pub executor: NodeGraphExecutor,
|
pub executor: NodeGraphExecutor,
|
||||||
pub selection_mode: SelectionMode,
|
pub selection_mode: SelectionMode,
|
||||||
pub reset_node_definitions_on_open: bool,
|
pub reset_node_definitions_on_open: bool,
|
||||||
pub focus_document: bool,
|
|
||||||
pub workspace_panel_layout: WorkspacePanelLayout,
|
pub workspace_panel_layout: WorkspacePanelLayout,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,9 +87,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
current_tool,
|
current_tool,
|
||||||
preferences,
|
preferences,
|
||||||
viewport,
|
viewport,
|
||||||
data_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Data) && !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.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.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)
|
document.process_message(message, responses, document_inputs)
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +104,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
|
|
||||||
// Tell frontend to load persistent preferences
|
// Tell frontend to load persistent preferences
|
||||||
responses.add(FrontendMessage::TriggerLoadPreferences);
|
responses.add(FrontendMessage::TriggerLoadPreferences);
|
||||||
|
responses.add(FrontendMessage::TriggerLoadWorkspaceLayout);
|
||||||
|
|
||||||
// Before loading any documents, initially prepare the welcome screen buttons layout
|
// Before loading any documents, initially prepare the welcome screen buttons layout
|
||||||
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);
|
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);
|
||||||
|
|
@ -155,9 +155,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
current_tool,
|
current_tool,
|
||||||
preferences,
|
preferences,
|
||||||
viewport,
|
viewport,
|
||||||
data_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Data) && !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.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.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)
|
document.process_message(message, responses, document_inputs)
|
||||||
}
|
}
|
||||||
|
|
@ -445,6 +445,17 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
document.load_layer_resources(responses);
|
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 } => {
|
PortfolioMessage::NewDocumentWithName { name } => {
|
||||||
let mut new_document = DocumentMessageHandler::default();
|
let mut new_document = DocumentMessageHandler::default();
|
||||||
new_document.name = name;
|
new_document.name = name;
|
||||||
|
|
@ -507,6 +518,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
|
|
||||||
responses.add(MenuBarMessage::SendLayout);
|
responses.add(MenuBarMessage::SendLayout);
|
||||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||||
|
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||||
|
|
||||||
// Refresh the new active tab
|
// 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()) {
|
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(MenuBarMessage::SendLayout);
|
||||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||||
|
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||||
|
|
||||||
// Refresh the moved panel's content in its new location
|
// Refresh the moved panel's content in its new location
|
||||||
self.refresh_panel_content(panel_type, responses);
|
self.refresh_panel_content(panel_type, responses);
|
||||||
|
|
@ -1190,6 +1203,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||||
|
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PortfolioMessage::RequestWelcomeScreenButtonsLayout => {
|
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
|
// Send the layout update first so the frontend mounts the new panel component before it receives content
|
||||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
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()) {
|
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);
|
self.refresh_panel_content(panel_type, responses);
|
||||||
|
|
@ -1303,6 +1318,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
|
|
||||||
responses.add(MenuBarMessage::SendLayout);
|
responses.add(MenuBarMessage::SendLayout);
|
||||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||||
|
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||||
|
|
||||||
// Refresh the new panel group's active tab
|
// 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()) {
|
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 => {
|
PortfolioMessage::ToggleFocusDocument => {
|
||||||
self.focus_document = !self.focus_document;
|
self.workspace_panel_layout.focus_document = !self.workspace_panel_layout.focus_document;
|
||||||
responses.add(MenuBarMessage::SendLayout);
|
|
||||||
|
|
||||||
let properties_present = self.workspace_panel_layout.is_panel_present(PanelType::Properties);
|
// Destroy or refresh non-document panel layouts based on focus mode
|
||||||
let layers_present = self.workspace_panel_layout.is_panel_present(PanelType::Layers);
|
for &panel_type in PanelType::non_document_panels() {
|
||||||
let data_present = self.workspace_panel_layout.is_panel_present(PanelType::Data);
|
if self.workspace_panel_layout.is_panel_present(panel_type) {
|
||||||
|
if self.workspace_panel_layout.focus_document {
|
||||||
if self.focus_document {
|
Self::destroy_panel_layouts(panel_type, responses);
|
||||||
if properties_present {
|
} else {
|
||||||
Self::destroy_panel_layouts(PanelType::Properties, responses);
|
self.refresh_panel_content(panel_type, 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()],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
responses.add(MenuBarMessage::SendLayout);
|
||||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||||
|
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||||
}
|
}
|
||||||
PortfolioMessage::TogglePropertiesPanelOpen => {
|
PortfolioMessage::TogglePropertiesPanelOpen => {
|
||||||
if self.focus_document {
|
if self.workspace_panel_layout.focus_document {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1509,7 +1507,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
self.toggle_dockable_panel(panel_type, responses);
|
self.toggle_dockable_panel(panel_type, responses);
|
||||||
}
|
}
|
||||||
PortfolioMessage::ToggleLayersPanelOpen => {
|
PortfolioMessage::ToggleLayersPanelOpen => {
|
||||||
if self.focus_document {
|
if self.workspace_panel_layout.focus_document {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1517,7 +1515,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
self.toggle_dockable_panel(panel_type, responses);
|
self.toggle_dockable_panel(panel_type, responses);
|
||||||
}
|
}
|
||||||
PortfolioMessage::ToggleDataPanelOpen => {
|
PortfolioMessage::ToggleDataPanelOpen => {
|
||||||
if self.focus_document {
|
if self.workspace_panel_layout.focus_document {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1538,11 +1536,72 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PortfolioMessage::UpdateWorkspacePanelLayout => {
|
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();
|
self.workspace_panel_layout.recalculate_default_sizes();
|
||||||
|
|
||||||
responses.add(FrontendMessage::UpdateWorkspacePanelLayout {
|
// Refresh all visible panels since the layout has been completely replaced
|
||||||
panel_layout: self.workspace_panel_layout.clone(),
|
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 => {
|
PortfolioMessage::UpdateOpenDocumentsList => {
|
||||||
// Send the list of document tab names
|
// 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
|
// 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;
|
common.extend(actions!(PortfolioMessageDiscriminant;
|
||||||
TogglePropertiesPanelOpen,
|
TogglePropertiesPanelOpen,
|
||||||
ToggleLayersPanelOpen,
|
ToggleLayersPanelOpen,
|
||||||
|
|
@ -1696,8 +1755,14 @@ impl PortfolioMessageHandler {
|
||||||
} else {
|
} else {
|
||||||
self.document_ids.push_back(document_id);
|
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_control_bar_widgets(
|
||||||
new_document.update_layers_panel_bottom_bar_widgets(self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.focus_document, responses);
|
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);
|
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.
|
/// 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> {
|
pub fn node_to_inspect(&self) -> Option<NodeId> {
|
||||||
// Skip if the Data panel is not open
|
// 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;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1794,6 +1859,7 @@ impl PortfolioMessageHandler {
|
||||||
|
|
||||||
responses.add(MenuBarMessage::SendLayout);
|
responses.add(MenuBarMessage::SendLayout);
|
||||||
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
|
||||||
|
responses.add(PortfolioMessage::SaveWorkspaceLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Destroy the stored layout for a panel that is no longer the active tab.
|
/// 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).
|
/// Unique identifier for a panel group (a leaf subdivision in the layout tree that holds tabs).
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
|
#[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)]
|
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct PanelGroupState {
|
pub struct PanelGroupState {
|
||||||
pub tabs: Vec<PanelType>,
|
pub tabs: Vec<PanelType>,
|
||||||
#[serde(rename = "activeTabIndex")]
|
|
||||||
pub active_tab_index: usize,
|
pub active_tab_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,6 +161,12 @@ pub enum PanelLayoutSubdivision {
|
||||||
Split { children: Vec<SplitChild> },
|
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.
|
/// A child within a split container, with a proportional size weight.
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
|
@ -170,13 +181,17 @@ pub struct SplitChild {
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct WorkspacePanelLayout {
|
pub struct WorkspacePanelLayout {
|
||||||
|
#[serde(default)]
|
||||||
pub root: PanelLayoutSubdivision,
|
pub root: PanelLayoutSubdivision,
|
||||||
/// Counter for generating unique panel group IDs.
|
/// Counter for generating unique panel group IDs.
|
||||||
#[serde(rename = "nextGroupId")]
|
#[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 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)>,
|
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 {
|
impl WorkspacePanelLayout {
|
||||||
|
|
@ -217,6 +232,14 @@ impl WorkspacePanelLayout {
|
||||||
self.root.prune();
|
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.
|
/// 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.
|
/// 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.
|
/// 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),
|
next_group_id: PanelGroupId(3),
|
||||||
saved_positions: Vec::new(),
|
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()));
|
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.
|
/// 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 {
|
||||||
|
|
|
||||||
|
|
@ -51,39 +51,25 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace {
|
.release-candidate-expiry {
|
||||||
position: relative;
|
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
|
.text-label {
|
||||||
.viewport-hole-punch .workspace .workspace-grid-subdivision:has(.panel.document-panel)::after {
|
line-height: 1.5;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@
|
||||||
const editor = getContext<EditorWrapper>("editor");
|
const editor = getContext<EditorWrapper>("editor");
|
||||||
const portfolio = getContext<PortfolioStore>("portfolio");
|
const portfolio = getContext<PortfolioStore>("portfolio");
|
||||||
|
|
||||||
export let subdivision: PanelLayoutSubdivision;
|
export let subdivision: PanelLayoutSubdivision | undefined;
|
||||||
export let depth: number;
|
export let depth: number;
|
||||||
|
export let splitPath: number[] = [];
|
||||||
|
|
||||||
// Local size overrides for gutter resizing (keyed by child index)
|
// Local size overrides for gutter resizing (keyed by child index)
|
||||||
let sizeOverrides: Record<number, number> = {};
|
let sizeOverrides: Record<number, number> = {};
|
||||||
|
|
@ -29,7 +30,7 @@
|
||||||
// Reset overrides when the subdivision changes (e.g., backend sends a new layout)
|
// Reset overrides when the subdivision changes (e.g., backend sends a new layout)
|
||||||
$: if (subdivision) sizeOverrides = {};
|
$: if (subdivision) sizeOverrides = {};
|
||||||
// Reactive array of resolved sizes (merging backend defaults with local overrides)
|
// 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) => {
|
$: documentTabLabels = $portfolio.documents.map((doc: OpenDocument) => {
|
||||||
const name = doc.details.name;
|
const name = doc.details.name;
|
||||||
const unsaved = !doc.details.isSaved;
|
const unsaved = !doc.details.isSaved;
|
||||||
|
|
@ -44,7 +45,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
function resizePanel(e: PointerEvent, prevIndex: number, nextIndex: number) {
|
function resizePanel(e: PointerEvent, prevIndex: number, nextIndex: number) {
|
||||||
if (!("Split" in subdivision)) return;
|
if (!(subdivision && "Split" in subdivision)) return;
|
||||||
|
|
||||||
const gutter = e.target;
|
const gutter = e.target;
|
||||||
if (!(gutter instanceof HTMLDivElement)) return;
|
if (!(gutter instanceof HTMLDivElement)) return;
|
||||||
|
|
@ -55,13 +56,13 @@
|
||||||
if (!(nextSibling instanceof HTMLDivElement) || !(prevSibling instanceof HTMLDivElement) || !(parentElement instanceof HTMLDivElement)) return;
|
if (!(nextSibling instanceof HTMLDivElement) || !(prevSibling instanceof HTMLDivElement) || !(parentElement instanceof HTMLDivElement)) return;
|
||||||
|
|
||||||
// Double-click resets both adjacent panels to their default sizes
|
// Double-click resets both adjacent panels to their default sizes
|
||||||
const children = subdivision.Split.children;
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const isDoubleClick = now - lastGutterClickTime < DOUBLE_CLICK_MILLISECONDS && lastGutterClickTarget === gutter;
|
const isDoubleClick = now - lastGutterClickTime < DOUBLE_CLICK_MILLISECONDS && lastGutterClickTarget === gutter;
|
||||||
lastGutterClickTime = now;
|
lastGutterClickTime = now;
|
||||||
lastGutterClickTarget = gutter;
|
lastGutterClickTarget = gutter;
|
||||||
if (isDoubleClick) {
|
if (isDoubleClick) {
|
||||||
sizeOverrides = { ...sizeOverrides, [prevIndex]: children[prevIndex].size, [nextIndex]: children[nextIndex].size };
|
sizeOverrides = {};
|
||||||
|
editor.resetPanelGroupSizes(splitPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,6 +114,12 @@
|
||||||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||||
removeListeners();
|
removeListeners();
|
||||||
activeResizeCleanup = undefined;
|
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) => {
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
|
@ -159,7 +166,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if "PanelGroup" in subdivision}
|
{#if subdivision && "PanelGroup" in subdivision}
|
||||||
{@const group = subdivision.PanelGroup}
|
{@const group = subdivision.PanelGroup}
|
||||||
{#if isDocumentGroup(group.state)}
|
{#if isDocumentGroup(group.state)}
|
||||||
<Panel
|
<Panel
|
||||||
|
|
@ -182,7 +189,7 @@
|
||||||
panelId={String(group.id)}
|
panelId={String(group.id)}
|
||||||
panelTypes={group.state.tabs}
|
panelTypes={group.state.tabs}
|
||||||
tabLabels={group.state.tabs.map((name) => ({ name }))}
|
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)}
|
clickAction={(tabIndex) => editor.setPanelGroupActiveTab(group.id, tabIndex)}
|
||||||
reorderAction={(oldIndex, newIndex) => editor.reorderPanelGroupTab(group.id, oldIndex, newIndex)}
|
reorderAction={(oldIndex, newIndex) => editor.reorderPanelGroupTab(group.id, oldIndex, newIndex)}
|
||||||
crossPanelDropAction={crossPanelDrop}
|
crossPanelDropAction={crossPanelDrop}
|
||||||
|
|
@ -190,7 +197,7 @@
|
||||||
splitDropAction={splitDrop}
|
splitDropAction={splitDrop}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if "Split" in subdivision}
|
{:else if subdivision && "Split" in subdivision}
|
||||||
{#each subdivision.Split.children as child, index}
|
{#each subdivision.Split.children as child, index}
|
||||||
{#if index > 0}
|
{#if index > 0}
|
||||||
{#if horizontal}
|
{#if horizontal}
|
||||||
|
|
@ -201,28 +208,17 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if horizontal}
|
{#if horizontal}
|
||||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": resolvedSizes[index] }}>
|
<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>
|
</LayoutCol>
|
||||||
{:else}
|
{:else}
|
||||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": resolvedSizes[index] }}>
|
<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>
|
</LayoutRow>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<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 {
|
.workspace-grid-resize-gutter {
|
||||||
flex: 0 0 4px;
|
flex: 0 0 4px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
@ -241,4 +237,25 @@
|
||||||
transition: background 0.2s 0.1s;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
import type { PortfolioStore } from "/src/stores/portfolio";
|
import type { PortfolioStore } from "/src/stores/portfolio";
|
||||||
import type { SubscriptionsRouter } from "/src/subscriptions-router";
|
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";
|
import type { EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
|
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
|
||||||
|
|
@ -22,6 +32,14 @@ export function createPersistenceManager(subscriptions: SubscriptionsRouter, edi
|
||||||
await loadEditorPreferences(editor);
|
await loadEditorPreferences(editor);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
subscriptions.subscribeFrontendMessage("TriggerSaveWorkspaceLayout", async (data) => {
|
||||||
|
await saveWorkspaceLayout(data.workspaceLayout);
|
||||||
|
});
|
||||||
|
|
||||||
|
subscriptions.subscribeFrontendMessage("TriggerLoadWorkspaceLayout", async () => {
|
||||||
|
await loadWorkspaceLayout(editor);
|
||||||
|
});
|
||||||
|
|
||||||
subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteDocument", async (data) => {
|
subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteDocument", async (data) => {
|
||||||
await storeDocument(data, portfolio);
|
await storeDocument(data, portfolio);
|
||||||
});
|
});
|
||||||
|
|
@ -53,6 +71,8 @@ export function destroyPersistenceManager() {
|
||||||
|
|
||||||
subscriptions.unsubscribeFrontendMessage("TriggerSavePreferences");
|
subscriptions.unsubscribeFrontendMessage("TriggerSavePreferences");
|
||||||
subscriptions.unsubscribeFrontendMessage("TriggerLoadPreferences");
|
subscriptions.unsubscribeFrontendMessage("TriggerLoadPreferences");
|
||||||
|
subscriptions.unsubscribeFrontendMessage("TriggerSaveWorkspaceLayout");
|
||||||
|
subscriptions.unsubscribeFrontendMessage("TriggerLoadWorkspaceLayout");
|
||||||
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteDocument");
|
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteDocument");
|
||||||
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceRemoveDocument");
|
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceRemoveDocument");
|
||||||
subscriptions.unsubscribeFrontendMessage("TriggerLoadFirstAutoSaveDocument");
|
subscriptions.unsubscribeFrontendMessage("TriggerLoadFirstAutoSaveDocument");
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const initialState: PortfolioStoreState = {
|
||||||
unsaved: false,
|
unsaved: false,
|
||||||
documents: [],
|
documents: [],
|
||||||
activeDocumentIndex: 0,
|
activeDocumentIndex: 0,
|
||||||
panelLayout: { root: { Split: { children: [] } }, nextGroupId: 0n },
|
panelLayout: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
|
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,15 @@ export async function loadEditorPreferences(editor: EditorWrapper) {
|
||||||
editor.loadPreferences(preferences ? JSON.stringify(preferences) : undefined);
|
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() {
|
export async function wipeDocuments() {
|
||||||
await databaseDelete("documents_tab_order");
|
await databaseDelete("documents_tab_order");
|
||||||
await databaseDelete("current_document_id");
|
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)]
|
#[wasm_bindgen(js_name = selectDocument)]
|
||||||
pub fn select_document(&self, document_id: u64) {
|
pub fn select_document(&self, document_id: u64) {
|
||||||
let document_id = DocumentId(document_id);
|
let document_id = DocumentId(document_id);
|
||||||
|
|
@ -486,6 +496,21 @@ impl EditorWrapper {
|
||||||
self.dispatch(message);
|
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)]
|
#[wasm_bindgen(js_name = closeDocumentWithConfirmation)]
|
||||||
pub fn close_document_with_confirmation(&self, document_id: u64) {
|
pub fn close_document_with_confirmation(&self, document_id: u64) {
|
||||||
let document_id = DocumentId(document_id);
|
let document_id = DocumentId(document_id);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue