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:
Keavon Chambers
2026-04-08 21:05:58 -07:00
committed by GitHub
parent b099e2faca
commit b100892bfa
20 changed files with 346 additions and 107 deletions
+10
View File
@@ -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 {
+1
View File
@@ -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
+1
View File
@@ -20,6 +20,7 @@ mod persist;
mod preferences;
mod render;
mod window;
mod workspace_layout;
pub(crate) mod consts;
+1 -1
View File
@@ -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");
+15
View File
@@ -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,
+7
View File
@@ -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,
},