Skip to content

Commit 48c1e12

Browse files
committed
Make Vue modifier keys work on the Mac keyboard layout
Part of #206
1 parent e78b940 commit 48c1e12

File tree

4 files changed

+192
-6
lines changed

4 files changed

+192
-6
lines changed

editor/src/input/keyboard.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub type KeyStates = BitVector<KEY_MASK_STORAGE_LENGTH>;
1616
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1717
pub enum Key {
1818
UnknownKey,
19-
// MouseKeys
19+
// Mouse keys
2020
Lmb,
2121
Rmb,
2222
Mmb,

frontend/src/components/panels/LayerTree.vue

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@
3636
class="layer"
3737
:class="{ selected: layer.layer_data.selected }"
3838
:style="{ marginLeft: layerIndent(layer) }"
39-
@click.shift.exact.stop="handleShiftClick(layer)"
40-
@click.ctrl.exact.stop="handleControlClick(layer)"
41-
@click.alt.exact.stop="handleControlClick(layer)"
42-
@click.exact.stop="handleClick(layer)"
39+
@click="
40+
handleInputEvent($event, 'layerTreeLayerClick', {
41+
handleControlClick: () => handleControlClick(layer),
42+
handleShiftClick: () => handleShiftClick(layer),
43+
handleClick: () => handleClick(layer),
44+
})
45+
"
4346
>
4447
<div class="layer-thumbnail" v-html="layer.thumbnail"></div>
4548
<div class="layer-type-icon">
@@ -183,6 +186,7 @@ import { defineComponent } from "vue";
183186
184187
import { ResponseType, registerResponseHandler, Response, BlendMode, DisplayFolderTreeStructure, UpdateLayer, LayerPanelEntry, LayerType } from "@/utilities/response-handler";
185188
import { panicProxy } from "@/utilities/panic-proxy";
189+
import { handleInputEvent } from "@/utilities/input";
186190
import { SeparatorType } from "@/components/widgets/widgets";
187191
188192
import LayoutRow from "@/components/layout/LayoutRow.vue";
@@ -254,6 +258,7 @@ export default defineComponent({
254258
MenuDirection,
255259
SeparatorType,
256260
LayerType,
261+
handleInputEvent,
257262
};
258263
},
259264
methods: {

frontend/src/components/widgets/inputs/NumberInput.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
spellcheck="false"
88
v-model="text"
99
@change="onTextChanged()"
10-
@keydown.esc="onCancelTextChange"
10+
@keydown.esc="handleInputEvent($event, 'numberInputAbort', { onCancelTextChange })"
1111
ref="input"
1212
:disabled="disabled"
1313
/>

frontend/src/utilities/input.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,187 @@ const wasm = import("@/../wasm/pkg").then(panicProxy);
66

77
let viewportMouseInteractionOngoing = false;
88

9+
// Keyboard and mouse events
10+
type MouseKeys = "Lmb" | "Rmb" | "Mmb" | "Bmb" | "Fmb";
11+
const keyboardKeys = {
12+
A: "KeyA",
13+
B: "KeyB",
14+
C: "KeyC",
15+
D: "KeyD",
16+
E: "KeyE",
17+
F: "KeyF",
18+
G: "KeyG",
19+
H: "KeyH",
20+
I: "KeyI",
21+
J: "KeyJ",
22+
K: "KeyK",
23+
L: "KeyL",
24+
M: "KeyM",
25+
N: "KeyN",
26+
O: "KeyO",
27+
P: "KeyP",
28+
Q: "KeyQ",
29+
R: "KeyR",
30+
S: "KeyS",
31+
T: "KeyT",
32+
U: "KeyU",
33+
V: "KeyV",
34+
W: "KeyW",
35+
X: "KeyX",
36+
Y: "KeyY",
37+
Z: "KeyZ",
38+
"0": "Key0",
39+
"1": "Key1",
40+
"2": "Key2",
41+
"3": "Key3",
42+
"4": "Key4",
43+
"5": "Key5",
44+
"6": "Key6",
45+
"7": "Key7",
46+
"8": "Key8",
47+
"9": "Key9",
48+
Enter: "KeyEnter",
49+
"=": "KeyEquals",
50+
"-": "KeyMinus",
51+
"+": "KeyPlus",
52+
Shift: "KeyShift",
53+
Space: "KeySpace",
54+
Control: "KeyControl",
55+
Delete: "KeyDelete",
56+
Backspace: "KeyBackspace",
57+
Alt: "KeyAlt",
58+
Escape: "KeyEscape",
59+
Tab: "KeyTab",
60+
ArrowUp: "KeyArrowUp",
61+
ArrowDown: "KeyArrowDown",
62+
ArrowLeft: "KeyArrowLeft",
63+
ArrowRight: "KeyArrowRight",
64+
LeftBracket: "KeyLeftBracket",
65+
RightBracket: "KeyRightBracket",
66+
LeftCurlyBracket: "KeyLeftCurlyBracket",
67+
RightCurlyBracket: "KeyRightCurlyBracket",
68+
PageUp: "KeyPageUp",
69+
PageDown: "KeyPageDown",
70+
Comma: "KeyComma",
71+
Period: "KeyPeriod",
72+
};
73+
type KeyboardKeys = typeof keyboardKeys[keyof typeof keyboardKeys];
74+
type ModifierKeys = "ctrl" | "cmd" | "shift" | "alt";
75+
type EventBehavior = "stop" | "prevent" | "self";
76+
type HandlerChoice = { functionName: string; includeKeys: (MouseKeys | KeyboardKeys)[]; includeModifiers: ModifierKeys[]; excludeModifiers: ModifierKeys[]; eventBehavior: EventBehavior[] };
77+
type Handlers = HandlerChoice[];
78+
79+
const standardKeymap: Record<string, Handlers> = {
80+
layerTreeLayerClick: [
81+
{ functionName: "handleControlClick", includeKeys: [], includeModifiers: ["ctrl"], excludeModifiers: ["shift", "alt"], eventBehavior: ["stop"] },
82+
{ functionName: "handleShiftClick", includeKeys: [], includeModifiers: ["shift"], excludeModifiers: ["ctrl", "alt"], eventBehavior: ["stop"] },
83+
{ functionName: "handleControlClick", includeKeys: [], includeModifiers: ["alt"], excludeModifiers: ["ctrl", "shift"], eventBehavior: ["stop"] },
84+
{ functionName: "handleClick", includeKeys: [], includeModifiers: [], excludeModifiers: ["ctrl", "shift", "alt"], eventBehavior: ["stop"] },
85+
],
86+
numberInputAbort: [{ functionName: "numberInputAbort", includeKeys: ["KeyEscape"], includeModifiers: [], excludeModifiers: [], eventBehavior: [] }],
87+
};
88+
const keymapApple: Record<string, Handlers> = {
89+
layerTreeLayerClick: [
90+
{ functionName: "handleControlClick", includeKeys: [], includeModifiers: ["cmd"], excludeModifiers: ["ctrl", "shift", "alt"], eventBehavior: ["stop"] },
91+
{ functionName: "handleShiftClick", includeKeys: [], includeModifiers: ["shift"], excludeModifiers: ["ctrl", "cmd", "alt"], eventBehavior: ["stop"] },
92+
{ functionName: "handleControlClick", includeKeys: [], includeModifiers: ["alt"], excludeModifiers: ["ctrl", "cmd", "shift"], eventBehavior: ["stop"] },
93+
{ functionName: "handleClick", includeKeys: [], includeModifiers: [], excludeModifiers: ["ctrl", "cmd", "shift", "alt"], eventBehavior: ["stop"] },
94+
],
95+
numberInputAbort: [{ functionName: "numberInputAbort", includeKeys: ["KeyEscape"], includeModifiers: [], excludeModifiers: [], eventBehavior: [] }],
96+
};
97+
98+
export function handleInputEvent(event: KeyboardEvent | MouseEvent | TouchEvent, keymapEntryId: keyof typeof standardKeymap, functions: Record<string, () => void>) {
99+
const isApple = /^Mac|^iPhone|^iPad/i.test(navigator.platform);
100+
const keymap = isApple ? keymapApple : standardKeymap;
101+
const handlers = keymap[keymapEntryId];
102+
103+
// Same physical key on all keyboard layouts but used differently between platform keymaps
104+
const ctrlModifier = event.ctrlKey;
105+
// Used only by the Apple keymap (Graphite should never use the meta/Windows key on the non-Apple platform keymap)
106+
const cmdModifier = isApple && event.metaKey;
107+
// Consistent across all keyboard layouts
108+
const shiftModifier = event.shiftKey;
109+
// Consistent across all keyboard layouts but this physical key is labeled "Option" on Apple layouts
110+
const altModifier = event.altKey;
111+
112+
const matchedHandlers = handlers.filter((handler) => {
113+
// Reject any excluded modifier keys
114+
if (ctrlModifier && handler.excludeModifiers.includes("ctrl")) return false;
115+
if (cmdModifier && handler.excludeModifiers.includes("cmd")) return false;
116+
if (shiftModifier && handler.excludeModifiers.includes("shift")) return false;
117+
if (altModifier && handler.excludeModifiers.includes("alt")) return false;
118+
119+
// Reject if missing any included modifier keys
120+
if (!ctrlModifier && handler.includeModifiers.includes("ctrl")) return false;
121+
if (!cmdModifier && handler.includeModifiers.includes("cmd")) return false;
122+
if (!shiftModifier && handler.includeModifiers.includes("shift")) return false;
123+
if (!altModifier && handler.includeModifiers.includes("alt")) return false;
124+
125+
if (event instanceof MouseEvent) {
126+
// handler.includeKeys.includes(mouseKeys[event.buttons])
127+
// const key = mouseKeys?[event.button + ""];
128+
let lmb = false; // Left
129+
let rmb = false; // Right
130+
let mmb = false; // Middle
131+
let bmb = false; // Back
132+
let fmb = false; // Forward
133+
134+
let buttonsValue = event.buttons;
135+
if (buttonsValue >= 16) {
136+
fmb = true;
137+
buttonsValue -= 16;
138+
}
139+
if (buttonsValue >= 8) {
140+
bmb = true;
141+
buttonsValue -= 8;
142+
}
143+
if (buttonsValue >= 4) {
144+
mmb = true;
145+
buttonsValue -= 4;
146+
}
147+
if (buttonsValue >= 2) {
148+
rmb = true;
149+
buttonsValue -= 2;
150+
}
151+
if (buttonsValue >= 1) {
152+
lmb = true;
153+
buttonsValue -= 1;
154+
}
155+
156+
if (!lmb && handler.includeKeys.includes("Lmb")) return false;
157+
if (!rmb && handler.includeKeys.includes("Rmb")) return false;
158+
if (!mmb && handler.includeKeys.includes("Mmb")) return false;
159+
if (!bmb && handler.includeKeys.includes("Bmb")) return false;
160+
if (!fmb && handler.includeKeys.includes("Fmb")) return false;
161+
}
162+
163+
if (event instanceof KeyboardEvent) {
164+
event.key;
165+
}
166+
167+
// Reject unmatched keybind
168+
return false;
169+
});
170+
171+
if (matchedHandlers.length === 0) return;
172+
if (matchedHandlers.length > 1) {
173+
console.log(`Ambiguous set of matched input event handlers.
174+
Keymap entry ID: ${keymapEntryId}
175+
Modifiers: [Ctrl: ${ctrlModifier}] [Cmd: ${cmdModifier}] [Shift: ${shiftModifier}] [Alt: ${altModifier}]
176+
Matched handlers: ${matchedHandlers}
177+
`);
178+
}
179+
const handler = matchedHandlers[0];
180+
181+
// Apply any additional event behavior
182+
if (handler.eventBehavior.includes("stop")) event.stopPropagation();
183+
if (handler.eventBehavior.includes("prevent")) event.preventDefault();
184+
if (handler.eventBehavior.includes("self") && event.target !== event.currentTarget) return;
185+
186+
// Execute the callback function
187+
functions[handler.functionName]();
188+
}
189+
9190
// Keyboard events
10191

11192
function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): boolean {

0 commit comments

Comments
 (0)