Skip to content

Commit

Permalink
[pluto] - added 'double' option to triggers to allow for detecting do…
Browse files Browse the repository at this point in the history
…uble press/click
  • Loading branch information
emilbon99 committed Sep 26, 2024
1 parent 784249e commit cb53272
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 46 deletions.
5 changes: 3 additions & 2 deletions console/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ const PREVENT_DEFAULT_TRIGGERS: Triggers.Trigger[] = [
["Control", "W"],
];

const triggersProps: Triggers.ProviderProps = {
const TRIGGERS_PROVIDER_PROPS: Triggers.ProviderProps = {
preventDefaultOn: PREVENT_DEFAULT_TRIGGERS,
preventDefaultOptions: { double: true },
};

const client = new QueryClient();
Expand Down Expand Up @@ -107,7 +108,7 @@ const MainUnderContext = (): ReactElement => {
workerEnabled
connParams={cluster?.props}
workerURL={WorkerURL}
triggers={triggersProps}
triggers={TRIGGERS_PROVIDER_PROPS}
haul={{ useState: useHaulState }}
alamos={{
level: "debug",
Expand Down
2 changes: 0 additions & 2 deletions pluto/src/align/Pack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ import { type ForwardedRef, forwardRef, type ReactElement } from "react";

import { Space, type SpaceElementType, type SpaceProps } from "@/align/Space";
import { CSS } from "@/css";
import { Text } from "@/text";

/** Props for the {@link Pack} component. */
export type PackProps<E extends SpaceElementType = "div"> = Omit<
SpaceProps<E>,
"empty"
> & {
shadow?: boolean;
borderShade?: Text.Shade;
};

const CorePack = <E extends SpaceElementType = "div">(
Expand Down
2 changes: 2 additions & 0 deletions pluto/src/align/Space.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface SpaceExtensionProps {
wrap?: boolean;
el?: SpaceElementType;
bordered?: boolean;
borderShade?: Theming.Shade;
rounded?: boolean;
background?: Theming.Shade;
}
Expand Down Expand Up @@ -93,6 +94,7 @@ const CoreSpace = <E extends SpaceElementType>(
direction: direction_ = "y",
wrap = false,
bordered = false,
borderShade,
rounded = false,
el = "div",
background,
Expand Down
4 changes: 2 additions & 2 deletions pluto/src/list/Hover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ export const Hover = <K extends Key = Key, E extends Keyed<K> = Keyed<K>>({
}
const move = () => {
const data = getData();
if (Triggers.match(triggers, [UP_TRIGGER], true))
if (Triggers.match(triggers, [UP_TRIGGER], { loose: true }))
setHover((pos) => (pos <= 0 ? data.length - 1 : pos - 1));
else if (Triggers.match(triggers, [DOWN_TRIGGER], true))
else if (Triggers.match(triggers, [DOWN_TRIGGER], { loose: true }))
setHover((pos) => (pos >= data.length - 1 ? 0 : pos + 1));
};
move();
Expand Down
29 changes: 15 additions & 14 deletions pluto/src/triggers/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type Callback,
eventKey,
match,
MatchOptions,
MOUSE_KEYS,
type MouseKey,
type Trigger,
Expand Down Expand Up @@ -59,6 +60,7 @@ const EXCLUDE_TRIGGERS = ["CapsLock"];

export interface ProviderProps extends PropsWithChildren {
preventDefaultOn?: Trigger[];
preventDefaultOptions?: MatchOptions;
}

const shouldNotTriggerOnKeyDown = (key: string, e: KeyboardEvent): boolean => {
Expand All @@ -73,6 +75,7 @@ const shouldNotTriggerOnKeyDown = (key: string, e: KeyboardEvent): boolean => {
export const Provider = ({
children,
preventDefaultOn,
preventDefaultOptions,
}: ProviderProps): ReactElement => {
// We track mouse movement to allow for cursor position on keybord events;
const cursor = useRef<xy.XY>(xy.ZERO);
Expand Down Expand Up @@ -115,7 +118,8 @@ export const Provider = ({
prev: prev.next,
last: new TimeStamp(),
};
if (shouldPreventDefault(next, preventDefaultOn)) e.preventDefault();
if (shouldPreventDefault(next, preventDefaultOn, preventDefaultOptions))
e.preventDefault();
updateListeners(nextState, e.target as HTMLElement);
return nextState;
});
Expand All @@ -132,12 +136,9 @@ export const Provider = ({
(k) => k !== key && !MOUSE_KEYS.includes(k as MouseKey),
);
const prev = prevS.next;
const nextS: RefState = {
...prevS,
next,
prev,
};
if (shouldPreventDefault(next, preventDefaultOn)) e.preventDefault();
const nextS: RefState = { ...prevS, next, prev };
if (shouldPreventDefault(next, preventDefaultOn, preventDefaultOptions))
e.preventDefault();
updateListeners(nextS, e.target as HTMLElement);
return nextS;
});
Expand All @@ -151,11 +152,7 @@ export const Provider = ({
const handlePageVisibility = useCallback((event: Event): void => {
setCurr((prevS) => {
const prev = prevS.next;
const nextS: RefState = {
...prevS,
next: [],
prev,
};
const nextS: RefState = { ...prevS, next: [], prev };
updateListeners(nextS, event.target as HTMLElement);
return nextS;
});
Expand Down Expand Up @@ -190,5 +187,9 @@ export const Provider = ({
return <Context.Provider value={{ listen }}>{children}</Context.Provider>;
};

const shouldPreventDefault = (t: Trigger, preventDefaultOn?: Trigger[]): boolean =>
preventDefaultOn != null && match([t], preventDefaultOn);
const shouldPreventDefault = (
t: Trigger,
preventDefaultOn?: Trigger[],
preventDefaultOptions?: MatchOptions,
): boolean =>
preventDefaultOn != null && match([t], preventDefaultOn, preventDefaultOptions);
18 changes: 10 additions & 8 deletions pluto/src/triggers/Triggers.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ describe("Triggers", () => {
["A", "C"],
],
[["A", "D"]],
true,
{ loose: true },
),
).toEqual([]);
});
it("Should return a list of triggers that match", () => {
expect(Triggers.filter([["A"], ["A", "C"]], [["A", "B"]], true)).toEqual([
["A"],
]);
expect(
Triggers.filter([["A"], ["A", "C"]], [["A", "B"]], { loose: true }),
).toEqual([["A"]]);
});
it("should return an empty list when no triggers match", () => {
expect(
Expand All @@ -83,7 +83,7 @@ describe("Triggers", () => {
["A", "C"],
],
[["A"]],
true,
{ loose: true },
),
).toEqual([]);
});
Expand Down Expand Up @@ -141,10 +141,12 @@ describe("Triggers", () => {
a: [["Shift"]],
b: [["Shift", "Control"]],
};
expect(Triggers.determineMode(config, [["Shift", "Control"]], true)).toEqual(
"b",
expect(
Triggers.determineMode(config, [["Shift", "Control"]], { loose: true }),
).toEqual("b");
expect(Triggers.determineMode(config, [["Shift"]], { loose: true })).toEqual(
"a",
);
expect(Triggers.determineMode(config, [["Shift"]], true)).toEqual("a");
});
});
});
Expand Down
26 changes: 19 additions & 7 deletions pluto/src/triggers/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ import {
import { useStateRef } from "@/hooks/ref";
import { useMemoCompare } from "@/memo";
import { useContext } from "@/triggers/Provider";
import { diff, filter, purge, type Stage, type Trigger } from "@/triggers/triggers";
import {
diff,
filter,
MatchOptions,
purge,
type Stage,
type Trigger,
} from "@/triggers/triggers";

export interface UseEvent {
target: HTMLElement;
Expand All @@ -28,14 +35,19 @@ export interface UseEvent {
cursor: xy.XY;
}

export interface UseProps {
export interface UseProps extends MatchOptions {
triggers?: Trigger[];
region?: RefObject<HTMLElement>;
loose?: boolean;
callback?: (e: UseEvent) => void;
}

export const use = ({ triggers, callback: f, region, loose }: UseProps): void => {
export const use = ({
triggers,
callback: f,
region,
loose,
double,
}: UseProps): void => {
const { listen } = useContext();
const memoTriggers = useMemoCompare(
() => triggers,
Expand All @@ -50,8 +62,8 @@ export const use = ({ triggers, callback: f, region, loose }: UseProps): void =>
useEffect(() => {
if (memoTriggers == null || memoTriggers.length === 0) return;
return listen((e) => {
const prevMatches = filter(memoTriggers, e.prev, /* loose */ loose);
const nextMatches = filter(memoTriggers, e.next, /* loose */ loose);
const prevMatches = filter(memoTriggers, e.prev, { loose, double });
const nextMatches = filter(memoTriggers, e.next, { loose, double });
const res = diff(nextMatches, prevMatches);
let added = res[0];
const removed = res[1];
Expand All @@ -61,7 +73,7 @@ export const use = ({ triggers, callback: f, region, loose }: UseProps): void =>
if (added.length > 0) f?.({ ...base, stage: "start", triggers: added });
if (removed.length > 0) f?.({ ...base, stage: "end", triggers: removed });
});
}, [f, memoTriggers, listen, loose, region]);
}, [f, memoTriggers, listen, loose, region, double]);
};

const filterInRegion = (
Expand Down
39 changes: 30 additions & 9 deletions pluto/src/triggers/triggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,21 @@ const MOUSE_BUTTONS: Record<number, MouseKey> = {
*/
export const mouseKey = (button: number): Key => MOUSE_BUTTONS[button] ?? "MouseLeft";

export interface MatchOptions {
/**
* If true, triggers in actual that are a superset of those in expected will still be
* considered a match i.e. if expected is [["Control"]] and actual is [["Control", "A"]],
* then match will return true.
*/
loose?: boolean;
/**
* If true, triggers in actual that are a double press of those in expected will still
* be considered a match i.e. if expected is [["Control", "W"]] and actual is
* [["Control", "W", "W"]], then match will return true.
*/
double?: boolean;
}

/**
* Match compares the expected triggers against the actual triggers.
*
Expand All @@ -234,8 +249,11 @@ export const mouseKey = (button: number): Key => MOUSE_BUTTONS[button] ?? "Mouse
* @returns true if any triggers in expected match those in actual.
*
*/
export const match = (expected: Trigger[], actual: Trigger[], loose = false): boolean =>
filter(expected, actual, loose).length > 0;
export const match = (
expected: Trigger[],
actual: Trigger[],
opts?: MatchOptions,
): boolean => filter(expected, actual, opts).length > 0;

export const matchCallback =
<E extends KeyboardEvent | MouseEvent | React.KeyboardEvent | React.MouseEvent>(
Expand All @@ -259,9 +277,9 @@ export const matchCallback =
export const filter = (
expected: Trigger[],
actual: Trigger[],
loose = false,
opts?: MatchOptions,
): Trigger[] => {
const f = compareF(loose);
const f = compareF(opts);
return expected.filter((o) => actual.some((t) => f(o, t) === compare.EQUAL));
};

Expand All @@ -287,7 +305,7 @@ export const purge = (source: Trigger[], toPurge: Trigger[]): Trigger[] =>
* and the second element is the triggers in b that are not in a.
*/
export const diff = (a: Trigger[], b: Trigger[]): [Trigger[], Trigger[]] => {
const f = compareF(false);
const f = compareF();
const added = a.filter((ta) => !b.some((tb) => f(ta, tb) === compare.EQUAL));
const removed = b.filter((tb) => !a.some((ta) => f(tb, ta) === compare.EQUAL));
return [added, removed];
Expand All @@ -299,8 +317,11 @@ export const diff = (a: Trigger[], b: Trigger[]): [Trigger[], Trigger[]] => {
* the triggers will be considered equal.
* @returns a comparison function that determines if two triggers are semantically equal.
*/
const compareF = (loose: boolean): compare.CompareF<Trigger> =>
loose ? _looseCompare : compare.unorderedPrimitiveArrays;
const compareF = (opts?: MatchOptions): compare.CompareF<Trigger> => {
if (opts?.loose === true) return _looseCompare;
if (opts?.double === true) return compare.uniqueUnorderedPrimitiveArrays;
return compare.unorderedPrimitiveArrays;
};

const _looseCompare: compare.CompareF<Trigger> = (a, b) =>
a.every((k) => b.includes(k)) ? compare.EQUAL : compare.LESS_THAN;
Expand All @@ -324,14 +345,14 @@ export type ModeConfig<M extends string | number | symbol> = Record<M, Trigger[]
export const determineMode = <K extends string | number | symbol>(
config: ModeConfig<K>,
triggers: Trigger[],
loose = false,
opts?: MatchOptions,
): K => {
const e = Object.entries(config).filter(
([k]) => k !== "defaultMode",
) as unknown as Array<[K, Trigger[]]>;
const flat = e.map(([k, v]) => v.map((t) => [k, t])).flat() as Array<[K, Trigger]>;
const complexitySorted = flat.sort(([, a], [, b]) => b.length - a.length);
const match_ = complexitySorted.find(([, v]) => match([v], triggers, loose));
const match_ = complexitySorted.find(([, v]) => match([v], triggers, opts));
if (match_ != null) return match_[0];
return config.defaultMode;
};
Expand Down
3 changes: 1 addition & 2 deletions pluto/src/vis/measure/Measure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const MEASURE_TRIGGERS: Triggers.ModeConfig<ClickMode> = {

const REDUCED_MEASURE_TRIGGERS = Triggers.flattenConfig(MEASURE_TRIGGERS);


export interface MeasureProps {}

export const Measure = Aether.wrap<MeasureProps>("Measure", ({ aetherKey }) => {
Expand Down Expand Up @@ -56,7 +55,7 @@ export const Measure = Aether.wrap<MeasureProps>("Measure", ({ aetherKey }) => {
const measureMode = Triggers.determineMode<ClickMode>(
MEASURE_TRIGGERS,
triggers.current.triggers,
true,
{ loose: true },
);
if (["one", "two"].includes(measureMode))
return setState((p) => ({ ...p, [measureMode]: cursor }));
Expand Down
10 changes: 10 additions & 0 deletions x/ts/src/compare/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { isStringer, type Primitive } from "@/primitive";
import { type spatial } from "@/spatial";
import { unique } from "@/unique";

export type CompareF<T> = (a: T, b: T) => number;

Expand Down Expand Up @@ -94,6 +95,15 @@ export const unorderedPrimitiveArrays = <T extends Primitive>(
return aSorted.every((v, i) => v === bSorted[i]) ? 0 : -1;
};

export const uniqueUnorderedPrimitiveArrays = <T extends Primitive>(
a: readonly T[] | T[],
b: readonly T[] | T[],
): number => {
const uniqueA = unique(a);
const uniqueB = unique(b);
return unorderedPrimitiveArrays(uniqueA, uniqueB);
};

export const order = (a: spatial.Order, b: spatial.Order): number => {
if (a === b) return 0;
if (a === "first" && b === "last") return 1;
Expand Down

0 comments on commit cb53272

Please sign in to comment.