@react-x11/components 0.4.0 → 0.6.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.
Files changed (248) hide show
  1. package/README.md +143 -8
  2. package/dist/charts/components.d.ts +3 -1
  3. package/dist/charts/components.d.ts.map +1 -1
  4. package/dist/charts/components.js +8 -4
  5. package/dist/charts/components.js.map +1 -1
  6. package/dist/charts/node.d.ts +13 -6
  7. package/dist/charts/node.d.ts.map +1 -1
  8. package/dist/charts/node.js +75 -24
  9. package/dist/charts/node.js.map +1 -1
  10. package/dist/charts/render.d.ts +4 -0
  11. package/dist/charts/render.d.ts.map +1 -1
  12. package/dist/charts/render.js +11 -6
  13. package/dist/charts/render.js.map +1 -1
  14. package/dist/code-editor/node.d.ts +47 -16
  15. package/dist/code-editor/node.d.ts.map +1 -1
  16. package/dist/code-editor/node.js +121 -51
  17. package/dist/code-editor/node.js.map +1 -1
  18. package/dist/flow/draw.d.ts +19 -1
  19. package/dist/flow/draw.d.ts.map +1 -1
  20. package/dist/flow/draw.js +64 -35
  21. package/dist/flow/draw.js.map +1 -1
  22. package/dist/flow/index.d.ts.map +1 -1
  23. package/dist/flow/index.js +24 -8
  24. package/dist/flow/index.js.map +1 -1
  25. package/dist/flow/model.d.ts +4 -2
  26. package/dist/flow/model.d.ts.map +1 -1
  27. package/dist/flow/model.js +4 -2
  28. package/dist/flow/model.js.map +1 -1
  29. package/dist/flow/node.d.ts +40 -9
  30. package/dist/flow/node.d.ts.map +1 -1
  31. package/dist/flow/node.js +142 -53
  32. package/dist/flow/node.js.map +1 -1
  33. package/dist/flow/types.d.ts +46 -21
  34. package/dist/flow/types.d.ts.map +1 -1
  35. package/dist/formula/index.d.ts +4 -2
  36. package/dist/formula/index.d.ts.map +1 -1
  37. package/dist/formula/index.js.map +1 -1
  38. package/dist/formula/node.d.ts +5 -1
  39. package/dist/formula/node.d.ts.map +1 -1
  40. package/dist/formula/node.js +19 -3
  41. package/dist/formula/node.js.map +1 -1
  42. package/dist/html/controls.d.ts +4 -1
  43. package/dist/html/controls.d.ts.map +1 -1
  44. package/dist/html/controls.js.map +1 -1
  45. package/dist/html/css/cascade.d.ts +11 -5
  46. package/dist/html/css/cascade.d.ts.map +1 -1
  47. package/dist/html/css/cascade.js +18 -8
  48. package/dist/html/css/cascade.js.map +1 -1
  49. package/dist/html/css/parse.d.ts +15 -10
  50. package/dist/html/css/parse.d.ts.map +1 -1
  51. package/dist/html/css/parse.js +41 -16
  52. package/dist/html/css/parse.js.map +1 -1
  53. package/dist/html/css/style.d.ts +3 -0
  54. package/dist/html/css/style.d.ts.map +1 -1
  55. package/dist/html/css/style.js +3 -1
  56. package/dist/html/css/style.js.map +1 -1
  57. package/dist/html/css/values.d.ts +14 -1
  58. package/dist/html/css/values.d.ts.map +1 -1
  59. package/dist/html/css/values.js +11 -8
  60. package/dist/html/css/values.js.map +1 -1
  61. package/dist/html/index.d.ts.map +1 -1
  62. package/dist/html/index.js +1 -0
  63. package/dist/html/index.js.map +1 -1
  64. package/dist/html/layout/boxes.d.ts +12 -5
  65. package/dist/html/layout/boxes.d.ts.map +1 -1
  66. package/dist/html/layout/boxes.js +7 -4
  67. package/dist/html/layout/boxes.js.map +1 -1
  68. package/dist/html/layout/inline.d.ts.map +1 -1
  69. package/dist/html/layout/inline.js +22 -1
  70. package/dist/html/layout/inline.js.map +1 -1
  71. package/dist/html/node.d.ts +22 -8
  72. package/dist/html/node.d.ts.map +1 -1
  73. package/dist/html/node.js +97 -15
  74. package/dist/html/node.js.map +1 -1
  75. package/dist/html/paint.d.ts +3 -0
  76. package/dist/html/paint.d.ts.map +1 -1
  77. package/dist/html/paint.js +2 -2
  78. package/dist/html/paint.js.map +1 -1
  79. package/dist/index.d.ts +8 -2
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +21 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/maps/clip.d.ts +43 -0
  84. package/dist/maps/clip.d.ts.map +1 -0
  85. package/dist/maps/clip.js +171 -0
  86. package/dist/maps/clip.js.map +1 -0
  87. package/dist/maps/gzip.d.ts +15 -0
  88. package/dist/maps/gzip.d.ts.map +1 -0
  89. package/dist/maps/gzip.js +56 -0
  90. package/dist/maps/gzip.js.map +1 -0
  91. package/dist/maps/index.d.ts +55 -0
  92. package/dist/maps/index.d.ts.map +1 -0
  93. package/dist/maps/index.js +141 -0
  94. package/dist/maps/index.js.map +1 -0
  95. package/dist/maps/labels.d.ts +123 -0
  96. package/dist/maps/labels.d.ts.map +1 -0
  97. package/dist/maps/labels.js +323 -0
  98. package/dist/maps/labels.js.map +1 -0
  99. package/dist/maps/mvt.d.ts +244 -0
  100. package/dist/maps/mvt.d.ts.map +1 -0
  101. package/dist/maps/mvt.js +820 -0
  102. package/dist/maps/mvt.js.map +1 -0
  103. package/dist/maps/node.d.ts +278 -0
  104. package/dist/maps/node.d.ts.map +1 -0
  105. package/dist/maps/node.js +1459 -0
  106. package/dist/maps/node.js.map +1 -0
  107. package/dist/maps/overlay.d.ts +157 -0
  108. package/dist/maps/overlay.d.ts.map +1 -0
  109. package/dist/maps/overlay.js +472 -0
  110. package/dist/maps/overlay.js.map +1 -0
  111. package/dist/maps/paint.d.ts +226 -0
  112. package/dist/maps/paint.d.ts.map +1 -0
  113. package/dist/maps/paint.js +705 -0
  114. package/dist/maps/paint.js.map +1 -0
  115. package/dist/maps/proj.d.ts +281 -0
  116. package/dist/maps/proj.d.ts.map +1 -0
  117. package/dist/maps/proj.js +451 -0
  118. package/dist/maps/proj.js.map +1 -0
  119. package/dist/maps/sources.d.ts +256 -0
  120. package/dist/maps/sources.d.ts.map +1 -0
  121. package/dist/maps/sources.js +210 -0
  122. package/dist/maps/sources.js.map +1 -0
  123. package/dist/maps/style.d.ts +141 -0
  124. package/dist/maps/style.d.ts.map +1 -0
  125. package/dist/maps/style.js +147 -0
  126. package/dist/maps/style.js.map +1 -0
  127. package/dist/maps/styles.d.ts +82 -0
  128. package/dist/maps/styles.d.ts.map +1 -0
  129. package/dist/maps/styles.js +898 -0
  130. package/dist/maps/styles.js.map +1 -0
  131. package/dist/maps/tiles.d.ts +283 -0
  132. package/dist/maps/tiles.d.ts.map +1 -0
  133. package/dist/maps/tiles.js +469 -0
  134. package/dist/maps/tiles.js.map +1 -0
  135. package/dist/maps/types.d.ts +244 -0
  136. package/dist/maps/types.d.ts.map +1 -0
  137. package/dist/maps/types.js +2 -0
  138. package/dist/maps/types.js.map +1 -0
  139. package/dist/markdown/ast.d.ts +80 -6
  140. package/dist/markdown/ast.d.ts.map +1 -1
  141. package/dist/markdown/ast.js +13 -4
  142. package/dist/markdown/ast.js.map +1 -1
  143. package/dist/markdown/expressions.d.ts +27 -0
  144. package/dist/markdown/expressions.d.ts.map +1 -0
  145. package/dist/markdown/expressions.js +203 -0
  146. package/dist/markdown/expressions.js.map +1 -0
  147. package/dist/markdown/index.d.ts +61 -2
  148. package/dist/markdown/index.d.ts.map +1 -1
  149. package/dist/markdown/index.js +101 -2
  150. package/dist/markdown/index.js.map +1 -1
  151. package/dist/markdown/parse.d.ts +1 -1
  152. package/dist/markdown/parse.d.ts.map +1 -1
  153. package/dist/markdown/parse.js +163 -15
  154. package/dist/markdown/parse.js.map +1 -1
  155. package/dist/markdown/spans.d.ts.map +1 -1
  156. package/dist/markdown/spans.js +4 -0
  157. package/dist/markdown/spans.js.map +1 -1
  158. package/dist/markdown/tags.d.ts +35 -0
  159. package/dist/markdown/tags.d.ts.map +1 -0
  160. package/dist/markdown/tags.js +213 -0
  161. package/dist/markdown/tags.js.map +1 -0
  162. package/dist/reorder/hx.d.ts +18 -0
  163. package/dist/reorder/hx.d.ts.map +1 -0
  164. package/dist/reorder/hx.js +10 -0
  165. package/dist/reorder/hx.js.map +1 -0
  166. package/dist/reorder/index.d.ts +314 -0
  167. package/dist/reorder/index.d.ts.map +1 -0
  168. package/dist/reorder/index.js +1539 -0
  169. package/dist/reorder/index.js.map +1 -0
  170. package/dist/reorder/model.d.ts +107 -0
  171. package/dist/reorder/model.d.ts.map +1 -0
  172. package/dist/reorder/model.js +162 -0
  173. package/dist/reorder/model.js.map +1 -0
  174. package/dist/richtext/node.d.ts +15 -5
  175. package/dist/richtext/node.d.ts.map +1 -1
  176. package/dist/richtext/node.js +40 -11
  177. package/dist/richtext/node.js.map +1 -1
  178. package/dist/richtext/runs.d.ts +35 -13
  179. package/dist/richtext/runs.d.ts.map +1 -1
  180. package/dist/richtext/runs.js +83 -27
  181. package/dist/richtext/runs.js.map +1 -1
  182. package/dist/tabs/index.d.ts +17 -0
  183. package/dist/tabs/index.d.ts.map +1 -1
  184. package/dist/tabs/index.js +849 -186
  185. package/dist/tabs/index.js.map +1 -1
  186. package/dist/terminal/vt/fonts.d.ts +45 -15
  187. package/dist/terminal/vt/fonts.d.ts.map +1 -1
  188. package/dist/terminal/vt/fonts.js +89 -19
  189. package/dist/terminal/vt/fonts.js.map +1 -1
  190. package/dist/terminal/vt/node.d.ts +42 -0
  191. package/dist/terminal/vt/node.d.ts.map +1 -1
  192. package/dist/terminal/vt/node.js +95 -11
  193. package/dist/terminal/vt/node.js.map +1 -1
  194. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  195. package/dist/terminal/vt/renderer.js +13 -3
  196. package/dist/terminal/vt/renderer.js.map +1 -1
  197. package/package.json +14 -3
  198. package/src/charts/components.ts +14 -7
  199. package/src/charts/node.ts +81 -26
  200. package/src/charts/render.ts +16 -6
  201. package/src/code-editor/node.ts +147 -53
  202. package/src/flow/draw.ts +104 -35
  203. package/src/flow/index.ts +33 -13
  204. package/src/flow/model.ts +4 -2
  205. package/src/flow/node.ts +158 -59
  206. package/src/flow/types.ts +46 -21
  207. package/src/formula/index.ts +4 -2
  208. package/src/formula/node.ts +24 -5
  209. package/src/html/controls.ts +4 -1
  210. package/src/html/css/cascade.ts +18 -7
  211. package/src/html/css/parse.ts +46 -21
  212. package/src/html/css/style.ts +6 -1
  213. package/src/html/css/values.ts +25 -9
  214. package/src/html/index.ts +1 -0
  215. package/src/html/layout/boxes.ts +19 -9
  216. package/src/html/layout/inline.ts +23 -1
  217. package/src/html/node.ts +110 -17
  218. package/src/html/paint.ts +17 -2
  219. package/src/index.ts +102 -0
  220. package/src/maps/clip.ts +184 -0
  221. package/src/maps/gzip.ts +66 -0
  222. package/src/maps/index.ts +316 -0
  223. package/src/maps/labels.ts +442 -0
  224. package/src/maps/mvt.ts +857 -0
  225. package/src/maps/node.ts +1819 -0
  226. package/src/maps/overlay.ts +639 -0
  227. package/src/maps/paint.ts +978 -0
  228. package/src/maps/proj.ts +641 -0
  229. package/src/maps/sources.ts +425 -0
  230. package/src/maps/style.ts +309 -0
  231. package/src/maps/styles.ts +1040 -0
  232. package/src/maps/tiles.ts +676 -0
  233. package/src/maps/types.ts +260 -0
  234. package/src/markdown/ast.ts +85 -9
  235. package/src/markdown/expressions.ts +223 -0
  236. package/src/markdown/index.ts +190 -5
  237. package/src/markdown/parse.ts +230 -15
  238. package/src/markdown/spans.ts +4 -0
  239. package/src/markdown/tags.ts +240 -0
  240. package/src/reorder/hx.ts +31 -0
  241. package/src/reorder/index.ts +2223 -0
  242. package/src/reorder/model.ts +232 -0
  243. package/src/richtext/node.ts +49 -14
  244. package/src/richtext/runs.ts +115 -30
  245. package/src/tabs/index.ts +1040 -204
  246. package/src/terminal/vt/fonts.ts +128 -36
  247. package/src/terminal/vt/node.ts +105 -12
  248. package/src/terminal/vt/renderer.ts +13 -3
