diff --git a/Cargo.lock b/Cargo.lock index 2950702c17..bbef9bbbbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1078,6 +1078,15 @@ dependencies = [ "itertools 0.13.0", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -2272,6 +2281,7 @@ dependencies = [ "graphite-desktop-wrapper", "libc", "metal", + "muda", "objc", "open", "rand 0.9.2", @@ -2335,8 +2345,10 @@ dependencies = [ "graphene-std", "graphite-editor", "image", + "keyboard-types", "ron", "serde", + "serde_json", "thiserror 2.0.16", "tracing", "vello", @@ -3398,6 +3410,24 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "muda" +version = "0.17.1" +source = "git+https://github.com/tauri-apps/muda.git?rev=3f460b8fbaed59cda6d95ceea6904f000f093f15#3f460b8fbaed59cda6d95ceea6904f000f093f15" +dependencies = [ + "crossbeam-channel", + "dpi", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "thiserror 2.0.16", + "windows-sys 0.60.2", +] + [[package]] name = "naga" version = "25.0.1" diff --git a/Cargo.toml b/Cargo.toml index 23ff4aaac2..3f7d16edc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,6 +135,7 @@ web-sys = { version = "=0.3.77", features = [ "ImageBitmapRenderingContext", ] } winit = { git = "https://github.com/rust-windowing/winit.git" } +keyboard-types = "0.8" url = "2.5" tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] } vello = { git = "https://github.com/linebender/vello.git", rev = "87cc5bee6d3a34d15017dbbb58634ddc7f33ff9b" } # TODO switch back to stable when a release is made @@ -225,3 +226,7 @@ debug = true [profile.profiling] inherits = "release" debug = true + +[patch.crates-io] +# Force cargo to use only one version of the dpi crate (vendoring breaks without this) +dpi = { git = "https://github.com/rust-windowing/winit.git" } diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index a73c3c2b71..a45b091ac4 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -66,6 +66,7 @@ windows = { version = "0.58.0", features = [ # macOS-specific dependencies [target.'cfg(target_os = "macos")'.dependencies] +muda = { git = "https://github.com/tauri-apps/muda.git", rev = "3f460b8fbaed59cda6d95ceea6904f000f093f15", default-features = false } metal = { version = "0.31.0", optional = true } objc = { version = "0.2", optional = true } core-foundation = { version = "0.10", optional = true } diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 0ad28d6c34..b16c6a326b 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -8,6 +8,7 @@ use std::thread; use std::time::Duration; use std::time::Instant; use winit::application::ApplicationHandler; +use winit::dpi::PhysicalSize; use winit::event::WindowEvent; use winit::event_loop::ActiveEventLoop; use winit::event_loop::ControlFlow; @@ -19,14 +20,15 @@ use crate::event::{AppEvent, AppEventScheduler}; use crate::persist::PersistentData; use crate::render::GraphicsState; use crate::window::Window; -use graphite_desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform}; -use graphite_desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; +use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform}; +use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; pub(crate) struct App { cef_context: Box, window: Option, + window_scale: f64, cef_schedule: Option, - cef_window_size_sender: Sender, + cef_view_info_sender: Sender, graphics_state: Option, wgpu_context: WgpuContext, app_event_receiver: Receiver, @@ -44,7 +46,7 @@ pub(crate) struct App { impl App { pub(crate) fn new( cef_context: Box, - window_size_sender: Sender, + cef_view_info_sender: Sender, wgpu_context: WgpuContext, app_event_receiver: Receiver, app_event_scheduler: AppEventScheduler, @@ -66,9 +68,10 @@ impl App { Self { cef_context, window: None, + window_scale: 1.0, cef_schedule: Some(Instant::now()), graphics_state: None, - cef_window_size_sender: window_size_sender, + cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, @@ -258,6 +261,11 @@ impl App { } }); } + DesktopFrontendMessage::UpdateMenu { entries } => { + if let Some(window) = &self.window { + window.update_menu(entries); + } + } } } @@ -338,20 +346,26 @@ impl App { tracing::info!("Exiting main event loop"); event_loop.exit(); } + AppEvent::MenuEvent { id } => { + self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id }); + } } } } impl ApplicationHandler for App { fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { - let window = Window::new(event_loop); + let window = Window::new(event_loop, self.app_event_scheduler.clone()); + + self.window_scale = window.scale_factor(); + let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Scale(self.window_scale)); + self.cef_context.notify_view_info_changed(); + self.window = Some(window); let graphics_state = GraphicsState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone()); self.graphics_state = Some(graphics_state); - tracing::info!("Winit window created and ready"); - self.desktop_wrapper.init(self.wgpu_context.clone()); #[cfg(target_os = "windows")] @@ -370,20 +384,28 @@ impl ApplicationHandler for App { } fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _window_id: WindowId, event: WindowEvent) { - self.cef_context.handle_window_event(&event); + self.cef_context.handle_window_event(&event, self.window_scale); match event { WindowEvent::CloseRequested => { self.app_event_scheduler.schedule(AppEvent::CloseWindow); } - WindowEvent::SurfaceResized(size) => { - let _ = self.cef_window_size_sender.send(size.into()); - self.cef_context.notify_of_resize(); + WindowEvent::SurfaceResized(PhysicalSize { width, height }) => { + let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Size { + width: width as usize, + height: height as usize, + }); + self.cef_context.notify_view_info_changed(); if let Some(window) = &self.window { let maximized = window.is_maximized(); self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::UpdateMaximized { maximized })); } } + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + self.window_scale = scale_factor; + let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Scale(self.window_scale)); + self.cef_context.notify_view_info_changed(); + } WindowEvent::RedrawRequested => { let Some(ref mut graphics_state) = self.graphics_state else { return }; // Only rerender once we have a new UI texture to display diff --git a/desktop/src/cef.rs b/desktop/src/cef.rs index aa2a5adbec..a83fbf1b3e 100644 --- a/desktop/src/cef.rs +++ b/desktop/src/cef.rs @@ -14,7 +14,7 @@ use crate::event::{AppEvent, AppEventScheduler}; use crate::render::FrameBufferRef; -use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message}; +use crate::wrapper::{WgpuContext, deserialize_editor_message}; use std::fs::File; use std::io::{Cursor, Read}; use std::path::PathBuf; @@ -39,7 +39,7 @@ use texture_import::SharedTextureHandle; pub(crate) use context::{CefContext, CefContextBuilder, InitError}; pub(crate) trait CefEventHandler: Clone + Send + Sync + 'static { - fn window_size(&self) -> WindowSize; + fn view_info(&self) -> ViewInfo; fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>); #[cfg(feature = "accelerated_paint")] fn draw_gpu(&self, shared_texture: SharedTextureHandle); @@ -53,21 +53,48 @@ pub(crate) trait CefEventHandler: Clone + Send + Sync + 'static { } #[derive(Clone, Copy)] -pub(crate) struct WindowSize { - pub(crate) width: usize, - pub(crate) height: usize, +pub(crate) struct ViewInfo { + width: usize, + height: usize, + scale: f64, } -impl WindowSize { - pub(crate) fn new(width: usize, height: usize) -> Self { - Self { width, height } +impl ViewInfo { + pub(crate) fn new() -> Self { + Self { width: 1, height: 1, scale: 1.0 } + } + pub(crate) fn apply_update(&mut self, update: ViewInfoUpdate) { + match update { + ViewInfoUpdate::Size { width, height } if width> 0 && height> 0 => { + self.width = width; + self.height = height; + } + ViewInfoUpdate::Scale(scale) if scale> 0.0 => { + self.scale = scale; + } + _ => {} + } + } + pub(crate) fn scale(&self) -> f64 { + self.scale + } + pub(crate) fn scaled_width(&self) -> usize { + (self.width as f64 / self.scale).round() as usize + } + pub(crate) fn scaled_height(&self) -> usize { + (self.height as f64 / self.scale).round() as usize } } -impl From> for WindowSize { - fn from(size: winit::dpi::PhysicalSize) -> Self { - Self::new(size.width as usize, size.height as usize) +impl Default for ViewInfo { + fn default() -> Self { + Self::new() } } +pub(crate) enum ViewInfoUpdate { + Size { width: usize, height: usize }, + Scale(f64), +} + #[derive(Clone)] pub(crate) struct Resource { pub(crate) reader: ResourceReader, @@ -93,30 +120,30 @@ impl Read for ResourceReader { pub(crate) struct CefHandler { wgpu_context: WgpuContext, app_event_scheduler: AppEventScheduler, - window_size_receiver: Arc>, + view_info_receiver: Arc>, } impl CefHandler { - pub(crate) fn new(wgpu_context: WgpuContext, app_event_scheduler: AppEventScheduler, window_size_receiver: Receiver) -> Self { + pub(crate) fn new(wgpu_context: WgpuContext, app_event_scheduler: AppEventScheduler, view_info_receiver: Receiver) -> Self { Self { wgpu_context, app_event_scheduler, - window_size_receiver: Arc::new(Mutex::new(WindowSizeReceiver::new(window_size_receiver))), + view_info_receiver: Arc::new(Mutex::new(ViewInfoReceiver::new(view_info_receiver))), } } } impl CefEventHandler for CefHandler { - fn window_size(&self) -> WindowSize { - let Ok(mut guard) = self.window_size_receiver.lock() else { - tracing::error!("Failed to lock window_size_receiver"); - return WindowSize::new(1, 1); + fn view_info(&self) -> ViewInfo { + let Ok(mut guard) = self.view_info_receiver.lock() else { + tracing::error!("Failed to lock view_info_receiver"); + return ViewInfo::new(); }; - let WindowSizeReceiver { receiver, window_size } = &mut *guard; - for new_window_size in receiver.try_iter() { - *window_size = new_window_size; + let ViewInfoReceiver { receiver, view_info } = &mut *guard; + for update in receiver.try_iter() { + view_info.apply_update(update); } - *window_size + *view_info } fn draw<'a>(&self, frame_buffer: FrameBufferRef<'a>) { let width = frame_buffer.width() as u32; @@ -246,15 +273,12 @@ impl CefEventHandler for CefHandler { } } -struct WindowSizeReceiver { - window_size: WindowSize, - receiver: Receiver, +struct ViewInfoReceiver { + view_info: ViewInfo, + receiver: Receiver, } -impl WindowSizeReceiver { - fn new(window_size_receiver: Receiver) -> Self { - Self { - window_size: WindowSize { width: 1, height: 1 }, - receiver: window_size_receiver, - } +impl ViewInfoReceiver { + fn new(receiver: Receiver) -> Self { + Self { view_info: ViewInfo::new(), receiver } } } diff --git a/desktop/src/cef/context.rs b/desktop/src/cef/context.rs index 1f6279c388..21c07a99eb 100644 --- a/desktop/src/cef/context.rs +++ b/desktop/src/cef/context.rs @@ -8,9 +8,9 @@ pub(crate) use builder::{CefContextBuilder, InitError}; pub(crate) trait CefContext { fn work(&mut self); - fn handle_window_event(&mut self, event: &winit::event::WindowEvent); + fn handle_window_event(&mut self, event: &winit::event::WindowEvent, scale: f64); - fn notify_of_resize(&self); + fn notify_view_info_changed(&self); fn send_web_message(&self, message: Vec); } diff --git a/desktop/src/cef/context/multithreaded.rs b/desktop/src/cef/context/multithreaded.rs index 9434a99d86..f871d85967 100644 --- a/desktop/src/cef/context/multithreaded.rs +++ b/desktop/src/cef/context/multithreaded.rs @@ -19,22 +19,22 @@ impl CefContext for MultiThreadedCefContextProxy { // CEF handles its own message loop in multi-threaded mode } - fn handle_window_event(&mut self, event: &WindowEvent) { + fn handle_window_event(&mut self, event: &WindowEvent, scale: f64) { let event_clone = event.clone(); run_on_ui_thread(move || { CONTEXT.with(|b| { if let Some(context) = b.borrow_mut().as_mut() { - context.handle_window_event(&event_clone); + context.handle_window_event(&event_clone, scale); } }); }); } - fn notify_of_resize(&self) { + fn notify_view_info_changed(&self) { run_on_ui_thread(move || { CONTEXT.with(|b| { if let Some(context) = b.borrow_mut().as_mut() { - context.notify_of_resize(); + context.notify_view_info_changed(); } }); }); diff --git a/desktop/src/cef/context/singlethreaded.rs b/desktop/src/cef/context/singlethreaded.rs index 2825b78418..9d9d434701 100644 --- a/desktop/src/cef/context/singlethreaded.rs +++ b/desktop/src/cef/context/singlethreaded.rs @@ -1,8 +1,9 @@ -use cef::{Browser, ImplBrowser, ImplBrowserHost}; +use cef::{Browser, ImplBrowser}; use winit::event::WindowEvent; use crate::cef::input; use crate::cef::input::InputState; +use crate::cef::internal::NotifyViewInfoChanged; use crate::cef::ipc::{MessageType, SendMessage}; use super::CefContext; @@ -18,12 +19,12 @@ impl CefContext for SingleThreadedCefContext { cef::do_message_loop_work(); } - fn handle_window_event(&mut self, event: &WindowEvent) { - input::handle_window_event(&self.browser, &mut self.input_state, event) + fn handle_window_event(&mut self, event: &WindowEvent, scale: f64) { + input::handle_window_event(&self.browser, &mut self.input_state, event, scale) } - fn notify_of_resize(&self) { - self.browser.host().unwrap().was_resized(); + fn notify_view_info_changed(&self) { + self.browser.host().unwrap().notify_view_info_changed(); } fn send_web_message(&self, message: Vec) { diff --git a/desktop/src/cef/input.rs b/desktop/src/cef/input.rs index feb9eff563..c53a405263 100644 --- a/desktop/src/cef/input.rs +++ b/desktop/src/cef/input.rs @@ -1,7 +1,7 @@ use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t}; use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, KeyEventType, MouseEvent}; use std::time::Instant; -use winit::dpi::PhysicalPosition; +use winit::dpi::LogicalPosition; use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent}; mod keymap; @@ -9,17 +9,17 @@ use keymap::{ToNativeKeycode, ToVKBits}; use super::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT, PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y}; -pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputState, event: &WindowEvent) { +pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputState, event: &WindowEvent, scale: f64) { match event { WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerEntered { position, .. } => { - input_state.cursor_move(position); + input_state.cursor_move(&position.to_logical(scale)); let Some(host) = browser.host() else { return }; host.send_mouse_move_event(Some(&input_state.into()), 0); } WindowEvent::PointerLeft { position, .. } => { if let Some(position) = position { - input_state.cursor_move(position); + input_state.cursor_move(&position.to_logical(scale)); } let Some(host) = browser.host() else { return }; @@ -159,7 +159,7 @@ impl InputState { self.modifiers = *modifiers; } - fn cursor_move(&mut self, position: &PhysicalPosition) { + fn cursor_move(&mut self, position: &LogicalPosition) { self.mouse_position = position.into(); } @@ -206,8 +206,8 @@ pub(crate) struct MousePosition { x: usize, y: usize, } -impl From<&physicalposition> for MousePosition { - fn from(position: &PhysicalPosition) -> Self { +impl From<&logicalposition> for MousePosition { + fn from(position: &LogicalPosition) -> Self { Self { x: position.x as usize, y: position.y as usize, diff --git a/desktop/src/cef/internal.rs b/desktop/src/cef/internal.rs index 1a9c8645a3..c6e44749b8 100644 --- a/desktop/src/cef/internal.rs +++ b/desktop/src/cef/internal.rs @@ -22,3 +22,13 @@ pub(super) use browser_process_client::BrowserProcessClientImpl; pub(super) use render_handler::RenderHandlerImpl; pub(super) use render_process_app::RenderProcessAppImpl; pub(super) use scheme_handler_factory::SchemeHandlerFactoryImpl; + +pub(super) trait NotifyViewInfoChanged { + fn notify_view_info_changed(&self); +} +impl NotifyViewInfoChanged for T { + fn notify_view_info_changed(&self) { + self.notify_screen_info_changed(); + self.was_resized(); + } +} diff --git a/desktop/src/cef/internal/render_handler.rs b/desktop/src/cef/internal/render_handler.rs index dd725a01b9..d7fa115b4d 100644 --- a/desktop/src/cef/internal/render_handler.rs +++ b/desktop/src/cef/internal/render_handler.rs @@ -21,16 +21,36 @@ impl RenderHandlerImpl { impl ImplRenderHandler for RenderHandlerImpl { fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) { if let Some(rect) = rect { - let view = self.event_handler.window_size(); + let view_info = self.event_handler.view_info(); *rect = Rect { x: 0, y: 0, - width: view.width as i32, - height: view.height as i32, + width: view_info.scaled_width() as i32, + height: view_info.scaled_height() as i32, }; } } + fn screen_info(&self, _browser: Option<&mut Browser>, screen_info: Option<&mut cef::ScreenInfo>) -> std::ffi::c_int { + if let Some(screen_info) = screen_info { + let view_info = self.event_handler.view_info(); + + screen_info.device_scale_factor = view_info.scale() as f32; + + let rect = Rect { + x: 0, + y: 0, + width: view_info.scaled_width() as i32, + height: view_info.scaled_height() as i32, + }; + screen_info.rect = rect.clone(); + screen_info.available_rect = rect; + + return 1; + } + 0 + } + fn on_paint( &self, _browser: Option<&mut Browser>, diff --git a/desktop/src/event.rs b/desktop/src/event.rs index b6809f5118..e4eaba58c4 100644 --- a/desktop/src/event.rs +++ b/desktop/src/event.rs @@ -1,5 +1,5 @@ -use graphite_desktop_wrapper::NodeGraphExecutionResult; -use graphite_desktop_wrapper::messages::DesktopWrapperMessage; +use crate::wrapper::NodeGraphExecutionResult; +use crate::wrapper::messages::DesktopWrapperMessage; pub(crate) enum AppEvent { UiUpdate(wgpu::Texture), @@ -9,6 +9,7 @@ pub(crate) enum AppEvent { DesktopWrapperMessage(DesktopWrapperMessage), NodeGraphExecutionResult(NodeGraphExecutionResult), CloseWindow, + MenuEvent { id: u64 }, } #[derive(Clone)] diff --git a/desktop/src/gpu_context.rs b/desktop/src/gpu_context.rs index 787d6cda2f..9b25f30fd9 100644 --- a/desktop/src/gpu_context.rs +++ b/desktop/src/gpu_context.rs @@ -1,4 +1,4 @@ -use graphite_desktop_wrapper::{WgpuContext, WgpuContextBuilder, WgpuFeatures}; +use crate::wrapper::{WgpuContext, WgpuContextBuilder, WgpuFeatures}; pub(super) async fn create_wgpu_context() -> WgpuContext { let wgpu_context_builder = WgpuContextBuilder::new().with_features(WgpuFeatures::PUSH_CONSTANTS); diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 9f1591675b..fdf504d04d 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -16,6 +16,8 @@ mod window; mod gpu_context; +pub(crate) use graphite_desktop_wrapper as wrapper; + use app::App; use cef::CefHandler; use cli::Cli; @@ -42,9 +44,9 @@ pub fn start() { let (app_event_sender, app_event_receiver) = std::sync::mpsc::channel(); let app_event_scheduler = event_loop.create_app_event_scheduler(app_event_sender); - let (window_size_sender, window_size_receiver) = std::sync::mpsc::channel(); + let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel(); - let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), window_size_receiver); + let cef_handler = cef::CefHandler::new(wgpu_context.clone(), app_event_scheduler.clone(), cef_view_info_receiver); let cef_context = match cef_context_builder.initialize(cef_handler, cli.disable_ui_acceleration) { Ok(c) => { tracing::info!("CEF initialized successfully"); @@ -68,7 +70,7 @@ pub fn start() { } }; - let mut app = App::new(Box::new(cef_context), window_size_sender, wgpu_context, app_event_receiver, app_event_scheduler, cli.files); + let mut app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, cli.files); event_loop.run_app(&mut app).unwrap(); } diff --git a/desktop/src/persist.rs b/desktop/src/persist.rs index e959112ffa..003bb83f83 100644 --- a/desktop/src/persist.rs +++ b/desktop/src/persist.rs @@ -1,4 +1,4 @@ -use graphite_desktop_wrapper::messages::{Document, DocumentId, Preferences}; +use crate::wrapper::messages::{Document, DocumentId, Preferences}; #[derive(Default, serde::Serialize, serde::Deserialize)] pub(crate) struct PersistentData { diff --git a/desktop/src/render/graphics_state.rs b/desktop/src/render/graphics_state.rs index d6797f937e..afe912053c 100644 --- a/desktop/src/render/graphics_state.rs +++ b/desktop/src/render/graphics_state.rs @@ -1,6 +1,6 @@ use crate::window::Window; -use graphite_desktop_wrapper::{Color, WgpuContext, WgpuExecutor}; +use crate::wrapper::{Color, WgpuContext, WgpuExecutor}; #[derive(derivative::Derivative)] #[derivative(Debug)] diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 8445e55f2a..e9994d23ca 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -3,10 +3,13 @@ use winit::event_loop::ActiveEventLoop; use winit::window::{Window as WinitWindow, WindowAttributes}; use crate::consts::APP_NAME; +use crate::event::AppEventScheduler; +use crate::wrapper::messages::MenuItem; pub(crate) trait NativeWindow { fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes; - fn new(window: &dyn WinitWindow) -> Self; + fn new(window: &dyn WinitWindow, app_event_scheduler: AppEventScheduler) -> Self; + fn update_menu(&self, _entries: Vec) {} } #[cfg(target_os = "linux")] @@ -31,7 +34,7 @@ pub(crate) struct Window { } impl Window { - pub(crate) fn new(event_loop: &dyn ActiveEventLoop) -> Self { + pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self { let mut attributes = WindowAttributes::default() .with_title(APP_NAME) .with_min_surface_size(winit::dpi::LogicalSize::new(400, 300)) @@ -42,7 +45,7 @@ impl Window { attributes = native::NativeWindowImpl::configure(attributes, event_loop); let winit_window = event_loop.create_window(attributes).unwrap(); - let native_handle = native::NativeWindowImpl::new(winit_window.as_ref()); + let native_handle = native::NativeWindowImpl::new(winit_window.as_ref(), app_event_scheduler); Self { winit_window: winit_window.into(), native_handle, @@ -65,6 +68,10 @@ impl Window { self.winit_window.surface_size() } + pub(crate) fn scale_factor(&self) -> f64 { + self.winit_window.scale_factor() + } + pub(crate) fn minimize(&self) { self.winit_window.set_minimized(true); } @@ -84,4 +91,8 @@ impl Window { pub(crate) fn set_cursor(&self, cursor: winit::cursor::Cursor) { self.winit_window.set_cursor(cursor); } + + pub(crate) fn update_menu(&self, entries: Vec) { + self.native_handle.update_menu(entries); + } } diff --git a/desktop/src/window/linux.rs b/desktop/src/window/linux.rs index 5a48385acc..f17928722e 100644 --- a/desktop/src/window/linux.rs +++ b/desktop/src/window/linux.rs @@ -5,12 +5,11 @@ use winit::platform::x11::WindowAttributesX11; use winit::window::{Window, WindowAttributes}; use crate::consts::{APP_ID, APP_NAME}; - -use super::NativeWindow; +use crate::event::AppEventScheduler; pub(super) struct NativeWindowImpl {} -impl NativeWindow for NativeWindowImpl { +impl super::NativeWindow for NativeWindowImpl { fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes { if event_loop.is_wayland() { let wayland_attributes = WindowAttributesWayland::default().with_name(APP_ID, "").with_prefer_csd(true); @@ -21,7 +20,7 @@ impl NativeWindow for NativeWindowImpl { } } - fn new(_window: &dyn Window) -> Self { + fn new(_window: &dyn Window, _app_event_scheduler: AppEventScheduler) -> Self { NativeWindowImpl {} } } diff --git a/desktop/src/window/mac.rs b/desktop/src/window/mac.rs index 83386909b5..4c4010a8b1 100644 --- a/desktop/src/window/mac.rs +++ b/desktop/src/window/mac.rs @@ -1,11 +1,15 @@ use winit::event_loop::ActiveEventLoop; use winit::window::{Window, WindowAttributes}; -use super::NativeWindow; +use crate::consts::APP_NAME; +use crate::event::AppEventScheduler; +use crate::wrapper::messages::MenuItem; -pub(super) struct NativeWindowImpl {} +pub(super) struct NativeWindowImpl { + menu: menu::Menu, +} -impl NativeWindow for NativeWindowImpl { +impl super::NativeWindow for NativeWindowImpl { fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes { let mac_window = winit::platform::macos::WindowAttributesMacOS::default() .with_titlebar_transparent(true) @@ -14,7 +18,15 @@ impl NativeWindow for NativeWindowImpl { attributes.with_platform_attributes(Box::new(mac_window)) } - fn new(_window: &dyn Window) -> Self { - NativeWindowImpl {} + fn new(_window: &dyn Window, app_event_scheduler: AppEventScheduler) -> Self { + let menu = menu::Menu::new(app_event_scheduler, APP_NAME); + + NativeWindowImpl { menu } + } + + fn update_menu(&self, entries: Vec) { + self.menu.update(entries); } } + +mod menu; diff --git a/desktop/src/window/mac/menu.rs b/desktop/src/window/mac/menu.rs new file mode 100644 index 0000000000..cc4ca07327 --- /dev/null +++ b/desktop/src/window/mac/menu.rs @@ -0,0 +1,99 @@ +use muda::Menu as MudaMenu; +use muda::accelerator::Accelerator; +use muda::{AboutMetadataBuilder, CheckMenuItem, IsMenuItem, MenuEvent, MenuId, MenuItem, MenuItemKind, PredefinedMenuItem, Submenu}; + +use crate::event::{AppEvent, AppEventScheduler}; +use crate::wrapper::messages::MenuItem as WrapperMenuItem; + +pub(super) struct Menu { + inner: MudaMenu, +} + +impl Menu { + pub(super) fn new(event_scheduler: AppEventScheduler, app_name: &str) -> Self { + let about = PredefinedMenuItem::about(None, Some(AboutMetadataBuilder::new().name(Some(app_name)).build())); + let hide = PredefinedMenuItem::hide(None); + let hide_others = PredefinedMenuItem::hide_others(None); + let show_all = PredefinedMenuItem::show_all(None); + let quit = PredefinedMenuItem::quit(None); + let app_submenu = Submenu::with_items( + "", + true, + &[&about, &PredefinedMenuItem::separator(), &hide, &hide_others, &show_all, &PredefinedMenuItem::separator(), &quit], + ) + .unwrap(); + + let menu = MudaMenu::new(); + menu.prepend(&app_submenu).unwrap(); + + menu.init_for_nsapp(); + + MenuEvent::set_event_handler(Some(move |event: MenuEvent| { + if let Some(id) = menu_id_to_u64(event.id()) { + event_scheduler.schedule(AppEvent::MenuEvent { id }); + } + })); + + Menu { inner: menu } + } + + pub(super) fn update(&self, entries: Vec) { + // remove all items except the first (app menu) + self.inner.items().iter().skip(1).for_each(|item: &muda::MenuItemKind| { + self.inner.remove(menu_item_kind_to_dyn(item)).unwrap(); + }); + + let items = menu_items_from_wrapper(entries); + let items = items.iter().map(|item| menu_item_kind_to_dyn(item)).collect::>(); + self.inner.append_items(items.as_ref()).unwrap(); + } +} + +fn menu_items_from_wrapper(entries: Vec) -> Vec { + let mut menu_items: Vec = Vec::new(); + for entry in entries { + match entry { + WrapperMenuItem::Action { id, text, enabled, shortcut } => { + let id = u64_to_menu_id(id); + let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key)); + let item = MenuItem::with_id(id, text, enabled, accelerator); + menu_items.push(MenuItemKind::MenuItem(item)); + } + WrapperMenuItem::Checkbox { id, text, enabled, shortcut, checked } => { + let id = u64_to_menu_id(id); + let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key)); + let check = CheckMenuItem::with_id(id, text, enabled, checked, accelerator); + menu_items.push(MenuItemKind::Check(check)); + } + WrapperMenuItem::SubMenu { text: name, items, .. } => { + let items = menu_items_from_wrapper(items); + let items = items.iter().map(|item| menu_item_kind_to_dyn(item)).collect::>(); + let submenu = Submenu::with_items(name, true, &items).unwrap(); + menu_items.push(MenuItemKind::Submenu(submenu)); + } + WrapperMenuItem::Separator => { + let separator = PredefinedMenuItem::separator(); + menu_items.push(MenuItemKind::Predefined(separator)); + } + } + } + menu_items +} + +fn menu_item_kind_to_dyn(item: &MenuItemKind) -> &dyn IsMenuItem { + match item { + MenuItemKind::MenuItem(i) => i, + MenuItemKind::Submenu(i) => i, + MenuItemKind::Predefined(i) => i, + MenuItemKind::Check(i) => i, + MenuItemKind::Icon(i) => i, + } +} + +fn u64_to_menu_id(id: u64) -> String { + format!("{id:08x}") +} + +fn menu_id_to_u64(id: &MenuId) -> Option { + u64::from_str_radix(&id.0, 16).ok() +} diff --git a/desktop/src/window/win.rs b/desktop/src/window/win.rs index 8f23866f08..24ee0ef452 100644 --- a/desktop/src/window/win.rs +++ b/desktop/src/window/win.rs @@ -1,13 +1,13 @@ use winit::event_loop::ActiveEventLoop; use winit::window::{Window, WindowAttributes}; -use super::NativeWindow; +use crate::event::AppEventScheduler; pub(super) struct NativeWindowImpl { native_handle: native_handle::NativeWindowHandle, } -impl NativeWindow for NativeWindowImpl { +impl super::NativeWindow for NativeWindowImpl { fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes { if let Ok(win_icon) = winit::platform::windows::WinIcon::from_resource(1, None) { let icon = winit::icon::Icon(std::sync::Arc::new(win_icon)); @@ -17,7 +17,7 @@ impl NativeWindow for NativeWindowImpl { } } - fn new(window: &dyn Window) -> Self { + fn new(window: &dyn Window, _app_event_scheduler: AppEventScheduler) -> Self { let native_handle = native_handle::NativeWindowHandle::new(window); NativeWindowImpl { native_handle } } diff --git a/desktop/wrapper/Cargo.toml b/desktop/wrapper/Cargo.toml index 53302baadb..7c11fa7c68 100644 --- a/desktop/wrapper/Cargo.toml +++ b/desktop/wrapper/Cargo.toml @@ -31,3 +31,5 @@ ron = { workspace = true} vello = { workspace = true } image = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } +keyboard-types = { workspace = true } diff --git a/desktop/wrapper/src/handle_desktop_wrapper_message.rs b/desktop/wrapper/src/handle_desktop_wrapper_message.rs index a833a5cdeb..09f279faa7 100644 --- a/desktop/wrapper/src/handle_desktop_wrapper_message.rs +++ b/desktop/wrapper/src/handle_desktop_wrapper_message.rs @@ -1,7 +1,9 @@ use graphene_std::Color; use graphene_std::raster::Image; use graphite_editor::messages::app_window::app_window_message_handler::AppWindowPlatform; +use graphite_editor::messages::layout::LayoutMessage; use graphite_editor::messages::prelude::{AppWindowMessage, DocumentMessage, FrontendMessage, PortfolioMessage, PreferencesMessage}; +use graphite_editor::messages::tool::tool_messages::tool_prelude::{LayoutTarget, WidgetId}; use crate::messages::Platform; @@ -148,5 +150,13 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess let message = PreferencesMessage::Load { preferences }; dispatcher.queue_editor_message(message.into()); } + DesktopWrapperMessage::MenuEvent { id } => { + let message = LayoutMessage::WidgetValueUpdate { + layout_target: LayoutTarget::MenuBar, + widget_id: WidgetId(id), + value: serde_json::Value::Bool(true), + }; + dispatcher.queue_editor_message(message.into()); + } } } diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index 6077626800..b359980aa1 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -1,9 +1,12 @@ use std::path::PathBuf; +use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, LayoutKey, LayoutKeysGroup}; +use graphite_editor::messages::input_mapper::utility_types::misc::ActionKeys; +use graphite_editor::messages::layout::utility_types::widgets::menu_widgets::MenuBarEntry; use graphite_editor::messages::prelude::FrontendMessage; use super::DesktopWrapperMessageDispatcher; -use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; +use super::messages::{DesktopFrontendMessage, Document, FileFilter, KeyCode, MenuItem, Modifiers, OpenFileDialogContext, SaveFileDialogContext, Shortcut}; pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option { match message { @@ -119,6 +122,206 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD FrontendMessage::TriggerLoadPreferences => { dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences); } + FrontendMessage::UpdateMenuBarLayout { layout_target, layout } => { + fn shortcut_from_layout_keys(layout_keys: &Vec) -> Option { + let mut key: Option = None; + let mut modifiers = Modifiers::default(); + for layout_key in layout_keys { + match layout_key.key { + Key::Shift => modifiers |= Modifiers::SHIFT, + Key::Control => modifiers |= Modifiers::CONTROL, + Key::Alt => modifiers |= Modifiers::ALT, + Key::Meta => modifiers |= Modifiers::META, + Key::Command => modifiers |= Modifiers::ALT, + Key::Accel => modifiers |= Modifiers::META, + Key::Digit0 => key = Some(KeyCode::Digit0), + Key::Digit1 => key = Some(KeyCode::Digit1), + Key::Digit2 => key = Some(KeyCode::Digit2), + Key::Digit3 => key = Some(KeyCode::Digit3), + Key::Digit4 => key = Some(KeyCode::Digit4), + Key::Digit5 => key = Some(KeyCode::Digit5), + Key::Digit6 => key = Some(KeyCode::Digit6), + Key::Digit7 => key = Some(KeyCode::Digit7), + Key::Digit8 => key = Some(KeyCode::Digit8), + Key::Digit9 => key = Some(KeyCode::Digit9), + Key::KeyA => key = Some(KeyCode::KeyA), + Key::KeyB => key = Some(KeyCode::KeyB), + Key::KeyC => key = Some(KeyCode::KeyC), + Key::KeyD => key = Some(KeyCode::KeyD), + Key::KeyE => key = Some(KeyCode::KeyE), + Key::KeyF => key = Some(KeyCode::KeyF), + Key::KeyG => key = Some(KeyCode::KeyG), + Key::KeyH => key = Some(KeyCode::KeyH), + Key::KeyI => key = Some(KeyCode::KeyI), + Key::KeyJ => key = Some(KeyCode::KeyJ), + Key::KeyK => key = Some(KeyCode::KeyK), + Key::KeyL => key = Some(KeyCode::KeyL), + Key::KeyM => key = Some(KeyCode::KeyM), + Key::KeyN => key = Some(KeyCode::KeyN), + Key::KeyO => key = Some(KeyCode::KeyO), + Key::KeyP => key = Some(KeyCode::KeyP), + Key::KeyQ => key = Some(KeyCode::KeyQ), + Key::KeyR => key = Some(KeyCode::KeyR), + Key::KeyS => key = Some(KeyCode::KeyS), + Key::KeyT => key = Some(KeyCode::KeyT), + Key::KeyU => key = Some(KeyCode::KeyU), + Key::KeyV => key = Some(KeyCode::KeyV), + Key::KeyW => key = Some(KeyCode::KeyW), + Key::KeyX => key = Some(KeyCode::KeyX), + Key::KeyY => key = Some(KeyCode::KeyY), + Key::KeyZ => key = Some(KeyCode::KeyZ), + Key::Backquote => key = Some(KeyCode::Backquote), + Key::Backslash => key = Some(KeyCode::Backslash), + Key::BracketLeft => key = Some(KeyCode::BracketLeft), + Key::BracketRight => key = Some(KeyCode::BracketRight), + Key::Comma => key = Some(KeyCode::Comma), + Key::Equal => key = Some(KeyCode::Equal), + Key::Minus => key = Some(KeyCode::Minus), + Key::Period => key = Some(KeyCode::Period), + Key::Quote => key = Some(KeyCode::Quote), + Key::Semicolon => key = Some(KeyCode::Semicolon), + Key::Slash => key = Some(KeyCode::Slash), + Key::Backspace => key = Some(KeyCode::Backspace), + Key::CapsLock => key = Some(KeyCode::CapsLock), + Key::ContextMenu => key = Some(KeyCode::ContextMenu), + Key::Enter => key = Some(KeyCode::Enter), + Key::Space => key = Some(KeyCode::Space), + Key::Tab => key = Some(KeyCode::Tab), + Key::Delete => key = Some(KeyCode::Delete), + Key::End => key = Some(KeyCode::End), + Key::Help => key = Some(KeyCode::Help), + Key::Home => key = Some(KeyCode::Home), + Key::Insert => key = Some(KeyCode::Insert), + Key::PageDown => key = Some(KeyCode::PageDown), + Key::PageUp => key = Some(KeyCode::PageUp), + Key::ArrowDown => key = Some(KeyCode::ArrowDown), + Key::ArrowLeft => key = Some(KeyCode::ArrowLeft), + Key::ArrowRight => key = Some(KeyCode::ArrowRight), + Key::ArrowUp => key = Some(KeyCode::ArrowUp), + Key::NumLock => key = Some(KeyCode::NumLock), + Key::NumpadAdd => key = Some(KeyCode::NumpadAdd), + Key::NumpadHash => key = Some(KeyCode::NumpadHash), + Key::NumpadMultiply => key = Some(KeyCode::NumpadMultiply), + Key::NumpadParenLeft => key = Some(KeyCode::NumpadParenLeft), + Key::NumpadParenRight => key = Some(KeyCode::NumpadParenRight), + Key::Escape => key = Some(KeyCode::Escape), + Key::F1 => key = Some(KeyCode::F1), + Key::F2 => key = Some(KeyCode::F2), + Key::F3 => key = Some(KeyCode::F3), + Key::F4 => key = Some(KeyCode::F4), + Key::F5 => key = Some(KeyCode::F5), + Key::F6 => key = Some(KeyCode::F6), + Key::F7 => key = Some(KeyCode::F7), + Key::F8 => key = Some(KeyCode::F8), + Key::F9 => key = Some(KeyCode::F9), + Key::F10 => key = Some(KeyCode::F10), + Key::F11 => key = Some(KeyCode::F11), + Key::F12 => key = Some(KeyCode::F12), + Key::F13 => key = Some(KeyCode::F13), + Key::F14 => key = Some(KeyCode::F14), + Key::F15 => key = Some(KeyCode::F15), + Key::F16 => key = Some(KeyCode::F16), + Key::F17 => key = Some(KeyCode::F17), + Key::F18 => key = Some(KeyCode::F18), + Key::F19 => key = Some(KeyCode::F19), + Key::F20 => key = Some(KeyCode::F20), + Key::F21 => key = Some(KeyCode::F21), + Key::F22 => key = Some(KeyCode::F22), + Key::F23 => key = Some(KeyCode::F23), + Key::F24 => key = Some(KeyCode::F24), + Key::Fn => key = Some(KeyCode::Fn), + Key::FnLock => key = Some(KeyCode::FnLock), + Key::PrintScreen => key = Some(KeyCode::PrintScreen), + Key::ScrollLock => key = Some(KeyCode::ScrollLock), + Key::Pause => key = Some(KeyCode::Pause), + Key::Unidentified => key = Some(KeyCode::Unidentified), + _ => key = None, + } + } + if let Some(key) = key { Some(Shortcut { key, modifiers }) } else { None } + } + + fn create_menu_item( + MenuBarEntry { + label, + icon, + shortcut, + action, + children, + disabled, + }: &MenuBarEntry, + ) -> Option { + let id = action.widget_id.0; + let text = if label.is_empty() { + return None; + } else { + label.clone() + }; + let enabled = !*disabled; + + if !children.0.is_empty() { + let items = items_from_children(&children.0); + return Some(MenuItem::SubMenu { id, text, enabled, items }); + } + + let shortcut = match shortcut { + Some(ActionKeys::Keys(LayoutKeysGroup(keys))) => { + if let Some(shortcut) = shortcut_from_layout_keys(&keys) { + Some(shortcut) + } else { + None + } + } + _ => None, + }; + + // TODO: Find a better way to determine if this is a checkbox + match icon.as_deref() { + Some("CheckboxChecked") => { + return Some(MenuItem::Checkbox { + id, + text, + enabled, + shortcut, + checked: true, + }); + } + Some("CheckboxUnchecked") => { + return Some(MenuItem::Checkbox { + id, + text, + enabled, + shortcut, + checked: false, + }); + } + _ => {} + } + + Some(MenuItem::Action { id, text, shortcut, enabled }) + } + + fn items_from_children(children: &Vec>) -> Vec { + let mut items = Vec::new(); + for (i, section) in children.iter().enumerate() { + for entry in section.iter() { + if let Some(item) = create_menu_item(entry) { + items.push(item); + } + } + if i != children.len() - 1 { + items.push(MenuItem::Separator); + } + } + items + } + + let entries: Vec = layout.iter().filter_map(|entry| create_menu_item(entry)).collect(); + + dispatcher.respond(DesktopFrontendMessage::UpdateMenu { entries }); + + return Some(FrontendMessage::UpdateMenuBarLayout { layout, layout_target }); + } m => return Some(m), } None diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index 33704c2548..a7e1a86edc 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -1,11 +1,10 @@ -pub use graphite_editor::messages::prelude::DocumentId; use graphite_editor::messages::prelude::FrontendMessage; use std::path::PathBuf; pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage; +pub use graphite_editor::messages::prelude::DocumentId; pub use graphite_editor::messages::prelude::PreferencesMessageHandler as Preferences; - pub enum DesktopFrontendMessage { ToWeb(Vec), OpenLaunchDocuments, @@ -56,6 +55,9 @@ pub enum DesktopFrontendMessage { preferences: Preferences, }, PersistenceLoadPreferences, + UpdateMenu { + entries: Vec, + }, } pub enum DesktopWrapperMessage { @@ -106,6 +108,9 @@ pub enum DesktopWrapperMessage { LoadPreferences { preferences: Option, }, + MenuEvent { + id: u64, + }, } #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] @@ -136,3 +141,32 @@ pub enum Platform { Mac, Linux, } + +pub enum MenuItem { + Action { + id: u64, + text: String, + enabled: bool, + shortcut: Option, + }, + Checkbox { + id: u64, + text: String, + enabled: bool, + shortcut: Option, + checked: bool, + }, + SubMenu { + id: u64, + text: String, + enabled: bool, + items: Vec, + }, + Separator, +} + +pub use keyboard_types::{Code as KeyCode, Modifiers}; +pub struct Shortcut { + pub key: KeyCode, + pub modifiers: Modifiers, +} diff --git a/editor/src/messages/input_mapper/utility_types/input_keyboard.rs b/editor/src/messages/input_mapper/utility_types/input_keyboard.rs index 5528d4386a..9d89234768 100644 --- a/editor/src/messages/input_mapper/utility_types/input_keyboard.rs +++ b/editor/src/messages/input_mapper/utility_types/input_keyboard.rs @@ -304,16 +304,13 @@ impl fmt::Display for Key { impl From for LayoutKey { fn from(key: Key) -> Self { - Self { - key: format!("{key:?}"), - label: key.to_string(), - } + Self { key, label: key.to_string() } } } #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type)] -struct LayoutKey { - key: String, +pub struct LayoutKey { + pub key: Key, label: String, } @@ -359,7 +356,7 @@ impl From for String { } #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct LayoutKeysGroup(Vec); +pub struct LayoutKeysGroup(pub Vec); impl From for LayoutKeysGroup { fn from(keys_group: KeysGroup) -> Self { diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index f02372ff11..6efc4fe99a 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -2,7 +2,7 @@ use super::utility_functions::overlay_canvas_context; use crate::consts::{ ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, - PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS, + PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SEGMENT_SELECTED_THICKNESS, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE, }; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::prelude::Message; @@ -153,6 +153,11 @@ impl core::hash::Hash for OverlayContext { } impl OverlayContext { + pub fn reverse_scale(&self, distance: f64) -> f64 { + // No-op when rendering to canvas + distance + } + pub fn quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>) { self.dashed_polygon(&quad.0, stroke_color, color_fill, None, None, None); } @@ -499,6 +504,20 @@ impl OverlayContext { self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE)); } + pub fn resize_handle(&mut self, position: DVec2, rotation: f64) { + let quad = DAffine2::from_angle_translation(rotation, position) * Quad::from_box([DVec2::splat(-RESIZE_HANDLE_SIZE / 2.), DVec2::splat(RESIZE_HANDLE_SIZE / 2.)]); + self.quad(quad, None, Some(COLOR_OVERLAY_WHITE)); + } + + pub fn skew_handles(&mut self, edge_start: DVec2, edge_end: DVec2) { + let edge_dir = (edge_end - edge_start).normalize(); + 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); + } + } + /// Transforms the canvas context to adjust for DPI scaling /// /// Overwrites all existing tranforms. This operation can be reversed with [`Self::reset_transform`]. 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 c188d3b574..48226667c2 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -1,7 +1,7 @@ use crate::consts::{ ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, - PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, + PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE, }; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::prelude::Message; @@ -235,6 +235,10 @@ impl OverlayContext { self.internal.lock().expect("Failed to lock internal overlay context") } + pub fn reverse_scale(&self, distance: f64) -> f64 { + self.internal().reverse_scale(distance) + } + pub fn quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>) { self.internal().quad(quad, stroke_color, color_fill); } @@ -280,6 +284,14 @@ impl OverlayContext { self.internal().manipulator_anchor(position, selected, color); } + pub fn resize_handle(&mut self, position: DVec2, rotation: f64) { + self.internal().resize_handle(position, rotation); + } + + pub fn skew_handles(&mut self, edge_start: DVec2, edge_end: DVec2) { + self.internal().skew_handles(edge_start, edge_end); + } + pub fn square(&mut self, position: DVec2, size: Option, color_fill: Option<&str>, color_stroke: Option<&str>) { self.internal().square(position, size, color_fill, color_stroke); } @@ -423,12 +435,13 @@ pub(super) struct OverlayContextInternal { scene: Scene, size: DVec2, device_pixel_ratio: f64, + scale: f64, visibility_settings: OverlaysVisibilitySettings, } impl Default for OverlayContextInternal { fn default() -> Self { - Self::new(DVec2::new(100., 100.), 1., OverlaysVisibilitySettings::default()) + Self::new(DVec2::new(100., 100.), 1.0, OverlaysVisibilitySettings::default()) } } @@ -437,11 +450,16 @@ impl OverlayContextInternal { Self { scene: Scene::new(), size, - device_pixel_ratio, + device_pixel_ratio: 1.0, + scale: device_pixel_ratio, visibility_settings, } } + fn reverse_scale(&self, distance: f64) -> f64 { + distance / self.scale + } + fn parse_color(color: &str) -> peniko::Color { let hex = color.trim_start_matches('#'); let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0); @@ -473,7 +491,7 @@ impl OverlayContextInternal { self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &path); - self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &path); + self.scene.stroke(&kurbo::Stroke::new(1.0 * self.scale), transform, Self::parse_color(color_stroke), None, &path); } fn dashed_quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { @@ -506,11 +524,11 @@ impl OverlayContextInternal { } let stroke_color = stroke_color.unwrap_or(COLOR_OVERLAY_BLUE); - let mut stroke = kurbo::Stroke::new(1.0); + let mut stroke = kurbo::Stroke::new(1.0 * self.scale); if let Some(dash_width) = dash_width { let dash_gap = dash_gap_width.unwrap_or(1.); - stroke = stroke.with_dashes(dash_offset.unwrap_or(0.), [dash_width, dash_gap]); + stroke = stroke.with_dashes(dash_offset.unwrap_or(0.), [dash_width * self.scale, dash_gap * self.scale]); } self.scene.stroke(&stroke, transform, Self::parse_color(stroke_color), None, &path); @@ -531,11 +549,11 @@ impl OverlayContextInternal { path.move_to(kurbo::Point::new(start.x, start.y)); path.line_to(kurbo::Point::new(end.x, end.y)); - let mut stroke = kurbo::Stroke::new(thickness.unwrap_or(1.)); + let mut stroke = kurbo::Stroke::new(thickness.unwrap_or(1.) * self.scale); if let Some(dash_width) = dash_width { let dash_gap = dash_gap_width.unwrap_or(1.); - stroke = stroke.with_dashes(dash_offset.unwrap_or(0.), [dash_width, dash_gap]); + stroke = stroke.with_dashes(dash_offset.unwrap_or(0.), [dash_width * self.scale, dash_gap * self.scale]); } self.scene.stroke(&stroke, transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &path); @@ -545,13 +563,13 @@ impl OverlayContextInternal { let transform = self.get_transform(); let position = position.round() - DVec2::splat(0.5); - let circle = kurbo::Circle::new((position.x, position.y), MANIPULATOR_GROUP_MARKER_SIZE / 2.); + let circle = kurbo::Circle::new((position.x, position.y), MANIPULATOR_GROUP_MARKER_SIZE / 2. * self.scale); let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle); self.scene - .stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &circle); + .stroke(&kurbo::Stroke::new(1.0 * self.scale), transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &circle); } fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) { @@ -559,17 +577,19 @@ impl OverlayContextInternal { let position = position.round() - DVec2::splat(0.5); - let circle = kurbo::Circle::new((position.x, position.y), (MANIPULATOR_GROUP_MARKER_SIZE + 2.) / 2.); + let circle = kurbo::Circle::new((position.x, position.y), (MANIPULATOR_GROUP_MARKER_SIZE + 2.) / 2. * self.scale); let fill = COLOR_OVERLAY_BLUE_50; self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle); - self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &circle); + self.scene + .stroke(&kurbo::Stroke::new(1.0 * self.scale), transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &circle); - let inner_circle = kurbo::Circle::new((position.x, position.y), MANIPULATOR_GROUP_MARKER_SIZE / 2.); + let inner_circle = kurbo::Circle::new((position.x, position.y), MANIPULATOR_GROUP_MARKER_SIZE / 2. * self.scale); let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &circle); - self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &inner_circle); + self.scene + .stroke(&kurbo::Stroke::new(1.0 * self.scale), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &inner_circle); } fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) { @@ -579,17 +599,36 @@ impl OverlayContextInternal { } fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) { - self.square(position, Some(MANIPULATOR_GROUP_MARKER_SIZE + 2.), Some(COLOR_OVERLAY_BLUE_50), Some(COLOR_OVERLAY_BLUE_50)); + self.square( + position, + Some((MANIPULATOR_GROUP_MARKER_SIZE + 2.) * self.scale), + Some(COLOR_OVERLAY_BLUE_50), + Some(COLOR_OVERLAY_BLUE_50), + ); let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE)); } + fn resize_handle(&mut self, position: DVec2, rotation: f64) { + let quad = DAffine2::from_angle_translation(rotation, position) * Quad::from_box([DVec2::splat(-RESIZE_HANDLE_SIZE / 2. * self.scale), DVec2::splat(RESIZE_HANDLE_SIZE / 2. * self.scale)]); + self.quad(quad, None, Some(COLOR_OVERLAY_WHITE)); + } + + fn skew_handles(&mut self, edge_start: DVec2, edge_end: DVec2) { + let edge_dir = (edge_end - edge_start).normalize(); + let mid = edge_end.midpoint(edge_start); + + for edge in [edge_dir, -edge_dir] { + self.draw_triangle(mid + edge * (3. + SKEW_TRIANGLE_OFFSET) * self.scale, edge, SKEW_TRIANGLE_SIZE * self.scale, None, None); + } + } + fn get_transform(&self) -> kurbo::Affine { kurbo::Affine::scale(self.device_pixel_ratio) } fn square(&mut self, position: DVec2, size: Option, color_fill: Option<&str>, color_stroke: Option<&str>) { - let size = size.unwrap_or(MANIPULATOR_GROUP_MARKER_SIZE); + let size = size.unwrap_or(MANIPULATOR_GROUP_MARKER_SIZE) * self.scale; let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE); let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); @@ -601,11 +640,11 @@ impl OverlayContextInternal { self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &rect); - self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &rect); + self.scene.stroke(&kurbo::Stroke::new(1.0 * self.scale), transform, Self::parse_color(color_stroke), None, &rect); } fn pixel(&mut self, position: DVec2, color: Option<&str>) { - let size = 1.; + let size = 1. * self.scale; let color_fill = color.unwrap_or(COLOR_OVERLAY_WHITE); let position = position.round() - DVec2::splat(0.5); @@ -627,7 +666,7 @@ impl OverlayContextInternal { self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &circle); - self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &circle); + self.scene.stroke(&kurbo::Stroke::new(1.0 * self.scale), transform, Self::parse_color(color_stroke), None, &circle); } fn dashed_ellipse( @@ -678,7 +717,8 @@ impl OverlayContextInternal { ); } - self.scene.stroke(&kurbo::Stroke::new(1.0), self.get_transform(), Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); + self.scene + .stroke(&kurbo::Stroke::new(1.0 * self.scale), self.get_transform(), Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); } fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) { @@ -700,9 +740,9 @@ impl OverlayContextInternal { let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_WHITE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_rgba_hex_srgb(); fill_color.insert(0, '#'); let fill_color = Some(fill_color.as_str()); - self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None, None); - self.circle(start, radius, fill_color, None); - self.circle(start, radius * scale.abs(), fill_color, None); + self.line(start + DVec2::X * radius * sign * self.scale, start + DVec2::X * (radius * scale * self.scale), None, None); + self.circle(start, radius * self.scale, fill_color, None); + self.circle(start, radius * scale.abs() * self.scale, fill_color, None); self.text( text, COLOR_OVERLAY_BLUE, @@ -714,14 +754,14 @@ impl OverlayContextInternal { } fn compass_rose(&mut self, compass_center: DVec2, angle: f64, show_compass_with_hover_ring: Option) { - const HOVER_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_HOVER_RING_DIAMETER / 2.; - const MAIN_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_MAIN_RING_DIAMETER / 2.; - const MAIN_RING_INNER_RADIUS: f64 = COMPASS_ROSE_RING_INNER_DIAMETER / 2.; - const ARROW_RADIUS: f64 = COMPASS_ROSE_ARROW_SIZE / 2.; - const HOVER_RING_STROKE_WIDTH: f64 = HOVER_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS; - const HOVER_RING_CENTERLINE_RADIUS: f64 = (HOVER_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.; - const MAIN_RING_STROKE_WIDTH: f64 = MAIN_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS; - const MAIN_RING_CENTERLINE_RADIUS: f64 = (MAIN_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.; + let hover_ring_outer_radius: f64 = COMPASS_ROSE_HOVER_RING_DIAMETER / 2. * self.scale; + let main_ring_outer_radius: f64 = COMPASS_ROSE_MAIN_RING_DIAMETER / 2. * self.scale; + let main_ring_inner_radius: f64 = COMPASS_ROSE_RING_INNER_DIAMETER / 2. * self.scale; + let arrow_radius: f64 = COMPASS_ROSE_ARROW_SIZE / 2. * self.scale; + let hover_ring_stroke_width: f64 = hover_ring_outer_radius - main_ring_inner_radius; + let hover_ring_centerline_radius: f64 = (hover_ring_outer_radius + main_ring_inner_radius) / 2.; + let main_ring_stroke_width: f64 = main_ring_outer_radius - main_ring_inner_radius; + let main_ring_centerline_radius: f64 = (main_ring_outer_radius + main_ring_inner_radius) / 2.; let Some(show_hover_ring) = show_compass_with_hover_ring else { return }; @@ -733,9 +773,9 @@ impl OverlayContextInternal { let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.5).to_rgba_hex_srgb(); fill_color.insert(0, '#'); - let circle = kurbo::Circle::new((center.x, center.y), HOVER_RING_CENTERLINE_RADIUS); + let circle = kurbo::Circle::new((center.x, center.y), hover_ring_centerline_radius); self.scene - .stroke(&kurbo::Stroke::new(HOVER_RING_STROKE_WIDTH), transform, Self::parse_color(&fill_color), None, &circle); + .stroke(&kurbo::Stroke::new(hover_ring_stroke_width), transform, Self::parse_color(&fill_color), None, &circle); } // Arrows @@ -743,11 +783,11 @@ impl OverlayContextInternal { let direction = DVec2::from_angle(i as f64 * FRAC_PI_2 + angle); let color = if i % 2 == 0 { COLOR_OVERLAY_RED } else { COLOR_OVERLAY_GREEN }; - let tip = center + direction * HOVER_RING_OUTER_RADIUS; - let base = center + direction * (MAIN_RING_INNER_RADIUS + MAIN_RING_OUTER_RADIUS) / 2.; + let tip = center + direction * hover_ring_outer_radius; + let base = center + direction * (main_ring_inner_radius + main_ring_outer_radius) / 2.; - let r = (ARROW_RADIUS.powi(2) + MAIN_RING_INNER_RADIUS.powi(2)).sqrt(); - let (cos, sin) = (MAIN_RING_INNER_RADIUS / r, ARROW_RADIUS / r); + let r = (arrow_radius.powi(2) + main_ring_inner_radius.powi(2)).sqrt(); + let (cos, sin) = (main_ring_inner_radius / r, arrow_radius / r); let side1 = center + r * DVec2::new(cos * direction.x - sin * direction.y, sin * direction.x + direction.y * cos); let side2 = center + r * DVec2::new(cos * direction.x + sin * direction.y, -sin * direction.x + direction.y * cos); @@ -764,9 +804,9 @@ impl OverlayContextInternal { } // Main ring - let circle = kurbo::Circle::new((center.x, center.y), MAIN_RING_CENTERLINE_RADIUS); + let circle = kurbo::Circle::new((center.x, center.y), main_ring_centerline_radius); self.scene - .stroke(&kurbo::Stroke::new(MAIN_RING_STROKE_WIDTH), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &circle); + .stroke(&kurbo::Stroke::new(main_ring_stroke_width), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &circle); } fn pivot(&mut self, position: DVec2, angle: f64) { @@ -776,26 +816,26 @@ impl OverlayContextInternal { let transform = self.get_transform(); // Circle - let circle = kurbo::Circle::new((x, y), PIVOT_DIAMETER / 2.); + let circle = kurbo::Circle::new((x, y), PIVOT_DIAMETER / 2. * self.scale); self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &circle); // Crosshair - const CROSSHAIR_RADIUS: f64 = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.; + let crosshair_radius: f64 = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2. * self.scale; - let mut stroke = kurbo::Stroke::new(PIVOT_CROSSHAIR_THICKNESS); + let mut stroke = kurbo::Stroke::new(PIVOT_CROSSHAIR_THICKNESS * self.scale); stroke = stroke.with_caps(kurbo::Cap::Round); // Horizontal line let mut path = BezPath::new(); - path.move_to(kurbo::Point::new(x + CROSSHAIR_RADIUS * uv.x, y + CROSSHAIR_RADIUS * uv.y)); - path.line_to(kurbo::Point::new(x - CROSSHAIR_RADIUS * uv.x, y - CROSSHAIR_RADIUS * uv.y)); + path.move_to(kurbo::Point::new(x + crosshair_radius * uv.x, y + crosshair_radius * uv.y)); + path.line_to(kurbo::Point::new(x - crosshair_radius * uv.x, y - crosshair_radius * uv.y)); self.scene.stroke(&stroke, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &path); // Vertical line let mut path = BezPath::new(); - path.move_to(kurbo::Point::new(x - CROSSHAIR_RADIUS * uv.y, y + CROSSHAIR_RADIUS * uv.x)); - path.line_to(kurbo::Point::new(x + CROSSHAIR_RADIUS * uv.y, y - CROSSHAIR_RADIUS * uv.x)); + path.move_to(kurbo::Point::new(x - crosshair_radius * uv.y, y + crosshair_radius * uv.x)); + path.line_to(kurbo::Point::new(x + crosshair_radius * uv.y, y - crosshair_radius * uv.x)); self.scene.stroke(&stroke, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &path); } @@ -807,7 +847,7 @@ impl OverlayContextInternal { let transform = self.get_transform(); // Draw the background circle with a white fill and colored outline - let circle = kurbo::Circle::new((x, y), DOWEL_PIN_RADIUS); + let circle = kurbo::Circle::new((x, y), DOWEL_PIN_RADIUS * self.scale); self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(COLOR_OVERLAY_WHITE), None, &circle); self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color), None, &circle); @@ -816,11 +856,11 @@ impl OverlayContextInternal { // Top-left sector path.move_to(kurbo::Point::new(x, y)); - let end_x = x + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle).cos(); - let end_y = y + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle).sin(); + let end_x = x + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle).cos() * self.scale; + let end_y = y + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle).sin() * self.scale; path.line_to(kurbo::Point::new(end_x, end_y)); // Draw arc manually - let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), FRAC_PI_2 + angle, FRAC_PI_2, 0.0); + let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS * self.scale, DOWEL_PIN_RADIUS * self.scale), FRAC_PI_2 + angle, FRAC_PI_2, 0.0); arc.to_cubic_beziers(0.1, |p1, p2, p| { path.curve_to(p1, p2, p); }); @@ -828,11 +868,11 @@ impl OverlayContextInternal { // Bottom-right sector path.move_to(kurbo::Point::new(x, y)); - let end_x = x + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle).cos(); - let end_y = y + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle).sin(); + let end_x = x + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle).cos() * self.scale; + let end_y = y + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle).sin() * self.scale; path.line_to(kurbo::Point::new(end_x, end_y)); // Draw arc manually - let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), PI + FRAC_PI_2 + angle, FRAC_PI_2, 0.0); + let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS * self.scale, DOWEL_PIN_RADIUS * self.scale), PI + FRAC_PI_2 + angle, FRAC_PI_2, 0.0); arc.to_cubic_beziers(0.1, |p1, p2, p| { path.curve_to(p1, p2, p); }); @@ -861,7 +901,8 @@ impl OverlayContextInternal { self.bezier_to_path(bezier, transform, move_to, &mut path); } - self.scene.stroke(&kurbo::Stroke::new(1.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); + self.scene + .stroke(&kurbo::Stroke::new(1.0 * self.scale), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); } /// Used by the Pen tool in order to show how the bezier curve would look like. @@ -870,7 +911,8 @@ impl OverlayContextInternal { let mut path = BezPath::new(); self.bezier_to_path(bezier, transform, true, &mut path); - self.scene.stroke(&kurbo::Stroke::new(1.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); + self.scene + .stroke(&kurbo::Stroke::new(1.0 * self.scale), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); } /// Used by the path tool segment mode in order to show the selected segments. @@ -879,7 +921,8 @@ impl OverlayContextInternal { let mut path = BezPath::new(); self.bezier_to_path(bezier, transform, true, &mut path); - self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); + self.scene + .stroke(&kurbo::Stroke::new(4.0 * self.scale), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); } fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) { @@ -887,7 +930,8 @@ impl OverlayContextInternal { let mut path = BezPath::new(); self.bezier_to_path(bezier, transform, true, &mut path); - self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &path); + self.scene + .stroke(&kurbo::Stroke::new(4.0 * self.scale), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &path); } fn bezier_to_path(&self, bezier: PathSeg, transform: DAffine2, move_to: bool, path: &mut BezPath) { @@ -963,7 +1007,7 @@ impl OverlayContextInternal { let path = self.push_path(subpaths.iter(), transform); let color = color.unwrap_or(COLOR_OVERLAY_BLUE); - self.scene.stroke(&kurbo::Stroke::new(1.0), self.get_transform(), Self::parse_color(color), None, &path); + self.scene.stroke(&kurbo::Stroke::new(1.0 * self.scale), self.get_transform(), Self::parse_color(color), None, &path); } } @@ -1020,7 +1064,7 @@ impl OverlayContextInternal { const FONT_SIZE: f64 = 12.0; let typesetting = TypesettingConfig { - font_size: FONT_SIZE, + font_size: FONT_SIZE * self.scale, line_height_ratio: 1.2, character_spacing: 0.0, max_width: None, @@ -1044,7 +1088,7 @@ impl OverlayContextInternal { // Create typesetting configuration let typesetting = TypesettingConfig { - font_size: FONT_SIZE, + font_size: FONT_SIZE * self.scale, line_height_ratio: 1.2, character_spacing: 0.0, max_width: None, @@ -1131,7 +1175,7 @@ impl OverlayContextInternal { Some(ref typed_string) => typed_string, None => &format!("{:.2}", translation.x).trim_end_matches('0').trim_end_matches('.').to_string(), }; - let x_transform = DAffine2::from_translation((quad.top_left() + quad.top_right()) / 2.); + let x_transform = DAffine2::from_translation((quad.top_left() + quad.top_right()) / 2. * self.scale); self.text(width, COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]); } @@ -1142,7 +1186,7 @@ impl OverlayContextInternal { Some(ref typed_string) => typed_string, None => &format!("{:.2}", translation.y).trim_end_matches('0').trim_end_matches('.').to_string(), }; - let y_transform = DAffine2::from_translation((quad.top_left() + quad.bottom_left()) / 2.); + let y_transform = DAffine2::from_translation((quad.top_left() + quad.bottom_left()) / 2. * self.scale); let height_pivot = if translation.x> -1e-3 { Pivot::Start } else { Pivot::End }; self.text(height, COLOR_OVERLAY_BLUE, None, y_transform, 3., [height_pivot, Pivot::Middle]); } diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index cb5ce0e91f..58bac688ef 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -1,8 +1,8 @@ use super::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint}; use crate::consts::{ - BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_OVERLAY_WHITE, MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY, - MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS, MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY, RESIZE_HANDLE_SIZE, - SELECTION_DRAG_ANGLE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE, + BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY, MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS, + MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY, RESIZE_HANDLE_SIZE, SELECTION_DRAG_ANGLE, SKEW_TRIANGLE_OFFSET, + SKEW_TRIANGLE_SIZE, }; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; @@ -489,13 +489,7 @@ impl BoundingBoxManager { if (end - start).length() < MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY { return; } - - let edge_dir = (end - start).normalize(); - let mid = end.midpoint(start); - - for edge in [edge_dir, -edge_dir] { - overlay_context.draw_triangle(mid + edge * (3. + SKEW_TRIANGLE_OFFSET), edge, SKEW_TRIANGLE_SIZE, None, None); - } + overlay_context.skew_handles(start, end); }; if let Some([start, end]) = self.edge_endpoints_vector_from_edge_bool(hover_edge) { @@ -565,11 +559,6 @@ impl BoundingBoxManager { self.render_quad(overlay_context); } - let mut draw_handle = |point: DVec2, angle: f64| { - let quad = DAffine2::from_angle_translation(angle, point) * Quad::from_box([DVec2::splat(-RESIZE_HANDLE_SIZE / 2.), DVec2::splat(RESIZE_HANDLE_SIZE / 2.)]); - overlay_context.quad(quad, None, Some(COLOR_OVERLAY_WHITE)); - }; - let horizontal_angle = (quad.top_left() - quad.bottom_left()).to_angle(); let vertical_angle = (quad.top_left() - quad.top_right()).to_angle(); @@ -579,7 +568,7 @@ impl BoundingBoxManager { TransformCageSizeCategory::Full | TransformCageSizeCategory::Narrow | TransformCageSizeCategory::ReducedLandscape ) { for point in horizontal_edges { - draw_handle(point, horizontal_angle); + overlay_context.resize_handle(point, horizontal_angle); } } @@ -589,7 +578,7 @@ impl BoundingBoxManager { TransformCageSizeCategory::Full | TransformCageSizeCategory::Narrow | TransformCageSizeCategory::ReducedPortrait ) { for point in vertical_edges { - draw_handle(point, vertical_angle); + overlay_context.resize_handle(point, vertical_angle); } } @@ -606,14 +595,14 @@ impl BoundingBoxManager { TransformCageSizeCategory::Full | TransformCageSizeCategory::ReducedBoth | TransformCageSizeCategory::ReducedLandscape | TransformCageSizeCategory::ReducedPortrait ) { for point in quad.0 { - draw_handle(point, angle); + overlay_context.resize_handle(point, angle); } } // Draw the flat line endpoint drag handles if category == TransformCageSizeCategory::Flat { - draw_handle(self.transform.transform_point2(self.bounds[0]), angle); - draw_handle(self.transform.transform_point2(self.bounds[1]), angle); + overlay_context.resize_handle(self.transform.transform_point2(self.bounds[0]), angle); + overlay_context.resize_handle(self.transform.transform_point2(self.bounds[1]), angle); } } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 90fbf6b831..11c51f572e 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -509,9 +509,8 @@ impl Fsm for TextToolFsmState { let ToolMessage::Text(event) = event else { return self }; match (self, event) { (TextToolFsmState::Editing, TextToolMessage::Overlays { context: mut overlay_context }) => { - responses.add(FrontendMessage::DisplayEditableTextboxTransform { - transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(), - }); + let transform = document.metadata().transform_to_viewport(tool_data.layer).to_cols_array().map(|n| overlay_context.reverse_scale(n)); + responses.add(FrontendMessage::DisplayEditableTextboxTransform { transform }); if let Some(editing_text) = tool_data.editing_text.as_mut() { let far = graphene_std::text::bounding_box(&tool_data.new_text, &editing_text.font, font_cache, editing_text.typesetting, false); if far.x != 0. && far.y != 0. { diff --git a/frontend/src/components/window/title-bar/TitleBar.svelte b/frontend/src/components/window/title-bar/TitleBar.svelte index eb24626978..dd1e96416f 100644 --- a/frontend/src/components/window/title-bar/TitleBar.svelte +++ b/frontend/src/components/window/title-bar/TitleBar.svelte @@ -9,7 +9,6 @@ import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte"; import WindowButtonsLinux from "@graphite/components/window/title-bar/WindowButtonsLinux.svelte"; - import WindowButtonsMac from "@graphite/components/window/title-bar/WindowButtonsMac.svelte"; import WindowButtonsWeb from "@graphite/components/window/title-bar/WindowButtonsWeb.svelte"; import WindowButtonsWindows from "@graphite/components/window/title-bar/WindowButtonsWindows.svelte"; import WindowTitle from "@graphite/components/window/title-bar/WindowTitle.svelte"; @@ -69,9 +68,7 @@ - {#if platform === "Mac"} - - {:else} + {#if platform !== "Mac"} {#each entries as entry} {/each} diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 6da2dddcda..06baf28798 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -39,6 +39,10 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli const shakeSamples: { x: number; y: number; time: number }[] = []; let lastShakeTime = 0; + // Get device pixel ratio for coordinate transformation + // This fixes the offset issue on high-DPI devices like iPad + const getDevicePixelRatio = () => window.devicePixelRatio || 1; + // Event listeners // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -160,9 +164,14 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli const inGraphOverlay = get(document).graphViewOverlayOpen; if (!viewportPointerInteractionOngoing && (inFloatingMenu || inGraphOverlay)) return; + // Scale coordinates by device pixel ratio to fix offset on high-DPI devices like iPad + const dpr = getDevicePixelRatio(); + const scaledX = e.clientX * dpr; + const scaledY = e.clientY * dpr; + const modifiers = makeKeyboardModifiersBitfield(e); - if (detectShake(e)) editor.handle.onMouseShake(e.clientX, e.clientY, e.buttons, modifiers); - editor.handle.onMouseMove(e.clientX, e.clientY, e.buttons, modifiers); + if (detectShake(e)) editor.handle.onMouseShake(scaledX, scaledY, e.buttons, modifiers); + editor.handle.onMouseMove(scaledX, scaledY, e.buttons, modifiers); } function onPointerDown(e: PointerEvent) { @@ -190,8 +199,13 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli } if (viewportPointerInteractionOngoing && isTargetingCanvas instanceof Element) { + // Scale coordinates by device pixel ratio to fix offset on high-DPI devices like iPad + const dpr = getDevicePixelRatio(); + const scaledX = e.clientX * dpr; + const scaledY = e.clientY * dpr; + const modifiers = makeKeyboardModifiersBitfield(e); - editor.handle.onMouseDown(e.clientX, e.clientY, e.buttons, modifiers); + editor.handle.onMouseDown(scaledX, scaledY, e.buttons, modifiers); } } @@ -208,8 +222,13 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli if (textToolInteractiveInputElement) return; + // Scale coordinates by device pixel ratio to fix offset on high-DPI devices like iPad + const dpr = getDevicePixelRatio(); + const scaledX = e.clientX * dpr; + const scaledY = e.clientY * dpr; + const modifiers = makeKeyboardModifiersBitfield(e); - editor.handle.onMouseUp(e.clientX, e.clientY, e.buttons, modifiers); + editor.handle.onMouseUp(scaledX, scaledY, e.buttons, modifiers); } // Mouse events @@ -233,8 +252,13 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli if (e.button === BUTTON_BACK) buttons = 8; // Back if (e.button === BUTTON_FORWARD) buttons = 16; // Forward + // Scale coordinates by device pixel ratio to fix offset on high-DPI devices like iPad + const dpr = getDevicePixelRatio(); + const scaledX = e.clientX * dpr; + const scaledY = e.clientY * dpr; + const modifiers = makeKeyboardModifiersBitfield(e); - editor.handle.onDoubleClick(e.clientX, e.clientY, buttons, modifiers); + editor.handle.onDoubleClick(scaledX, scaledY, buttons, modifiers); } function onMouseDown(e: MouseEvent) { @@ -268,8 +292,13 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli if (isTargetingCanvas) { e.preventDefault(); + // Scale coordinates by device pixel ratio to fix offset on high-DPI devices like iPad + const dpr = getDevicePixelRatio(); + const scaledX = e.clientX * dpr; + const scaledY = e.clientY * dpr; + const modifiers = makeKeyboardModifiersBitfield(e); - editor.handle.onWheelScroll(e.clientX, e.clientY, e.buttons, e.deltaX, e.deltaY, e.deltaZ, modifiers); + editor.handle.onWheelScroll(scaledX, scaledY, e.buttons, e.deltaX, e.deltaY, e.deltaZ, modifiers); } } diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index aff6380c2e..fae16e6e67 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -337,7 +337,7 @@ export class HintInfo { // Rust enum `Key` export type KeyRaw = string; -// Serde converts a Rust `Key` enum variant into this format (via a custom serializer) with both the `Key` variant name (called `RawKey` in TS) and the localized `label` for the key +// Serde converts a Rust `Key` enum variant into this format with both the `Key` variant name (called `RawKey` in TS) and the localized `label` for the key export type Key = { key: KeyRaw; label: string }; export type LayoutKeysGroup = Key[]; export type ActionKeys = { keys: LayoutKeysGroup }; diff --git a/frontend/src/utility-functions/viewports.ts b/frontend/src/utility-functions/viewports.ts index 134ece0b31..9570101c0d 100644 --- a/frontend/src/utility-functions/viewports.ts +++ b/frontend/src/utility-functions/viewports.ts @@ -2,9 +2,14 @@ import { type Editor } from "@graphite/editor"; export function updateBoundsOfViewports(editor: Editor) { const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]")); + + // Get device pixel ratio to scale bounds for high-DPI devices like iPad + const dpr = window.devicePixelRatio || 1; + const boundsOfViewports = viewports.map((canvas) => { const bounds = canvas.getBoundingClientRect(); - return [bounds.left, bounds.top, bounds.right, bounds.bottom]; + // Scale bounds by device pixel ratio to match scaled pointer coordinates + return [bounds.left * dpr, bounds.top * dpr, bounds.right * dpr, bounds.bottom * dpr]; }); const flattened = boundsOfViewports.flat();

AltStyle によって変換されたページ (->オリジナル) /