Desktop: Add fullscreen window mode (#3625)

This commit is contained in:
Timon
2026-01-19 17:32:03 +01:00
committed by GitHub
parent fd0addf61c
commit 07fbcd489c
20 changed files with 195 additions and 153 deletions

View File

@@ -11,6 +11,7 @@ pub enum AppWindowMessage {
Close,
Minimize,
Maximize,
Fullscreen,
Drag,
Hide,
HideOthers,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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())

View File

@@ -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());
}
}

View File

@@ -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],

View File

@@ -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)]

View File

@@ -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" })

View File

@@ -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),