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/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" && t) {
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;