@neuroplastio/xterm-addon-hotty 0.1.0-next.3 → 0.1.0-next.5

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.
@@ -1384,6 +1384,9 @@ var Surface = class {
1384
1384
  /** The document's base URL (SPEC §7.3). */
1385
1385
  base = NO_BASE;
1386
1386
  keyboard = false;
1387
+ /** The element the program knows has focus: the last `focus` named it,
1388
+ * or the program's `a=focus` gave it (SPEC §10.1). */
1389
+ told = null;
1387
1390
  /** Detached (SPEC §5.5): nothing in the surface reaches the program. */
1388
1391
  detachedState = false;
1389
1392
  /** A press is on something that takes no focus (SPEC §10.1): the focus
@@ -1560,6 +1563,7 @@ var Surface = class {
1560
1563
  this.dropDrag();
1561
1564
  const had = this.keyboard || this.frameFocused();
1562
1565
  this.keyboard = false;
1566
+ this.told = null;
1563
1567
  this.focusedControl()?.blur();
1564
1568
  if (had) this.giveBack();
1565
1569
  }
@@ -1751,17 +1755,21 @@ var Surface = class {
1751
1755
  if (target && !el) throw new Error(target);
1752
1756
  this.programFocus = true;
1753
1757
  try {
1754
- this.frame.focus();
1755
- this.frame.contentWindow?.focus();
1758
+ if (!this.frameFocused()) {
1759
+ this.frame.focus();
1760
+ this.frame.contentWindow?.focus();
1761
+ }
1756
1762
  if (el) el.focus();
1757
1763
  else if (!this.focusedControl()) this.tabbable()[0]?.focus();
1758
1764
  this.keyboard = true;
1765
+ this.told = this.focusedControl();
1759
1766
  } finally {
1760
1767
  setTimeout(() => this.programFocus = false, 0);
1761
1768
  }
1762
1769
  }
1763
1770
  /** `a=blur`, or the keyboard leaving: the edited control commits first. */
1764
1771
  blur() {
1772
+ this.pressed = null;
1765
1773
  this.commit();
1766
1774
  if (this.keyboard) this.giveBack();
1767
1775
  }
@@ -1812,6 +1820,7 @@ var Surface = class {
1812
1820
  if (this.keyboard) {
1813
1821
  this.commit();
1814
1822
  this.keyboard = false;
1823
+ this.told = null;
1815
1824
  this.emit("blur", "");
1816
1825
  }
1817
1826
  this.settle();
@@ -1839,16 +1848,24 @@ var Surface = class {
1839
1848
  }
1840
1849
  /** An element got focus: the surface takes the keyboard if the element is
1841
1850
  * one that takes focus, and the focus is neither the program's (no echo)
1842
- * nor a detached surface's. */
1851
+ * nor a detached surface's. The program hears where the user put focus,
1852
+ * each time it is another element than the one it knows (SPEC §10.1). */
1843
1853
  onFocusIn(e) {
1844
- if (this.programFocus || this.auxPress) return;
1845
- if (this.detachedState || this.pressNothing || !isElement(e.target) || !takesFocus(e.target, this.hyper)) {
1854
+ const el = isElement(e.target) ? e.target : null;
1855
+ if (this.programFocus) {
1856
+ this.told = el;
1857
+ return;
1858
+ }
1859
+ if (this.auxPress) return;
1860
+ if (this.detachedState || this.pressNothing || !el || !takesFocus(el, this.hyper)) {
1846
1861
  this.settle();
1847
1862
  return;
1848
1863
  }
1849
- if (this.keyboard) return;
1864
+ const gained = !this.keyboard;
1850
1865
  this.keyboard = true;
1851
- this.emit("focus", "");
1866
+ if (!gained && el === this.told) return;
1867
+ this.told = el;
1868
+ this.emit("focus", el.closest('[id]:not([id=""])')?.id ?? "");
1852
1869
  }
1853
1870
  tabbable() {
1854
1871
  const all = this.doc.querySelectorAll(
@@ -2607,17 +2624,21 @@ var Surface = class {
2607
2624
  /**
2608
2625
  * The cells an element's border box covers as the user sees it, scrolled
2609
2626
  * included (SPEC §9: `area`), counted from the surface's top left cell:
2610
- * whole, where it is clipped or scrolled away. An edge within half a
2611
- * device pixel of a cell's is on it: layout rounds positions to fractions
2612
- * of a pixel, and the user sees no less than a device pixel.
2627
+ * whole, where it is clipped or scrolled away. An edge close enough to a
2628
+ * cell's is on it: within half a device pixel, which the user cannot see,
2629
+ * plus what layout loses on the way there. Layout keeps lengths in fixed
2630
+ * units (1/64 px in Chromium, 1/60 in Firefox) and cuts each box's to
2631
+ * them, so with a cell a fraction of a pixel, a row 29 boxes of one cell
2632
+ * down starts up to 29/64 px short of its cell.
2613
2633
  */
2614
2634
  areaOf(el) {
2615
2635
  const b = el.getBoundingClientRect();
2616
- const tol = 0.5 / (this.frame.ownerDocument.defaultView?.devicePixelRatio || 1);
2617
- const c = Math.floor((b.left + tol) / this.cellW);
2618
- const r = Math.floor((b.top + tol) / this.cellH);
2619
- const right = Math.ceil((b.right - tol) / this.cellW);
2620
- const bottom = Math.ceil((b.bottom - tol) / this.cellH);
2636
+ const half = 0.5 / (this.frame.ownerDocument.defaultView?.devicePixelRatio || 1);
2637
+ const slack = (edge, cell) => half + Math.abs(edge / cell) / 60;
2638
+ const c = Math.floor((b.left + slack(b.left, this.cellW)) / this.cellW);
2639
+ const r = Math.floor((b.top + slack(b.top, this.cellH)) / this.cellH);
2640
+ const right = Math.ceil((b.right - slack(b.right, this.cellW)) / this.cellW);
2641
+ const bottom = Math.ceil((b.bottom - slack(b.bottom, this.cellH)) / this.cellH);
2621
2642
  return { c, r, w: Math.max(0, right - c), h: Math.max(0, bottom - r) };
2622
2643
  }
2623
2644
  // --- Events for the program (PROTOCOL §8) --------------------------------
@@ -2708,6 +2729,7 @@ var Surface = class {
2708
2729
  this.commit();
2709
2730
  if (this.keyboard) {
2710
2731
  this.keyboard = false;
2732
+ this.told = null;
2711
2733
  this.emit("blur", "");
2712
2734
  }
2713
2735
  });
@@ -3318,6 +3340,9 @@ var HottyAddon = class {
3318
3340
  css = "";
3319
3341
  policy;
3320
3342
  metrics = { cellW: 0, cellH: 0, key: "" };
3343
+ /** The cell (CSS px) and font the program last heard of, or saw at the
3344
+ * start: a `resize` is owed when both have changed since (SPEC §5.3). */
3345
+ told = { cellW: 0, cellH: 0, font: "" };
3321
3346
  resizeTimer = null;
3322
3347
  /** A surface's key is being forwarded to the terminal (`key`), and the
3323
3348
  * terminal's own focus is held off meanwhile. A program in the page may
@@ -3972,23 +3997,40 @@ var HottyAddon = class {
3972
3997
  const o = this.term.options;
3973
3998
  const key = `${cellW}x${cellH}|${o.fontFamily}|${o.fontSize}|${JSON.stringify(o.theme ?? {})}`;
3974
3999
  if (key === this.metrics.key) return;
3975
- const sizeChanged = this.metrics.cellW !== 0 && (cellW !== this.metrics.cellW || cellH !== this.metrics.cellH);
4000
+ const first = this.metrics.cellW === 0;
3976
4001
  this.metrics = { cellW, cellH, key };
4002
+ if (first) this.told = { cellW, cellH, font: this.font() };
4003
+ else this.settle();
3977
4004
  if (this.surfaces.size === 0) return;
3978
4005
  const css = this.hostCss();
3979
4006
  const scheme = this.scheme();
3980
4007
  for (const s of this.surfaces.values()) s.setHostCss(css, scheme);
3981
4008
  this.reposition();
3982
- if (sizeChanged) {
3983
- if (this.resizeTimer) clearTimeout(this.resizeTimer);
3984
- this.resizeTimer = setTimeout(() => {
3985
- for (const p of this.placements.values()) {
3986
- const s = p.surface;
3987
- if (s.detached) continue;
3988
- this.host().event(s.name, "resize", "", { w: Math.round(s.cols * cellW), h: s.rows * cellH });
3989
- }
3990
- }, 100);
3991
- }
4009
+ }
4010
+ font() {
4011
+ return `${this.term.options.fontFamily}|${this.term.options.fontSize}`;
4012
+ }
4013
+ /** Once the cell and font settle: a new font that changed the cell is
4014
+ * re-rendered with no program involvement, so the program hears its new
4015
+ * size (SPEC §5.3, §9). A browser's zoom moves the cell only by device
4016
+ * pixel rounding and keeps the font, and is not told. xterm.js may take
4017
+ * the font and its cell in two renders, so this waits for both. */
4018
+ settle() {
4019
+ if (this.resizeTimer) clearTimeout(this.resizeTimer);
4020
+ this.resizeTimer = setTimeout(() => {
4021
+ this.resizeTimer = null;
4022
+ const { cellW, cellH } = this.cell();
4023
+ const font = this.font();
4024
+ const t = this.told;
4025
+ const owed = font !== t.font && (cellW !== t.cellW || cellH !== t.cellH);
4026
+ this.told = { cellW, cellH, font };
4027
+ if (!owed) return;
4028
+ for (const p of this.placements.values()) {
4029
+ const s = p.surface;
4030
+ if (s.detached) continue;
4031
+ this.host().event(s.name, "resize", "", { w: Math.round(s.cols * cellW), h: s.rows * cellH });
4032
+ }
4033
+ }, 100);
3992
4034
  }
3993
4035
  };
3994
4036
  function sequence(chain, f) {