From 7a52e50a947a1feb7f6358d4a4e6fcc1638068fb Mon Sep 17 00:00:00 2001 From: multisn8 Date: Fri, 24 Feb 2023 07:18:04 +0100 Subject: [PATCH] Add 'Zoom with Scroll' input navigation scheme to preferences (#1021) * Add use_scroll_as_zoom field to preference handler * Add {Create,Delete}Mapping variants to message * Revert "Add {Create,Delete}Mapping variants to message" This reverts commit 0ba74754c9fb0c78d0b590c96e1d4fe2cfdd13e7. * Revert "Add use_scroll_as_zoom field to preference handler" This reverts commit d30f7c9edfa6d6e156939ca07f4db81f288975fd. * Add basic scroll_as_zoom mapping * Create overengineered mapping patch abstraction * Add (for now passthrough) input layout manager * Actually handle ModifyLayout messages (untested) * Add backend preferences <-> layout manager comms * Add scroll-as-zoom to actual preferences UI * Rename LayoutManager -> KeyMapping * Add Input section to preferences and title case * Add scrollAsZoom frontend handling code (untested) * Handle frontend <-> preferences comms * (broken) Move scrollAsZoom persistence into state * Fix scrollAsZoom having no effect on node graph * Remove debugging helpers * Fix confusion between horizontal and vertical * Rename feature * Move new message handler into folder --------- Co-authored-by: Keavon Chambers --- editor/src/dispatcher.rs | 20 +++---- .../preferences_dialog_message_handler.rs | 32 +++++++++- .../src/messages/frontend/frontend_message.rs | 4 ++ .../messages/input_mapper/default_mapping.rs | 47 +++++++++++++++ .../input_mapper/input_mapper_message.rs | 2 +- .../input_mapper_message_handler.rs | 4 ++ .../key_mapping/key_mapping_message.rs | 24 ++++++++ .../key_mapping_message_handler.rs | 23 ++++++++ .../messages/input_mapper/key_mapping/mod.rs | 7 +++ editor/src/messages/input_mapper/mod.rs | 1 + .../input_mapper/utility_types/misc.rs | 58 ++++++++++++++----- editor/src/messages/message.rs | 4 +- .../preferences/preferences_message.rs | 1 + .../preferences_message_handler.rs | 13 +++++ editor/src/messages/prelude.rs | 1 + .../src/wasm-communication/messages.ts | 5 ++ frontend/src/components/panels/NodeGraph.vue | 19 ++++-- frontend/src/state-providers/node-graph.ts | 5 ++ frontend/src/wasm-communication/messages.ts | 5 ++ 19 files changed, 242 insertions(+), 33 deletions(-) create mode 100644 editor/src/messages/input_mapper/key_mapping/key_mapping_message.rs create mode 100644 editor/src/messages/input_mapper/key_mapping/key_mapping_message_handler.rs create mode 100644 editor/src/messages/input_mapper/key_mapping/mod.rs diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index da5ff9b7..a8bd204c 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -18,8 +18,8 @@ struct DispatcherMessageHandlers { debug_message_handler: DebugMessageHandler, dialog_message_handler: DialogMessageHandler, globals_message_handler: GlobalsMessageHandler, - input_mapper_message_handler: InputMapperMessageHandler, input_preprocessor_message_handler: InputPreprocessorMessageHandler, + key_mapping_message_handler: KeyMappingMessageHandler, layout_message_handler: LayoutMessageHandler, portfolio_message_handler: PortfolioMessageHandler, preferences_message_handler: PreferencesMessageHandler, @@ -133,20 +133,20 @@ impl Dispatcher { Globals(message) => { self.message_handlers.globals_message_handler.process_message(message, &mut queue, ()); } - InputMapper(message) => { - let actions = self.collect_actions(); - - self.message_handlers - .input_mapper_message_handler - .process_message(message, &mut queue, (&self.message_handlers.input_preprocessor_message_handler, actions)); - } InputPreprocessor(message) => { let keyboard_platform = GLOBAL_PLATFORM.get().copied().unwrap_or_default().as_keyboard_platform_layout(); self.message_handlers.input_preprocessor_message_handler.process_message(message, &mut queue, keyboard_platform); } + KeyMapping(message) => { + let actions = self.collect_actions(); + + self.message_handlers + .key_mapping_message_handler + .process_message(message, &mut queue, (&self.message_handlers.input_preprocessor_message_handler, actions)); + } Layout(message) => { - let action_input_mapping = &|action_to_find: &MessageDiscriminant| self.message_handlers.input_mapper_message_handler.action_input_mapping(action_to_find); + let action_input_mapping = &|action_to_find: &MessageDiscriminant| self.message_handlers.key_mapping_message_handler.action_input_mapping(action_to_find); self.message_handlers.layout_message_handler.process_message(message, &mut queue, action_input_mapping); } @@ -195,7 +195,7 @@ impl Dispatcher { let mut list = Vec::new(); list.extend(self.message_handlers.dialog_message_handler.actions()); list.extend(self.message_handlers.input_preprocessor_message_handler.actions()); - list.extend(self.message_handlers.input_mapper_message_handler.actions()); + list.extend(self.message_handlers.key_mapping_message_handler.actions()); list.extend(self.message_handlers.debug_message_handler.actions()); if self.message_handlers.portfolio_message_handler.active_document().is_some() { list.extend(self.message_handlers.tool_message_handler.actions()); diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index 58d70553..888e376f 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -1,7 +1,7 @@ use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::widgets::button_widgets::TextButton; -use crate::messages::layout::utility_types::widgets::input_widgets::{NumberInput, TextInput}; +use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInput, NumberInput, TextInput}; use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel}; use crate::messages::prelude::*; @@ -33,6 +33,35 @@ impl PreferencesDialogMessageHandler { } fn properties(&self, preferences: &PreferencesMessageHandler) -> Layout { + let zoom_with_scroll = vec![ + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Input".into(), + min_width: 60, + italic: true, + ..Default::default() + })), + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: "Zoom with Scroll".into(), + table_align: true, + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { + checked: preferences.zoom_with_scroll, + tooltip: "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)".into(), + on_update: WidgetCallback::new(|checkbox_input: &CheckboxInput| { + PreferencesMessage::ModifyLayout { + zoom_with_scroll: checkbox_input.checked, + } + .into() + }), + ..Default::default() + })), + ]; + let imaginate_server_hostname = vec![ WidgetHolder::new(Widget::TextLabel(TextLabel { value: "Imaginate".into(), @@ -107,6 +136,7 @@ impl PreferencesDialogMessageHandler { ..Default::default() }))], }, + LayoutGroup::Row { widgets: zoom_with_scroll }, LayoutGroup::Row { widgets: imaginate_server_hostname }, LayoutGroup::Row { widgets: imaginate_refresh_frequency }, LayoutGroup::Row { widgets: button_widgets }, diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 13381524..311c5b76 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -266,4 +266,8 @@ pub enum FrontendMessage { layout_target: LayoutTarget, diff: Vec, }, + UpdateZoomWithScroll { + #[serde(rename = "zoomWithScroll")] + zoom_with_scroll: bool, + }, } diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index 47f1994d..f1b0d999 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -1,4 +1,5 @@ use crate::consts::{BIG_NUDGE_AMOUNT, NUDGE_AMOUNT}; +use crate::messages::input_mapper::key_mapping::MappingVariant; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates}; use crate::messages::input_mapper::utility_types::macros::*; use crate::messages::input_mapper::utility_types::misc::MappingEntry; @@ -8,6 +9,15 @@ use crate::messages::prelude::*; use glam::DVec2; +impl From for Mapping { + fn from(value: MappingVariant) -> Self { + match value { + MappingVariant::Default => default_mapping(), + MappingVariant::ZoomWithScroll => zoom_with_scroll(), + } + } +} + pub fn default_mapping() -> Mapping { use InputMapperMessage::*; use Key::*; @@ -337,3 +347,40 @@ pub fn default_mapping() -> Mapping { pointer_move, } } + +/// Defaults except that scrolling without modifiers is bound to zooming instead of vertical panning +pub fn zoom_with_scroll() -> Mapping { + // TODO(multisn8): for other keymaps this patterns might be useful + use InputMapperMessage::*; + + let mut mapping = default_mapping(); + + let remove = [ + entry!(WheelScroll; modifiers=[Control], action_dispatch=NavigationMessage::WheelCanvasZoom), + entry!(WheelScroll; modifiers=[Shift], action_dispatch=NavigationMessage::WheelCanvasTranslate { use_y_as_x: true }), + entry!(WheelScroll; action_dispatch=NavigationMessage::WheelCanvasTranslate { use_y_as_x: false }), + ]; + let add = [ + entry!(WheelScroll; modifiers=[Control], action_dispatch=NavigationMessage::WheelCanvasTranslate { use_y_as_x: true }), + entry!(WheelScroll; modifiers=[Shift], action_dispatch=NavigationMessage::WheelCanvasTranslate { use_y_as_x: false }), + entry!(WheelScroll; action_dispatch=NavigationMessage::WheelCanvasZoom), + ]; + + apply_mapping_patch(&mut mapping, remove, add); + + mapping +} + +fn apply_mapping_patch<'a, const N: usize, const M: usize, const X: usize, const Y: usize>( + mapping: &mut Mapping, + remove: impl IntoIterator, + add: impl IntoIterator, +) { + for entry in remove.into_iter().flat_map(|inner| inner.iter()).flat_map(|inner| inner.iter()) { + mapping.remove(entry); + } + + for entry in add.into_iter().flat_map(|inner| inner.iter()).flat_map(|inner| inner.iter()) { + mapping.add(entry.clone()); + } +} diff --git a/editor/src/messages/input_mapper/input_mapper_message.rs b/editor/src/messages/input_mapper/input_mapper_message.rs index 0673e80a..67d77bdb 100644 --- a/editor/src/messages/input_mapper/input_mapper_message.rs +++ b/editor/src/messages/input_mapper/input_mapper_message.rs @@ -4,7 +4,7 @@ use crate::messages::prelude::*; use serde::{Deserialize, Serialize}; #[remain::sorted] -#[impl_message(Message, InputMapper)] +#[impl_message(Message, KeyMappingMessage, Lookup)] #[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum InputMapperMessage { // Sub-messages diff --git a/editor/src/messages/input_mapper/input_mapper_message_handler.rs b/editor/src/messages/input_mapper/input_mapper_message_handler.rs index 016c3fea..ac409241 100644 --- a/editor/src/messages/input_mapper/input_mapper_message_handler.rs +++ b/editor/src/messages/input_mapper/input_mapper_message_handler.rs @@ -20,6 +20,10 @@ impl MessageHandler String { let mut output = String::new(); let mut actions = actions diff --git a/editor/src/messages/input_mapper/key_mapping/key_mapping_message.rs b/editor/src/messages/input_mapper/key_mapping/key_mapping_message.rs new file mode 100644 index 00000000..ef9838c2 --- /dev/null +++ b/editor/src/messages/input_mapper/key_mapping/key_mapping_message.rs @@ -0,0 +1,24 @@ +use crate::messages::prelude::*; + +use serde::{Deserialize, Serialize}; + +#[remain::sorted] +#[impl_message(Message, KeyMapping)] +#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum KeyMappingMessage { + #[child] + Lookup(InputMapperMessage), + #[child] + ModifyMapping(MappingVariant), +} + +#[remain::sorted] +#[impl_message(Message, KeyMappingMessage, ModifyMapping)] +#[derive(PartialEq, Eq, Clone, Debug, Default, Hash, Serialize, Deserialize)] +pub enum MappingVariant { + #[remain::unsorted] + #[default] + Default, + + ZoomWithScroll, +} diff --git a/editor/src/messages/input_mapper/key_mapping/key_mapping_message_handler.rs b/editor/src/messages/input_mapper/key_mapping/key_mapping_message_handler.rs new file mode 100644 index 00000000..ea9be60a --- /dev/null +++ b/editor/src/messages/input_mapper/key_mapping/key_mapping_message_handler.rs @@ -0,0 +1,23 @@ +use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup; +use crate::messages::prelude::*; + +#[derive(Debug, Default)] +pub struct KeyMappingMessageHandler { + mapping_handler: InputMapperMessageHandler, +} + +impl MessageHandler for KeyMappingMessageHandler { + fn process_message(&mut self, message: KeyMappingMessage, responses: &mut VecDeque, data: (&InputPreprocessorMessageHandler, ActionList)) { + match message { + KeyMappingMessage::Lookup(input) => self.mapping_handler.process_message(input, responses, data), + KeyMappingMessage::ModifyMapping(new_layout) => self.mapping_handler.set_mapping(new_layout.into()), + } + } + advertise_actions!(); +} + +impl KeyMappingMessageHandler { + pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant) -> Vec { + self.mapping_handler.action_input_mapping(action_to_find) + } +} diff --git a/editor/src/messages/input_mapper/key_mapping/mod.rs b/editor/src/messages/input_mapper/key_mapping/mod.rs new file mode 100644 index 00000000..26be1f69 --- /dev/null +++ b/editor/src/messages/input_mapper/key_mapping/mod.rs @@ -0,0 +1,7 @@ +mod key_mapping_message; +mod key_mapping_message_handler; + +#[doc(inline)] +pub use key_mapping_message::{KeyMappingMessage, KeyMappingMessageDiscriminant, MappingVariant, MappingVariantDiscriminant}; +#[doc(inline)] +pub use key_mapping_message_handler::KeyMappingMessageHandler; diff --git a/editor/src/messages/input_mapper/mod.rs b/editor/src/messages/input_mapper/mod.rs index 55b4bb52..74a0e666 100644 --- a/editor/src/messages/input_mapper/mod.rs +++ b/editor/src/messages/input_mapper/mod.rs @@ -2,6 +2,7 @@ mod input_mapper_message; mod input_mapper_message_handler; pub mod default_mapping; +pub mod key_mapping; pub mod utility_types; #[doc(inline)] diff --git a/editor/src/messages/input_mapper/utility_types/misc.rs b/editor/src/messages/input_mapper/utility_types/misc.rs index 16ad83b6..d48b4da7 100644 --- a/editor/src/messages/input_mapper/utility_types/misc.rs +++ b/editor/src/messages/input_mapper/utility_types/misc.rs @@ -1,5 +1,5 @@ use super::input_keyboard::{all_required_modifiers_pressed, KeysGroup, LayoutKeysGroup}; -use crate::messages::input_mapper::default_mapping::default_mapping; +use crate::messages::input_mapper::key_mapping::MappingVariant; use crate::messages::input_mapper::utility_types::input_keyboard::{KeyStates, NUMBER_OF_KEYS}; use crate::messages::prelude::*; @@ -14,22 +14,46 @@ pub struct Mapping { pub pointer_move: KeyMappingEntries, } -impl Mapping { - pub fn match_input_message(&self, message: InputMapperMessage, keyboard_state: &KeyStates, actions: ActionList) -> Option { - let list = match message { - InputMapperMessage::KeyDown(key) => &self.key_down[key as usize], - InputMapperMessage::KeyUp(key) => &self.key_up[key as usize], - InputMapperMessage::DoubleClick => &self.double_click, - InputMapperMessage::WheelScroll => &self.wheel_scroll, - InputMapperMessage::PointerMove => &self.pointer_move, - }; - list.match_mapping(keyboard_state, actions) +impl Default for Mapping { + fn default() -> Self { + MappingVariant::Default.into() } } -impl Default for Mapping { - fn default() -> Self { - default_mapping() +impl Mapping { + pub fn match_input_message(&self, message: InputMapperMessage, keyboard_state: &KeyStates, actions: ActionList) -> Option { + let list = self.associated_entries(&message); + 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], + InputMapperMessage::KeyUp(key) => &self.key_up[*key as usize], + InputMapperMessage::DoubleClick => &self.double_click, + InputMapperMessage::WheelScroll => &self.wheel_scroll, + InputMapperMessage::PointerMove => &self.pointer_move, + } + } + + 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::DoubleClick => &mut self.double_click, + InputMapperMessage::WheelScroll => &mut self.wheel_scroll, + InputMapperMessage::PointerMove => &mut self.pointer_move, + } } } @@ -52,7 +76,11 @@ impl KeyMappingEntries { } pub fn push(&mut self, entry: MappingEntry) { - self.0.push(entry) + self.0.push(entry); + } + + pub fn remove(&mut self, target_entry: &MappingEntry) { + self.0.retain(|entry| entry != target_entry); } pub const fn new() -> Self { diff --git a/editor/src/messages/message.rs b/editor/src/messages/message.rs index 673ec912..bee7fccf 100644 --- a/editor/src/messages/message.rs +++ b/editor/src/messages/message.rs @@ -26,10 +26,10 @@ pub enum Message { #[child] Globals(GlobalsMessage), #[child] - InputMapper(InputMapperMessage), - #[child] InputPreprocessor(InputPreprocessorMessage), #[child] + KeyMapping(KeyMappingMessage), + #[child] Layout(LayoutMessage), #[child] Portfolio(PortfolioMessage), diff --git a/editor/src/messages/preferences/preferences_message.rs b/editor/src/messages/preferences/preferences_message.rs index bf5b9de1..492a72dd 100644 --- a/editor/src/messages/preferences/preferences_message.rs +++ b/editor/src/messages/preferences/preferences_message.rs @@ -10,4 +10,5 @@ pub enum PreferencesMessage { ImaginateRefreshFrequency { seconds: f64 }, ImaginateServerHostname { hostname: String }, + ModifyLayout { zoom_with_scroll: bool }, } diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index 26f1e038..06a62da7 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -1,3 +1,4 @@ +use crate::messages::input_mapper::key_mapping::MappingVariant; use crate::messages::prelude::*; use serde::{Deserialize, Serialize}; @@ -6,6 +7,7 @@ use serde::{Deserialize, Serialize}; pub struct PreferencesMessageHandler { pub imaginate_server_hostname: String, pub imaginate_refresh_frequency: f64, + pub zoom_with_scroll: bool, } impl Default for PreferencesMessageHandler { @@ -13,6 +15,7 @@ impl Default for PreferencesMessageHandler { Self { imaginate_server_hostname: "http://localhost:7860/".into(), imaginate_refresh_frequency: 1., + zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll), } } } @@ -53,6 +56,16 @@ impl MessageHandler for PreferencesMessageHandler { self.imaginate_server_hostname = hostname; responses.push_back(PortfolioMessage::ImaginateCheckServerStatus.into()); } + PreferencesMessage::ModifyLayout { zoom_with_scroll } => { + self.zoom_with_scroll = zoom_with_scroll; + + let variant = match zoom_with_scroll { + false => MappingVariant::Default, + true => MappingVariant::ZoomWithScroll, + }; + responses.push_back(KeyMappingMessage::ModifyMapping(variant).into()); + responses.push_back(FrontendMessage::UpdateZoomWithScroll { zoom_with_scroll }.into()); + } } responses.push_back(FrontendMessage::TriggerSavePreferences { preferences: self.clone() }.into()); diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index d5818614..1fc6a377 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -10,6 +10,7 @@ pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, pub use crate::messages::dialog::{DialogMessage, DialogMessageDiscriminant, DialogMessageHandler}; pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant}; pub use crate::messages::globals::{GlobalsMessage, GlobalsMessageDiscriminant, GlobalsMessageHandler}; +pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappingMessageDiscriminant, KeyMappingMessageHandler}; pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageDiscriminant, InputMapperMessageHandler}; pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler}; pub use crate::messages::layout::{LayoutMessage, LayoutMessageDiscriminant, LayoutMessageHandler}; diff --git a/frontend-svelte/src/wasm-communication/messages.ts b/frontend-svelte/src/wasm-communication/messages.ts index 7202c284..a9c970b4 100644 --- a/frontend-svelte/src/wasm-communication/messages.ts +++ b/frontend-svelte/src/wasm-communication/messages.ts @@ -53,6 +53,10 @@ export class UpdateOpenDocumentsList extends JsMessage { readonly openDocuments!: FrontendDocumentDetails[]; } +export class UpdateZoomWithScroll extends JsMessage { + readonly zoomWithScroll!: boolean; +} + // Allows the auto save system to use a string for the id rather than a BigInt. // IndexedDb does not allow for BigInts as primary keys. // TypeScript does not allow subclasses to change the type of class variables in subclasses. @@ -1432,6 +1436,7 @@ export const messageMakers: Record = { UpdateOpenDocumentsList, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout, + UpdateZoomWithScroll, UpdateToolOptionsLayout, UpdateToolShelfLayout, UpdateWorkingColorsLayout, diff --git a/frontend/src/components/panels/NodeGraph.vue b/frontend/src/components/panels/NodeGraph.vue index dece0db9..6a58e90d 100644 --- a/frontend/src/components/panels/NodeGraph.vue +++ b/frontend/src/components/panels/NodeGraph.vue @@ -513,9 +513,20 @@ export default defineComponent({ scroll(e: WheelEvent) { const scrollX = e.deltaX; const scrollY = e.deltaY; + const zoomWithScroll = this.nodeGraph.state.zoomWithScroll; + + let zoom; + let horizontalPan; + if (zoomWithScroll) { + zoom = !(e.ctrlKey || e.shiftKey); + horizontalPan = e.ctrlKey; + } else { + zoom = e.ctrlKey; + horizontalPan = !(e.ctrlKey || e.shiftKey); + } // Zoom - if (e.ctrlKey) { + if (zoom) { let zoomFactor = 1 + Math.abs(scrollY) * WHEEL_RATE; if (scrollY > 0) zoomFactor = 1 / zoomFactor; @@ -541,11 +552,11 @@ export default defineComponent({ e.preventDefault(); } // Pan - else if (!e.shiftKey) { + else if (horizontalPan) { + this.transform.x -= scrollY / this.transform.scale; + } else { this.transform.x -= scrollX / this.transform.scale; this.transform.y -= scrollY / this.transform.scale; - } else { - this.transform.x -= scrollY / this.transform.scale; } }, keydown(e: KeyboardEvent): void { diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 3c6c6b49..cad4f486 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -8,6 +8,7 @@ import { UpdateNodeGraph, UpdateNodeTypes, UpdateNodeGraphBarLayout, + UpdateZoomWithScroll, defaultWidgetLayout, patchWidgetLayout, } from "@/wasm-communication/messages"; @@ -19,6 +20,7 @@ export function createNodeGraphState(editor: Editor) { links: [] as FrontendNodeLink[], nodeTypes: [] as FrontendNodeType[], nodeGraphBarLayout: defaultWidgetLayout(), + zoomWithScroll: false as boolean, }); // Set up message subscriptions on creation @@ -32,6 +34,9 @@ export function createNodeGraphState(editor: Editor) { editor.subscriptions.subscribeJsMessage(UpdateNodeGraphBarLayout, (updateNodeGraphBarLayout) => { patchWidgetLayout(state.nodeGraphBarLayout, updateNodeGraphBarLayout); }); + editor.subscriptions.subscribeJsMessage(UpdateZoomWithScroll, (updateZoomWithScroll) => { + state.zoomWithScroll = updateZoomWithScroll.zoomWithScroll; + }); return { state: readonly(state) as typeof state, diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index 66d45490..437825e8 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -53,6 +53,10 @@ export class UpdateOpenDocumentsList extends JsMessage { readonly openDocuments!: FrontendDocumentDetails[]; } +export class UpdateZoomWithScroll extends JsMessage { + readonly zoomWithScroll!: boolean; +} + // Allows the auto save system to use a string for the id rather than a BigInt. // IndexedDb does not allow for BigInts as primary keys. // TypeScript does not allow subclasses to change the type of class variables in subclasses. @@ -1422,6 +1426,7 @@ export const messageMakers: Record = { UpdateOpenDocumentsList, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout, + UpdateZoomWithScroll, UpdateToolOptionsLayout, UpdateToolShelfLayout, UpdateWorkingColorsLayout,