react-x11 2.8.3 → 2.9.1
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 +4 -2
- package/src/Reconciler.js +7 -0
- package/src/cocoa/app.js +35 -0
- package/src/cocoa/calendar.js +186 -0
- package/src/cocoa/context2d.js +220 -4
- package/src/cocoa/native.js +20 -3
- package/src/cocoa/permissions.js +14 -5
- package/src/cocoa/window.js +21 -5
- package/src/debug.js +8 -2
- package/src/desktopcalendar.js +1415 -0
- package/src/desktopcalendarhooks.js +259 -0
- package/src/glnodes.js +63 -4
- package/src/index.d.ts +11 -1
- package/src/index.js +9 -0
- package/src/node.d.ts +214 -2
- package/src/nodes.js +208 -13
- package/src/pacing.js +476 -0
- package/src/permissionhooks.js +9 -6
- package/src/permissions.js +12 -2
- package/src/trace-registry.js +4 -3
- package/src/types/desktopcalendar.d.ts +207 -0
- package/src/types/elements.d.ts +54 -0
- package/src/types/permissions.d.ts +12 -4
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// `useDesktopCalendarEvents()` — the desktop's calendar as rendering state:
|
|
2
|
+
// the occurrences in a range, grouped the way a calendar grid asks for them,
|
|
3
|
+
// re-read when the store moves.
|
|
4
|
+
//
|
|
5
|
+
// The imperative half in `desktopcalendar.js` is complete; what a component
|
|
6
|
+
// wants on top is binding — the tree's connection, a handle that is closed
|
|
7
|
+
// when the view goes away, the grant asked for on the first read rather than
|
|
8
|
+
// at start-up — not another rung.
|
|
9
|
+
//
|
|
10
|
+
// ```jsx
|
|
11
|
+
// const { byDay } = useDesktopCalendarEvents({ from, to, watch: true });
|
|
12
|
+
//
|
|
13
|
+
// <Calendar
|
|
14
|
+
// dayContent={(day) =>
|
|
15
|
+
// byDay.get(day)?.slice(0, 3).map((ev) => (
|
|
16
|
+
// <box key={ev.uid} style={{ width: 4, height: 4, borderRadius: 2,
|
|
17
|
+
// backgroundColor: ev.calendar.color ?? '$accent' }} />
|
|
18
|
+
// ))
|
|
19
|
+
// }
|
|
20
|
+
// />
|
|
21
|
+
// ```
|
|
22
|
+
//
|
|
23
|
+
// `<Calendar>` is `@react-x11/components`; the `'YYYY-MM-DD'` keys are the
|
|
24
|
+
// one contract between the two packages, which is why `byDay` is here and
|
|
25
|
+
// the grid is there.
|
|
26
|
+
|
|
27
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
28
|
+
|
|
29
|
+
import { useAppOrNull } from './appcontext.js';
|
|
30
|
+
import {
|
|
31
|
+
CalendarAccessError,
|
|
32
|
+
byDay as groupByDay,
|
|
33
|
+
desktopCalendar,
|
|
34
|
+
} from './desktopcalendar.js';
|
|
35
|
+
import { openPrivacySettings } from './permissions.js';
|
|
36
|
+
|
|
37
|
+
const NO_EVENTS = [];
|
|
38
|
+
const NO_CALENDARS = [];
|
|
39
|
+
const NO_ERRORS = [];
|
|
40
|
+
|
|
41
|
+
/** The calendars a caller asked for by uid, or every enabled one. `onlyKey`
|
|
42
|
+
* is the comma-joined list, because an array prop is a new identity every
|
|
43
|
+
* render and these effects are keyed on it. */
|
|
44
|
+
function pickCalendars(all, onlyKey) {
|
|
45
|
+
if (!onlyKey) return all.filter((c) => c.enabled);
|
|
46
|
+
const wanted = new Set(onlyKey.split(','));
|
|
47
|
+
return all.filter((c) => wanted.has(c.uid));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The user's desktop calendar events, as rendering state.
|
|
52
|
+
*
|
|
53
|
+
* **`from` and `to` are read by their timestamps, not their identity**, so
|
|
54
|
+
* `new Date(...)` inline in the render body is fine and will not re-query on
|
|
55
|
+
* every paint. That is the mistake this hook would otherwise invite, and it
|
|
56
|
+
* costs a round trip per frame.
|
|
57
|
+
*
|
|
58
|
+
* `status` is the whole answer:
|
|
59
|
+
*
|
|
60
|
+
* - `'idle'` — `enabled: false`, nothing asked yet.
|
|
61
|
+
* - `'loading'` — a read is in flight. The first one may be showing the
|
|
62
|
+
* system's permission prompt: the first read is what asks, so that a
|
|
63
|
+
* picker the user never opens never prompts.
|
|
64
|
+
* - `'ready'` — `events` is this range.
|
|
65
|
+
* - `'denied'` — the **user's** answer. `openSettings()` puts them in front
|
|
66
|
+
* of the switch; do not show that button on any other status.
|
|
67
|
+
* - `'unavailable'` — the **machine's** answer: no bridge, not a Mac, no
|
|
68
|
+
* Evolution Data Server on the bus. An ordinary state, not a failure —
|
|
69
|
+
* hide the feature rather than reporting it.
|
|
70
|
+
*
|
|
71
|
+
* `errors` is different from all of them: calendars that would not answer
|
|
72
|
+
* while others did, which is one unreachable CalDAV server rather than a
|
|
73
|
+
* failed read.
|
|
74
|
+
*/
|
|
75
|
+
export function useDesktopCalendarEvents(options) {
|
|
76
|
+
const { from, to, calendars: only, watch = false, enabled = true } = options;
|
|
77
|
+
const app = useAppOrNull();
|
|
78
|
+
|
|
79
|
+
const [cal, setCal] = useState(null);
|
|
80
|
+
const [backend, setBackend] = useState(null);
|
|
81
|
+
const [events, setEvents] = useState(NO_EVENTS);
|
|
82
|
+
const [found, setFound] = useState(NO_CALENDARS);
|
|
83
|
+
const [errors, setErrors] = useState(NO_ERRORS);
|
|
84
|
+
const [status, setStatus] = useState('idle');
|
|
85
|
+
const [error, setError] = useState(null);
|
|
86
|
+
const [nonce, setNonce] = useState(0);
|
|
87
|
+
const live = useRef(true);
|
|
88
|
+
|
|
89
|
+
const refresh = useCallback(() => setNonce((n) => n + 1), []);
|
|
90
|
+
const openSettings = useCallback(() => openPrivacySettings('calendars'), []);
|
|
91
|
+
|
|
92
|
+
// Timestamps, not the `Date` objects: a caller writing
|
|
93
|
+
// `from={new Date(y, m, 1)}` in the render body hands us a new identity
|
|
94
|
+
// every paint, and an effect keyed on that would re-query every frame.
|
|
95
|
+
const fromMs = from.getTime();
|
|
96
|
+
const toMs = to.getTime();
|
|
97
|
+
const onlyKey = only ? only.join(',') : '';
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
live.current = true;
|
|
101
|
+
return () => {
|
|
102
|
+
live.current = false;
|
|
103
|
+
};
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
// The handle, opened once per connection: the `osascript` child and the
|
|
107
|
+
// bus reference behind it are shared, and one hook must not open a second
|
|
108
|
+
// of either every time the month changes.
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!enabled) {
|
|
111
|
+
setCal(null);
|
|
112
|
+
setBackend(null);
|
|
113
|
+
setStatus('idle');
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let alive = true;
|
|
118
|
+
let handle = null;
|
|
119
|
+
setStatus('loading');
|
|
120
|
+
desktopCalendar({ app }).then(
|
|
121
|
+
(opened) => {
|
|
122
|
+
if (!alive) {
|
|
123
|
+
void opened?.close();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
handle = opened;
|
|
127
|
+
setCal(opened);
|
|
128
|
+
setBackend(opened ? opened.backend : null);
|
|
129
|
+
if (!opened) {
|
|
130
|
+
setEvents(NO_EVENTS);
|
|
131
|
+
setStatus('unavailable');
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
(err) => {
|
|
135
|
+
if (!alive) return;
|
|
136
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
137
|
+
setStatus('unavailable');
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
alive = false;
|
|
143
|
+
setCal(null);
|
|
144
|
+
void handle?.close();
|
|
145
|
+
};
|
|
146
|
+
}, [app, enabled]);
|
|
147
|
+
|
|
148
|
+
// The query.
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!cal) return undefined;
|
|
151
|
+
|
|
152
|
+
let alive = true;
|
|
153
|
+
void (async () => {
|
|
154
|
+
setStatus('loading');
|
|
155
|
+
setError(null);
|
|
156
|
+
try {
|
|
157
|
+
const all = await cal.listCalendars();
|
|
158
|
+
if (!alive) return;
|
|
159
|
+
setFound(all);
|
|
160
|
+
|
|
161
|
+
const result = await cal.eventsBetween(
|
|
162
|
+
new Date(fromMs),
|
|
163
|
+
new Date(toMs),
|
|
164
|
+
{
|
|
165
|
+
calendars: pickCalendars(all, onlyKey),
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
if (!alive) return;
|
|
169
|
+
setEvents(result.events);
|
|
170
|
+
setErrors(result.errors);
|
|
171
|
+
setStatus('ready');
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (!alive) return;
|
|
174
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
175
|
+
setEvents(NO_EVENTS);
|
|
176
|
+
// The user's refusal and the machine's silence are different words:
|
|
177
|
+
// one has a Settings switch behind it and the other does not. A
|
|
178
|
+
// request that came back still undecided is the machine's — TCC
|
|
179
|
+
// would not even ask — so it is 'unavailable', not a refusal the
|
|
180
|
+
// user could take back.
|
|
181
|
+
const refused =
|
|
182
|
+
err instanceof CalendarAccessError && err.status !== 'prompt';
|
|
183
|
+
setStatus(refused ? 'denied' : 'unavailable');
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
|
|
187
|
+
return () => {
|
|
188
|
+
alive = false;
|
|
189
|
+
};
|
|
190
|
+
// `refresh` is stable; `nonce` is what a manual refresh moves.
|
|
191
|
+
}, [cal, fromMs, toMs, onlyKey, nonce]);
|
|
192
|
+
|
|
193
|
+
// The subscription is a **separate** effect, and deliberately not keyed on
|
|
194
|
+
// `nonce`: a change has to re-run the query, and only the query. Watching
|
|
195
|
+
// from inside the effect the change re-runs means every notification tears
|
|
196
|
+
// the views down and starts new ones — which is a loop as soon as anything
|
|
197
|
+
// is delivered while a view is starting, and was one for as long as EDS's
|
|
198
|
+
// `Start()` reported the range's existing contents as a change.
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (!cal || !watch) return undefined;
|
|
201
|
+
|
|
202
|
+
let alive = true;
|
|
203
|
+
let stopWatching = null;
|
|
204
|
+
void (async () => {
|
|
205
|
+
try {
|
|
206
|
+
const all = await cal.listCalendars();
|
|
207
|
+
if (!alive) return;
|
|
208
|
+
stopWatching = await cal.watch(
|
|
209
|
+
new Date(fromMs),
|
|
210
|
+
new Date(toMs),
|
|
211
|
+
() => {
|
|
212
|
+
// Re-query rather than patch: a recurrence master edited months
|
|
213
|
+
// away changes what this range looks like.
|
|
214
|
+
if (alive && live.current) refresh();
|
|
215
|
+
},
|
|
216
|
+
{ calendars: pickCalendars(all, onlyKey) },
|
|
217
|
+
);
|
|
218
|
+
if (!alive) await stopWatching();
|
|
219
|
+
} catch {
|
|
220
|
+
// No grant, no service, nothing to watch. The query effect above is
|
|
221
|
+
// what reports that to the caller; a second copy would only race.
|
|
222
|
+
}
|
|
223
|
+
})();
|
|
224
|
+
|
|
225
|
+
return () => {
|
|
226
|
+
alive = false;
|
|
227
|
+
void (async () => {
|
|
228
|
+
if (stopWatching) await stopWatching();
|
|
229
|
+
})();
|
|
230
|
+
};
|
|
231
|
+
}, [cal, watch, fromMs, toMs, onlyKey, refresh]);
|
|
232
|
+
|
|
233
|
+
const grouped = useMemo(() => groupByDay(events), [events]);
|
|
234
|
+
|
|
235
|
+
return useMemo(
|
|
236
|
+
() => ({
|
|
237
|
+
events,
|
|
238
|
+
byDay: grouped,
|
|
239
|
+
calendars: found,
|
|
240
|
+
errors,
|
|
241
|
+
status,
|
|
242
|
+
backend,
|
|
243
|
+
error,
|
|
244
|
+
refresh,
|
|
245
|
+
openSettings,
|
|
246
|
+
}),
|
|
247
|
+
[
|
|
248
|
+
events,
|
|
249
|
+
grouped,
|
|
250
|
+
found,
|
|
251
|
+
errors,
|
|
252
|
+
status,
|
|
253
|
+
backend,
|
|
254
|
+
error,
|
|
255
|
+
refresh,
|
|
256
|
+
openSettings,
|
|
257
|
+
],
|
|
258
|
+
);
|
|
259
|
+
}
|
package/src/glnodes.js
CHANGED
|
@@ -11,6 +11,7 @@ import { cssColorStraight } from 'ntk';
|
|
|
11
11
|
export { directGLFailure, hasDirectGL } from './glbackend.js';
|
|
12
12
|
|
|
13
13
|
import { Node } from './nodes.js';
|
|
14
|
+
import { FramePacer, resolveFrameRate } from './pacing.js';
|
|
14
15
|
|
|
15
16
|
// One visual query per (app, spec): GetFBConfigs is a round trip and every
|
|
16
17
|
// <glarea> in an app wants the same answer.
|
|
@@ -99,6 +100,9 @@ const px = (v) => Math.max(1, Math.round(v || 0));
|
|
|
99
100
|
* - `clearColor` — CSS colour or `[r, g, b, a]` floats (default black).
|
|
100
101
|
* - `frameLoop` — `'demand'` (default: redraw on prop/size/expose changes)
|
|
101
102
|
* or `'always'` (drive ntk's frame clock continuously).
|
|
103
|
+
* - `frameRate` — how the frames are paced when they are expensive
|
|
104
|
+
* (src/pacing.js): a preset, a cap, or the three numbers, the same
|
|
105
|
+
* vocabulary as `<window frameRate>`. Defaults to the owning window's.
|
|
102
106
|
* - `glx` — a `chooseGLXConfig` spec, e.g. `{ DEPTH_SIZE: 24 }`.
|
|
103
107
|
*
|
|
104
108
|
* The X child window is stacked above everything drawn in the parent, so 2D
|
|
@@ -114,12 +118,36 @@ export class GlAreaNode extends Node {
|
|
|
114
118
|
this._frameScheduled = false;
|
|
115
119
|
this._created = false;
|
|
116
120
|
this._pointerDirty = true;
|
|
121
|
+
// The frame pacer (src/pacing.js), the surface's own: a scene's frames
|
|
122
|
+
// are drawn on a clock of their own, and what one costs — `onDraw` and
|
|
123
|
+
// the swap, on this thread — is what decides whether the next may
|
|
124
|
+
// start at once. The policy is this element's `frameRate`, else the
|
|
125
|
+
// owning window's, read at each request so a change on either follows.
|
|
126
|
+
this._pacer = new FramePacer();
|
|
127
|
+
this._ownPolicy = null;
|
|
128
|
+
this._syncFramePolicy();
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
get isGlArea() {
|
|
120
132
|
return true;
|
|
121
133
|
}
|
|
122
134
|
|
|
135
|
+
/** The policy this surface paces by: its own prop, else the window's. */
|
|
136
|
+
_framePolicy() {
|
|
137
|
+
if (this.props.frameRate !== undefined && this.props.frameRate !== null) {
|
|
138
|
+
return this._ownPolicy;
|
|
139
|
+
}
|
|
140
|
+
return this.root?._framePolicy ?? this._pacer.policy;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_syncFramePolicy() {
|
|
144
|
+
const value = this.props.frameRate;
|
|
145
|
+
this._ownPolicy =
|
|
146
|
+
value === undefined || value === null
|
|
147
|
+
? null
|
|
148
|
+
: resolveFrameRate(value, '<glarea frameRate>');
|
|
149
|
+
}
|
|
150
|
+
|
|
123
151
|
_setRoot(root) {
|
|
124
152
|
super._setRoot(root);
|
|
125
153
|
// the owning window may already exist (a <glarea> mounted into a live
|
|
@@ -275,8 +303,21 @@ export class GlAreaNode extends Node {
|
|
|
275
303
|
this.requestFrame();
|
|
276
304
|
}
|
|
277
305
|
|
|
278
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* Draw one frame on the child window's next frame tick — after whatever
|
|
308
|
+
* wait the pacer asks for (src/pacing.js). Off by default it answers
|
|
309
|
+
* "now"; under an adaptive policy a scene whose frames cost more than
|
|
310
|
+
* their share of the thread is held between them, and a `frameLoop` of
|
|
311
|
+
* `'always'` becomes a loop at the budget rather than at the display.
|
|
312
|
+
*/
|
|
279
313
|
requestFrame() {
|
|
314
|
+
if (!this.window || this.destroyed || this._frameScheduled) return;
|
|
315
|
+
this._pacer.configure(this._framePolicy());
|
|
316
|
+
if (this._pacer.defer(() => this._requestFrameNow())) return;
|
|
317
|
+
this._requestFrameNow();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_requestFrameNow() {
|
|
280
321
|
if (!this.window || this.destroyed || this._frameScheduled) return;
|
|
281
322
|
this._frameScheduled = true;
|
|
282
323
|
const schedule =
|
|
@@ -290,13 +331,28 @@ export class GlAreaNode extends Node {
|
|
|
290
331
|
}
|
|
291
332
|
|
|
292
333
|
_drawFrame() {
|
|
334
|
+
const pacer = this._pacer;
|
|
335
|
+
pacer.began();
|
|
336
|
+
let drawn = false;
|
|
337
|
+
try {
|
|
338
|
+
drawn = this._drawFrameNow();
|
|
339
|
+
} finally {
|
|
340
|
+
pacer.ended(undefined, drawn);
|
|
341
|
+
}
|
|
342
|
+
// after the frame is priced, so the loop's next frame is judged by
|
|
343
|
+
// this one rather than by the one before
|
|
344
|
+
if (drawn && this.props.frameLoop === 'always') this.requestFrame();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** The frame itself; true when it drew. */
|
|
348
|
+
_drawFrameNow() {
|
|
293
349
|
const gl = this.gl;
|
|
294
|
-
if (!gl || this.destroyed) return;
|
|
350
|
+
if (!gl || this.destroyed) return false;
|
|
295
351
|
const direct = gl.backend === 'direct';
|
|
296
352
|
// On the direct backend every buffer may still be held by the display,
|
|
297
353
|
// and drawing into one before it comes back would paint what is on
|
|
298
354
|
// screen. `onFrameAvailable` asks for this frame again when one frees.
|
|
299
|
-
if (direct && gl.canRender && !gl.canRender()) return;
|
|
355
|
+
if (direct && gl.canRender && !gl.canRender()) return false;
|
|
300
356
|
// binds this surface — the GPU context is shared between every <glarea>
|
|
301
357
|
// on the connection — and picks up a resize
|
|
302
358
|
gl.makeCurrent?.();
|
|
@@ -325,7 +381,7 @@ export class GlAreaNode extends Node {
|
|
|
325
381
|
}
|
|
326
382
|
this.props.onDraw?.(gl, info);
|
|
327
383
|
gl.SwapBuffers();
|
|
328
|
-
|
|
384
|
+
return true;
|
|
329
385
|
}
|
|
330
386
|
|
|
331
387
|
/**
|
|
@@ -357,7 +413,9 @@ export class GlAreaNode extends Node {
|
|
|
357
413
|
}
|
|
358
414
|
|
|
359
415
|
applyProps(newProps, oldProps) {
|
|
416
|
+
const before = oldProps ?? this.props;
|
|
360
417
|
super.applyProps(newProps, oldProps);
|
|
418
|
+
if (newProps.frameRate !== before.frameRate) this._syncFramePolicy();
|
|
361
419
|
// onDraw/clearColor are read at frame time, so any update is a new frame
|
|
362
420
|
this.requestFrame();
|
|
363
421
|
}
|
|
@@ -375,6 +433,7 @@ export class GlAreaNode extends Node {
|
|
|
375
433
|
destroySubtree() {
|
|
376
434
|
if (this.destroyed) return;
|
|
377
435
|
super.destroySubtree();
|
|
436
|
+
this._pacer.cancel();
|
|
378
437
|
this.gl?.destroy?.();
|
|
379
438
|
this.gl = null;
|
|
380
439
|
this.window?.destroy?.();
|
package/src/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { ReactNode, RefObject } from 'react';
|
|
11
11
|
import type { DrawnNode, NtkApp, NtkWindow } from './types/nodes.js';
|
|
12
|
-
import type { ReactX11Elements } from './types/elements.js';
|
|
12
|
+
import type { FrameRate, ReactX11Elements } from './types/elements.js';
|
|
13
13
|
|
|
14
14
|
export * from './types/style.js';
|
|
15
15
|
export * from './types/events.js';
|
|
@@ -29,6 +29,7 @@ export * from './types/launcher.js';
|
|
|
29
29
|
export * from './types/tray.js';
|
|
30
30
|
export * from './types/permissions.js';
|
|
31
31
|
export * from './types/notifications.js';
|
|
32
|
+
export * from './types/desktopcalendar.js';
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* The XID of the X11 window a ref points at, or `null` if there is not one
|
|
@@ -232,6 +233,15 @@ export interface RootOptions {
|
|
|
232
233
|
* run each. 6 by default; 0 paints glyphs at every size.
|
|
233
234
|
*/
|
|
234
235
|
textStripBelow?: number;
|
|
236
|
+
/**
|
|
237
|
+
* How this root's windows pace their frames when their content changes
|
|
238
|
+
* faster than the display refreshes — the default for every `<window>`
|
|
239
|
+
* and `<glarea>` that names no `frameRate` of its own. `'display'` (every
|
|
240
|
+
* frame the clock gives) unless said otherwise; `'adaptive'` holds paints
|
|
241
|
+
* to a quarter of the time under a flood. `REACT_X11_FRAME_RATE` overrides
|
|
242
|
+
* it, and the props, from the environment. See {@link FrameRate}.
|
|
243
|
+
*/
|
|
244
|
+
frameRate?: FrameRate;
|
|
235
245
|
/** `':1'`, `'host:0.0'`, or a unix socket path. Defaults to `$DISPLAY`. */
|
|
236
246
|
display?: string;
|
|
237
247
|
/**
|
package/src/index.js
CHANGED
|
@@ -27,6 +27,15 @@ export {
|
|
|
27
27
|
notify,
|
|
28
28
|
} from './notifications.js';
|
|
29
29
|
export { useNotifier } from './notificationhooks.js';
|
|
30
|
+
export {
|
|
31
|
+
CalendarAccessError,
|
|
32
|
+
NoCalendarServiceError,
|
|
33
|
+
byDay,
|
|
34
|
+
calendarBackend,
|
|
35
|
+
dayKey,
|
|
36
|
+
desktopCalendar,
|
|
37
|
+
} from './desktopcalendar.js';
|
|
38
|
+
export { useDesktopCalendarEvents } from './desktopcalendarhooks.js';
|
|
30
39
|
export { parseUriList } from './transfer.js';
|
|
31
40
|
export { useApp, useClipboard, useSupports } from './appcontext.js';
|
|
32
41
|
export { BusUnavailableError, closeBus, sessionBus, systemBus } from './bus.js';
|
package/src/node.d.ts
CHANGED
|
@@ -25,8 +25,178 @@ import type {
|
|
|
25
25
|
WheelEvent,
|
|
26
26
|
} from './types/events.js';
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
export
|
|
28
|
+
/** A gradient, as `createLinearGradient` answers with. */
|
|
29
|
+
export interface CanvasGradientLike {
|
|
30
|
+
addColorStop(offset: number, color: string): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** An RGBA pixel block, as `createImageData` answers with. */
|
|
34
|
+
export interface ImageDataLike {
|
|
35
|
+
readonly data: Uint8ClampedArray;
|
|
36
|
+
readonly width: number;
|
|
37
|
+
readonly height: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The 2d context a node paints into — the canvas-shaped subset **both**
|
|
42
|
+
* backends implement: ntk's `RenderingContext2D` over XRender on X11, and
|
|
43
|
+
* `CocoaContext2D` over CoreGraphics on macOS. Declared as the contract an
|
|
44
|
+
* element may rely on rather than as either class: what is here is on
|
|
45
|
+
* both, and a member one backend has and the other does not is optional
|
|
46
|
+
* here or absent. Coordinates are device pixels in the owning window's
|
|
47
|
+
* space — `abs`, `contentBox()` — unless a transform says otherwise.
|
|
48
|
+
* Anything ntk documents beyond this is reachable at runtime and is
|
|
49
|
+
* ntk's to change.
|
|
50
|
+
*/
|
|
51
|
+
export interface Context2D {
|
|
52
|
+
fillStyle: string | CanvasGradientLike;
|
|
53
|
+
strokeStyle: string | CanvasGradientLike;
|
|
54
|
+
lineWidth: number;
|
|
55
|
+
lineCap: 'butt' | 'round' | 'square';
|
|
56
|
+
lineJoin: 'miter' | 'round' | 'bevel';
|
|
57
|
+
globalAlpha: number;
|
|
58
|
+
/** A CSS font shorthand, `'13px sans-serif'`. */
|
|
59
|
+
font: string;
|
|
60
|
+
shadowBlur: number;
|
|
61
|
+
shadowOffsetX: number;
|
|
62
|
+
shadowOffsetY: number;
|
|
63
|
+
shadowColor: string;
|
|
64
|
+
/**
|
|
65
|
+
* X11 only: XRender's compositing operator — `'source-over'` (the
|
|
66
|
+
* default), `'copy'`, and the rest ntk documents. Absent on the cocoa
|
|
67
|
+
* backend, whose bridge has no blend-mode verb yet; test for it with
|
|
68
|
+
* `'globalCompositeOperation' in ctx`.
|
|
69
|
+
*/
|
|
70
|
+
globalCompositeOperation?: string;
|
|
71
|
+
save(): void;
|
|
72
|
+
restore(): void;
|
|
73
|
+
translate(x: number, y: number): void;
|
|
74
|
+
scale(x: number, y: number): void;
|
|
75
|
+
rotate(angle: number): void;
|
|
76
|
+
transform(
|
|
77
|
+
a: number,
|
|
78
|
+
b: number,
|
|
79
|
+
c: number,
|
|
80
|
+
d: number,
|
|
81
|
+
e: number,
|
|
82
|
+
f: number,
|
|
83
|
+
): void;
|
|
84
|
+
setTransform(
|
|
85
|
+
a: number,
|
|
86
|
+
b: number,
|
|
87
|
+
c: number,
|
|
88
|
+
d: number,
|
|
89
|
+
e: number,
|
|
90
|
+
f: number,
|
|
91
|
+
): void;
|
|
92
|
+
resetTransform(): void;
|
|
93
|
+
getTransform(): {
|
|
94
|
+
a: number;
|
|
95
|
+
b: number;
|
|
96
|
+
c: number;
|
|
97
|
+
d: number;
|
|
98
|
+
e: number;
|
|
99
|
+
f: number;
|
|
100
|
+
};
|
|
101
|
+
setLineDash(segments: number[]): void;
|
|
102
|
+
getLineDash(): number[];
|
|
103
|
+
beginPath(): void;
|
|
104
|
+
closePath(): void;
|
|
105
|
+
moveTo(x: number, y: number): void;
|
|
106
|
+
lineTo(x: number, y: number): void;
|
|
107
|
+
bezierCurveTo(
|
|
108
|
+
cp1x: number,
|
|
109
|
+
cp1y: number,
|
|
110
|
+
cp2x: number,
|
|
111
|
+
cp2y: number,
|
|
112
|
+
x: number,
|
|
113
|
+
y: number,
|
|
114
|
+
): void;
|
|
115
|
+
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void;
|
|
116
|
+
arc(
|
|
117
|
+
x: number,
|
|
118
|
+
y: number,
|
|
119
|
+
radius: number,
|
|
120
|
+
startAngle: number,
|
|
121
|
+
endAngle: number,
|
|
122
|
+
anticlockwise?: boolean,
|
|
123
|
+
): void;
|
|
124
|
+
rect(x: number, y: number, width: number, height: number): void;
|
|
125
|
+
roundRect(
|
|
126
|
+
x: number,
|
|
127
|
+
y: number,
|
|
128
|
+
width: number,
|
|
129
|
+
height: number,
|
|
130
|
+
radii: number | number[],
|
|
131
|
+
): void;
|
|
132
|
+
fill(fillRule?: 'nonzero' | 'evenodd'): void;
|
|
133
|
+
stroke(): void;
|
|
134
|
+
clip(fillRule?: 'nonzero' | 'evenodd'): void;
|
|
135
|
+
fillRect(x: number, y: number, width: number, height: number): void;
|
|
136
|
+
/** Many rectangles in one operation — one request on X11, one
|
|
137
|
+
* CoreGraphics call on macOS. `[x, y, w, h]` quadruples, flat or nested. */
|
|
138
|
+
fillRects(rects: number[] | number[][]): void;
|
|
139
|
+
strokeRect(x: number, y: number, width: number, height: number): void;
|
|
140
|
+
clearRect(x: number, y: number, width: number, height: number): void;
|
|
141
|
+
fillText(text: string, x: number, y: number): void;
|
|
142
|
+
measureText(text: string): { width: number; [key: string]: unknown };
|
|
143
|
+
/**
|
|
144
|
+
* Composite an offscreen `Surface` (`react-x11/ntk`) — whole at a point,
|
|
145
|
+
* whole into a rect, or a source rect of it into a destination rect: the
|
|
146
|
+
* canvas overloads. One composite either way.
|
|
147
|
+
*/
|
|
148
|
+
drawImage(image: unknown, dx: number, dy: number): void;
|
|
149
|
+
drawImage(
|
|
150
|
+
image: unknown,
|
|
151
|
+
dx: number,
|
|
152
|
+
dy: number,
|
|
153
|
+
dw: number,
|
|
154
|
+
dh: number,
|
|
155
|
+
): void;
|
|
156
|
+
drawImage(
|
|
157
|
+
image: unknown,
|
|
158
|
+
sx: number,
|
|
159
|
+
sy: number,
|
|
160
|
+
sw: number,
|
|
161
|
+
sh: number,
|
|
162
|
+
dx: number,
|
|
163
|
+
dy: number,
|
|
164
|
+
dw: number,
|
|
165
|
+
dh: number,
|
|
166
|
+
): void;
|
|
167
|
+
createLinearGradient(
|
|
168
|
+
x0: number,
|
|
169
|
+
y0: number,
|
|
170
|
+
x1: number,
|
|
171
|
+
y1: number,
|
|
172
|
+
): CanvasGradientLike;
|
|
173
|
+
createImageData(width: number, height: number): ImageDataLike;
|
|
174
|
+
/** Raw pixels, straight RGBA, written transform- and clip-free — the
|
|
175
|
+
* canvas contract (docs/elements.md "<canvas>"). */
|
|
176
|
+
putImageData(data: ImageDataLike, x: number, y: number): void;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* What `paintCachePlan` answers with: draw this node's content once into a
|
|
181
|
+
* surface under `key`, and composite that until the key changes
|
|
182
|
+
* (docs/extending.md "Drawing once instead of every frame").
|
|
183
|
+
*/
|
|
184
|
+
export interface PaintCachePlan {
|
|
185
|
+
/** Identity: the same key must mean the same pixels. Name every input
|
|
186
|
+
* `paintCached` reads. */
|
|
187
|
+
key: string;
|
|
188
|
+
/** Where the surface goes, in device pixels — window coordinates. */
|
|
189
|
+
x: number;
|
|
190
|
+
y: number;
|
|
191
|
+
/** Its size, in device pixels. */
|
|
192
|
+
width: number;
|
|
193
|
+
height: number;
|
|
194
|
+
/** `'argb32'` (the default), or `'a8'` for coverage tinted at composite
|
|
195
|
+
* time — one rendered copy per key serves every ink. */
|
|
196
|
+
format?: 'argb32' | 'a8';
|
|
197
|
+
/** The colour an `'a8'` surface is painted through. */
|
|
198
|
+
tint?: string;
|
|
199
|
+
}
|
|
30
200
|
|
|
31
201
|
/** How an axis is bounded when layout asks an element for its size.
|
|
32
202
|
* `'exactly'` — the style decided this axis; `'at-most'` — that many pixels
|
|
@@ -284,6 +454,48 @@ export declare class Node {
|
|
|
284
454
|
/** Draw. A subclass calls `super.paint(ctx)` first, for the background,
|
|
285
455
|
* border and clip, then draws inside `this.abs`. */
|
|
286
456
|
paint(ctx: Context2D): void;
|
|
457
|
+
/**
|
|
458
|
+
* Draw between the background and the children — where every built-in
|
|
459
|
+
* draws, and the seam to override rather than `paint`: `paint` draws the
|
|
460
|
+
* background, then this, then the children, the border and the focus
|
|
461
|
+
* ring, and a scroller's bars, and an element that overrides `paint`
|
|
462
|
+
* has to keep all of that in the right order itself. Draws inside
|
|
463
|
+
* `contentBox()`; the clip is already set. The default draws nothing.
|
|
464
|
+
*/
|
|
465
|
+
paintContent(ctx: Context2D): void;
|
|
466
|
+
/**
|
|
467
|
+
* The paint-cache protocol, with `paintCached`: implement both or
|
|
468
|
+
* neither. Answer a plan to have this frame's content drawn once into a
|
|
469
|
+
* surface under `plan.key` and composited from it until the key changes,
|
|
470
|
+
* or `null` to opt out this frame — the right answer whenever the paint
|
|
471
|
+
* depends on something the key cannot see (a caret, a hover, anything
|
|
472
|
+
* animating). The key is the whole correctness surface.
|
|
473
|
+
*/
|
|
474
|
+
paintCachePlan?(ctx: Context2D): PaintCachePlan | null;
|
|
475
|
+
/**
|
|
476
|
+
* Draw the content the plan named, at the **origin of `box`** — surface
|
|
477
|
+
* coordinates, not `this.abs`. `ink` is the colour a mono drawing (one
|
|
478
|
+
* that asked for `'a8'`) must paint in: white where the surface is
|
|
479
|
+
* coverage and the tint arrives at composite time, the tint itself on a
|
|
480
|
+
* backend without coverage surfaces. A multi-colour drawing ignores it.
|
|
481
|
+
* `paintDamage()` is null in here: a cached copy is drawn whole.
|
|
482
|
+
*/
|
|
483
|
+
paintCached?(ctx: Context2D, box: Rect, ink?: string): void;
|
|
484
|
+
/**
|
|
485
|
+
* The rect this element writes opaque pixels over on every paint — in
|
|
486
|
+
* window coordinates like `abs`, whole pixels — or `null`, the default,
|
|
487
|
+
* which promises nothing.
|
|
488
|
+
*
|
|
489
|
+
* A pass that lies inside it is painted without the fills that would be
|
|
490
|
+
* under it: the window's background, this node's own and every
|
|
491
|
+
* ancestor's. That is what an element with a retained surface it draws
|
|
492
|
+
* whole — a terminal, a media frame, a chart — answers with, and it then
|
|
493
|
+
* claims its damage as a **rect inside the answer** rather than as the
|
|
494
|
+
* node, which is inflated by a pixel of slop and so never covered. The
|
|
495
|
+
* promise is the element's to keep: every pixel, alpha one, on every
|
|
496
|
+
* paint, whatever the props. A translucent element answers `null`.
|
|
497
|
+
*/
|
|
498
|
+
opaqueRect(): Rect | null;
|
|
287
499
|
/**
|
|
288
500
|
* The rect this paint pass is repainting, or null when it is repainting
|
|
289
501
|
* the whole window — and null outside a paint, which means the same
|