drawnui-react 0.1.0-preview.16 → 0.1.0-preview.17
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 +1 -1
- package/SKIPPED.md +5 -5
- package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
- package/dist/controls/AnimatedFramesRenderer.js +16 -1
- package/dist/controls/DrawnGame.d.ts +4 -1
- package/dist/controls/DrawnGame.js +7 -3
- package/dist/controls/GridStructure.d.ts +13 -1
- package/dist/controls/GridStructure.js +38 -2
- package/dist/controls/SkiaCarousel.d.ts +1 -1
- package/dist/controls/SkiaCarousel.js +3 -2
- package/dist/controls/SkiaEditor.d.ts +5 -1
- package/dist/controls/SkiaEditor.js +19 -17
- package/dist/controls/SkiaGif.d.ts +3 -0
- package/dist/controls/SkiaGif.js +11 -1
- package/dist/controls/SkiaImage.js +15 -10
- package/dist/controls/SkiaLabel.d.ts +52 -3
- package/dist/controls/SkiaLabel.js +222 -50
- package/dist/controls/SkiaLayout.d.ts +5 -0
- package/dist/controls/SkiaLayout.js +7 -4
- package/dist/controls/SkiaLottie.js +3 -1
- package/dist/controls/SkiaProgress.d.ts +2 -1
- package/dist/controls/SkiaProgress.js +4 -1
- package/dist/controls/SkiaScroll.d.ts +12 -0
- package/dist/controls/SkiaScroll.js +76 -8
- package/dist/controls/SkiaShape.d.ts +13 -2
- package/dist/controls/SkiaShape.js +50 -10
- package/dist/controls/SkiaSlider.d.ts +5 -1
- package/dist/controls/SkiaSlider.js +7 -1
- package/dist/controls/SkiaSprite.d.ts +1 -0
- package/dist/controls/SkiaSprite.js +4 -0
- package/dist/controls/SnappingLayout.d.ts +5 -0
- package/dist/controls/SnappingLayout.js +8 -0
- package/dist/core/Accessibility.d.ts +13 -0
- package/dist/core/Accessibility.js +10 -4
- package/dist/core/Animators.d.ts +28 -1
- package/dist/core/Animators.js +76 -8
- package/dist/core/Canvas.d.ts +21 -4
- package/dist/core/Canvas.js +122 -23
- package/dist/core/Easing.d.ts +12 -1
- package/dist/core/Easing.js +25 -1
- package/dist/core/Gestures.d.ts +24 -1
- package/dist/core/Gestures.js +75 -2
- package/dist/core/Hover.d.ts +14 -4
- package/dist/core/Hover.js +38 -22
- package/dist/core/ScrollAnimators.js +9 -0
- package/dist/core/SkiaControl.d.ts +17 -5
- package/dist/core/SkiaControl.js +69 -37
- package/dist/react/SkiaShell.js +33 -14
- package/dist/react/index.js +29 -2
- package/package.json +15 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Mirrors DrawnUi scrolling physics: DecelerationTimingParameters + ScrollFlingAnimator (fling),
|
|
2
2
|
// Spring + SpringTimingParameters + SpringWithVelocityAnimator (bounce), VelocityAccumulator, RubberBandUtils.
|
|
3
3
|
import { SkiaValueAnimator } from "./Animators";
|
|
4
|
+
import { VelocityTrail } from "./Gestures";
|
|
4
5
|
/** UIKit-style exponential deceleration: value(t) = v0 * (rate^(1000 t) - 1) / k, k = 1000 ln(rate). */
|
|
5
6
|
export class DecelerationTimingParameters {
|
|
6
7
|
InitialValue;
|
|
@@ -201,6 +202,14 @@ export class VelocityAccumulator {
|
|
|
201
202
|
Clear() { this.samples.length = 0; }
|
|
202
203
|
CaptureVelocity(x, y, arrivedTimeNanos = 0) {
|
|
203
204
|
const time = arrivedTimeNanos > 0 ? arrivedTimeNanos / 1e6 : performance.now();
|
|
205
|
+
// a move in the same burst as the last one replaces it: the first move of a burst carries only part of the burst's
|
|
206
|
+
// distance (DrawnUi.Rust VelocityAccumulator, 6d525f7)
|
|
207
|
+
const last = this.samples[this.samples.length - 1];
|
|
208
|
+
if (last && time - last.time < VelocityTrail.BurstMs) {
|
|
209
|
+
last.X = x;
|
|
210
|
+
last.Y = y;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
204
213
|
if (this.samples.length === VelocityAccumulator.MaxSampleSize)
|
|
205
214
|
this.samples.shift();
|
|
206
215
|
this.samples.push({ X: x, Y: y, time });
|
|
@@ -166,15 +166,15 @@ export declare class SkiaControl {
|
|
|
166
166
|
private compositeDeep?;
|
|
167
167
|
/** Set while an UpdateDraw climbs: the ancestors re-measure but an ImageComposite among them keeps its partial record. */
|
|
168
168
|
private static drawOrigin;
|
|
169
|
-
/** Nonzero while a composite redraws only some areas: layouts skip children entirely outside the clip. */
|
|
170
|
-
static CompositionCulling: number;
|
|
171
169
|
/** The children a composite record can re-record individually; layouts return their views. */
|
|
172
170
|
protected GetCompositeChildren(): readonly SkiaControl[];
|
|
173
171
|
/** C# TrackChildAsDirty (DirtyChildrenTracker): the child changed without a remeasure of this control. */
|
|
174
172
|
TrackChildAsDirty(child: SkiaControl): void;
|
|
175
173
|
private static readonly rejectRect;
|
|
176
|
-
/**
|
|
177
|
-
|
|
174
|
+
/** Its cache must be recorded before it can be drawn (none, dirty, another type, scale, size or GPU context). */
|
|
175
|
+
private CacheStale;
|
|
176
|
+
/** Its rect grown by its effects margin, through the matrix it is drawn with, lies outside the canvas clip. */
|
|
177
|
+
private OutsideClip;
|
|
178
178
|
/** Drawn bounds in canvas pixels including transforms and effects margins (C# GetTransformedDirtyBounds). */
|
|
179
179
|
GetTransformedDirtyBounds(): SKRect;
|
|
180
180
|
/** Current cache, if any. */
|
|
@@ -214,7 +214,7 @@ export declare class SkiaControl {
|
|
|
214
214
|
* top-most first, by the routing rules of ProcessGestures. True when this subtree blocks gestures below it, so the
|
|
215
215
|
* controls under it get no hover (a popup over a list).
|
|
216
216
|
*/
|
|
217
|
-
CollectHovered(point: SKPoint, into: SkiaControl[]): boolean;
|
|
217
|
+
CollectHovered(point: SKPoint, into: SkiaControl[], xs?: number[], ys?: number[]): boolean;
|
|
218
218
|
TouchEffectColor: Color;
|
|
219
219
|
/** Plays a ripple (from the tap point) when Tapped fires. */
|
|
220
220
|
AnimationTapped: SkiaTouchAnimation;
|
|
@@ -332,6 +332,8 @@ export declare class SkiaControl {
|
|
|
332
332
|
* host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
|
|
333
333
|
*/
|
|
334
334
|
WantsPointerCursor(_x: number, _y: number): boolean;
|
|
335
|
+
/** A range control's value for screen readers (slider, progress); undefined for everything else (drawnui-cross 6c). */
|
|
336
|
+
get AccessibilityValue(): import("./Accessibility").AccessibilityValue | undefined;
|
|
335
337
|
/** aria-pressed for toggles; undefined = not a toggle. */
|
|
336
338
|
get AccessibilityIsPressed(): boolean | undefined;
|
|
337
339
|
set AccessibilityIsPressed(v: boolean | undefined);
|
|
@@ -471,6 +473,16 @@ export declare class SkiaControl {
|
|
|
471
473
|
Update(): void;
|
|
472
474
|
/** Redraw without remeasure and WITHOUT dropping caches (position/overlay changes). */
|
|
473
475
|
Repaint(): void;
|
|
476
|
+
/**
|
|
477
|
+
* A parent (not the control itself) is hidden: a change here asks for no frame and the latest state shows when the
|
|
478
|
+
* parent is shown again (C# 329f6c44 HiddenUpdateTests, DrawnUi.Rust hidden_updates). The control's own visibility
|
|
479
|
+
* change still draws: it is the one that hides or shows it.
|
|
480
|
+
*/
|
|
481
|
+
UnderHiddenParent(): boolean;
|
|
482
|
+
/** The control or a parent is hidden: its animators pause until it shows again. */
|
|
483
|
+
HiddenInTree(): boolean;
|
|
484
|
+
/** The control an invalidation started at while it climbs (InvalidateMeasure): a hidden parent of it wakes no frame. */
|
|
485
|
+
private static invalidateOrigin;
|
|
474
486
|
/**
|
|
475
487
|
* Transform / Opacity changed: this control's own cache is still valid (content unchanged) but every ancestor
|
|
476
488
|
* cache holds its composited output, so those are staled before redrawing (DrawnUi RedrawCanvas + parent invalidation).
|
package/dist/core/SkiaControl.js
CHANGED
|
@@ -189,17 +189,24 @@ export class SkiaControl {
|
|
|
189
189
|
compositeDeep;
|
|
190
190
|
/** Set while an UpdateDraw climbs: the ancestors re-measure but an ImageComposite among them keeps its partial record. */
|
|
191
191
|
static drawOrigin = null;
|
|
192
|
-
/** Nonzero while a composite redraws only some areas: layouts skip children entirely outside the clip. */
|
|
193
|
-
static CompositionCulling = 0;
|
|
194
192
|
/** The children a composite record can re-record individually; layouts return their views. */
|
|
195
193
|
GetCompositeChildren() { return []; }
|
|
196
194
|
/** C# TrackChildAsDirty (DirtyChildrenTracker): the child changed without a remeasure of this control. */
|
|
197
195
|
TrackChildAsDirty(child) { if (this.IsCacheComposite)
|
|
198
196
|
this.DirtyChildrenInternal.add(child); }
|
|
199
197
|
static rejectRect = new Float32Array(4);
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
const
|
|
198
|
+
/** Its cache must be recorded before it can be drawn (none, dirty, another type, scale, size or GPU context). */
|
|
199
|
+
CacheStale(scale) {
|
|
200
|
+
const cacheType = this.UsingCacheType;
|
|
201
|
+
if (cacheType === "None")
|
|
202
|
+
return false;
|
|
203
|
+
const o = this.RenderObject, r = cacheType === "Operations" ? this.ExpandedCacheRect(scale) : this.AlignedCacheRect(scale);
|
|
204
|
+
return this.cacheDirty || !o || o.Type !== cacheType || o.Epoch !== (this.Superview?.GpuEpoch ?? 0) || o.Scale !== scale
|
|
205
|
+
|| Math.round(o.Bounds.Width) !== Math.round(r.Width) || Math.round(o.Bounds.Height) !== Math.round(r.Height);
|
|
206
|
+
}
|
|
207
|
+
/** Its rect grown by its effects margin, through the matrix it is drawn with, lies outside the canvas clip. */
|
|
208
|
+
OutsideClip(canvas, scale) {
|
|
209
|
+
const b = SkiaControl.MapRect(this.RenderTransformMatrix, this.ExpandedCacheRect(scale)), q = SkiaControl.rejectRect;
|
|
203
210
|
q[0] = b.Left;
|
|
204
211
|
q[1] = b.Top;
|
|
205
212
|
q[2] = b.Right;
|
|
@@ -259,15 +266,17 @@ export class SkiaControl {
|
|
|
259
266
|
* top-most first, by the routing rules of ProcessGestures. True when this subtree blocks gestures below it, so the
|
|
260
267
|
* controls under it get no hover (a popup over a list).
|
|
261
268
|
*/
|
|
262
|
-
CollectHovered(point, into) {
|
|
269
|
+
CollectHovered(point, into, xs, ys) {
|
|
263
270
|
into.push(this);
|
|
271
|
+
xs?.push(point.X);
|
|
272
|
+
ys?.push(point.Y);
|
|
264
273
|
if (!this.CheckChildrenGesturesLocked("Pointer")) {
|
|
265
274
|
const listeners = this.GetGestureListeners();
|
|
266
275
|
for (let i = listeners.length - 1; i >= 0; i--) {
|
|
267
276
|
const child = listeners[i];
|
|
268
277
|
if (!child.IsVisible || child.InputTransparent || !this.IsGestureForChild(child, point))
|
|
269
278
|
continue;
|
|
270
|
-
if (child.CollectHovered(child.TransformPointToLocalSpace(point), into))
|
|
279
|
+
if (child.CollectHovered(child.TransformPointToLocalSpace(point), into, xs, ys))
|
|
271
280
|
return true;
|
|
272
281
|
}
|
|
273
282
|
}
|
|
@@ -528,6 +537,8 @@ export class SkiaControl {
|
|
|
528
537
|
* host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
|
|
529
538
|
*/
|
|
530
539
|
WantsPointerCursor(_x, _y) { return this.AccessibilityCanInteract; }
|
|
540
|
+
/** A range control's value for screen readers (slider, progress); undefined for everything else (drawnui-cross 6c). */
|
|
541
|
+
get AccessibilityValue() { return undefined; }
|
|
531
542
|
/** aria-pressed for toggles; undefined = not a toggle. */
|
|
532
543
|
get AccessibilityIsPressed() { return this.accessibilityIsPressed; }
|
|
533
544
|
set AccessibilityIsPressed(v) { if (this.accessibilityIsPressed !== v) {
|
|
@@ -691,6 +702,23 @@ export class SkiaControl {
|
|
|
691
702
|
const offsetOnly = (this.Left !== 0 || this.Top !== 0) && this.UsingCacheType !== "None" && this.TranslationX === 0 && this.TranslationY === 0
|
|
692
703
|
&& this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0;
|
|
693
704
|
const needTransform = this.HasTransform && !offsetOnly;
|
|
705
|
+
let dx = 0, dy = 0;
|
|
706
|
+
if (needTransform) {
|
|
707
|
+
this.RenderTransformMatrix = this.CreateRenderTransformMatrix(this.DrawingRect, ctx.Scale);
|
|
708
|
+
}
|
|
709
|
+
else if (offsetOnly) {
|
|
710
|
+
dx = this.Left * ctx.Scale;
|
|
711
|
+
dy = this.Top * ctx.Scale;
|
|
712
|
+
this.RenderTransformMatrix = Super.CK.Matrix.translated(dx, dy); // gestures / accessibility still map through it
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
this.RenderTransformMatrix = undefined;
|
|
716
|
+
}
|
|
717
|
+
// drawn entirely outside the clip: skipped with its subtree (C# layout Virtualisation, DrawnUi.Rust); the matrix
|
|
718
|
+
// above stays current for hit tests. A cache that must be recorded is still recorded here, as before, so its cost
|
|
719
|
+
// does not move into the frame where the control scrolls in (DrawnUi.Rust records it then)
|
|
720
|
+
if (this.OutsideClip(canvas, ctx.Scale) && !this.CacheStale(ctx.Scale))
|
|
721
|
+
return;
|
|
694
722
|
let saved = false;
|
|
695
723
|
// same as DrawnUi: opacity = a layer with alpha, transforms = canvas matrix around the whole subtree (cache included)
|
|
696
724
|
if (applyOpacity) {
|
|
@@ -704,19 +732,8 @@ export class SkiaControl {
|
|
|
704
732
|
canvas.save();
|
|
705
733
|
saved = true;
|
|
706
734
|
}
|
|
707
|
-
|
|
708
|
-
if (needTransform) {
|
|
709
|
-
this.RenderTransformMatrix = this.CreateRenderTransformMatrix(this.DrawingRect, ctx.Scale);
|
|
735
|
+
if (needTransform)
|
|
710
736
|
canvas.concat(this.RenderTransformMatrix);
|
|
711
|
-
}
|
|
712
|
-
else if (offsetOnly) {
|
|
713
|
-
dx = this.Left * ctx.Scale;
|
|
714
|
-
dy = this.Top * ctx.Scale;
|
|
715
|
-
this.RenderTransformMatrix = Super.CK.Matrix.translated(dx, dy); // gestures / accessibility still map through it
|
|
716
|
-
}
|
|
717
|
-
else {
|
|
718
|
-
this.RenderTransformMatrix = undefined;
|
|
719
|
-
}
|
|
720
737
|
if (this.IsClippedToBounds) {
|
|
721
738
|
if (!saved) {
|
|
722
739
|
canvas.save();
|
|
@@ -781,10 +798,7 @@ export class SkiaControl {
|
|
|
781
798
|
const epoch = this.Superview?.GpuEpoch ?? 0;
|
|
782
799
|
if (this.RenderObjectPrevious && this.RenderObjectPrevious.Epoch !== epoch)
|
|
783
800
|
this.DisposePrevious(); // made on a lost GPU context
|
|
784
|
-
|
|
785
|
-
|| this.RenderObject.Scale !== ctx.Scale
|
|
786
|
-
|| Math.round(this.RenderObject.Bounds.Width) !== Math.round(r.Width) || Math.round(this.RenderObject.Bounds.Height) !== Math.round(r.Height);
|
|
787
|
-
if (stale)
|
|
801
|
+
if (this.CacheStale(ctx.Scale))
|
|
788
802
|
this.CreateRenderingObject({ ...ctx, Destination: this.DrawingRect }, cacheType);
|
|
789
803
|
if (this.RenderObject) {
|
|
790
804
|
// C# DrawRenderObject: with post renderers an Image cache is not blitted, the effects sample it (CachedImage)
|
|
@@ -982,13 +996,8 @@ export class SkiaControl {
|
|
|
982
996
|
this.DirtyChildrenInternal.add(c);
|
|
983
997
|
for (const c of inside)
|
|
984
998
|
this.DirtyChildrenInternal.add(c);
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
this.PaintContent({ ...ctx, Context: { Canvas: canvas, Surface: surface, Origin: { X: r.Left, Y: r.Top } } });
|
|
988
|
-
}
|
|
989
|
-
finally {
|
|
990
|
-
SkiaControl.CompositionCulling--;
|
|
991
|
-
}
|
|
999
|
+
// children entirely outside the redrawn areas are skipped by their own clip check (Render)
|
|
1000
|
+
this.PaintContent({ ...ctx, Context: { Canvas: canvas, Surface: surface, Origin: { X: r.Left, Y: r.Top } } });
|
|
992
1001
|
this.IsRenderingWithComposition = false;
|
|
993
1002
|
const redrawn = children.filter((c) => this.DirtyChildrenInternal.has(c));
|
|
994
1003
|
this.LastCompositeRecord = { Mode: "partial", Children: redrawn.length, Dirty: redrawn, Areas: areas, Changed: changed };
|
|
@@ -1257,8 +1266,24 @@ export class SkiaControl {
|
|
|
1257
1266
|
}
|
|
1258
1267
|
/** Redraw without remeasure and WITHOUT dropping caches (position/overlay changes). */
|
|
1259
1268
|
Repaint() {
|
|
1260
|
-
this.
|
|
1269
|
+
if (!this.UnderHiddenParent())
|
|
1270
|
+
this.Superview?.Update();
|
|
1261
1271
|
}
|
|
1272
|
+
/**
|
|
1273
|
+
* A parent (not the control itself) is hidden: a change here asks for no frame and the latest state shows when the
|
|
1274
|
+
* parent is shown again (C# 329f6c44 HiddenUpdateTests, DrawnUi.Rust hidden_updates). The control's own visibility
|
|
1275
|
+
* change still draws: it is the one that hides or shows it.
|
|
1276
|
+
*/
|
|
1277
|
+
UnderHiddenParent() {
|
|
1278
|
+
for (let p = this.Parent; p; p = p.Parent)
|
|
1279
|
+
if (!p.IsVisible)
|
|
1280
|
+
return true;
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
/** The control or a parent is hidden: its animators pause until it shows again. */
|
|
1284
|
+
HiddenInTree() { return !this.IsVisible || this.UnderHiddenParent(); }
|
|
1285
|
+
/** The control an invalidation started at while it climbs (InvalidateMeasure): a hidden parent of it wakes no frame. */
|
|
1286
|
+
static invalidateOrigin = null;
|
|
1262
1287
|
/**
|
|
1263
1288
|
* Transform / Opacity changed: this control's own cache is still valid (content unchanged) but every ancestor
|
|
1264
1289
|
* cache holds its composited output, so those are staled before redrawing (DrawnUi RedrawCanvas + parent invalidation).
|
|
@@ -1356,12 +1381,19 @@ export class SkiaControl {
|
|
|
1356
1381
|
if (SkiaControl.drawOrigin === null || SkiaControl.drawOrigin === this)
|
|
1357
1382
|
this.compositeFull = true;
|
|
1358
1383
|
this.effectsMarginCache = undefined;
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
this.Parent.
|
|
1363
|
-
|
|
1364
|
-
this.
|
|
1384
|
+
const outer = SkiaControl.invalidateOrigin;
|
|
1385
|
+
SkiaControl.invalidateOrigin ??= this;
|
|
1386
|
+
try {
|
|
1387
|
+
if (this.Parent && this.IsParentIndependent)
|
|
1388
|
+
this.RepaintComposition();
|
|
1389
|
+
else if (this.Parent)
|
|
1390
|
+
this.Parent.InvalidateMeasure();
|
|
1391
|
+
else if (!SkiaControl.invalidateOrigin.UnderHiddenParent())
|
|
1392
|
+
this.Superview?.Update();
|
|
1393
|
+
}
|
|
1394
|
+
finally {
|
|
1395
|
+
SkiaControl.invalidateOrigin = outer;
|
|
1396
|
+
}
|
|
1365
1397
|
}
|
|
1366
1398
|
// ---- gestures (same names as DrawnUi) ----
|
|
1367
1399
|
/** Hit rect in pixels (no transforms in this port, so it is DrawingRect). */
|
package/dist/react/SkiaShell.js
CHANGED
|
@@ -389,6 +389,12 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
|
|
|
389
389
|
prev ? prev.TranslateToAsync(-dir * w, 0, TabsAnimationSpeed, TabsEasing) : Promise.resolve(),
|
|
390
390
|
]), TabsAnimationSpeed + 500);
|
|
391
391
|
}
|
|
392
|
+
// the tab left stays mounted (its state kept): hidden first, then back in place for its next turn
|
|
393
|
+
if (prev) {
|
|
394
|
+
prev.IsVisible = false;
|
|
395
|
+
prev.TranslationX = 0;
|
|
396
|
+
prev.TranslationY = 0;
|
|
397
|
+
}
|
|
392
398
|
setTabTransition(null);
|
|
393
399
|
}, [hasTabs, Tabs, AnimateTabs, TabsAnimationSpeed]);
|
|
394
400
|
// popstate: the browser popped OUR last entry (back) — or moved forward to a hash we can rebuild
|
|
@@ -405,7 +411,13 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
|
|
|
405
411
|
}
|
|
406
412
|
const onPop = () => {
|
|
407
413
|
const depth = window.history.state?.[HISTORY_KEY] ?? 0;
|
|
408
|
-
|
|
414
|
+
const target = parseHash();
|
|
415
|
+
// a Back lands on one of OUR entries: its hash is the stack it was pushed with (the first N pages, N = the page
|
|
416
|
+
// entries below it). A typed hash or a plain <a href="#/route"> makes a new entry with no state (depth 0):
|
|
417
|
+
// that is a navigation to the hash, not a Back to the root
|
|
418
|
+
const pagesBelow = history.current.slice(0, depth).filter((k) => k === "page").length;
|
|
419
|
+
const back = depth < history.current.length && target.join("/") === live.current.stack.slice(0, pagesBelow).join("/");
|
|
420
|
+
if (back) {
|
|
409
421
|
// back: unwind our entries down to depth
|
|
410
422
|
void (async () => {
|
|
411
423
|
while (history.current.length > depth) {
|
|
@@ -441,9 +453,8 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
|
|
|
441
453
|
})();
|
|
442
454
|
}
|
|
443
455
|
else {
|
|
444
|
-
// forward (depth grew), or
|
|
456
|
+
// forward (depth grew), or another hash = a plain <a href="#/route"> link or a typed hash:
|
|
445
457
|
// the hash names the pages, rebuild the stack without animation (overlays cannot be restored)
|
|
446
|
-
const target = parseHash();
|
|
447
458
|
if (depth === history.current.length && target.join("/") === live.current.stack.join("/"))
|
|
448
459
|
return;
|
|
449
460
|
history.current = target.map(() => "page");
|
|
@@ -466,28 +477,36 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
|
|
|
466
477
|
const transitioning = transitions > 0 || leaving !== null;
|
|
467
478
|
const rootBottom = hasTabs ? TabBarHeight + insets.Bottom : 0;
|
|
468
479
|
const navTop = NavBarHeight + insets.Top;
|
|
469
|
-
|
|
480
|
+
// every tab keeps its root and its pages mounted (state, scroll positions), only the selected one shows (C# tab
|
|
481
|
+
// views, DrawnUi.Rust "tabs keep their stacks")
|
|
482
|
+
const tabIndexes = hasTabs ? Tabs.map((_, i) => i) : [0];
|
|
470
483
|
const entering = tabTransition ? { TranslationX: tabTransition.dir * 0.75 * (typeof window !== "undefined" ? window.innerWidth : 0), Opacity: 0.001, ZIndex: 1 } : {};
|
|
471
|
-
return (_jsx(ShellContext.Provider, { value: nav, children: _jsxs(SkiaLayer, { VerticalOptions: "Fill", children: [
|
|
472
|
-
|
|
473
|
-
|
|
484
|
+
return (_jsx(ShellContext.Provider, { value: nav, children: _jsxs(SkiaLayer, { VerticalOptions: "Fill", children: [tabIndexes.map((t) => {
|
|
485
|
+
const selected = t === selectedTab;
|
|
486
|
+
return (_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, 0, 0, rootBottom), BackgroundColor: hasTabs ? PageBackgroundColor : undefined, IsVisible: selected ? visiblePages.length === 0 || transitioning : tabTransition?.from === t, ZIndex: 0, ...(selected ? entering : {}), ref: (c) => { if (c)
|
|
487
|
+
tabRoots.current.set(t, c); }, children: hasTabs ? RenderRoute(Tabs[t]?.route ?? "") : children }, hasTabs ? `tab-${t}` : "root"));
|
|
488
|
+
}), tabIndexes.flatMap((t) => (t === selectedTab ? visiblePages : stacks[t] ?? []).map((r, i) => (_jsxs(PageHost, { route: r, speed: PagesAnimationSpeed, active: t === selectedTab, visible: t === selectedTab && (r === topRoute || transitioning), background: PageBackgroundColor, bottom: rootBottom, register: (c) => { if (c)
|
|
474
489
|
pageCtrls.current.set(r, c);
|
|
475
490
|
else
|
|
476
|
-
pageCtrls.current.delete(r); }, children: [_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, navTop, 0, 0), children: RenderRoute(r) }), _jsxs(SkiaLayer, { HeightRequest: navTop, BackgroundColor: NavBarColor, Padding: new Thickness(0, insets.Top, 0, 0), children: [_jsx(SkiaButton, { Text: "\u2039 Back", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, VerticalOptions: "Center", Margin: new Thickness(8, 0), ApplyEffect: "Ripple", Tapped: () => void GoBackAsync(), AccessibilityRole: "button", AccessibilityLabel: "Back" }), _jsx(SkiaLabel, { Text: Titles?.[RouteName(r)] ?? RouteName(r), FontSize: 18, FontFamily: "FontTextBold", TextColor: Colors.White, HorizontalOptions: "Fill", HorizontalTextAlignment: "Center", VerticalOptions: "Center", MaxLines: 1, Margin: new Thickness(96, 0), AccessibilityRole: "heading" }), _jsx(SkiaButton, { Text: "Home", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, HorizontalOptions: "End", VerticalOptions: "Center", Margin: new Thickness(0, 0, 8, 0), ApplyEffect: "Ripple", Tapped: () => void PopToRootAsync(), AccessibilityRole: "button", AccessibilityLabel: "Home" }), _jsx(SkiaLayer, { HeightRequest: 1, VerticalOptions: "End", BackgroundColor: "#343A40" })] })] }, `${
|
|
491
|
+
pageCtrls.current.delete(r); }, children: [_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, navTop, 0, 0), children: RenderRoute(r) }), _jsxs(SkiaLayer, { HeightRequest: navTop, BackgroundColor: NavBarColor, Padding: new Thickness(0, insets.Top, 0, 0), children: [_jsx(SkiaButton, { Text: "\u2039 Back", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, VerticalOptions: "Center", Margin: new Thickness(8, 0), ApplyEffect: "Ripple", Tapped: () => void GoBackAsync(), AccessibilityRole: "button", AccessibilityLabel: "Back" }), _jsx(SkiaLabel, { Text: Titles?.[RouteName(r)] ?? RouteName(r), FontSize: 18, FontFamily: "FontTextBold", TextColor: Colors.White, HorizontalOptions: "Fill", HorizontalTextAlignment: "Center", VerticalOptions: "Center", MaxLines: 1, Margin: new Thickness(96, 0), AccessibilityRole: "heading" }), _jsx(SkiaButton, { Text: "Home", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, HorizontalOptions: "End", VerticalOptions: "Center", Margin: new Thickness(0, 0, 8, 0), ApplyEffect: "Ripple", Tapped: () => void PopToRootAsync(), AccessibilityRole: "button", AccessibilityLabel: "Home" }), _jsx(SkiaLayer, { HeightRequest: 1, VerticalOptions: "End", BackgroundColor: "#343A40" })] })] }, `${t}:${r}#${i}`)))), hasTabs && (_jsxs(SkiaLayer, { HeightRequest: TabBarHeight + insets.Bottom, VerticalOptions: "End", BackgroundColor: TabBarColor, ZIndex: ShellDefaults.ZIndexModals - 1, BlockGesturesBelow: true, Padding: new Thickness(0, 0, 0, insets.Bottom), children: [_jsx(SkiaLayer, { HeightRequest: 1, BackgroundColor: "#343A40" }), _jsx(SkiaGrid, { ColumnDefinitions: Tabs.map(() => "*").join(","), HorizontalOptions: "Fill", VerticalOptions: "Fill", children: Tabs.map((t, i) => (_jsxs(SkiaLayer, { Column: i, HorizontalOptions: "Fill", VerticalOptions: "Fill", AnimationTapped: "Ripple", Tapped: () => void SelectTabAsync(i), AccessibilityRole: "button", AccessibilityLabel: t.title, children: [_jsx(SkiaLabel, { Text: t.title, FontSize: 13, FontFamily: i === selectedTab ? "FontTextBold" : undefined, TextColor: i === selectedTab ? ShellDefaults.TabSelectedColor : ShellDefaults.TabColor, HorizontalOptions: "Center", VerticalOptions: "Center", AccessibilityRole: "presentation" }), i === selectedTab && _jsx(SkiaShape, { Type: "Rectangle", HeightRequest: 3, WidthRequest: 36, CornerRadius: 2, BackgroundColor: ShellDefaults.TabSelectedColor, HorizontalOptions: "Center", VerticalOptions: "End" })] }, t.route))) })] })), modals.map((m) => m.node), popups.map((p) => p.node), toasts.map((t) => t.node)] }) }));
|
|
477
492
|
});
|
|
478
493
|
/** A pushed page: slides in from the right on mount (C# SkiaViewSwitcher PushView), covers what is below. */
|
|
479
|
-
function PageHost({ route, speed, visible, background, bottom, register, children }) {
|
|
494
|
+
function PageHost({ route, speed, active, visible, background, bottom, register, children }) {
|
|
480
495
|
const ctrl = useRef(null);
|
|
496
|
+
// slides in once, when pushed; a page of another tab stays mounted and is registered only while its tab is selected
|
|
481
497
|
useEffect(() => {
|
|
482
498
|
const c = ctrl.current;
|
|
483
|
-
if (
|
|
484
|
-
return;
|
|
485
|
-
register(c);
|
|
486
|
-
if (speed > 0) {
|
|
499
|
+
if (c && speed > 0) {
|
|
487
500
|
c.TranslationX = CanvasWidthPts(c);
|
|
488
501
|
void c.TranslateToAsync(0, 0, speed);
|
|
489
502
|
}
|
|
490
|
-
return () => register(null);
|
|
491
503
|
}, [route]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
const c = ctrl.current;
|
|
506
|
+
if (!c || !active)
|
|
507
|
+
return;
|
|
508
|
+
register(c);
|
|
509
|
+
return () => register(null);
|
|
510
|
+
}, [route, active]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
492
511
|
return (_jsx(SkiaLayer, { ref: (c) => { ctrl.current = c; }, VerticalOptions: "Fill", HorizontalOptions: "Fill", Margin: new Thickness(0, 0, 0, bottom), BackgroundColor: background, IsVisible: visible, BlockGesturesBelow: true, children: children }));
|
|
493
512
|
}
|
package/dist/react/index.js
CHANGED
|
@@ -126,6 +126,13 @@ const NAVIGATION_KEYS = new Set(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDow
|
|
|
126
126
|
* canvas. Keyboard and screen-reader input arrive as DOM events: Enter / Space activate (a Tapped), the navigation keys
|
|
127
127
|
* go to the focused control and then to its arrow-key group; a group is one Tab stop (roving tabindex).
|
|
128
128
|
*/
|
|
129
|
+
/** aria-valuenow / min / max / valuetext of a range control (drawnui-cross 6c); a slider reads as horizontal. */
|
|
130
|
+
function valueAttributes(n) {
|
|
131
|
+
const v = n.Value;
|
|
132
|
+
if (!v)
|
|
133
|
+
return undefined;
|
|
134
|
+
return { "aria-valuenow": v.Now, "aria-valuemin": v.Min, "aria-valuemax": v.Max, "aria-valuetext": v.Text || undefined, "aria-orientation": n.Role === "slider" ? "horizontal" : undefined };
|
|
135
|
+
}
|
|
129
136
|
function AccessibilityOverlay({ view }) {
|
|
130
137
|
const [nodes, setNodes] = useState(() => view.AccessibilityManager.Snapshot);
|
|
131
138
|
// keyboard focus moved by the manager (arrow keys in a group): focus that node's element once it is rendered
|
|
@@ -147,6 +154,23 @@ function AccessibilityOverlay({ view }) {
|
|
|
147
154
|
setFocusRequest(undefined);
|
|
148
155
|
elementOf(focusRequest.id)?.focus({ preventScroll: true });
|
|
149
156
|
}, [focusRequest]);
|
|
157
|
+
// the focused node went away (its page or popup closed): the focus goes to the first Tab stop on screen in reading
|
|
158
|
+
// order instead of falling back to the page (drawnui-cross 6c refocus); a focus the user moved away stays away
|
|
159
|
+
const focusedId = useRef(0);
|
|
160
|
+
useLayoutEffect(() => {
|
|
161
|
+
const id = focusedId.current;
|
|
162
|
+
if (!id || nodes.some((n) => n.Id === id))
|
|
163
|
+
return;
|
|
164
|
+
focusedId.current = 0;
|
|
165
|
+
const active = document.activeElement;
|
|
166
|
+
if (active && active !== document.body)
|
|
167
|
+
return;
|
|
168
|
+
const box = overlayRef.current?.getBoundingClientRect();
|
|
169
|
+
const first = nodes.find((n) => n.CanInteract && view.AccessibilityManager.IsTabStop(n.Source)
|
|
170
|
+
&& (!box || (n.Rect.Bottom > 0 && n.Rect.Top < box.height && n.Rect.Right > 0 && n.Rect.Left < box.width)));
|
|
171
|
+
if (first)
|
|
172
|
+
elementOf(first.Id)?.focus({ preventScroll: true });
|
|
173
|
+
}, [nodes]);
|
|
150
174
|
// Tab / Shift+Tab in a text field leave it for the next node, also after a click into it: DOM focus goes to the
|
|
151
175
|
// field's own element (without taking the caret again) and the browser's Tab moves on from there
|
|
152
176
|
const tabbingOut = useRef(0);
|
|
@@ -198,7 +222,7 @@ function AccessibilityOverlay({ view }) {
|
|
|
198
222
|
return (_jsxs("div", { className: "drawnui-a11y-overlay", onScroll: pin, ref: overlayRef, children: [_jsx("style", { children: A11Y_CSS }), nodes.map((n) => {
|
|
199
223
|
const pos = { left: n.Rect.Left, top: n.Rect.Top, width: n.Rect.Width, height: n.Rect.Height };
|
|
200
224
|
const activate = () => SkiaAccessibilityManager.Activate(n.Source);
|
|
201
|
-
return n.CanInteract ? (_jsx("div", { "data-a11y-id": n.Id, role: n.Role, "aria-label": n.Label, title: n.Hint, "aria-pressed": n.Role === "button" ? n.IsPressed : undefined, "aria-checked": n.Role === "switch" || n.Role === "checkbox" || n.Role === "radio" ? n.IsPressed : undefined, "aria-live": n.Live, tabIndex: view.AccessibilityManager.IsTabStop(n.Source) ? 0 : -1, className: "drawnui-a11y-node", style: { ...pos, fontSize: 0, userSelect: "none" }, onClick: activate, onKeyDown: (e) => {
|
|
225
|
+
return n.CanInteract ? (_jsx("div", { "data-a11y-id": n.Id, role: n.Role, "aria-label": n.Label, title: n.Hint, ...valueAttributes(n), "aria-pressed": n.Role === "button" ? n.IsPressed : undefined, "aria-checked": n.Role === "switch" || n.Role === "checkbox" || n.Role === "radio" ? n.IsPressed : undefined, "aria-live": n.Live, tabIndex: view.AccessibilityManager.IsTabStop(n.Source) ? 0 : -1, className: "drawnui-a11y-node", style: { ...pos, fontSize: 0, userSelect: "none" }, onClick: activate, onKeyDown: (e) => {
|
|
202
226
|
if (e.key === "Enter" || e.key === " ") {
|
|
203
227
|
e.preventDefault();
|
|
204
228
|
activate();
|
|
@@ -206,6 +230,7 @@ function AccessibilityOverlay({ view }) {
|
|
|
206
230
|
else if (NAVIGATION_KEYS.has(e.key) && SkiaAccessibilityManager.Key(n.Source, e.key))
|
|
207
231
|
e.preventDefault();
|
|
208
232
|
}, onFocus: (e) => {
|
|
233
|
+
focusedId.current = n.Id;
|
|
209
234
|
pin({ currentTarget: e.currentTarget.parentElement });
|
|
210
235
|
SkiaScrollCtrl.EnsureVisible(n.Source); // inside every enclosing scroll
|
|
211
236
|
if (tabbingOut.current === n.Id)
|
|
@@ -215,6 +240,8 @@ function AccessibilityOverlay({ view }) {
|
|
|
215
240
|
n.Source.NotifyAccessibilityFocused(true);
|
|
216
241
|
setFocusTick((t) => t + 1);
|
|
217
242
|
}, onBlur: (e) => {
|
|
243
|
+
if (e.currentTarget.isConnected && focusedId.current === n.Id)
|
|
244
|
+
focusedId.current = 0; // moved away on purpose
|
|
218
245
|
if (TextInputProxy.IsProxyTarget(e.relatedTarget))
|
|
219
246
|
return; // a text field moved DOM focus to its own input
|
|
220
247
|
n.Source.OnAccessibilityFocused(false);
|
|
@@ -223,7 +250,7 @@ function AccessibilityOverlay({ view }) {
|
|
|
223
250
|
// AccessibilityTextSelectable: one positioned span per drawn line, in the drawn font, so the browser selects and copies it like HTML
|
|
224
251
|
_jsx("div", { role: n.Role, title: n.Hint, "aria-disabled": n.Disabled || undefined, "aria-live": n.Live, className: "drawnui-a11y-node drawnui-a11y-text", style: pos, children: n.TextLines.map((l, i) => (_jsxs("span", { "data-w": l.Width, style: { left: l.Left, top: l.Top, height: l.Height, fontFamily: l.FontFamily, fontWeight: l.FontWeight, fontSize: l.FontSize, lineHeight: `${l.Height}px` }, children: [l.Text, i < n.TextLines.length - 1 ? "\n" : ""] }, i))) }, n.Id)) : (
|
|
225
252
|
// static text is exposed as real (transparent) text content; aria-label only for roles that need a name
|
|
226
|
-
_jsx("div", { role: n.Role, "aria-label": n.Role === "text" ? undefined : n.Label, title: n.Hint, "aria-disabled": n.Disabled || undefined, "aria-live": n.Live, className: "drawnui-a11y-node", style: pos, children: n.Label }, n.Id));
|
|
253
|
+
_jsx("div", { role: n.Role, "aria-label": n.Role === "text" ? undefined : n.Label, title: n.Hint, ...valueAttributes(n), "aria-disabled": n.Disabled || undefined, "aria-live": n.Live, className: "drawnui-a11y-node", style: pos, children: n.Label }, n.Id));
|
|
227
254
|
})] }));
|
|
228
255
|
}
|
|
229
256
|
// One import for apps: React tags + every engine type (Colors, Thickness, Super, gestures, animators...).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drawnui-react",
|
|
3
|
-
"version": "0.1.0-preview.
|
|
3
|
+
"version": "0.1.0-preview.17",
|
|
4
4
|
"description": "DrawnUi for React: the DrawnUi (.NET) drawn-UI engine in TypeScript on CanvasKit (Skia WASM). Same controls, same PascalCase API as DrawnUi.Net.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nick Kovalsky (AppoMobi)",
|
|
@@ -59,6 +59,20 @@
|
|
|
59
59
|
"check:children": "node dev/check.mjs dev/check-children.tsx",
|
|
60
60
|
"check:composite": "node dev/check.mjs dev/check-composite.tsx",
|
|
61
61
|
"check:hover": "node dev/check.mjs dev/check-hover.ts",
|
|
62
|
+
"check:grid-label": "node dev/check.mjs dev/check-grid-label.ts",
|
|
63
|
+
"check:snap": "node dev/check.mjs dev/check-snap.ts",
|
|
64
|
+
"check:game": "node dev/check.mjs dev/check-game.ts",
|
|
65
|
+
"check:animation": "node dev/check.mjs dev/check-animation.ts",
|
|
66
|
+
"check:shape": "node dev/check.mjs dev/check-shape.ts",
|
|
67
|
+
"check:label": "node dev/check.mjs dev/check-label.ts",
|
|
68
|
+
"check:image": "node dev/check.mjs dev/check-image.ts",
|
|
69
|
+
"check:longpress": "node dev/check.mjs dev/check-longpress.ts",
|
|
70
|
+
"check:hidden": "node dev/check.mjs dev/check-hidden.ts",
|
|
71
|
+
"check:shell": "node dev/check.mjs dev/check-shell.tsx",
|
|
72
|
+
"check:wheel-routing": "node dev/check.mjs dev/check-wheel-routing.ts",
|
|
73
|
+
"check:velocity": "node dev/check.mjs dev/check-velocity.ts",
|
|
74
|
+
"check:scroll-index": "node dev/check.mjs dev/check-scroll-index.ts",
|
|
75
|
+
"check:culling": "node dev/check.mjs dev/check-culling.ts",
|
|
62
76
|
"site:extras": "node dev/site-extras.mjs dist/demo",
|
|
63
77
|
"build:lib": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
|
64
78
|
"prepublishOnly": "npm run build:lib",
|