@@ -17,6 +17,21 @@
17
17
  // `onKeyDown` handler that consumes the keys the popup owns (arrows,
18
18
  // Return, Tab-accept, Escape-close) and, by consuming them, keeps this
19
19
  // node's default action from also acting on them.
20
+ //
21
+ // **Units.** Everything this node measures is a device-pixel quantity:
22
+ // `this.style` and `this.abs` arrive already multiplied by the display
23
+ // scale (react-x11's docs/scale.md), so the layouts `app.fonts` shapes at
24
+ // that `fontSize`, the line height and column width read off them, the
25
+ // scroll offsets and everything painted are all on the panel's grid. The
26
+ // pointer is the exception — a synthetic event's `x`/`y` are logical — so
27
+ // it is converted on the way in (`_devicePoint`), and the geometry the
28
+ // handle answers is converted on the way out (`caretRect`, `metrics`,
29
+ // `measureText`, `scrollBy`), because a `<popup anchor>` and a style length
30
+ // are logical. The paint constants below never pass through a style, so
31
+ // each use multiplies by `_scale`. At 1x every conversion is the identity,
32
+ // which is how a press that handed `ev.x` to a hit test measured in glyph
33
+ // metrics passed every test and then landed at half the pointer's distance
34
+ // the day a native backend reported a retina panel.
20
35
  import { CARET_BLINK_MS, Node } from 'react-x11/node';
