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..ca7e9dc048 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -2,6 +2,7 @@ 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; @@ -118,6 +119,16 @@ impl Window { self.winit_window.is_maximized() } + 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))); + } + } + 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..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 0f47b6f5f7..e1f5694cae 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -372,6 +372,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..2a55034642 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`). @@ -54,6 +56,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=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), + // // ClipboardMessage entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=ClipboardMessage::Cut), entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=ClipboardMessage::Copy), @@ -416,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.) }), @@ -471,7 +482,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 +491,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..f8e406da98 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, + $(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 +71,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 +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.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/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/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 05d7cc8b31..47f2b9e0e1 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; 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) {