react-x11 2.24.0 → 2.25.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 +5 -5
- package/src/backend/context2d.js +368 -20
- package/src/backend/shadowtiles.js +84 -0
- package/src/compositing.js +21 -1
- package/src/events.js +42 -0
- package/src/node.d.ts +20 -1
- package/src/nodes/boxpaint.js +32 -0
- package/src/nodes/invalidate.js +23 -0
- package/src/nodes/node.js +6 -1
- package/src/nodes/paint.js +38 -13
- package/src/nodes/scrollable.js +16 -6
- package/src/nodes/scrollblit.js +38 -1
- package/src/nodes/text.js +80 -3
- package/src/nodes/window/floors.js +1 -1
- package/src/nodes/window/flush.js +13 -1
- package/src/nodes/window/size.js +56 -32
- package/src/nodes/window/spine.js +143 -10
- package/src/nodes/window/window.js +6 -0
- package/src/xsettings.js +29 -3
package/src/events.js
CHANGED
|
@@ -431,6 +431,10 @@ export class EventManager {
|
|
|
431
431
|
this._wheelOwed = { x: 0, y: 0 };
|
|
432
432
|
// the window's DragSession while a press has armed it (src/dnd.js)
|
|
433
433
|
this._dragArmed = null;
|
|
434
|
+
// where the pointer last was over this window — the motion or the wheel
|
|
435
|
+
// that said so — for asking again what is under it once what is under
|
|
436
|
+
// it has moved (`refreshHover`). Null while the pointer is elsewhere.
|
|
437
|
+
this._pointer = null;
|
|
434
438
|
// on a *top-level* window's manager: the chords bound anywhere in its
|
|
435
439
|
// tree, newest last (`registerAccelerator`). Null until one is bound,
|
|
436
440
|
// which is the common case and costs one null check per key.
|
|
@@ -792,6 +796,7 @@ export class EventManager {
|
|
|
792
796
|
* render it started for the notch before.
|
|
793
797
|
*/
|
|
794
798
|
_onWheel(native) {
|
|
799
|
+
this._pointer = native;
|
|
795
800
|
// The first wheel is what says this window wants smooth scrolling. It was
|
|
796
801
|
// created on core events — an XI2 selection costs four times as many
|
|
797
802
|
// bytes per *pointer move*, which a window that is never scrolled would
|
|
@@ -1004,6 +1009,7 @@ export class EventManager {
|
|
|
1004
1009
|
}
|
|
1005
1010
|
|
|
1006
1011
|
_onMouseMove(native) {
|
|
1012
|
+
this._pointer = native;
|
|
1007
1013
|
if (inspectHandler) {
|
|
1008
1014
|
inspectHandler('move', this._hit(native), native);
|
|
1009
1015
|
return;
|
|
@@ -1090,6 +1096,7 @@ export class EventManager {
|
|
|
1090
1096
|
// ancestor was told the pointer had gone and then that it was back, and
|
|
1091
1097
|
// `onMouseOut` that it had left a window it was still in.
|
|
1092
1098
|
if (this._leftIntoSurface(native)) return;
|
|
1099
|
+
this._pointer = null;
|
|
1093
1100
|
runWithPriority(ContinuousEventPriority, () => {
|
|
1094
1101
|
this._updateHover([], native);
|
|
1095
1102
|
// the pointer is somewhere else entirely: whatever it was heading for
|
|
@@ -1186,6 +1193,41 @@ export class EventManager {
|
|
|
1186
1193
|
this.pressPath = path;
|
|
1187
1194
|
}
|
|
1188
1195
|
|
|
1196
|
+
/**
|
|
1197
|
+
* Hover follows content. The pointer stayed where it was and what is
|
|
1198
|
+
* under it moved — a scroll, or a layout that put something else there —
|
|
1199
|
+
* so the question a motion asks is asked again at the point, as a browser
|
|
1200
|
+
* asks it at the frame after a scroll: `:hover`, enter and leave change
|
|
1201
|
+
* as a motion would change them, and the node under the pointer hears
|
|
1202
|
+
* `defaultMouseMove` there, since an element that paints its own hover
|
|
1203
|
+
* (a document, a graph) has content that moved under the point even where
|
|
1204
|
+
* the node did not. The application's `onMouseMove` is not called: the
|
|
1205
|
+
* pointer did not move. Nothing is asked while a press or a capture holds
|
|
1206
|
+
* the pointer, where hover is frozen for the gesture.
|
|
1207
|
+
*
|
|
1208
|
+
* Called by the window after a frame that laid out (nodes/window/flush.js),
|
|
1209
|
+
* which is every frame a scroll moves content in.
|
|
1210
|
+
*/
|
|
1211
|
+
refreshHover() {
|
|
1212
|
+
const at = this._pointer;
|
|
1213
|
+
if (!at || inspectHandler || this.node.destroyed) return;
|
|
1214
|
+
if (this._captured() || this.downNode || this._dragArmed) return;
|
|
1215
|
+
runWithPriority(ContinuousEventPriority, () => {
|
|
1216
|
+
const target = this._hit(at);
|
|
1217
|
+
const path = this._path(target);
|
|
1218
|
+
const old = this.hoverPath;
|
|
1219
|
+
if (
|
|
1220
|
+
path.length !== old.length ||
|
|
1221
|
+
path.some((node, i) => node !== old[i])
|
|
1222
|
+
) {
|
|
1223
|
+
this._updateHover(path, at);
|
|
1224
|
+
}
|
|
1225
|
+
if (typeof target.defaultMouseMove === 'function') {
|
|
1226
|
+
target.defaultMouseMove(this._makeEvent('mouseMove', at, target));
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1189
1231
|
/** enter/leave do not propagate: each node on the diff gets its own call. */
|
|
1190
1232
|
_updateHover(newPath, native) {
|
|
1191
1233
|
const oldPath = this.hoverPath;
|
package/src/node.d.ts
CHANGED
|
@@ -48,6 +48,10 @@ export interface ImageDataLike {
|
|
|
48
48
|
* Anything ntk documents beyond this is reachable at runtime and is
|
|
49
49
|
* ntk's to change.
|
|
50
50
|
*/
|
|
51
|
+
/** One corner's radius for `roundRect`: circular, or elliptical as a
|
|
52
|
+
* `DOMPointInit`. */
|
|
53
|
+
export type RoundRectRadius = number | { x?: number; y?: number };
|
|
54
|
+
|
|
51
55
|
export interface Context2D {
|
|
52
56
|
fillStyle: string | CanvasGradientLike;
|
|
53
57
|
strokeStyle: string | CanvasGradientLike;
|
|
@@ -125,12 +129,16 @@ export interface Context2D {
|
|
|
125
129
|
anticlockwise?: boolean,
|
|
126
130
|
): void;
|
|
127
131
|
rect(x: number, y: number, width: number, height: number): void;
|
|
132
|
+
/** Canvas's `roundRect`: a radius for every corner, a `{ x, y }` point
|
|
133
|
+
* for an elliptical one, or a list of one to four of either, clockwise
|
|
134
|
+
* from the top left. A shadowed `fill` of one — or of a `rect` less one,
|
|
135
|
+
* filled `'evenodd'` — is drawn from a shadow tile made once. */
|
|
128
136
|
roundRect(
|
|
129
137
|
x: number,
|
|
130
138
|
y: number,
|
|
131
139
|
width: number,
|
|
132
140
|
height: number,
|
|
133
|
-
radii:
|
|
141
|
+
radii: RoundRectRadius | RoundRectRadius[],
|
|
134
142
|
): void;
|
|
135
143
|
fill(fillRule?: 'nonzero' | 'evenodd'): void;
|
|
136
144
|
stroke(): void;
|
|
@@ -515,6 +523,17 @@ export declare class Node {
|
|
|
515
523
|
* paint, whatever the props. A translucent element answers `null`.
|
|
516
524
|
*/
|
|
517
525
|
opaqueRect(): Rect | null;
|
|
526
|
+
/**
|
|
527
|
+
* What this element draws that stays put in the viewport of the scroll
|
|
528
|
+
* pane it is in while the pane scrolls — a document's fixed background, a
|
|
529
|
+
* `position: fixed` header — as rects in window coordinates, device
|
|
530
|
+
* pixels, or null, the default, for drawing that moves with the content.
|
|
531
|
+
* A scroll of that pane that blits repaints them where they are and where
|
|
532
|
+
* the copy dragged their image; one that covers most of the viewport
|
|
533
|
+
* makes the scroll a repaint. Only the nearest scroll pane asks. Read on
|
|
534
|
+
* each scroll the pane blits, after layout.
|
|
535
|
+
*/
|
|
536
|
+
viewportFixedRects(): Rect[] | null;
|
|
518
537
|
/**
|
|
519
538
|
* How far this element's own drawing reaches past its box, in device
|
|
520
539
|
* pixels on every side — 0, the default, for an element that stays inside
|
package/src/nodes/boxpaint.js
CHANGED
|
@@ -267,10 +267,42 @@ export class NodeBoxPaint {
|
|
|
267
267
|
ctx.fillShadow(rect, r, shadow.blur, color);
|
|
268
268
|
continue;
|
|
269
269
|
}
|
|
270
|
+
// No X server to bake a blur on — CoreGraphics, Direct2D: the
|
|
271
|
+
// context's own shadow, which draws a rounded rect's from a tile it
|
|
272
|
+
// keeps (src/backend/shadowtiles.js). The coverage route below needs
|
|
273
|
+
// RENDER and drew nothing there.
|
|
274
|
+
if (!this.app?.display?.Render) {
|
|
275
|
+
this._paintShadowOnContext(ctx, rect, r, shadow.blur, color);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
270
278
|
this._paintBlurredShadow(ctx, rect, r, shadow.blur, color);
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
|
|
282
|
+
/**
|
|
283
|
+
* One blurred shadow through the context's `shadowBlur`: the rounded rect
|
|
284
|
+
* filled clear of the surface to its left, and its shadow thrown back by
|
|
285
|
+
* as much, so that only the shadow lands. Offsets are device pixels and
|
|
286
|
+
* the shape moves in the context's own units, hence the scale between.
|
|
287
|
+
*/
|
|
288
|
+
_paintShadowOnContext(ctx, rect, radius, blur, color) {
|
|
289
|
+
if (!('shadowBlur' in ctx) || typeof ctx.roundRect !== 'function') return;
|
|
290
|
+
const m = ctx.getTransform?.() ?? { a: 1, e: 0 };
|
|
291
|
+
const reach = Math.ceil(blur * 1.5) + 1;
|
|
292
|
+
const away =
|
|
293
|
+
Math.ceil(Math.abs(rect.x) + rect.width + Math.abs(m.e) + reach) + 1;
|
|
294
|
+
ctx.save();
|
|
295
|
+
ctx.shadowColor = color;
|
|
296
|
+
ctx.shadowBlur = blur;
|
|
297
|
+
ctx.shadowOffsetX = away * m.a;
|
|
298
|
+
ctx.shadowOffsetY = 0;
|
|
299
|
+
ctx.fillStyle = '#000000';
|
|
300
|
+
ctx.beginPath();
|
|
301
|
+
ctx.roundRect(rect.x - away, rect.y, rect.width, rect.height, radius);
|
|
302
|
+
ctx.fill();
|
|
303
|
+
ctx.restore();
|
|
304
|
+
}
|
|
305
|
+
|
|
274
306
|
/**
|
|
275
307
|
* One blurred shadow, through the paint cache when there is one.
|
|
276
308
|
*
|
package/src/nodes/invalidate.js
CHANGED
|
@@ -114,6 +114,29 @@ export class NodeInvalidate {
|
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* What this element draws that stays put in the viewport of the scroll
|
|
119
|
+
* pane it is in while the pane scrolls, as rects in window coordinates,
|
|
120
|
+
* device pixels, like `abs` — or null, the default, for an element
|
|
121
|
+
* whose drawing moves with the content. A document's fixed background or
|
|
122
|
+
* `position: fixed` header is drawn where the pane's viewport is, not
|
|
123
|
+
* where the content under it is, and a scroll's blit copies the pane's
|
|
124
|
+
* pixels with the content: it dragged the header along, and left the
|
|
125
|
+
* background scrolling with the text.
|
|
126
|
+
*
|
|
127
|
+
* A pane scroll that blits repaints what these rects answer where they
|
|
128
|
+
* are and where the copy dragged their image, as it does the corners of
|
|
129
|
+
* a rounded box around the pane; one that covers most of the viewport
|
|
130
|
+
* makes the scroll a repaint. Only the nearest scroll pane's scroll is
|
|
131
|
+
* asked about: a pane further out moves the inner one, and what is fixed
|
|
132
|
+
* to it, with its content. Read on each scroll the pane blits, after
|
|
133
|
+
* layout, so it may follow `abs` and the pane's `contentBox()`; the
|
|
134
|
+
* element draws those rects in its `paint` from the same geometry.
|
|
135
|
+
*/
|
|
136
|
+
viewportFixedRects() {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
117
140
|
/**
|
|
118
141
|
* How far this element's own drawing reaches past its box — device
|
|
119
142
|
* pixels, on every side — or 0, the default, for one that stays inside
|
package/src/nodes/node.js
CHANGED
|
@@ -607,6 +607,7 @@ export class Node {
|
|
|
607
607
|
// nothing else ever asks for a frame
|
|
608
608
|
this.root?._animating.delete(this);
|
|
609
609
|
this.root?._opaqueNodes?.delete(this);
|
|
610
|
+
this.root?._viewportFixedNodes?.delete(this);
|
|
610
611
|
// a surface that goes away takes its selection with it, and the app-wide
|
|
611
612
|
// claim on being the one showing one goes with it too
|
|
612
613
|
this._textSelection?.destroy();
|
|
@@ -625,11 +626,15 @@ export class Node {
|
|
|
625
626
|
this.root?._layoutHosts?.delete(this);
|
|
626
627
|
root?._layoutHosts?.add(this);
|
|
627
628
|
}
|
|
628
|
-
// an element answering `opaqueRect()` is one the window asks per pass
|
|
629
|
+
// an element answering `opaqueRect()` is one the window asks per pass,
|
|
630
|
+
// and one answering `viewportFixedRects()` one it asks per scroll blit
|
|
629
631
|
const opaque = this.opaqueRect !== Node.prototype.opaqueRect;
|
|
632
|
+
const fixed = this.viewportFixedRects !== Node.prototype.viewportFixedRects;
|
|
630
633
|
if (opaque) this.root?._opaqueNodes?.delete(this);
|
|
634
|
+
if (fixed) this.root?._viewportFixedNodes?.delete(this);
|
|
631
635
|
this.root = root;
|
|
632
636
|
if (opaque) root?._opaqueNodes?.add(this);
|
|
637
|
+
if (fixed) root?._viewportFixedNodes?.add(this);
|
|
633
638
|
// styled before it had a window, so this is where a placed node is
|
|
634
639
|
// first registered (WindowNode._placeNodes)
|
|
635
640
|
if (this.style && isPlaced(this.style)) root?._placedNodes?.add(this);
|
package/src/nodes/paint.js
CHANGED
|
@@ -135,9 +135,9 @@ export class NodePaint {
|
|
|
135
135
|
* wherever nothing inside it overlaps.
|
|
136
136
|
*/
|
|
137
137
|
_paintGroup(ctx, opacity) {
|
|
138
|
-
|
|
138
|
+
let rect = this._groupRect();
|
|
139
139
|
if (!rect) return;
|
|
140
|
-
|
|
140
|
+
let surface = this._groupSurfaceFor(rect.width, rect.height);
|
|
141
141
|
const alpha = ctx.globalAlpha ?? 1;
|
|
142
142
|
if (!surface) {
|
|
143
143
|
ctx.globalAlpha = alpha * opacity;
|
|
@@ -148,17 +148,32 @@ export class NodePaint {
|
|
|
148
148
|
}
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
151
|
+
const draw = () =>
|
|
152
|
+
surface.render((sctx) => {
|
|
153
|
+
sctx.clearRect(0, 0, rect.width, rect.height);
|
|
154
|
+
sctx.translate(-rect.x, -rect.y);
|
|
155
|
+
const damage = this.root?._paintDamage;
|
|
156
|
+
if (damage) {
|
|
157
|
+
sctx.beginPath();
|
|
158
|
+
sctx.rect(damage.x, damage.y, damage.width, damage.height);
|
|
159
|
+
sctx.clip();
|
|
160
|
+
}
|
|
161
|
+
this._paintNode(sctx);
|
|
162
|
+
});
|
|
163
|
+
draw();
|
|
164
|
+
// A paragraph learns how far its ink reaches past its box by drawing
|
|
165
|
+
// (`TextNode._keepInkReach`). One its paint lays out at a width nothing
|
|
166
|
+
// measured it at — a box of no width is drawn unwrapped — learns it only
|
|
167
|
+
// here, after the surface was sized, which cut its overflow off on the
|
|
168
|
+
// first frame and drew it whole on every frame after. Drawn once more,
|
|
169
|
+
// at the size it turned out to need: the one time it is not known.
|
|
170
|
+
const after = this._groupRect();
|
|
171
|
+
if (after && !rectContains(rect, after)) {
|
|
172
|
+
rect = after;
|
|
173
|
+
surface = this._groupSurfaceFor(rect.width, rect.height);
|
|
174
|
+
if (!surface) return;
|
|
175
|
+
draw();
|
|
176
|
+
}
|
|
162
177
|
ctx.globalAlpha = alpha * opacity;
|
|
163
178
|
try {
|
|
164
179
|
ctx.drawImage(surface, rect.x, rect.y);
|
|
@@ -522,6 +537,16 @@ export class NodePaint {
|
|
|
522
537
|
}
|
|
523
538
|
}
|
|
524
539
|
|
|
540
|
+
/** Whether `outer` holds all of `inner`. */
|
|
541
|
+
function rectContains(outer, inner) {
|
|
542
|
+
return (
|
|
543
|
+
inner.x >= outer.x &&
|
|
544
|
+
inner.y >= outer.y &&
|
|
545
|
+
inner.x + inner.width <= outer.x + outer.width &&
|
|
546
|
+
inner.y + inner.height <= outer.y + outer.height
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
525
550
|
/** WindowNode's half of the paint walk, installed onto `WindowNode.prototype` by window/window.js. */
|
|
526
551
|
export class WindowPaint {
|
|
527
552
|
/** Repaint one damage rect, or the whole window when `damage` is null. */
|
package/src/nodes/scrollable.js
CHANGED
|
@@ -67,6 +67,8 @@ function contentReach(node, top) {
|
|
|
67
67
|
const boxes = layoutDiff.boxes;
|
|
68
68
|
for (const child of node.children) {
|
|
69
69
|
if (child.isWindow || !child.yoga || child.hidden) continue;
|
|
70
|
+
// not laid out, and not there: see `absolutize`
|
|
71
|
+
if (child.style.display === 'none') continue;
|
|
70
72
|
const box = child.yoga.getComputedLayout();
|
|
71
73
|
// …for the walk that places this child, later in the same one
|
|
72
74
|
boxes?.set(child, box);
|
|
@@ -239,12 +241,20 @@ export const Scrollable = (Base) =>
|
|
|
239
241
|
try {
|
|
240
242
|
this._assignAbs(x, y, width, height);
|
|
241
243
|
if (this.props.onLayout) this._reportLayout();
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
// A `display: 'none'` subtree takes part in no layout: yoga leaves
|
|
245
|
+
// its children where the last pass that reached them did — and a
|
|
246
|
+
// child inserted or moved while it was hidden with no layout at all,
|
|
247
|
+
// NaN, which a scroll pane in there measured its content from and
|
|
248
|
+
// threw on, out of the frame. The box itself has its empty rect;
|
|
249
|
+
// what is inside is placed when it is shown and laid out again.
|
|
250
|
+
if (this.style.display !== 'none') {
|
|
251
|
+
this._absolutizeChildren(
|
|
252
|
+
this.abs.x,
|
|
253
|
+
this.abs.y,
|
|
254
|
+
this.abs.x - wasX,
|
|
255
|
+
this.abs.y - wasY,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
248
258
|
} finally {
|
|
249
259
|
if (move) {
|
|
250
260
|
layoutDiff.shift = null;
|
package/src/nodes/scrollblit.js
CHANGED
|
@@ -218,6 +218,13 @@ function unionIsBox(a, b) {
|
|
|
218
218
|
);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/** Two lists of pins as one, either of which may be null. */
|
|
222
|
+
function joinPins(a, b) {
|
|
223
|
+
if (!a) return b;
|
|
224
|
+
if (!b) return a;
|
|
225
|
+
return [...a, ...b];
|
|
226
|
+
}
|
|
227
|
+
|
|
221
228
|
/** Is `rect` inside one of `pins`, the furniture an element blit pinned —
|
|
222
229
|
* repaired whole after the copy, whatever changed in it (issue #682)? */
|
|
223
230
|
function insidePinned(rect, pins) {
|
|
@@ -862,7 +869,13 @@ export class WindowScrollBlit {
|
|
|
862
869
|
// (issue #691) and repaired below, where they are and where the copy
|
|
863
870
|
// dragged their image. Refused instead, a list or a document inside a
|
|
864
871
|
// rounded frame repainted its whole viewport on every step of a scroll.
|
|
865
|
-
|
|
872
|
+
// …and so is what an element in the pane draws fixed to its viewport
|
|
873
|
+
// (`viewportFixedRects()`): a document's fixed background or header,
|
|
874
|
+
// which the copy would drag along with the text under it
|
|
875
|
+
const corners = joinPins(
|
|
876
|
+
this._cornerPins(node, vp, target.top),
|
|
877
|
+
this._viewportFixedPins(node, vp),
|
|
878
|
+
);
|
|
866
879
|
if (!this._scrollBlitSafe(node, vp, target.top, null, corners)) return;
|
|
867
880
|
// The band the scrolled bar's thumb travels in is repaired on its own.
|
|
868
881
|
// The thumb's rects are thin and run along the viewport's edge, and a
|
|
@@ -1189,6 +1202,30 @@ export class WindowScrollBlit {
|
|
|
1189
1202
|
* Ancestors below `upTo`, the node the blit's surface belongs to
|
|
1190
1203
|
* (`_blitTarget`): a pane is a rectangle no corner above it clips.
|
|
1191
1204
|
*/
|
|
1205
|
+
/**
|
|
1206
|
+
* What the elements in scroll pane `node` draw fixed to its viewport
|
|
1207
|
+
* (`viewportFixedRects()`), inside `vp` and out to whole pixels: pinned
|
|
1208
|
+
* like a rounded ancestor's corners, repaired where they are and where the
|
|
1209
|
+
* blit dragged their image. Asked only of an element whose nearest scroll
|
|
1210
|
+
* pane is `node` — a pane further in moves with the content, and whatever
|
|
1211
|
+
* is fixed to it with it — and not of one hidden or gone.
|
|
1212
|
+
*/
|
|
1213
|
+
_viewportFixedPins(node, vp) {
|
|
1214
|
+
let out = null;
|
|
1215
|
+
for (const fixed of this._viewportFixedNodes ?? []) {
|
|
1216
|
+
if (fixed.destroyed || fixed.hidden || fixed.root !== this) continue;
|
|
1217
|
+
let pane = fixed.parent;
|
|
1218
|
+
while (pane && !pane.isWindow && !pane.isScroller?.()) pane = pane.parent;
|
|
1219
|
+
if (pane !== node) continue;
|
|
1220
|
+
for (const rect of fixed.viewportFixedRects() ?? []) {
|
|
1221
|
+
if (!rectsOverlap(rect, vp)) continue;
|
|
1222
|
+
const inside = intersectRects(outerPixels(rect), vp);
|
|
1223
|
+
if (inside) (out ??= []).push(inside);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return out;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1192
1229
|
_cornerPins(node, vp, upTo = this) {
|
|
1193
1230
|
let out = null;
|
|
1194
1231
|
for (let n = node.parent; n && n !== upTo; n = n.parent) {
|
package/src/nodes/text.js
CHANGED
|
@@ -52,6 +52,72 @@ function outside(ink, box) {
|
|
|
52
52
|
/** `TextNode._trim`'s answer, per layout. */
|
|
53
53
|
const trims = new WeakMap();
|
|
54
54
|
|
|
55
|
+
/** {@link glyphInk}'s answer, per layout. */
|
|
56
|
+
const glyphInks = new WeakMap();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Where a layout's glyphs put ink past its lines' boxes, from its origin —
|
|
60
|
+
* or null where the engine cannot say. A glyph inks past its advance: a `j`
|
|
61
|
+
* that starts a line hooks back over the line's start and an `f` that ends
|
|
62
|
+
* one reaches past its end, by their own boxes (`glyphExtents`, ntk's). And
|
|
63
|
+
* a line set tighter than its face has ascenders over its top and
|
|
64
|
+
* descenders under its bottom, by the face's own reach (`metrics`).
|
|
65
|
+
*
|
|
66
|
+
* Found once per layout, and only where it can leave the layout's box: at a
|
|
67
|
+
* line's two ends, and over the first line and under the last. A glyph an
|
|
68
|
+
* em inside a line's ends reaches neither end, and measuring every glyph
|
|
69
|
+
* cost about as much again as laying the paragraph out.
|
|
70
|
+
*/
|
|
71
|
+
function glyphInk(layout) {
|
|
72
|
+
const known = glyphInks.get(layout);
|
|
73
|
+
if (known !== undefined) return known;
|
|
74
|
+
let left = Infinity;
|
|
75
|
+
let top = Infinity;
|
|
76
|
+
let right = -Infinity;
|
|
77
|
+
let bottom = -Infinity;
|
|
78
|
+
const lines = layout.lines;
|
|
79
|
+
for (let i = 0; i < lines.length; i++) {
|
|
80
|
+
const line = lines[i];
|
|
81
|
+
const outer = i === 0 || i === lines.length - 1;
|
|
82
|
+
const start = line.x;
|
|
83
|
+
const end = line.x + line.width;
|
|
84
|
+
for (const placed of line.runs ?? []) {
|
|
85
|
+
const run = placed.run;
|
|
86
|
+
if (!run?.glyphs) continue;
|
|
87
|
+
if (outer) {
|
|
88
|
+
const m = run.font?.metrics?.(run.size);
|
|
89
|
+
if (m && m.ascent >= 0 && m.descent >= 0) {
|
|
90
|
+
top = Math.min(top, line.baseline - m.ascent);
|
|
91
|
+
bottom = Math.max(bottom, line.baseline + m.descent);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// the pen as `layout.draw` moves it
|
|
95
|
+
let pen = line.x + placed.x;
|
|
96
|
+
for (const glyph of run.glyphs) {
|
|
97
|
+
const gx = pen + glyph.dx;
|
|
98
|
+
const gy = line.baseline - glyph.dy;
|
|
99
|
+
pen += glyph.ax;
|
|
100
|
+
if (gx - run.size > start && gx + run.size < end) continue;
|
|
101
|
+
// a face that cannot say leaves its glyphs to the lines' boxes, and
|
|
102
|
+
// a blank glyph — a space — has no ink to reach anywhere with
|
|
103
|
+
const face = glyph.font ?? run.font;
|
|
104
|
+
const e = face?.glyphExtents?.(glyph.id, run.size);
|
|
105
|
+
if (!e || !(e.maxX > e.minX && e.maxY > e.minY)) continue;
|
|
106
|
+
left = Math.min(left, gx + e.minX);
|
|
107
|
+
right = Math.max(right, gx + e.maxX);
|
|
108
|
+
if (outer) {
|
|
109
|
+
top = Math.min(top, gy + e.minY);
|
|
110
|
+
bottom = Math.max(bottom, gy + e.maxY);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const ink =
|
|
116
|
+
left <= right || top <= bottom ? { left, top, right, bottom } : null;
|
|
117
|
+
glyphInks.set(layout, ink);
|
|
118
|
+
return ink;
|
|
119
|
+
}
|
|
120
|
+
|
|
55
121
|
/** The size under which this connection's text is painted as strips. */
|
|
56
122
|
export function setTextStripBelow(app, below) {
|
|
57
123
|
if (below === undefined) return;
|
|
@@ -739,7 +805,8 @@ export class TextNode extends Node {
|
|
|
739
805
|
if (placed) layoutDiff.sink(insetRect(placed, -DAMAGE_SLOP), this);
|
|
740
806
|
}
|
|
741
807
|
|
|
742
|
-
/** The rect the lines of a placed layout cover,
|
|
808
|
+
/** The rect the lines of a placed layout cover, and their glyphs' ink
|
|
809
|
+
* where it reaches past them, in window coordinates. */
|
|
743
810
|
_inkOf({ layout, x, y }) {
|
|
744
811
|
const lines = layout.lines;
|
|
745
812
|
if (!lines?.length) return null;
|
|
@@ -749,11 +816,21 @@ export class TextNode extends Node {
|
|
|
749
816
|
left = Math.min(left, line.x);
|
|
750
817
|
right = Math.max(right, line.x + line.width);
|
|
751
818
|
}
|
|
819
|
+
let top = 0;
|
|
820
|
+
let bottom = layout.height;
|
|
821
|
+
const glyphs = glyphInk(layout);
|
|
822
|
+
if (glyphs) {
|
|
823
|
+
// each is ±Infinity where nothing was found to reach that way
|
|
824
|
+
left = Math.min(left, glyphs.left);
|
|
825
|
+
right = Math.max(right, glyphs.right);
|
|
826
|
+
top = Math.min(top, glyphs.top);
|
|
827
|
+
bottom = Math.max(bottom, glyphs.bottom);
|
|
828
|
+
}
|
|
752
829
|
return {
|
|
753
830
|
x: x + left,
|
|
754
|
-
y,
|
|
831
|
+
y: y + top,
|
|
755
832
|
width: right - left,
|
|
756
|
-
height:
|
|
833
|
+
height: bottom - top,
|
|
757
834
|
};
|
|
758
835
|
}
|
|
759
836
|
|
|
@@ -56,7 +56,7 @@ export function captureLeafHeights(node, out) {
|
|
|
56
56
|
* is back to meaning only how eagerly the space *above* the floor is given
|
|
57
57
|
* up.
|
|
58
58
|
*/
|
|
59
|
-
function namesOwnFloor(node, axis) {
|
|
59
|
+
export function namesOwnFloor(node, axis) {
|
|
60
60
|
const style = node.style;
|
|
61
61
|
if (style.overflow === 'scroll' || style.overflow === 'hidden') return true;
|
|
62
62
|
return (
|
|
@@ -138,6 +138,14 @@ export class WindowFlush {
|
|
|
138
138
|
if (!this.destroyed) this._scheduleFrame();
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
+
// A frame that laid out may have moved what is under a pointer that did
|
|
142
|
+
// not move — a scroll does, every frame of it — and hover follows the
|
|
143
|
+
// content (`EventManager.refreshHover`). After the frame: what hover
|
|
144
|
+
// changes is the next frame's to paint.
|
|
145
|
+
if (this._hoverRecheck) {
|
|
146
|
+
this._hoverRecheck = false;
|
|
147
|
+
if (!this.destroyed) this.events?.refreshHover();
|
|
148
|
+
}
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
/** The frame itself. True when it painted or presented something. */
|
|
@@ -289,7 +297,11 @@ export class WindowFlush {
|
|
|
289
297
|
// like `presentFrame`; an ntk window has no such half.
|
|
290
298
|
this.window.prepareFrame?.(this, layoutRan);
|
|
291
299
|
// any node this pass laid out may be what an open popup is anchored to
|
|
292
|
-
|
|
300
|
+
// — or what a pointer that has not moved is over now (`flush`)
|
|
301
|
+
if (layoutRan) {
|
|
302
|
+
this._notifyAnchorChange();
|
|
303
|
+
this._hoverRecheck = true;
|
|
304
|
+
}
|
|
293
305
|
// A subtree that only moved and covers its box opaquely is copied where
|
|
294
306
|
// it went, and the frame paints around the copy — or its move is
|
|
295
307
|
// claimed, where it was and where it is (nodes/moveblit.js). Once every
|
package/src/nodes/window/size.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// floors, laying the root out, deferring floors during a live resize and
|
|
3
3
|
// catching up after, and refitting the window to what it holds.
|
|
4
4
|
|
|
5
|
-
import { measuringExactly } from '../../styles.js';
|
|
5
|
+
import { exactLayoutConfig, measuringExactly } from '../../styles.js';
|
|
6
6
|
import { availableArea } from '../../screens.js';
|
|
7
7
|
import { Yoga } from '../../yoga.js';
|
|
8
8
|
import {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
columnHeightSpan,
|
|
12
12
|
markUnreadLeaves,
|
|
13
13
|
declaresOwnMinimum,
|
|
14
|
+
namesOwnFloor,
|
|
14
15
|
dirtyOutside,
|
|
15
16
|
floorBoundaryOf,
|
|
16
17
|
probeHeightFloors,
|
|
@@ -327,12 +328,18 @@ export class WindowSize {
|
|
|
327
328
|
|
|
328
329
|
/**
|
|
329
330
|
* One spine root's floors, measured with it laid out alone — at the width
|
|
330
|
-
* it has in each of the passes over the whole tree (`spine.js`), and
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
331
|
+
* it has in each of the passes over the whole tree (`spine.js`), and, for
|
|
332
|
+
* its heights, as the one item of a column that offers it what each of
|
|
333
|
+
* those passes does: no height in the first, and in the collapse what
|
|
334
|
+
* the column it sits in offers there (`frameFor`), which is 0 under the
|
|
335
|
+
* window. Laid out as an item rather than as a root, it is resolved as
|
|
336
|
+
* its column resolves it there — a basis counts, a floor of its own is
|
|
337
|
+
* given way to — and what it holds is laid out in the bound yoga passes
|
|
338
|
+
* down, which squashes a pane in it that names a floor: laid out with no
|
|
339
|
+
* height at all, a <Tree>'s pane came out as tall as its 100,000 rows.
|
|
340
|
+
* Its own extents are written here: unlike a box that sizes itself, a
|
|
341
|
+
* root's are its content's, and the column it sits in is summed from
|
|
342
|
+
* them (`_sumSpine`).
|
|
336
343
|
*/
|
|
337
344
|
_measureSpineRoot(root, widths, axis, probe = false) {
|
|
338
345
|
onExactCopy(root, () =>
|
|
@@ -356,33 +363,50 @@ export class WindowSize {
|
|
|
356
363
|
restoreShrink(shrunk);
|
|
357
364
|
return;
|
|
358
365
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
// the column it is laid out in (`frameFor`), as wide as the one it sits in
|
|
367
|
+
const column = Yoga.Node.create(exactLayoutConfig());
|
|
368
|
+
if (widths.frame.scrolls) column.setOverflow(Yoga.OVERFLOW_SCROLL);
|
|
369
|
+
column.insertChild(yoga, 0);
|
|
370
|
+
const pass = (height) => {
|
|
371
|
+
this._scopedFloorPasses += 1;
|
|
372
|
+
column.calculateLayout(widths.width, height, dir);
|
|
373
|
+
};
|
|
374
|
+
try {
|
|
375
|
+
pass(undefined);
|
|
376
|
+
if (probe) {
|
|
377
|
+
const hit = { marked: false, owed: false };
|
|
378
|
+
probeHeightFloors(root, this, hit, root);
|
|
379
|
+
if (hit.marked) {
|
|
380
|
+
this._writeFloors('height');
|
|
381
|
+
pass(undefined);
|
|
382
|
+
}
|
|
368
383
|
}
|
|
384
|
+
const intrinsic = new Map();
|
|
385
|
+
captureLeafHeights(root, intrinsic);
|
|
386
|
+
const frozen = [];
|
|
387
|
+
freezeWidths(root, frozen);
|
|
388
|
+
const shrunk = [];
|
|
389
|
+
setMeasuringShrink(root, axis, shrunk);
|
|
390
|
+
// …and gives way in it as `setMeasuringShrink` has everything else
|
|
391
|
+
// give way: to a floor it names, and to nothing else
|
|
392
|
+
const shrink = namesOwnFloor(root, axis) ? 1 : 0;
|
|
393
|
+
if ((root.style.flexShrink ?? 1) !== shrink) {
|
|
394
|
+
yoga.setFlexShrink(shrink);
|
|
395
|
+
shrunk.push(root);
|
|
396
|
+
}
|
|
397
|
+
pass(widths.frame.height);
|
|
398
|
+
const span = contentSpan(root, axis, intrinsic, this);
|
|
399
|
+
root._floorH = declaresOwnMinimum(root, axis)
|
|
400
|
+
? yoga.getComputedHeight()
|
|
401
|
+
: span;
|
|
402
|
+
root._floorAtW = yoga.getComputedWidth();
|
|
403
|
+
this._floorsMeasured += 1;
|
|
404
|
+
restoreWidths(frozen);
|
|
405
|
+
restoreShrink(shrunk);
|
|
406
|
+
} finally {
|
|
407
|
+
column.removeChild(yoga);
|
|
408
|
+
column.free();
|
|
369
409
|
}
|
|
370
|
-
const intrinsic = new Map();
|
|
371
|
-
captureLeafHeights(root, intrinsic);
|
|
372
|
-
const frozen = [];
|
|
373
|
-
freezeWidths(root, frozen);
|
|
374
|
-
const shrunk = [];
|
|
375
|
-
setMeasuringShrink(root, axis, shrunk);
|
|
376
|
-
this._scopedFloorPasses += 1;
|
|
377
|
-
yoga.calculateLayout(widths.width, undefined, dir);
|
|
378
|
-
const span = contentSpan(root, axis, intrinsic, this);
|
|
379
|
-
root._floorH = declaresOwnMinimum(root, axis)
|
|
380
|
-
? yoga.getComputedHeight()
|
|
381
|
-
: span;
|
|
382
|
-
root._floorAtW = yoga.getComputedWidth();
|
|
383
|
-
this._floorsMeasured += 1;
|
|
384
|
-
restoreWidths(frozen);
|
|
385
|
-
restoreShrink(shrunk);
|
|
386
410
|
}
|
|
387
411
|
|
|
388
412
|
/** The columns of this frame's spines, deepest first, summed from their
|