Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ tmp_playground
/public_version.ts
website/_plug
tmp
.idea
.idea
.swp
18 changes: 4 additions & 14 deletions client/editor_ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FilterList } from "./components/filter.tsx";
import { AnythingPicker } from "./components/anything_picker.tsx";
import { TopBar } from "./components/top_bar.tsx";
import reducer from "./reducer.ts";
import { setupTouchRouter } from "./input/touch_router.ts";
import {
type Action,
type AppViewState,
Expand All @@ -30,6 +31,7 @@ import {

export class MainUI {
viewState: AppViewState = initialViewState;
private _touch?: { dispose: () => void; refresh: () => void };

constructor(private client: Client) {
// Make keyboard shortcuts work even when the editor is in read only mode or not focused
Expand All @@ -54,20 +56,8 @@ export class MainUI {
}
});

globalThis.addEventListener("touchstart", (ev) => {
// Launch the page picker on a two-finger tap
if (ev.touches.length === 2) {
ev.stopPropagation();
ev.preventDefault();
client.startPageNavigate("page");
}
// Launch the command palette using a three-finger tap
if (ev.touches.length === 3) {
ev.stopPropagation();
ev.preventDefault();
client.startCommandPalette();
}
});
// Install touch router (reads ui.touch.bindings)
this._touch = setupTouchRouter(client);

globalThis.addEventListener("mouseup", (_) => {
setTimeout(() => {
Expand Down
49 changes: 49 additions & 0 deletions client/input/touch_registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// client/input/touch_registry.ts
// Build a Map<fingers, binding> from (1) built-in defaults, (2) config ui.touch.bindings.
// Later sources override earlier ones per finger count.

export type TouchBinding = {
fingers: number; // e.g. 2 or 3
command: string; // e.g. "Navigate: Page Picker" | "Command: Open Palette" | other command name
preventDefault?: boolean; // default true
};

export type TouchMapEntry = { command: string; preventDefault: boolean };

/** Built-in fallbacks that mirror current behavior */
export const defaultBindings: TouchBinding[] = [
{ fingers: 2, command: "Navigate: Page Picker", preventDefault: true },
{ fingers: 3, command: "Command: Open Palette", preventDefault: true },
];

/**
* Merge a list of TouchBinding into a map, overriding existing entries for the same fingers.
*/
function merge(list: TouchBinding[], into: Map<number, TouchMapEntry>) {
for (const b of list) {
if (!b) continue;
const pd = b.preventDefault ?? true;
into.set(b.fingers, { command: b.command, preventDefault: pd });
}
}

/**
* Build a finger→binding map from defaults, config and command metadata.
* @param commands Command registry list (objects with name + optional `touch` array)
* @param configBindings Values from config.get("ui.touch.bindings", [])
*/
export function buildTouchMap(
configBindings: TouchBinding[] = [],
): Map<number, TouchMapEntry> {
const map = new Map<number, TouchMapEntry>();

// 1) Built-in defaults
merge(defaultBindings, map);

// 2) Config-provided bindings (if any)
if (Array.isArray(configBindings)) {
merge(configBindings, map);
}

return map;
}
85 changes: 85 additions & 0 deletions client/input/touch_router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// client/input/touch_router.ts
// A single non-passive touchstart listener that dispatches actions based on finger-count.

import type { TouchMapEntry } from "./touch_registry.ts";
import { buildTouchMap } from "./touch_registry.ts";
import type { Client } from "../client.ts";

export function setupTouchRouter(client: Client) {
if ((navigator as any).maxTouchPoints <= 0) {
return { dispose: () => {}, refresh: () => {} }; // noop
}

function readConfigBindings(): {
fingers: number;
command: string;
preventDefault?: boolean;
}[] {
return client.config.get("ui.touch.bindings", []);
}

function compile(): Map<number, TouchMapEntry> {
const cfg = readConfigBindings();
try {
map = buildTouchMap(cfg);
} catch {
client.flashNotification(
"unexpected errur in touch_router:compile()",
"error",
);
}
return map;
}

let map: Map<number, TouchMapEntry>;

client.clientSystem?.commandHook?.on?.({
commandsUpdated: () => {
map = compile();
},
});
// Optional: one-shot delayed refresh for slow boots
// setTimeout(() => { map = compile(); }, 1500);

const onTouchStart = (ev: TouchEvent) => {
const n = ev.touches?.length ?? 0;
if (!n) return;
let binding = map.get(n);
if (!binding) {
try {
map = compile();
} catch { /* ignore */ }
binding = map.get(n);
if (!binding) return;
}

const name = binding.command;

// Disabled if empty or "none"
if (!name || name === "none") return;

if (binding.preventDefault) {
ev.preventDefault();
ev.stopPropagation();
}

try {
client.runCommandByName(name);
} catch {
client.flashNotification(
"unexpected errur in touch_router:onTouchStart()",
"error",
);
}
};

globalThis.addEventListener("touchstart", onTouchStart, { passive: false });

function refresh() {
map = compile();
}

const dispose = () =>
globalThis.removeEventListener("touchstart", onTouchStart);
return { dispose, refresh };
}
24 changes: 24 additions & 0 deletions docs/Touch Bindings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Touch Bindings

SilverBullet supports **multi‑touch gestures** for quick actions.

## Configure (Space `CONFIG`)

```lua
config.set("ui.touch.bindings", {
{ fingers = 2, command = "Navigate: Page Picker" }, -- default
{ fingers = 3, command = "Command: Open Palette" }, -- default
-- Example override:
-- { fingers = 2, command = "Navigate: Back", preventDefault = true },
})
```

- `preventDefault` defaults to **true**. Set **false** to allow browser/system zoom.
- Later entries override earlier ones when they use the same `fingers`.

## Developer notes

- The UI installs a single `touchstart` listener (non-passive) and resolves the gesture using:
1. Built-in defaults
2. `ui.touch.bindings` from config
- Handler lives in `client/input/touch_router.ts` and registry in `client/input/touch_registry.ts`.
38 changes: 38 additions & 0 deletions libraries/Library/Std/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,3 +384,41 @@ config.set {
},
}
```



## Touch input

`ui.touch.bindings` — configure multi-touch gesture bindings.

Default (mirroring current behavior):

```lua
config.set("ui.touch.bindings", {
{ fingers = 2, command = "Navigate: Page Picker", preventDefault = true },
{ fingers = 3, command = "Command: Open Palette", preventDefault = true },
})
```

Schema (reference):

```lua
config.define("ui.touch.bindings", {
description = "Multi-touch gesture bindings",
type = "array",
items = {
type = "object",
properties = {
fingers = { type = "number", minimum = 2, maximum = 5 },
command = { type = "string" },
preventDefault = { type = "boolean" },
},
required = { "fingers", "command" },
additionalProperties = false,
},
default = {
{ fingers = 2, command = "Navigate: Page Picker", preventDefault = true },
{ fingers = 3, command = "Command: Open Palette", preventDefault = true },
}
})
```