Skip to content

Commit

Permalink
fix(zoom): making zoom in+out a usable feature
Browse files Browse the repository at this point in the history
- feat: keyboard viewport + tools to pan-around and reset
  • Loading branch information
pongstr committed Dec 22, 2024
1 parent e5ab3b5 commit e617a01
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 72 deletions.
45 changes: 12 additions & 33 deletions src/keyboard/Keyboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ import { BehaviorBindingPicker } from "../behaviors/BehaviorBindingPicker";
import { produce } from "immer";
import { LockStateContext } from "../rpc/LockStateContext";
import { LockState } from "@zmkfirmware/zmk-studio-ts-client/core";
import { deserializeLayoutZoom, LayoutZoom } from "./PhysicalLayout";
import { useLocalStorageState } from "../misc/useLocalStorageState";
import { KeyboardViewport } from "./KeyboardViewport";

type BehaviorMap = Record<number, GetBehaviorDetailsResponse>;

Expand Down Expand Up @@ -174,10 +173,6 @@ export default function Keyboard() {
true
);

const [keymapScale, setKeymapScale] = useLocalStorageState<LayoutZoom>("keymapScale", "auto", {
deserialize: deserializeLayoutZoom,
});

const [selectedLayerIndex, setSelectedLayerIndex] = useState<number>(0);
const [selectedKeyPosition, setSelectedKeyPosition] = useState<
number | undefined
Expand Down Expand Up @@ -520,33 +515,17 @@ export default function Keyboard() {
)}
</div>
{layouts && keymap && behaviors && (
<div className="p-2 col-start-2 row-start-1 grid items-center justify-center relative min-w-0">
<KeymapComp
keymap={keymap}
layout={layouts[selectedPhysicalLayoutIndex]}
behaviors={behaviors}
scale={keymapScale}
selectedLayerIndex={selectedLayerIndex}
selectedKeyPosition={selectedKeyPosition}
onKeyPositionClicked={setSelectedKeyPosition}
/>
<select
className="absolute top-2 right-2 h-8 rounded px-2"
value={keymapScale}
onChange={(e) => {
const value = deserializeLayoutZoom(e.target.value);
setKeymapScale(value);
}}
>
<option value="auto">Auto</option>
<option value={0.25}>25%</option>
<option value={0.5}>50%</option>
<option value={0.75}>75%</option>
<option value={1}>100%</option>
<option value={1.25}>125%</option>
<option value={1.5}>150%</option>
<option value={2}>200%</option>
</select>
<div className="col-start-2 row-start-1 flex items-center justify-center relative min-w-0">
<KeyboardViewport>
<KeymapComp
keymap={keymap}
layout={layouts[selectedPhysicalLayoutIndex]}
behaviors={behaviors}
selectedLayerIndex={selectedLayerIndex}
selectedKeyPosition={selectedKeyPosition}
onKeyPositionClicked={setSelectedKeyPosition}
/>
</KeyboardViewport>
</div>
)}
{keymap && selectedBinding && (
Expand Down
153 changes: 153 additions & 0 deletions src/keyboard/KeyboardViewport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { FC, PropsWithChildren, useEffect, useRef } from "react";

type KeyboardViewportType = PropsWithChildren<{
className?: string;
}>;

const KEYMAP_SCALE = "keymap:scale";
const DEFAULT_SCALE = window.localStorage.getItem(KEYMAP_SCALE) ?? "1";

export const KeyboardViewport: FC<KeyboardViewportType> = ({
children,
className,
}) => {
const targetRef = useRef<HTMLDivElement>(null);
const scaleRef = useRef<HTMLInputElement>(null);

const setScale = (param: "increase" | "decrease") => {
if (!targetRef.current || !scaleRef.current) return;

const current = scaleRef.current.value;

if (param === "increase" && Number(current) < 2) {
scaleRef.current.value = String(Number(scaleRef.current.value) + 0.2);
}

if (param === "decrease" && Number(current) > 0.2) {
scaleRef.current.value = String(Number(scaleRef.current.value) - 0.2);
}

localStorage.setItem(KEYMAP_SCALE, scaleRef.current.value);
targetRef.current.style.setProperty(
"transform",
`scale(${scaleRef.current.value})`,
);
};

const resetScale = () => {
if (!targetRef.current || !scaleRef.current) return;
targetRef.current.style.translate = "unset";
targetRef.current.style.setProperty("transform", "scale(1)");
scaleRef.current.value = "1";
localStorage.setItem(KEYMAP_SCALE, "1");
};

useEffect(() => {
if (!targetRef.current) return;

const target = targetRef.current;
const offset = { x: 0, y: 0 };
let isPanningActive = false;

function panStart(e: KeyboardEvent) {
if (e.key !== " ") return;
e.preventDefault();

target.style.cursor = "grab";
isPanningActive = true;
}

function panEnd(e: KeyboardEvent) {
if (e.key !== " ") return;
isPanningActive = false;
target.style.cursor = "unset";
}

function panMove(e: PointerEvent) {
if (!isPanningActive) return;
offset.x += e.movementX;
offset.y += e.movementY;
target.style.translate = `${offset.x}px ${offset.y}px`;
}

document.addEventListener("keydown", panStart);
document.addEventListener("keyup", panEnd);
target.addEventListener("pointermove", panMove);

return () => {
document.removeEventListener("keydown", panStart);
document.removeEventListener("keyup", panEnd);
target.removeEventListener("pointermove", panMove);
};
}, []);

useEffect(() => {
if (!scaleRef.current || !targetRef.current) return;

const input = scaleRef.current;
const target = targetRef.current;

input.value = DEFAULT_SCALE;
target.style.setProperty("transform", `scale(${DEFAULT_SCALE})`);

function onInputChange(e: Event) {
const value = (e.currentTarget as HTMLInputElement).value;
target.style.setProperty("transform", `scale(${value})`);
localStorage.setItem(KEYMAP_SCALE, value);
}

input.addEventListener("change", onInputChange);
return () => {
input.removeEventListener("change", onInputChange);
};
}, []);

return (
<div
className={[
"relative size-full overflow-hidden p-0 touch-none",
className,
].join(" ")}
>
<div
ref={targetRef}
className="flex size-full origin-center items-center justify-center transition-transform"
>
{children}
</div>

<div className="absolute bottom-[10px] left-1/2 ml-[-170px] flex justify-center items-center w-[298px] gap-1 rounded-xl bg-muted py-1 select-none bg-base-300">
<button
className="block px-4 py-1.5 bg-base-100 rounded-l-lg"
onClick={() => setScale("decrease")}
>
-
</button>
<div className="flex h-9 px-2 justify-center items-center bg-base-100">
<input
type="range"
name="scale"
min={0.25}
max={2}
step={0.01}
ref={scaleRef}
defaultValue={DEFAULT_SCALE}
className="mx-auto h-1 w-28 cursor-pointer appearance-none rounded-lg"
/>
</div>
<button
className="block px-4 py-1.5 bg-base-100"
onClick={() => setScale("increase")}
>
+
</button>
<button
className="block px-4 py-1.5 bg-base-100 rounded-r-lg"
onClick={resetScale}
>
Auto
</button>
</div>
</div>
);
};
2 changes: 1 addition & 1 deletion src/keyboard/Keymap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface KeymapProps {
layout: PhysicalLayout;
keymap: KeymapMsg;
behaviors: BehaviorMap;
scale: LayoutZoom;
scale?: LayoutZoom;
selectedLayerIndex: number;
selectedKeyPosition: number | undefined;
onKeyPositionClicked: (keyPosition: number) => void;
Expand Down
38 changes: 0 additions & 38 deletions src/keyboard/PhysicalLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {
CSSProperties,
PropsWithChildren,
useLayoutEffect,
useRef,
useState,
} from "react";
import { Key } from "./Key";

Expand Down Expand Up @@ -78,41 +76,6 @@ export const PhysicalLayout = ({
...props
}: PhysicalLayoutProps) => {
const ref = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);

useLayoutEffect(() => {
const element = ref.current;
if (!element) return;

const parent = element.parentElement;
if (!parent) return;

const calculateScale = () => {
if (props.zoom === "auto") {
const padding = Math.min(window.innerWidth, window.innerHeight) * 0.05; // Padding when in auto mode
const newScale = Math.min(
parent.clientWidth / (element.clientWidth + 2 * padding),
parent.clientHeight / (element.clientHeight + 2 * padding),
);
setScale(newScale);
} else {
setScale(props.zoom || 1);
}
};

calculateScale(); // Initial calculation

const resizeObserver = new ResizeObserver(() => {
calculateScale();
});

resizeObserver.observe(element);
resizeObserver.observe(parent);

return () => {
resizeObserver.disconnect();
};
}, [props.zoom]);

// TODO: Add a bit of padding for rotation when supported
let rightMost = positions
Expand Down Expand Up @@ -145,7 +108,6 @@ export const PhysicalLayout = ({
style={{
height: bottomMost * oneU + "px",
width: rightMost * oneU + "px",
transform: `scale(${scale})`,
}}
ref={ref}
{...props}
Expand Down

0 comments on commit e617a01

Please sign in to comment.