21
36
  import type {
22
37
  Context2D,
@@ -82,6 +97,9 @@ export const ELEMENT = 'codeeditor';
82
97
 
83
98
  const UNDO_LIMIT = 200;
84
99
  const DEFAULT_ROWS = 6;
100
+ // Logical pixels, like every length an app writes. None of these pass
101
+ // through a style, so each use multiplies by `_scale` — the way core's own
102
+ // caret width does (see "Units" above).
85
103
  const PREFERRED_WIDTH = 360;
86
104
  const GUTTER_PAD = 8; // each side of the line numbers
87
105
  const CARET_MARGIN = 24; // keep this many px visible beside the caret
@@ -119,7 +137,10 @@ export interface CodeEditorHandle {
119
137
  toggleLineComment(): void;
120
138
  copySelection(selection?: string): void;
121
139
  pasteFrom(selection?: string): void;
140
+ /** Logical pixels, like a style length; true when the offset moved. */
122
141
  scrollBy(dx: number, dy: number): boolean;
142
+ /** The caret in the editor's own coordinates, logical pixels — what a
143
+ * `<popup anchor={{ at }}>` takes. */
123
144
  caretRect(): { x: number; y: number; height: number };
124
145
  focus(): void;
125
146
  blur(): void;
@@ -274,11 +295,17 @@ export interface EditorKeyEvent {
274
295
  }
275
296
 
276
297
  export interface EditorMouseEvent {
298
+ /** Window-relative, in logical pixels — what a synthetic event carries. */
277
299
  x: number;
278
300
  y: number;
279
301
  button?: number;
280
302
  detail?: number;
281
303
  shiftKey: boolean;
304
+ /** The X event underneath, when there is one. Its `x`/`y` are the device
305
+ * pixels `abs` is in, and the editor reads those when present — core's
306
+ * own idiom for its fields. A caller synthesizing an event leaves it out,
307
+ * and the logical `x`/`y` are multiplied by the display scale instead. */
308
+ nativeEvent?: unknown;
282
309
  capturePointer?(): void;
283
310
  releasePointer?(): void;
284
311
  preventDefault?(): void;
@@ -360,8 +387,10 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
360
387
  */
361
388
  override measureContent({ width }: MeasureConstraints): MeasuredSize {
362
389
  const rows = Math.max(1, Number(this.props.rows ?? DEFAULT_ROWS));
390
+ // Device pixels, like the constraints: the line height already is, and
391
+ // the preferred width is a constant that never passed through a style.
363
392
  return {
364
- width: Math.min(PREFERRED_WIDTH, width),
393
+ width: Math.min(PREFERRED_WIDTH * this._scale, width),
365
394
  height: Math.ceil(this._lineHeight() * rows),
366
395
  };
367
396
  }
@@ -451,6 +480,33 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
451
480
 
452
481
  // --- metrics & style -----------------------------------------------------
453
482
 
483
+ /**
484
+ * Device pixels per logical pixel — the display scale this editor's
485
+ * window resolved to (react-x11's docs/scale.md): `1` on an ordinary
486
+ * display, `2` on a retina panel, fractional on a desktop set to 1.5.
487
+ * Resolved once by `createRoot` and constant for the node's life; `1`
488
+ * when the node reports nothing usable, the guard the terminal keeps.
489
+ */
490
+ private get _scale(): number {
491
+ return this.scale > 0 ? this.scale : 1;
492
+ }
493
+
494
+ /**
495
+ * A pointer event's position in device pixels — the unit `abs`, the
496
+ * content rect and every glyph metric here are in. The X event underneath
497
+ * carries it already; a caller that synthesized the event without one
498
+ * wrote logical coordinates, which multiply.
499
+ */
500
+ private _devicePoint(ev: EditorMouseEvent): { x: number; y: number } {
501
+ const native = ev.nativeEvent as
502
+ { x?: unknown; y?: unknown } | null | undefined;
503
+ const s = this._scale;
504
+ return {
505
+ x: typeof native?.x === 'number' ? native.x : ev.x * s,
506
+ y: typeof native?.y === 'number' ? native.y : ev.y * s,
507
+ };
508
+ }
509
+
454
510
  private _textStyle(): Record<string, unknown> {
455
511
  const s = this.style as Record<string, unknown>;
456
512
  return {
@@ -501,9 +557,9 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
501
557
 
502
558
  /**
503
559
  * The content box: `abs` inset by border and padding, computed from the
504
- * flattened style. [react-x11 gap] built-in fields read an internal
505
- * `contentBox()`; a public equivalent would remove this arithmetic —
506
- * filed as react-x11#254.
560
+ * flattened style device pixels, since both of those are. [react-x11
561
+ * gap] built-in fields read an internal `contentBox()`; a public
562
+ * equivalent would remove this arithmetic — filed as react-x11#254.
507
563
  */
508
564
  private _contentRect(): {
509
565
  x: number;
@@ -534,7 +590,7 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
534
590
  if (!this.props.lineNumbers) return 0;
535
591
  this._metrics();
536
592
  const digits = Math.max(2, String(this._lines.length).length);
537
- return Math.ceil(digits * this._charW) + GUTTER_PAD * 2;
593
+ return Math.ceil(digits * this._charW) + GUTTER_PAD * this._scale * 2;
538
594
  }
539
595
 
540
596
  private _tabSize(): number {
@@ -634,7 +690,7 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
634
690
  return entry;
635
691
  }
636
692
 
637
- /** Caret x (display pixels from the text origin) for a position. */
693
+ /** Caret x (device pixels from the text origin) for a position. */
638
694
  private _caretX(pos: Position): number {
639
695
  const entry = this._lineEntry(pos.line);
640
696
  if (!entry.layout) return 0;
@@ -642,7 +698,7 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
642
698
  return entry.layout.caretPosition(utf16ToCp(entry.display, disp)).x;
643
699
  }
644
700
 
645
- /** Window coordinates → document position. */
701
+ /** Window coordinates, device pixels → document position. */
646
702
  private _posAt(x: number, y: number): Position {
647
703
  this._metrics();
648
704
  const content = this._contentRect();
@@ -661,7 +717,9 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
661
717
  return clampPos(this._lines, { line, ch: entry.map.toRaw(disp) });
662
718
  }
663
719
 
664
- /** Font facts the completion popup sizes itself with. */
720
+ /** Font facts the completion popup sizes itself with — logical pixels,
721
+ * because they become style lengths (`height`, `fontSize`) on its rows.
722
+ * The face was shaped at the device size, so a 2x panel divides. */
665
723
  metrics(): {
666
724
  lineHeight: number;
667
725
  charWidth: number;
@@ -670,37 +728,47 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
670
728
  } {
671
729
  this._metrics();
672
730
  const s = this._textStyle();
731
+ const scale = this._scale;
673
732
  return {
674
- lineHeight: this._lineH,
675
- charWidth: this._charW,
733
+ lineHeight: this._lineH / scale,
734
+ charWidth: this._charW / scale,
676
735
  family: String(s.family),
677
- size: Number(s.size),
736
+ size: Number(s.size) / scale,
678
737
  };
679
738
  }
680
739
 
681
- /** Text width in this editor's font. The built-in completion popup no
682
- * longer needs it — a `<popup anchor>` measures its own rows — but an app
683
- * laying out its own overlay against the text still does. */
740
+ /** Text width in this editor's font, in logical pixels. The built-in
741
+ * completion popup no longer needs it — a `<popup anchor>` measures its
742
+ * own rows — but an app laying out its own overlay against the text still
743
+ * does, and an overlay is laid out in style lengths. */
684
744
  measureText(text: string): number {
685
745
  const fonts = this._fonts();
686
- if (!fonts) return text.length * this._charW;
687
- return fonts.layout(text, this._textStyle()).width;
746
+ const width = fonts
747
+ ? fonts.layout(text, this._textStyle()).width
748
+ : text.length * this._charW;
749
+ return width / this._scale;
688
750
  }
689
751
 
690
- /** Caret geometry relative to this node's origin — node coordinates, which
691
- * is exactly what a `<popup anchor={{at}}>` takes, so the completion popup
692
- * hands this straight through and nothing here has to know where the
693
- * editor sits on screen. */
752
+ /** Caret geometry relative to this node's origin — node coordinates, in
753
+ * logical pixels, which is exactly what a `<popup anchor={{at}}>` takes,
754
+ * so the completion popup hands this straight through and nothing here
755
+ * has to know where the editor sits on screen, or on what panel. */
694
756
  caretRect(): { x: number; y: number; height: number } {
695
757
  this._syncProps();
696
758
  this._metrics();
697
759
  const content = this._contentRect();
698
760
  const textX = content.x + this._gutterWidth() - this.abs.x;
761
+ // everything above is device; the anchor is logical, so divide once
762
+ const s = this._scale;
699
763
  return {
700
- x: textX + this._caretX(this._caret) - this._scrollX,
764
+ x: (textX + this._caretX(this._caret) - this._scrollX) / s,
701
765
  y:
702
- content.y - this.abs.y + this._caret.line * this._lineH - this._scrollY,
703
- height: this._lineH,
766
+ (content.y -
767
+ this.abs.y +
768
+ this._caret.line * this._lineH -
769
+ this._scrollY) /
770
+ s,
771
+ height: this._lineH / s,
704
772
  };
705
773
  }
706
774
 
@@ -717,18 +785,26 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
717
785
  private _maxScrollX(): number {
718
786
  const content = this._contentRect();
719
787
  const textW = content.width - this._gutterWidth();
720
- return Math.max(0, this._widest + CARET_MARGIN - textW);
788
+ return Math.max(0, this._widest + CARET_MARGIN * this._scale - textW);
721
789
  }
722
790
 
723
- /** Wheel notches (deltaX/deltaY arrive as ±1 per notch) → pixels. */
791
+ /** Wheel notches (deltaX/deltaY arrive as ±1 per notch) → three lines a
792
+ * notch, in the device pixels the offsets are kept in. */
724
793
  handleWheel(ev: { deltaX: number; deltaY: number }): boolean {
725
794
  const step = this._lineHeight() * 3;
726
- return this.scrollBy(ev.deltaX * step, ev.deltaY * step);
795
+ return this._scrollByDevice(ev.deltaX * step, ev.deltaY * step);
727
796
  }
728
797
 
729
- /** Returns true when the offset moved (the component consumes the wheel
730
- * event exactly then, so an unscrollable editor lets the page scroll). */
798
+ /** Logical pixels, like core's own `scrollBy`: application code writes
799
+ * the unit it writes its styles in. Returns true when the offset moved
800
+ * (the component consumes the wheel event exactly then, so an
801
+ * unscrollable editor lets the page scroll). */
731
802
  scrollBy(dx: number, dy: number): boolean {
803
+ const s = this._scale;
804
+ return this._scrollByDevice(dx * s, dy * s);
805
+ }
806
+
807
+ private _scrollByDevice(dx: number, dy: number): boolean {
732
808
  const nx = Math.max(0, Math.min(this._scrollX + dx, this._maxScrollX()));
733
809
  const ny = Math.max(0, Math.min(this._scrollY + dy, this._maxScrollY()));
734
810
  if (nx === this._scrollX && ny === this._scrollY) return false;
@@ -749,11 +825,12 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
749
825
  }
750
826
  const textW = content.width - this._gutterWidth();
751
827
  const x = this._caretX(this._caret);
752
- if (x < this._scrollX + CARET_MARGIN) {
753
- this._scrollX = Math.max(0, x - CARET_MARGIN);
828
+ const margin = CARET_MARGIN * this._scale;
829
+ if (x < this._scrollX + margin) {
830
+ this._scrollX = Math.max(0, x - margin);
754
831
  }
755
- if (x > this._scrollX + textW - CARET_MARGIN) {
756
- this._scrollX = x - textW + CARET_MARGIN;
832
+ if (x > this._scrollX + textW - margin) {
833
+ this._scrollX = x - textW + margin;
757
834
  }
758
835
  this._scrollY = Math.max(0, Math.min(this._scrollY, this._maxScrollY()));
759
836
  this._scrollX = Math.max(0, this._scrollX);
@@ -1422,7 +1499,8 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1422
1499
  handleMouseDown(ev: EditorMouseEvent): boolean {
1423
1500
  this._syncProps();
1424
1501
  this.breakUndoRun();
1425
- const pos = this._posAt(ev.x, ev.y);
1502
+ const at = this._devicePoint(ev);
1503
+ const pos = this._posAt(at.x, at.y);
1426
1504
  if (ev.button === 3) {
1427
1505
  const [a, b] = this._selectionRange();
1428
1506
  if (comparePos(pos, a) < 0 || comparePos(pos, b) > 0) {
@@ -1455,7 +1533,8 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1455
1533
 
1456
1534
  handleMouseMove(ev: EditorMouseEvent): boolean {
1457
1535
  if (!this._drag) return false;
1458
- const pos = this._posAt(ev.x, ev.y);
1536
+ const at = this._devicePoint(ev);
1537
+ const pos = this._posAt(at.x, at.y);
1459
1538
  if (this._drag === 'char') this.moveCaret(pos, true);
1460
1539
  else if (this._drag === 'word')
1461
1540
  this._selectWordRange(this._dragOrigin, pos);
@@ -1598,6 +1677,9 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1598
1677
  const lineH = this._lineH;
1599
1678
  const gutterW = this._gutterWidth();
1600
1679
  const textX = content.x + gutterW - this._scrollX;
1680
+ // the context draws in device pixels, like everything measured above;
1681
+ // the hairlines, insets and minimums below are logical constants
1682
+ const s = this._scale;
1601
1683
  const props = this.props as unknown as CodeEditorProps;
1602
1684
  const styles = this._textStyle();
1603
1685
  const baseColor = styles.color as string;
@@ -1685,7 +1767,12 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1685
1767
  i === b.line
1686
1768
  ? this._caretX({ line: i, ch: b.ch })
1687
1769
  : (entry.layout?.width ?? 0) + this._charW * 0.5; // the newline
1688
- c.fillRect(textX + startX, lineY(i), Math.max(2, endX - startX), lineH);
1770
+ c.fillRect(
1771
+ textX + startX,
1772
+ lineY(i),
1773
+ Math.max(2 * s, endX - startX),
1774
+ lineH,
1775
+ );
1689
1776
  }
1690
1777
  }
1691
1778
 
@@ -1700,7 +1787,7 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1700
1787
  if (pos.line < first || pos.line > last) continue;
1701
1788
  const x0 = this._caretX(pos);
1702
1789
  const x1 = this._caretX({ line: pos.line, ch: pos.ch + 1 });
1703
- c.fillRect(textX + x0, lineY(pos.line), Math.max(1, x1 - x0), lineH);
1790
+ c.fillRect(textX + x0, lineY(pos.line), Math.max(s, x1 - x0), lineH);
1704
1791
  }
1705
1792
  }
1706
1793
  }
@@ -1763,7 +1850,7 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1763
1850
  lineY(i) +
1764
1851
  (lineH - (entry.layout?.height || lineH)) / 2 +
1765
1852
  (entry.layout?.height || lineH);
1766
- const y = Math.min(lineY(i) + lineH - 2, glyphBottom - 1);
1853
+ const y = Math.min(lineY(i) + lineH - 2 * s, glyphBottom - s);
1767
1854
  const width = Math.max(this._charW * 0.6, x1 - x0);
1768
1855
  if (
1769
1856
  typeof c.beginPath === 'function' &&
@@ -1772,34 +1859,35 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1772
1859
  typeof c.stroke === 'function'
1773
1860
  ) {
1774
1861
  c.beginPath();
1775
- const step = 4;
1862
+ // a hairline zig-zag, 4px a step and 2px tall, on the panel's grid
1863
+ const step = 4 * s;
1776
1864
  let up = true;
1777
1865
  for (let x = 0; x <= width; x += step) {
1778
1866
  const px = textX + x0 + x;
1779
- const py = y + (up ? 0 : 2);
1867
+ const py = y + (up ? 0 : 2 * s);
1780
1868
  if (x === 0) c.moveTo(px, py);
1781
1869
  else c.lineTo(px, py);
1782
1870
  up = !up;
1783
1871
  }
1784
1872
  c.strokeStyle = color;
1785
- c.lineWidth = 1;
1873
+ c.lineWidth = s;
1786
1874
  c.stroke();
1787
1875
  } else {
1788
1876
  c.fillStyle = color;
1789
- c.fillRect(textX + x0, y + 1, width, 1);
1877
+ c.fillRect(textX + x0, y + s, width, s);
1790
1878
  }
1791
1879
  }
1792
1880
  }
1793
1881
 
1794
- // caret — 2px, in the (theme-resolved) text colour
1882
+ // caret — 2 logical px, in the (theme-resolved) text colour
1795
1883
  if (this._focused && this._caretOn) {
1796
1884
  const x = this._caretX(this._caret);
1797
1885
  c.fillStyle = this._resolveColor(props.caretColor) ?? baseColor;
1798
1886
  c.fillRect(
1799
1887
  Math.round(textX + x),
1800
- lineY(this._caret.line) + 1,
1801
- 2,
1802
- lineH - 2,
1888
+ lineY(this._caret.line) + s,
1889
+ 2 * s,
1890
+ lineH - 2 * s,
1803
1891
  );
1804
1892
  }
1805
1893
 
@@ -1826,40 +1914,46 @@ export class CodeEditorNode extends Node implements CodeEditorHandle {
1826
1914
  );
1827
1915
  layout.draw(
1828
1916
  ctx,
1829
- content.x + gutterW - GUTTER_PAD - layout.width,
1917
+ content.x + gutterW - GUTTER_PAD * s - layout.width,
1830
1918
  lineY(i) + (lineH - layout.height) / 2,
1831
1919
  );
1832
1920
  }
1833
1921
  }
1834
1922
  }
1835
1923
 
1836
- // scroll thumbs — minimal, panel-style
1924
+ // scroll thumbs — minimal, panel-style: a 3px bar inset 4px, never
1925
+ // shorter than 24px, every one of those logical
1837
1926
  const maxY = this._maxScrollY();
1838
1927
  if (maxY > 0) {
1839
1928
  const trackH = content.height;
1840
1929
  const thumbH = Math.max(
1841
- 24,
1930
+ 24 * s,
1842
1931
  (trackH * trackH) / (this._lines.length * lineH),
1843
1932
  );
1844
1933
  const yOff = (this._scrollY / maxY) * (trackH - thumbH);
1845
1934
  c.fillStyle = 'rgba(128,128,128,0.35)';
1846
- c.fillRect(content.x + content.width - 4, content.y + yOff, 3, thumbH);
1935
+ c.fillRect(
1936
+ content.x + content.width - 4 * s,
1937
+ content.y + yOff,
1938
+ 3 * s,
1939
+ thumbH,
1940
+ );
1847
1941
  }
1848
1942
  const maxX = this._maxScrollX();
1849
1943
  if (maxX > 0) {
1850
1944
  const textW = content.width - gutterW;
1851
1945
  const trackW = textW;
1852
1946
  const thumbW = Math.max(
1853
- 24,
1854
- (trackW * trackW) / (this._widest + CARET_MARGIN),
1947
+ 24 * s,
1948
+ (trackW * trackW) / (this._widest + CARET_MARGIN * s),
1855
1949
  );
1856
1950
  const xOff = (this._scrollX / maxX) * (trackW - thumbW);
1857
1951
  c.fillStyle = 'rgba(128,128,128,0.35)';
1858
1952
  c.fillRect(
1859
1953
  content.x + gutterW + xOff,
1860
- content.y + content.height - 4,
1954
+ content.y + content.height - 4 * s,
1861
1955
  thumbW,
1862
- 3,
1956
+ 3 * s,
1863
1957
  );
1864
1958
  }
1865
1959