From 7ae69c715e91cb7a1e0eef295a1d9857cf71d31c Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sun, 11 Jan 2026 15:41:37 +0000 Subject: [PATCH 01/21] wip --- desktop/src/app.rs | 5 +++++ desktop/src/window.rs | 10 +++++++++- desktop/wrapper/src/intercept_frontend_message.rs | 3 +++ desktop/wrapper/src/messages.rs | 1 + editor/src/messages/app_window/app_window_message.rs | 1 + .../messages/app_window/app_window_message_handler.rs | 3 +++ editor/src/messages/frontend/frontend_message.rs | 1 + editor/src/messages/input_mapper/input_mappings.rs | 3 +++ 8 files changed, 26 insertions(+), 1 deletion(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 1648616f29..004d44c5b2 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -352,6 +352,11 @@ impl App { window.toggle_maximize(); } } + DesktopFrontendMessage::WindowFullscreen => { + if let Some(window) = &self.window { + window.toggle_fullscreen(); + } + } DesktopFrontendMessage::WindowDrag => { if let Some(window) = &self.window { window.start_drag(); diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 249ba2d455..914c7b29cd 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use winit::cursor::{CursorIcon, CustomCursor, CustomCursorSource}; use winit::event_loop::ActiveEventLoop; -use winit::window::{Window as WinitWindow, WindowAttributes}; +use winit::window::{Fullscreen, Window as WinitWindow, WindowAttributes}; use crate::consts::APP_NAME; use crate::event::AppEventScheduler; @@ -118,6 +118,14 @@ impl Window { self.winit_window.is_maximized() } + pub(crate) fn toggle_fullscreen(&self) { + if self.is_fullscreen() { + self.winit_window.set_fullscreen(None); + } else { + self.winit_window.set_fullscreen(Some(Fullscreen::Borderless(None))); + } + } + pub(crate) fn is_fullscreen(&self) -> bool { self.winit_window.fullscreen().is_some() } diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index baa8000b39..9b64020eb5 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -148,6 +148,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD FrontendMessage::WindowMaximize => { dispatcher.respond(DesktopFrontendMessage::WindowMaximize); } + FrontendMessage::WindowFullscreen => { + dispatcher.respond(DesktopFrontendMessage::WindowFullscreen); + } FrontendMessage::WindowDrag => { dispatcher.respond(DesktopFrontendMessage::WindowDrag); } diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index f9d4bc5ecc..08c31df36a 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -69,6 +69,7 @@ pub enum DesktopFrontendMessage { WindowClose, WindowMinimize, WindowMaximize, + WindowFullscreen, WindowDrag, WindowHide, WindowHideOthers, diff --git a/editor/src/messages/app_window/app_window_message.rs b/editor/src/messages/app_window/app_window_message.rs index 7a988c6cde..0dcc39ae5d 100644 --- a/editor/src/messages/app_window/app_window_message.rs +++ b/editor/src/messages/app_window/app_window_message.rs @@ -11,6 +11,7 @@ pub enum AppWindowMessage { Close, Minimize, Maximize, + Fullscreen, Drag, Hide, HideOthers, diff --git a/editor/src/messages/app_window/app_window_message_handler.rs b/editor/src/messages/app_window/app_window_message_handler.rs index b862d3b18e..0d56f7c4ee 100644 --- a/editor/src/messages/app_window/app_window_message_handler.rs +++ b/editor/src/messages/app_window/app_window_message_handler.rs @@ -30,6 +30,9 @@ impl MessageHandler for AppWindowMessageHandler { AppWindowMessage::Maximize => { responses.add(FrontendMessage::WindowMaximize); } + AppWindowMessage::Fullscreen => { + responses.add(FrontendMessage::WindowFullscreen); + } AppWindowMessage::Drag => { responses.add(FrontendMessage::WindowDrag); } diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index e6c724069f..912cc205f9 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -371,6 +371,7 @@ pub enum FrontendMessage { WindowClose, WindowMinimize, WindowMaximize, + WindowFullscreen, WindowDrag, WindowHide, WindowHideOthers, diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 9e774b6c25..a570d2082a 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -54,6 +54,9 @@ pub fn input_mappings() -> Mapping { // Hack to prevent Left Click + Accel + Z combo (this effectively blocks you from making a double undo with AbortTransaction) entry!(KeyDown(KeyZ); modifiers=[Accel, MouseLeft], action_dispatch=DocumentMessage::Noop), // + // AppWindowMessage + entry!(KeyDown(F11); action_dispatch=AppWindowMessage::Fullscreen), + // // ClipboardMessage entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=ClipboardMessage::Cut), entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=ClipboardMessage::Copy), From f7b40b657e46014b1be75986528812295f73d5a9 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sun, 11 Jan 2026 18:53:17 +0000 Subject: [PATCH 02/21] fix --- desktop/src/window.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 914c7b29cd..f3af68b170 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; use std::sync::Arc; use winit::cursor::{CursorIcon, CustomCursor, CustomCursorSource}; use winit::event_loop::ActiveEventLoop; -use winit::window::{Fullscreen, Window as WinitWindow, WindowAttributes}; +use winit::monitor::Fullscreen; +use winit::window::{Window as WinitWindow, WindowAttributes}; use crate::consts::APP_NAME; use crate::event::AppEventScheduler; From 20f61203428e9405f8da97cfe54993d772600396 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sun, 11 Jan 2026 18:58:21 +0000 Subject: [PATCH 03/21] fix web --- frontend/src/io-managers/input.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 05d7cc8b31..8434eeab91 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -91,8 +91,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // Don't redirect paste in web if (key === "KeyV" && accelKey) return false; - // Don't redirect a fullscreen request - if (key === "F11" && e.type === "keydown" && !e.repeat) { + // Don't redirect a fullscreen request on web + if (key === "F11" && e.type === "keydown" && !e.repeat && isDesktop()) { e.preventDefault(); fullscreen.toggleFullscreen(); return false; From ce143d80140cdbbfa2761047bfa82fbe13db20b8 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sun, 11 Jan 2026 19:08:32 +0000 Subject: [PATCH 04/21] fix --- frontend/src/io-managers/input.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 8434eeab91..47f2b9e0e1 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -92,7 +92,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli if (key === "KeyV" && accelKey) return false; // Don't redirect a fullscreen request on web - if (key === "F11" && e.type === "keydown" && !e.repeat && isDesktop()) { + if (key === "F11" && e.type === "keydown" && !e.repeat && !isDesktop()) { e.preventDefault(); fullscreen.toggleFullscreen(); return false; From 0d2710f762a0cdc3da2ac72363d776b8f24c5d85 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sun, 11 Jan 2026 23:25:08 +0000 Subject: [PATCH 05/21] fix --- editor/src/messages/app_window/app_window_message_handler.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/messages/app_window/app_window_message_handler.rs b/editor/src/messages/app_window/app_window_message_handler.rs index 0d56f7c4ee..66c4ef0bab 100644 --- a/editor/src/messages/app_window/app_window_message_handler.rs +++ b/editor/src/messages/app_window/app_window_message_handler.rs @@ -51,6 +51,7 @@ impl MessageHandler for AppWindowMessageHandler { Close, Minimize, Maximize, + Fullscreen, Drag, Hide, HideOthers, From 59c8b8d7a4d0567d2a7bfa359b3209be4bc51b79 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Tue, 13 Jan 2026 15:39:31 +0000 Subject: [PATCH 06/21] Use default fullscreen shortcut on mac --- .../messages/input_mapper/input_mappings.rs | 15 ++---- .../input_mapper/utility_types/macros.rs | 47 ++++++++++++++++--- .../input_mapper/utility_types/misc.rs | 2 + 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index a570d2082a..48abf5f47c 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -55,7 +55,9 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyZ); modifiers=[Accel, MouseLeft], action_dispatch=DocumentMessage::Noop), // // AppWindowMessage - entry!(KeyDown(F11); action_dispatch=AppWindowMessage::Fullscreen), + entry!(KeyDown(F11); active=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Fullscreen), + entry!(KeyDown(KeyF); modifiers=[Accel, Meta], active=cfg!(target_os = "macos"), action_dispatch=AppWindowMessage::Fullscreen), + entry!(KeyDown(KeyQ); modifiers=[Accel], active=cfg!(target_os = "macos"), action_dispatch=AppWindowMessage::Close), // // ClipboardMessage entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=ClipboardMessage::Cut), @@ -474,7 +476,7 @@ pub fn input_mappings() -> Mapping { // Sort `pointer_shake` sort(&mut pointer_shake); - let mut mapping = Mapping { + Mapping { key_up, key_down, key_up_no_repeat, @@ -483,16 +485,7 @@ pub fn input_mappings() -> Mapping { wheel_scroll, pointer_move, pointer_shake, - }; - - if cfg!(target_os = "macos") { - let remove: [&[&[MappingEntry; 0]; 0]; 0] = []; - let add = [entry!(KeyDown(KeyQ); modifiers=[Accel], action_dispatch=AppWindowMessage::Close)]; - - apply_mapping_patch(&mut mapping, remove, add); } - - mapping } /// Default mappings except that scrolling without modifier keys held down is bound to zooming instead of vertical panning diff --git a/editor/src/messages/input_mapper/utility_types/macros.rs b/editor/src/messages/input_mapper/utility_types/macros.rs index bd0354d280..267f2b0d6d 100644 --- a/editor/src/messages/input_mapper/utility_types/macros.rs +++ b/editor/src/messages/input_mapper/utility_types/macros.rs @@ -25,17 +25,45 @@ macro_rules! modifiers { /// When an action is currently available, and the user enters that input, the action's message is dispatched on the message bus. macro_rules! entry { // Pattern with canonical parameter - ($input:expr_2021; $(modifiers=[$($modifier:ident),*],)? $(refresh_keys=[$($refresh:ident),* $(,)?],)? canonical, action_dispatch=$action_dispatch:expr_2021$(,)?) => { - entry!($input; $($($modifier),*)?; $($($refresh),*)?; $action_dispatch; true) + ( + $input:expr_2021; + $(modifiers=[$($modifier:ident),*],)? + $(refresh_keys=[$($refresh:ident),* $(,)?],)? + canonical, + $(active=$active:expr,)? + action_dispatch=$action_dispatch:expr_2021$(,)? + ) => { + entry!( + $input; + $($($modifier),*)?; + $($($refresh),*)?; + $action_dispatch; + true; + true $( && $active )? + ) }; // Pattern without canonical parameter - ($input:expr_2021; $(modifiers=[$($modifier:ident),*],)? $(refresh_keys=[$($refresh:ident),* $(,)?],)? action_dispatch=$action_dispatch:expr_2021$(,)?) => { - entry!($input; $($($modifier),*)?; $($($refresh),*)?; $action_dispatch; false) + ( + $input:expr_2021; + $(modifiers=[$($modifier:ident),*],)? + $(refresh_keys=[$($refresh:ident),* $(,)?],)? + $(active=$active:expr,)? + action_dispatch=$action_dispatch:expr_2021$(,)? + ) => { + entry!( + $input; + $($($modifier),*)?; + $($($refresh),*)?; + $action_dispatch; + false; + true $( && $active )? + ) }; // Implementation macro to avoid code duplication - ($input:expr; $($modifier:ident),*; $($refresh:ident),*; $action_dispatch:expr; $canonical:expr) => { + ($input:expr; $($modifier:ident),*; $($refresh:ident),*; $action_dispatch:expr; $canonical:expr; $active:expr) => { + &[&[ // Cause the `action_dispatch` message to be sent when the specified input occurs. MappingEntry { @@ -43,33 +71,37 @@ macro_rules! entry { input: $input, modifiers: modifiers!($($modifier),*), canonical: $canonical, + active: $active, }, - // Also cause the `action_dispatch` message to be sent when any of the specified refresh keys change. $( MappingEntry { action: $action_dispatch.into(), input: InputMapperMessage::KeyDown(Key::$refresh), modifiers: modifiers!(), canonical: $canonical, + active: $active, }, MappingEntry { action: $action_dispatch.into(), input: InputMapperMessage::KeyUp(Key::$refresh), modifiers: modifiers!(), canonical: $canonical, + active: $active, }, MappingEntry { action: $action_dispatch.into(), input: InputMapperMessage::KeyDownNoRepeat(Key::$refresh), modifiers: modifiers!(), canonical: $canonical, + active: $active, }, MappingEntry { action: $action_dispatch.into(), input: InputMapperMessage::KeyUpNoRepeat(Key::$refresh), modifiers: modifiers!(), canonical: $canonical, + active: $active, }, )* ]] @@ -97,6 +129,9 @@ macro_rules! mapping { for entry_slice in $entry { // Each entry in the slice (usually just one, except when `refresh_keys` adds additional key entries) for entry in entry_slice.into_iter() { + if !entry.active { + continue; + } let corresponding_list = match entry.input { InputMapperMessage::KeyDown(key) => &mut key_down[key as usize], InputMapperMessage::KeyUp(key) => &mut key_up[key as usize], diff --git a/editor/src/messages/input_mapper/utility_types/misc.rs b/editor/src/messages/input_mapper/utility_types/misc.rs index 1f8be1056e..34f2efd073 100644 --- a/editor/src/messages/input_mapper/utility_types/misc.rs +++ b/editor/src/messages/input_mapper/utility_types/misc.rs @@ -125,6 +125,8 @@ pub struct MappingEntry { pub modifiers: KeyStates, /// True indicates that this takes priority as the labeled hotkey shown in UI menus and tooltips instead of an alternate binding for the same action pub canonical: bool, + /// Whether this mapping is active + pub active: bool, } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] From 78e8679e2f03e6c4111ec3508b824dc4831c373a Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 15 Jan 2026 13:06:28 +0000 Subject: [PATCH 07/21] review improvements --- desktop/src/window.rs | 2 + .../messages/input_mapper/input_mappings.rs | 66 +++++-------------- .../input_mapper/utility_types/macros.rs | 22 +++---- .../input_mapper/utility_types/misc.rs | 4 +- .../menu_bar/menu_bar_message_handler.rs | 9 ++- .../src/components/window/TitleBar.svelte | 16 +++-- frontend/wasm/src/editor_api.rs | 6 ++ 7 files changed, 56 insertions(+), 69 deletions(-) diff --git a/desktop/src/window.rs b/desktop/src/window.rs index f3af68b170..ca7e9dc048 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -122,7 +122,9 @@ impl Window { pub(crate) fn toggle_fullscreen(&self) { if self.is_fullscreen() { self.winit_window.set_fullscreen(None); + self.winit_window.set_maximized(true); } else { + self.winit_window.set_maximized(false); self.winit_window.set_fullscreen(Some(Fullscreen::Borderless(None))); } } diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 48abf5f47c..d72a374c00 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -17,16 +17,18 @@ use glam::DVec2; impl From for Mapping { fn from(value: MappingVariant) -> Self { match value { - MappingVariant::Default => input_mappings(), - MappingVariant::ZoomWithScroll => zoom_with_scroll(), + MappingVariant::Default => input_mappings(false), + MappingVariant::ZoomWithScroll => input_mappings(true), } } } -pub fn input_mappings() -> Mapping { +pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { use InputMapperMessage::*; use Key::*; + let keyboard_platform = GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout(); + // NOTICE: // If a new mapping you added here isn't working (and perhaps another lower-precedence one is instead), make sure to advertise // it as an available action in the respective message handler file (such as the bottom of `document_message_handler.rs`). @@ -55,9 +57,9 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyZ); modifiers=[Accel, MouseLeft], action_dispatch=DocumentMessage::Noop), // // AppWindowMessage - entry!(KeyDown(F11); active=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Fullscreen), - entry!(KeyDown(KeyF); modifiers=[Accel, Meta], active=cfg!(target_os = "macos"), action_dispatch=AppWindowMessage::Fullscreen), - entry!(KeyDown(KeyQ); modifiers=[Accel], active=cfg!(target_os = "macos"), action_dispatch=AppWindowMessage::Close), + entry!(KeyDown(F11); disabled=cfg!(target_os = "macos"), action_dispatch=AppWindowMessage::Fullscreen), + entry!(KeyDown(KeyF); modifiers=[Accel, Meta], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Fullscreen), + entry!(KeyDown(KeyQ); modifiers=[Accel], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Close), // // ClipboardMessage entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=ClipboardMessage::Cut), @@ -421,10 +423,14 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(FakeKeyPlus); modifiers=[Accel], canonical, action_dispatch=NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }), entry!(KeyDown(Equal); modifiers=[Accel], action_dispatch=NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }), entry!(KeyDown(Minus); modifiers=[Accel], action_dispatch=NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }), - entry!(WheelScroll; modifiers=[Control], action_dispatch=NavigationMessage::CanvasZoomMouseWheel), - entry!(WheelScroll; modifiers=[Command], action_dispatch=NavigationMessage::CanvasZoomMouseWheel), - entry!(WheelScroll; modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }), - entry!(WheelScroll; action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: false }), + entry!(WheelScroll; modifiers=[Control], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasZoomMouseWheel), + entry!(WheelScroll; modifiers=[Command], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasZoomMouseWheel), + entry!(WheelScroll; modifiers=[Shift], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }), + entry!(WheelScroll; disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: false }), + // On Mac, the OS already converts Shift+scroll into horizontal scrolling so we have to reverse the behavior from normal to produce the same outcome + entry!(WheelScroll; modifiers=[Control], disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: keyboard_platform == KeyboardPlatformLayout::Mac }), + entry!(WheelScroll; modifiers=[Shift], disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: keyboard_platform != KeyboardPlatformLayout::Mac }), + entry!(WheelScroll; disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasZoomMouseWheel), entry!(KeyDown(PageUp); modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanByViewportFraction { delta: DVec2::new(1., 0.) }), entry!(KeyDown(PageDown); modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanByViewportFraction { delta: DVec2::new(-1., 0.) }), entry!(KeyDown(PageUp); action_dispatch=NavigationMessage::CanvasPanByViewportFraction { delta: DVec2::new(0., 1.) }), @@ -487,43 +493,3 @@ pub fn input_mappings() -> Mapping { pointer_shake, } } - -/// Default mappings except that scrolling without modifier keys held down is bound to zooming instead of vertical panning -pub fn zoom_with_scroll() -> Mapping { - use InputMapperMessage::*; - - // On Mac, the OS already converts Shift+scroll into horizontal scrolling so we have to reverse the behavior from normal to produce the same outcome - let keyboard_platform = GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout(); - - let mut mapping = input_mappings(); - - let remove = [ - entry!(WheelScroll; modifiers=[Control], action_dispatch=NavigationMessage::CanvasZoomMouseWheel), - entry!(WheelScroll; modifiers=[Command], action_dispatch=NavigationMessage::CanvasZoomMouseWheel), - entry!(WheelScroll; modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }), - entry!(WheelScroll; action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: false }), - ]; - let add = [ - entry!(WheelScroll; modifiers=[Control], action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: keyboard_platform == KeyboardPlatformLayout::Mac }), - entry!(WheelScroll; modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: keyboard_platform != KeyboardPlatformLayout::Mac }), - entry!(WheelScroll; action_dispatch=NavigationMessage::CanvasZoomMouseWheel), - ]; - - apply_mapping_patch(&mut mapping, remove, add); - - mapping -} - -fn apply_mapping_patch<'a, const N: usize, const M: usize, const X: usize, const Y: usize>( - mapping: &mut Mapping, - remove: impl IntoIterator, - add: impl IntoIterator, -) { - for entry in remove.into_iter().flat_map(|inner| inner.iter()).flat_map(|inner| inner.iter()) { - mapping.remove(entry); - } - - for entry in add.into_iter().flat_map(|inner| inner.iter()).flat_map(|inner| inner.iter()) { - mapping.add(entry.clone()); - } -} diff --git a/editor/src/messages/input_mapper/utility_types/macros.rs b/editor/src/messages/input_mapper/utility_types/macros.rs index 267f2b0d6d..5a9d162b25 100644 --- a/editor/src/messages/input_mapper/utility_types/macros.rs +++ b/editor/src/messages/input_mapper/utility_types/macros.rs @@ -30,7 +30,7 @@ macro_rules! entry { $(modifiers=[$($modifier:ident),*],)? $(refresh_keys=[$($refresh:ident),* $(,)?],)? canonical, - $(active=$active:expr,)? + $(disabled=$disabled:expr,)? action_dispatch=$action_dispatch:expr_2021$(,)? ) => { entry!( @@ -39,7 +39,7 @@ macro_rules! entry { $($($refresh),*)?; $action_dispatch; true; - true $( && $active )? + true $( && $disabled )? ) }; @@ -48,7 +48,7 @@ macro_rules! entry { $input:expr_2021; $(modifiers=[$($modifier:ident),*],)? $(refresh_keys=[$($refresh:ident),* $(,)?],)? - $(active=$active:expr,)? + $(disabled=$disabled:expr,)? action_dispatch=$action_dispatch:expr_2021$(,)? ) => { entry!( @@ -57,12 +57,12 @@ macro_rules! entry { $($($refresh),*)?; $action_dispatch; false; - true $( && $active )? + true $( && $disabled )? ) }; // Implementation macro to avoid code duplication - ($input:expr; $($modifier:ident),*; $($refresh:ident),*; $action_dispatch:expr; $canonical:expr; $active:expr) => { + ($input:expr; $($modifier:ident),*; $($refresh:ident),*; $action_dispatch:expr; $canonical:expr; $disabled:expr) => { &[&[ // Cause the `action_dispatch` message to be sent when the specified input occurs. @@ -71,7 +71,7 @@ macro_rules! entry { input: $input, modifiers: modifiers!($($modifier),*), canonical: $canonical, - active: $active, + disabled: $disabled, }, $( @@ -80,28 +80,28 @@ macro_rules! entry { input: InputMapperMessage::KeyDown(Key::$refresh), modifiers: modifiers!(), canonical: $canonical, - active: $active, + disabled: $disabled, }, MappingEntry { action: $action_dispatch.into(), input: InputMapperMessage::KeyUp(Key::$refresh), modifiers: modifiers!(), canonical: $canonical, - active: $active, + disabled: $disabled, }, MappingEntry { action: $action_dispatch.into(), input: InputMapperMessage::KeyDownNoRepeat(Key::$refresh), modifiers: modifiers!(), canonical: $canonical, - active: $active, + disabled: $disabled, }, MappingEntry { action: $action_dispatch.into(), input: InputMapperMessage::KeyUpNoRepeat(Key::$refresh), modifiers: modifiers!(), canonical: $canonical, - active: $active, + disabled: $disabled, }, )* ]] @@ -129,7 +129,7 @@ macro_rules! mapping { for entry_slice in $entry { // Each entry in the slice (usually just one, except when `refresh_keys` adds additional key entries) for entry in entry_slice.into_iter() { - if !entry.active { + if entry.disabled { continue; } let corresponding_list = match entry.input { diff --git a/editor/src/messages/input_mapper/utility_types/misc.rs b/editor/src/messages/input_mapper/utility_types/misc.rs index 34f2efd073..bd0f4187be 100644 --- a/editor/src/messages/input_mapper/utility_types/misc.rs +++ b/editor/src/messages/input_mapper/utility_types/misc.rs @@ -125,8 +125,8 @@ pub struct MappingEntry { pub modifiers: KeyStates, /// True indicates that this takes priority as the labeled hotkey shown in UI menus and tooltips instead of an alternate binding for the same action pub canonical: bool, - /// Whether this mapping is active - pub active: bool, + /// Whether this mapping is disabled + pub disabled: bool, } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] diff --git a/editor/src/messages/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/menu_bar/menu_bar_message_handler.rs index b06b7397a7..ec785db165 100644 --- a/editor/src/messages/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/menu_bar/menu_bar_message_handler.rs @@ -621,6 +621,13 @@ impl LayoutHolder for MenuBarMessageHandler { .label("Window") .flush(true) .menu_list_children(vec![ + vec![ + MenuListEntry::new("Fullscreen") + .label("Fullscreen") + .icon("FullscreenEnter") + .tooltip_shortcut(action_shortcut!(AppWindowMessageDiscriminant::Fullscreen)) + .on_commit(|_| AppWindowMessage::Fullscreen.into()), + ], vec![ MenuListEntry::new("Properties") .label("Properties") @@ -632,8 +639,6 @@ impl LayoutHolder for MenuBarMessageHandler { .icon(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }) .tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen)) .on_commit(|_| PortfolioMessage::ToggleLayersPanelOpen.into()), - ], - vec![ MenuListEntry::new("Data") .label("Data") .icon(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }) diff --git a/frontend/src/components/window/TitleBar.svelte b/frontend/src/components/window/TitleBar.svelte index 24a1716fab..3ce892176b 100644 --- a/frontend/src/components/window/TitleBar.svelte +++ b/frontend/src/components/window/TitleBar.svelte @@ -11,6 +11,7 @@ import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte"; + import { isDesktop } from "/src/utility-functions/platform"; const appWindow = getContext("appWindow"); const editor = getContext("editor"); @@ -19,7 +20,8 @@ let menuBarLayout: Layout = []; - $: showFullscreenButton = $appWindow.platform === "Web" || $fullscreen.windowFullscreen; + $: showFullscreenButton = $appWindow.platform === "Web" || $fullscreen.windowFullscreen || (isDesktop() && $appWindow.fullscreen); + $: isFullscreen = isDesktop() ? $appWindow.fullscreen : $fullscreen.windowFullscreen; // On Mac, the menu bar height needs to be scaled by the inverse of the UI scale to fit its native window buttons $: height = $appWindow.platform === "Mac" ? 28 * (1 / $appWindow.uiScale) : 28; @@ -45,14 +47,20 @@ {#if $appWindow.platform !== "Mac"} {#if showFullscreenButton} ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)()} + on:click={() => { + if (isDesktop()) { + editor.handle.appWindowFullscreen(); + } else { + ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)(); + } + }} > - + {:else} editor.handle.appWindowMinimize()}> diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 6d637f0d1b..cfb4024914 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -295,6 +295,12 @@ impl EditorHandle { self.dispatch(message); } + #[wasm_bindgen(js_name = appWindowFullscreen)] + pub fn app_window_fullscreen(&self) { + let message = AppWindowMessage::Fullscreen; + self.dispatch(message); + } + /// Closes the application window #[wasm_bindgen(js_name = appWindowClose)] pub fn app_window_close(&self) { From 718369d26a8cb43f2197c7fc93eac48788fb57f9 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 15 Jan 2026 13:28:18 +0000 Subject: [PATCH 08/21] fix --- editor/src/messages/input_mapper/input_mappings.rs | 2 +- editor/src/messages/input_mapper/utility_types/macros.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index d72a374c00..2a55034642 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -58,7 +58,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { // // AppWindowMessage entry!(KeyDown(F11); disabled=cfg!(target_os = "macos"), action_dispatch=AppWindowMessage::Fullscreen), - entry!(KeyDown(KeyF); modifiers=[Accel, Meta], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Fullscreen), + entry!(KeyDown(KeyF); modifiers=[Accel, Control], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Fullscreen), entry!(KeyDown(KeyQ); modifiers=[Accel], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Close), // // ClipboardMessage diff --git a/editor/src/messages/input_mapper/utility_types/macros.rs b/editor/src/messages/input_mapper/utility_types/macros.rs index 5a9d162b25..f8e406da98 100644 --- a/editor/src/messages/input_mapper/utility_types/macros.rs +++ b/editor/src/messages/input_mapper/utility_types/macros.rs @@ -39,7 +39,7 @@ macro_rules! entry { $($($refresh),*)?; $action_dispatch; true; - true $( && $disabled )? + false $( || $disabled )? ) }; @@ -57,7 +57,7 @@ macro_rules! entry { $($($refresh),*)?; $action_dispatch; false; - true $( && $disabled )? + false $( || $disabled )? ) }; From 87009a4d7ba6d4608ca59de221a54bffbbe0270f Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 15 Jan 2026 14:50:07 +0000 Subject: [PATCH 09/21] fixup --- desktop/src/app.rs | 2 +- desktop/src/window.rs | 7 +++++-- frontend/src/components/window/TitleBar.svelte | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 004d44c5b2..e3a3970c85 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -353,7 +353,7 @@ impl App { } } DesktopFrontendMessage::WindowFullscreen => { - if let Some(window) = &self.window { + if let Some(window) = &mut self.window { window.toggle_fullscreen(); } } diff --git a/desktop/src/window.rs b/desktop/src/window.rs index ca7e9dc048..8e7c3da726 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -43,6 +43,7 @@ pub(crate) struct Window { native_handle: native::NativeWindowImpl, custom_cursors: HashMap, clipboard: Option, + was_maximized_before_fullscreen: bool, } impl Drop for Window { fn drop(&mut self) { @@ -75,6 +76,7 @@ impl Window { native_handle, custom_cursors: HashMap::new(), clipboard, + was_maximized_before_fullscreen: false, } } @@ -119,11 +121,12 @@ impl Window { self.winit_window.is_maximized() } - pub(crate) fn toggle_fullscreen(&self) { + pub(crate) fn toggle_fullscreen(&mut self) { if self.is_fullscreen() { self.winit_window.set_fullscreen(None); - self.winit_window.set_maximized(true); + self.winit_window.set_maximized(self.was_maximized_before_fullscreen); } else { + self.was_maximized_before_fullscreen = self.winit_window.is_maximized(); self.winit_window.set_maximized(false); self.winit_window.set_fullscreen(Some(Fullscreen::Borderless(None))); } diff --git a/frontend/src/components/window/TitleBar.svelte b/frontend/src/components/window/TitleBar.svelte index 3ce892176b..e3968ebc42 100644 --- a/frontend/src/components/window/TitleBar.svelte +++ b/frontend/src/components/window/TitleBar.svelte @@ -69,7 +69,7 @@ editor.handle.appWindowMaximize()}> - editor.handle.appWindowClose()}> + editor.handle.appWindowClose()}> {/if} @@ -130,7 +130,7 @@ background: #2d2d2d; } - &:last-of-type:hover { + &.close:hover { background: #c42b1c; } } From 85eb40ad985640684c2986d7caad155b02fc464a Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 15 Jan 2026 15:00:57 +0000 Subject: [PATCH 10/21] fix --- desktop/src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 8e7c3da726..5f75b99cb5 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -123,8 +123,8 @@ impl Window { pub(crate) fn toggle_fullscreen(&mut self) { if self.is_fullscreen() { - self.winit_window.set_fullscreen(None); self.winit_window.set_maximized(self.was_maximized_before_fullscreen); + self.winit_window.set_fullscreen(None); } else { self.was_maximized_before_fullscreen = self.winit_window.is_maximized(); self.winit_window.set_maximized(false); From 54bb870a63f1ca41e96fb054f2b88bc7828af4e7 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 15 Jan 2026 15:03:24 +0000 Subject: [PATCH 11/21] fix --- desktop/src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 5f75b99cb5..de5203b555 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -127,8 +127,8 @@ impl Window { self.winit_window.set_fullscreen(None); } else { self.was_maximized_before_fullscreen = self.winit_window.is_maximized(); - self.winit_window.set_maximized(false); self.winit_window.set_fullscreen(Some(Fullscreen::Borderless(None))); + self.winit_window.set_maximized(false); } } From 9ecf6c867e7714a9651ffbcdec296e8296b501c1 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 15 Jan 2026 15:04:09 +0000 Subject: [PATCH 12/21] fix --- frontend/src/components/window/TitleBar.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/window/TitleBar.svelte b/frontend/src/components/window/TitleBar.svelte index e3968ebc42..2d36c03b6f 100644 --- a/frontend/src/components/window/TitleBar.svelte +++ b/frontend/src/components/window/TitleBar.svelte @@ -69,7 +69,7 @@ editor.handle.appWindowMaximize()}> - editor.handle.appWindowClose()}> + editor.handle.appWindowClose()}> {/if} From f7c9bb1a515f2615099c581c19b5a14101858a17 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 15 Jan 2026 15:12:53 +0000 Subject: [PATCH 13/21] fix --- desktop/src/window.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/desktop/src/window.rs b/desktop/src/window.rs index de5203b555..af94604ae9 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -43,7 +43,6 @@ pub(crate) struct Window { native_handle: native::NativeWindowImpl, custom_cursors: HashMap, clipboard: Option, - was_maximized_before_fullscreen: bool, } impl Drop for Window { fn drop(&mut self) { @@ -76,7 +75,6 @@ impl Window { native_handle, custom_cursors: HashMap::new(), clipboard, - was_maximized_before_fullscreen: false, } } @@ -123,12 +121,9 @@ impl Window { pub(crate) fn toggle_fullscreen(&mut self) { if self.is_fullscreen() { - self.winit_window.set_maximized(self.was_maximized_before_fullscreen); self.winit_window.set_fullscreen(None); } else { - self.was_maximized_before_fullscreen = self.winit_window.is_maximized(); self.winit_window.set_fullscreen(Some(Fullscreen::Borderless(None))); - self.winit_window.set_maximized(false); } } From 6a06508e20421252a4f818ba8ab93b44203f2c46 Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 15 Jan 2026 16:42:23 +0100 Subject: [PATCH 14/21] fix on windows --- desktop/src/window/win/native_handle.rs | 35 ++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/desktop/src/window/win/native_handle.rs b/desktop/src/window/win/native_handle.rs index 228324dc0f..23db92d69c 100644 --- a/desktop/src/window/win/native_handle.rs +++ b/desktop/src/window/win/native_handle.rs @@ -211,7 +211,7 @@ unsafe fn ensure_helper_class() { unsafe extern "system" fn main_window_handle_message(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { if msg == WM_NCCALCSIZE && wparam.0 != 0 { // When maximized, shrink to visible frame so content doesn't extend beyond it. - if unsafe { IsZoomed(hwnd).as_bool() } { + if unsafe { IsZoomed(hwnd).as_bool() } && !is_effectively_fullscreen(hwnd) { let params = unsafe { &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS) }; let dpi = unsafe { GetDpiForWindow(hwnd) }; @@ -366,3 +366,36 @@ unsafe fn calculate_resize_direction(helper: HWND, lparam: LPARAM) -> Option None, } } + +// Check if the window is effectively fullscreen, meaning it covers the entire monitor. +// We need to use this heuristic because Windows doesn't provide a way to check for fullscreen state. +fn is_effectively_fullscreen(hwnd: HWND) -> bool { + if hwnd.is_invalid() { + return false; + } + + let mut view_rect = RECT::default(); + if unsafe { GetWindowRect(hwnd, &mut view_rect) }.is_err() { + return false; + } + + let hmon = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + if hmon.is_invalid() { + return false; + } + + let mut monitor_info = MONITORINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + if !unsafe { GetMonitorInfoW(hmon, &mut monitor_info) }.as_bool() { + return false; + } + + // Allow a tiny tolerance for DPI / rounding issues + const EPS: i32 = 1; + (view_rect.left - monitor_info.rcMonitor.left).abs() <= EPS + && (view_rect.top - monitor_info.rcMonitor.top).abs() <= EPS + && (view_rect.right - monitor_info.rcMonitor.right).abs() <= EPS + && (view_rect.bottom - monitor_info.rcMonitor.bottom).abs() <= EPS +} From 738d2d8b572f0e3f66e527cbf673fe69ada4ba03 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 15 Jan 2026 16:19:59 -0800 Subject: [PATCH 15/21] Fix CSS --- frontend/src/components/window/TitleBar.svelte | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/window/TitleBar.svelte b/frontend/src/components/window/TitleBar.svelte index 2d36c03b6f..2ad7ceea22 100644 --- a/frontend/src/components/window/TitleBar.svelte +++ b/frontend/src/components/window/TitleBar.svelte @@ -53,11 +53,8 @@ : undefined} tooltipShortcut={$tooltip.f11Shortcut} on:click={() => { - if (isDesktop()) { - editor.handle.appWindowFullscreen(); - } else { - ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)(); - } + if (isDesktop()) editor.handle.appWindowFullscreen(); + else ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)(); }} > @@ -69,7 +66,7 @@ editor.handle.appWindowMaximize()}> - editor.handle.appWindowClose()}> + editor.handle.appWindowClose()}> {/if} @@ -123,19 +120,19 @@ padding: 0 8px; } - &.windows > .layout-row { + &.windows:not(.fullscreen) > .layout-row { padding: 0 17px; &:hover { background: #2d2d2d; } - &.close:hover { + &:last-of-type:hover { background: #c42b1c; } } - &.linux > .layout-row { + &.linux:not(.fullscreen) > .layout-row { padding: 0 12px; &:hover { From a0c8e8d12682be44c46321294386e6a3b6fa46d0 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Fri, 16 Jan 2026 10:21:58 +0000 Subject: [PATCH 16/21] don't allow drag and maximize when in fullscreen --- desktop/src/window.rs | 6 ++++++ frontend/src/components/window/TitleBar.svelte | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/desktop/src/window.rs b/desktop/src/window.rs index af94604ae9..b69a0be2e7 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -112,6 +112,9 @@ impl Window { } pub(crate) fn toggle_maximize(&self) { + if self.is_fullscreen() { + return; + } self.winit_window.set_maximized(!self.winit_window.is_maximized()); } @@ -132,6 +135,9 @@ impl Window { } pub(crate) fn start_drag(&self) { + if self.is_fullscreen() { + return; + } let _ = self.winit_window.drag_window(); } diff --git a/frontend/src/components/window/TitleBar.svelte b/frontend/src/components/window/TitleBar.svelte index 2ad7ceea22..a633bc16e3 100644 --- a/frontend/src/components/window/TitleBar.svelte +++ b/frontend/src/components/window/TitleBar.svelte @@ -41,7 +41,7 @@ {/if} - editor.handle.appWindowDrag()} on:dblclick={() => editor.handle.appWindowMaximize()} /> + {if (!isFullscreen) editor.handle.appWindowDrag()}} on:dblclick={() => {if (!isFullscreen)editor.handle.appWindowMaximize()}} /> {#if $appWindow.platform !== "Mac"} From 6d736d06878d881577dee7dcd7585ff106ed25ee Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 16 Jan 2026 02:25:05 -0800 Subject: [PATCH 17/21] JS nitpick --- frontend/src/components/window/TitleBar.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/window/TitleBar.svelte b/frontend/src/components/window/TitleBar.svelte index a633bc16e3..b3d72faded 100644 --- a/frontend/src/components/window/TitleBar.svelte +++ b/frontend/src/components/window/TitleBar.svelte @@ -41,7 +41,7 @@ {/if} - {if (!isFullscreen) editor.handle.appWindowDrag()}} on:dblclick={() => {if (!isFullscreen)editor.handle.appWindowMaximize()}} /> + !isFullscreen && editor.handle.appWindowDrag()} on:dblclick={() => !isFullscreen && editor.handle.appWindowMaximize()} /> {#if $appWindow.platform !== "Mac"} From bd91aca1ff40d4e29133e1e1e059ee4698c0e927 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 16 Jan 2026 13:31:26 +0100 Subject: [PATCH 18/21] fix fullscreen after maximize --- desktop/src/window/win/native_handle.rs | 28 +++++++++---------------- 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/desktop/src/window/win/native_handle.rs b/desktop/src/window/win/native_handle.rs index 23db92d69c..333477e65b 100644 --- a/desktop/src/window/win/native_handle.rs +++ b/desktop/src/window/win/native_handle.rs @@ -210,10 +210,10 @@ unsafe fn ensure_helper_class() { // Main window message handler, called on the UI thread for every message the main window receives. unsafe extern "system" fn main_window_handle_message(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { if msg == WM_NCCALCSIZE && wparam.0 != 0 { - // When maximized, shrink to visible frame so content doesn't extend beyond it. - if unsafe { IsZoomed(hwnd).as_bool() } && !is_effectively_fullscreen(hwnd) { - let params = unsafe { &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS) }; + let params = unsafe { &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS) }; + // When maximized, shrink to visible frame so content doesn't extend beyond it. + if unsafe { IsZoomed(hwnd).as_bool() } && !is_effectively_fullscreen(params.rgrc[0]) { let dpi = unsafe { GetDpiForWindow(hwnd) }; let size = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) }; let pad = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) }; @@ -367,19 +367,11 @@ unsafe fn calculate_resize_direction(helper: HWND, lparam: LPARAM) -> Option bool { - if hwnd.is_invalid() { - return false; - } - - let mut view_rect = RECT::default(); - if unsafe { GetWindowRect(hwnd, &mut view_rect) }.is_err() { - return false; - } +fn is_effectively_fullscreen(rect: RECT) -> bool { - let hmon = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + let hmon = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) }; if hmon.is_invalid() { return false; } @@ -394,8 +386,8 @@ fn is_effectively_fullscreen(hwnd: HWND) -> bool { // Allow a tiny tolerance for DPI / rounding issues const EPS: i32 = 1; - (view_rect.left - monitor_info.rcMonitor.left).abs() <= EPS - && (view_rect.top - monitor_info.rcMonitor.top).abs() <= EPS - && (view_rect.right - monitor_info.rcMonitor.right).abs() <= EPS - && (view_rect.bottom - monitor_info.rcMonitor.bottom).abs() <= EPS + (rect.left - monitor_info.rcMonitor.left).abs() <= EPS + && (rect.top - monitor_info.rcMonitor.top).abs() <= EPS + && (rect.right - monitor_info.rcMonitor.right).abs() <= EPS + && (rect.bottom - monitor_info.rcMonitor.bottom).abs() <= EPS } From 45b759fcb808db7bdb7e7c1ca32c59f8e6e8a35b Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 16 Jan 2026 14:37:23 -0800 Subject: [PATCH 19/21] Fix eslint crash --- frontend/src/components/window/TitleBar.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/window/TitleBar.svelte b/frontend/src/components/window/TitleBar.svelte index b3d72faded..6888d419dd 100644 --- a/frontend/src/components/window/TitleBar.svelte +++ b/frontend/src/components/window/TitleBar.svelte @@ -141,4 +141,6 @@ } } } + + // paddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpadding From a63817eb20a9a32b5dfab40d574a6443fe7ebcd3 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 16 Jan 2026 15:18:50 -0800 Subject: [PATCH 20/21] Show correct fullscreen button tooltip shortcut on Mac web version; code review --- desktop/src/window.rs | 7 ++-- desktop/src/window/win/native_handle.rs | 1 - .../src/messages/frontend/frontend_message.rs | 4 +- .../messages/input_mapper/input_mappings.rs | 6 +-- .../input_mapper/utility_types/macros.rs | 2 +- .../portfolio/portfolio_message_handler.rs | 3 +- .../src/components/window/TitleBar.svelte | 2 +- frontend/src/io-managers/input.ts | 38 ++++++++++--------- frontend/src/messages.ts | 6 ++- frontend/src/state-providers/tooltip.ts | 9 +++-- 10 files changed, 42 insertions(+), 36 deletions(-) diff --git a/desktop/src/window.rs b/desktop/src/window.rs index b69a0be2e7..4540e56200 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -1,3 +1,6 @@ +use crate::consts::APP_NAME; +use crate::event::AppEventScheduler; +use crate::wrapper::messages::MenuItem; use std::collections::HashMap; use std::sync::Arc; use winit::cursor::{CursorIcon, CustomCursor, CustomCursorSource}; @@ -5,10 +8,6 @@ use winit::event_loop::ActiveEventLoop; use winit::monitor::Fullscreen; 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 init() {} fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes; diff --git a/desktop/src/window/win/native_handle.rs b/desktop/src/window/win/native_handle.rs index 333477e65b..b0a08fefdf 100644 --- a/desktop/src/window/win/native_handle.rs +++ b/desktop/src/window/win/native_handle.rs @@ -370,7 +370,6 @@ unsafe fn calculate_resize_direction(helper: HWND, lparam: LPARAM) -> Option bool { - let hmon = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) }; if hmon.is_invalid() { return false; diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 912cc205f9..c4c19cfe64 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -64,8 +64,10 @@ pub enum FrontendMessage { #[serde(rename = "nodeTypes")] node_types: Vec, }, - SendShortcutF11 { + SendShortcutFullscreen { shortcut: Option, + #[serde(rename = "shortcutMc")] + shortcut_mac: Option, }, SendShortcutAltClick { shortcut: Option, diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 2a55034642..f0f7d1e8e6 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -58,8 +58,8 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { // // AppWindowMessage entry!(KeyDown(F11); disabled=cfg!(target_os = "macos"), action_dispatch=AppWindowMessage::Fullscreen), - entry!(KeyDown(KeyF); modifiers=[Accel, Control], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Fullscreen), - entry!(KeyDown(KeyQ); modifiers=[Accel], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Close), + entry!(KeyDown(KeyF); modifiers=[Command, Control], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Fullscreen), + entry!(KeyDown(KeyQ); modifiers=[Command], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Close), // // ClipboardMessage entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=ClipboardMessage::Cut), @@ -426,7 +426,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(WheelScroll; modifiers=[Control], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasZoomMouseWheel), entry!(WheelScroll; modifiers=[Command], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasZoomMouseWheel), entry!(WheelScroll; modifiers=[Shift], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }), - entry!(WheelScroll; disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: false }), + entry!(WheelScroll; disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: false }), // On Mac, the OS already converts Shift+scroll into horizontal scrolling so we have to reverse the behavior from normal to produce the same outcome entry!(WheelScroll; modifiers=[Control], disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: keyboard_platform == KeyboardPlatformLayout::Mac }), entry!(WheelScroll; modifiers=[Shift], disabled=!zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: keyboard_platform != KeyboardPlatformLayout::Mac }), diff --git a/editor/src/messages/input_mapper/utility_types/macros.rs b/editor/src/messages/input_mapper/utility_types/macros.rs index f8e406da98..9dae86c9a5 100644 --- a/editor/src/messages/input_mapper/utility_types/macros.rs +++ b/editor/src/messages/input_mapper/utility_types/macros.rs @@ -63,7 +63,6 @@ macro_rules! entry { // Implementation macro to avoid code duplication ($input:expr; $($modifier:ident),*; $($refresh:ident),*; $action_dispatch:expr; $canonical:expr; $disabled:expr) => { - &[&[ // Cause the `action_dispatch` message to be sent when the specified input occurs. MappingEntry { @@ -132,6 +131,7 @@ macro_rules! mapping { if entry.disabled { continue; } + let corresponding_list = match entry.input { InputMapperMessage::KeyDown(key) => &mut key_down[key as usize], InputMapperMessage::KeyUp(key) => &mut key_up[key as usize], diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 2a704f49e4..83e4e35af1 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -117,8 +117,9 @@ impl MessageHandler> for Portfolio }); // Send shortcuts for widgets created in the frontend which need shortcut tooltips - responses.add(FrontendMessage::SendShortcutF11 { + responses.add(FrontendMessage::SendShortcutFullscreen { shortcut: action_shortcut_manual!(Key::F11), + shortcut_mac: action_shortcut_manual!(Key::Control, Key::Command, Key::KeyF), }); responses.add(FrontendMessage::SendShortcutAltClick { shortcut: action_shortcut_manual!(Key::Alt, Key::MouseLeft), diff --git a/frontend/src/components/window/TitleBar.svelte b/frontend/src/components/window/TitleBar.svelte index 6888d419dd..25a724131c 100644 --- a/frontend/src/components/window/TitleBar.svelte +++ b/frontend/src/components/window/TitleBar.svelte @@ -51,7 +51,7 @@ tooltipDescription={$appWindow.platform === "Web" && $fullscreen.keyboardLockApiSupported ? "While fullscreen, keyboard shortcuts normally reserved by the browser become available." : undefined} - tooltipShortcut={$tooltip.f11Shortcut} + tooltipShortcut={$tooltip.fullscreenShortcut} on:click={() => { if (isDesktop()) editor.handle.appWindowFullscreen(); else ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)(); diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 47f2b9e0e1..e830cad007 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -84,28 +84,12 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // Cut, copy, and paste is handled in the backend on desktop if (isDesktop() && accelKey && ["KeyX", "KeyC", "KeyV"].includes(key)) return true; + // But on web, we want to not redirect paste + if (!isDesktop() && key === "KeyV" && accelKey) return false; // Don't redirect user input from text entry into HTML elements if (targetIsTextField(e.target || undefined) && key !== "Escape" && !(accelKey && ["Enter", "NumpadEnter"].includes(key))) return false; - // Don't redirect paste in web - if (key === "KeyV" && accelKey) return false; - - // Don't redirect a fullscreen request on web - if (key === "F11" && e.type === "keydown" && !e.repeat && !isDesktop()) { - e.preventDefault(); - fullscreen.toggleFullscreen(); - return false; - } - - // Don't redirect a reload request - if (key === "F5") return false; - if (key === "KeyR" && accelKey) return false; - - // Don't redirect debugging tools - if (["F12", "F8"].includes(key)) return false; - if (["KeyC", "KeyI", "KeyJ"].includes(key) && accelKey && e.shiftKey) return false; - // Don't redirect tab or enter if not in canvas (to allow navigating elements) potentiallyRestoreCanvasFocus(e); if (!canvasFocused && !targetIsTextField(e.target || undefined) && ["Tab", "Enter", "NumpadEnter", "Space", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp"].includes(key)) return false; @@ -113,6 +97,24 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // Don't redirect if a MenuList is open if (window.document.querySelector("[data-floating-menu-content]")) return false; + // Web-only keyboard shortcuts + if (!isDesktop()) { + // Don't redirect a fullscreen request + if (key === "F11" && e.type === "keydown" && !e.repeat) { + e.preventDefault(); + fullscreen.toggleFullscreen(); + return false; + } + + // Don't redirect a reload request + if (key === "F5") return false; + if (key === "KeyR" && accelKey) return false; + + // Don't redirect debugging tools + if (["F12", "F8"].includes(key)) return false; + if (["KeyC", "KeyI", "KeyJ"].includes(key) && accelKey && e.shiftKey) return false; + } + // Redirect to the backend return true; } diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 788b2a911e..59263aeb94 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -107,9 +107,11 @@ export class SendUIMetadata extends JsMessage { readonly nodeTypes!: FrontendNodeType[]; } -export class SendShortcutF11 extends JsMessage { +export class SendShortcutFullscreen extends JsMessage { @Transform(({ value }: { value: ActionShortcut }) => value || undefined) readonly shortcut!: ActionShortcut | undefined; + @Transform(({ value }: { value: ActionShortcut }) => value || undefined) + readonly shortcutMac!: ActionShortcut | undefined; } export class SendShortcutAltClick extends JsMessage { @@ -1666,7 +1668,7 @@ export const messageMakers: Record = { DisplayEditableTextboxTransform, DisplayRemoveEditableTextbox, SendUIMetadata, - SendShortcutF11, + SendShortcutFullscreen, SendShortcutAltClick, SendShortcutShiftClick, TriggerAboutGraphiteLocalizedCommitDate, diff --git a/frontend/src/state-providers/tooltip.ts b/frontend/src/state-providers/tooltip.ts index 8d6055693f..f3943083f4 100644 --- a/frontend/src/state-providers/tooltip.ts +++ b/frontend/src/state-providers/tooltip.ts @@ -1,7 +1,8 @@ import { writable } from "svelte/store"; import { type Editor } from "@graphite/editor"; -import { SendShortcutAltClick, SendShortcutF11, SendShortcutShiftClick, type ActionShortcut } from "@graphite/messages"; +import { SendShortcutAltClick, SendShortcutFullscreen, SendShortcutShiftClick, type ActionShortcut } from "@graphite/messages"; +import { operatingSystem } from "@graphite/utility-functions/platform"; const SHOW_TOOLTIP_DELAY_MS = 500; @@ -12,7 +13,7 @@ export function createTooltipState(editor: Editor) { position: { x: 0, y: 0 }, shiftClickShortcut: undefined as ActionShortcut | undefined, altClickShortcut: undefined as ActionShortcut | undefined, - f11Shortcut: undefined as ActionShortcut | undefined, + fullscreenShortcut: undefined as ActionShortcut | undefined, }); let tooltipTimeout: ReturnType | undefined = undefined; @@ -63,9 +64,9 @@ export function createTooltipState(editor: Editor) { return state; }); }); - editor.subscriptions.subscribeJsMessage(SendShortcutF11, async (data) => { + editor.subscriptions.subscribeJsMessage(SendShortcutFullscreen, async (data) => { update((state) => { - state.f11Shortcut = data.shortcut; + state.fullscreenShortcut = operatingSystem() === "Mac" ? data.shortcutMac : data.shortcut; return state; }); }); From 05e70213d96a7a8589f7c549028a211804c4b2a5 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 16 Jan 2026 17:07:07 -0800 Subject: [PATCH 21/21] Fix fullscreen on Mac web --- editor/src/messages/frontend/frontend_message.rs | 2 +- editor/src/messages/input_mapper/input_mappings.rs | 5 +++-- frontend/src/io-managers/input.ts | 6 +++--- frontend/src/messages.ts | 3 +++ frontend/src/state-providers/fullscreen.ts | 7 ++++++- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index c4c19cfe64..e5e64720ed 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -66,7 +66,7 @@ pub enum FrontendMessage { }, SendShortcutFullscreen { shortcut: Option, - #[serde(rename = "shortcutMc")] + #[serde(rename = "shortcutMac")] shortcut_mac: Option, }, SendShortcutAltClick { diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index f0f7d1e8e6..6ad22cb71c 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -27,6 +27,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { use InputMapperMessage::*; use Key::*; + // TODO: Fix this failing to load the correct data (and throwing a console warning) because it's occurring before the value has been supplied during initialization from the JS `initAfterFrontendReady` let keyboard_platform = GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout(); // NOTICE: @@ -57,8 +58,8 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping { entry!(KeyDown(KeyZ); modifiers=[Accel, MouseLeft], action_dispatch=DocumentMessage::Noop), // // AppWindowMessage - entry!(KeyDown(F11); disabled=cfg!(target_os = "macos"), action_dispatch=AppWindowMessage::Fullscreen), - entry!(KeyDown(KeyF); modifiers=[Command, Control], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Fullscreen), + entry!(KeyDown(F11); disabled=(keyboard_platform == KeyboardPlatformLayout::Mac), action_dispatch=AppWindowMessage::Fullscreen), + entry!(KeyDown(KeyF); modifiers=[Command, Control], disabled=(keyboard_platform != KeyboardPlatformLayout::Mac), action_dispatch=AppWindowMessage::Fullscreen), entry!(KeyDown(KeyQ); modifiers=[Command], disabled=cfg!(not(target_os = "macos")), action_dispatch=AppWindowMessage::Close), // // ClipboardMessage diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index e830cad007..c606bbe998 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -99,8 +99,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // Web-only keyboard shortcuts if (!isDesktop()) { - // Don't redirect a fullscreen request - if (key === "F11" && e.type === "keydown" && !e.repeat) { + // Don't redirect a fullscreen request, but process it immediately instead + if (((operatingSystem() !== "Mac" && key === "F11") || (operatingSystem() === "Mac" && e.ctrlKey && e.metaKey && key === "KeyF")) && e.type === "keydown" && !e.repeat) { e.preventDefault(); fullscreen.toggleFullscreen(); return false; @@ -241,7 +241,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli } function onMouseDown(e: MouseEvent) { - // Block middle mouse button auto-scroll mode (the circlar gizmo that appears and allows quick scrolling by moving the cursor above or below it) + // Block middle mouse button auto-scroll mode (the circular gizmo that appears and allows quick scrolling by moving the cursor above or below it) if (e.button === BUTTON_MIDDLE) e.preventDefault(); } diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 59263aeb94..5be4a1eea7 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -337,6 +337,8 @@ export class WindowPointerLockMove extends JsMessage { readonly y!: number; } +export class WindowFullscreen extends JsMessage {} + // Rust enum `Key` export type KeyRaw = string; // 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 @@ -1736,6 +1738,7 @@ export const messageMakers: Record = { UpdateMaximized, UpdateFullscreen, WindowPointerLockMove, + WindowFullscreen, UpdatePropertiesPanelLayout, UpdatePropertiesPanelState, UpdateStatusBarHintsLayout, diff --git a/frontend/src/state-providers/fullscreen.ts b/frontend/src/state-providers/fullscreen.ts index 1378864e04..bde7b42cd7 100644 --- a/frontend/src/state-providers/fullscreen.ts +++ b/frontend/src/state-providers/fullscreen.ts @@ -1,8 +1,9 @@ import { writable } from "svelte/store"; import { type Editor } from "@graphite/editor"; +import { WindowFullscreen } from "@graphite/messages"; -export function createFullscreenState(_: Editor) { +export function createFullscreenState(editor: Editor) { // Experimental Keyboard API: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/keyboard // eslint-disable-next-line @typescript-eslint/no-explicit-any const keyboardLockApiSupported: Readonly = "keyboard" in navigator && (navigator as any).keyboard && "lock" in (navigator as any).keyboard; @@ -50,6 +51,10 @@ export function createFullscreenState(_: Editor) { }); } + editor.subscriptions.subscribeJsMessage(WindowFullscreen, () => { + toggleFullscreen(); + }); + return { subscribe, fullscreenModeChanged,