react-x11 2.38.0 → 2.40.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/package.json +3 -3
- package/src/acceleratorhooks.js +24 -8
- package/src/accelerators.js +26 -9
- package/src/backend/context2d.js +292 -35
- package/src/backend/filter.js +285 -0
- package/src/cocoa/app.js +7 -0
- package/src/cocoa/globalmenu.js +32 -1
- package/src/cocoa/panehost.js +117 -1
- package/src/cocoa/sprites.js +95 -18
- package/src/components/Menu.js +57 -11
- package/src/components/PasswordInput.js +11 -6
- package/src/editmenu.js +9 -2
- package/src/events.js +24 -0
- package/src/frame/index.js +1 -1
- package/src/globalmenu.js +21 -3
- package/src/index.d.ts +4 -1
- package/src/keysyms.d.ts +30 -0
- package/src/keysyms.js +29 -0
- package/src/menuitem.js +63 -17
- package/src/node.d.ts +44 -3
- package/src/nodes/editmenupopup.js +21 -2
- package/src/nodes/sprites.js +6 -3
- package/src/nodes/textarea.js +3 -1
- package/src/nodes/textinput.js +13 -3
- package/src/textselection.js +7 -7
- package/src/types/components.d.ts +8 -2
- package/src/types/elements.d.ts +1 -1
- package/src/types/events.d.ts +21 -3
- package/src/types/nodes.d.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.40.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -109,9 +109,9 @@
|
|
|
109
109
|
"yoga-layout": "^3.2.1"
|
|
110
110
|
},
|
|
111
111
|
"optionalDependencies": {
|
|
112
|
-
"@windowkit/appkit": "^0.
|
|
112
|
+
"@windowkit/appkit": "^0.24.0",
|
|
113
113
|
"@windowkit/wayland": "^3.1.1",
|
|
114
|
-
"@windowkit/win32": "^0.0.
|
|
114
|
+
"@windowkit/win32": "^0.0.7",
|
|
115
115
|
"dbus-native": "^0.16.0",
|
|
116
116
|
"x11-dri": "^0.9.0"
|
|
117
117
|
},
|
package/src/acceleratorhooks.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import { useEffect, useMemo, useRef } from 'react';
|
|
10
10
|
|
|
11
11
|
import { matchesShortcut } from './accelerators.js';
|
|
12
|
+
import { useAppOrNull } from './appcontext.js';
|
|
13
|
+
import { primaryModifier } from './keysyms.js';
|
|
12
14
|
import { useTopLevelWindow } from './windowid.js';
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -52,10 +54,20 @@ function warnUnanchored() {
|
|
|
52
54
|
* *not* a dependency: a chord pressed three minutes from now must run the
|
|
53
55
|
* handler from the current render, and re-registering on every render would
|
|
54
56
|
* also reorder the bindings under it.
|
|
57
|
+
*
|
|
58
|
+
* `reserves(ev)`, read the same way, says the key belongs to something
|
|
59
|
+
* outside the window that answers it by itself — a delegated `MenuBar`'s
|
|
60
|
+
* key equivalents on the Cocoa backend — so no binding gets it
|
|
61
|
+
* (`EventManager._runAccelerators`).
|
|
55
62
|
*/
|
|
56
|
-
export function useAcceleratorEntry(
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
export function useAcceleratorEntry(
|
|
64
|
+
anchorRef,
|
|
65
|
+
handle,
|
|
66
|
+
enabled = true,
|
|
67
|
+
reserves = null,
|
|
68
|
+
) {
|
|
69
|
+
const live = useRef({ handle, reserves });
|
|
70
|
+
live.current = { handle, reserves };
|
|
59
71
|
useEffect(() => {
|
|
60
72
|
if (!enabled) return undefined;
|
|
61
73
|
const manager = anchorRef.current?.root?.events;
|
|
@@ -65,7 +77,8 @@ export function useAcceleratorEntry(anchorRef, handle, enabled = true) {
|
|
|
65
77
|
}
|
|
66
78
|
return manager.registerAccelerator({
|
|
67
79
|
anchor: () => anchorRef.current ?? null,
|
|
68
|
-
handle: (ev) => live.current?.(ev) ?? false,
|
|
80
|
+
handle: (ev) => live.current.handle?.(ev) ?? false,
|
|
81
|
+
reserves: (ev) => live.current.reserves?.(ev) ?? false,
|
|
69
82
|
});
|
|
70
83
|
}, [anchorRef, enabled]);
|
|
71
84
|
}
|
|
@@ -78,7 +91,8 @@ export function useAcceleratorEntry(anchorRef, handle, enabled = true) {
|
|
|
78
91
|
* a shortcut can be moved into or out of a menu without being rewritten, and
|
|
79
92
|
* the same rules apply to it: exact on Control/Alt/Shift/Super, indifferent
|
|
80
93
|
* to Caps Lock and Num Lock, matched against the Latin keysym so it survives
|
|
81
|
-
* a layout switch,
|
|
94
|
+
* a layout switch, `Control` meaning ⌘ on the Cocoa backend
|
|
95
|
+
* (`primaryModifier`), and behind whatever a focused element consumed with
|
|
82
96
|
* `preventDefault()`.
|
|
83
97
|
*
|
|
84
98
|
* The handler is called with the key event, and the key is consumed.
|
|
@@ -106,12 +120,14 @@ export function useAccelerator(shortcut, handler, options = {}) {
|
|
|
106
120
|
},
|
|
107
121
|
[scope, owner],
|
|
108
122
|
);
|
|
109
|
-
const
|
|
110
|
-
live
|
|
123
|
+
const primary = primaryModifier(useAppOrNull());
|
|
124
|
+
const live = useRef({ shortcut, handler, primary });
|
|
125
|
+
live.current = { shortcut, handler, primary };
|
|
111
126
|
useAcceleratorEntry(
|
|
112
127
|
anchorRef,
|
|
113
128
|
(ev) => {
|
|
114
|
-
|
|
129
|
+
const { shortcut: chords, primary: mod } = live.current;
|
|
130
|
+
if (!matchesShortcut(ev, chords, { primary: mod })) return false;
|
|
115
131
|
live.current.handler?.(ev);
|
|
116
132
|
return true;
|
|
117
133
|
},
|
package/src/accelerators.js
CHANGED
|
@@ -25,6 +25,16 @@
|
|
|
25
25
|
// bit 1 and Num Lock is normally Mod2, so neither is in the comparison to
|
|
26
26
|
// begin with. This is the line every hand-rolled binding gets wrong, which is
|
|
27
27
|
// most of why it is here rather than in an application.
|
|
28
|
+
//
|
|
29
|
+
// ## Which modifier `Control` is
|
|
30
|
+
//
|
|
31
|
+
// The app's primary one (`primaryModifier`, src/keysyms.js): Ctrl, except on
|
|
32
|
+
// the Cocoa backend, where a cross-platform chord's `Control` is ⌘ — the rule
|
|
33
|
+
// a menu bar's key equivalents (cocoa/globalmenu.js) and a drawn row's label
|
|
34
|
+
// (`formatShortcut`) already follow, so the key a menu prints is the key that
|
|
35
|
+
// fires it. `Super` is ⌘ there too, and ⌃ has no token: a chord is matched
|
|
36
|
+
// with it up. The matcher is pure, so the primary is an option rather than
|
|
37
|
+
// something it looks up; the hooks hand it the app's.
|
|
28
38
|
|
|
29
39
|
import {
|
|
30
40
|
isEnabled,
|
|
@@ -68,17 +78,21 @@ export function chordKeysym(chord) {
|
|
|
68
78
|
* The second path is **letters excluded** deliberately: it is exactly what
|
|
69
79
|
* would make Ctrl+Shift+S fire a Ctrl+S binding, which is the thing the
|
|
70
80
|
* modifier comparison is here to prevent.
|
|
81
|
+
*
|
|
82
|
+
* `primary` is the app's primary modifier: `'Super'` reads `Control` as ⌘,
|
|
83
|
+
* the way the Cocoa backend's menus print and install it.
|
|
71
84
|
*/
|
|
72
|
-
export function matchesChord(ev, chord) {
|
|
85
|
+
export function matchesChord(ev, chord, { primary = 'Control' } = {}) {
|
|
73
86
|
const { modifiers, key } = splitChord(chord);
|
|
74
87
|
if (key == null) return false;
|
|
75
88
|
const wanted = keysymFromName(key);
|
|
76
89
|
if (wanted === undefined) return false;
|
|
77
90
|
|
|
78
|
-
const
|
|
91
|
+
const mac = primary === 'Super';
|
|
92
|
+
const ctrl = !mac && modifiers.has('Control');
|
|
79
93
|
const alt = modifiers.has('Alt');
|
|
80
94
|
const shift = modifiers.has('Shift');
|
|
81
|
-
const meta = modifiers.has('Super');
|
|
95
|
+
const meta = modifiers.has('Super') || (mac && modifiers.has('Control'));
|
|
82
96
|
if (
|
|
83
97
|
Boolean(ev?.ctrlKey) !== ctrl ||
|
|
84
98
|
Boolean(ev?.altKey) !== alt ||
|
|
@@ -98,11 +112,14 @@ export function matchesChord(ev, chord) {
|
|
|
98
112
|
return (!shift || Boolean(ev?.shiftKey)) && ev?.codepoint === wanted;
|
|
99
113
|
}
|
|
100
114
|
|
|
101
|
-
/**
|
|
102
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Does this key event press any alternative of an `aas` shortcut? `options`
|
|
117
|
+
* is `matchesChord`'s: `{ primary }`.
|
|
118
|
+
*/
|
|
119
|
+
export function matchesShortcut(ev, shortcut, options) {
|
|
103
120
|
if (!Array.isArray(shortcut)) return false;
|
|
104
121
|
return shortcut.some(
|
|
105
|
-
(chord) => Array.isArray(chord) && matchesChord(ev, chord),
|
|
122
|
+
(chord) => Array.isArray(chord) && matchesChord(ev, chord, options),
|
|
106
123
|
);
|
|
107
124
|
}
|
|
108
125
|
|
|
@@ -117,11 +134,11 @@ export function matchesShortcut(ev, shortcut) {
|
|
|
117
134
|
* the walk goes on, so a disabled "Save" does not also swallow the key from
|
|
118
135
|
* whatever else claims it.
|
|
119
136
|
*/
|
|
120
|
-
export function acceleratedItem(items, ev) {
|
|
137
|
+
export function acceleratedItem(items, ev, options) {
|
|
121
138
|
for (const item of visibleItems(items)) {
|
|
122
139
|
if (isSeparator(item) || !isEnabled(item)) continue;
|
|
123
|
-
if (matchesShortcut(ev, item.shortcut)) return item;
|
|
124
|
-
const inner = acceleratedItem(item.items, ev);
|
|
140
|
+
if (matchesShortcut(ev, item.shortcut, options)) return item;
|
|
141
|
+
const inner = acceleratedItem(item.items, ev, options);
|
|
125
142
|
if (inner) return inner;
|
|
126
143
|
}
|
|
127
144
|
return null;
|
package/src/backend/context2d.js
CHANGED
|
@@ -27,6 +27,13 @@ import { cssColorStraight } from 'ntk/color';
|
|
|
27
27
|
|
|
28
28
|
import { normalizeRadii, planShadowTiles } from 'ntk/shadow-tiles';
|
|
29
29
|
|
|
30
|
+
import {
|
|
31
|
+
filterColour,
|
|
32
|
+
filterPixels,
|
|
33
|
+
filterStops,
|
|
34
|
+
filters,
|
|
35
|
+
parseCanvasFilter,
|
|
36
|
+
} from './filter.js';
|
|
30
37
|
import { shadowTilesFor } from './shadowtiles.js';
|
|
31
38
|
|
|
32
39
|
const BLACK = [0, 0, 0, 1];
|
|
@@ -107,6 +114,37 @@ function uploadImage(native, image) {
|
|
|
107
114
|
return handle;
|
|
108
115
|
}
|
|
109
116
|
|
|
117
|
+
/** `drawImage`'s arguments as the eight numbers of its longest form, the
|
|
118
|
+
* source rect and then the destination, whichever form they came in. */
|
|
119
|
+
function imageRects(size, args) {
|
|
120
|
+
let sx = 0;
|
|
121
|
+
let sy = 0;
|
|
122
|
+
let sw = size.width;
|
|
123
|
+
let sh = size.height;
|
|
124
|
+
let dx;
|
|
125
|
+
let dy;
|
|
126
|
+
let dw;
|
|
127
|
+
let dh;
|
|
128
|
+
if (args.length >= 8) {
|
|
129
|
+
[sx, sy, sw, sh, dx, dy, dw, dh] = args;
|
|
130
|
+
} else if (args.length >= 4) {
|
|
131
|
+
[dx, dy, dw, dh] = args;
|
|
132
|
+
} else {
|
|
133
|
+
[dx, dy] = args;
|
|
134
|
+
dw = sw;
|
|
135
|
+
dh = sh;
|
|
136
|
+
}
|
|
137
|
+
return [sx, sy, sw, sh, dx, dy, dw, dh];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Free a bitmap of the context's own making now, on a bridge that can;
|
|
141
|
+
* an older one frees it with the handle's finalizer. */
|
|
142
|
+
function release(native, handle) {
|
|
143
|
+
if (typeof native.releaseSurface === 'function') {
|
|
144
|
+
native.releaseSurface(handle);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
110
148
|
/** Free an Image's bitmap now, if it has one; drawing it again uploads it
|
|
111
149
|
* again, as ntk's `destroy()` promises for its own copies. */
|
|
112
150
|
export function releaseImageUpload(image) {
|
|
@@ -393,6 +431,13 @@ function meets(a, b) {
|
|
|
393
431
|
);
|
|
394
432
|
}
|
|
395
433
|
|
|
434
|
+
/** The qualities `imageSmoothingQuality` takes, canvas's. */
|
|
435
|
+
const SMOOTHING_QUALITIES = new Set(['low', 'medium', 'high']);
|
|
436
|
+
|
|
437
|
+
/** What the bridge is told to resample with: the quality, or the nearest
|
|
438
|
+
* pixel where smoothing is off. */
|
|
439
|
+
const smoothingOf = (st) => (st.smoothing ? st.smoothingQuality : 'none');
|
|
440
|
+
|
|
396
441
|
/**
|
|
397
442
|
* The off switch for shadow tiles (src/backend/shadowtiles.js): every
|
|
398
443
|
* shadow drawn by the bridge, as before them. For measuring the tiles
|
|
@@ -453,6 +498,13 @@ export class BackendContext2D {
|
|
|
453
498
|
// null: nothing clipped. A rect (surface pixels): that rect. NON_RECT:
|
|
454
499
|
// a clip this class cannot name.
|
|
455
500
|
clip: null,
|
|
501
|
+
// how an image drawn scaled or turned is resampled
|
|
502
|
+
smoothing: true,
|
|
503
|
+
smoothingQuality: 'medium',
|
|
504
|
+
// canvas's `filter` as it was set, and what it comes to
|
|
505
|
+
// (src/backend/filter.js): null for none
|
|
506
|
+
filter: 'none',
|
|
507
|
+
filterInk: null,
|
|
456
508
|
};
|
|
457
509
|
// What this bridge can do, asked once. Both verbs arrived together in
|
|
458
510
|
// @windowkit/appkit 0.7.0, and anything older is a bridge that draws
|
|
@@ -461,6 +513,18 @@ export class BackendContext2D {
|
|
|
461
513
|
// blit path is simply never taken.
|
|
462
514
|
this._blendModes = typeof native.ctxSetBlendMode === 'function';
|
|
463
515
|
this._blits = typeof native.blitSurface === 'function';
|
|
516
|
+
// and how an image is resampled, @windowkit/appkit 0.23.0's — asked
|
|
517
|
+
// with `in` as well, as `ctxPath` is below: a test's bridge that answers
|
|
518
|
+
// every name must not look like one that has it
|
|
519
|
+
this._smoothing =
|
|
520
|
+
'ctxSetImageSmoothing' in native &&
|
|
521
|
+
typeof native.ctxSetImageSmoothing === 'function';
|
|
522
|
+
// and what a filter is applied to pixels with: reading a surface and
|
|
523
|
+
// writing one, which every Cocoa bridge has had since 0.4 — asked all
|
|
524
|
+
// the same, so a bridge without them keeps `filter` at none
|
|
525
|
+
this._filters = ['createSurface', 'ctxGetImageData', 'ctxPutImageData']
|
|
526
|
+
.map((name) => name in native && typeof native[name] === 'function')
|
|
527
|
+
.every(Boolean);
|
|
464
528
|
this._onDirty = null;
|
|
465
529
|
// the recorded path, and whether the native one still matches it (a
|
|
466
530
|
// chunked stroke leaves only its last chunk behind)
|
|
@@ -527,6 +591,10 @@ export class BackendContext2D {
|
|
|
527
591
|
if (this._blendModes && st.gco !== 'source-over') {
|
|
528
592
|
n.ctxSetBlendMode(surface, st.gco);
|
|
529
593
|
}
|
|
594
|
+
// and one made at 'medium', which a context starts at
|
|
595
|
+
if (this._smoothing && smoothingOf(st) !== 'medium') {
|
|
596
|
+
n.ctxSetImageSmoothing(surface, smoothingOf(st));
|
|
597
|
+
}
|
|
530
598
|
// a fresh surface is unclipped, whatever the old one had in force
|
|
531
599
|
st.clip = null;
|
|
532
600
|
this._stack.length = 0;
|
|
@@ -642,6 +710,86 @@ export class BackendContext2D {
|
|
|
642
710
|
this._state.gco = value;
|
|
643
711
|
}
|
|
644
712
|
|
|
713
|
+
/**
|
|
714
|
+
* Canvas's `imageSmoothingEnabled` and `imageSmoothingQuality`: how an
|
|
715
|
+
* image or a surface drawn scaled or turned is resampled — the nearest
|
|
716
|
+
* pixel with smoothing off, and otherwise `'low'`, which is bilinear,
|
|
717
|
+
* `'medium'` or `'high'`. A context starts at `'medium'`, which is what
|
|
718
|
+
* the bridge makes its contexts at, and not at canvas's `'low'`: an image
|
|
719
|
+
* drawn small keeps the quality it was drawn at. Both are state `save`
|
|
720
|
+
* and `restore` keep.
|
|
721
|
+
*
|
|
722
|
+
* On macOS `'medium'` resamples the whole source for a draw through a
|
|
723
|
+
* matrix, whatever the clip, so a surface drawn a tile at a time through
|
|
724
|
+
* a perspective — `<Html>`'s box out of the plane — costs each tile all
|
|
725
|
+
* of it: 784 tiles of a 1400x1120 surface took 200 ms on an M1 Pro, and
|
|
726
|
+
* 27 at `'low'` (windowkit/appkit#109).
|
|
727
|
+
*
|
|
728
|
+
* A bridge with no `ctxSetImageSmoothing` keeps both where they start,
|
|
729
|
+
* so a caller learns whether an assignment took by reading it back, as
|
|
730
|
+
* with `globalCompositeOperation`.
|
|
731
|
+
*/
|
|
732
|
+
get imageSmoothingEnabled() {
|
|
733
|
+
return this._state.smoothing;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
set imageSmoothingEnabled(value) {
|
|
737
|
+
if (!this._smoothing) return;
|
|
738
|
+
// the surface first: a fresh one is told the state it is taking over
|
|
739
|
+
const surface = this._s();
|
|
740
|
+
this._state.smoothing = !!value;
|
|
741
|
+
this._native.ctxSetImageSmoothing(surface, smoothingOf(this._state));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
get imageSmoothingQuality() {
|
|
745
|
+
return this._state.smoothingQuality;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
set imageSmoothingQuality(value) {
|
|
749
|
+
if (!this._smoothing || !SMOOTHING_QUALITIES.has(value)) return;
|
|
750
|
+
const surface = this._s();
|
|
751
|
+
this._state.smoothingQuality = value;
|
|
752
|
+
this._native.ctxSetImageSmoothing(surface, smoothingOf(this._state));
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Canvas's `filter`: a CSS `<filter-value-list>` applied to everything
|
|
757
|
+
* drawn after it, which `save` and `restore` keep. This context applies
|
|
758
|
+
* the colour functions — `grayscale()`, `sepia()`, `saturate()`,
|
|
759
|
+
* `hue-rotate()`, `invert()`, `brightness()`, `contrast()` — and
|
|
760
|
+
* `opacity()` (src/backend/filter.js): a fill, a stroke, a gradient, a
|
|
761
|
+
* glyph run and a symbol in the colours the filter makes of theirs; an
|
|
762
|
+
* image, a surface and a text layout that carries colours of its own
|
|
763
|
+
* through their pixels. A list with `blur()`, `drop-shadow()` or a
|
|
764
|
+
* `url()` in it, or what is no list, does not stick, nor does any on a
|
|
765
|
+
* bridge that cannot read a surface's pixels back: a caller knows
|
|
766
|
+
* whether an assignment took by reading it back, as with
|
|
767
|
+
* `globalCompositeOperation`. The value reads back as it was written.
|
|
768
|
+
*/
|
|
769
|
+
get filter() {
|
|
770
|
+
return this._state.filter;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
set filter(value) {
|
|
774
|
+
if (!this._filters) return;
|
|
775
|
+
const ink = parseCanvasFilter(value);
|
|
776
|
+
if (ink === undefined) return;
|
|
777
|
+
this._state.filter = ink === null ? 'none' : String(value).trim();
|
|
778
|
+
this._state.filterInk = filters(ink) ? ink : null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** A straight `[r, g, b, a]` as the filter in force draws it. */
|
|
782
|
+
_ink(rgba) {
|
|
783
|
+
const ink = this._state.filterInk;
|
|
784
|
+
return ink ? filterColour(rgba, ink) : rgba;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** A gradient's flat stops as the filter in force draws them. */
|
|
788
|
+
_stops(flat) {
|
|
789
|
+
const ink = this._state.filterInk;
|
|
790
|
+
return ink ? filterStops(flat, ink) : flat;
|
|
791
|
+
}
|
|
792
|
+
|
|
645
793
|
/**
|
|
646
794
|
* Whether a stroke of a path with many subpaths may go out as several
|
|
647
795
|
* `CGContextStrokePath` calls — on by default, because the alternative
|
|
@@ -1188,12 +1336,12 @@ export class BackendContext2D {
|
|
|
1188
1336
|
}
|
|
1189
1337
|
|
|
1190
1338
|
_applyFill() {
|
|
1191
|
-
const [r, g, b, a] = parseColor(this._state.fillStyle);
|
|
1339
|
+
const [r, g, b, a] = this._ink(parseColor(this._state.fillStyle));
|
|
1192
1340
|
this._native.ctxSetFillColor(this._s(), r, g, b, a);
|
|
1193
1341
|
}
|
|
1194
1342
|
|
|
1195
1343
|
_applyStroke() {
|
|
1196
|
-
const [r, g, b, a] = parseColor(this._state.strokeStyle);
|
|
1344
|
+
const [r, g, b, a] = this._ink(parseColor(this._state.strokeStyle));
|
|
1197
1345
|
this._native.ctxSetStrokeColor(this._s(), r, g, b, a);
|
|
1198
1346
|
}
|
|
1199
1347
|
|
|
@@ -1305,7 +1453,8 @@ export class BackendContext2D {
|
|
|
1305
1453
|
this._path();
|
|
1306
1454
|
const style = this._state.fillStyle;
|
|
1307
1455
|
if (style instanceof LinearGradient) {
|
|
1308
|
-
const { coords, flat } = style._normalized();
|
|
1456
|
+
const { coords, flat: stops } = style._normalized();
|
|
1457
|
+
const flat = this._stops(stops);
|
|
1309
1458
|
this._native.ctxFillLinearGradient(
|
|
1310
1459
|
this._s(),
|
|
1311
1460
|
coords[0],
|
|
@@ -1384,10 +1533,16 @@ export class BackendContext2D {
|
|
|
1384
1533
|
if (!plan) return false;
|
|
1385
1534
|
const clip = st.clip === NON_RECT ? null : st.clip;
|
|
1386
1535
|
if (clip && !meets(plan.bounds, clip)) return true;
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1536
|
+
// the shadow of what a filter's `opacity()` fades is faded with it;
|
|
1537
|
+
// its colour is the shadow's own, cast after the filter (canvas)
|
|
1538
|
+
const [sr, sg, sb, sa] = parseColor(st.shadowColor);
|
|
1539
|
+
const fade = st.filterInk?.alpha ?? 1;
|
|
1540
|
+
const tile = shadowTilesFor(this._native).get(plan, [
|
|
1541
|
+
sr,
|
|
1542
|
+
sg,
|
|
1543
|
+
sb,
|
|
1544
|
+
sa * fade,
|
|
1545
|
+
]);
|
|
1391
1546
|
if (tile == null) return false;
|
|
1392
1547
|
const s = this._s();
|
|
1393
1548
|
const n = this._native;
|
|
@@ -1441,16 +1596,19 @@ export class BackendContext2D {
|
|
|
1441
1596
|
this._dirty();
|
|
1442
1597
|
}
|
|
1443
1598
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1599
|
+
/**
|
|
1600
|
+
* `clip()`, `clip(rule)`, `clip(path)`, `clip(path, rule)`, as canvas has
|
|
1601
|
+
* them and as `fill` takes them. The rule reaches the bridge as the flag
|
|
1602
|
+
* `ctxFill` takes: SvgView cuts to a `<clipPath>` with its `clip-rule`, and
|
|
1603
|
+
* a ring clipped `evenodd` by nonzero is a filled square. A bridge that
|
|
1604
|
+
* predates the flag ignores it and clips nonzero, as every bridge did.
|
|
1605
|
+
*/
|
|
1606
|
+
clip(pathOrRule, maybeRule) {
|
|
1607
|
+
const hasPath = pathOrRule != null && typeof pathOrRule === 'object';
|
|
1608
|
+
const rule = hasPath ? maybeRule : pathOrRule;
|
|
1609
|
+
if (hasPath && !this._replayPath(pathOrRule)) return;
|
|
1452
1610
|
this._state.clip = this._clipAfter();
|
|
1453
|
-
this._native.ctxClip(this._path());
|
|
1611
|
+
this._native.ctxClip(this._path(), rule === 'evenodd');
|
|
1454
1612
|
}
|
|
1455
1613
|
|
|
1456
1614
|
/**
|
|
@@ -1466,6 +1624,10 @@ export class BackendContext2D {
|
|
|
1466
1624
|
* sets — the damage rect, a scroll viewport, a square-cornered `overflow`
|
|
1467
1625
|
* — are whole pixels under a translate, and those are the ones this
|
|
1468
1626
|
* recognises.
|
|
1627
|
+
*
|
|
1628
|
+
* It answers for either fill rule. One rect is one closed subpath, wound
|
|
1629
|
+
* once round everything inside it, so `evenodd` and `nonzero` cut the
|
|
1630
|
+
* same pixels; a path of more than one shape is never a rect here.
|
|
1469
1631
|
*/
|
|
1470
1632
|
_pathRect() {
|
|
1471
1633
|
const cmds = this._cmds;
|
|
@@ -1527,7 +1689,8 @@ export class BackendContext2D {
|
|
|
1527
1689
|
_fillRectNow(x, y, w, h) {
|
|
1528
1690
|
const style = this._state.fillStyle;
|
|
1529
1691
|
if (style instanceof LinearGradient) {
|
|
1530
|
-
const { coords, flat } = style._normalized();
|
|
1692
|
+
const { coords, flat: stops } = style._normalized();
|
|
1693
|
+
const flat = this._stops(stops);
|
|
1531
1694
|
this._native.ctxFillLinearGradient(
|
|
1532
1695
|
this._s(),
|
|
1533
1696
|
coords[0],
|
|
@@ -1592,22 +1755,11 @@ export class BackendContext2D {
|
|
|
1592
1755
|
const src = this._sourceHandle(image);
|
|
1593
1756
|
if (!src) return;
|
|
1594
1757
|
const size = this._native.surfaceSize(src);
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
let dy;
|
|
1601
|
-
let dw;
|
|
1602
|
-
let dh;
|
|
1603
|
-
if (args.length >= 8) {
|
|
1604
|
-
[sx, sy, sw, sh, dx, dy, dw, dh] = args;
|
|
1605
|
-
} else if (args.length >= 4) {
|
|
1606
|
-
[dx, dy, dw, dh] = args;
|
|
1607
|
-
} else {
|
|
1608
|
-
[dx, dy] = args;
|
|
1609
|
-
dw = sw;
|
|
1610
|
-
dh = sh;
|
|
1758
|
+
const [sx, sy, sw, sh, dx, dy, dw, dh] = imageRects(size, args);
|
|
1759
|
+
const ink = this._state.filterInk;
|
|
1760
|
+
if (ink) {
|
|
1761
|
+
this._drawFiltered(src, size, [sx, sy, sw, sh, dx, dy, dw, dh], ink);
|
|
1762
|
+
return;
|
|
1611
1763
|
}
|
|
1612
1764
|
if (this._blit(src, sx, sy, sw, sh, dx, dy, dw, dh)) {
|
|
1613
1765
|
// a row memcpy
|
|
@@ -1642,6 +1794,44 @@ export class BackendContext2D {
|
|
|
1642
1794
|
this._dirty();
|
|
1643
1795
|
}
|
|
1644
1796
|
|
|
1797
|
+
/**
|
|
1798
|
+
* `drawImage` under a filter: the whole pixels of the source the source
|
|
1799
|
+
* rect covers, read from the bridge as they are, run through the filter,
|
|
1800
|
+
* put on a bitmap of their own, and that drawn as the source would have
|
|
1801
|
+
* been — the filter set aside for the draw, which a blit, a fade and the
|
|
1802
|
+
* resampling an `imageSmoothingQuality` sets all take as before.
|
|
1803
|
+
*/
|
|
1804
|
+
_drawFiltered(src, size, [sx, sy, sw, sh, dx, dy, dw, dh], ink) {
|
|
1805
|
+
const x0 = Math.max(0, Math.floor(sx));
|
|
1806
|
+
const y0 = Math.max(0, Math.floor(sy));
|
|
1807
|
+
const w = Math.min(size.width, Math.ceil(sx + sw)) - x0;
|
|
1808
|
+
const h = Math.min(size.height, Math.ceil(sy + sh)) - y0;
|
|
1809
|
+
if (!(w > 0 && h > 0)) return;
|
|
1810
|
+
const n = this._native;
|
|
1811
|
+
const pixels = n.ctxGetImageData(src, x0, y0, w, h);
|
|
1812
|
+
filterPixels(pixels, pixels, ink);
|
|
1813
|
+
const filtered = n.createSurface(w, h, 1);
|
|
1814
|
+
const st = this._state;
|
|
1815
|
+
try {
|
|
1816
|
+
n.ctxPutImageData(filtered, pixels, w, h, 0, 0);
|
|
1817
|
+
st.filterInk = null;
|
|
1818
|
+
this.drawImage(
|
|
1819
|
+
{ _surfaceHandle: filtered },
|
|
1820
|
+
sx - x0,
|
|
1821
|
+
sy - y0,
|
|
1822
|
+
sw,
|
|
1823
|
+
sh,
|
|
1824
|
+
dx,
|
|
1825
|
+
dy,
|
|
1826
|
+
dw,
|
|
1827
|
+
dh,
|
|
1828
|
+
);
|
|
1829
|
+
} finally {
|
|
1830
|
+
st.filterInk = ink;
|
|
1831
|
+
release(n, filtered);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1645
1835
|
/**
|
|
1646
1836
|
* Whether a surface is drawn under this `globalAlpha` from its pixels
|
|
1647
1837
|
* scaled by it (@windowkit/appkit's `ctxDrawSurfaceFaded`): where the
|
|
@@ -1906,7 +2096,7 @@ export class BackendContext2D {
|
|
|
1906
2096
|
}
|
|
1907
2097
|
}
|
|
1908
2098
|
if (batches.size === 0) return;
|
|
1909
|
-
const [r, g, b, a] = this._inkOf(src);
|
|
2099
|
+
const [r, g, b, a] = this._ink(this._inkOf(src));
|
|
1910
2100
|
const surface = this._s();
|
|
1911
2101
|
this._native.ctxSetFillColor(surface, r, g, b, a);
|
|
1912
2102
|
const runs = [];
|
|
@@ -1945,10 +2135,15 @@ export class BackendContext2D {
|
|
|
1945
2135
|
// --- text (minimal: enough for <canvas onDraw> users) --------------------
|
|
1946
2136
|
|
|
1947
2137
|
_drawLayout(layout, x, y) {
|
|
2138
|
+
if (!layout._contextInk && this._state.filterInk) {
|
|
2139
|
+
this._drawLayoutFiltered(layout, x, y, this._state.filterInk);
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
1948
2142
|
if (layout._contextInk) {
|
|
1949
2143
|
const style = this._state.fillStyle;
|
|
1950
2144
|
if (style instanceof LinearGradient) {
|
|
1951
|
-
const { coords, flat } = style._normalized();
|
|
2145
|
+
const { coords, flat: stops } = style._normalized();
|
|
2146
|
+
const flat = this._stops(stops);
|
|
1952
2147
|
this._native.drawLayoutGradient(
|
|
1953
2148
|
this._s(),
|
|
1954
2149
|
layout._handle,
|
|
@@ -1969,6 +2164,68 @@ export class BackendContext2D {
|
|
|
1969
2164
|
this._dirty();
|
|
1970
2165
|
}
|
|
1971
2166
|
|
|
2167
|
+
/**
|
|
2168
|
+
* A layout that carries colours of its own, under a filter: the bridge
|
|
2169
|
+
* sets its glyphs in them, so it is drawn on a bitmap of its own the size
|
|
2170
|
+
* of where it lands on the surface — its box through the transform, and
|
|
2171
|
+
* half its height more each way for ink past the box — through the same
|
|
2172
|
+
* transform, and the bitmap is run through the filter and drawn where it
|
|
2173
|
+
* came from.
|
|
2174
|
+
*/
|
|
2175
|
+
_drawLayoutFiltered(layout, x, y, ink) {
|
|
2176
|
+
const st = this._state;
|
|
2177
|
+
const [a, b, c, d, e, f] = st.ctm;
|
|
2178
|
+
const pad = (layout.height || 0) / 2;
|
|
2179
|
+
const left = x - pad;
|
|
2180
|
+
const top = y - pad;
|
|
2181
|
+
const right = x + (layout.width || 0) + pad;
|
|
2182
|
+
const bottom = y + (layout.height || 0) + pad;
|
|
2183
|
+
let X0 = Infinity;
|
|
2184
|
+
let Y0 = Infinity;
|
|
2185
|
+
let X1 = -Infinity;
|
|
2186
|
+
let Y1 = -Infinity;
|
|
2187
|
+
for (const [px, py] of [
|
|
2188
|
+
[left, top],
|
|
2189
|
+
[right, top],
|
|
2190
|
+
[left, bottom],
|
|
2191
|
+
[right, bottom],
|
|
2192
|
+
]) {
|
|
2193
|
+
const qx = a * px + c * py + e;
|
|
2194
|
+
const qy = b * px + d * py + f;
|
|
2195
|
+
X0 = Math.min(X0, qx);
|
|
2196
|
+
Y0 = Math.min(Y0, qy);
|
|
2197
|
+
X1 = Math.max(X1, qx);
|
|
2198
|
+
Y1 = Math.max(Y1, qy);
|
|
2199
|
+
}
|
|
2200
|
+
const size = this._native.surfaceSize(this._s());
|
|
2201
|
+
X0 = Math.max(0, Math.floor(X0));
|
|
2202
|
+
Y0 = Math.max(0, Math.floor(Y0));
|
|
2203
|
+
X1 = Math.min(size.width, Math.ceil(X1));
|
|
2204
|
+
Y1 = Math.min(size.height, Math.ceil(Y1));
|
|
2205
|
+
const w = X1 - X0;
|
|
2206
|
+
const h = Y1 - Y0;
|
|
2207
|
+
if (!(w > 0 && h > 0)) return;
|
|
2208
|
+
const n = this._native;
|
|
2209
|
+
const own = n.createSurface(w, h, 1);
|
|
2210
|
+
try {
|
|
2211
|
+
n.ctxTransform(own, a, b, c, d, e - X0, f - Y0);
|
|
2212
|
+
n.drawLayout(own, layout._handle, x, y);
|
|
2213
|
+
const pixels = n.ctxGetImageData(own, 0, 0, w, h);
|
|
2214
|
+
filterPixels(pixels, pixels, ink);
|
|
2215
|
+
n.ctxPutImageData(own, pixels, w, h, 0, 0);
|
|
2216
|
+
this.save();
|
|
2217
|
+
try {
|
|
2218
|
+
this._state.filterInk = null;
|
|
2219
|
+
this.setTransform(1, 0, 0, 1, 0, 0);
|
|
2220
|
+
this.drawImage({ _surfaceHandle: own }, X0, Y0);
|
|
2221
|
+
} finally {
|
|
2222
|
+
this.restore();
|
|
2223
|
+
}
|
|
2224
|
+
} finally {
|
|
2225
|
+
release(n, own);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
|
|
1972
2229
|
measureText(text) {
|
|
1973
2230
|
const layout = this._fontLayout(text);
|
|
1974
2231
|
return layout
|