Skip to content
Open
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ impl App {
let wake = Arc::new(move || {
wake_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::Wake));
});
let desktop_wrapper = DesktopWrapper::new(rand::rng().random(), Arc::new(resource_storage), wgpu_context.clone(), wake);
let desktop_wrapper = DesktopWrapper::new(rand::rng().random(), Arc::new(resource_storage), dirs::app_autosave_documents_dir(), wgpu_context.clone(), wake);

Self {
render_state: None,
Expand Down
7 changes: 5 additions & 2 deletions desktop/wrapper/src/intercept_frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
if let Some(path) = path {
dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content });
} else {
// Derive the dialog filter from the suggested filename's extension so it tracks the
// editor's save-format preference (.gdd container or plain .graphite).
let extension = std::path::Path::new(&name).extension().and_then(|extension| extension.to_str()).unwrap_or("graphite").to_string();
dispatcher.respond(DesktopFrontendMessage::SaveFileDialog {
title: "Save Document".to_string(),
default_filename: name,
default_folder: folder,
filters: vec![FileFilter {
name: "Graphite".to_string(),
extensions: vec!["graphite".to_string()],
name: "Graphite Document".to_string(),
extensions: vec![extension],
}],
context: SaveFileDialogContext::Document { document_id, content },
});
Expand Down
4 changes: 2 additions & 2 deletions desktop/wrapper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub struct DesktopWrapper {
}

impl DesktopWrapper {
pub fn new(uuid_random_seed: u64, resource_storage: Arc<dyn ResourceStorage>, wgpu_context: WgpuContext, schedule_wake: Wake) -> Self {
pub fn new(uuid_random_seed: u64, resource_storage: Arc<dyn ResourceStorage>, working_copy_root: std::path::PathBuf, wgpu_context: WgpuContext, schedule_wake: Wake) -> Self {
#[cfg(target_os = "windows")]
let host = Host::Windows;
#[cfg(target_os = "macos")]
Expand All @@ -37,7 +37,7 @@ impl DesktopWrapper {
let application_io = PlatformApplicationIo::new_with_context(wgpu_context);

Self {
editor: Editor::new(env, uuid_random_seed, resource_storage, application_io, schedule_wake),
editor: Editor::new(env, uuid_random_seed, resource_storage, Some(working_copy_root), application_io, schedule_wake),
}
}

Expand Down
3 changes: 3 additions & 0 deletions editor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ gpu = ["interpreted-executor/gpu", "dep:wgpu-executor"]
# Local dependencies
graphite-proc-macros = { workspace = true }
graph-craft = { workspace = true }
graph-storage = { workspace = true, features = ["conversion"] }
document-format = { workspace = true }
document-container = { workspace = true }
graphene-hash = { workspace = true }
interpreted-executor = { workspace = true }
graphene-std = { workspace = true } # NOTE: `core-types` should not be added here because `graphene-std` re-exports its contents
Expand Down
11 changes: 9 additions & 2 deletions editor/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@ pub struct Editor {
}

impl Editor {
pub fn new(environment: Environment, uuid_random_seed: u64, resource_storage: Arc<dyn ResourceStorage>, mut application_io: PlatformApplicationIo, wake: Wake) -> Self {
pub fn new(
environment: Environment,
uuid_random_seed: u64,
resource_storage: Arc<dyn ResourceStorage>,
working_copy_root: Option<std::path::PathBuf>,
mut application_io: PlatformApplicationIo,
wake: Wake,
) -> Self {
ENVIRONMENT.set(environment).expect("Editor shoud only be initialized once");
graphene_std::uuid::set_uuid_seed(uuid_random_seed);

let mut dispatcher = Dispatcher::new(resource_storage);
let mut dispatcher = Dispatcher::new(resource_storage, working_copy_root);
dispatcher.message_handlers.future_message_handler.set_wake(wake);
application_io.inject_resource_proxy(dispatcher.message_handlers.resource_storage_message_handler.resources());
crate::node_graph_executor::replace_application_io(application_io);
Expand Down
3 changes: 3 additions & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf";

// DOCUMENT
pub const FILE_EXTENSION: &str = "graphite";
/// New document container format. Save now writes a `.gdd` (with the legacy `.graphite` embedded as a
/// fallback during the dual-write soak); `.graphite` stays a supported open/import input.
pub const GDD_FILE_EXTENSION: &str = "gdd";
Comment thread
TrueDoctor marked this conversation as resolved.
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
Expand Down
4 changes: 3 additions & 1 deletion editor/src/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"];

impl Dispatcher {
pub fn new(resource_storage: Arc<dyn ResourceStorage>) -> Self {
pub fn new(resource_storage: Arc<dyn ResourceStorage>, working_copy_root: Option<std::path::PathBuf>) -> Self {
let mut s = Self::default();
s.message_handlers.resource_storage_message_handler = ResourceStorageMessageHandler::new(resource_storage);
s.message_handlers.portfolio_message_handler.set_working_copy_root(working_copy_root);
s
}

Expand Down Expand Up @@ -264,6 +265,7 @@ impl Dispatcher {
menu_bar_message_handler.properties_panel_open = layout.is_panel_present(PanelType::Properties);
menu_bar_message_handler.message_logging_verbosity = self.message_handlers.debug_message_handler.message_logging_verbosity;
menu_bar_message_handler.reset_node_definitions_on_open = self.message_handlers.portfolio_message_handler.reset_node_definitions_on_open;
menu_bar_message_handler.show_storage_preferences = self.message_handlers.preferences_message_handler.show_storage_preferences;

if let Some(document) = self
.message_handlers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,62 @@ impl PreferencesDialogMessageHandler {
rows.extend_from_slice(&[header, node_graph_wires_label, graph_wire_style, brush_tool]);
}

// =========
// DOCUMENTS
// =========
// Soak-only `.gdd` storage options, hidden unless enabled from the developer debug menu.
if preferences.show_storage_preferences {
let header = vec![TextLabel::new("Documents").italic(true).widget_instance()];

let save_as_gdd_description = "
Save documents in the new .gdd container format (with the legacy .graphite file embedded as a recovery fallback) instead of a plain .graphite file. The .gdd format is still being validated, so this is opt-in for now.\n\
\n\
*Default: Off.*
"
.trim();
let save_as_gdd_checkbox_id = CheckboxId::new();
let save_as_gdd = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
CheckboxInput::new(preferences.save_as_gdd)
.tooltip_label("Save as .gdd")
.tooltip_description(save_as_gdd_description)
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::SaveAsGdd { enabled: checkbox_input.checked }.into())
.for_label(save_as_gdd_checkbox_id)
.widget_instance(),
TextLabel::new("Save as .gdd")
.tooltip_label("Save as .gdd")
.tooltip_description(save_as_gdd_description)
.for_checkbox(save_as_gdd_checkbox_id)
.widget_instance(),
];

let validate_description = "
Validate every document save, open, and undo by round-tripping it through the .gdd storage format and comparing against the legacy path, logging any mismatch. Useful for debugging the .gdd format during its soak, but the per-edit round-trip has a performance cost.\n\
\n\
*Default: Off.*
"
.trim();
let validate_checkbox_id = CheckboxId::new();
let validate_storage_round_trip = vec![
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
CheckboxInput::new(preferences.validate_storage_round_trip)
.tooltip_label("Validate Storage Round-Trip")
.tooltip_description(validate_description)
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::ValidateStorageRoundTrip { enabled: checkbox_input.checked }.into())
.for_label(validate_checkbox_id)
.widget_instance(),
TextLabel::new("Validate Storage Round-Trip")
.tooltip_label("Validate Storage Round-Trip")
.tooltip_description(validate_description)
.for_checkbox(validate_checkbox_id)
.widget_instance(),
];

rows.extend_from_slice(&[header, save_as_gdd, validate_storage_round_trip]);
}

// =============
// COMPATIBILITY
// =============
Expand Down
45 changes: 20 additions & 25 deletions editor/src/messages/future/future_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded};

use crate::messages::prelude::*;

// Native spawns onto a multi-thread tokio runtime, so the boxed future must be `Send`. Wasm uses
// `spawn_local` on the single JS thread, where `Send` is unavailable (OPFS/`JsFuture` are `!Send`)
// and unnecessary. `WasmNotSend` (`Send` on native, no-op on wasm) expresses the input bound on
// `MessageFuture::new`; the stored trait-object alias still needs a `cfg` split because `Send` is
// an auto trait usable in a `dyn` bound while `WasmNotSend` is not.
#[cfg(not(target_family = "wasm"))]
type InnerMessageFuture = Pin<Box<dyn Future<Output = Message> + Send + 'static>>;
#[cfg(target_family = "wasm")]
Expand Down Expand Up @@ -132,45 +137,35 @@ impl MessageHandler<FutureMessage, FutureMessageContext> for FutureMessageHandle

#[cfg(not(target_family = "wasm"))]
fn default_spawner() -> Arc<dyn MessageSpawner> {
Arc::new(TokioSpawner::default())
Arc::new(TokioSpawner)
}

#[cfg(target_family = "wasm")]
fn default_spawner() -> Arc<dyn MessageSpawner> {
Arc::new(WasmSpawner)
}

/// Process-global runtime for editor async work. Held in a `LazyLock` so it lives for the lifetime
/// of the process and is never dropped: dropping a `tokio::runtime::Runtime` blocks to join its
/// worker threads, which panics if it happens inside an async context (e.g. a `#[tokio::test]` body
/// or the desktop event loop). A leaked-for-process runtime sidesteps that entirely.
#[cfg(not(target_family = "wasm"))]
struct TokioSpawner {
/// Built lazily on first spawn. `multi_thread(1)` lets Tokio manage its own driver.
runtime: std::sync::OnceLock<tokio::runtime::Runtime>,
}

#[cfg(not(target_family = "wasm"))]
impl Default for TokioSpawner {
fn default() -> Self {
Self { runtime: std::sync::OnceLock::new() }
}
}
static EDITOR_ASYNC_RUNTIME: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.thread_name("graphite-async")
.enable_all()
.build()
.expect("failed to construct async-message tokio runtime")
});

#[cfg(not(target_family = "wasm"))]
impl TokioSpawner {
fn runtime(&self) -> &tokio::runtime::Runtime {
self.runtime.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.thread_name("graphite-async")
.enable_all()
.build()
.expect("failed to construct async-message tokio runtime")
})
}
}
struct TokioSpawner;

#[cfg(not(target_family = "wasm"))]
impl MessageSpawner for TokioSpawner {
fn spawn(&self, future: InnerMessageFuture, results: UnboundedSender<Message>, wake: Wake) {
self.runtime().spawn(async move {
EDITOR_ASYNC_RUNTIME.spawn(async move {
let message = future.await;
let _ = results.unbounded_send(message);
wake();
Expand Down
6 changes: 6 additions & 0 deletions editor/src/messages/menu_bar/menu_bar_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub struct MenuBarMessageHandler {
pub has_selection_history: (bool, bool),
pub message_logging_verbosity: MessageLoggingVerbosity,
pub reset_node_definitions_on_open: bool,
pub show_storage_preferences: bool,
pub make_path_editable_is_allowed: bool,
pub data_panel_open: bool,
pub layers_panel_open: bool,
Expand Down Expand Up @@ -50,6 +51,7 @@ impl LayoutHolder for MenuBarMessageHandler {
let message_logging_verbosity_names = self.message_logging_verbosity == MessageLoggingVerbosity::Names;
let message_logging_verbosity_contents = self.message_logging_verbosity == MessageLoggingVerbosity::Contents;
let reset_node_definitions_on_open = self.reset_node_definitions_on_open;
let show_storage_preferences = self.show_storage_preferences;
let make_path_editable_is_allowed = self.make_path_editable_is_allowed;

let about = MenuListEntry::new("About Graphite…")
Expand Down Expand Up @@ -718,6 +720,10 @@ impl LayoutHolder for MenuBarMessageHandler {
.label("Reset Nodes to Definitions on Open")
.icon(if reset_node_definitions_on_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
.on_commit(|_| PortfolioMessage::ToggleResetNodesToDefinitionsOnOpen.into()),
MenuListEntry::new("Show Storage Preferences")
.label("Show Storage Preferences")
.icon(if show_storage_preferences { "CheckboxChecked" } else { "CheckboxUnchecked" })
.on_commit(|_| PreferencesMessage::ToggleShowStoragePreferences.into()),
],
vec![
MenuListEntry::new("Print Trace Logs")
Expand Down
Loading
Loading