drawnui-react 0.1.0-preview.16 → 0.1.0-preview.18

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.
Files changed (55) hide show
  1. package/README.md +1 -1
  2. package/SKIPPED.md +7 -7
  3. package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
  4. package/dist/controls/AnimatedFramesRenderer.js +16 -1
  5. package/dist/controls/DrawnGame.d.ts +4 -1
  6. package/dist/controls/DrawnGame.js +7 -3
  7. package/dist/controls/GridStructure.d.ts +13 -1
  8. package/dist/controls/GridStructure.js +38 -2
  9. package/dist/controls/SkiaButton.d.ts +12 -2
  10. package/dist/controls/SkiaButton.js +19 -6
  11. package/dist/controls/SkiaCarousel.d.ts +1 -1
  12. package/dist/controls/SkiaCarousel.js +3 -2
  13. package/dist/controls/SkiaEditor.d.ts +5 -1
  14. package/dist/controls/SkiaEditor.js +19 -17
  15. package/dist/controls/SkiaGif.d.ts +3 -0
  16. package/dist/controls/SkiaGif.js +11 -1
  17. package/dist/controls/SkiaImage.js +15 -10
  18. package/dist/controls/SkiaLabel.d.ts +52 -3
  19. package/dist/controls/SkiaLabel.js +222 -50
  20. package/dist/controls/SkiaLayout.d.ts +5 -0
  21. package/dist/controls/SkiaLayout.js +7 -4
  22. package/dist/controls/SkiaLottie.js +3 -1
  23. package/dist/controls/SkiaProgress.d.ts +2 -1
  24. package/dist/controls/SkiaProgress.js +4 -1
  25. package/dist/controls/SkiaRadioButton.js +3 -1
  26. package/dist/controls/SkiaScroll.d.ts +17 -1
  27. package/dist/controls/SkiaScroll.js +84 -9
  28. package/dist/controls/SkiaShape.d.ts +13 -2
  29. package/dist/controls/SkiaShape.js +50 -10
  30. package/dist/controls/SkiaSlider.d.ts +10 -2
  31. package/dist/controls/SkiaSlider.js +15 -2
  32. package/dist/controls/SkiaSprite.d.ts +1 -0
  33. package/dist/controls/SkiaSprite.js +4 -0
  34. package/dist/controls/SkiaToggle.d.ts +5 -1
  35. package/dist/controls/SkiaToggle.js +11 -2
  36. package/dist/controls/SnappingLayout.d.ts +10 -1
  37. package/dist/controls/SnappingLayout.js +16 -1
  38. package/dist/core/Accessibility.d.ts +13 -0
  39. package/dist/core/Accessibility.js +10 -4
  40. package/dist/core/Animators.d.ts +28 -1
  41. package/dist/core/Animators.js +76 -8
  42. package/dist/core/Canvas.d.ts +21 -4
  43. package/dist/core/Canvas.js +122 -23
  44. package/dist/core/Easing.d.ts +12 -1
  45. package/dist/core/Easing.js +25 -1
  46. package/dist/core/Gestures.d.ts +24 -1
  47. package/dist/core/Gestures.js +75 -2
  48. package/dist/core/Hover.d.ts +14 -4
  49. package/dist/core/Hover.js +38 -22
  50. package/dist/core/ScrollAnimators.js +9 -0
  51. package/dist/core/SkiaControl.d.ts +27 -6
  52. package/dist/core/SkiaControl.js +81 -38
  53. package/dist/react/SkiaShell.js +33 -14
  54. package/dist/react/index.js +29 -2
  55. package/package.json +16 -1
@@ -13,15 +13,23 @@ export declare class HoverManager {
13
13
  /** The hovered controls, root first. */
14
14
  readonly Hovered: SkiaControl[];
15
15
  private readonly next;
16
- private readonly path;
16
+ /** The controls under the pointer at the last hit test, root first, and the pointer in each one's own space. */
17
+ readonly Path: SkiaControl[];
18
+ readonly PathX: number[];
19
+ readonly PathY: number[];
17
20
  private readonly movers;
18
21
  private hasPointer;
19
22
  private x;
20
23
  private y;
21
24
  private paused;
22
25
  private check;
23
- /** The mouse (or a pen in range) moved to (x, y), canvas pixels, nothing pressed. */
26
+ /**
27
+ * The mouse (or a pen in range) moved to (x, y), canvas pixels, nothing pressed. The hit path is taken also while
28
+ * hover waits (the cursor follows it), hover changes only when nothing moves.
29
+ */
24
30
  Move(root: SkiaControl | undefined, x: number, y: number): void;
31
+ /** Fills Path with the controls under the pointer by the routing rules of ProcessGestures. */
32
+ private HitTest;
25
33
  /** The pointer left the canvas: nothing is hovered, also while content moves. */
26
34
  Leave(): void;
27
35
  /** The controls under a still pointer may have changed: checked once at the end of the frame. */
@@ -31,7 +39,9 @@ export declare class HoverManager {
31
39
  /** End of a frame: pause while content moves, else run a pending check (at most one a frame, none while pressed). */
32
40
  AfterFrame(root: SkiaControl | undefined, pressed: boolean): void;
33
41
  private static Shown;
34
- private Update;
35
- /** `next` becomes the hovered set: the ones that left get HoverChanged(false), then the new ones (true). */
42
+ /**
43
+ * The hovered set becomes the controls of the hit path that take hover (`fromPath`), or none: the ones that left get
44
+ * HoverChanged(false), then the new ones (true).
45
+ */
36
46
  private Apply;
37
47
  }
@@ -13,29 +13,50 @@ export class HoverManager {
13
13
  /** The hovered controls, root first. */
14
14
  Hovered = [];
15
15
  next = [];
16
- path = [];
16
+ /** The controls under the pointer at the last hit test, root first, and the pointer in each one's own space. */
17
+ Path = [];
18
+ PathX = [];
19
+ PathY = [];
17
20
  movers = [];
18
21
  hasPointer = false;
19
22
  x = 0;
20
23
  y = 0;
21
24
  paused = false;
22
25
  check = false;
23
- /** The mouse (or a pen in range) moved to (x, y), canvas pixels, nothing pressed. */
26
+ /**
27
+ * The mouse (or a pen in range) moved to (x, y), canvas pixels, nothing pressed. The hit path is taken also while
28
+ * hover waits (the cursor follows it), hover changes only when nothing moves.
29
+ */
24
30
  Move(root, x, y) {
25
31
  this.hasPointer = true;
26
32
  this.x = x;
27
33
  this.y = y;
34
+ this.HitTest(root);
28
35
  if (this.paused)
29
36
  return; // content moves under the pointer: what was hovered stays, one check when it stops
30
37
  this.check = false;
31
- this.Update(root);
38
+ this.Apply(true);
39
+ }
40
+ /** Fills Path with the controls under the pointer by the routing rules of ProcessGestures. */
41
+ HitTest(root) {
42
+ const path = this.Path;
43
+ path.length = 0;
44
+ this.PathX.length = 0;
45
+ this.PathY.length = 0;
46
+ if (root && root.IsVisible && !root.InputTransparent) {
47
+ const point = root.TransformPointToLocalSpace(new SKPoint(this.x, this.y));
48
+ if (root.HitIsInside(point.X, point.Y))
49
+ root.CollectHovered(point, path, this.PathX, this.PathY);
50
+ }
32
51
  }
33
52
  /** The pointer left the canvas: nothing is hovered, also while content moves. */
34
53
  Leave() {
35
54
  this.hasPointer = false;
36
55
  this.check = false;
37
- this.next.length = 0;
38
- this.Apply();
56
+ this.Path.length = 0;
57
+ this.PathX.length = 0;
58
+ this.PathY.length = 0;
59
+ this.Apply(false);
39
60
  }
40
61
  /** The controls under a still pointer may have changed: checked once at the end of the frame. */
41
62
  RequestCheck() { if (this.hasPointer)
@@ -72,7 +93,8 @@ export class HoverManager {
72
93
  this.check = false;
73
94
  if (!this.hasPointer || pressed)
74
95
  return;
75
- this.Update(root);
96
+ this.HitTest(root);
97
+ this.Apply(true);
76
98
  }
77
99
  static Shown(control) {
78
100
  if (control.IsDisposed || !control.Superview)
@@ -82,23 +104,17 @@ export class HoverManager {
82
104
  return false;
83
105
  return true;
84
106
  }
85
- Update(root) {
86
- const path = this.path, next = this.next;
87
- path.length = 0;
88
- next.length = 0;
89
- if (root && root.IsVisible && !root.InputTransparent) {
90
- const point = root.TransformPointToLocalSpace(new SKPoint(this.x, this.y));
91
- if (root.HitIsInside(point.X, point.Y))
92
- root.CollectHovered(point, path);
93
- }
94
- for (const c of path)
95
- if (c.ReceivesHover)
96
- next.push(c);
97
- this.Apply();
98
- }
99
- /** `next` becomes the hovered set: the ones that left get HoverChanged(false), then the new ones (true). */
100
- Apply() {
107
+ /**
108
+ * The hovered set becomes the controls of the hit path that take hover (`fromPath`), or none: the ones that left get
109
+ * HoverChanged(false), then the new ones (true).
110
+ */
111
+ Apply(fromPath) {
101
112
  const now = this.Hovered, next = this.next;
113
+ next.length = 0;
114
+ if (fromPath)
115
+ for (const c of this.Path)
116
+ if (c.ReceivesHover)
117
+ next.push(c);
102
118
  if (now.length === next.length) {
103
119
  let same = true;
104
120
  for (let i = 0; i < now.length && same; i++)
@@ -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
- /** During a partial composite record: true when this control draws nothing inside the canvas clip (skipped whole). */
177
- OutsideCompositionClip(canvas: SkCanvas): boolean;
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);
@@ -373,8 +375,17 @@ export declare class SkiaControl {
373
375
  * for the pointer. Keyboard navigation uses it: Tapped for Tab and Enter / Space, Panning for the arrow keys.
374
376
  */
375
377
  CanReceiveGesture(gesture: SkiaGesturesParameters["Type"]): boolean;
376
- /** False while the control ignores gestures itself, e.g. a disabled button. Default true. */
378
+ /**
379
+ * False while the control ignores input itself: RespondsToGestures = false on a button, toggle, slider, scroll...
380
+ * Taps, keyboard and the accessibility state (reported disabled) all follow it (drawnui-cross 6w). Default true.
381
+ */
377
382
  protected AcceptsInput(): boolean;
383
+ /**
384
+ * What a control that takes no input returns for a gesture that reached it (C# BlockedGestureResult): it keeps a
385
+ * press, tap or long press, so nothing under it reacts to them, and lets moves and wheel go on to the controls around
386
+ * it (a scroll behind it still scrolls).
387
+ */
388
+ protected BlockedGestureResult(args: SkiaGesturesParameters): SkiaControl | null;
378
389
  NotifyAccessibilityFocused(focused: boolean): void;
379
390
  /** DrawnUi SmartMax: larger of two sizes, an infinite one loses. */
380
391
  protected static SmartMax(a: number, b: number): number;
@@ -471,6 +482,16 @@ export declare class SkiaControl {
471
482
  Update(): void;
472
483
  /** Redraw without remeasure and WITHOUT dropping caches (position/overlay changes). */
473
484
  Repaint(): void;
485
+ /**
486
+ * A parent (not the control itself) is hidden: a change here asks for no frame and the latest state shows when the
487
+ * parent is shown again (C# 329f6c44 HiddenUpdateTests, DrawnUi.Rust hidden_updates). The control's own visibility
488
+ * change still draws: it is the one that hides or shows it.
489
+ */
490
+ UnderHiddenParent(): boolean;
491
+ /** The control or a parent is hidden: its animators pause until it shows again. */
492
+ HiddenInTree(): boolean;
493
+ /** The control an invalidation started at while it climbs (InvalidateMeasure): a hidden parent of it wakes no frame. */
494
+ private static invalidateOrigin;
474
495
  /**
475
496
  * Transform / Opacity changed: this control's own cache is still valid (content unchanged) but every ancestor
476
497
  * cache holds its composited output, so those are staled before redrawing (DrawnUi RedrawCanvas + parent invalidation).
@@ -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
- /** During a partial composite record: true when this control draws nothing inside the canvas clip (skipped whole). */
201
- OutsideCompositionClip(canvas) {
202
- const b = this.DrawnBoundsNow(this.RenderingScale), q = SkiaControl.rejectRect;
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) {
@@ -622,8 +633,19 @@ export class SkiaControl {
622
633
  return false;
623
634
  return true;
624
635
  }
625
- /** False while the control ignores gestures itself, e.g. a disabled button. Default true. */
636
+ /**
637
+ * False while the control ignores input itself: RespondsToGestures = false on a button, toggle, slider, scroll...
638
+ * Taps, keyboard and the accessibility state (reported disabled) all follow it (drawnui-cross 6w). Default true.
639
+ */
626
640
  AcceptsInput() { return true; }
641
+ /**
642
+ * What a control that takes no input returns for a gesture that reached it (C# BlockedGestureResult): it keeps a
643
+ * press, tap or long press, so nothing under it reacts to them, and lets moves and wheel go on to the controls around
644
+ * it (a scroll behind it still scrolls).
645
+ */
646
+ BlockedGestureResult(args) {
647
+ return args.Type === "Down" || args.Type === "Tapped" || args.Type === "LongPressing" ? this : null;
648
+ }
627
649
  NotifyAccessibilityFocused(focused) { this.Superview?.AccessibilityManager.NotifyFocused(focused ? this : undefined); }
628
650
  /** DrawnUi SmartMax: larger of two sizes, an infinite one loses. */
629
651
  static SmartMax(a, b) {
@@ -691,6 +713,23 @@ export class SkiaControl {
691
713
  const offsetOnly = (this.Left !== 0 || this.Top !== 0) && this.UsingCacheType !== "None" && this.TranslationX === 0 && this.TranslationY === 0
692
714
  && this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0;
693
715
  const needTransform = this.HasTransform && !offsetOnly;
716
+ let dx = 0, dy = 0;
717
+ if (needTransform) {
718
+ this.RenderTransformMatrix = this.CreateRenderTransformMatrix(this.DrawingRect, ctx.Scale);
719
+ }
720
+ else if (offsetOnly) {
721
+ dx = this.Left * ctx.Scale;
722
+ dy = this.Top * ctx.Scale;
723
+ this.RenderTransformMatrix = Super.CK.Matrix.translated(dx, dy); // gestures / accessibility still map through it
724
+ }
725
+ else {
726
+ this.RenderTransformMatrix = undefined;
727
+ }
728
+ // drawn entirely outside the clip: skipped with its subtree (C# layout Virtualisation, DrawnUi.Rust); the matrix
729
+ // above stays current for hit tests. A cache that must be recorded is still recorded here, as before, so its cost
730
+ // does not move into the frame where the control scrolls in (DrawnUi.Rust records it then)
731
+ if (this.OutsideClip(canvas, ctx.Scale) && !this.CacheStale(ctx.Scale))
732
+ return;
694
733
  let saved = false;
695
734
  // same as DrawnUi: opacity = a layer with alpha, transforms = canvas matrix around the whole subtree (cache included)
696
735
  if (applyOpacity) {
@@ -704,19 +743,8 @@ export class SkiaControl {
704
743
  canvas.save();
705
744
  saved = true;
706
745
  }
707
- let dx = 0, dy = 0;
708
- if (needTransform) {
709
- this.RenderTransformMatrix = this.CreateRenderTransformMatrix(this.DrawingRect, ctx.Scale);
746
+ if (needTransform)
710
747
  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
748
  if (this.IsClippedToBounds) {
721
749
  if (!saved) {
722
750
  canvas.save();
@@ -781,10 +809,7 @@ export class SkiaControl {
781
809
  const epoch = this.Superview?.GpuEpoch ?? 0;
782
810
  if (this.RenderObjectPrevious && this.RenderObjectPrevious.Epoch !== epoch)
783
811
  this.DisposePrevious(); // made on a lost GPU context
784
- const stale = this.cacheDirty || !this.RenderObject || this.RenderObject.Type !== cacheType || this.RenderObject.Epoch !== epoch
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)
812
+ if (this.CacheStale(ctx.Scale))
788
813
  this.CreateRenderingObject({ ...ctx, Destination: this.DrawingRect }, cacheType);
789
814
  if (this.RenderObject) {
790
815
  // C# DrawRenderObject: with post renderers an Image cache is not blitted, the effects sample it (CachedImage)
@@ -982,13 +1007,8 @@ export class SkiaControl {
982
1007
  this.DirtyChildrenInternal.add(c);
983
1008
  for (const c of inside)
984
1009
  this.DirtyChildrenInternal.add(c);
985
- SkiaControl.CompositionCulling++;
986
- try {
987
- this.PaintContent({ ...ctx, Context: { Canvas: canvas, Surface: surface, Origin: { X: r.Left, Y: r.Top } } });
988
- }
989
- finally {
990
- SkiaControl.CompositionCulling--;
991
- }
1010
+ // children entirely outside the redrawn areas are skipped by their own clip check (Render)
1011
+ this.PaintContent({ ...ctx, Context: { Canvas: canvas, Surface: surface, Origin: { X: r.Left, Y: r.Top } } });
992
1012
  this.IsRenderingWithComposition = false;
993
1013
  const redrawn = children.filter((c) => this.DirtyChildrenInternal.has(c));
994
1014
  this.LastCompositeRecord = { Mode: "partial", Children: redrawn.length, Dirty: redrawn, Areas: areas, Changed: changed };
@@ -1257,8 +1277,24 @@ export class SkiaControl {
1257
1277
  }
1258
1278
  /** Redraw without remeasure and WITHOUT dropping caches (position/overlay changes). */
1259
1279
  Repaint() {
1260
- this.Superview?.Update();
1280
+ if (!this.UnderHiddenParent())
1281
+ this.Superview?.Update();
1282
+ }
1283
+ /**
1284
+ * A parent (not the control itself) is hidden: a change here asks for no frame and the latest state shows when the
1285
+ * parent is shown again (C# 329f6c44 HiddenUpdateTests, DrawnUi.Rust hidden_updates). The control's own visibility
1286
+ * change still draws: it is the one that hides or shows it.
1287
+ */
1288
+ UnderHiddenParent() {
1289
+ for (let p = this.Parent; p; p = p.Parent)
1290
+ if (!p.IsVisible)
1291
+ return true;
1292
+ return false;
1261
1293
  }
1294
+ /** The control or a parent is hidden: its animators pause until it shows again. */
1295
+ HiddenInTree() { return !this.IsVisible || this.UnderHiddenParent(); }
1296
+ /** The control an invalidation started at while it climbs (InvalidateMeasure): a hidden parent of it wakes no frame. */
1297
+ static invalidateOrigin = null;
1262
1298
  /**
1263
1299
  * Transform / Opacity changed: this control's own cache is still valid (content unchanged) but every ancestor
1264
1300
  * cache holds its composited output, so those are staled before redrawing (DrawnUi RedrawCanvas + parent invalidation).
@@ -1356,12 +1392,19 @@ export class SkiaControl {
1356
1392
  if (SkiaControl.drawOrigin === null || SkiaControl.drawOrigin === this)
1357
1393
  this.compositeFull = true;
1358
1394
  this.effectsMarginCache = undefined;
1359
- if (this.Parent && this.IsParentIndependent)
1360
- this.RepaintComposition();
1361
- else if (this.Parent)
1362
- this.Parent.InvalidateMeasure();
1363
- else
1364
- this.Superview?.Update();
1395
+ const outer = SkiaControl.invalidateOrigin;
1396
+ SkiaControl.invalidateOrigin ??= this;
1397
+ try {
1398
+ if (this.Parent && this.IsParentIndependent)
1399
+ this.RepaintComposition();
1400
+ else if (this.Parent)
1401
+ this.Parent.InvalidateMeasure();
1402
+ else if (!SkiaControl.invalidateOrigin.UnderHiddenParent())
1403
+ this.Superview?.Update();
1404
+ }
1405
+ finally {
1406
+ SkiaControl.invalidateOrigin = outer;
1407
+ }
1365
1408
  }
1366
1409
  // ---- gestures (same names as DrawnUi) ----
1367
1410
  /** Hit rect in pixels (no transforms in this port, so it is DrawingRect). */
@@ -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
- if (depth < history.current.length) {
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 the same depth with another hash = a plain <a href="#/route"> link or a typed hash:
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
- const rootNode = hasTabs ? RenderRoute(Tabs[selectedTab]?.route ?? "") : children;
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: [tabTransition && (_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, 0, 0, rootBottom), ZIndex: 0, BackgroundColor: PageBackgroundColor, ref: (c) => { if (c)
472
- tabRoots.current.set(tabTransition.from, c); }, children: RenderRoute(Tabs[tabTransition.from]?.route ?? "") }, `tab-leaving-${tabTransition.from}`)), _jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, 0, 0, rootBottom), IsVisible: visiblePages.length === 0 || transitioning, BackgroundColor: hasTabs ? PageBackgroundColor : undefined, ...entering, ref: (c) => { if (c)
473
- tabRoots.current.set(selectedTab, c); }, children: rootNode }, hasTabs ? `tab-${selectedTab}` : "root"), visiblePages.map((r, i) => (_jsxs(PageHost, { route: r, speed: PagesAnimationSpeed, visible: r === topRoute || transitioning, background: PageBackgroundColor, bottom: rootBottom, register: (c) => { if (c)
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" })] })] }, `${selectedTab}:${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)] }) }));
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 (!c)
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
  }
@@ -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...).