mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Desktop: Add fullscreen window mode (#3625)
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user