diff --git a/Cargo.lock b/Cargo.lock index 6b3e9a923..d8d686732 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -765,7 +765,7 @@ dependencies = [ "thiserror 2.0.16", "tracing", "wgpu", - "windows", + "windows 0.58.0", "windows-sys 0.61.2", ] @@ -1242,6 +1242,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "ctrlc" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + [[package]] name = "cursor-icon" version = "1.2.0" @@ -1764,7 +1775,7 @@ dependencies = [ "read-fonts 0.35.0", "roxmltree", "smallvec", - "windows", + "windows 0.58.0", "windows-core 0.58.0", "yeslogic-fontconfig-sys", ] @@ -2082,7 +2093,7 @@ dependencies = [ "log", "presser", "thiserror 1.0.69", - "windows", + "windows 0.58.0", ] [[package]] @@ -2262,6 +2273,7 @@ dependencies = [ "cef", "cef-dll-sys", "clap", + "ctrlc", "derivative", "dirs", "futures", @@ -2273,6 +2285,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "open", + "pidlock", "rand 0.9.2", "rfd", "ron", @@ -2282,7 +2295,7 @@ dependencies = [ "tracing-subscriber", "vello", "wgpu", - "windows", + "windows 0.58.0", "winit", ] @@ -4198,6 +4211,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pidlock" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f837924d5368f9f35a1c404699de3c074311358035c77d7164f5948c08b31382" +dependencies = [ + "nix", + "thiserror 1.0.69", + "windows 0.62.2", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -7029,7 +7053,7 @@ dependencies = [ "wasm-bindgen", "web-sys", "wgpu-types", - "windows", + "windows 0.58.0", "windows-core 0.58.0", ] @@ -7088,6 +7112,27 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.58.0" @@ -7107,13 +7152,37 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.0", - "windows-interface 0.59.1", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -7127,9 +7196,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -7149,9 +7218,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -7170,6 +7239,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-registry" version = "0.5.3" @@ -7199,6 +7278,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -7218,6 +7306,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -7311,6 +7408,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 6a22efd31..339df2599 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -43,6 +43,8 @@ open = { workspace = true } rand = { workspace = true, features = ["thread_rng"] } serde = { workspace = true } clap = { workspace = true, features = ["derive"] } +pidlock = "0.2.2" +ctrlc = "3.5.1" # Windows-specific dependencies [target.'cfg(target_os = "windows")'.dependencies] diff --git a/desktop/bundle/src/mac.rs b/desktop/bundle/src/mac.rs index 90684ce85..030e8f060 100644 --- a/desktop/bundle/src/mac.rs +++ b/desktop/bundle/src/mac.rs @@ -40,7 +40,7 @@ fn bundle(out_dir: &Path, app_bin: &Path, helper_bin: &Path) -> PathBuf { create_app(&app_dir, APP_ID, APP_NAME, app_bin, false); - for helper_type in [None, Some("GPU"), Some("Renderer"), Some("Plugin"), Some("Alerts")] { + for helper_type in [None, Some("GPU"), Some("Renderer")] { let helper_id_suffix = helper_type.map(|t| format!(".{t}")).unwrap_or_default(); let helper_id = format!("{APP_ID}.helper{helper_id_suffix}"); let helper_name_suffix = helper_type.map(|t| format!(" ({t})")).unwrap_or_default(); diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 6fd0b2aeb..2b8f7df42 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -56,6 +56,13 @@ impl App { app_event_scheduler: AppEventScheduler, launch_documents: Vec, ) -> Self { + let ctrlc_app_event_scheduler = app_event_scheduler.clone(); + ctrlc::set_handler(move || { + tracing::info!("Termination signal received, exiting..."); + ctrlc_app_event_scheduler.schedule(AppEvent::CloseWindow); + }) + .expect("Error setting Ctrl-C handler"); + let rendering_app_event_scheduler = app_event_scheduler.clone(); let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1); std::thread::spawn(move || { @@ -365,6 +372,7 @@ impl App { tracing::info!("Exiting main event loop"); event_loop.exit(); } + #[cfg(target_os = "macos")] AppEvent::MenuEvent { id } => { self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id }); } diff --git a/desktop/src/cef/context/builder.rs b/desktop/src/cef/context/builder.rs index 8932c5212..566ef1a0b 100644 --- a/desktop/src/cef/context/builder.rs +++ b/desktop/src/cef/context/builder.rs @@ -11,7 +11,7 @@ use super::CefContext; use super::singlethreaded::SingleThreadedCefContext; use crate::cef::CefEventHandler; use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME}; -use crate::cef::dirs::create_instance_dir; +use crate::cef::dirs::{create_instance_dir, delete_instance_dirs}; use crate::cef::input::InputState; use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderProcessAppImpl, SchemeHandlerFactoryImpl}; @@ -85,6 +85,7 @@ impl CefContextBuilder { #[cfg(target_os = "macos")] pub(crate) fn initialize(self, event_handler: H, disable_gpu_acceleration: bool) -> Result { + delete_instance_dirs(); let instance_dir = create_instance_dir(); let exe = std::env::current_exe().expect("cannot get current exe path"); @@ -105,6 +106,7 @@ impl CefContextBuilder { #[cfg(not(target_os = "macos"))] pub(crate) fn initialize(self, event_handler: H, disable_gpu_acceleration: bool) -> Result { + delete_instance_dirs(); let instance_dir = create_instance_dir(); let settings = Settings { diff --git a/desktop/src/cef/context/singlethreaded.rs b/desktop/src/cef/context/singlethreaded.rs index 94acc658e..61662c4d4 100644 --- a/desktop/src/cef/context/singlethreaded.rs +++ b/desktop/src/cef/context/singlethreaded.rs @@ -43,7 +43,19 @@ impl CefContext for SingleThreadedCefContext { impl Drop for SingleThreadedCefContext { fn drop(&mut self) { cef::shutdown(); - std::fs::remove_dir_all(&self.instance_dir).expect("Failed to remove CEF cache directory"); + + // Sometimes some CEF processes still linger at this point and hold file handles to the cache directory. + // To mitigate this, we try to remove the directory multiple times with some delay. + // TODO: find a better solution if possible. + for _ in 0..30 { + match std::fs::remove_dir_all(&self.instance_dir) { + Ok(_) => break, + Err(e) => { + tracing::warn!("Failed to remove CEF cache directory, retrying...: {e}"); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + } + } } } diff --git a/desktop/src/cef/dirs.rs b/desktop/src/cef/dirs.rs index 8511011e2..5046fc6e7 100644 --- a/desktop/src/cef/dirs.rs +++ b/desktop/src/cef/dirs.rs @@ -1,12 +1,24 @@ use std::path::PathBuf; -use crate::dirs::{ensure_dir_exists, graphite_data_dir}; +use crate::dirs::{app_data_dir, ensure_dir_exists}; static CEF_DIR_NAME: &str = "browser"; +pub(crate) fn delete_instance_dirs() { + let cef_dir = app_data_dir().join(CEF_DIR_NAME); + if let Ok(entries) = std::fs::read_dir(&cef_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + let _ = std::fs::remove_dir_all(&path); + } + } + } +} + pub(crate) fn create_instance_dir() -> PathBuf { let instance_id: String = (0..32).map(|_| format!("{:x}", rand::random::() % 16)).collect(); - let path = graphite_data_dir().join(CEF_DIR_NAME).join(instance_id); + let path = app_data_dir().join(CEF_DIR_NAME).join(instance_id); ensure_dir_exists(&path); path } diff --git a/desktop/src/cef/input.rs b/desktop/src/cef/input.rs index 763d82b12..fef0e1e43 100644 --- a/desktop/src/cef/input.rs +++ b/desktop/src/cef/input.rs @@ -1,4 +1,4 @@ -use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t}; +use cef::sys::{cef_key_event_type_t, cef_mouse_button_type_t}; use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent}; use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent}; @@ -6,7 +6,7 @@ mod keymap; use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits}; mod state; -pub(crate) use state::InputState; +pub(crate) use state::{CefModifiers, InputState}; use super::consts::{PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y}; @@ -129,9 +129,10 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat } let Some(host) = browser.host() else { return }; - let mut mouse_event: MouseEvent = input_state.into(); - mouse_event.modifiers |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0 as u32; - mouse_event.modifiers |= cef_event_flags_t::EVENTFLAG_PRECISION_SCROLLING_DELTA.0 as u32; + let mouse_event = MouseEvent { + modifiers: CefModifiers::PINCH_MODIFIERS.into(), + ..input_state.into() + }; let delta = (delta * PINCH_ZOOM_SPEED).round() as i32; diff --git a/desktop/src/cef/input/state.rs b/desktop/src/cef/input/state.rs index f6fb981fc..d3c249cba 100644 --- a/desktop/src/cef/input/state.rs +++ b/desktop/src/cef/input/state.rs @@ -240,10 +240,17 @@ impl CefModifiers { Self(inner) } + + pub(super) const PINCH_MODIFIERS: Self = Self(cef_event_flags_t( + cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0 | cef_event_flags_t::EVENTFLAG_PRECISION_SCROLLING_DELTA.0, + )); } -impl Into for CefModifiers { - fn into(self) -> u32 { - self.0.0 as u32 +impl From for u32 { + fn from(val: CefModifiers) -> Self { + #[cfg(not(target_os = "windows"))] + return val.0.0; + #[cfg(target_os = "windows")] + return val.0.0 as u32; } } diff --git a/desktop/src/consts.rs b/desktop/src/consts.rs index 431f27337..28879574d 100644 --- a/desktop/src/consts.rs +++ b/desktop/src/consts.rs @@ -1,7 +1,8 @@ pub(crate) const APP_NAME: &str = "Graphite"; pub(crate) const APP_ID: &str = "rs.graphite.Graphite"; -pub(crate) const APP_DIRECTORY_NAME: &str = "graphite-editor"; +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_DOCUMENTS_DIRECTORY_NAME: &str = "documents"; diff --git a/desktop/src/dirs.rs b/desktop/src/dirs.rs index 94f62c686..584f1a290 100644 --- a/desktop/src/dirs.rs +++ b/desktop/src/dirs.rs @@ -9,14 +9,14 @@ pub(crate) fn ensure_dir_exists(path: &PathBuf) { } } -pub(crate) fn graphite_data_dir() -> PathBuf { +pub(crate) fn app_data_dir() -> PathBuf { let path = dirs::data_dir().expect("Failed to get data directory").join(APP_DIRECTORY_NAME); ensure_dir_exists(&path); path } -pub(crate) fn graphite_autosave_documents_dir() -> PathBuf { - let path = graphite_data_dir().join(APP_DOCUMENTS_DIRECTORY_NAME); +pub(crate) fn app_autosave_documents_dir() -> PathBuf { + let path = app_data_dir().join(APP_DOCUMENTS_DIRECTORY_NAME); ensure_dir_exists(&path); path } diff --git a/desktop/src/event.rs b/desktop/src/event.rs index 44ce71bd1..6d8dbbe10 100644 --- a/desktop/src/event.rs +++ b/desktop/src/event.rs @@ -9,7 +9,10 @@ pub(crate) enum AppEvent { DesktopWrapperMessage(DesktopWrapperMessage), NodeGraphExecutionResult(NodeGraphExecutionResult), CloseWindow, - MenuEvent { id: String }, + #[cfg(target_os = "macos")] + MenuEvent { + id: String, + }, } #[derive(Clone)] diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 35e604c2a..692adbab0 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -23,6 +23,8 @@ use cef::CefHandler; use cli::Cli; use event::CreateAppEventSchedulerEventLoopExt; +use crate::consts::APP_LOCK_FILE_NAME; + pub fn start() { tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init(); @@ -36,6 +38,22 @@ pub fn start() { return; } + let mut lock = pidlock::Pidlock::new_validated(dirs::app_data_dir().join(APP_LOCK_FILE_NAME)).unwrap(); + match lock.acquire() { + Ok(lock) => { + tracing::info!("Acquired application lock"); + lock + } + Err(pidlock::PidlockError::LockExists) => { + tracing::error!("Another instance is already running, Exiting."); + exit(0); + } + Err(err) => { + tracing::error!("Failed to acquire application lock: {err}"); + exit(1); + } + }; + App::init(); let cli = Cli::parse(); @@ -56,7 +74,7 @@ pub fn start() { } Err(cef::InitError::AlreadyRunning) => { tracing::error!("Another instance is already running, Exiting."); - exit(0); + exit(1); } Err(cef::InitError::InitializationFailed(code)) => { tracing::error!("Cef initialization failed with code: {code}"); diff --git a/desktop/src/persist.rs b/desktop/src/persist.rs index 003bb83f8..a5baa211b 100644 --- a/desktop/src/persist.rs +++ b/desktop/src/persist.rs @@ -125,13 +125,13 @@ impl PersistentData { } fn state_file_path() -> std::path::PathBuf { - let mut path = crate::dirs::graphite_data_dir(); + let mut path = crate::dirs::app_data_dir(); path.push(crate::consts::APP_STATE_FILE_NAME); path } fn preferences_file_path() -> std::path::PathBuf { - let mut path = crate::dirs::graphite_data_dir(); + let mut path = crate::dirs::app_data_dir(); path.push(crate::consts::APP_PREFERENCES_FILE_NAME); path } @@ -189,7 +189,7 @@ impl DocumentStore { } fn document_path(id: &DocumentId) -> std::path::PathBuf { - let mut path = crate::dirs::graphite_autosave_documents_dir(); + let mut path = crate::dirs::app_autosave_documents_dir(); path.push(format!("{:x}.graphite", id.0)); path } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index 202fec98a..4a0c50b0e 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -593,7 +593,7 @@ impl OverlayContextInternal { let mid = edge_end.midpoint(edge_start); for edge in [edge_dir, -edge_dir] { - self.draw_triangle(mid + edge * 3. + SKEW_TRIANGLE_OFFSET, edge, SKEW_TRIANGLE_SIZE, None, None); + self.draw_triangle(mid + edge * (3. + SKEW_TRIANGLE_OFFSET), edge, SKEW_TRIANGLE_SIZE, None, None); } }