@react-x11/components 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -31
- package/dist/color-picker/ColorPicker.d.ts.map +1 -1
- package/dist/color-picker/ColorPicker.js +43 -33
- package/dist/color-picker/ColorPicker.js.map +1 -1
- package/dist/html/dom.d.ts +8 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +17 -2
- package/dist/html/dom.js.map +1 -1
- package/dist/html/index.d.ts +4 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js.map +1 -1
- package/dist/internal/scroll.d.ts.map +1 -1
- package/dist/internal/scroll.js +32 -15
- package/dist/internal/scroll.js.map +1 -1
- package/dist/internal/units.d.ts +2 -0
- package/dist/internal/units.d.ts.map +1 -0
- package/dist/internal/units.js +35 -0
- package/dist/internal/units.js.map +1 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +4 -2
- package/dist/internal/window.js.map +1 -1
- package/dist/table/index.d.ts.map +1 -1
- package/dist/table/index.js +14 -5
- package/dist/table/index.js.map +1 -1
- package/dist/terminal/vt/node.d.ts +56 -3
- package/dist/terminal/vt/node.d.ts.map +1 -1
- package/dist/terminal/vt/node.js +106 -26
- package/dist/terminal/vt/node.js.map +1 -1
- package/dist/terminal/vt/renderer.d.ts +4 -0
- package/dist/terminal/vt/renderer.d.ts.map +1 -1
- package/dist/terminal/vt/renderer.js +14 -5
- package/dist/terminal/vt/renderer.js.map +1 -1
- package/dist/tree/index.d.ts.map +1 -1
- package/dist/tree/index.js +6 -4
- package/dist/tree/index.js.map +1 -1
- package/package.json +3 -3
- package/src/color-picker/ColorPicker.ts +107 -81
- package/src/html/dom.ts +17 -2
- package/src/html/index.ts +4 -0
- package/src/internal/scroll.ts +34 -15
- package/src/internal/units.ts +34 -0
- package/src/internal/window.ts +4 -2
- package/src/table/index.ts +14 -5
- package/src/terminal/vt/node.ts +128 -29
- package/src/terminal/vt/renderer.ts +27 -5
- package/src/tree/index.ts +6 -4
|
@@ -491,7 +491,16 @@ export function ColorPicker(props: ColorPickerProps): ReactElement {
|
|
|
491
491
|
node: DrawnNode | null,
|
|
492
492
|
ev: MouseEvent,
|
|
493
493
|
): { x: number; y: number } | null => {
|
|
494
|
-
|
|
494
|
+
// `getClientRects()`, not `abs`. `abs` is **device** pixels and an
|
|
495
|
+
// event's `x`/`y` are **logical** — react-x11's docs/scale.md — so
|
|
496
|
+
// subtracting one from the other is only right at scale 1. On a retina
|
|
497
|
+
// panel a press at the middle of this box read as a press a quarter of
|
|
498
|
+
// the way in from its corner: the thumb sat near the corner however the
|
|
499
|
+
// pointer moved, and the far side of every axis was unreachable. The
|
|
500
|
+
// client rect is the same box already divided by the node's scale,
|
|
501
|
+
// which is the unit the pointer arrives in, so the arithmetic below is
|
|
502
|
+
// the arithmetic it always was.
|
|
503
|
+
const [box] = node?.getClientRects() ?? [];
|
|
495
504
|
if (!box?.width || !box.height) return null;
|
|
496
505
|
return {
|
|
497
506
|
x: clamp01((ev.x - box.x) / box.width),
|
|
@@ -658,6 +667,41 @@ export function ColorPicker(props: ColorPickerProps): ReactElement {
|
|
|
658
667
|
onKeyDown: focusable ? handleKey : undefined,
|
|
659
668
|
});
|
|
660
669
|
|
|
670
|
+
/**
|
|
671
|
+
* The gradient and its border, as one element *under* the thumb.
|
|
672
|
+
*
|
|
673
|
+
* Core paints a node's children first and its border after them, so a
|
|
674
|
+
* border on the box the thumb lives in is painted across the thumb — the
|
|
675
|
+
* grey line over the white handle. On a sibling below it, the thumb wins,
|
|
676
|
+
* which is the order a handle wants: it is the thing being pointed at.
|
|
677
|
+
*
|
|
678
|
+
* It fixes the geometry too. An absolutely positioned child is laid out
|
|
679
|
+
* from its parent's *padding* box, so a border on the axis box shifted
|
|
680
|
+
* every thumb by its width — a strip handle overhung the top by 1px and
|
|
681
|
+
* the bottom by 3, and sat 1px right of the colour it was naming. Here
|
|
682
|
+
* the thumb's parent has no border, so `left` and `top` mean what they
|
|
683
|
+
* say: the same outer box `fractionIn` measures a press against.
|
|
684
|
+
*/
|
|
685
|
+
const framed = (
|
|
686
|
+
axis: 'area' | 'hue' | 'alpha',
|
|
687
|
+
pane: ReactElement,
|
|
688
|
+
): ReactElement =>
|
|
689
|
+
hx(
|
|
690
|
+
'box',
|
|
691
|
+
{
|
|
692
|
+
key: 'frame',
|
|
693
|
+
style: [
|
|
694
|
+
s.pane,
|
|
695
|
+
{
|
|
696
|
+
borderWidth: theme.borderWidth,
|
|
697
|
+
borderColor:
|
|
698
|
+
cursorShown && active === axis ? theme.borderFocus : theme.border,
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
},
|
|
702
|
+
pane,
|
|
703
|
+
);
|
|
704
|
+
|
|
661
705
|
const renderArea = (): ReactElement =>
|
|
662
706
|
hx(
|
|
663
707
|
'box',
|
|
@@ -671,40 +715,34 @@ export function ColorPicker(props: ColorPickerProps): ReactElement {
|
|
|
671
715
|
model.s * 100,
|
|
672
716
|
100,
|
|
673
717
|
),
|
|
674
|
-
style:
|
|
675
|
-
s.area,
|
|
676
|
-
{
|
|
677
|
-
borderWidth: theme.borderWidth,
|
|
678
|
-
borderColor:
|
|
679
|
-
cursorShown && active === 'area'
|
|
680
|
-
? theme.borderFocus
|
|
681
|
-
: theme.border,
|
|
682
|
-
},
|
|
683
|
-
],
|
|
718
|
+
style: s.area,
|
|
684
719
|
...track('area', areaTo),
|
|
685
720
|
},
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
ctx
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
721
|
+
framed(
|
|
722
|
+
'area',
|
|
723
|
+
hx('canvas', {
|
|
724
|
+
style: s.pane,
|
|
725
|
+
// Everything the drawing reads: the hue, and the size, which is a
|
|
726
|
+
// constant here precisely so this key can name it.
|
|
727
|
+
cacheKey: `sv:${Math.round(model.h)}:${CONTENT}x${AREA_H}`,
|
|
728
|
+
onDraw: (ctx, { width, height }) => {
|
|
729
|
+
ctx.fillStyle = hue;
|
|
730
|
+
ctx.fillRect(0, 0, width, height);
|
|
731
|
+
// Legacy comma spelling in the stops, for the same reason
|
|
732
|
+
// `formatColor` emits it: it is what ntk's parser understands.
|
|
733
|
+
const white = ctx.createLinearGradient(0, 0, width, 0);
|
|
734
|
+
white.addColorStop(0, 'rgb(255, 255, 255)');
|
|
735
|
+
white.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
|
736
|
+
ctx.fillStyle = white;
|
|
737
|
+
ctx.fillRect(0, 0, width, height);
|
|
738
|
+
const black = ctx.createLinearGradient(0, 0, 0, height);
|
|
739
|
+
black.addColorStop(0, 'rgba(0, 0, 0, 0)');
|
|
740
|
+
black.addColorStop(1, 'rgb(0, 0, 0)');
|
|
741
|
+
ctx.fillStyle = black;
|
|
742
|
+
ctx.fillRect(0, 0, width, height);
|
|
743
|
+
},
|
|
744
|
+
}),
|
|
745
|
+
),
|
|
708
746
|
hx('box', {
|
|
709
747
|
style: [
|
|
710
748
|
s.areaThumb,
|
|
@@ -730,33 +768,27 @@ export function ColorPicker(props: ColorPickerProps): ReactElement {
|
|
|
730
768
|
model.h,
|
|
731
769
|
360,
|
|
732
770
|
),
|
|
733
|
-
style:
|
|
734
|
-
s.strip,
|
|
735
|
-
{
|
|
736
|
-
borderWidth: theme.borderWidth,
|
|
737
|
-
borderColor:
|
|
738
|
-
cursorShown && active === 'hue'
|
|
739
|
-
? theme.borderFocus
|
|
740
|
-
: theme.border,
|
|
741
|
-
},
|
|
742
|
-
],
|
|
771
|
+
style: s.strip,
|
|
743
772
|
...track('hue', hueTo),
|
|
744
773
|
},
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
ramp.
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
774
|
+
framed(
|
|
775
|
+
'hue',
|
|
776
|
+
hx('canvas', {
|
|
777
|
+
style: s.pane,
|
|
778
|
+
cacheKey: `hue:${CONTENT}x${STRIP_H}`,
|
|
779
|
+
onDraw: (ctx, { width, height }) => {
|
|
780
|
+
const ramp = ctx.createLinearGradient(0, 0, width, 0);
|
|
781
|
+
for (let stop = 0; stop <= 6; stop += 1) {
|
|
782
|
+
ramp.addColorStop(
|
|
783
|
+
stop / 6,
|
|
784
|
+
opaqueHex(channelsFromHsv(stop * 60, 1, 1)),
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
ctx.fillStyle = ramp;
|
|
788
|
+
ctx.fillRect(0, 0, width, height);
|
|
789
|
+
},
|
|
790
|
+
}),
|
|
791
|
+
),
|
|
760
792
|
hx('box', {
|
|
761
793
|
style: [
|
|
762
794
|
s.stripThumb,
|
|
@@ -778,31 +810,25 @@ export function ColorPicker(props: ColorPickerProps): ReactElement {
|
|
|
778
810
|
model.a * 100,
|
|
779
811
|
100,
|
|
780
812
|
),
|
|
781
|
-
style:
|
|
782
|
-
s.strip,
|
|
783
|
-
{
|
|
784
|
-
borderWidth: theme.borderWidth,
|
|
785
|
-
borderColor:
|
|
786
|
-
cursorShown && active === 'alpha'
|
|
787
|
-
? theme.borderFocus
|
|
788
|
-
: theme.border,
|
|
789
|
-
},
|
|
790
|
-
],
|
|
813
|
+
style: s.strip,
|
|
791
814
|
...track('alpha', alphaTo),
|
|
792
815
|
},
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
816
|
+
framed(
|
|
817
|
+
'alpha',
|
|
818
|
+
hx('canvas', {
|
|
819
|
+
style: s.pane,
|
|
820
|
+
cacheKey: `alpha:${opaqueHex(model)}:${CONTENT}x${STRIP_H}`,
|
|
821
|
+
onDraw: (ctx, { width, height }) => {
|
|
822
|
+
drawChecker(ctx, width, height);
|
|
823
|
+
const ramp = ctx.createLinearGradient(0, 0, width, 0);
|
|
824
|
+
const { r, g, b } = model;
|
|
825
|
+
ramp.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0)`);
|
|
826
|
+
ramp.addColorStop(1, `rgb(${r}, ${g}, ${b})`);
|
|
827
|
+
ctx.fillStyle = ramp;
|
|
828
|
+
ctx.fillRect(0, 0, width, height);
|
|
829
|
+
},
|
|
830
|
+
}),
|
|
831
|
+
),
|
|
806
832
|
hx('box', {
|
|
807
833
|
style: [
|
|
808
834
|
s.stripThumb,
|
package/src/html/dom.ts
CHANGED
|
@@ -157,6 +157,11 @@ export type SheetRef =
|
|
|
157
157
|
* an extension resets the parser, because a mid-document edit can change the
|
|
158
158
|
* tree arbitrarily and pretending otherwise is how a streaming renderer
|
|
159
159
|
* shows a document nobody wrote.
|
|
160
|
+
*
|
|
161
|
+
* That delta is available only until the source says it is `complete`: an
|
|
162
|
+
* ended parse cannot be extended, so every later change re-parses. A caller
|
|
163
|
+
* that wants the append path — a stream, a `<Markdown partial>` — keeps
|
|
164
|
+
* `complete` false until the last chunk, which is what the flag is for.
|
|
160
165
|
*/
|
|
161
166
|
export class HtmlSource {
|
|
162
167
|
/** The tree, live: it grows as chunks are written. */
|
|
@@ -178,12 +183,22 @@ export class HtmlSource {
|
|
|
178
183
|
this.document = handler.root;
|
|
179
184
|
}
|
|
180
185
|
|
|
181
|
-
/** Set the whole source. Appends when it can
|
|
186
|
+
/** Set the whole source. Appends when it can — an extension of what was
|
|
187
|
+
* written, into a parser that has not been ended — and re-parses when it
|
|
188
|
+
* cannot. */
|
|
182
189
|
setSource(source: string, complete: boolean): boolean {
|
|
183
190
|
let changed = false;
|
|
184
191
|
if (source === this._written) {
|
|
185
192
|
changed = false;
|
|
186
|
-
} else if (source.startsWith(this._written)) {
|
|
193
|
+
} else if (!this.complete && source.startsWith(this._written)) {
|
|
194
|
+
// The append path is open only while the parser is. `end()` runs the
|
|
195
|
+
// tokenizer's EOF, and htmlparser2 answers any later chunk with
|
|
196
|
+
// `.write() after done!`, so a completed parse re-parses like any other
|
|
197
|
+
// edit. Without the guard the failure landed far from its cause: `<Html
|
|
198
|
+
// partial={false}>` passes `complete` on *every* render, so whether an
|
|
199
|
+
// edit crashed depended on the caret — one in the middle of the document
|
|
200
|
+
// is not a prefix and took the reset path, one at the end was a prefix,
|
|
201
|
+
// took this branch, and threw inside the reconciler's commit.
|
|
187
202
|
this._parser.write(source.slice(this._written.length));
|
|
188
203
|
this._written = source;
|
|
189
204
|
changed = true;
|
package/src/html/index.ts
CHANGED
|
@@ -87,6 +87,10 @@ export interface HtmlProps {
|
|
|
87
87
|
* that extends the last one is written as a delta, so the nodes already
|
|
88
88
|
* parsed keep their identity and their layout. Set false when the stream
|
|
89
89
|
* ends.
|
|
90
|
+
*
|
|
91
|
+
* False ends the parse, and that is final: a later `source` re-parses even
|
|
92
|
+
* when it extends the last one. An editor handing over the whole document
|
|
93
|
+
* on every keystroke wants the default.
|
|
90
94
|
*/
|
|
91
95
|
partial?: boolean;
|
|
92
96
|
/** Mouse selection, Ctrl+A / Ctrl+C, PRIMARY. Default true. */
|
package/src/internal/scroll.ts
CHANGED
|
@@ -44,6 +44,7 @@ import type { DrawnNode, ScrollableNode } from 'react-x11';
|
|
|
44
44
|
import type { RowHeights, RowKey } from './heights.js';
|
|
45
45
|
import { afterLayout, cancelAfterLayout } from './timers.js';
|
|
46
46
|
import type { LayoutTick } from './timers.js';
|
|
47
|
+
import { scaleOf } from './units.js';
|
|
47
48
|
|
|
48
49
|
/** All a reveal needs of a row: `<TreeRow>` and `<TableRow>` are both this. */
|
|
49
50
|
interface Keyed {
|
|
@@ -148,13 +149,17 @@ export function useReveal(sources: RevealSources): Reveal {
|
|
|
148
149
|
const scrollTo = useCallback((y: number): void => {
|
|
149
150
|
const box = src.current.box.current;
|
|
150
151
|
if (!box) return;
|
|
152
|
+
// `y` is logical, like everything the reveal computes with; the raw
|
|
153
|
+
// node properties are device (../internal/units.ts), divided here so the
|
|
154
|
+
// clamp and the early-out speak the caller's unit.
|
|
155
|
+
const s = scaleOf(box);
|
|
151
156
|
// Clamped here rather than left to the container, so what comes back on
|
|
152
157
|
// `onScroll` is the number that was asked for and can be recognised.
|
|
153
158
|
const to = Math.min(
|
|
154
159
|
Math.max(0, y),
|
|
155
|
-
Math.max(0, box.contentHeight - box.abs.height),
|
|
160
|
+
Math.max(0, (box.contentHeight - box.abs.height) / s),
|
|
156
161
|
);
|
|
157
|
-
if (to === box.scrollY) return;
|
|
162
|
+
if (to === box.scrollY / s) return;
|
|
158
163
|
asked.current = to;
|
|
159
164
|
box.scrollTo({ y: to });
|
|
160
165
|
}, []);
|
|
@@ -166,9 +171,10 @@ export function useReveal(sources: RevealSources): Reveal {
|
|
|
166
171
|
const box = src.current.box.current;
|
|
167
172
|
const want = owedShift.current;
|
|
168
173
|
if (!box || want === 0) return;
|
|
169
|
-
const
|
|
174
|
+
const s = scaleOf(box);
|
|
175
|
+
const was = box.scrollY / s;
|
|
170
176
|
scrollTo(was + want);
|
|
171
|
-
owedShift.current = want - (box.scrollY - was);
|
|
177
|
+
owedShift.current = want - (box.scrollY / s - was);
|
|
172
178
|
}, [scrollTo]);
|
|
173
179
|
|
|
174
180
|
const nudge = useCallback(
|
|
@@ -208,7 +214,10 @@ export function useReveal(sources: RevealSources): Reveal {
|
|
|
208
214
|
settled.current = null;
|
|
209
215
|
return;
|
|
210
216
|
}
|
|
211
|
-
|
|
217
|
+
// Logical, like the height index the unmounted arithmetic mixes with —
|
|
218
|
+
// the raw properties are device (../internal/units.ts).
|
|
219
|
+
const s = scaleOf(box);
|
|
220
|
+
const viewport = box.abs.height / s;
|
|
212
221
|
if (viewport <= 0) return; // nothing laid out to scroll inside yet
|
|
213
222
|
|
|
214
223
|
// How far the row is outside the pane, in pixels of scrolling. The row's
|
|
@@ -223,16 +232,20 @@ export function useReveal(sources: RevealSources): Reveal {
|
|
|
223
232
|
: null;
|
|
224
233
|
const placed = mounted
|
|
225
234
|
? {
|
|
226
|
-
above: box.abs.y - mounted.abs.y,
|
|
227
|
-
below:
|
|
228
|
-
|
|
235
|
+
above: (box.abs.y - mounted.abs.y) / s,
|
|
236
|
+
below:
|
|
237
|
+
(mounted.abs.y +
|
|
238
|
+
mounted.abs.height -
|
|
239
|
+
(box.abs.y + box.abs.height)) /
|
|
240
|
+
s,
|
|
241
|
+
height: mounted.abs.height / s,
|
|
229
242
|
}
|
|
230
243
|
: {
|
|
231
|
-
above: box.scrollY - heights.offsetAt(at),
|
|
244
|
+
above: box.scrollY / s - heights.offsetAt(at),
|
|
232
245
|
below:
|
|
233
246
|
heights.offsetAt(at) +
|
|
234
247
|
heights.heightAt(at) -
|
|
235
|
-
(box.scrollY + viewport),
|
|
248
|
+
(box.scrollY / s + viewport),
|
|
236
249
|
height: heights.heightAt(at),
|
|
237
250
|
};
|
|
238
251
|
/**
|
|
@@ -254,8 +267,8 @@ export function useReveal(sources: RevealSources): Reveal {
|
|
|
254
267
|
*/
|
|
255
268
|
let certain = mounted !== null || heights.isMeasured(at);
|
|
256
269
|
if (certain && heights.hasMeasurements()) {
|
|
257
|
-
const from = Math.min(at, heights.indexAt(box.scrollY));
|
|
258
|
-
const to = Math.max(at, heights.indexAt(box.scrollY + viewport));
|
|
270
|
+
const from = Math.min(at, heights.indexAt(box.scrollY / s));
|
|
271
|
+
const to = Math.max(at, heights.indexAt(box.scrollY / s + viewport));
|
|
259
272
|
for (let i = from; i <= to && certain; i++) {
|
|
260
273
|
certain = heights.isMeasured(i);
|
|
261
274
|
}
|
|
@@ -283,8 +296,10 @@ export function useReveal(sources: RevealSources): Reveal {
|
|
|
283
296
|
}
|
|
284
297
|
// A move the pane cannot make yet leaves the debt standing: the layout
|
|
285
298
|
// that admits the rows just appended is the one that will let it finish.
|
|
299
|
+
// `was` in device, compared against the same raw property — whether it
|
|
300
|
+
// moved is unit-free; the request itself goes through logical scrollTo.
|
|
286
301
|
const was = box.scrollY;
|
|
287
|
-
scrollTo(was + move);
|
|
302
|
+
scrollTo(was / s + move);
|
|
288
303
|
if (box.scrollY !== was) {
|
|
289
304
|
stuck.current = null; // it moved; the scroll it caused brings us back
|
|
290
305
|
return;
|
|
@@ -323,8 +338,12 @@ export function useReveal(sources: RevealSources): Reveal {
|
|
|
323
338
|
|
|
324
339
|
const heard = useCallback((scrollY: number): void => {
|
|
325
340
|
// A scroll this component did not ask for ends the whole chase, not just
|
|
326
|
-
// the outstanding half of it.
|
|
327
|
-
|
|
341
|
+
// the outstanding half of it. Within half a pixel is ours: the payload
|
|
342
|
+
// is the device offset divided back to logical, and at a fractional
|
|
343
|
+
// scale that round trip need not be bit-identical to what was asked.
|
|
344
|
+
const ours =
|
|
345
|
+
asked.current !== null && Math.abs(scrollY - asked.current) < 0.5;
|
|
346
|
+
if (!ours) {
|
|
328
347
|
owed.current = null;
|
|
329
348
|
settled.current = null;
|
|
330
349
|
owedShift.current = 0;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// One logical pixel is `scale` device pixels — react-x11's docs/scale.md.
|
|
2
|
+
//
|
|
3
|
+
// **Shared between `<Tree>` and `<Table>`** like the rest of this directory;
|
|
4
|
+
// see the header of `./heights.ts` for why it is internal rather than a
|
|
5
|
+
// shared module with a subpath.
|
|
6
|
+
//
|
|
7
|
+
// The virtualizers keep their whole model in logical pixels: style lengths,
|
|
8
|
+
// the `onScroll`/`onViewport` payloads, the height index, and the props
|
|
9
|
+
// (`rowHeight`, `estimatedRowHeight`) are all logical. But the **raw node
|
|
10
|
+
// properties** they read back between events — `scrollY`, `contentHeight`,
|
|
11
|
+
// `abs` — are device pixels. At scale 1 the two units coincide, which is how
|
|
12
|
+
// every harness test and every Xvfb check passed while a retina display
|
|
13
|
+
// doubled every raw read: the offset re-read after layout landed at twice
|
|
14
|
+
// the logical value, the slice was rebuilt a viewport below where the user
|
|
15
|
+
// was looking, and the table example went blank on a flick (and stayed
|
|
16
|
+
// blank after an append-and-reveal). Every raw read goes through this
|
|
17
|
+
// divisor so the arithmetic stays in one unit.
|
|
18
|
+
//
|
|
19
|
+
// **A pointer is the other half of this.** An event's `x`/`y` are logical,
|
|
20
|
+
// so a gesture measured against a laid-out box wants the box in that unit:
|
|
21
|
+
// `node.getClientRects()[0]`, which is `abs` already divided, rather than
|
|
22
|
+
// `abs` and a multiply. `<ColorPicker>` compared the two directly and put
|
|
23
|
+
// its thumb a quarter of the way from the corner on every retina panel.
|
|
24
|
+
// Reach for `scaleOf` where there is no rect to ask for — a raw `scrollY`,
|
|
25
|
+
// a `contentHeight`, a constant that never passes through a style.
|
|
26
|
+
//
|
|
27
|
+
// `scale` is on every retained node at run time but not on the public
|
|
28
|
+
// `DrawnNode` type — hence the structural read, and `object` rather than
|
|
29
|
+
// `{ scale?: unknown }` in the signature: a weak type with no property in
|
|
30
|
+
// common with `DrawnNode` fails TS2559 at every call site.
|
|
31
|
+
export function scaleOf(node: object | null | undefined): number {
|
|
32
|
+
const s = node ? (node as { scale?: unknown }).scale : undefined;
|
|
33
|
+
return typeof s === 'number' && s > 0 ? s : 1;
|
|
34
|
+
}
|
package/src/internal/window.ts
CHANGED
|
@@ -48,6 +48,7 @@ import type { ScrollableNode } from 'react-x11';
|
|
|
48
48
|
import type { RowHeights, RowKey } from './heights.js';
|
|
49
49
|
import { cancelLater, later } from './timers.js';
|
|
50
50
|
import type { DelayTick } from './timers.js';
|
|
51
|
+
import { scaleOf } from './units.js';
|
|
51
52
|
|
|
52
53
|
/** All this needs of a row — `TreeRow` and `TableRow` are both this. */
|
|
53
54
|
interface Keyed {
|
|
@@ -298,7 +299,6 @@ export function useVirtualWindow(inputs: VirtualWindowInputs): VirtualWindow {
|
|
|
298
299
|
// one has no slice to rebuild, and re-rendering it on a scroll it
|
|
299
300
|
// already drew would be work for nothing.
|
|
300
301
|
if (!inp.current.virtualizing) return;
|
|
301
|
-
(globalThis as any).__scrolls = ((globalThis as any).__scrolls ?? 0) + 1;
|
|
302
302
|
const now = Date.now();
|
|
303
303
|
const s = vel.current;
|
|
304
304
|
const dt = now - s.t;
|
|
@@ -332,7 +332,9 @@ export function useVirtualWindow(inputs: VirtualWindowInputs): VirtualWindow {
|
|
|
332
332
|
const { box, virtualizing } = inp.current;
|
|
333
333
|
const node = box.current;
|
|
334
334
|
if (!node || !virtualizing) return;
|
|
335
|
-
|
|
335
|
+
// The raw property is device pixels; the viewport model — like the
|
|
336
|
+
// `onScroll` payload `scrolled` is fed — is logical (../internal/units.ts).
|
|
337
|
+
const y = node.scrollY / scaleOf(node);
|
|
336
338
|
setView((prev) => (prev.top === y ? prev : { ...prev, top: y }));
|
|
337
339
|
}, []);
|
|
338
340
|
|
package/src/table/index.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
cancelLater,
|
|
71
71
|
later,
|
|
72
72
|
} from '../internal/timers.js';
|
|
73
|
+
import { scaleOf } from '../internal/units.js';
|
|
73
74
|
import type { DelayTick } from '../internal/timers.js';
|
|
74
75
|
import { useReveal } from '../internal/scroll.js';
|
|
75
76
|
import {
|
|
@@ -953,7 +954,10 @@ export function Table<Row = any>(props: TableProps<Row>): ReactElement {
|
|
|
953
954
|
const syncScroll = useCallback((): void => {
|
|
954
955
|
const box = body.current;
|
|
955
956
|
if (!box) return;
|
|
956
|
-
|
|
957
|
+
// Device on the raw property; the header margin it feeds is a style
|
|
958
|
+
// length, logical — the same division every raw read here makes
|
|
959
|
+
// (../internal/units.ts).
|
|
960
|
+
const x = box.scrollX / scaleOf(box);
|
|
957
961
|
setScrollX((prev) => (prev === x ? prev : x));
|
|
958
962
|
winSync();
|
|
959
963
|
}, [winSync]);
|
|
@@ -992,13 +996,18 @@ export function Table<Row = any>(props: TableProps<Row>): ReactElement {
|
|
|
992
996
|
).total;
|
|
993
997
|
const box = body.current;
|
|
994
998
|
const rows = orderedRef.current;
|
|
995
|
-
const anchor = box ? heights.indexAt(box.scrollY) : 0;
|
|
999
|
+
const anchor = box ? heights.indexAt(box.scrollY / scaleOf(box)) : 0;
|
|
996
1000
|
let shift = 0;
|
|
997
1001
|
let changed = false;
|
|
998
1002
|
for (const [id, { node, at }] of rowNodes.current) {
|
|
999
1003
|
if (rows[at]?.id !== id) continue; // drawn against a list that moved
|
|
1000
|
-
|
|
1001
|
-
|
|
1004
|
+
// `abs` is device; the resolved widths and the height index are
|
|
1005
|
+
// logical. Within a pixel rather than exact: a fractional scale
|
|
1006
|
+
// rounds the device width to the pixel grid, and the stale-width
|
|
1007
|
+
// layouts this gate exists for are whole columns off, not fractions.
|
|
1008
|
+
const s = scaleOf(node);
|
|
1009
|
+
if (Math.abs(node.abs.width / s - expected) >= 1) continue;
|
|
1010
|
+
const height = node.abs.height / s;
|
|
1002
1011
|
const was = heights.heightAt(at);
|
|
1003
1012
|
if (!heights.measure(id, at, height)) continue;
|
|
1004
1013
|
changed = true;
|
|
@@ -1026,7 +1035,7 @@ export function Table<Row = any>(props: TableProps<Row>): ReactElement {
|
|
|
1026
1035
|
if (uniform || !virtualizing) return false;
|
|
1027
1036
|
const box = body.current;
|
|
1028
1037
|
if (!box) return false;
|
|
1029
|
-
const anchor = heights.indexAt(box.scrollY);
|
|
1038
|
+
const anchor = heights.indexAt(box.scrollY / scaleOf(box));
|
|
1030
1039
|
const before = heights.offsetAt(anchor);
|
|
1031
1040
|
if (!heights.adapt()) return false;
|
|
1032
1041
|
reveal.nudge(heights.offsetAt(anchor) - before);
|