From e617a01614183ba22e080bd6052ab517dd546210 Mon Sep 17 00:00:00 2001 From: Pongstr Date: Sun, 22 Dec 2024 19:01:16 +0200 Subject: [PATCH 1/3] fix(zoom): making zoom in+out a usable feature - feat: keyboard viewport + tools to pan-around and reset --- src/keyboard/Keyboard.tsx | 45 +++------ src/keyboard/KeyboardViewport.tsx | 153 ++++++++++++++++++++++++++++++ src/keyboard/Keymap.tsx | 2 +- src/keyboard/PhysicalLayout.tsx | 38 -------- 4 files changed, 166 insertions(+), 72 deletions(-) create mode 100644 src/keyboard/KeyboardViewport.tsx diff --git a/src/keyboard/Keyboard.tsx b/src/keyboard/Keyboard.tsx index e8ffaf7..4af0862 100644 --- a/src/keyboard/Keyboard.tsx +++ b/src/keyboard/Keyboard.tsx @@ -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; @@ -174,10 +173,6 @@ export default function Keyboard() { true ); - const [keymapScale, setKeymapScale] = useLocalStorageState("keymapScale", "auto", { - deserialize: deserializeLayoutZoom, - }); - const [selectedLayerIndex, setSelectedLayerIndex] = useState(0); const [selectedKeyPosition, setSelectedKeyPosition] = useState< number | undefined @@ -520,33 +515,17 @@ export default function Keyboard() { )} {layouts && keymap && behaviors && ( -
- - +
+ + +
)} {keymap && selectedBinding && ( diff --git a/src/keyboard/KeyboardViewport.tsx b/src/keyboard/KeyboardViewport.tsx new file mode 100644 index 0000000..458b308 --- /dev/null +++ b/src/keyboard/KeyboardViewport.tsx @@ -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 = ({ + children, + className, +}) => { + const targetRef = useRef(null); + const scaleRef = useRef(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 ( +
+
+ {children} +
+ +
+ +
+ +
+ + +
+
+ ); +}; diff --git a/src/keyboard/Keymap.tsx b/src/keyboard/Keymap.tsx index 4d457b1..2db6617 100644 --- a/src/keyboard/Keymap.tsx +++ b/src/keyboard/Keymap.tsx @@ -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; diff --git a/src/keyboard/PhysicalLayout.tsx b/src/keyboard/PhysicalLayout.tsx index 6c7d98c..9d05847 100644 --- a/src/keyboard/PhysicalLayout.tsx +++ b/src/keyboard/PhysicalLayout.tsx @@ -1,9 +1,7 @@ import { CSSProperties, PropsWithChildren, - useLayoutEffect, useRef, - useState, } from "react"; import { Key } from "./Key"; @@ -78,41 +76,6 @@ export const PhysicalLayout = ({ ...props }: PhysicalLayoutProps) => { const ref = useRef(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 @@ -145,7 +108,6 @@ export const PhysicalLayout = ({ style={{ height: bottomMost * oneU + "px", width: rightMost * oneU + "px", - transform: `scale(${scale})`, }} ref={ref} {...props} From f76a3a347ae01daf395a299afa1459fe2ffe67b0 Mon Sep 17 00:00:00 2001 From: Pongstr Date: Mon, 23 Dec 2024 13:42:31 +0200 Subject: [PATCH 2/3] fix: code review revisions --- src/keyboard/KeyboardViewport.tsx | 114 +++++++++++++----------------- src/misc/useLocalStorageState.ts | 42 +++++------ 2 files changed, 73 insertions(+), 83 deletions(-) diff --git a/src/keyboard/KeyboardViewport.tsx b/src/keyboard/KeyboardViewport.tsx index 458b308..bc4913b 100644 --- a/src/keyboard/KeyboardViewport.tsx +++ b/src/keyboard/KeyboardViewport.tsx @@ -1,45 +1,27 @@ import { FC, PropsWithChildren, useEffect, useRef } from "react"; +import { useLocalStorageState } from "../misc/useLocalStorageState"; +import { ExpandIcon, MaximizeIcon, ShrinkIcon } from "lucide-react"; type KeyboardViewportType = PropsWithChildren<{ className?: string; }>; -const KEYMAP_SCALE = "keymap:scale"; -const DEFAULT_SCALE = window.localStorage.getItem(KEYMAP_SCALE) ?? "1"; +const KEYMAP_SCALE = "keymapScale"; +const DEFAULT_SCALE = 1; export const KeyboardViewport: FC = ({ children, className, }) => { const targetRef = useRef(null); - const scaleRef = useRef(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 [scale, setScale] = useLocalStorageState(KEYMAP_SCALE, DEFAULT_SCALE); const resetScale = () => { - if (!targetRef.current || !scaleRef.current) return; + if (!targetRef.current) return; targetRef.current.style.translate = "unset"; targetRef.current.style.setProperty("transform", "scale(1)"); - scaleRef.current.value = "1"; - localStorage.setItem(KEYMAP_SCALE, "1"); + setScale(DEFAULT_SCALE); }; useEffect(() => { @@ -49,7 +31,7 @@ export const KeyboardViewport: FC = ({ const offset = { x: 0, y: 0 }; let isPanningActive = false; - function panStart(e: KeyboardEvent) { + function keyDownPanStart(e: KeyboardEvent) { if (e.key !== " ") return; e.preventDefault(); @@ -57,7 +39,15 @@ export const KeyboardViewport: FC = ({ isPanningActive = true; } - function panEnd(e: KeyboardEvent) { + function pointerDownPanStart(e: PointerEvent) { + if (e.button !== 0) return; + e.preventDefault(); + + target.style.cursor = "grab"; + isPanningActive = true; + } + + function keyUpPanEnd(e: KeyboardEvent) { if (e.key !== " ") return; isPanningActive = false; target.style.cursor = "unset"; @@ -70,35 +60,27 @@ export const KeyboardViewport: FC = ({ 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; + function pointerUpPanEnd() { + isPanningActive = false; + target.style.cursor = "unset"; + } - input.value = DEFAULT_SCALE; - target.style.setProperty("transform", `scale(${DEFAULT_SCALE})`); + document.addEventListener("keydown", keyDownPanStart); + document.addEventListener("keyup", keyUpPanEnd); - function onInputChange(e: Event) { - const value = (e.currentTarget as HTMLInputElement).value; - target.style.setProperty("transform", `scale(${value})`); - localStorage.setItem(KEYMAP_SCALE, value); - } + target.addEventListener("pointermove", panMove); + target.addEventListener("pointerdown", pointerDownPanStart); + target.addEventListener("pointerup", pointerUpPanEnd); + target.addEventListener("pointerleave", pointerUpPanEnd); - input.addEventListener("change", onInputChange); return () => { - input.removeEventListener("change", onInputChange); + document.removeEventListener("keydown", keyDownPanStart); + document.removeEventListener("keyup", keyUpPanEnd); + + target.removeEventListener("pointermove", panMove); + target.removeEventListener("pointerdown", pointerDownPanStart); + target.removeEventListener("pointerup", pointerUpPanEnd); + target.removeEventListener("pointerleave", pointerUpPanEnd); }; }, []); @@ -111,17 +93,20 @@ export const KeyboardViewport: FC = ({ >
{children}
-
+
= ({ 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" + value={scale} + onChange={(e) => setScale(Number(e.target.value))} />
diff --git a/src/misc/useLocalStorageState.ts b/src/misc/useLocalStorageState.ts index e066212..064be3c 100644 --- a/src/misc/useLocalStorageState.ts +++ b/src/misc/useLocalStorageState.ts @@ -1,10 +1,15 @@ import { useEffect, useState } from "react"; function basicSerialize(value: T): string { - if (typeof value === "object") { - return JSON.stringify(value); + return typeof value !== "string" ? JSON.stringify(value) : String(value); +} + +function toJson(value: string): T { + try { + return JSON.parse(value) as T; + } catch { + return value as T; } - return String(value); } export function useLocalStorageState( @@ -14,25 +19,22 @@ export function useLocalStorageState( serialize?: (value: T) => string; deserialize?: (value: string) => T; }, -) { - const reactState = useState(() => { - const savedValue = localStorage.getItem(key); - if (savedValue !== null) { - if (options?.deserialize) { - return options.deserialize(savedValue); - } - return savedValue as T; // Assuming T is a string - } - return defaultValue; - }); +): [T, React.Dispatch>] { + const [state, setState] = useState(() => { + const saved = localStorage.getItem(key); - const [state] = reactState; + if (saved === null) return defaultValue; + return ( + options?.deserialize?.(saved) ?? (toJson(saved) as T) ?? defaultValue + ); + }); useEffect(() => { - const serializedState = - options?.serialize?.(state) || basicSerialize(state); - localStorage.setItem(key, serializedState); - }, [state, key, options]); + localStorage.setItem( + key, + options?.serialize?.(state) ?? basicSerialize(state), + ); + }, [key, state, options]); - return reactState; + return [state, setState]; } From cedbd0316d83b8cfa63c0123785ea39c5dad8f5c Mon Sep 17 00:00:00 2001 From: Pongstr Date: Sun, 29 Dec 2024 23:06:19 +0200 Subject: [PATCH 3/3] chore: small tweak to improve input range visual output --- src/keyboard/KeyboardViewport.tsx | 72 ++++++++++++++++--------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/keyboard/KeyboardViewport.tsx b/src/keyboard/KeyboardViewport.tsx index bc4913b..87ddd17 100644 --- a/src/keyboard/KeyboardViewport.tsx +++ b/src/keyboard/KeyboardViewport.tsx @@ -99,42 +99,44 @@ export const KeyboardViewport: FC = ({ {children}
-
- -
- setScale(Number(e.target.value))} - /> +
+
+ +
+ setScale(Number(e.target.value))} + /> +
+ +
- -
);