diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 1648616f29..e3a3970c85 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) = &mut 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..4540e56200 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -1,13 +1,13 @@ +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}; 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; @@ -111,6 +111,9 @@ impl Window { } pub(crate) fn toggle_maximize(&self) { + if self.is_fullscreen() { + return; + } self.winit_window.set_maximized(!self.winit_window.is_maximized()); } @@ -118,11 +121,22 @@ impl Window { self.winit_window.is_maximized() } + pub(crate) fn toggle_fullscreen(&mut 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() } pub(crate) fn start_drag(&self) { + if self.is_fullscreen() { + return; + } let _ = self.winit_window.drag_window(); } diff --git a/desktop/src/window/win/native_handle.rs b/desktop/src/window/win/native_handle.rs index 228324dc0f..b0a08fefdf 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() } { - 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) }; @@ -366,3 +366,27 @@ unsafe fn calculate_resize_direction(helper: HWND, lparam: LPARAM) -> Option None, } } + +// Check if the rect is effectively fullscreen, meaning it would cover 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(rect: RECT) -> bool { + let hmon = unsafe { MonitorFromRect(&rect, 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; + (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 +} 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..66c4ef0bab 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); } @@ -48,6 +51,7 @@ impl MessageHandler for AppWindowMessageHandler { Close, Minimize, Maximize, + Fullscreen, Drag, Hide, HideOthers, diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index e6c724069f..e5e64720ed 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 = "shortcutMac")] + shortcut_mac: Option, }, SendShortcutAltClick { shortcut: Option, @@ -371,6 +373,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..6ad22cb71c 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -17,16 +17,19 @@ 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::*; + // 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: // 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`). @@ -54,6 +57,11 @@ 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); 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 entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=ClipboardMessage::Cut), entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=ClipboardMessage::Copy), @@ -416,10 +424,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.) }), @@ -471,7 +483,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, @@ -480,54 +492,5 @@ 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 -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 bd0354d280..9dae86c9a5 100644 --- a/editor/src/messages/input_mapper/utility_types/macros.rs +++ b/editor/src/messages/input_mapper/utility_types/macros.rs @@ -25,17 +25,44 @@ 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, + $(disabled=$disabled:expr,)? + action_dispatch=$action_dispatch:expr_2021$(,)? + ) => { + entry!( + $input; + $($($modifier),*)?; + $($($refresh),*)?; + $action_dispatch; + true; + false $( || $disabled )? + ) }; // 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),* $(,)?],)? + $(disabled=$disabled:expr,)? + action_dispatch=$action_dispatch:expr_2021$(,)? + ) => { + entry!( + $input; + $($($modifier),*)?; + $($($refresh),*)?; + $action_dispatch; + false; + false $( || $disabled )? + ) }; // 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; $disabled:expr) => { &[&[ // Cause the `action_dispatch` message to be sent when the specified input occurs. MappingEntry { @@ -43,33 +70,37 @@ macro_rules! entry { input: $input, modifiers: modifiers!($($modifier),*), canonical: $canonical, + disabled: $disabled, }, - // 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, + disabled: $disabled, }, MappingEntry { action: $action_dispatch.into(), input: InputMapperMessage::KeyUp(Key::$refresh), modifiers: modifiers!(), canonical: $canonical, + disabled: $disabled, }, MappingEntry { action: $action_dispatch.into(), input: InputMapperMessage::KeyDownNoRepeat(Key::$refresh), modifiers: modifiers!(), canonical: $canonical, + disabled: $disabled, }, MappingEntry { action: $action_dispatch.into(), input: InputMapperMessage::KeyUpNoRepeat(Key::$refresh), modifiers: modifiers!(), canonical: $canonical, + disabled: $disabled, }, )* ]] @@ -97,6 +128,10 @@ 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.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/input_mapper/utility_types/misc.rs b/editor/src/messages/input_mapper/utility_types/misc.rs index 1f8be1056e..bd0f4187be 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 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/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 24a1716fab..25a724131c 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; @@ -39,20 +41,23 @@ {/if} - editor.handle.appWindowDrag()} on:dblclick={() => editor.handle.appWindowMaximize()} /> + !isFullscreen && editor.handle.appWindowDrag()} on:dblclick={() => !isFullscreen && editor.handle.appWindowMaximize()} /> {#if $appWindow.platform !== "Mac"} {#if showFullscreenButton} ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)()} + tooltipShortcut={$tooltip.fullscreenShortcut} + on:click={() => { + if (isDesktop()) editor.handle.appWindowFullscreen(); + else ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)(); + }} > - + {:else} editor.handle.appWindowMinimize()}> @@ -115,7 +120,7 @@ padding: 0 8px; } - &.windows > .layout-row { + &.windows:not(.fullscreen) > .layout-row { padding: 0 17px; &:hover { @@ -127,7 +132,7 @@ } } - &.linux > .layout-row { + &.linux:not(.fullscreen) > .layout-row { padding: 0 12px; &:hover { @@ -136,4 +141,6 @@ } } } + + // paddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpadding diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 05d7cc8b31..c606bbe998 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 - 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; - // 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, 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; + } + + // 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; } @@ -239,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 788b2a911e..5be4a1eea7 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 { @@ -335,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 @@ -1666,7 +1670,7 @@ export const messageMakers: Record = { DisplayEditableTextboxTransform, DisplayRemoveEditableTextbox, SendUIMetadata, - SendShortcutF11, + SendShortcutFullscreen, SendShortcutAltClick, SendShortcutShiftClick, TriggerAboutGraphiteLocalizedCommitDate, @@ -1734,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, 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; }); }); 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) {