From e6e7d3a61455348ee70342995b430dd311b93f00 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 5 May 2026 15:10:27 +0200 Subject: [PATCH 1/3] Desktop: Implement native file open handler --- desktop/src/app.rs | 59 ++++++++++++++++++--------- desktop/src/event.rs | 2 + desktop/src/lib.rs | 11 ++++- desktop/src/window/mac.rs | 2 +- desktop/src/window/mac/app.rs | 76 +++++++++++++++++++++++++++++++++-- 5 files changed, 125 insertions(+), 25 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 44eadbe046..74c5098b4e 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -2,6 +2,7 @@ use rand::Rng; use rfd::AsyncFileDialog; use std::fs; use std::io::Read; +use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{Receiver, Sender, SyncSender}; @@ -14,7 +15,6 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::window::WindowId; use crate::cef; -use crate::cli::Cli; use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS; use crate::event::{AppEvent, AppEventScheduler}; use crate::persist; @@ -47,7 +47,8 @@ pub(crate) struct App { web_communication_startup_buffer: Vec>, #[cfg_attr(not(target_os = "macos"), expect(unused))] preferences: Preferences, - cli: Cli, + launch_documents: Option>, + disable_ui_acceleration: bool, startup_time: Option, exiting: Arc, exit_reason: ExitReason, @@ -58,6 +59,7 @@ impl App { Window::init(); } + #[allow(clippy::too_many_arguments)] pub(crate) fn new( cef_context: Box, cef_view_info_sender: Sender, @@ -65,7 +67,8 @@ impl App { app_event_receiver: Receiver, app_event_scheduler: AppEventScheduler, preferences: Preferences, - cli: Cli, + launch_documents: Vec, + disable_ui_acceleration: bool, ) -> Self { let ctrlc_app_event_scheduler = app_event_scheduler.clone(); ctrlc::set_handler(move || { @@ -115,7 +118,8 @@ impl App { web_communication_initialized: false, web_communication_startup_buffer: Vec::new(), preferences, - cli, + launch_documents: Some(launch_documents), + disable_ui_acceleration, startup_time: None, exiting, exit_reason: ExitReason::Shutdown, @@ -307,22 +311,11 @@ impl App { responses.push(message); } DesktopFrontendMessage::OpenLaunchDocuments => { - if self.cli.files.is_empty() { + let Some(launch_documents) = std::mem::take(&mut self.launch_documents) else { + tracing::error!("OpenLaunchDocuments should only be send once"); return; - } - let app_event_scheduler = self.app_event_scheduler.clone(); - let launch_documents = std::mem::take(&mut self.cli.files); - let _ = thread::spawn(move || { - for path in launch_documents { - tracing::info!("Opening file from command line: {}", path.display()); - if let Ok(content) = fs::read(&path) { - let message = DesktopWrapperMessage::OpenFile { path, content }; - app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message)); - } else { - tracing::error!("Failed to read file: {}", path.display()); - } - } - }); + }; + self.open_files(launch_documents); } DesktopFrontendMessage::UpdateMenu { entries } => { if let Some(window) = &self.window { @@ -476,11 +469,37 @@ impl App { event_loop.exit(); } #[cfg(target_os = "macos")] + AppEvent::AddLaunchDocuments(paths) => { + if let Some(launch_documents) = &mut self.launch_documents { + launch_documents.extend(paths); + } else { + self.open_files(paths); + } + } + #[cfg(target_os = "macos")] AppEvent::MenuEvent { id } => { self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id }); } } } + + fn open_files(&mut self, paths: Vec) { + if paths.is_empty() { + return; + } + let app_event_scheduler = self.app_event_scheduler.clone(); + let _ = thread::spawn(move || { + for path in paths { + tracing::info!("Opening file: {}", path.display()); + if let Ok(content) = fs::read(&path) { + let message = DesktopWrapperMessage::OpenFile { path, content }; + app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(message)); + } else { + tracing::error!("Failed to read file: {}", path.display()); + } + } + }); + } } impl ApplicationHandler for App { fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { @@ -570,7 +589,7 @@ impl ApplicationHandler for App { } if !self.cef_init_successful - && !self.cli.disable_ui_acceleration + && !self.disable_ui_acceleration && self.web_communication_initialized && let Some(startup_time) = self.startup_time && startup_time.elapsed() > Duration::from_secs(3) diff --git a/desktop/src/event.rs b/desktop/src/event.rs index 9e8bb2bf4e..99776e6812 100644 --- a/desktop/src/event.rs +++ b/desktop/src/event.rs @@ -10,6 +10,8 @@ pub(crate) enum AppEvent { NodeGraphExecutionResult(NodeGraphExecutionResult), Exit, #[cfg(target_os = "macos")] + AddLaunchDocuments(Vec), + #[cfg(target_os = "macos")] MenuEvent { id: String, }, diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index a22f3386f5..5a3dad5691 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -102,7 +102,16 @@ pub fn start() { } }; - let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli); + let app = App::new( + Box::new(cef_context), + cef_view_info_sender, + wgpu_context, + app_event_receiver, + app_event_scheduler, + prefs, + cli.files, + cli.disable_ui_acceleration, + ); let exit_reason = app.run(event_loop); diff --git a/desktop/src/window/mac.rs b/desktop/src/window/mac.rs index 4949159f3f..56f6a82fd5 100644 --- a/desktop/src/window/mac.rs +++ b/desktop/src/window/mac.rs @@ -26,8 +26,8 @@ impl super::NativeWindow for NativeWindowImpl { } fn new(_window: &dyn Window, app_event_scheduler: AppEventScheduler) -> Self { + app::setup(app_event_scheduler.clone()); let menu = menu::Menu::new(app_event_scheduler); - NativeWindowImpl { menu } } diff --git a/desktop/src/window/mac/app.rs b/desktop/src/window/mac/app.rs index 03d9ff579b..c42e1cdf41 100644 --- a/desktop/src/window/mac/app.rs +++ b/desktop/src/window/mac/app.rs @@ -1,6 +1,22 @@ -use objc2::{ClassType, define_class, msg_send}; -use objc2_app_kit::{NSApplication, NSEvent, NSEventType, NSResponder}; -use objc2_foundation::NSObject; +use std::ffi::CStr; +use std::ffi::OsStr; +use std::ops::Deref; +use std::os::unix::ffi::OsStrExt; +use std::path::PathBuf; +use std::sync::{Mutex, Once}; + +use objc2::rc::Retained; +use objc2::runtime::ProtocolObject; +use objc2::{ClassType, MainThreadMarker, MainThreadOnly, define_class, msg_send}; +use objc2_app_kit::{NSApplication, NSApplicationDelegate, NSEvent, NSEventType, NSResponder}; +use objc2_foundation::{NSArray, NSObject, NSObjectProtocol, NSURL}; + +use crate::event::{AppEvent, AppEventScheduler}; + +static APP_EVENT_SCHEDULER: Mutex> = Mutex::new(None); +static INSTALL_DELEGATE: Once = Once::new(); + +static LAUNCH_DOCUMENTS: Mutex> = Mutex::new(Vec::new()); define_class!( #[unsafe(super(NSApplication, NSResponder, NSObject))] @@ -20,12 +36,66 @@ define_class!( } ); +define_class!( + #[unsafe(super(NSObject))] + #[thread_kind = MainThreadOnly] + #[name = "GraphiteApplicationDelegate"] + struct GraphiteApplicationDelegate; + + unsafe impl NSObjectProtocol for GraphiteApplicationDelegate {} + + unsafe impl NSApplicationDelegate for GraphiteApplicationDelegate { + #[unsafe(method(application:openURLs:))] + fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray) { + let Some(app_event_scheduler) = APP_EVENT_SCHEDULER.lock().ok() else { + tracing::error!("Received macOS open URL event before the app event scheduler was initialized"); + return; + }; + + let mut pending_paths_to_open = LAUNCH_DOCUMENTS.lock().unwrap(); + + for index in 0..urls.count() { + let url = urls.objectAtIndex(index); + if !url.isFileURL() { + tracing::error!("Ignoring macOS open URL event for non-file URL: {:?}", url); + continue; + } + + let path = unsafe { CStr::from_ptr(url.fileSystemRepresentation().as_ptr()) }; + let path = PathBuf::from(OsStr::from_bytes(path.to_bytes())); + + pending_paths_to_open.push(path); + } + + if let Some(app_event_scheduler) = app_event_scheduler.deref() { + app_event_scheduler.schedule(AppEvent::AddLaunchDocuments(std::mem::take(&mut pending_paths_to_open))); + } + } + } +); + fn instance() -> objc2::rc::Retained { unsafe { msg_send![GraphiteApplication::class(), sharedApplication] } } pub(super) fn init() { let _ = instance(); + + INSTALL_DELEGATE.call_once(|| { + let mtm = MainThreadMarker::new().expect("only ever called from main thread"); + let delegate: Retained = unsafe { msg_send![super(GraphiteApplicationDelegate::alloc(mtm).set_ivars(())), init] }; + instance().setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + std::mem::forget(delegate); + }); +} + +pub(super) fn setup(app_event_scheduler: AppEventScheduler) { + let mut app_event_scheduler_guard = APP_EVENT_SCHEDULER.lock().unwrap(); + + let mut pending_paths_to_open = LAUNCH_DOCUMENTS.lock().unwrap(); + app_event_scheduler.schedule(AppEvent::AddLaunchDocuments(std::mem::take(&mut pending_paths_to_open))); + + *app_event_scheduler_guard = Some(app_event_scheduler); } pub(super) fn hide() { From c0195af63a7934d45dd176bc4415172810cbf613 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 5 May 2026 15:46:31 +0200 Subject: [PATCH 2/3] Desktop: Register file types on Mac --- desktop/bundle/src/mac.rs | 97 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/desktop/bundle/src/mac.rs b/desktop/bundle/src/mac.rs index 3e53195ef6..38f8e866eb 100644 --- a/desktop/bundle/src/mac.rs +++ b/desktop/bundle/src/mac.rs @@ -13,6 +13,9 @@ const EXEC_PATH: &str = "Contents/MacOS"; const FRAMEWORKS_PATH: &str = "Contents/Frameworks"; const RESOURCES_PATH: &str = "Contents/Resources"; const CEF_FRAMEWORK: &str = "Chromium Embedded Framework.framework"; +const GRAPHITE_DOCUMENT_TYPE: &str = "art.graphite.document"; +const GRAPHITE_FILE_EXTENSION: &str = "graphite"; +const GRAPHITE_MIME_TYPE: &str = "application/graphite+json"; pub fn main() -> Result<(), Box> { let app_bin = build_bin("graphite-desktop-platform-mac", None)?; @@ -73,7 +76,7 @@ fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) -> cf_bundle_identifier: id.to_string(), cf_bundle_display_name: exec_name.to_string(), cf_bundle_executable: exec_name.to_string(), - cf_bundle_icon_file: ICONS_FILE_NAME.to_string(), + cf_bundle_icon_file: if is_helper { None } else { Some(ICONS_FILE_NAME.to_string()) }, cf_bundle_info_dictionary_version: "6.0".to_string(), cf_bundle_package_type: "APPL".to_string(), cf_bundle_signature: "????".to_string(), @@ -85,6 +88,8 @@ fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) -> ls_minimum_system_version: "11.0".to_string(), ls_ui_element: if is_helper { Some("1".to_string()) } else { None }, ns_supports_automatic_graphics_switching: true, + cf_bundle_document_types: (!is_helper).then(document_types), + ut_exported_type_declarations: (!is_helper).then(exported_type_declarations), }; let plist_file = dir.join("Info.plist"); @@ -92,6 +97,47 @@ fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) -> Ok(()) } +fn document_types() -> Vec { + vec![ + DocumentType { + cf_bundle_type_name: "Graphite Document".to_string(), + cf_bundle_type_role: "Editor".to_string(), + cf_bundle_type_extensions: Some(vec![GRAPHITE_FILE_EXTENSION.to_string()]), + cf_bundle_type_icon_file: Some(ICONS_FILE_NAME.to_string()), + ls_handler_rank: Some("Owner".to_string()), + ls_item_content_types: vec![GRAPHITE_DOCUMENT_TYPE.to_string()], + }, + DocumentType { + cf_bundle_type_name: "SVG Image".to_string(), + cf_bundle_type_role: "Editor".to_string(), + cf_bundle_type_extensions: Some(vec!["svg".to_string()]), + cf_bundle_type_icon_file: None, + ls_handler_rank: Some("Alternate".to_string()), + ls_item_content_types: vec!["public.svg-image".to_string()], + }, + DocumentType { + cf_bundle_type_name: "Image".to_string(), + cf_bundle_type_role: "Editor".to_string(), + cf_bundle_type_extensions: None, + cf_bundle_type_icon_file: None, + ls_handler_rank: Some("Alternate".to_string()), + ls_item_content_types: vec!["public.image".to_string()], + }, + ] +} + +fn exported_type_declarations() -> Vec { + vec![ExportedTypeDeclaration { + ut_type_identifier: GRAPHITE_DOCUMENT_TYPE.to_string(), + ut_type_description: "Graphite Document".to_string(), + ut_type_conforms_to: vec!["public.json".to_string()], + ut_type_tag_specification: TypeTagSpecification { + public_filename_extension: vec![GRAPHITE_FILE_EXTENSION.to_string()], + public_mime_type: GRAPHITE_MIME_TYPE.to_string(), + }, + }] +} + #[derive(serde::Serialize)] struct InfoPlist { #[serde(rename = "CFBundleName")] @@ -103,7 +149,8 @@ struct InfoPlist { #[serde(rename = "CFBundleExecutable")] cf_bundle_executable: String, #[serde(rename = "CFBundleIconFile")] - cf_bundle_icon_file: String, + #[serde(skip_serializing_if = "Option::is_none")] + cf_bundle_icon_file: Option, #[serde(rename = "CFBundleInfoDictionaryVersion")] cf_bundle_info_dictionary_version: String, #[serde(rename = "CFBundlePackageType")] @@ -123,7 +170,53 @@ struct InfoPlist { #[serde(rename = "LSMinimumSystemVersion")] ls_minimum_system_version: String, #[serde(rename = "LSUIElement")] + #[serde(skip_serializing_if = "Option::is_none")] ls_ui_element: Option, #[serde(rename = "NSSupportsAutomaticGraphicsSwitching")] ns_supports_automatic_graphics_switching: bool, + #[serde(rename = "CFBundleDocumentTypes")] + #[serde(skip_serializing_if = "Option::is_none")] + cf_bundle_document_types: Option>, + #[serde(rename = "UTExportedTypeDeclarations")] + #[serde(skip_serializing_if = "Option::is_none")] + ut_exported_type_declarations: Option>, +} + +#[derive(serde::Serialize)] +struct DocumentType { + #[serde(rename = "CFBundleTypeName")] + cf_bundle_type_name: String, + #[serde(rename = "CFBundleTypeRole")] + cf_bundle_type_role: String, + #[serde(rename = "CFBundleTypeExtensions")] + #[serde(skip_serializing_if = "Option::is_none")] + cf_bundle_type_extensions: Option>, + #[serde(rename = "CFBundleTypeIconFile")] + #[serde(skip_serializing_if = "Option::is_none")] + cf_bundle_type_icon_file: Option, + #[serde(rename = "LSHandlerRank")] + #[serde(skip_serializing_if = "Option::is_none")] + ls_handler_rank: Option, + #[serde(rename = "LSItemContentTypes")] + ls_item_content_types: Vec, +} + +#[derive(serde::Serialize)] +struct ExportedTypeDeclaration { + #[serde(rename = "UTTypeIdentifier")] + ut_type_identifier: String, + #[serde(rename = "UTTypeDescription")] + ut_type_description: String, + #[serde(rename = "UTTypeConformsTo")] + ut_type_conforms_to: Vec, + #[serde(rename = "UTTypeTagSpecification")] + ut_type_tag_specification: TypeTagSpecification, +} + +#[derive(serde::Serialize)] +struct TypeTagSpecification { + #[serde(rename = "public.filename-extension")] + public_filename_extension: Vec, + #[serde(rename = "public.mime-type")] + public_mime_type: String, } From 50eb2a8dcf619cc9d8eac4f3bdb9da2c077d03bf Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 6 May 2026 13:05:10 +0200 Subject: [PATCH 3/3] Review --- desktop/src/app.rs | 7 ++----- desktop/src/cef/context/builder.rs | 4 ++-- desktop/src/lib.rs | 21 +++++++-------------- desktop/src/window/mac/app.rs | 5 +---- 4 files changed, 12 insertions(+), 25 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 74c5098b4e..d69b578350 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -48,7 +48,6 @@ pub(crate) struct App { #[cfg_attr(not(target_os = "macos"), expect(unused))] preferences: Preferences, launch_documents: Option>, - disable_ui_acceleration: bool, startup_time: Option, exiting: Arc, exit_reason: ExitReason, @@ -68,7 +67,6 @@ impl App { app_event_scheduler: AppEventScheduler, preferences: Preferences, launch_documents: Vec, - disable_ui_acceleration: bool, ) -> Self { let ctrlc_app_event_scheduler = app_event_scheduler.clone(); ctrlc::set_handler(move || { @@ -119,7 +117,6 @@ impl App { web_communication_startup_buffer: Vec::new(), preferences, launch_documents: Some(launch_documents), - disable_ui_acceleration, startup_time: None, exiting, exit_reason: ExitReason::Shutdown, @@ -312,7 +309,7 @@ impl App { } DesktopFrontendMessage::OpenLaunchDocuments => { let Some(launch_documents) = std::mem::take(&mut self.launch_documents) else { - tracing::error!("OpenLaunchDocuments should only be send once"); + tracing::error!("OpenLaunchDocuments should only be sent once"); return; }; self.open_files(launch_documents); @@ -589,7 +586,7 @@ impl ApplicationHandler for App { } if !self.cef_init_successful - && !self.disable_ui_acceleration + && !self.preferences.disable_ui_acceleration && self.web_communication_initialized && let Some(startup_time) = self.startup_time && startup_time.elapsed() > Duration::from_secs(3) diff --git a/desktop/src/cef/context/builder.rs b/desktop/src/cef/context/builder.rs index f6cddf79ff..f47856cf60 100644 --- a/desktop/src/cef/context/builder.rs +++ b/desktop/src/cef/context/builder.rs @@ -131,13 +131,13 @@ fn platform_settings(instance_dir: &Path) -> Settings { { let exe = std::env::current_exe().expect("cannot get current exe path"); let app_root = exe.parent().and_then(|p| p.parent()).expect("bad path structure").parent().expect("bad path structure"); - return Settings { + Settings { main_bundle_path: app_root.to_str().map(CefString::from).unwrap(), multi_threaded_message_loop: 0, external_message_pump: 1, no_sandbox: 1, // GPU helper crashes when running with sandbox ..base - }; + } } #[cfg(not(target_os = "macos"))] diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 5a3dad5691..a23d80edd2 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -67,7 +67,7 @@ pub fn start() { // TODO: Eventually remove this cleanup code for the old "browser" CEF directory dirs::delete_old_cef_browser_directory(); - let prefs = preferences::read(); + let mut prefs = preferences::read(); // Must be called before event loop initialization or native window integrations will break App::init(); @@ -80,13 +80,15 @@ pub fn start() { let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel(); - let disable_ui_acceleration = prefs.disable_ui_acceleration || cli.disable_ui_acceleration; - if disable_ui_acceleration { + if cli.disable_ui_acceleration { + prefs.disable_ui_acceleration = true; + } + if prefs.disable_ui_acceleration { println!("UI acceleration is disabled"); } let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), cef_view_info_receiver); - let cef_context = match cef_context_builder.create(cef_handler, disable_ui_acceleration) { + let cef_context = match cef_context_builder.create(cef_handler, prefs.disable_ui_acceleration) { Ok(context) => { tracing::info!("CEF initialized successfully"); context @@ -102,16 +104,7 @@ pub fn start() { } }; - let app = App::new( - Box::new(cef_context), - cef_view_info_sender, - wgpu_context, - app_event_receiver, - app_event_scheduler, - prefs, - cli.files, - cli.disable_ui_acceleration, - ); + let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli.files); let exit_reason = app.run(event_loop); diff --git a/desktop/src/window/mac/app.rs b/desktop/src/window/mac/app.rs index c42e1cdf41..cc97aca100 100644 --- a/desktop/src/window/mac/app.rs +++ b/desktop/src/window/mac/app.rs @@ -47,10 +47,7 @@ define_class!( unsafe impl NSApplicationDelegate for GraphiteApplicationDelegate { #[unsafe(method(application:openURLs:))] fn application_open_urls(&self, _application: &NSApplication, urls: &NSArray) { - let Some(app_event_scheduler) = APP_EVENT_SCHEDULER.lock().ok() else { - tracing::error!("Received macOS open URL event before the app event scheduler was initialized"); - return; - }; + let app_event_scheduler = APP_EVENT_SCHEDULER.lock().unwrap(); let mut pending_paths_to_open = LAUNCH_DOCUMENTS.lock().unwrap();