pi-openui 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +40 -1
- package/package.json +3 -2
- package/src/blocks/code.ts +16 -3
- package/src/blocks/core.ts +78 -27
- package/src/blocks/diagrams.ts +8 -26
- package/src/blocks/fallback.ts +25 -0
- package/src/blocks/gesture.ts +112 -0
- package/src/blocks/interact.ts +55 -3
- package/src/blocks/markdown.ts +13 -3
- package/src/blocks/pan.ts +139 -0
- package/src/blocks/panblock.ts +44 -0
- package/src/blocks/render.ts +25 -17
- package/src/blocks/sequence.ts +4 -4
- package/src/blocks/table.ts +122 -14
- package/src/blocks/types.ts +14 -2
- package/src/blocks/util.ts +5 -0
- package/src/diagram/dag.ts +4 -3
- package/src/diagram/erd.ts +4 -2
- package/src/diagram/infra.ts +3 -2
- package/src/lang/docs.ts +38 -14
- package/src/lang/library.ts +122 -19
- package/src/lang/samples.ts +134 -0
- package/src/pi/config.ts +185 -0
- package/src/pi/events.ts +150 -0
- package/src/pi/extension.ts +63 -18
- package/src/pi/widget.ts +67 -14
package/src/pi/widget.ts
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { drawBubble } from "../blocks/bubble.ts";
|
|
4
4
|
import { BADGE } from "../blocks/core.ts";
|
|
5
|
-
import { activate, describe, nextTarget, readingOrder, reveal, targetAt, type Env, type NavKey } from "../blocks/interact.ts";
|
|
5
|
+
import { activate, describe, nextTarget, panKey, readingOrder, reveal, targetAt, type Env, type NavKey } from "../blocks/interact.ts";
|
|
6
6
|
import { otherLength, setOther } from "../blocks/choice.ts";
|
|
7
7
|
import { buildMessage, cleanNote, NOTE_LIMITS, widgetTitle } from "../blocks/notes.ts";
|
|
8
|
+
import { Recognizer } from "../blocks/gesture.ts";
|
|
8
9
|
import { renderTree } from "../blocks/render.ts";
|
|
9
10
|
import { newState, type Block, type Note, type Target, type WState } from "../blocks/types.ts";
|
|
10
11
|
import { cell, padRow, textCells } from "../blocks/util.ts";
|
|
11
12
|
import { rowsToLines, textWidth, truncateText, type Cell } from "../diagram/canvas.ts";
|
|
12
13
|
import { createPalette } from "../diagram/palette.ts";
|
|
13
14
|
import { LIMITS, type El } from "../lang/library.ts";
|
|
15
|
+
import { DEFAULTS, type Settings } from "./config.ts";
|
|
16
|
+
import { recorder as defaultRecorder, type Recorder } from "./events.ts";
|
|
14
17
|
import { imageService } from "./images.ts";
|
|
15
18
|
|
|
16
19
|
const GUTTER = 2;
|
|
@@ -74,9 +77,28 @@ interface Entry {
|
|
|
74
77
|
/** the finished lines and what they were drawn from: pi asks for them on every frame, so an unchanged widget answers from here */
|
|
75
78
|
lines: string[] | null;
|
|
76
79
|
lineKey: string;
|
|
80
|
+
/** what the pointer is doing to a clipped block (flicks, the thumb, the tap that follows a flick) */
|
|
81
|
+
gesture: Recognizer;
|
|
82
|
+
/** what was changed to draw this widget from an older source (see compile's restore mode); shown as one quiet line */
|
|
83
|
+
notes: string[];
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
const entries = new Map<string, Entry>();
|
|
87
|
+
let settings: Settings = { ...DEFAULTS };
|
|
88
|
+
/** what reaches a widget is recorded when /openui-events is on; tests swap it */
|
|
89
|
+
let recorder: Recorder = defaultRecorder;
|
|
90
|
+
export const setRecorder = (r: Recorder): void => void (recorder = r);
|
|
91
|
+
export const currentSettings = (): Settings => settings;
|
|
92
|
+
/** The user's settings changed (the file was read, or /openui-config wrote it): every widget is drawn again. */
|
|
93
|
+
export function applySettings(next: Settings): void {
|
|
94
|
+
if (next.tableMinCol === settings.tableMinCol && next.scrollStep === settings.scrollStep) return;
|
|
95
|
+
settings = { ...next };
|
|
96
|
+
for (const e of entries.values()) {
|
|
97
|
+
e.version++;
|
|
98
|
+
e.lines = null;
|
|
99
|
+
}
|
|
100
|
+
hooks.requestRender();
|
|
101
|
+
}
|
|
80
102
|
export const active: { id: string | null } = { id: null };
|
|
81
103
|
export const newestId: { id: string | null } = { id: null };
|
|
82
104
|
/** A dialog (the note editor) is open: our key and mouse handlers stand down so its keys are its own. */
|
|
@@ -115,7 +137,7 @@ imageService.onLate = () => {
|
|
|
115
137
|
function getEntry(id: string, root: El, mode: Mode): Entry {
|
|
116
138
|
let e = entries.get(id);
|
|
117
139
|
if (!e) {
|
|
118
|
-
e = { root, state: newState(), mode, block: null, key: "", version: 0, warnings: [], notice: null, noticeTimer: null, pending: null, lines: null, lineKey: "" };
|
|
140
|
+
e = { root, state: newState(), mode, block: null, key: "", version: 0, warnings: [], notice: null, noticeTimer: null, pending: null, lines: null, lineKey: "", gesture: new Recognizer(), notes: [] };
|
|
119
141
|
entries.set(id, e);
|
|
120
142
|
newestId.id = id;
|
|
121
143
|
}
|
|
@@ -377,12 +399,12 @@ export class OpenUIWidget {
|
|
|
377
399
|
private pal: ReturnType<typeof themePalette> | null = null;
|
|
378
400
|
private root: El;
|
|
379
401
|
private mode: Mode;
|
|
380
|
-
constructor(id: string, root: El, theme: any, mode: Mode) {
|
|
402
|
+
constructor(id: string, root: El, theme: any, mode: Mode, notes: string[] = []) {
|
|
381
403
|
this.id = id;
|
|
382
404
|
this.theme = theme;
|
|
383
405
|
this.root = root;
|
|
384
406
|
this.mode = mode;
|
|
385
|
-
getEntry(id, root, mode);
|
|
407
|
+
getEntry(id, root, mode).notes = notes;
|
|
386
408
|
}
|
|
387
409
|
|
|
388
410
|
private layout(width: number): Entry {
|
|
@@ -390,7 +412,7 @@ export class OpenUIWidget {
|
|
|
390
412
|
const e = getEntry(this.id, this.root, this.mode);
|
|
391
413
|
const key = `${width}:${e.version}`;
|
|
392
414
|
if (!e.block || e.key !== key) {
|
|
393
|
-
const r = renderTree(e.root, width, e.state, { images: imageService });
|
|
415
|
+
const r = renderTree(e.root, width, e.state, { images: imageService, settings });
|
|
394
416
|
let block = r.block;
|
|
395
417
|
// a footer row with the notes chip, when there is anything to attach a note to
|
|
396
418
|
if (block.targets.some((t) => t.kind === "item")) {
|
|
@@ -511,6 +533,7 @@ export class OpenUIWidget {
|
|
|
511
533
|
imageRows.add(t.rect.y);
|
|
512
534
|
}
|
|
513
535
|
for (const w of e.warnings) lines.push(th.fg("warning", `! ${w}`));
|
|
536
|
+
if (e.notes.length) lines.push(th.fg("dim", `(redrawn by a newer version of the extension: ${e.notes.length} change${e.notes.length === 1 ? "" : "s"}. ${e.notes[0]})`));
|
|
514
537
|
const armed = active.id === this.id;
|
|
515
538
|
const edgeFor = (y: number) => (gutter.has(y) ? th.fg("accent", "┃") + " " : armed ? th.fg("accent", "▎") + " " : " ");
|
|
516
539
|
return lines.map((l, i) => (imageRows.has(i) ? edgeFor(i) + l : truncateToWidth(edgeFor(i) + l, width)));
|
|
@@ -528,24 +551,41 @@ export class OpenUIWidget {
|
|
|
528
551
|
const e = entries.get(this.id);
|
|
529
552
|
if (!e || !e.block || dialog.open) return undefined;
|
|
530
553
|
const t = targetAt(e.block, ev.x - GUTTER, ev.y, e.state.noteMode);
|
|
554
|
+
recorder.mouse(ev);
|
|
555
|
+
const act = e.gesture.feed({ type: ev.type, button: ev.button, x: ev.x - GUTTER, y: ev.y, shift: !!ev.shift, alt: !!ev.alt, wheelDelta: ev.wheelDelta }, e.block, e.state.pan);
|
|
556
|
+
if (act.kind !== "none") recorder.note(act.kind === "pan" ? `pan to ${act.to}${act.capture ? " (dragging the thumb)" : ""}` : act.kind === "tap" ? `tap at ${act.x},${act.y} (after a flick)` : "a click already handled as a tap");
|
|
557
|
+
if (act.kind === "pan") {
|
|
558
|
+
if ((e.state.pan[act.key] ?? 0) !== act.to) {
|
|
559
|
+
e.state.pan[act.key] = act.to;
|
|
560
|
+
e.version++;
|
|
561
|
+
}
|
|
562
|
+
hooks.requestRender();
|
|
563
|
+
return act.capture ? { handled: true, render: true, capture: true } : { handled: true, render: true };
|
|
564
|
+
}
|
|
565
|
+
if (act.kind === "swallow") return { handled: true };
|
|
566
|
+
if (act.kind === "tap") return this.click(e, t, ev);
|
|
531
567
|
if (ev.type === "move") {
|
|
532
568
|
const id = t?.id ?? null;
|
|
533
569
|
if (id === e.state.hover) return undefined;
|
|
534
570
|
e.state.hover = id;
|
|
535
571
|
return { handled: !!t, render: true };
|
|
536
572
|
}
|
|
537
|
-
if (ev.type === "click" && ev.button === "left"
|
|
538
|
-
active.id = this.id;
|
|
539
|
-
e.state.focus = t.id;
|
|
540
|
-
setNotice(this.id, null);
|
|
541
|
-
perform(this.id, e, t, !!ev.shift);
|
|
542
|
-
if (reveal(t, e.state)) e.version++;
|
|
543
|
-
hooks.requestRender();
|
|
544
|
-
return { handled: true, render: true };
|
|
545
|
-
}
|
|
573
|
+
if (ev.type === "click" && ev.button === "left") return this.click(e, t, ev);
|
|
546
574
|
return undefined;
|
|
547
575
|
}
|
|
548
576
|
|
|
577
|
+
/** A click, or a tap the recognizer found for pi (see gesture.ts). */
|
|
578
|
+
private click(e: Entry, t: Target | null, ev: any) {
|
|
579
|
+
if (!t) return undefined;
|
|
580
|
+
active.id = this.id;
|
|
581
|
+
e.state.focus = t.id;
|
|
582
|
+
setNotice(this.id, null);
|
|
583
|
+
perform(this.id, e, t, !!ev.shift);
|
|
584
|
+
if (reveal(t, e.state)) e.version++;
|
|
585
|
+
hooks.requestRender();
|
|
586
|
+
return { handled: true, render: true };
|
|
587
|
+
}
|
|
588
|
+
|
|
549
589
|
/** pi calls this when the theme, the cell size or a converter changes: the colours (and a picture's size) are stale. */
|
|
550
590
|
invalidate() {
|
|
551
591
|
this.pal = null;
|
|
@@ -589,6 +629,13 @@ function keyInner(data: string): boolean {
|
|
|
589
629
|
hooks.requestRender();
|
|
590
630
|
return true;
|
|
591
631
|
}
|
|
632
|
+
// the pan bar is focused: Shift+arrow, PageUp and PageDown scroll a page (the plain arrows, below, a fine step)
|
|
633
|
+
const page = matchesKey(data, Key.shift("left")) || matchesKey(data, Key.pageUp) ? "left" : matchesKey(data, Key.shift("right")) || matchesKey(data, Key.pageDown) ? "right" : null;
|
|
634
|
+
if (page && panKey(block, e.state, e.state.focus, page, true)) {
|
|
635
|
+
e.version++;
|
|
636
|
+
hooks.requestRender();
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
592
639
|
let nav: NavKey | null = null;
|
|
593
640
|
if (matchesKey(data, Key.tab)) nav = "next";
|
|
594
641
|
else if (matchesKey(data, Key.shift("tab"))) nav = "prev";
|
|
@@ -597,6 +644,12 @@ function keyInner(data: string): boolean {
|
|
|
597
644
|
else if (matchesKey(data, Key.up)) nav = "up";
|
|
598
645
|
else if (matchesKey(data, Key.down)) nav = "down";
|
|
599
646
|
if (nav) {
|
|
647
|
+
// the pan bar is focused: Left and Right scroll instead of moving the focus
|
|
648
|
+
if ((nav === "left" || nav === "right") && panKey(block, e.state, e.state.focus, nav)) {
|
|
649
|
+
e.version++;
|
|
650
|
+
hooks.requestRender();
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
600
653
|
const t = nextTarget(block, e.state.focus, nav, notes);
|
|
601
654
|
if (!t) return false;
|
|
602
655
|
e.state.focus = t.id;
|