react-x11 2.11.0 → 2.12.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 (96) hide show
  1. package/README.md +278 -129
  2. package/package.json +10 -3
  3. package/src/Reconciler.js +15 -17
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/bootstrap.js +14 -0
  7. package/src/clientmessage.js +1 -1
  8. package/src/cocoa/app.js +292 -49
  9. package/src/cocoa/bezels.js +175 -30
  10. package/src/cocoa/dnd.js +27 -13
  11. package/src/cocoa/fonts.js +3 -3
  12. package/src/cocoa/glarea.js +20 -3
  13. package/src/cocoa/main.d.ts +8 -0
  14. package/src/cocoa/main.js +43 -0
  15. package/src/cocoa/panehost.js +15 -5
  16. package/src/cocoa/presenter.js +13 -9
  17. package/src/cocoa/promotion.js +4 -7
  18. package/src/cocoa/relaunch.js +207 -0
  19. package/src/cocoa/threaded.js +246 -0
  20. package/src/cocoa/window.js +256 -42
  21. package/src/components/Select.js +2 -2
  22. package/src/components/anchor.js +3 -3
  23. package/src/components/native.js +12 -7
  24. package/src/components/theme.js +2 -2
  25. package/src/debug.js +1 -1
  26. package/src/decorations.js +1 -1
  27. package/src/editmenu.js +2 -2
  28. package/src/errors.js +46 -0
  29. package/src/events.js +6 -6
  30. package/src/foreignnodes.js +3 -2
  31. package/src/frames.js +2 -2
  32. package/src/glnodes.js +1 -1
  33. package/src/grid.js +1653 -0
  34. package/src/host.d.ts +230 -0
  35. package/src/host.js +11 -3
  36. package/src/imagesource.js +1 -1
  37. package/src/index.d.ts +21 -4
  38. package/src/index.js +9 -1
  39. package/src/layouts.js +721 -0
  40. package/src/node.d.ts +4 -2
  41. package/src/node.js +19 -21
  42. package/src/nodes/animation.js +644 -0
  43. package/src/nodes/box.js +21 -0
  44. package/src/nodes/boxpaint.js +473 -0
  45. package/src/nodes/canvas.js +269 -0
  46. package/src/nodes/cascade.js +600 -0
  47. package/src/nodes/damage.js +183 -0
  48. package/src/nodes/edithistory.js +124 -0
  49. package/src/nodes/editmenupopup.js +260 -0
  50. package/src/nodes/hittest.js +185 -0
  51. package/src/nodes/image.js +266 -0
  52. package/src/nodes/install.js +75 -0
  53. package/src/nodes/invalidate.js +465 -0
  54. package/src/nodes/kinds.js +31 -0
  55. package/src/nodes/layout.js +439 -0
  56. package/src/nodes/layouthost.js +949 -0
  57. package/src/nodes/node.js +868 -0
  58. package/src/nodes/paint.js +466 -0
  59. package/src/nodes/position.js +366 -0
  60. package/src/nodes/preedit.js +127 -0
  61. package/src/nodes/queries.js +330 -0
  62. package/src/nodes/rects.js +102 -0
  63. package/src/nodes/scrollable.js +891 -0
  64. package/src/nodes/scrollbars.js +138 -0
  65. package/src/nodes/scrollblit.js +1034 -0
  66. package/src/nodes/selectable.js +142 -0
  67. package/src/nodes/styling.js +225 -0
  68. package/src/nodes/text.js +649 -0
  69. package/src/nodes/textarea.js +391 -0
  70. package/src/nodes/textinput.js +1146 -0
  71. package/src/nodes/util.js +17 -0
  72. package/src/nodes/window/anchoring.js +161 -0
  73. package/src/nodes/window/capabilities.js +190 -0
  74. package/src/nodes/window/debugpaint.js +83 -0
  75. package/src/nodes/window/droptarget.js +145 -0
  76. package/src/nodes/window/floors.js +577 -0
  77. package/src/nodes/window/flush.js +334 -0
  78. package/src/nodes/window/hints.js +482 -0
  79. package/src/nodes/window/listeners.js +222 -0
  80. package/src/nodes/window/popup.js +71 -0
  81. package/src/nodes/window/size.js +591 -0
  82. package/src/nodes/window/window.js +945 -0
  83. package/src/palette.js +1 -1
  84. package/src/registry.js +7 -3
  85. package/src/styles.js +137 -15
  86. package/src/svgnodes.js +2 -1
  87. package/src/testing/harness.js +2 -2
  88. package/src/textselection.js +5 -3
  89. package/src/trace-registry.js +1 -1
  90. package/src/types/components.d.ts +38 -6
  91. package/src/types/elements.d.ts +11 -1
  92. package/src/types/nodes.d.ts +17 -2
  93. package/src/types/style.d.ts +94 -3
  94. package/src/windowstate.js +1 -1
  95. package/src/yoga.js +1 -1
  96. package/src/nodes.js +0 -13120
@@ -0,0 +1,473 @@
1
+ // What a box paints under its content: the background (a colour or a
2
+ // gradient), the shadow, the border and the outline. decorations.js parses
3
+ // the gradient and shadow grammar; this is the renderer half.
4
+
5
+ import {
6
+ DEFAULT_FOCUS_RING,
7
+ resolveBorderWidths,
8
+ resolveBorderColors,
9
+ } from '../styles.js';
10
+ import {
11
+ blurKernel,
12
+ gradientSpec,
13
+ linearGradientGeometry,
14
+ shadowExtent,
15
+ shadowSpecs,
16
+ } from '../decorations.js';
17
+ // Namespace import for `Surface`, the same shape and the same reason as
18
+ // `paintcache.js`: a named import of something an older ntk does not export
19
+ // is a *load-time* SyntaxError, which would take the renderer down rather
20
+ // than the one feature that needs it.
21
+ import * as ntk from 'ntk';
22
+ import { isFocusable as a11yFocusable } from '../a11y.js';
23
+ import { DEV } from './util.js';
24
+
25
+ /** CSS's `transparent` keyword means "paint nothing". ntk's colour parser
26
+ * does not know it and throws deep inside the 2d context, taking the whole
27
+ * frame with it, so filter it out at the source alongside null/''. */
28
+ export function isPaintedColor(color) {
29
+ return Boolean(color) && color !== 'transparent';
30
+ }
31
+
32
+ /**
33
+ * `rect`, rounded, as a path — the one shape the drawn layer is made of.
34
+ * `roundRect` needs ntk >= 3.2.0 and the square fallback is what an older
35
+ * one gets, which is the same degradation `_paintBorder` has always made.
36
+ */
37
+ function roundedPath(ctx, rect, radius) {
38
+ ctx.beginPath();
39
+ if (radius > 0 && typeof ctx.roundRect === 'function') {
40
+ ctx.roundRect(rect.x, rect.y, rect.width, rect.height, radius);
41
+ } else {
42
+ ctx.rect(rect.x, rect.y, rect.width, rect.height);
43
+ }
44
+ }
45
+
46
+ /** A style's shadow reach, for the callers that have a style rather than
47
+ * a node (`_retarget`'s before/after pair). `scale` because the value is a
48
+ * string the style funnel could not convert (see decorations.js). */
49
+ export function shadowExtentOf(style, scale = 1) {
50
+ const value = style?.boxShadow;
51
+ if (!value || value === 'none') return 0;
52
+ return shadowExtent(shadowSpecs(value, scale));
53
+ }
54
+
55
+ // `borderRadius` on a non-uniform border warned about once (`_paintBorderSides`).
56
+ let warnedSideRadius = false;
57
+
58
+ /** Box decorations, installed onto `Node.prototype` by node.js. */
59
+ export class NodeBoxPaint {
60
+ /**
61
+ * The focus ring this node would draw: its own `outline*` style, falling
62
+ * back to the theme's focus-ring tokens for anything it leaves out. Null
63
+ * when the node is not focusable and set no outline of its own, and when
64
+ * `outlineWidth: 0` opts out.
65
+ *
66
+ * Resolved here rather than folded into the style so that the default
67
+ * costs nothing until it is asked for — which is once per focused node
68
+ * per frame, not once per node per commit.
69
+ */
70
+ _outline(style = this.style) {
71
+ const explicit = style.outlineWidth !== undefined;
72
+ if (!explicit && !this._focusableForRing()) return null;
73
+ const theme = this.theme;
74
+ const width = style.outlineWidth ?? theme?.focusRingWidth;
75
+ const resolved = width ?? DEFAULT_FOCUS_RING.width;
76
+ if (!(resolved > 0)) return null;
77
+ return {
78
+ width: resolved,
79
+ color: style.outlineColor ?? theme?.focusRing ?? DEFAULT_FOCUS_RING.color,
80
+ offset:
81
+ style.outlineOffset ??
82
+ theme?.focusRingOffset ??
83
+ DEFAULT_FOCUS_RING.offset,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * How far outside `abs` this node's ink currently reaches. Zero unless
89
+ * the outline is actually being drawn — every focusable node is a
90
+ * candidate for the default ring, and inflating all of their damage
91
+ * rects for a ring that is not there would widen every claim in the tree
92
+ * and cost the scroll-blit fast path its containment test. The frame that
93
+ * *erases* a ring is the one case where the state has already flipped
94
+ * back, and `EventManager.focus` claims the region before it does.
95
+ */
96
+ _outlineExtent(style = this.style) {
97
+ if (style.outlineWidth === undefined && !this.states[':focus-visible'])
98
+ return 0;
99
+ const outline = this._outline(style);
100
+ return outline ? outline.width + Math.max(0, outline.offset) : 0;
101
+ }
102
+
103
+ /**
104
+ * How far outside `abs` this node's `boxShadow` reaches — the offset, the
105
+ * spread and the blur's tail, symmetrically (see `shadowExtent`).
106
+ */
107
+ _shadowExtent() {
108
+ return shadowExtentOf(this.style, this.scale);
109
+ }
110
+
111
+ /** Would a keyboard focus land here? The one rule lives in a11y.js —
112
+ * `EventManager._isFocusable` and the AT-SPI FOCUSABLE state read the
113
+ * same function, so the ring, the keyboard and the screen reader cannot
114
+ * disagree. */
115
+ _focusableForRing() {
116
+ return Boolean(a11yFocusable(this));
117
+ }
118
+
119
+ /**
120
+ * The focus ring. Drawn on `:focus-visible` — keyboard focus — so a press
121
+ * moves focus without lighting a ring the pointer user did not ask for,
122
+ * and Tab always lights one.
123
+ *
124
+ * A node that sets `outlineWidth` outside a state block gets the ring
125
+ * whenever it is styled to, focused or not; that is the escape hatch for
126
+ * anything wanting an outline for a reason of its own.
127
+ */
128
+ _paintOutline(ctx) {
129
+ const always = this.style.outlineWidth !== undefined;
130
+ if (!always && !this.states[':focus-visible']) return;
131
+ const outline = this._outline();
132
+ if (!outline || !isPaintedColor(outline.color)) return;
133
+ const { width, offset } = outline;
134
+ // stroked centred on the path, like the border, so half the width sits
135
+ // inside the offset gap and half outside it
136
+ const grow = offset + width / 2;
137
+ const radius = this.style.borderRadius ?? 0;
138
+ ctx.strokeStyle = outline.color;
139
+ ctx.lineWidth = width;
140
+ ctx.beginPath();
141
+ const x = this.abs.x - grow;
142
+ const y = this.abs.y - grow;
143
+ const w = this.abs.width + grow * 2;
144
+ const h = this.abs.height + grow * 2;
145
+ if (radius > 0 && typeof ctx.roundRect === 'function') {
146
+ ctx.roundRect(x, y, w, h, radius + grow);
147
+ } else {
148
+ ctx.rect(x, y, w, h);
149
+ }
150
+ ctx.stroke();
151
+ }
152
+
153
+ _roundedPath(ctx, radius) {
154
+ roundedPath(ctx, this.abs, radius);
155
+ }
156
+
157
+ _paintBackground(ctx) {
158
+ // under an element that covers this pass with opaque pixels, this fill
159
+ // is never seen (`WindowNode._coverFor`)
160
+ if (this.root?._coverChain?.has(this)) return;
161
+ const { backgroundColor, borderRadius = 0 } = this.style;
162
+ const fill = (style) => {
163
+ ctx.fillStyle = style;
164
+ if (borderRadius > 0) {
165
+ this._roundedPath(ctx, borderRadius);
166
+ ctx.fill();
167
+ } else {
168
+ ctx.fillRect(this.abs.x, this.abs.y, this.abs.width, this.abs.height);
169
+ }
170
+ };
171
+ if (isPaintedColor(backgroundColor)) fill(backgroundColor);
172
+ // …and the gradient over it, which is CSS's order and not a detail: a
173
+ // translucent gradient over a solid colour is how a tint is written, and
174
+ // a node that sets only the gradient pays one composite either way.
175
+ const gradient = this._backgroundGradient(ctx);
176
+ if (gradient) fill(gradient);
177
+ }
178
+
179
+ /**
180
+ * The `backgroundImage` gradient for this node's current box, as the ntk
181
+ * `CanvasGradient` a fill style takes — or null when there is none, when
182
+ * the box has no area, or when the context cannot make one (the headless
183
+ * mock).
184
+ *
185
+ * Cached on the node and keyed by the value *and the rect*, because the
186
+ * coordinates are absolute. They have to be: a gradient created after a
187
+ * `translate()` ignores the transform (sidorares/ntk#271), and the drawn
188
+ * layer paints in window coordinates anyway, so there is no translation to
189
+ * be wrong about — the cost is that a node which moves rebuilds its
190
+ * gradient, which is one small request and a picture the GC reclaims
191
+ * through ntk's finalizer. Headers, cards and rows are the customers here
192
+ * and they move on layout, not on input.
193
+ */
194
+ _backgroundGradient(ctx, rect = this.abs) {
195
+ const value = this.style.backgroundImage;
196
+ if (!value || value === 'none') return null;
197
+ if (typeof ctx.createLinearGradient !== 'function') return null;
198
+ const spec = gradientSpec(value, this.scale);
199
+ if (!spec) return null;
200
+ const { x, y, width, height } = rect;
201
+ const key = `${value}|${x},${y},${width},${height}`;
202
+ if (this._gradient?.key === key) return this._gradient.value;
203
+ const line = linearGradientGeometry(spec, rect);
204
+ if (!line) return null;
205
+ const gradient = ctx.createLinearGradient(
206
+ line.x0,
207
+ line.y0,
208
+ line.x1,
209
+ line.y1,
210
+ );
211
+ for (const [offset, color] of line.stops)
212
+ gradient.addColorStop(offset, color);
213
+ this._gradient = { key, value: gradient };
214
+ return gradient;
215
+ }
216
+
217
+ /**
218
+ * `boxShadow`. Back to front, like CSS: the first shadow in the list is
219
+ * the nearest the viewer, so the list is walked in reverse.
220
+ *
221
+ * A shadow with no blur is a rounded rectangle and costs one composite. A
222
+ * blurred one is coverage — an a8 surface holding the rectangle, blurred
223
+ * server-side by RENDER's convolution filter and then painted *through*
224
+ * the shadow colour, which is the same trick the glyph cache and `<canvas
225
+ * mono>` run on. That is what keeps the colour out of the cache key, so a
226
+ * `:hover` that only darkens the shadow reuses the surface it already
227
+ * rendered.
228
+ */
229
+ _paintShadow(ctx) {
230
+ const value = this.style.boxShadow;
231
+ if (!value || value === 'none') return;
232
+ const shadows = shadowSpecs(value, this.scale);
233
+ if (!shadows?.length) return;
234
+ const radius = this.style.borderRadius ?? 0;
235
+ for (let i = shadows.length - 1; i >= 0; i--) {
236
+ const shadow = shadows[i];
237
+ // CSS's `currentColor`: this node's *resolved* ink, its own `color`
238
+ // over what it inherits — so a shadow written without a colour follows
239
+ // the text it sits under, including down a `:hover` that dims both
240
+ const color = shadow.color ?? this.resolvedTextStyle().color;
241
+ if (!isPaintedColor(color)) continue;
242
+ const rect = {
243
+ x: this.abs.x + shadow.dx - shadow.spread,
244
+ y: this.abs.y + shadow.dy - shadow.spread,
245
+ width: this.abs.width + shadow.spread * 2,
246
+ height: this.abs.height + shadow.spread * 2,
247
+ };
248
+ if (!(rect.width > 0) || !(rect.height > 0)) continue;
249
+ // the spread grows the corner with the box, the way CSS's does
250
+ const r = Math.max(0, radius + shadow.spread);
251
+ if (!(shadow.blur > 0)) {
252
+ ctx.fillStyle = color;
253
+ roundedPath(ctx, rect, r);
254
+ ctx.fill();
255
+ continue;
256
+ }
257
+ this._paintBlurredShadow(ctx, rect, r, shadow.blur, color);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * One blurred shadow, through the paint cache when there is one.
263
+ *
264
+ * The surface is the shadow's rectangle plus `pad` on every side, and the
265
+ * padding is load-bearing: a convolution reads outside the picture as
266
+ * transparent, so a kernel that runs off the edge ends the shadow in a
267
+ * straight line. `blurKernel` takes that reach from the same function
268
+ * ntk builds the kernel with, so the two cannot drift apart.
269
+ *
270
+ * The blur is **baked into the pixels** by `blurCoverage` (ntk 8.6,
271
+ * ntk#335) rather than set as a filter on the picture. That is the
272
+ * difference between a cached shadow and a cached shadow that costs
273
+ * nothing to draw: a picture's filter is re-applied by the server on every
274
+ * composite, so the entry would hit, re-render nothing, and still pay its
275
+ * whole kernel every frame — 244M multiply-accumulates for one card-sized
276
+ * shadow, which was 1.6s per `:hover` on XQuartz. Baked, what the cache
277
+ * holds composites as an ordinary mask however wide the blur was, and the
278
+ * two separable passes run once per distinct geometry.
279
+ *
280
+ * `maxPixels` is raised well above the cache's default: a card's shadow is
281
+ * as big as the card, an entry for one is a8 (a byte a pixel), and the
282
+ * thing being avoided is exactly the cost the default cap bounds
283
+ * elsewhere.
284
+ */
285
+ _paintBlurredShadow(ctx, rect, radius, blur, color) {
286
+ const { sigma, pad } = blurKernel(blur);
287
+ // integral, because the surface is pixels; the blur is far wider than
288
+ // the rounding, so nothing about the result is visibly quantized
289
+ const width = Math.round(rect.width);
290
+ const height = Math.round(rect.height);
291
+ const plan = {
292
+ key: `shadow|${width}x${height}|r${radius}|b${blur}`,
293
+ x: Math.round(rect.x) - pad,
294
+ y: Math.round(rect.y) - pad,
295
+ width: width + pad * 2,
296
+ height: height + pad * 2,
297
+ format: 'a8',
298
+ tint: color,
299
+ maxPixels: 1024 * 1024,
300
+ // Cache on the first sighting rather than the second: what the gate
301
+ // saves elsewhere is a cheap redraw, and what it costs here is a whole
302
+ // gaussian — the one thing this entry exists to avoid running twice.
303
+ eager: true,
304
+ draw: (sctx, box) => {
305
+ // full coverage: the colour arrives at composite time
306
+ sctx.fillStyle = '#ffffff';
307
+ roundedPath(
308
+ sctx,
309
+ { x: box.x + pad, y: box.y + pad, width, height },
310
+ radius,
311
+ );
312
+ sctx.fill();
313
+ },
314
+ after: (surface) => ntk.blurCoverage(surface, sigma),
315
+ live: () => this._paintShadowLive(ctx, plan),
316
+ };
317
+ const cache = this.root?._paintCache;
318
+ if (cache) cache.drawing(ctx, plan);
319
+ else this._paintShadowLive(ctx, plan);
320
+ }
321
+
322
+ /**
323
+ * The same drawing with no cache behind it — the paint-cache-disabled
324
+ * build, an entry too big for the budget, and the first frame of a shadow
325
+ * the cache has only seen once. A surface per frame is what a shadow costs
326
+ * without a cache; it is still one composite on the wire, and the
327
+ * alternative is not painting it. The blur is baked here too: two
328
+ * separable passes and a plain composite still beat one composite through
329
+ * a k x k kernel, by the ratio of 2k to k squared.
330
+ */
331
+ _paintShadowLive(ctx, plan) {
332
+ if (typeof ntk.Surface !== 'function' || !this.app?.display?.Render) return;
333
+ let surface = null;
334
+ try {
335
+ surface = new ntk.Surface(this.app, {
336
+ width: plan.width,
337
+ height: plan.height,
338
+ format: 'a8',
339
+ });
340
+ surface.render((sctx) =>
341
+ plan.draw(sctx, { x: 0, y: 0, width: plan.width, height: plan.height }),
342
+ );
343
+ // `after` may hand back a *different* surface — the blur is baked into
344
+ // a second one and the sharp copy destroyed — so both the drawing and
345
+ // the cleanup below follow what it returned.
346
+ surface = plan.after(surface) ?? surface;
347
+ const before = ctx.fillStyle;
348
+ ctx.fillStyle = plan.tint;
349
+ ctx.drawImage(surface, plan.x, plan.y);
350
+ ctx.fillStyle = before;
351
+ } catch {
352
+ // A server that will not give us a pixmap: the frame is still owed
353
+ // everything else in it, and a missing shadow is a cosmetic loss.
354
+ } finally {
355
+ surface?.destroy();
356
+ }
357
+ }
358
+
359
+ _paintBorder(ctx) {
360
+ const { borderRadius = 0 } = this.style;
361
+ // Both through the node's resolved direction: a `borderStartWidth` lays
362
+ // out on one side and has to paint on the same one.
363
+ const w = resolveBorderWidths(this.style, this.direction);
364
+ const colors = resolveBorderColors(this.style, this.direction);
365
+ const uniform =
366
+ w.top === w.right &&
367
+ w.top === w.bottom &&
368
+ w.top === w.left &&
369
+ colors.top === colors.right &&
370
+ colors.top === colors.bottom &&
371
+ colors.top === colors.left;
372
+ if (!uniform) {
373
+ this._paintBorderSides(ctx, w, colors);
374
+ return;
375
+ }
376
+ const borderWidth = w.top;
377
+ if (!(borderWidth > 0) || !isPaintedColor(colors.top)) return;
378
+ // dashed borders need ntk >= 3.2.0 (setLineDash); solid fallback below
379
+ const dashed =
380
+ this.style.borderStyle === 'dashed' &&
381
+ typeof ctx.setLineDash === 'function';
382
+ if (dashed) {
383
+ ctx.setLineDash([borderWidth * 2 + 2, borderWidth + 2]);
384
+ }
385
+ ctx.strokeStyle = colors.top;
386
+ ctx.lineWidth = borderWidth;
387
+ // stroke centered on the box edge inset by half the border width
388
+ const inset = borderWidth / 2;
389
+ ctx.beginPath();
390
+ if (borderRadius > 0 && typeof ctx.roundRect === 'function') {
391
+ ctx.roundRect(
392
+ this.abs.x + inset,
393
+ this.abs.y + inset,
394
+ this.abs.width - borderWidth,
395
+ this.abs.height - borderWidth,
396
+ Math.max(0, borderRadius - inset),
397
+ );
398
+ } else {
399
+ ctx.rect(
400
+ this.abs.x + inset,
401
+ this.abs.y + inset,
402
+ this.abs.width - borderWidth,
403
+ this.abs.height - borderWidth,
404
+ );
405
+ }
406
+ ctx.stroke();
407
+ if (dashed) {
408
+ ctx.setLineDash([]);
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Non-uniform borders: four independent strokes, square corners. The join
414
+ * rule is CSS-adjacent and deterministic — top and bottom span the full
415
+ * width of the box, left and right run between them — which every square
416
+ * case (bars, rules, accent edges) never notices, because it only has one
417
+ * painted side to begin with.
418
+ *
419
+ * `borderRadius` requires uniform borders in v1: a rounded corner between
420
+ * two sides of different width or colour has no honest square answer, so
421
+ * the radius is ignored here and DEV says so once rather than bending the
422
+ * strokes halfway.
423
+ */
424
+ _paintBorderSides(ctx, w, colors) {
425
+ if (DEV && (this.style.borderRadius ?? 0) > 0 && !warnedSideRadius) {
426
+ warnedSideRadius = true;
427
+ console.warn(
428
+ 'react-x11: borderRadius needs uniform borders — same width and ' +
429
+ 'colour on all four sides. This border is painted square. Round ' +
430
+ 'the corners with a uniform border, or drop the radius.',
431
+ );
432
+ }
433
+ const dashable = typeof ctx.setLineDash === 'function';
434
+ const dashed = this.style.borderStyle === 'dashed' && dashable;
435
+ const { x, y, width, height } = this.abs;
436
+ const side = (sw, color, x1, y1, x2, y2) => {
437
+ if (!(sw > 0) || !isPaintedColor(color)) return;
438
+ if (dashed) ctx.setLineDash([sw * 2 + 2, sw + 2]);
439
+ ctx.strokeStyle = color;
440
+ ctx.lineWidth = sw;
441
+ ctx.beginPath();
442
+ ctx.moveTo(x1, y1);
443
+ ctx.lineTo(x2, y2);
444
+ ctx.stroke();
445
+ };
446
+ side(w.top, colors.top, x, y + w.top / 2, x + width, y + w.top / 2);
447
+ side(
448
+ w.bottom,
449
+ colors.bottom,
450
+ x,
451
+ y + height - w.bottom / 2,
452
+ x + width,
453
+ y + height - w.bottom / 2,
454
+ );
455
+ side(
456
+ w.left,
457
+ colors.left,
458
+ x + w.left / 2,
459
+ y + w.top,
460
+ x + w.left / 2,
461
+ y + height - w.bottom,
462
+ );
463
+ side(
464
+ w.right,
465
+ colors.right,
466
+ x + width - w.right / 2,
467
+ y + w.top,
468
+ x + width - w.right / 2,
469
+ y + height - w.bottom,
470
+ );
471
+ if (dashed) ctx.setLineDash([]);
472
+ }
473
+ }