Skip to content

Commit 14a1578

Browse files
committed
feat: Add HID usage modifier editing.
1 parent 837e4a7 commit 14a1578

File tree

2 files changed

+95
-19
lines changed

2 files changed

+95
-19
lines changed

src/behaviors/HidUsagePicker.stories.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export const Keyboard: Story = {
2525
},
2626
};
2727

28+
export const KeyboardModSelection: Story = {
29+
args: {
30+
usagePages: [{ id: 7 }],
31+
value: 458756
32+
}
33+
};
34+
2835
export const KeyboardAndConsumer: Story = {
2936
args: {
3037
usagePages: [{ id: 7 }, { id: 12 }],

src/behaviors/HidUsagePicker.tsx

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {
22
Button,
3+
Checkbox,
4+
CheckboxGroup,
35
Collection,
46
ComboBox,
57
Header,
@@ -61,38 +63,105 @@ const UsageSection = ({id, min, max}: UsageSectionProps) => {
6163
);
6264
}
6365

66+
enum Mods {
67+
LeftControl = 0x01,
68+
LeftShift = 0x02,
69+
LeftAlt = 0x04,
70+
LeftGUI = 0x08,
71+
RightControl = 0x10,
72+
RightShift = 0x20,
73+
RightAlt = 0x40,
74+
RightGUI = 0x80,
75+
}
76+
77+
const mod_labels: Record<Mods, string> = {
78+
[Mods.LeftControl]: "L Ctrl",
79+
[Mods.LeftShift]: "L Shift",
80+
[Mods.LeftAlt]: "L Alt",
81+
[Mods.LeftGUI]: "L GUI",
82+
[Mods.RightControl]: "R Ctrl",
83+
[Mods.RightShift]: "R Shift",
84+
[Mods.RightAlt]: "R Alt",
85+
[Mods.RightGUI]: "R GUI",
86+
}
87+
88+
const all_mods = [
89+
Mods.LeftControl,
90+
Mods.LeftShift,
91+
Mods.LeftAlt,
92+
Mods.LeftGUI,
93+
Mods.RightControl,
94+
Mods.RightShift,
95+
Mods.RightAlt,
96+
Mods.RightGUI,
97+
];
98+
99+
function mods_to_flags(mods: Mods[]): number {
100+
return mods.reduce((a, v) => a + v, 0);
101+
}
102+
103+
function mask_mods(value: number) {
104+
return (value & ~(mods_to_flags(all_mods) << 24));
105+
}
106+
64107
export const HidUsagePicker = ({
65108
label,
66109
value,
67110
usagePages,
68111
onValueChanged,
69112
}: HidUsagePickerProps) => {
113+
const mods = useMemo(() => {
114+
let flags = value ? (value >> 24) : 0;
115+
116+
return all_mods.filter(m => m & flags).map(m => m.toLocaleString());
117+
}, [value]);
118+
70119
const selectionChanged = useCallback(
71120
(e: Key | null) => {
72121
let value = typeof e == "number" ? e : undefined;
122+
if (value !== undefined) {
123+
let mod_flags = mods_to_flags(mods.map(m => parseInt(m)));
124+
value = value | (mod_flags << 24);
125+
}
126+
73127
onValueChanged(value);
74128
},
75-
[onValueChanged],
129+
[onValueChanged, mods],
76130
);
77131

132+
const modifiersChanged = useCallback((m: string[]) => {
133+
if (!value) {
134+
return;
135+
}
136+
137+
let mod_flags = mods_to_flags(m.map(m => parseInt(m)));
138+
let new_value = mask_mods(value) | (mod_flags << 24);
139+
onValueChanged(new_value);
140+
}, [value]);
141+
78142
return (
79-
<ComboBox selectedKey={value} onSelectionChange={selectionChanged}>
80-
{ label && <Label>{label}</Label> }
81-
<div>
82-
<Input className="p-1 rounded" />
83-
<Button className="ml-[-1.75em] px-[0.25em] py-0 rounded rounded-md bg-accent w-6 h-6">
84-
85-
</Button>
86-
</div>
87-
<Popover className="w-[var(--trigger-width)] max-h-4 border rounded border-text-base bg-bg-base">
88-
<ListBox
89-
items={usagePages}
90-
className="block max-h-[30vh] min-h-[unset] overflow-auto m-2"
91-
selectionMode="single"
92-
>
93-
{({id, min, max}) => <UsageSection id={id} min={min} max={max} />}
94-
</ListBox>
95-
</Popover>
96-
</ComboBox>
143+
<div className="flex mt-8 relative">
144+
<ComboBox selectedKey={value ? mask_mods(value) : undefined} onSelectionChange={selectionChanged}>
145+
{ label && <Label className="absolute top-[-1.75em] left-[0.25em]">{label}</Label> }
146+
<div>
147+
<Input className="p-1 rounded" />
148+
<Button className="ml-[-1.75em] px-[0.25em] py-0 rounded rounded-md bg-accent w-6 h-6">
149+
150+
</Button>
151+
</div>
152+
<Popover className="w-[var(--trigger-width)] max-h-4 border rounded border-text-base bg-bg-base">
153+
<ListBox
154+
items={usagePages}
155+
className="block max-h-[30vh] min-h-[unset] overflow-auto m-2"
156+
selectionMode="single"
157+
>
158+
{({id, min, max}) => <UsageSection id={id} min={min} max={max} />}
159+
</ListBox>
160+
</Popover>
161+
</ComboBox>
162+
<CheckboxGroup aria-label="Implicit Modifiers" className="grid grid-flow-col gap-x-px auto-cols-fr content-stretch divide-x rounded border border-text-base" value={mods} onChange={modifiersChanged}>
163+
{ all_mods.map(m => <Checkbox key={m} value={m.toLocaleString()} className="text-nowrap grid content-center justify-center rac-selected:bg-secondary first:rounded-s last:rounded-e">{mod_labels[m]}</Checkbox> )}
164+
</CheckboxGroup>
165+
</div>
97166
);
98167
};

0 commit comments

Comments
 (0)