Skip to content

Commit d66d560

Browse files
committed
feat: Various layout/key render work, theme fixes
* Incremental fixes to key/physical layouts, including more compact size until hovering over a key, some HID usage display, etc. * Fix up color usage for light/dark theme usage.
1 parent 34ac4f0 commit d66d560

15 files changed

+20128
-81
lines changed

src/HidUsageTables-1.5.json

Lines changed: 19819 additions & 0 deletions
Large diffs are not rendered by default.

src/RpcTest.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type { AvailableDevice } from './tauri/index';
1212
import { connect as tauri_ble_connect, list_devices as ble_list_devices } from './tauri/ble';
1313
import { connect as tauri_serial_connect, list_devices as serial_list_devices } from './tauri/serial';
1414

15+
import { hid_usage_get_label, hid_usage_page_and_id_from_usage } from './hid-usages';
16+
import { LayerPicker } from './keyboard/LayerPicker';
1517
import { PhysicalLayout as PhysicalLayoutComp } from './keyboard/PhysicalLayout';
1618

1719
type BehaviorMap = Record<number, GetBehaviorDetailsResponse>;
@@ -72,27 +74,41 @@ async function test(factory: TransportFactory, setPhysicalLayout: Dispatch<Physi
7274
console.log("Keymap", keymap);
7375
}
7476

