Desktop: Add fullscreen window mode (#3625)
This commit is contained in:
parent
fd0addf61c
commit
07fbcd489c
|
|
@ -352,6 +352,11 @@ impl App {
|
||||||
window.toggle_maximize();
|
window.toggle_maximize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DesktopFrontendMessage::WindowFullscreen => {
|
||||||
|
if let Some(window) = &mut self.window {
|
||||||
|
window.toggle_fullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
DesktopFrontendMessage::WindowDrag => {
|
DesktopFrontendMessage::WindowDrag => {
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
window.start_drag();
|
window.start_drag();
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
|
use crate::consts::APP_NAME;
|
||||||
|
use crate::event::AppEventScheduler;
|
||||||
|
use crate::wrapper::messages::MenuItem;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use winit::cursor::{CursorIcon, CustomCursor, CustomCursorSource};
|
use winit::cursor::{CursorIcon, CustomCursor, CustomCursorSource};
|
||||||
use winit::event_loop::ActiveEventLoop;
|
use winit::event_loop::ActiveEventLoop;
|
||||||
|
use winit::monitor::Fullscreen;
|
||||||
use winit::window::{Window as WinitWindow, WindowAttributes};
|
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 {
|
pub(crate) trait NativeWindow {
|
||||||
fn init() {}
|
fn init() {}
|
||||||
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes;
|
fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes;
|
||||||
|
|
@ -111,6 +111,9 @@ impl Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn toggle_maximize(&self) {
|
pub(crate) fn toggle_maximize(&self) {
|
||||||
|
if self.is_fullscreen() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.winit_window.set_maximized(!self.winit_window.is_maximized());
|
self.winit_window.set_maximized(!self.winit_window.is_maximized());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,11 +121,22 @@ impl Window {
|
||||||
self.winit_window.is_maximized()
|
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 {
|
pub(crate) fn is_fullscreen(&self) -> bool {
|
||||||
self.winit_window.fullscreen().is_some()
|
self.winit_window.fullscreen().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn start_drag(&self) {
|
pub(crate) fn start_drag(&self) {
|
||||||
|
if self.is_fullscreen() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let _ = self.winit_window.drag_window();
|
let _ = self.winit_window.drag_window();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// 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 {
|
unsafe extern "system" fn main_window_handle_message(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||||
if msg == WM_NCCALCSIZE && wparam.0 != 0 {
|
if msg == WM_NCCALCSIZE && wparam.0 != 0 {
|
||||||
// When maximized, shrink to visible frame so content doesn't extend beyond it.
|
let params = unsafe { &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS) };
|
||||||
if unsafe { IsZoomed(hwnd).as_bool() } {
|
|
||||||
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 dpi = unsafe { GetDpiForWindow(hwnd) };
|
||||||
let size = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) };
|
let size = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) };
|
||||||
let pad = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
|
let pad = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
|
||||||
|
|
@ -366,3 +366,27 @@ unsafe fn calculate_resize_direction(helper: HWND, lparam: LPARAM) -> Option<u32
|
||||||
_ => None,
|
_ => 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::<MONITORINFO>() 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
|
||||||
FrontendMessage::WindowMaximize => {
|
FrontendMessage::WindowMaximize => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::WindowMaximize);
|
dispatcher.respond(DesktopFrontendMessage::WindowMaximize);
|
||||||
}
|
}
|
||||||
|
FrontendMessage::WindowFullscreen => {
|
||||||
|
dispatcher.respond(DesktopFrontendMessage::WindowFullscreen);
|
||||||
|
}
|
||||||
FrontendMessage::WindowDrag => {
|
FrontendMessage::WindowDrag => {
|
||||||
dispatcher.respond(DesktopFrontendMessage::WindowDrag);
|
dispatcher.respond(DesktopFrontendMessage::WindowDrag);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ pub enum DesktopFrontendMessage {
|
||||||
WindowClose,
|
WindowClose,
|
||||||
WindowMinimize,
|
WindowMinimize,
|
||||||
WindowMaximize,
|
WindowMaximize,
|
||||||
|
WindowFullscreen,
|
||||||
WindowDrag,
|
WindowDrag,
|
||||||
WindowHide,
|
WindowHide,
|
||||||
WindowHideOthers,
|
WindowHideOthers,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ pub enum AppWindowMessage {
|
||||||
Close,
|
Close,
|
||||||
Minimize,
|
Minimize,
|
||||||
Maximize,
|
Maximize,
|
||||||
|
Fullscreen,
|
||||||
Drag,
|
Drag,
|
||||||
Hide,
|
Hide,
|
||||||
HideOthers,
|
HideOthers,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
||||||
AppWindowMessage::Maximize => {
|
AppWindowMessage::Maximize => {
|
||||||
responses.add(FrontendMessage::WindowMaximize);
|
responses.add(FrontendMessage::WindowMaximize);
|
||||||
}
|
}
|
||||||
|
AppWindowMessage::Fullscreen => {
|
||||||
|
responses.add(FrontendMessage::WindowFullscreen);
|
||||||
|
}
|
||||||
AppWindowMessage::Drag => {
|
AppWindowMessage::Drag => {
|
||||||
responses.add(FrontendMessage::WindowDrag);
|
responses.add(FrontendMessage::WindowDrag);
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +51,7 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
||||||
Close,
|
Close,
|
||||||
Minimize,
|
Minimize,
|
||||||
Maximize,
|
Maximize,
|
||||||
|
Fullscreen,
|
||||||
Drag,
|
Drag,
|
||||||
Hide,
|
Hide,
|
||||||
HideOthers,
|
HideOthers,
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,10 @@ pub enum FrontendMessage {
|
||||||
#[serde(rename = "nodeTypes")]
|
#[serde(rename = "nodeTypes")]
|
||||||
node_types: Vec<FrontendNodeType>,
|
node_types: Vec<FrontendNodeType>,
|
||||||
},
|
},
|
||||||
SendShortcutF11 {
|
SendShortcutFullscreen {
|
||||||
shortcut: Option<ActionShortcut>,
|
shortcut: Option<ActionShortcut>,
|
||||||
|
#[serde(rename = "shortcutMac")]
|
||||||
|
shortcut_mac: Option<ActionShortcut>,
|
||||||
},
|
},
|
||||||
SendShortcutAltClick {
|
SendShortcutAltClick {
|
||||||
shortcut: Option<ActionShortcut>,
|
shortcut: Option<ActionShortcut>,
|
||||||
|
|
@ -371,6 +373,7 @@ pub enum FrontendMessage {
|
||||||
WindowClose,
|
WindowClose,
|
||||||
WindowMinimize,
|
WindowMinimize,
|
||||||
WindowMaximize,
|
WindowMaximize,
|
||||||
|
WindowFullscreen,
|
||||||
WindowDrag,
|
WindowDrag,
|
||||||
WindowHide,
|
WindowHide,
|
||||||
WindowHideOthers,
|
WindowHideOthers,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ use crate::messages::input_mapper::utility_types::input_keyboard::{self, Key};
|
||||||
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
||||||
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
|
use crate::messages::portfolio::utility_types::KeyboardPlatformLayout;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
#[derive(ExtractField)]
|
#[derive(ExtractField)]
|
||||||
pub struct InputMapperMessageContext<'a> {
|
pub struct InputMapperMessageContext<'a> {
|
||||||
|
|
@ -34,27 +33,6 @@ impl InputMapperMessageHandler {
|
||||||
self.mapping = mapping;
|
self.mapping = mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hints(&self, actions: ActionList) -> String {
|
|
||||||
let mut output = String::new();
|
|
||||||
let mut actions = actions
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.filter(|a| !matches!(*a, MessageDiscriminant::Tool(ToolMessageDiscriminant::ActivateTool) | MessageDiscriminant::Debug(_)));
|
|
||||||
self.mapping
|
|
||||||
.key_down
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(i, m)| {
|
|
||||||
let ma = m.0.iter().find_map(|m| actions.find_map(|a| (a == m.action.to_discriminant()).then(|| m.action.to_discriminant())));
|
|
||||||
|
|
||||||
ma.map(|a| ((i as u8).try_into().unwrap(), a))
|
|
||||||
})
|
|
||||||
.for_each(|(k, a): (Key, _)| {
|
|
||||||
let _ = write!(output, "{}: {}, ", k.to_discriminant().local_name(), a.local_name().split('.').next_back().unwrap());
|
|
||||||
});
|
|
||||||
output.replace("Key", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Option<KeysGroup> {
|
pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Option<KeysGroup> {
|
||||||
let all_key_mapping_entries = std::iter::empty()
|
let all_key_mapping_entries = std::iter::empty()
|
||||||
.chain(self.mapping.key_up.iter())
|
.chain(self.mapping.key_up.iter())
|
||||||
|
|
|
||||||
|
|
@ -17,16 +17,19 @@ use glam::DVec2;
|
||||||
impl From<MappingVariant> for Mapping {
|
impl From<MappingVariant> for Mapping {
|
||||||
fn from(value: MappingVariant) -> Self {
|
fn from(value: MappingVariant) -> Self {
|
||||||
match value {
|
match value {
|
||||||
MappingVariant::Default => input_mappings(),
|
MappingVariant::Default => input_mappings(false),
|
||||||
MappingVariant::ZoomWithScroll => zoom_with_scroll(),
|
MappingVariant::ZoomWithScroll => input_mappings(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn input_mappings() -> Mapping {
|
pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
|
||||||
use InputMapperMessage::*;
|
use InputMapperMessage::*;
|
||||||
use Key::*;
|
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:
|
// NOTICE:
|
||||||
// If a new mapping you added here isn't working (and perhaps another lower-precedence one is instead), make sure to advertise
|
// 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`).
|
// 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)
|
// 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),
|
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
|
// ClipboardMessage
|
||||||
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=ClipboardMessage::Cut),
|
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=ClipboardMessage::Cut),
|
||||||
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=ClipboardMessage::Copy),
|
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(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(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!(KeyDown(Minus); modifiers=[Accel], action_dispatch=NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }),
|
||||||
entry!(WheelScroll; modifiers=[Control], action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
|
entry!(WheelScroll; modifiers=[Control], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
|
||||||
entry!(WheelScroll; modifiers=[Command], action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
|
entry!(WheelScroll; modifiers=[Command], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
|
||||||
entry!(WheelScroll; modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }),
|
entry!(WheelScroll; modifiers=[Shift], disabled=zoom_with_scroll, action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }),
|
||||||
entry!(WheelScroll; 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 }),
|
||||||
|
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(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(PageDown); modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanByViewportFraction { delta: DVec2::new(-1., 0.) }),
|
||||||
entry!(KeyDown(PageUp); action_dispatch=NavigationMessage::CanvasPanByViewportFraction { delta: DVec2::new(0., 1.) }),
|
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 `pointer_shake`
|
||||||
sort(&mut pointer_shake);
|
sort(&mut pointer_shake);
|
||||||
|
|
||||||
let mut mapping = Mapping {
|
Mapping {
|
||||||
key_up,
|
key_up,
|
||||||
key_down,
|
key_down,
|
||||||
key_up_no_repeat,
|
key_up_no_repeat,
|
||||||
|
|
@ -480,54 +492,5 @@ pub fn input_mappings() -> Mapping {
|
||||||
wheel_scroll,
|
wheel_scroll,
|
||||||
pointer_move,
|
pointer_move,
|
||||||
pointer_shake,
|
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<Item = &'a [&'a [MappingEntry; N]; M]>,
|
|
||||||
add: impl IntoIterator<Item = &'a [&'a [MappingEntry; X]; Y]>,
|
|
||||||
) {
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// 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 {
|
macro_rules! entry {
|
||||||
// Pattern with canonical parameter
|
// 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
|
// 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
|
// 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.
|
// Cause the `action_dispatch` message to be sent when the specified input occurs.
|
||||||
MappingEntry {
|
MappingEntry {
|
||||||
|
|
@ -43,33 +70,37 @@ macro_rules! entry {
|
||||||
input: $input,
|
input: $input,
|
||||||
modifiers: modifiers!($($modifier),*),
|
modifiers: modifiers!($($modifier),*),
|
||||||
canonical: $canonical,
|
canonical: $canonical,
|
||||||
|
disabled: $disabled,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Also cause the `action_dispatch` message to be sent when any of the specified refresh keys change.
|
|
||||||
$(
|
$(
|
||||||
MappingEntry {
|
MappingEntry {
|
||||||
action: $action_dispatch.into(),
|
action: $action_dispatch.into(),
|
||||||
input: InputMapperMessage::KeyDown(Key::$refresh),
|
input: InputMapperMessage::KeyDown(Key::$refresh),
|
||||||
modifiers: modifiers!(),
|
modifiers: modifiers!(),
|
||||||
canonical: $canonical,
|
canonical: $canonical,
|
||||||
|
disabled: $disabled,
|
||||||
},
|
},
|
||||||
MappingEntry {
|
MappingEntry {
|
||||||
action: $action_dispatch.into(),
|
action: $action_dispatch.into(),
|
||||||
input: InputMapperMessage::KeyUp(Key::$refresh),
|
input: InputMapperMessage::KeyUp(Key::$refresh),
|
||||||
modifiers: modifiers!(),
|
modifiers: modifiers!(),
|
||||||
canonical: $canonical,
|
canonical: $canonical,
|
||||||
|
disabled: $disabled,
|
||||||
},
|
},
|
||||||
MappingEntry {
|
MappingEntry {
|
||||||
action: $action_dispatch.into(),
|
action: $action_dispatch.into(),
|
||||||
input: InputMapperMessage::KeyDownNoRepeat(Key::$refresh),
|
input: InputMapperMessage::KeyDownNoRepeat(Key::$refresh),
|
||||||
modifiers: modifiers!(),
|
modifiers: modifiers!(),
|
||||||
canonical: $canonical,
|
canonical: $canonical,
|
||||||
|
disabled: $disabled,
|
||||||
},
|
},
|
||||||
MappingEntry {
|
MappingEntry {
|
||||||
action: $action_dispatch.into(),
|
action: $action_dispatch.into(),
|
||||||
input: InputMapperMessage::KeyUpNoRepeat(Key::$refresh),
|
input: InputMapperMessage::KeyUpNoRepeat(Key::$refresh),
|
||||||
modifiers: modifiers!(),
|
modifiers: modifiers!(),
|
||||||
canonical: $canonical,
|
canonical: $canonical,
|
||||||
|
disabled: $disabled,
|
||||||
},
|
},
|
||||||
)*
|
)*
|
||||||
]]
|
]]
|
||||||
|
|
@ -97,6 +128,10 @@ macro_rules! mapping {
|
||||||
for entry_slice in $entry {
|
for entry_slice in $entry {
|
||||||
// Each entry in the slice (usually just one, except when `refresh_keys` adds additional key entries)
|
// Each entry in the slice (usually just one, except when `refresh_keys` adds additional key entries)
|
||||||
for entry in entry_slice.into_iter() {
|
for entry in entry_slice.into_iter() {
|
||||||
|
if entry.disabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let corresponding_list = match entry.input {
|
let corresponding_list = match entry.input {
|
||||||
InputMapperMessage::KeyDown(key) => &mut key_down[key as usize],
|
InputMapperMessage::KeyDown(key) => &mut key_down[key as usize],
|
||||||
InputMapperMessage::KeyUp(key) => &mut key_up[key as usize],
|
InputMapperMessage::KeyUp(key) => &mut key_up[key as usize],
|
||||||
|
|
|
||||||
|
|
@ -29,16 +29,6 @@ impl Mapping {
|
||||||
list.match_mapping(keyboard_state, actions)
|
list.match_mapping(keyboard_state, actions)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, target_entry: &MappingEntry) {
|
|
||||||
let list = self.associated_entries_mut(&target_entry.input);
|
|
||||||
list.remove(target_entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&mut self, new_entry: MappingEntry) {
|
|
||||||
let list = self.associated_entries_mut(&new_entry.input);
|
|
||||||
list.push(new_entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn associated_entries(&self, message: &InputMapperMessage) -> &KeyMappingEntries {
|
fn associated_entries(&self, message: &InputMapperMessage) -> &KeyMappingEntries {
|
||||||
match message {
|
match message {
|
||||||
InputMapperMessage::KeyDown(key) => &self.key_down[*key as usize],
|
InputMapperMessage::KeyDown(key) => &self.key_down[*key as usize],
|
||||||
|
|
@ -51,19 +41,6 @@ impl Mapping {
|
||||||
InputMapperMessage::PointerShake => &self.pointer_shake,
|
InputMapperMessage::PointerShake => &self.pointer_shake,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn associated_entries_mut(&mut self, message: &InputMapperMessage) -> &mut KeyMappingEntries {
|
|
||||||
match message {
|
|
||||||
InputMapperMessage::KeyDown(key) => &mut self.key_down[*key as usize],
|
|
||||||
InputMapperMessage::KeyUp(key) => &mut self.key_up[*key as usize],
|
|
||||||
InputMapperMessage::KeyDownNoRepeat(key) => &mut self.key_down_no_repeat[*key as usize],
|
|
||||||
InputMapperMessage::KeyUpNoRepeat(key) => &mut self.key_up_no_repeat[*key as usize],
|
|
||||||
InputMapperMessage::DoubleClick(key) => &mut self.double_click[*key as usize],
|
|
||||||
InputMapperMessage::WheelScroll => &mut self.wheel_scroll,
|
|
||||||
InputMapperMessage::PointerMove => &mut self.pointer_move,
|
|
||||||
InputMapperMessage::PointerShake => &mut self.pointer_shake,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -125,6 +102,8 @@ pub struct MappingEntry {
|
||||||
pub modifiers: KeyStates,
|
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
|
/// 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,
|
pub canonical: bool,
|
||||||
|
/// Whether this mapping is disabled
|
||||||
|
pub disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||||
|
|
|
||||||
|
|
@ -621,6 +621,13 @@ impl LayoutHolder for MenuBarMessageHandler {
|
||||||
.label("Window")
|
.label("Window")
|
||||||
.flush(true)
|
.flush(true)
|
||||||
.menu_list_children(vec![
|
.menu_list_children(vec![
|
||||||
|
vec![
|
||||||
|
MenuListEntry::new("Fullscreen")
|
||||||
|
.label("Fullscreen")
|
||||||
|
.icon("FullscreenEnter")
|
||||||
|
.tooltip_shortcut(action_shortcut!(AppWindowMessageDiscriminant::Fullscreen))
|
||||||
|
.on_commit(|_| AppWindowMessage::Fullscreen.into()),
|
||||||
|
],
|
||||||
vec![
|
vec![
|
||||||
MenuListEntry::new("Properties")
|
MenuListEntry::new("Properties")
|
||||||
.label("Properties")
|
.label("Properties")
|
||||||
|
|
@ -632,8 +639,6 @@ impl LayoutHolder for MenuBarMessageHandler {
|
||||||
.icon(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
|
.icon(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
|
||||||
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen))
|
.tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen))
|
||||||
.on_commit(|_| PortfolioMessage::ToggleLayersPanelOpen.into()),
|
.on_commit(|_| PortfolioMessage::ToggleLayersPanelOpen.into()),
|
||||||
],
|
|
||||||
vec![
|
|
||||||
MenuListEntry::new("Data")
|
MenuListEntry::new("Data")
|
||||||
.label("Data")
|
.label("Data")
|
||||||
.icon(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
|
.icon(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" })
|
||||||
|
|
|
||||||
|
|
@ -117,8 +117,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send shortcuts for widgets created in the frontend which need shortcut tooltips
|
// 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: action_shortcut_manual!(Key::F11),
|
||||||
|
shortcut_mac: action_shortcut_manual!(Key::Control, Key::Command, Key::KeyF),
|
||||||
});
|
});
|
||||||
responses.add(FrontendMessage::SendShortcutAltClick {
|
responses.add(FrontendMessage::SendShortcutAltClick {
|
||||||
shortcut: action_shortcut_manual!(Key::Alt, Key::MouseLeft),
|
shortcut: action_shortcut_manual!(Key::Alt, Key::MouseLeft),
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||||
|
import { isDesktop } from "/src/utility-functions/platform";
|
||||||
|
|
||||||
const appWindow = getContext<AppWindowState>("appWindow");
|
const appWindow = getContext<AppWindowState>("appWindow");
|
||||||
const editor = getContext<Editor>("editor");
|
const editor = getContext<Editor>("editor");
|
||||||
|
|
@ -19,7 +20,8 @@
|
||||||
|
|
||||||
let menuBarLayout: Layout = [];
|
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
|
// 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;
|
$: height = $appWindow.platform === "Mac" ? 28 * (1 / $appWindow.uiScale) : 28;
|
||||||
|
|
||||||
|
|
@ -39,20 +41,23 @@
|
||||||
{/if}
|
{/if}
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<!-- Window frame -->
|
<!-- Window frame -->
|
||||||
<LayoutRow class="window-frame" on:mousedown={() => editor.handle.appWindowDrag()} on:dblclick={() => editor.handle.appWindowMaximize()} />
|
<LayoutRow class="window-frame" on:mousedown={() => !isFullscreen && editor.handle.appWindowDrag()} on:dblclick={() => !isFullscreen && editor.handle.appWindowMaximize()} />
|
||||||
<!-- Window buttons -->
|
<!-- Window buttons -->
|
||||||
<LayoutRow class="window-buttons" classes={{ fullscreen: showFullscreenButton, windows: $appWindow.platform === "Windows", linux: $appWindow.platform === "Linux" }}>
|
<LayoutRow class="window-buttons" classes={{ fullscreen: showFullscreenButton, windows: $appWindow.platform === "Windows", linux: $appWindow.platform === "Linux" }}>
|
||||||
{#if $appWindow.platform !== "Mac"}
|
{#if $appWindow.platform !== "Mac"}
|
||||||
{#if showFullscreenButton}
|
{#if showFullscreenButton}
|
||||||
<LayoutRow
|
<LayoutRow
|
||||||
tooltipLabel={$fullscreen.windowFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
|
tooltipLabel={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
|
||||||
tooltipDescription={$appWindow.platform === "Web" && $fullscreen.keyboardLockApiSupported
|
tooltipDescription={$appWindow.platform === "Web" && $fullscreen.keyboardLockApiSupported
|
||||||
? "While fullscreen, keyboard shortcuts normally reserved by the browser become available."
|
? "While fullscreen, keyboard shortcuts normally reserved by the browser become available."
|
||||||
: undefined}
|
: undefined}
|
||||||
tooltipShortcut={$tooltip.f11Shortcut}
|
tooltipShortcut={$tooltip.fullscreenShortcut}
|
||||||
on:click={() => ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)()}
|
on:click={() => {
|
||||||
|
if (isDesktop()) editor.handle.appWindowFullscreen();
|
||||||
|
else ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<IconLabel icon={$fullscreen.windowFullscreen ? "FullscreenExit" : "FullscreenEnter"} />
|
<IconLabel icon={isFullscreen ? "FullscreenExit" : "FullscreenEnter"} />
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
{:else}
|
{:else}
|
||||||
<LayoutRow tooltipLabel="Minimize" on:click={() => editor.handle.appWindowMinimize()}>
|
<LayoutRow tooltipLabel="Minimize" on:click={() => editor.handle.appWindowMinimize()}>
|
||||||
|
|
@ -115,7 +120,7 @@
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.windows > .layout-row {
|
&.windows:not(.fullscreen) > .layout-row {
|
||||||
padding: 0 17px;
|
padding: 0 17px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
@ -127,7 +132,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.linux > .layout-row {
|
&.linux:not(.fullscreen) > .layout-row {
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
@ -136,4 +141,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// paddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpaddingpadding
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -84,28 +84,12 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
|
|
||||||
// Cut, copy, and paste is handled in the backend on desktop
|
// Cut, copy, and paste is handled in the backend on desktop
|
||||||
if (isDesktop() && accelKey && ["KeyX", "KeyC", "KeyV"].includes(key)) return true;
|
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
|
// 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;
|
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)
|
// Don't redirect tab or enter if not in canvas (to allow navigating elements)
|
||||||
potentiallyRestoreCanvasFocus(e);
|
potentiallyRestoreCanvasFocus(e);
|
||||||
if (!canvasFocused && !targetIsTextField(e.target || undefined) && ["Tab", "Enter", "NumpadEnter", "Space", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp"].includes(key)) return false;
|
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
|
// Don't redirect if a MenuList is open
|
||||||
if (window.document.querySelector("[data-floating-menu-content]")) return false;
|
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
|
// Redirect to the backend
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +241,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseDown(e: MouseEvent) {
|
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();
|
if (e.button === BUTTON_MIDDLE) e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,11 @@ export class SendUIMetadata extends JsMessage {
|
||||||
readonly nodeTypes!: FrontendNodeType[];
|
readonly nodeTypes!: FrontendNodeType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SendShortcutF11 extends JsMessage {
|
export class SendShortcutFullscreen extends JsMessage {
|
||||||
@Transform(({ value }: { value: ActionShortcut }) => value || undefined)
|
@Transform(({ value }: { value: ActionShortcut }) => value || undefined)
|
||||||
readonly shortcut!: ActionShortcut | undefined;
|
readonly shortcut!: ActionShortcut | undefined;
|
||||||
|
@Transform(({ value }: { value: ActionShortcut }) => value || undefined)
|
||||||
|
readonly shortcutMac!: ActionShortcut | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SendShortcutAltClick extends JsMessage {
|
export class SendShortcutAltClick extends JsMessage {
|
||||||
|
|
@ -335,6 +337,8 @@ export class WindowPointerLockMove extends JsMessage {
|
||||||
readonly y!: number;
|
readonly y!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class WindowFullscreen extends JsMessage {}
|
||||||
|
|
||||||
// Rust enum `Key`
|
// Rust enum `Key`
|
||||||
export type KeyRaw = string;
|
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
|
// 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<string, MessageMaker> = {
|
||||||
DisplayEditableTextboxTransform,
|
DisplayEditableTextboxTransform,
|
||||||
DisplayRemoveEditableTextbox,
|
DisplayRemoveEditableTextbox,
|
||||||
SendUIMetadata,
|
SendUIMetadata,
|
||||||
SendShortcutF11,
|
SendShortcutFullscreen,
|
||||||
SendShortcutAltClick,
|
SendShortcutAltClick,
|
||||||
SendShortcutShiftClick,
|
SendShortcutShiftClick,
|
||||||
TriggerAboutGraphiteLocalizedCommitDate,
|
TriggerAboutGraphiteLocalizedCommitDate,
|
||||||
|
|
@ -1734,6 +1738,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||||
UpdateMaximized,
|
UpdateMaximized,
|
||||||
UpdateFullscreen,
|
UpdateFullscreen,
|
||||||
WindowPointerLockMove,
|
WindowPointerLockMove,
|
||||||
|
WindowFullscreen,
|
||||||
UpdatePropertiesPanelLayout,
|
UpdatePropertiesPanelLayout,
|
||||||
UpdatePropertiesPanelState,
|
UpdatePropertiesPanelState,
|
||||||
UpdateStatusBarHintsLayout,
|
UpdateStatusBarHintsLayout,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
import { type Editor } from "@graphite/editor";
|
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
|
// Experimental Keyboard API: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/keyboard
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const keyboardLockApiSupported: Readonly<boolean> = "keyboard" in navigator && (navigator as any).keyboard && "lock" in (navigator as any).keyboard;
|
const keyboardLockApiSupported: Readonly<boolean> = "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 {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
fullscreenModeChanged,
|
fullscreenModeChanged,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
import { type Editor } from "@graphite/editor";
|
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;
|
const SHOW_TOOLTIP_DELAY_MS = 500;
|
||||||
|
|
||||||
|
|
@ -12,7 +13,7 @@ export function createTooltipState(editor: Editor) {
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
shiftClickShortcut: undefined as ActionShortcut | undefined,
|
shiftClickShortcut: undefined as ActionShortcut | undefined,
|
||||||
altClickShortcut: undefined as ActionShortcut | undefined,
|
altClickShortcut: undefined as ActionShortcut | undefined,
|
||||||
f11Shortcut: undefined as ActionShortcut | undefined,
|
fullscreenShortcut: undefined as ActionShortcut | undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
let tooltipTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
let tooltipTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||||
|
|
@ -63,9 +64,9 @@ export function createTooltipState(editor: Editor) {
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(SendShortcutF11, async (data) => {
|
editor.subscriptions.subscribeJsMessage(SendShortcutFullscreen, async (data) => {
|
||||||
update((state) => {
|
update((state) => {
|
||||||
state.f11Shortcut = data.shortcut;
|
state.fullscreenShortcut = operatingSystem() === "Mac" ? data.shortcutMac : data.shortcut;
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,12 @@ impl EditorHandle {
|
||||||
self.dispatch(message);
|
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
|
/// Closes the application window
|
||||||
#[wasm_bindgen(js_name = appWindowClose)]
|
#[wasm_bindgen(js_name = appWindowClose)]
|
||||||
pub fn app_window_close(&self) {
|
pub fn app_window_close(&self) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue