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.
- package/README.md +278 -129
- package/package.json +10 -3
- package/src/Reconciler.js +15 -17
- package/src/a11y.js +2 -2
- package/src/anchor.js +7 -5
- package/src/bootstrap.js +14 -0
- package/src/clientmessage.js +1 -1
- package/src/cocoa/app.js +292 -49
- package/src/cocoa/bezels.js +175 -30
- package/src/cocoa/dnd.js +27 -13
- package/src/cocoa/fonts.js +3 -3
- package/src/cocoa/glarea.js +20 -3
- package/src/cocoa/main.d.ts +8 -0
- package/src/cocoa/main.js +43 -0
- package/src/cocoa/panehost.js +15 -5
- package/src/cocoa/presenter.js +13 -9
- package/src/cocoa/promotion.js +4 -7
- package/src/cocoa/relaunch.js +207 -0
- package/src/cocoa/threaded.js +246 -0
- package/src/cocoa/window.js +256 -42
- package/src/components/Select.js +2 -2
- package/src/components/anchor.js +3 -3
- package/src/components/native.js +12 -7
- package/src/components/theme.js +2 -2
- package/src/debug.js +1 -1
- package/src/decorations.js +1 -1
- package/src/editmenu.js +2 -2
- package/src/errors.js +46 -0
- package/src/events.js +6 -6
- package/src/foreignnodes.js +3 -2
- package/src/frames.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1653 -0
- package/src/host.d.ts +230 -0
- package/src/host.js +11 -3
- package/src/imagesource.js +1 -1
- package/src/index.d.ts +21 -4
- package/src/index.js +9 -1
- package/src/layouts.js +721 -0
- package/src/node.d.ts +4 -2
- package/src/node.js +19 -21
- package/src/nodes/animation.js +644 -0
- package/src/nodes/box.js +21 -0
- package/src/nodes/boxpaint.js +473 -0
- package/src/nodes/canvas.js +269 -0
- package/src/nodes/cascade.js +600 -0
- package/src/nodes/damage.js +183 -0
- package/src/nodes/edithistory.js +124 -0
- package/src/nodes/editmenupopup.js +260 -0
- package/src/nodes/hittest.js +185 -0
- package/src/nodes/image.js +266 -0
- package/src/nodes/install.js +75 -0
- package/src/nodes/invalidate.js +465 -0
- package/src/nodes/kinds.js +31 -0
- package/src/nodes/layout.js +439 -0
- package/src/nodes/layouthost.js +949 -0
- package/src/nodes/node.js +868 -0
- package/src/nodes/paint.js +466 -0
- package/src/nodes/position.js +366 -0
- package/src/nodes/preedit.js +127 -0
- package/src/nodes/queries.js +330 -0
- package/src/nodes/rects.js +102 -0
- package/src/nodes/scrollable.js +891 -0
- package/src/nodes/scrollbars.js +138 -0
- package/src/nodes/scrollblit.js +1034 -0
- package/src/nodes/selectable.js +142 -0
- package/src/nodes/styling.js +225 -0
- package/src/nodes/text.js +649 -0
- package/src/nodes/textarea.js +391 -0
- package/src/nodes/textinput.js +1146 -0
- package/src/nodes/util.js +17 -0
- package/src/nodes/window/anchoring.js +161 -0
- package/src/nodes/window/capabilities.js +190 -0
- package/src/nodes/window/debugpaint.js +83 -0
- package/src/nodes/window/droptarget.js +145 -0
- package/src/nodes/window/floors.js +577 -0
- package/src/nodes/window/flush.js +334 -0
- package/src/nodes/window/hints.js +482 -0
- package/src/nodes/window/listeners.js +222 -0
- package/src/nodes/window/popup.js +71 -0
- package/src/nodes/window/size.js +591 -0
- package/src/nodes/window/window.js +945 -0
- package/src/palette.js +1 -1
- package/src/registry.js +7 -3
- package/src/styles.js +137 -15
- package/src/svgnodes.js +2 -1
- package/src/testing/harness.js +2 -2
- package/src/textselection.js +5 -3
- package/src/trace-registry.js +1 -1
- package/src/types/components.d.ts +38 -6
- package/src/types/elements.d.ts +11 -1
- package/src/types/nodes.d.ts +17 -2
- package/src/types/style.d.ts +94 -3
- package/src/windowstate.js +1 -1
- package/src/yoga.js +1 -1
- 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
|
+
}
|