75-
function renderLayout(layout: PhysicalLayout, keymap: Keymap, behaviors: BehaviorMap) {
76-
if (!keymap.layers[0]) {
77+
function renderLayout(layout: PhysicalLayout, keymap: Keymap, behaviors: BehaviorMap, selectedLayoutIndex: number) {
78+
console.log("Render", keymap, selectedLayoutIndex);
79+
if (!keymap.layers[selectedLayoutIndex]) {
7780
return (<></>);
7881
}
7982

80-
let positions = layout.keys.map((k, i) => ({ label: behaviors[keymap.layers[0].bindings[i].behaviorId]?.friendlyName || "Unknown", x: k.x / 100.0, y: k.y / 100.0, width: k.width / 100, height: k.height / 100.0}));
83+
let positions = layout.keys.map((k, i) => {
84+
let [page, id] = hid_usage_page_and_id_from_usage(keymap.layers[selectedLayoutIndex].bindings[i].param1);
85+
86+
// TODO: Do something with implicit mods!
87+
page &= 0xFF;
88+
89+
let label = hid_usage_get_label(page, id)?.replace(/^Keyboard /, '');
90+
91+
return { header: behaviors[keymap.layers[selectedLayoutIndex].bindings[i].behaviorId]?.friendlyName || "Unknown", x: k.x / 100.0, y: k.y / 100.0, width: k.width / 100, height: k.height / 100.0, children: (<span>{label}</span>)};
92+
});
8193

8294
return <PhysicalLayoutComp positions={positions} />;
8395
}
8496

8597
export default function RpcTest() {
8698
const [layout, setLayout] = useState<PhysicalLayout | undefined>(undefined);
8799
const [keymap, setKeymap] = useState<Keymap | undefined>(undefined);
100+
const [selectedLayerIndex, setSelectedLayerIndex] = useState<number>(0);
88101
const [behaviors, setBehaviors] = useState<BehaviorMap>({});
89102

90103
let connections = TRANSPORTS.filter((t) => t !== undefined).map((t) => <button key={t.label} onClick={() => test(t, setLayout, setKeymap, setBehaviors)}>{t.label}</button>);
91104

92105
return (
93106
<div>
94107
<div>{connections}</div>
95-
{layout && keymap && behaviors && (<div>{renderLayout(layout, keymap, behaviors)}</div>)}
108+
<div>
109+
{keymap && (<LayerPicker layers={keymap.layers} selectedLayerIndex={selectedLayerIndex} onLayerClicked={setSelectedLayerIndex} />) }
110+
{layout && keymap && behaviors && (<div>{renderLayout(layout, keymap, behaviors, selectedLayerIndex)}</div>)}
111+
</div>
96112
</div>
97113
)
98114
}

src/hid-usages.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
import { UsagePages } from './HidUsageTables-1.5.json';
3+
4+
5+
export interface UsageId {
6+
Id: number;
7+
Name: string;
8+
Kinds?: string[];
9+
};
10+
11+
export const hid_usage_page_and_id_from_usage = (usage: number): [number, number] => [((usage >> 16) & 0xFF), (usage & 0xFFFF)]
12+
13+
export const hid_usage_get_label = (usage_page: number, usage_id: number): string | undefined =>
14+
UsagePages.find((p) => p.Id === usage_page)?.UsageIds?.find((u) => u.Id === usage_id)?.Name;
15+

src/index.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
font-weight: 400;
55

66
color-scheme: light dark;
7-
color: rgba(255, 255, 255, 0.87);
8-
background-color: #242424;
7+
color: light-dark(#242424, rgba(255, 255, 255, 0.87));
8+
background-color: light-dark(rgba(255, 255, 255, 0.87), #242424);
99

1010
font-synthesis: none;
1111
text-rendering: optimizeLegibility;
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
import type { Meta, StoryObj } from '@storybook/react';
23
import { fn } from '@storybook/test';
34
import { Key } from './Key';
@@ -29,22 +30,25 @@ export const Primary: Story = {
2930
primary: true,
3031
width: 1,
3132
height: 1,
32-
label: 'Key',
33+
header: 'Key Press',
34+
children: [<span>A</span>],
3335
},
3436
};
3537

3638
export const Secondary: Story = {
3739
args: {
3840
width: 1,
3941
height: 1,
40-
label: 'Key',
42+
header: 'Key Press',
43+
children: [<span>B</span>],
4144
},
4245
};
4346

4447
export const Large: Story = {
4548
args: {
4649
width: 2,
4750
height: 1,
48-
label: 'Key',
51+
header: 'Key Press',
52+
children: [<span>C</span>],
4953
},
5054
};

src/keyboard/Key.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import './key.css';
22

3+
import { PropsWithChildren, Children } from 'react';
34
import { scale } from './geometry';
45

56
interface KeyProps {
@@ -15,7 +16,7 @@ interface KeyProps {
1516
/**
1617
* Button contents
1718
*/
18-
label: string;
19+
header: string;
1920
/**
2021
* Optional click handler
2122
*/
@@ -27,13 +28,13 @@ interface KeyDimension {
2728
height: number,
2829
};
2930

30-
function makeSize({ width, height}: KeyDimension): KeyDimension {
31+
function makeSize({ width, height}: KeyDimension) {
3132
width = scale(width);
3233
height = scale(height);
3334

3435
return {
35-
width,
36-
height
36+
'--zmk-key-center-width': 'calc(' + width + 'px - 2px)',
37+
'--zmk-key-center-height': 'calc(' + height + 'px - 2px)'
3738
};
3839
}
3940

@@ -42,11 +43,13 @@ function makeSize({ width, height}: KeyDimension): KeyDimension {
4243
*/
4344
export const Key = ({
4445
primary = false,
45-
label,
46+
header,
4647
...props
47-
}: KeyProps) => {
48+
}: PropsWithChildren<KeyProps>) => {
4849
const mode = primary ? 'zmk-key__button--primary' : 'zmk-key__button--secondary';
4950
const size = makeSize(props);
51+
const children = Children.map(props.children, (c) => <div className="zmk-key__button__child">{c}</div>);
52+
5053
return (
5154
<div
5255
className='zmk-key'
@@ -56,7 +59,8 @@ export const Key = ({
5659
type="button"
5760
className={['zmk-key__button', mode].join(' ')}
5861
>
59-
{label}
62+
<span className="zmk-key__button__header">{header}</span>
63+
{children}
6064
</button>
6165
</div>
6266
);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { fn } from '@storybook/test';
3+
import { LayerPicker } from './LayerPicker';
4+
5+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
6+
const meta = {
7+
title: 'Keyboard/LayerPicker',
8+
component: LayerPicker,
9+
parameters: {
10+
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
11+
layout: 'centered',
12+
},
13+
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
14+
tags: ['autodocs'],
15+
// More on argTypes: https://storybook.js.org/docs/api/argtypes
16+
argTypes: {
17+
// backgroundColor: { control: 'color' },
18+
},
19+
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
20+
args: { },
21+
} satisfies Meta<typeof LayerPicker>;
22+
23+
export default meta;
24+
type Story = StoryObj<typeof meta>;
25+
26+
export const Named: Story = {
27+
args: {
28+
layers: [
29+
{ label: "Base"},
30+
{ label: "Num"},
31+
{ label: "Nav"},
32+
{ label: "Symbol"},
33+
],
34+
selectedLayerIndex: 2,
35+
onLayerClicked: (layer, index) => console.log("Layer clicked", layer),
36+
onAddClicked: () => console.log("add item!"),
37+
},
38+
};
39+
40+
export const NoAdd: Story = {
41+
args: {
42+
layers: [
43+
{ label: "Base"},
44+
{ label: "Num"},
45+
{ label: "Nav"},
46+
{ label: "Symbol"},
47+
],
48+
selectedLayerIndex: 2,
49+
},
50+
};
51+
52+
export const NoNames: Story = {
53+
args: {
54+
layers: [
55+
{ },
56+
{ },
57+
{ },
58+
],
59+
selectedLayerIndex: 0,
60+
},
61+
};
62+

src/keyboard/LayerPicker.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import './layer-picker.css';
2+
3+
interface Layer {
4+
name?: string;
5+
}
6+
7+
type LayerClickCallback = (index: number) => void;
8+
9+
interface LayerPickerProps {
10+
layers: Array<Layer>;
11+
selectedLayerIndex: number;
12+
13+
onAddClicked?: () => void;
14+
15+
onLayerClicked?: LayerClickCallback;
16+
}
17+
18+
function renderItem(layer: Layer, index: number, selected: boolean, onClick?: LayerClickCallback) {
19+
let className = "zmk-layer-picker__item";
20+
if (selected) {
21+
className += " zmk-layer-picker__item--selected";
22+
}
23+
return <li className={className} onClick={() => onClick?.(index)}>{layer.name || index.toLocaleString() }</li>;
24+
}
25+
26+
/**
27+
* Primary UI component for user interaction
28+
*/
29+
export const LayerPicker = ({
30+
layers,
31+
selectedLayerIndex,
32+
onAddClicked,
33+
onLayerClicked,
34+
...props
35+
}: LayerPickerProps) => {
36+
let layer_items = layers.map((layer, index) => renderItem(layer, index, index === selectedLayerIndex, onLayerClicked));
37+
38+
return (
39+
<ul
40+
className='zmk-layer-picker'
41+
{...props}>
42+
{layer_items}
43+
{onAddClicked && <li className="zmk-layer-picker__add" onClick={onAddClicked}>+</li>}
44+
</ul>
45+
);
46+
};

src/keyboard/PhysicalLayout.stories.ts

Lines changed: 0 additions & 54 deletions
This file was deleted.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { PhysicalLayout } from './PhysicalLayout';
3+
4+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
5+
const meta = {
6+
title: 'Keyboard/PhysicalLayout',
7+
component: PhysicalLayout,
8+
parameters: {
9+
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
10+
},
11+
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
12+
tags: ['autodocs'],
13+
// More on argTypes: https://storybook.js.org/docs/api/argtypes
14+
argTypes: {
15+
},
16+
args: { },
17+
} satisfies Meta<typeof PhysicalLayout>;
18+
19+
export default meta;
20+
type Story = StoryObj<typeof meta>;
21+
22+
const TOP = ["Esc", ..."QWERTYUIOP"];
23+
const MIDDLE = [..."ASDFGHJKL;"];
24+
const LOWER = [..."ZXCVBNM<>", "Up", "Shift"];
25+
26+
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
27+
export const Minivan: Story = {
28+
args: {
29+
positions: [
30+
...TOP.map((k,i) => ({
31+
width: 1,
32+
height: 1,
33+
x: i,
34+
y: 0,
35+
header: "Key Press",
36+
children: [<span>{k}</span>]
37+
}))
38+
, { x: TOP.length, y: 0, width: 1.75, height: 1, header: "Key Press", children: [<span>Backspace</span>]}
39+
, { x: 0, y: 1, width: 1.25, height: 1, header: "Key Press", children: [<span>Tab</span>]}
40+
, ...MIDDLE.map((k,i) => ({ x: i+1.25, y: 1, width: 1, height: 1, header: "Key Press", children: [<span>{k}</span>]}))
41+
, { x: MIDDLE.length + 1.25, y: 1, width: 1.5, height: 1, header: "Key Press", children: [<span>Enter</span>]}
42+
, { x: 0, y: 2, width: 1.75, height: 1, header: "Key Press", children: [<span>Shift</span>]}
43+
, ...LOWER.map((k,i) => ({ x: i+1.75, y: 2, width: 1, height: 1, header: "Key Press", children: [<span>{k}</span>]}))
44+
, { x: 0, y: 3, width: 1.25, height: 1, header: "Key Press", children: [<span>Control</span>]}
45+
, { x: 1.25, y: 3, width: 1.5, height: 1, header: "Key Press", children: [<span>Code</span>]}
46+
, { x: 2.75, y: 3, width: 1.25, height: 1, header: "Key Press", children: [<span>Alt</span>]}
47+
, { x: 4, y: 3, width: 2.25, height: 1, header: "Key Press", children: [<span></span>]}
48+
, { x: 6.25, y: 3, width: 2, height: 1, header: "Key Press", children: [<span></span>]}
49+
, { x: 8.25, y: 3, width: 1.5, height: 1, header: "Key Press", children: [<span>Alt</span>]}
50+
, { x: 9.75, y: 3, width: 1, height: 1, header: "Key Press", children: [<span>Left</span>]}
51+
, { x: 10.75, y: 3, width: 1, height: 1, header: "Key Press", children: [<span>Down</span>]}
52+
, { x: 11.75, y: 3, width: 1, height: 1, header: "Key Press", children: [<span>Right</span>]}
53+
]
54+
},
55+
};

0 commit comments

Comments
 (0)