diff --git a/editor/src/input/input_preprocessor_message_handler.rs b/editor/src/input/input_preprocessor_message_handler.rs index b9d3a5257c..331145327d 100644 --- a/editor/src/input/input_preprocessor_message_handler.rs +++ b/editor/src/input/input_preprocessor_message_handler.rs @@ -114,13 +114,18 @@ impl MessageHandler for InputPreprocessorMessageHa } impl InputPreprocessorMessageHandler { - fn translate_mouse_event(&mut self, new_state: MouseState, allow_first_button_down: bool, responses: &mut VecDeque) { + fn translate_mouse_event(&mut self, mut new_state: MouseState, allow_first_button_down: bool, responses: &mut VecDeque) { for (bit_flag, key) in [(MouseKeys::LEFT, Key::Lmb), (MouseKeys::RIGHT, Key::Rmb), (MouseKeys::MIDDLE, Key::Mmb)] { // Calculate the intersection between the two key states let old_down = self.mouse.mouse_keys & bit_flag == bit_flag; let new_down = new_state.mouse_keys & bit_flag == bit_flag; - if !old_down && new_down && (allow_first_button_down || self.mouse.mouse_keys != MouseKeys::NONE) { - responses.push_back(InputMapperMessage::KeyDown(key).into()); + if !old_down && new_down { + if allow_first_button_down || self.mouse.mouse_keys != MouseKeys::NONE { + responses.push_back(InputMapperMessage::KeyDown(key).into()); + } else { + // Required to stop a keyup being emitted for a keydown outside canvas + new_state.mouse_keys ^= bit_flag; + } } if old_down && !new_down { responses.push_back(InputMapperMessage::KeyUp(key).into()); diff --git a/frontend/public/index.html b/frontend/public/index.html index dab2df124b..937a49f069 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -20,7 +20,7 @@ -
+
diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 7b2f3e20a3..90f5fac3b2 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -11,6 +11,9 @@ type EventListenerTarget = { }; export function createInputManager(editor: Editor, container: HTMLElement, dialog: DialogState, document: PortfolioState, fullscreen: FullscreenState): () => void { + const app = window.document.querySelector("[data-app]") as HTMLElement | undefined; + app?.focus(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: boolean | AddEventListenerOptions }[] = [ { target: window, eventName: "resize", action: (): void => onWindowResize(container) }, @@ -27,10 +30,20 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo { target: window, eventName: "wheel", action: (e: WheelEvent): void => onMouseScroll(e), options: { passive: false } }, { target: window, eventName: "modifyinputfield", action: (e: CustomEvent): void => onModifyInputField(e) }, { target: window.document.body, eventName: "paste", action: (e: ClipboardEvent): void => onPaste(e) }, + { + target: app as EventListenerTarget, + eventName: "blur", + action: (): void => blurApp(), + }, ]; let viewportPointerInteractionOngoing = false; let textInput = undefined as undefined | HTMLDivElement; + let canvasFocused = true; + + function blurApp(): void { + canvasFocused = false; + } // Keyboard events @@ -66,8 +79,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo if (e.ctrlKey && e.shiftKey && key === "j") return false; // Don't redirect tab or enter if not in canvas (to allow navigating elements) - const inCanvas = e.target instanceof Element && e.target.closest("[data-canvas]"); - if (!inCanvas && ["tab", "enter", " ", "arrowdown", "arrowup", "arrowleft", "arrowright"].includes(key.toLowerCase())) return false; + if (!canvasFocused && ["tab", "enter", " ", "arrowdown", "arrowup", "arrowleft", "arrowright"].includes(key.toLowerCase())) return false; // Redirect to the backend return true; @@ -113,13 +125,20 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo const inFloatingMenu = e.target instanceof Element && e.target.closest("[data-floating-menu-content]"); if (!viewportPointerInteractionOngoing && inFloatingMenu) return; + const { target } = e; + const newInCanvas = (target instanceof Element && target.closest("[data-canvas]")) instanceof Element && !targetIsTextField(window.document.activeElement); + if (newInCanvas && !canvasFocused) { + canvasFocused = true; + app?.focus(); + } + const modifiers = makeKeyboardModifiersBitfield(e); editor.instance.on_mouse_move(e.clientX, e.clientY, e.buttons, modifiers); } function onPointerDown(e: PointerEvent): void { const { target } = e; - const inCanvas = target instanceof Element && target.closest("[data-canvas]"); + const isTargetingCanvas = target instanceof Element && target.closest("[data-canvas]"); const inDialog = target instanceof Element && target.closest("[data-dialog-modal] [data-floating-menu-content]"); const inTextInput = target === textInput; @@ -131,7 +150,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo if (!inTextInput) { if (textInput) editor.instance.on_change_text(textInputCleanup(textInput.innerText)); - else if (inCanvas) viewportPointerInteractionOngoing = true; + else viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element; } if (viewportPointerInteractionOngoing) { @@ -168,7 +187,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo function onMouseScroll(e: WheelEvent): void { const { target } = e; - const inCanvas = target instanceof Element && target.closest("[data-canvas]"); + const isTargetingCanvas = target instanceof Element && target.closest("[data-canvas]"); // Redirect vertical scroll wheel movement into a horizontal scroll on a horizontally scrollable element // There seems to be no possible way to properly employ the browser's smooth scrolling interpolation @@ -178,7 +197,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo return; } - if (inCanvas) { + if (isTargetingCanvas) { e.preventDefault(); const modifiers = makeKeyboardModifiersBitfield(e); editor.instance.on_mouse_scroll(e.clientX, e.clientY, e.buttons, e.deltaX, e.deltaY, e.deltaZ, modifiers); @@ -246,7 +265,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo }); } - function targetIsTextField(target: EventTarget | null): boolean { + function targetIsTextField(target: EventTarget | HTMLElement | null): boolean { return target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable); }