Desktop: Add fullscreen window mode (#3625)
This commit is contained in:
parent
fd0addf61c
commit
07fbcd489c
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) };
|
||||
|
||||
// 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<u32
|
|||
_ => 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 => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WindowMaximize);
|
||||
}
|
||||
FrontendMessage::WindowFullscreen => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WindowFullscreen);
|
||||
}
|
||||
FrontendMessage::WindowDrag => {
|
||||
dispatcher.respond(DesktopFrontendMessage::WindowDrag);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ pub enum DesktopFrontendMessage {
|
|||
WindowClose,
|
||||
WindowMinimize,
|
||||
WindowMaximize,
|
||||
WindowFullscreen,
|
||||
WindowDrag,
|
||||
WindowHide,
|
||||
WindowHideOthers,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pub enum AppWindowMessage {
|
|||
Close,
|
||||
Minimize,
|
||||
Maximize,
|
||||
Fullscreen,
|
||||
Drag,
|
||||
Hide,
|
||||
HideOthers,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ impl MessageHandler<AppWindowMessage, ()> 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<AppWindowMessage, ()> for AppWindowMessageHandler {
|
|||
Close,
|
||||
Minimize,
|
||||
Maximize,
|
||||
Fullscreen,
|
||||
Drag,
|
||||
Hide,
|
||||
HideOthers,
|
||||
|
|
|
|||
|
|
@ -64,8 +64,10 @@ pub enum FrontendMessage {
|
|||
#[serde(rename = "nodeTypes")]
|
||||
node_types: Vec<FrontendNodeType>,
|
||||
},
|
||||
SendShortcutF11 {
|
||||
SendShortcutFullscreen {
|
||||
shortcut: Option<ActionShortcut>,
|
||||
#[serde(rename = "shortcutMac")]
|
||||
shortcut_mac: Option<ActionShortcut>,
|
||||
},
|
||||
SendShortcutAltClick {
|
||||
shortcut: Option<ActionShortcut>,
|
||||
|
|
@ -371,6 +373,7 @@ pub enum FrontendMessage {
|
|||
WindowClose,
|
||||
WindowMinimize,
|
||||
WindowMaximize,
|
||||
WindowFullscreen,
|
||||
WindowDrag,
|
||||
WindowHide,
|
||||
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::portfolio::utility_types::KeyboardPlatformLayout;
|
||||
use crate::messages::prelude::*;
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(ExtractField)]
|
||||
pub struct InputMapperMessageContext<'a> {
|
||||
|
|
@ -34,27 +33,6 @@ impl InputMapperMessageHandler {
|
|||
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> {
|
||||
let all_key_mapping_entries = std::iter::empty()
|
||||
.chain(self.mapping.key_up.iter())
|
||||
|
|
|
|||
|
|
@ -17,16 +17,19 @@ use glam::DVec2;
|
|||
impl From<MappingVariant> 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<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.
|
||||
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],
|
||||
|
|
|
|||
|
|
@ -29,16 +29,6 @@ impl Mapping {
|
|||
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 {
|
||||
match message {
|
||||
InputMapperMessage::KeyDown(key) => &self.key_down[*key as usize],
|
||||
|
|
@ -51,19 +41,6 @@ impl Mapping {
|
|||
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)]
|
||||
|
|
@ -125,6 +102,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)]
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
|
|
|
|||
|
|
@ -117,8 +117,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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),
|
||||
|
|
|
|||
|
|
@ -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<AppWindowState>("appWindow");
|
||||
const editor = getContext<Editor>("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}
|
||||
</LayoutRow>
|
||||
<!-- 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 -->
|
||||
<LayoutRow class="window-buttons" classes={{ fullscreen: showFullscreenButton, windows: $appWindow.platform === "Windows", linux: $appWindow.platform === "Linux" }}>
|
||||
{#if $appWindow.platform !== "Mac"}
|
||||
{#if showFullscreenButton}
|
||||
<LayoutRow
|
||||
tooltipLabel={$fullscreen.windowFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
|
||||
tooltipLabel={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
|
||||
tooltipDescription={$appWindow.platform === "Web" && $fullscreen.keyboardLockApiSupported
|
||||
? "While fullscreen, keyboard shortcuts normally reserved by the browser become available."
|
||||
: undefined}
|
||||
tooltipShortcut={$tooltip.f11Shortcut}
|
||||
on:click={() => ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)()}
|
||||
tooltipShortcut={$tooltip.fullscreenShortcut}
|
||||
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>
|
||||
{:else}
|
||||
<LayoutRow tooltipLabel="Minimize" on:click={() => 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
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -84,15 +84,23 @@ 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 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;
|
||||
|
||||
// Don't redirect a fullscreen request
|
||||
if (key === "F11" && e.type === "keydown" && !e.repeat) {
|
||||
// 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;
|
||||
|
|
@ -105,13 +113,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
// 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;
|
||||
|
||||
// Don't redirect if a MenuList is open
|
||||
if (window.document.querySelector("[data-floating-menu-content]")) 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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, MessageMaker> = {
|
|||
DisplayEditableTextboxTransform,
|
||||
DisplayRemoveEditableTextbox,
|
||||
SendUIMetadata,
|
||||
SendShortcutF11,
|
||||
SendShortcutFullscreen,
|
||||
SendShortcutAltClick,
|
||||
SendShortcutShiftClick,
|
||||
TriggerAboutGraphiteLocalizedCommitDate,
|
||||
|
|
@ -1734,6 +1738,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
UpdateMaximized,
|
||||
UpdateFullscreen,
|
||||
WindowPointerLockMove,
|
||||
WindowFullscreen,
|
||||
UpdatePropertiesPanelLayout,
|
||||
UpdatePropertiesPanelState,
|
||||
UpdateStatusBarHintsLayout,
|
||||
|
|
|
|||
|
|
@ -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<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 {
|
||||
subscribe,
|
||||
fullscreenModeChanged,
|
||||
|
|
|
|||
|
|
@ -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<typeof setTimeout> | 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;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue