drawnui-react 0.1.0-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/PARITY.md +287 -0
  3. package/README.md +92 -0
  4. package/SKIPPED.md +107 -0
  5. package/dist/controls/AnimatedFramesRenderer.d.ts +58 -0
  6. package/dist/controls/AnimatedFramesRenderer.js +133 -0
  7. package/dist/controls/GridStructure.d.ts +79 -0
  8. package/dist/controls/GridStructure.js +403 -0
  9. package/dist/controls/RefreshIndicator.d.ts +35 -0
  10. package/dist/controls/RefreshIndicator.js +82 -0
  11. package/dist/controls/SkiaBackdrop.d.ts +25 -0
  12. package/dist/controls/SkiaBackdrop.js +80 -0
  13. package/dist/controls/SkiaButton.d.ts +58 -0
  14. package/dist/controls/SkiaButton.js +148 -0
  15. package/dist/controls/SkiaCarousel.d.ts +133 -0
  16. package/dist/controls/SkiaCarousel.js +603 -0
  17. package/dist/controls/SkiaCheckbox.d.ts +21 -0
  18. package/dist/controls/SkiaCheckbox.js +118 -0
  19. package/dist/controls/SkiaDecoratedGrid.d.ts +15 -0
  20. package/dist/controls/SkiaDecoratedGrid.js +46 -0
  21. package/dist/controls/SkiaDrawer.d.ts +46 -0
  22. package/dist/controls/SkiaDrawer.js +212 -0
  23. package/dist/controls/SkiaDynamicDrawnCell.d.ts +11 -0
  24. package/dist/controls/SkiaDynamicDrawnCell.js +17 -0
  25. package/dist/controls/SkiaEditor.d.ts +163 -0
  26. package/dist/controls/SkiaEditor.js +803 -0
  27. package/dist/controls/SkiaGif.d.ts +54 -0
  28. package/dist/controls/SkiaGif.js +206 -0
  29. package/dist/controls/SkiaHotspot.d.ts +13 -0
  30. package/dist/controls/SkiaHotspot.js +36 -0
  31. package/dist/controls/SkiaImage.d.ts +75 -0
  32. package/dist/controls/SkiaImage.js +222 -0
  33. package/dist/controls/SkiaImageTiles.d.ts +27 -0
  34. package/dist/controls/SkiaImageTiles.js +76 -0
  35. package/dist/controls/SkiaLabel.d.ts +160 -0
  36. package/dist/controls/SkiaLabel.js +580 -0
  37. package/dist/controls/SkiaLayout.d.ts +163 -0
  38. package/dist/controls/SkiaLayout.js +723 -0
  39. package/dist/controls/SkiaLottie.d.ts +75 -0
  40. package/dist/controls/SkiaLottie.js +290 -0
  41. package/dist/controls/SkiaProgress.d.ts +40 -0
  42. package/dist/controls/SkiaProgress.js +80 -0
  43. package/dist/controls/SkiaRadioButton.d.ts +32 -0
  44. package/dist/controls/SkiaRadioButton.js +157 -0
  45. package/dist/controls/SkiaRichLabel.d.ts +77 -0
  46. package/dist/controls/SkiaRichLabel.js +274 -0
  47. package/dist/controls/SkiaScroll.d.ts +231 -0
  48. package/dist/controls/SkiaScroll.js +958 -0
  49. package/dist/controls/SkiaScrollBar.d.ts +56 -0
  50. package/dist/controls/SkiaScrollBar.js +158 -0
  51. package/dist/controls/SkiaShaderCarousel.d.ts +67 -0
  52. package/dist/controls/SkiaShaderCarousel.js +246 -0
  53. package/dist/controls/SkiaShape.d.ts +63 -0
  54. package/dist/controls/SkiaShape.js +317 -0
  55. package/dist/controls/SkiaSlider.d.ts +74 -0
  56. package/dist/controls/SkiaSlider.js +243 -0
  57. package/dist/controls/SkiaSprite.d.ts +99 -0
  58. package/dist/controls/SkiaSprite.js +324 -0
  59. package/dist/controls/SkiaSpriteSet.d.ts +27 -0
  60. package/dist/controls/SkiaSpriteSet.js +75 -0
  61. package/dist/controls/SkiaSvg.d.ts +43 -0
  62. package/dist/controls/SkiaSvg.js +181 -0
  63. package/dist/controls/SkiaSwitch.d.ts +26 -0
  64. package/dist/controls/SkiaSwitch.js +147 -0
  65. package/dist/controls/SkiaToggle.d.ts +50 -0
  66. package/dist/controls/SkiaToggle.js +71 -0
  67. package/dist/controls/SnappingLayout.d.ts +78 -0
  68. package/dist/controls/SnappingLayout.js +217 -0
  69. package/dist/controls/TextSpan.d.ts +70 -0
  70. package/dist/controls/TextSpan.js +76 -0
  71. package/dist/core/Accessibility.d.ts +104 -0
  72. package/dist/core/Accessibility.js +150 -0
  73. package/dist/core/Animators.d.ts +82 -0
  74. package/dist/core/Animators.js +247 -0
  75. package/dist/core/Canvas.d.ts +89 -0
  76. package/dist/core/Canvas.js +328 -0
  77. package/dist/core/ControlStyle.d.ts +6 -0
  78. package/dist/core/ControlStyle.js +11 -0
  79. package/dist/core/Easing.d.ts +13 -0
  80. package/dist/core/Easing.js +15 -0
  81. package/dist/core/Gestures.d.ts +74 -0
  82. package/dist/core/Gestures.js +104 -0
  83. package/dist/core/ImageEffects.d.ts +29 -0
  84. package/dist/core/ImageEffects.js +55 -0
  85. package/dist/core/KeyboardManager.d.ts +25 -0
  86. package/dist/core/KeyboardManager.js +54 -0
  87. package/dist/core/ScrollAnimators.d.ts +90 -0
  88. package/dist/core/ScrollAnimators.js +244 -0
  89. package/dist/core/SkiaControl.d.ts +401 -0
  90. package/dist/core/SkiaControl.js +1239 -0
  91. package/dist/core/SkiaEffect.d.ts +216 -0
  92. package/dist/core/SkiaEffect.js +571 -0
  93. package/dist/core/SkiaImageManager.d.ts +43 -0
  94. package/dist/core/SkiaImageManager.js +122 -0
  95. package/dist/core/Super.d.ts +61 -0
  96. package/dist/core/Super.js +178 -0
  97. package/dist/core/TextInputProxy.d.ts +31 -0
  98. package/dist/core/TextInputProxy.js +144 -0
  99. package/dist/core/Types.d.ts +177 -0
  100. package/dist/core/Types.js +122 -0
  101. package/dist/core/ViewsAdapter.d.ts +41 -0
  102. package/dist/core/ViewsAdapter.js +113 -0
  103. package/dist/index.d.ts +48 -0
  104. package/dist/index.js +48 -0
  105. package/dist/react/SkiaShell.d.ts +144 -0
  106. package/dist/react/SkiaShell.js +490 -0
  107. package/dist/react/index.d.ts +110 -0
  108. package/dist/react/index.js +158 -0
  109. package/dist/react/reconciler.d.ts +14 -0
  110. package/dist/react/reconciler.js +160 -0
  111. package/package.json +60 -0
@@ -0,0 +1,217 @@
1
+ import { SKPoint } from "../core/Gestures";
2
+ import { Easing } from "../core/Easing";
3
+ import { SkiaValueAnimator } from "../core/Animators";
4
+ import { Spring, SpringWithVelocityAnimator, RubberBandUtils, VelocityAccumulator } from "../core/ScrollAnimators";
5
+ import { SKRect } from "../core/Types";
6
+ import { SkiaLayout } from "./SkiaLayout";
7
+ /**
8
+ * Mirrors DrawnUi SnappingLayout: a layout whose content position snaps to `SnapPoints` (points). Base of
9
+ * SkiaCarousel and SkiaDrawer: nearest/next anchor selection by velocity, spring (Bounces) or linear snap
10
+ * animation, rubber-band clamping, `Scrolled` / `TransitionChanged` / `Stopped` callbacks.
11
+ */
12
+ export class SnappingLayout extends SkiaLayout {
13
+ Animated = true;
14
+ RespondsToGestures = true;
15
+ IgnoreWrongDirection = false;
16
+ Bounces = false;
17
+ RubberDamping = 0.7;
18
+ AutoVelocityMultiplyPts = 25;
19
+ RubberEffect = 0.15;
20
+ /** Fraction of a snap step a drag must cover before it counts as a direction (C# SnapDistanceRatio). */
21
+ SnapDistanceRatio = 0.2;
22
+ SnapPoints = [];
23
+ /** Current content offset, points (C# CurrentPosition). */
24
+ CurrentPosition = SKPoint.Empty;
25
+ /** Anchor the content rests on or travels to (C# CurrentSnap). */
26
+ CurrentSnap = new SKPoint(-1, -1);
27
+ ContentOffsetBounds = SKRect.Empty;
28
+ IsUserPanning = false;
29
+ IsUserFocused = false;
30
+ Scrolled;
31
+ TransitionChanged;
32
+ Stopped;
33
+ inTransition = false;
34
+ get InTransition() { return this.inTransition; }
35
+ set InTransition(v) { if (this.inTransition !== v) {
36
+ this.inTransition = v;
37
+ this.OnTransitionChanged();
38
+ } }
39
+ OnTransitionChanged() { this.TransitionChanged?.(this, this.inTransition); }
40
+ velocityAccumulator = new VelocityAccumulator();
41
+ springX = new SpringWithVelocityAnimator(this);
42
+ springY = new SpringWithVelocityAnimator(this);
43
+ range = new SkiaValueAnimator(this);
44
+ rangeFrom = SKPoint.Empty;
45
+ rangeTo = SKPoint.Empty;
46
+ /** Easing of the linear (non-bouncing) snap. */
47
+ SnapEasing = Easing.CubicInOut;
48
+ constructor() {
49
+ super();
50
+ this.Type = "Absolute";
51
+ this.springX.OnUpdated = (v) => this.ApplyPosition(new SKPoint(v, this.CurrentPosition.Y));
52
+ this.springY.OnUpdated = (v) => this.ApplyPosition(new SKPoint(this.CurrentPosition.X, v));
53
+ this.springX.OnStop = this.springY.OnStop = () => { if (!this.springX.IsRunning && !this.springY.IsRunning)
54
+ this.OnAnimationStopped(); };
55
+ this.range.mMinValue = 0;
56
+ this.range.mMaxValue = 1;
57
+ this.range.OnUpdated = (t) => this.ApplyPosition(new SKPoint(this.rangeFrom.X + (this.rangeTo.X - this.rangeFrom.X) * t, this.rangeFrom.Y + (this.rangeTo.Y - this.rangeFrom.Y) * t));
58
+ this.range.OnStop = () => this.OnAnimationStopped();
59
+ }
60
+ stoppingSilently = false;
61
+ OnAnimationStopped() {
62
+ if (this.stoppingSilently)
63
+ return; // interrupted / restarted: not a finished transition
64
+ this.InTransition = !this.CheckTransitionEnded();
65
+ this.UpdateReportedPosition(); // subclasses gate reporting on InTransition: report the settled state now
66
+ this.Stopped?.(this, this.CurrentPosition);
67
+ }
68
+ OnDisposing() { this.StopSnapAnimators(); }
69
+ get IsSnapAnimating() { return this.springX.IsRunning || this.springY.IsRunning || this.range.IsRunning; }
70
+ StopSnapAnimators() {
71
+ this.stoppingSilently = true;
72
+ try {
73
+ this.springX.Stop();
74
+ this.springY.Stop();
75
+ this.range.Stop();
76
+ }
77
+ finally {
78
+ this.stoppingSilently = false;
79
+ }
80
+ }
81
+ /** C# SnappingLayout.Render: the transition state follows the position every frame. */
82
+ Render(ctx) {
83
+ super.Render(ctx);
84
+ this.InTransition = !this.CheckTransitionEnded();
85
+ }
86
+ // ---- clamping ----
87
+ ClampOffset(x, y, rubber) {
88
+ const b = this.ContentOffsetBounds;
89
+ if (!rubber)
90
+ return new SKPoint(Math.max(b.Left, Math.min(b.Right, x)), Math.max(b.Top, Math.min(b.Bottom, y)));
91
+ const c = RubberBandUtils.ClampOnTrack(x, y, b, this.RubberEffect, this.DrawingRect.Width / this.RenderingScale, this.DrawingRect.Height / this.RenderingScale);
92
+ return new SKPoint(c.X, c.Y);
93
+ }
94
+ /** C# GetContentOffsetBounds: the extent covered by the snap points. */
95
+ BoundsFromSnapPoints() {
96
+ if (this.SnapPoints.length === 0)
97
+ return SKRect.Empty;
98
+ let l = Infinity, t = Infinity, r = -Infinity, b = -Infinity;
99
+ for (const p of this.SnapPoints) {
100
+ l = Math.min(l, p.X);
101
+ t = Math.min(t, p.Y);
102
+ r = Math.max(r, p.X);
103
+ b = Math.max(b, p.Y);
104
+ }
105
+ return new SKRect(l, t, r, b);
106
+ }
107
+ // ---- anchors ----
108
+ static Dist(a, b) { return Math.hypot(a.X - b.X, a.Y - b.Y); }
109
+ FindNearestAnchor(current) {
110
+ let best = current, d = Infinity;
111
+ for (const p of this.SnapPoints) {
112
+ const dd = SnappingLayout.Dist(p, current);
113
+ if (dd < d) {
114
+ d = dd;
115
+ best = p;
116
+ }
117
+ }
118
+ return best;
119
+ }
120
+ /** Anchor used as the origin of a snap decision (C# FindNearestAnchorInternal; a looped carousel maps virtual anchors here). */
121
+ FindNearestAnchorInternal(current, _velocity) { return this.FindNearestAnchor(current); }
122
+ /** The closest anchor lying in the direction of the velocity (dot product), else the origin. */
123
+ SelectNextAnchor(origin, velocity) {
124
+ const len = Math.hypot(velocity.X, velocity.Y);
125
+ if (len === 0)
126
+ return origin;
127
+ const nx = velocity.X / len, ny = velocity.Y / len;
128
+ const ordered = [...this.SnapPoints].sort((a, b) => SnappingLayout.Dist(a, origin) - SnappingLayout.Dist(b, origin));
129
+ for (const anchor of ordered) {
130
+ const dx = anchor.X - origin.X, dy = anchor.Y - origin.Y, l = Math.hypot(dx, dy);
131
+ if (l === 0)
132
+ continue;
133
+ if (nx * (dx / l) + ny * (dy / l) > 0)
134
+ return anchor;
135
+ }
136
+ return origin;
137
+ }
138
+ ScrollToNearestAnchor(location, velocity) {
139
+ if (this.SnapPoints.length === 0)
140
+ return;
141
+ const origin = this.FindNearestAnchorInternal(location, velocity);
142
+ const target = this.SelectNextAnchor(origin, velocity);
143
+ if (SnappingLayout.Dist(location, target) >= 0.5)
144
+ this.ScrollToOffset(target, velocity, this.CanAnimate);
145
+ else
146
+ this.UpdateReportedPosition();
147
+ }
148
+ get CanAnimate() { return this.IsVisible && this.DrawingRect.Height > 0; }
149
+ GetAutoVelocity(displacement) {
150
+ const v = this.AutoVelocityMultiplyPts * this.RenderingScale;
151
+ return new SKPoint(-v * Math.sign(displacement.X), -v * Math.sign(displacement.Y));
152
+ }
153
+ // ---- snap animation tuning (SkiaCarousel overrides these for SwipeSpeed / LinearSpeedMs) ----
154
+ ScaleSnapVelocity(velocity) { return velocity; }
155
+ CreateSnapSpring() { return new Spring(1 * (1 + this.RubberDamping), 200, 0.5 * (1 + this.RubberDamping)); }
156
+ SpringVelocity(velocity) { return velocity; }
157
+ /** Seconds of the linear snap (C# SnappingLayout: 0.3 s scaled by distance, or 0.1–0.8 s from the velocity). */
158
+ GetSnapDurationSeconds(_start, _end, velocity, displacement) {
159
+ const magnitude = Math.hypot(velocity.X, velocity.Y);
160
+ if (magnitude > 10)
161
+ return 0.7 * Math.max(0.1, Math.min(0.8, 300 / magnitude));
162
+ const h = this.DrawingRect.Height / this.RenderingScale;
163
+ return 0.3 * (Math.max(Math.abs(displacement.X), Math.abs(displacement.Y)) / (h || 1));
164
+ }
165
+ /** C# ScrollToOffset: spring when Bounces, else a linear animation with the duration from GetSnapDurationSeconds. */
166
+ ScrollToOffset(target, velocity, animate) {
167
+ if (target.X === this.CurrentSnap.X && target.Y === this.CurrentSnap.Y)
168
+ return false;
169
+ this.StopSnapAnimators();
170
+ if (animate && this.DrawingRect.Height > 0) {
171
+ const start = this.CurrentPosition;
172
+ const displacement = new SKPoint(start.X - target.X, start.Y - target.Y);
173
+ if (velocity.X === 0 && velocity.Y === 0)
174
+ velocity = this.GetAutoVelocity(displacement);
175
+ velocity = this.ScaleSnapVelocity(velocity);
176
+ if (displacement.X !== 0 || displacement.Y !== 0) {
177
+ this.InTransition = true;
178
+ if (this.Bounces) {
179
+ const spring = this.CreateSnapSpring(), v = this.SpringVelocity(velocity);
180
+ if (displacement.X !== 0) {
181
+ this.springX.Initialize(target.X, displacement.X, v.X, spring);
182
+ this.springX.Start();
183
+ }
184
+ if (displacement.Y !== 0) {
185
+ this.springY.Initialize(target.Y, displacement.Y, v.Y, spring);
186
+ this.springY.Start();
187
+ }
188
+ }
189
+ else {
190
+ const seconds = this.GetSnapDurationSeconds(start, target, velocity, displacement);
191
+ this.rangeFrom = start;
192
+ this.rangeTo = target;
193
+ this.range.Easing = this.SnapEasing;
194
+ this.range.Speed = Math.max(60, seconds * 1000);
195
+ this.range.Start();
196
+ }
197
+ }
198
+ }
199
+ else {
200
+ this.CurrentSnap = target;
201
+ this.ApplyPosition(target);
202
+ }
203
+ this.CurrentSnap = target;
204
+ this.UpdateReportedPosition();
205
+ return true;
206
+ }
207
+ /** Subclasses move their content here; base records the position and reports (C# ApplyPosition). */
208
+ ApplyPosition(position) {
209
+ this.CurrentPosition = position;
210
+ this.UpdateReportedPosition();
211
+ this.Scrolled?.(this, position);
212
+ }
213
+ /** C# SkiaCarousel.CheckTransitionEnded: never "ended" while a snap animator still runs (the position may touch the target early). */
214
+ CheckTransitionEnded() { return !this.IsSnapAnimating && Math.abs(this.CurrentPosition.X - this.CurrentSnap.X) <= 1 && Math.abs(this.CurrentPosition.Y - this.CurrentSnap.Y) <= 1; }
215
+ /** Subclasses translate CurrentSnap into their own state (SelectedIndex, IsOpen). */
216
+ UpdateReportedPosition() { }
217
+ }
@@ -0,0 +1,70 @@
1
+ import type { ControlTappedEventArgs } from "../core/Gestures";
2
+ import { type Color, type SKRect } from "../core/Types";
3
+ import type { SkiaLabel } from "./SkiaLabel";
4
+ /**
5
+ * Mirrors DrawnUi TextSpan: a styled fragment inside a SkiaLabel (`<SkiaLabel><TextSpan .../></SkiaLabel>`).
6
+ * Unset TextColor / FontSize / FontFamily inherit from the label (C# HasSetColor / HasSetSize / HasSetFont).
7
+ * Not a SkiaControl: it has no box of its own, the label lays it out and keeps its hit `Rects`.
8
+ */
9
+ export declare class TextSpan {
10
+ /** The label hosting this span (C# ParentControl). */
11
+ Parent?: SkiaLabel;
12
+ /** Set by the reconciler when React hides the element. */
13
+ IsVisible: boolean;
14
+ /** Rectangles of the drawn fragments, pixels relative to the label's top-left, refreshed by every paint. */
15
+ readonly Rects: SKRect[];
16
+ Tapped?: (span: TextSpan, e: ControlTappedEventArgs) => void;
17
+ /** Free payload (SkiaRichLabel stores the link url here). */
18
+ Tag?: string;
19
+ /** Listen to taps even without a Tapped handler. */
20
+ ForceCaptureInput: boolean;
21
+ private text;
22
+ private textColor?;
23
+ private fontSize?;
24
+ private fontFamily?;
25
+ private fontWeight;
26
+ private isBold;
27
+ private isItalic;
28
+ private underline;
29
+ private underlineWidth;
30
+ private strikeout;
31
+ private strikeoutWidth;
32
+ private strikeoutColor;
33
+ private backgroundColor?;
34
+ private Set;
35
+ get Text(): string;
36
+ set Text(v: string);
37
+ get TextColor(): Color | undefined;
38
+ set TextColor(v: Color | undefined);
39
+ get FontSize(): number | undefined;
40
+ set FontSize(v: number | undefined);
41
+ get FontFamily(): string | undefined;
42
+ set FontFamily(v: string | undefined);
43
+ get FontWeight(): number;
44
+ set FontWeight(v: number);
45
+ get IsBold(): boolean;
46
+ set IsBold(v: boolean);
47
+ get IsItalic(): boolean;
48
+ set IsItalic(v: boolean);
49
+ get Underline(): boolean;
50
+ set Underline(v: boolean);
51
+ /** Points; negative = that many pixels (C# default -1 = 1px). */
52
+ get UnderlineWidth(): number;
53
+ set UnderlineWidth(v: number);
54
+ get Strikeout(): boolean;
55
+ set Strikeout(v: boolean);
56
+ /** Points. */
57
+ get StrikeoutWidth(): number;
58
+ set StrikeoutWidth(v: number);
59
+ get StrikeoutColor(): Color;
60
+ set StrikeoutColor(v: Color);
61
+ get BackgroundColor(): Color | undefined;
62
+ set BackgroundColor(v: Color | undefined);
63
+ get HasTapHandler(): boolean;
64
+ get HasDecorations(): boolean;
65
+ /** Invalidates the hosting label (a span has no rendering of its own). */
66
+ Update(): void;
67
+ /** x/y in pixels relative to the label's top-left. */
68
+ HitIsInside(x: number, y: number): boolean;
69
+ FireTap(e: ControlTappedEventArgs): void;
70
+ }
@@ -0,0 +1,76 @@
1
+ import { Colors } from "../core/Types";
2
+ /**
3
+ * Mirrors DrawnUi TextSpan: a styled fragment inside a SkiaLabel (`<SkiaLabel><TextSpan .../></SkiaLabel>`).
4
+ * Unset TextColor / FontSize / FontFamily inherit from the label (C# HasSetColor / HasSetSize / HasSetFont).
5
+ * Not a SkiaControl: it has no box of its own, the label lays it out and keeps its hit `Rects`.
6
+ */
7
+ export class TextSpan {
8
+ /** The label hosting this span (C# ParentControl). */
9
+ Parent;
10
+ /** Set by the reconciler when React hides the element. */
11
+ IsVisible = true;
12
+ /** Rectangles of the drawn fragments, pixels relative to the label's top-left, refreshed by every paint. */
13
+ Rects = [];
14
+ Tapped;
15
+ /** Free payload (SkiaRichLabel stores the link url here). */
16
+ Tag;
17
+ /** Listen to taps even without a Tapped handler. */
18
+ ForceCaptureInput = false;
19
+ text = "";
20
+ textColor;
21
+ fontSize;
22
+ fontFamily;
23
+ fontWeight = 0;
24
+ isBold = false;
25
+ isItalic = false;
26
+ underline = false;
27
+ underlineWidth = -1;
28
+ strikeout = false;
29
+ strikeoutWidth = 1;
30
+ strikeoutColor = Colors.Red;
31
+ backgroundColor;
32
+ Set(key, v) { if (this[key] !== v) {
33
+ this[key] = v;
34
+ this.Update();
35
+ } }
36
+ get Text() { return this.text; }
37
+ set Text(v) { this.Set("text", v); }
38
+ get TextColor() { return this.textColor; }
39
+ set TextColor(v) { this.Set("textColor", v); }
40
+ get FontSize() { return this.fontSize; }
41
+ set FontSize(v) { this.Set("fontSize", v); }
42
+ get FontFamily() { return this.fontFamily; }
43
+ set FontFamily(v) { this.Set("fontFamily", v); }
44
+ get FontWeight() { return this.fontWeight; }
45
+ set FontWeight(v) { this.Set("fontWeight", v); }
46
+ get IsBold() { return this.isBold; }
47
+ set IsBold(v) { this.Set("isBold", v); }
48
+ get IsItalic() { return this.isItalic; }
49
+ set IsItalic(v) { this.Set("isItalic", v); }
50
+ get Underline() { return this.underline; }
51
+ set Underline(v) { this.Set("underline", v); }
52
+ /** Points; negative = that many pixels (C# default -1 = 1px). */
53
+ get UnderlineWidth() { return this.underlineWidth; }
54
+ set UnderlineWidth(v) { this.Set("underlineWidth", v); }
55
+ get Strikeout() { return this.strikeout; }
56
+ set Strikeout(v) { this.Set("strikeout", v); }
57
+ /** Points. */
58
+ get StrikeoutWidth() { return this.strikeoutWidth; }
59
+ set StrikeoutWidth(v) { this.Set("strikeoutWidth", v); }
60
+ get StrikeoutColor() { return this.strikeoutColor; }
61
+ set StrikeoutColor(v) { this.Set("strikeoutColor", v); }
62
+ get BackgroundColor() { return this.backgroundColor; }
63
+ set BackgroundColor(v) { this.Set("backgroundColor", v); }
64
+ get HasTapHandler() { return !!this.Tapped || this.ForceCaptureInput; }
65
+ get HasDecorations() { return (this.underline && this.underlineWidth !== 0) || this.strikeout; }
66
+ /** Invalidates the hosting label (a span has no rendering of its own). */
67
+ Update() { this.Parent?.Update(); }
68
+ /** x/y in pixels relative to the label's top-left. */
69
+ HitIsInside(x, y) {
70
+ for (const r of this.Rects)
71
+ if (x >= r.Left && x < r.Right && y >= r.Top && y < r.Bottom)
72
+ return true;
73
+ return false;
74
+ }
75
+ FireTap(e) { this.Tapped?.(this, e); }
76
+ }
@@ -0,0 +1,104 @@
1
+ import type { SkiaControl } from "./SkiaControl";
2
+ import { SKRect } from "./Types";
3
+ /** ARIA role constants (DrawnUi.Models.Aria) for `AccessibilityRole`. */
4
+ export declare const Aria: {
5
+ readonly RoleButton: "button";
6
+ readonly RoleLink: "link";
7
+ readonly RoleCheckbox: "checkbox";
8
+ readonly RoleRadio: "radio";
9
+ readonly RoleSwitch: "switch";
10
+ readonly RoleSlider: "slider";
11
+ readonly RoleSpinButton: "spinbutton";
12
+ readonly RoleTextBox: "textbox";
13
+ readonly RoleSearchBox: "searchbox";
14
+ readonly RoleComboBox: "combobox";
15
+ readonly RoleListBox: "listbox";
16
+ readonly RoleOption: "option";
17
+ readonly RoleTab: "tab";
18
+ readonly RoleTabPanel: "tabpanel";
19
+ readonly RoleTabList: "tablist";
20
+ readonly RoleMenu: "menu";
21
+ readonly RoleMenuItem: "menuitem";
22
+ readonly RoleMenuItemCheckbox: "menuitemcheckbox";
23
+ readonly RoleMenuItemRadio: "menuitemradio";
24
+ readonly RoleScrollBar: "scrollbar";
25
+ readonly RoleText: "text";
26
+ readonly RoleHeading: "heading";
27
+ readonly RoleImg: "img";
28
+ readonly RoleList: "list";
29
+ readonly RoleListItem: "listitem";
30
+ readonly RoleSeparator: "separator";
31
+ readonly RoleProgressBar: "progressbar";
32
+ readonly RoleTooltip: "tooltip";
33
+ readonly RoleDialog: "dialog";
34
+ readonly RoleAlertDialog: "alertdialog";
35
+ readonly RoleStatus: "status";
36
+ readonly RoleAlert: "alert";
37
+ readonly RoleGroup: "group";
38
+ readonly RoleRegion: "region";
39
+ readonly RoleNavigation: "navigation";
40
+ readonly RoleMain: "main";
41
+ /** Removes the control from the accessibility tree even when a default role would apply (inner label of a button). */
42
+ readonly RolePresentation: "presentation";
43
+ readonly LivePolite: "polite";
44
+ readonly LiveAssertive: "assertive";
45
+ };
46
+ /** One laid-out text line exposed for native selection (AccessibilityTextSelectable): CSS px relative to the node. */
47
+ export interface AccessibilityTextLine {
48
+ Text: string;
49
+ Left: number;
50
+ Top: number;
51
+ Width: number;
52
+ Height: number;
53
+ FontFamily: string;
54
+ FontWeight: number;
55
+ FontSize: number;
56
+ }
57
+ /** One entry of the accessibility snapshot (DrawnUi AccessibilityNode). Rect is CSS pixels relative to the canvas. */
58
+ export interface AccessibilityNode {
59
+ Id: number;
60
+ Label?: string;
61
+ Hint?: string;
62
+ Role: string;
63
+ Rect: SKRect;
64
+ CanInteract: boolean;
65
+ IsPressed?: boolean;
66
+ Live?: string;
67
+ Source: SkiaControl;
68
+ /** Real text lines rendered into the overlay so the browser can select / copy them (opt-in per control). */
69
+ TextLines?: AccessibilityTextLine[];
70
+ }
71
+ /**
72
+ * Mirrors DrawnUi SkiaAccessibilityManager: registry of accessible controls + a rate-limited snapshot
73
+ * (at most one rebuild per MinUpdateIntervalMs, taken at frame end) that the DOM overlay renders.
74
+ * Detached / hidden / far-off-canvas controls drop out of the snapshot on the next rebuild, so nothing has
75
+ * to unregister explicitly; positions follow scrolling because rects are read from the arranged DrawingRect.
76
+ */
77
+ export declare class SkiaAccessibilityManager {
78
+ private readonly nodes;
79
+ private dirty;
80
+ private lastRebuild;
81
+ private pending;
82
+ private readonly changed;
83
+ private readonly liveUpdated;
84
+ /** Minimum milliseconds between snapshot rebuilds. */
85
+ MinUpdateIntervalMs: number;
86
+ Snapshot: AccessibilityNode[];
87
+ FocusedNode?: SkiaControl;
88
+ /** Subscribes to snapshot changes; returns the unsubscribe function. */
89
+ OnChanged(cb: () => void): () => void;
90
+ /** Fired immediately (bypassing the rate limit) when a live-region node's value changes. */
91
+ OnLiveRegionUpdated(cb: (node: SkiaControl) => void): () => void;
92
+ NotifyFocused(node?: SkiaControl): void;
93
+ Register(node: SkiaControl): void;
94
+ NotifyUpdated(node: SkiaControl): void;
95
+ ForceRebuildOnNextFrame(): void;
96
+ Unregister(node: SkiaControl): void;
97
+ UnregisterSubtree(root: SkiaControl): void;
98
+ /**
99
+ * Called by Canvas at the end of every drawn frame. Rebuilds when something was invalidated, and at most once
100
+ * per MinUpdateIntervalMs otherwise (keeps rects in sync with scrolling); notifies only when the snapshot differs.
101
+ */
102
+ OnFrameEnd(scale: number, canvasWidthPx: number, canvasHeightPx: number, requestFrame: () => void): void;
103
+ private static Same;
104
+ }
@@ -0,0 +1,150 @@
1
+ import { SKRect } from "./Types";
2
+ /** ARIA role constants (DrawnUi.Models.Aria) for `AccessibilityRole`. */
3
+ export const Aria = {
4
+ // interactive widgets
5
+ RoleButton: "button", RoleLink: "link", RoleCheckbox: "checkbox", RoleRadio: "radio", RoleSwitch: "switch",
6
+ RoleSlider: "slider", RoleSpinButton: "spinbutton", RoleTextBox: "textbox", RoleSearchBox: "searchbox",
7
+ RoleComboBox: "combobox", RoleListBox: "listbox", RoleOption: "option", RoleTab: "tab", RoleTabPanel: "tabpanel",
8
+ RoleTabList: "tablist", RoleMenu: "menu", RoleMenuItem: "menuitem", RoleMenuItemCheckbox: "menuitemcheckbox",
9
+ RoleMenuItemRadio: "menuitemradio", RoleScrollBar: "scrollbar",
10
+ // structure & landmarks
11
+ RoleText: "text", RoleHeading: "heading", RoleImg: "img", RoleList: "list", RoleListItem: "listitem",
12
+ RoleSeparator: "separator", RoleProgressBar: "progressbar", RoleTooltip: "tooltip", RoleDialog: "dialog",
13
+ RoleAlertDialog: "alertdialog", RoleStatus: "status", RoleAlert: "alert", RoleGroup: "group", RoleRegion: "region",
14
+ RoleNavigation: "navigation", RoleMain: "main",
15
+ /** Removes the control from the accessibility tree even when a default role would apply (inner label of a button). */
16
+ RolePresentation: "presentation",
17
+ // live regions
18
+ LivePolite: "polite", LiveAssertive: "assertive",
19
+ };
20
+ /**
21
+ * Mirrors DrawnUi SkiaAccessibilityManager: registry of accessible controls + a rate-limited snapshot
22
+ * (at most one rebuild per MinUpdateIntervalMs, taken at frame end) that the DOM overlay renders.
23
+ * Detached / hidden / far-off-canvas controls drop out of the snapshot on the next rebuild, so nothing has
24
+ * to unregister explicitly; positions follow scrolling because rects are read from the arranged DrawingRect.
25
+ */
26
+ export class SkiaAccessibilityManager {
27
+ nodes = new Set();
28
+ dirty = false;
29
+ lastRebuild = -Infinity;
30
+ pending = 0;
31
+ changed = new Set();
32
+ liveUpdated = new Set();
33
+ /** Minimum milliseconds between snapshot rebuilds. */
34
+ MinUpdateIntervalMs = 1000;
35
+ Snapshot = [];
36
+ FocusedNode;
37
+ /** Subscribes to snapshot changes; returns the unsubscribe function. */
38
+ OnChanged(cb) { this.changed.add(cb); return () => this.changed.delete(cb); }
39
+ /** Fired immediately (bypassing the rate limit) when a live-region node's value changes. */
40
+ OnLiveRegionUpdated(cb) { this.liveUpdated.add(cb); return () => this.liveUpdated.delete(cb); }
41
+ NotifyFocused(node) { if (this.FocusedNode !== node)
42
+ this.FocusedNode = node; }
43
+ Register(node) { this.nodes.add(node); this.dirty = true; }
44
+ NotifyUpdated(node) {
45
+ if (!this.nodes.has(node))
46
+ return;
47
+ this.dirty = true;
48
+ if (node.AccessibilityLive)
49
+ for (const cb of this.liveUpdated)
50
+ cb(node);
51
+ }
52
+ ForceRebuildOnNextFrame() { this.lastRebuild = 0; this.dirty = true; }
53
+ Unregister(node) {
54
+ if (this.nodes.delete(node)) {
55
+ node.OnAccessibilityUnregistered();
56
+ this.dirty = true;
57
+ }
58
+ }
59
+ UnregisterSubtree(root) {
60
+ for (const n of Array.from(this.nodes)) {
61
+ let c = n;
62
+ while (c && c !== root)
63
+ c = c.Parent;
64
+ if (c)
65
+ this.Unregister(n);
66
+ }
67
+ }
68
+ /**
69
+ * Called by Canvas at the end of every drawn frame. Rebuilds when something was invalidated, and at most once
70
+ * per MinUpdateIntervalMs otherwise (keeps rects in sync with scrolling); notifies only when the snapshot differs.
71
+ */
72
+ OnFrameEnd(scale, canvasWidthPx, canvasHeightPx, requestFrame) {
73
+ if (this.nodes.size === 0 && this.Snapshot.length === 0)
74
+ return;
75
+ const now = performance.now();
76
+ const wait = this.MinUpdateIntervalMs - (now - this.lastRebuild);
77
+ if (wait > 0) {
78
+ // rate-limited: rendering is on demand, so make sure a frame comes back once the interval has passed
79
+ if (!this.pending)
80
+ this.pending = window.setTimeout(() => { this.pending = 0; requestFrame(); }, wait);
81
+ return;
82
+ }
83
+ this.dirty = false;
84
+ this.lastRebuild = now;
85
+ const list = [];
86
+ for (const n of this.nodes) {
87
+ if (!n.Superview) {
88
+ this.nodes.delete(n);
89
+ n.OnAccessibilityUnregistered();
90
+ continue;
91
+ } // detached from the tree
92
+ if (!n.IsVisible || !n.IsAccessibilityElement || n.AccessibilityRole === Aria.RolePresentation)
93
+ continue;
94
+ if (AccessibilityManagerHiddenByAncestor(n))
95
+ continue; // e.g. the root page kept mounted under a pushed shell page
96
+ const px = n.GetAccessibilityPixelRect();
97
+ if (px.Width <= 0 || px.Height <= 0)
98
+ continue;
99
+ // nodes beyond the canvas stay in the tree (scroll content reachable with Tab, the overlay scrolls them into view);
100
+ // only what is entirely outside a clipped canvas by more than a screen is dropped to keep the DOM small
101
+ if (px.Right < -canvasWidthPx || px.Bottom < -canvasHeightPx || px.Left > 2 * canvasWidthPx || px.Top > 2 * canvasHeightPx)
102
+ continue;
103
+ list.push({
104
+ Id: n.AccessibilityId, Label: n.AccessibilityLabel, Hint: n.AccessibilityHint, Role: n.AccessibilityRole,
105
+ Rect: new SKRect(px.Left / scale, px.Top / scale, px.Right / scale, px.Bottom / scale),
106
+ CanInteract: n.AccessibilityCanInteract, IsPressed: n.AccessibilityIsPressed, Live: n.AccessibilityLive, Source: n,
107
+ TextLines: n.AccessibilityTextSelectable ? n.GetAccessibilityTextLines(scale) : undefined,
108
+ });
109
+ }
110
+ list.sort((a, b) => a.Rect.Top - b.Rect.Top || a.Rect.Left - b.Rect.Left);
111
+ if (SkiaAccessibilityManager.Same(list, this.Snapshot))
112
+ return;
113
+ this.Snapshot = list;
114
+ for (const cb of this.changed)
115
+ cb();
116
+ }
117
+ static Same(a, b) {
118
+ if (a.length !== b.length)
119
+ return false;
120
+ for (let i = 0; i < a.length; i++) {
121
+ const x = a[i], y = b[i];
122
+ if (x.Source !== y.Source || x.Label !== y.Label || x.Hint !== y.Hint || x.Role !== y.Role || x.CanInteract !== y.CanInteract
123
+ || x.IsPressed !== y.IsPressed || x.Live !== y.Live
124
+ || Math.abs(x.Rect.Left - y.Rect.Left) > 0.5 || Math.abs(x.Rect.Top - y.Rect.Top) > 0.5
125
+ || Math.abs(x.Rect.Right - y.Rect.Right) > 0.5 || Math.abs(x.Rect.Bottom - y.Rect.Bottom) > 0.5)
126
+ return false;
127
+ const tx = x.TextLines, ty = y.TextLines;
128
+ if (!!tx !== !!ty)
129
+ return false;
130
+ if (tx && ty) {
131
+ if (tx.length !== ty.length)
132
+ return false;
133
+ for (let j = 0; j < tx.length; j++)
134
+ if (tx[j].Text !== ty[j].Text || Math.abs(tx[j].Left - ty[j].Left) > 0.5 || Math.abs(tx[j].Top - ty[j].Top) > 0.5 || tx[j].FontSize !== ty[j].FontSize)
135
+ return false;
136
+ }
137
+ }
138
+ return true;
139
+ }
140
+ }
141
+ /** True when any ancestor is hidden (IsVisible false or Opacity 0): the subtree is not drawn, so it gets no nodes. */
142
+ function AccessibilityManagerHiddenByAncestor(control) {
143
+ let p = control.Parent;
144
+ while (p) {
145
+ if (!p.IsVisible || p.Opacity <= 0)
146
+ return true;
147
+ p = p.Parent;
148
+ }
149
+ return false;
150
+ }