diff --git a/src/settings/SettingsView.tsx b/src/settings/SettingsView.tsx
index 2cd9f49f..9c0edcbd 100644
--- a/src/settings/SettingsView.tsx
+++ b/src/settings/SettingsView.tsx
@@ -2,8 +2,9 @@ import { useEffect, useRef } from "react";
import { Settings } from "lucide-react";
import type { AppSettings, AppState } from "../types";
import {
- SettingGroup, Toggle, accelerationOptions, dwellDelayOptions, keyRepeatDelayOptions,
- movementValue, pointerSpeedOptions, pointerSpeedValues, repeatIntervalOptions,
+ OptionGroup, SettingGroup, Toggle, accelerationOptions, dwellDelayOptions, keyRepeatDelayOptions,
+ movementValue, overlaySizeOptions, overlayVisibilityOptions, pointerSpeedOptions,
+ pointerSpeedValues, repeatIntervalOptions, secondsOptions,
} from "./controls";
import { UpdateControls, type UpdateAction } from "./UpdatesSection";
@@ -29,47 +30,33 @@ export function SettingsView({ state, settings, onChange, chooseTelemetry, updat
update("mouseRepeatEnabled", value)} />
-
Movement interval
- {repeatIntervalOptions.map((value) => update("moveRepeatIntervalMs", value)}>{value / 1000}s )}
-
-
Movement acceleration
- {accelerationOptions.map(({ value, label }) => update("mouseRepeatAccelerationDurationMs", value)}>{label} )}
-
-
Scroll interval
- {repeatIntervalOptions.map((value) => update("scrollRepeatIntervalMs", value)}>{value / 1000}s )}
-
+
legend="Movement interval" columns="four" disabled={!settings.mouseRepeatEnabled} options={secondsOptions(repeatIntervalOptions)} value={settings.moveRepeatIntervalMs} onChange={(next) => update("moveRepeatIntervalMs", next)} />
+ legend="Movement acceleration" columns="four" disabled={!settings.mouseRepeatEnabled} options={accelerationOptions} value={settings.mouseRepeatAccelerationDurationMs} onChange={(next) => update("mouseRepeatAccelerationDurationMs", next)} />
+ legend="Scroll interval" columns="four" disabled={!settings.mouseRepeatEnabled} options={secondsOptions(repeatIntervalOptions)} value={settings.scrollRepeatIntervalMs} onChange={(next) => update("scrollRepeatIntervalMs", next)} />
update("keyRepeatEnabled", value)} />
-
Delay before repeating
- {keyRepeatDelayOptions.map(({ value, label }) => update("keyRepeatInitialDelayMs", value)}>{label} )}
-
-
Key interval
- {repeatIntervalOptions.map((value) => update("keyRepeatIntervalMs", value)}>{value / 1000}s )}
-
+
legend="Delay before repeating" columns="four" disabled={!settings.keyRepeatEnabled} options={keyRepeatDelayOptions} value={settings.keyRepeatInitialDelayMs} onChange={(next) => update("keyRepeatInitialDelayMs", next)} />
+ legend="Key interval" columns="four" disabled={!settings.keyRepeatEnabled} options={secondsOptions(repeatIntervalOptions)} value={settings.keyRepeatIntervalMs} onChange={(next) => update("keyRepeatIntervalMs", next)} />
Holding a navigation key on the remote repeats it, like holding a key on a keyboard. Applies to the arrow keys, Tab, Backspace, Delete, Page Up, and Page Down.
update("dwellClickEnabled", value)} />
-
Dwell delay
- {dwellDelayOptions.map((value) => update("dwellClickDelayMs", value)}>{value / 1000}s )}
-
+
legend="Dwell delay" columns="five" disabled={!settings.dwellClickEnabled} options={secondsOptions(dwellDelayOptions)} value={settings.dwellClickDelayMs} onChange={(next) => update("dwellClickDelayMs", next)} />
After Android pointer movement stops, a countdown appears and performs one left click. Move again to rearm it.
{state.capabilities.cursorOverlay && <>
update("cursorOverlayEnabled", value)} />
-
Overlay visibility
- {(["onInput", "whileControlling"] as const).map((value) => update("cursorOverlayVisibility", value)}>{value === "onInput" ? "On input" : "While controlling"} )}
-
On input hides shortly after pointer activity stops. While controlling stays visible until the session ends.
-
Overlay size
- {(["small", "medium", "large"] as const).map((value) => update("cursorOverlaySize", value)}>{value[0].toUpperCase() + value.slice(1)} )}
-
+
legend="Overlay visibility" disabled={!settings.cursorOverlayEnabled} options={overlayVisibilityOptions} value={settings.cursorOverlayVisibility} onChange={(next) => update("cursorOverlayVisibility", next)}>
+ On input hides shortly after pointer activity stops. While controlling stays visible until the session ends.
+
+
legend="Overlay size" columns="three" disabled={!settings.cursorOverlayEnabled} options={overlaySizeOptions} value={settings.cursorOverlaySize} onChange={(next) => update("cursorOverlaySize", next)} />
Overlay color
{(["red", "green", "blue", "yellow", "white"] as const).map((value) => update("cursorOverlayColor", value)} />{value[0].toUpperCase() + value.slice(1)} )}
diff --git a/src/settings/controls.tsx b/src/settings/controls.tsx
index 905c0c35..dd091bbe 100644
--- a/src/settings/controls.tsx
+++ b/src/settings/controls.tsx
@@ -30,3 +30,29 @@ export function movementValue(base: number, scale: number) {
const value = Math.min(50, Math.max(1, Math.round((base * scale / 100) * 2) / 2));
return Number.isInteger(value) ? String(value) : value.toFixed(1);
}
+
+export function OptionGroup({ legend, options, value, onChange, disabled, columns, children }: {
+ legend: string;
+ options: ReadonlyArray<{ value: T; label: ReactNode }>;
+ value: T;
+ onChange: (next: T) => void;
+ disabled: boolean;
+ columns?: "three" | "four" | "five";
+ children?: ReactNode;
+}) {
+ return {legend}
+ {options.map((option) => onChange(option.value)}>{option.label} )}
+
{children} ;
+}
+
+export function secondsOptions(values: readonly T[]) {
+ return values.map((value) => ({ value, label: <>{value / 1000}s> }));
+}
+
+export const overlayVisibilityOptions = [
+ { value: "onInput", label: "On input" },
+ { value: "whileControlling", label: "While controlling" },
+] as const;
+
+export const overlaySizeOptions = (["small", "medium", "large"] as const)
+ .map((value) => ({ value, label: value[0].toUpperCase() + value.slice(1) }));