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
|
@@ -333,6 +333,8 @@ export class SkiaScroll extends SkiaControl {
|
|
|
333
333
|
loadMoreTopArmed = false;
|
|
334
334
|
/** C# CheckNeedToLoadMore: fires once per content extent near an edge; re-arms when the content grows or the user moves away (>offset+100pt, >2 s). */
|
|
335
335
|
CheckLoadMore() {
|
|
336
|
+
if (this.indexOrder)
|
|
337
|
+
return; // an open ScrollToIndex has the viewport on its way: the edges are looked at after it
|
|
336
338
|
const vertical = this.Orientation !== "Horizontal";
|
|
337
339
|
const offset = vertical ? this.offsetY : this.offsetX;
|
|
338
340
|
const min = vertical ? this.ContentOffsetBounds.Top : this.ContentOffsetBounds.Left; // most negative offset
|
|
@@ -635,6 +637,7 @@ export class SkiaScroll extends SkiaControl {
|
|
|
635
637
|
}
|
|
636
638
|
Paint(ctx) {
|
|
637
639
|
this.Superview?.Hover?.RegisterMover(this);
|
|
640
|
+
this.PursueIndexOrder();
|
|
638
641
|
const c = this.content;
|
|
639
642
|
if (!c)
|
|
640
643
|
return;
|
|
@@ -737,6 +740,8 @@ export class SkiaScroll extends SkiaControl {
|
|
|
737
740
|
this.ViewportOffsetY = target;
|
|
738
741
|
}
|
|
739
742
|
ScrollTo(x, y, maxSpeedSecs, clamp = true) {
|
|
743
|
+
if (!this.pursuing)
|
|
744
|
+
this.indexOrder = undefined; // another scroll drops an open ScrollToIndex
|
|
740
745
|
this.StopAnimators(); // also forgets any pending edge bounce: a programmatic scroll never bounces
|
|
741
746
|
let tx = x, ty = y;
|
|
742
747
|
if (clamp) {
|
|
@@ -769,16 +774,82 @@ export class SkiaScroll extends SkiaControl {
|
|
|
769
774
|
else
|
|
770
775
|
this.ScrollTo(this.offsetX, this.ContentOffsetBounds.Top, maxTimeSecs);
|
|
771
776
|
}
|
|
772
|
-
StopScrolling() { this.StopAnimators(); this.IsUserPanning = false; }
|
|
777
|
+
StopScrolling() { this.indexOrder = undefined; this.StopAnimators(); this.IsUserPanning = false; }
|
|
773
778
|
/**
|
|
774
779
|
* Scrolls so that item `index` is at the viewport start (or end). Like DrawnUi, Content must BE the
|
|
775
780
|
* templated layout (the recycled list is the scroll's only child; a header goes above the scroll or in Header).
|
|
776
781
|
*/
|
|
777
782
|
ScrollToIndex(index, animate, option = "Start") {
|
|
783
|
+
this.indexOrder = { Index: index, Option: option, Secs: animate ? this.ScrollingSpeedMs / 1000 : 0, Stalled: 0 };
|
|
784
|
+
this.PursueIndexOrder();
|
|
785
|
+
this.Repaint();
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* An open ScrollToIndex (C# OrderedScrollToIndex, DrawnUi.Rust scroll_to_index): a row not measured yet is gone to by
|
|
789
|
+
* the sizes as they are estimated, and the order is aimed again every frame until the row has its real place and the
|
|
790
|
+
* viewport stands on it; an animated move goes on to a moved target in the time it has left, later corrections jump.
|
|
791
|
+
* A press, the wheel or another scroll drops it; while it is open LoadMore waits.
|
|
792
|
+
*/
|
|
793
|
+
indexOrder;
|
|
794
|
+
pursuing = false;
|
|
795
|
+
/** One step of the open order (C# ExecuteScrollToIndexOrder, Rust `pursue`), at the start of every paint. */
|
|
796
|
+
PursueIndexOrder() {
|
|
797
|
+
const order = this.indexOrder;
|
|
798
|
+
if (!order)
|
|
799
|
+
return;
|
|
800
|
+
const horizontal = this.Orientation === "Horizontal";
|
|
801
|
+
const before = horizontal ? this.offsetX : this.offsetY;
|
|
802
|
+
const running = horizontal ? this.animatorFlingX : this.animatorFlingY;
|
|
803
|
+
const aim = this.AimIndex(order.Index, order.Option);
|
|
804
|
+
let done = false;
|
|
805
|
+
if (aim) {
|
|
806
|
+
const c = this.ClampOffset(horizontal ? aim.Offset : this.offsetX, horizontal ? this.offsetY : aim.Offset, this.ContentOffsetBounds, true);
|
|
807
|
+
const target = horizontal ? c.X : c.Y;
|
|
808
|
+
const runningTo = horizontal ? this.scrollToTargetX : this.scrollToTargetY;
|
|
809
|
+
const move = (to, secs) => {
|
|
810
|
+
this.pursuing = true;
|
|
811
|
+
try {
|
|
812
|
+
this.ScrollTo(horizontal ? to : this.offsetX, horizontal ? this.offsetY : to, secs, true);
|
|
813
|
+
}
|
|
814
|
+
finally {
|
|
815
|
+
this.pursuing = false;
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
if (running.IsRunning && runningTo !== null) {
|
|
819
|
+
if (Math.abs(runningTo - target) > 0.5) {
|
|
820
|
+
// on the way: the move goes on to the new target in the time it has left
|
|
821
|
+
const now = this.Superview?.FrameTimeNanos || performance.now() * 1_000_000;
|
|
822
|
+
move(target, running.StartFrameTimeNanos ? Math.max(0.016, running.Speed - (now - running.StartFrameTimeNanos) / 1e9) : running.Speed);
|
|
823
|
+
running.StartFrameTimeNanos = running.LastFrameTimeNanos = now;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
else if (Math.abs(before - target) <= 0.5) {
|
|
827
|
+
if (horizontal)
|
|
828
|
+
this.ViewportOffsetX = target;
|
|
829
|
+
else
|
|
830
|
+
this.ViewportOffsetY = target;
|
|
831
|
+
done = aim.Exact;
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
move(target, order.Secs);
|
|
835
|
+
order.Secs = 0;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// no list yet, or a row that never gets its real place: not forever
|
|
839
|
+
order.Stalled = (horizontal ? this.offsetX : this.offsetY) === before && !running.IsRunning ? order.Stalled + 1 : 0;
|
|
840
|
+
if (done || order.Stalled >= 10) {
|
|
841
|
+
this.indexOrder = undefined;
|
|
842
|
+
this.CheckLoadMore();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
this.Repaint(); // the next frame aims again
|
|
846
|
+
}
|
|
847
|
+
/** The offset (points) that puts item `index` where `option` says, by the sizes the list knows now; Exact = its place is measured. */
|
|
848
|
+
AimIndex(index, option) {
|
|
778
849
|
const layout = this.content instanceof SkiaLayout && this.content.IsTemplated ? this.content : undefined;
|
|
779
850
|
const items = layout?.ItemsSource;
|
|
780
851
|
if (!layout || !items || items.length === 0 || !this.content)
|
|
781
|
-
return;
|
|
852
|
+
return undefined;
|
|
782
853
|
const i = Math.max(0, Math.min(items.length - 1, index));
|
|
783
854
|
const scale = this.RenderingScale;
|
|
784
855
|
const horizontal = this.Orientation === "Horizontal";
|
|
@@ -795,7 +866,7 @@ export class SkiaScroll extends SkiaControl {
|
|
|
795
866
|
// arranged rect, like C# reads the structure cell's Destination.
|
|
796
867
|
const cell = layout.ChildrenFactory.GetViewForIndex(i);
|
|
797
868
|
if (!cell)
|
|
798
|
-
return;
|
|
869
|
+
return undefined;
|
|
799
870
|
const r = cell.DrawingRect, c = this.content.DrawingRect;
|
|
800
871
|
itemStart = (horizontal ? r.Left - c.Left : r.Top - c.Top) / scale;
|
|
801
872
|
itemSize = (horizontal ? r.Width : r.Height) / scale;
|
|
@@ -805,11 +876,7 @@ export class SkiaScroll extends SkiaControl {
|
|
|
805
876
|
target -= viewport - itemSize;
|
|
806
877
|
else if (option === "Center")
|
|
807
878
|
target -= (viewport - itemSize) / 2;
|
|
808
|
-
|
|
809
|
-
if (horizontal)
|
|
810
|
-
this.ScrollTo(-target, this.offsetY, time, true);
|
|
811
|
-
else
|
|
812
|
-
this.ScrollTo(this.offsetX, -target, time, true);
|
|
879
|
+
return { Offset: -target, Exact: layout.IsItemMeasured(i) };
|
|
813
880
|
}
|
|
814
881
|
StopAnimators() {
|
|
815
882
|
this.flingEdgeX = null;
|
|
@@ -895,6 +962,7 @@ export class SkiaScroll extends SkiaControl {
|
|
|
895
962
|
if (this.ProcessScrollBarGestures(args, apply))
|
|
896
963
|
return this;
|
|
897
964
|
if (args.Type === "Down") {
|
|
965
|
+
this.indexOrder = undefined; // a press drops an open ScrollToIndex
|
|
898
966
|
this.hadDown = true;
|
|
899
967
|
this.snapped = false;
|
|
900
968
|
if (this.RespondsToGestures) {
|
|
@@ -20,6 +20,10 @@ export declare class SkiaShape extends SkiaLayout {
|
|
|
20
20
|
StrokeCap: StrokeCap;
|
|
21
21
|
/** Gradient painted along the stroke instead of StrokeColor (C# StrokeGradient). */
|
|
22
22
|
StrokeGradient?: SkiaGradient;
|
|
23
|
+
/** Dashes in points: on, off, on, off... (C# StrokePath, each rounded to pixels). Empty = a solid line. */
|
|
24
|
+
StrokePath?: number[];
|
|
25
|
+
/** Blend mode of the stroke, an SKBlendMode name (C# StrokeBlendMode). */
|
|
26
|
+
StrokeBlendMode: string;
|
|
23
27
|
/** Raised (Bevel) or pressed-in (Emboss) edges, drawn with Bevel after the background (C# BevelType). */
|
|
24
28
|
BevelType: BevelType;
|
|
25
29
|
private bevel?;
|
|
@@ -43,11 +47,18 @@ export declare class SkiaShape extends SkiaLayout {
|
|
|
43
47
|
static ShadowFilter(shadow: SkiaShadow, scale: number): import("canvaskit-wasm").ImageFilter;
|
|
44
48
|
constructor();
|
|
45
49
|
private StrokePixels;
|
|
46
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* The outline the stroke runs on (C# CalculateShapeSizeForStroke): the bounds inset by half the stroke rounded up, at
|
|
52
|
+
* least a pixel, then on whole pixels inward, so a 1 px stroke on a plain rectangle sits on the line between two
|
|
53
|
+
* pixels as upstream draws it.
|
|
54
|
+
*/
|
|
47
55
|
private StrokeAwareRect;
|
|
56
|
+
/** The fill (C# MeasuredStrokeAwareClipSize): a third of the stroke inside the outline, so it does not show under the stroke's edge. */
|
|
57
|
+
private BackgroundRect;
|
|
48
58
|
private UsesStroke;
|
|
49
59
|
/** Builds the outline path for rect (canvas pixels). Caller deletes it. */
|
|
50
|
-
|
|
60
|
+
/** `radiusReduce`: pixels taken off every corner radius (the fill inside a thick stroke, C# GetCorrectedBackgroundRadii). */
|
|
61
|
+
protected CreateShapePath(rect: SKRect, scale: number, radiusReduce?: number): Path;
|
|
51
62
|
/** Children live inside the stroke (full width) + Padding. */
|
|
52
63
|
private ChildrenRect;
|
|
53
64
|
/** Clip for overlay effects (ripple) and children: the shape itself, inset by the stroke. */
|
|
@@ -17,6 +17,10 @@ export class SkiaShape extends SkiaLayout {
|
|
|
17
17
|
StrokeCap = "Round";
|
|
18
18
|
/** Gradient painted along the stroke instead of StrokeColor (C# StrokeGradient). */
|
|
19
19
|
StrokeGradient;
|
|
20
|
+
/** Dashes in points: on, off, on, off... (C# StrokePath, each rounded to pixels). Empty = a solid line. */
|
|
21
|
+
StrokePath;
|
|
22
|
+
/** Blend mode of the stroke, an SKBlendMode name (C# StrokeBlendMode). */
|
|
23
|
+
StrokeBlendMode = "SrcOver";
|
|
20
24
|
/** Raised (Bevel) or pressed-in (Emboss) edges, drawn with Bevel after the background (C# BevelType). */
|
|
21
25
|
BevelType = "None";
|
|
22
26
|
bevel;
|
|
@@ -66,14 +70,28 @@ export class SkiaShape extends SkiaLayout {
|
|
|
66
70
|
return 0;
|
|
67
71
|
return this.StrokeWidth > 0 ? this.StrokeWidth * scale : -this.StrokeWidth;
|
|
68
72
|
}
|
|
69
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* The outline the stroke runs on (C# CalculateShapeSizeForStroke): the bounds inset by half the stroke rounded up, at
|
|
75
|
+
* least a pixel, then on whole pixels inward, so a 1 px stroke on a plain rectangle sits on the line between two
|
|
76
|
+
* pixels as upstream draws it.
|
|
77
|
+
*/
|
|
70
78
|
StrokeAwareRect(r, scale) {
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
if (!this.UsesStroke())
|
|
80
|
+
return r;
|
|
81
|
+
const half = this.StrokePixels(scale) / 2, inset = half < 0.5 ? 1 : Math.ceil(half);
|
|
82
|
+
return new SKRect(Math.ceil(r.Left + inset), Math.ceil(r.Top + inset), Math.floor(r.Right - inset), Math.floor(r.Bottom - inset));
|
|
83
|
+
}
|
|
84
|
+
/** The fill (C# MeasuredStrokeAwareClipSize): a third of the stroke inside the outline, so it does not show under the stroke's edge. */
|
|
85
|
+
BackgroundRect(r, scale) {
|
|
86
|
+
if (!this.UsesStroke())
|
|
87
|
+
return r;
|
|
88
|
+
const o = this.StrokeAwareRect(r, scale), k = this.StrokePixels(scale) / 3;
|
|
89
|
+
return new SKRect(o.Left + k, o.Top + k, o.Right - k, o.Bottom - k);
|
|
73
90
|
}
|
|
74
91
|
UsesStroke() { return this.StrokeWidth !== 0 && this.StrokeColor !== Colors.Transparent; }
|
|
75
92
|
/** Builds the outline path for rect (canvas pixels). Caller deletes it. */
|
|
76
|
-
|
|
93
|
+
/** `radiusReduce`: pixels taken off every corner radius (the fill inside a thick stroke, C# GetCorrectedBackgroundRadii). */
|
|
94
|
+
CreateShapePath(rect, scale, radiusReduce = 0) {
|
|
77
95
|
const CK = Super.CK;
|
|
78
96
|
const b = new CK.PathBuilder();
|
|
79
97
|
const type = this.Type;
|
|
@@ -114,7 +132,8 @@ export class SkiaShape extends SkiaLayout {
|
|
|
114
132
|
else {
|
|
115
133
|
const c = this.cornerRadius;
|
|
116
134
|
if (c.TopLeft || c.TopRight || c.BottomLeft || c.BottomRight) {
|
|
117
|
-
const
|
|
135
|
+
const k = (v) => Math.max(0, v * scale - radiusReduce);
|
|
136
|
+
const tl = k(c.TopLeft), tr = k(c.TopRight), br = k(c.BottomRight), bl = k(c.BottomLeft);
|
|
118
137
|
b.addRRect(Float32Array.of(rect.Left, rect.Top, rect.Right, rect.Bottom, tl, tl, tr, tr, br, br, bl, bl));
|
|
119
138
|
}
|
|
120
139
|
else {
|
|
@@ -151,8 +170,11 @@ export class SkiaShape extends SkiaLayout {
|
|
|
151
170
|
return; // open shapes: stroke only
|
|
152
171
|
const CK = Super.CK;
|
|
153
172
|
const canvas = ctx.Context.Canvas;
|
|
154
|
-
const rect = this.
|
|
155
|
-
|
|
173
|
+
const rect = this.BackgroundRect(ctx.Destination, ctx.Scale);
|
|
174
|
+
// inside a stroke wider than a point the corners of the fill are rounded less (C# GetCorrectedBackgroundRadii)
|
|
175
|
+
const outline = this.StrokeAwareRect(ctx.Destination, ctx.Scale);
|
|
176
|
+
const reduce = this.UsesStroke() && this.StrokeWidth > 1 ? Math.min((outline.Width - rect.Width) / 2, (outline.Height - rect.Height) / 2) : 0;
|
|
177
|
+
const path = this.CreateShapePath(rect, ctx.Scale, reduce);
|
|
156
178
|
// C# PaintWithShadowsInternal: the fill is drawn once per shadow with a drop-shadow filter; a hollow shape
|
|
157
179
|
// (ClipBackgroundColor) clips its own outline away so only the shadow outside remains
|
|
158
180
|
for (const shadow of this.shadows) {
|
|
@@ -295,18 +317,36 @@ export class SkiaShape extends SkiaLayout {
|
|
|
295
317
|
clip.delete();
|
|
296
318
|
}
|
|
297
319
|
if (this.UsesStroke()) {
|
|
298
|
-
const
|
|
299
|
-
|
|
320
|
+
const scale = ctx.Scale, px = this.StrokePixels(scale), type = this.Type;
|
|
321
|
+
let rect = this.StrokeAwareRect(ctx.Destination, scale);
|
|
322
|
+
const c = this.cornerRadius;
|
|
323
|
+
const rounded = type === "Rectangle" && !!(c.TopLeft || c.TopRight || c.BottomLeft || c.BottomRight);
|
|
324
|
+
// C# PaintStroke: on curves a stroke of a point or less is thinned (its antialiasing makes it look heavier than
|
|
325
|
+
// the straight parts), a thin rounded rectangle runs through pixel centers, a line is at least a pixel wide
|
|
326
|
+
const curved = rounded || type === "Circle" || type === "Ellipse" || type === "Arc" || type === "Path";
|
|
327
|
+
const width = type === "Line" ? Math.max(1, px) : curved && px <= scale ? px * 0.55 : px;
|
|
328
|
+
if (rounded && px <= 2 * scale) {
|
|
329
|
+
const even = (v) => { const r = Math.round(v); return Math.abs(v % 1) === 0.5 && r % 2 !== 0 ? r - 1 : r; }; // C# Math.Round: ties to even
|
|
330
|
+
rect = new SKRect(even(rect.Left) + 0.5, even(rect.Top) + 0.5, even(rect.Right) - 0.5, even(rect.Bottom) - 0.5);
|
|
331
|
+
}
|
|
332
|
+
const path = this.CreateShapePath(rect, scale);
|
|
300
333
|
const paint = new CK.Paint();
|
|
301
334
|
paint.setAntiAlias(true);
|
|
302
335
|
paint.setStyle(CK.PaintStyle.Stroke);
|
|
303
|
-
paint.setStrokeWidth(
|
|
336
|
+
paint.setStrokeWidth(width);
|
|
304
337
|
paint.setColor(Super.ParseColor(this.StrokeColor));
|
|
305
338
|
if (this.StrokeGradient)
|
|
306
339
|
this.SetupGradient(paint, this.StrokeGradient, rect);
|
|
340
|
+
const blend = CK.BlendMode[this.StrokeBlendMode];
|
|
341
|
+
if (blend && this.StrokeBlendMode !== "SrcOver")
|
|
342
|
+
paint.setBlendMode(blend);
|
|
307
343
|
paint.setStrokeCap(this.StrokeCap === "Butt" ? CK.StrokeCap.Butt : this.StrokeCap === "Square" ? CK.StrokeCap.Square : CK.StrokeCap.Round);
|
|
308
344
|
paint.setStrokeJoin(this.StrokeCap === "Round" ? CK.StrokeJoin.Round : CK.StrokeJoin.Miter);
|
|
345
|
+
const dash = this.StrokePath?.length ? CK.PathEffect.MakeDash(this.StrokePath.map((v) => Math.round(v * scale)), 0) : null;
|
|
346
|
+
if (dash)
|
|
347
|
+
paint.setPathEffect(dash);
|
|
309
348
|
canvas.drawPath(path, paint);
|
|
349
|
+
dash?.delete();
|
|
310
350
|
paint.delete();
|
|
311
351
|
path.delete();
|
|
312
352
|
}
|
|
@@ -80,5 +80,9 @@ export declare class SkiaSlider extends SkiaControl {
|
|
|
80
80
|
* Home / End go to Min / Max. A ranged slider moves its End thumb, which stops at Start.
|
|
81
81
|
*/
|
|
82
82
|
OnAccessibilityKey(key: string): boolean;
|
|
83
|
-
|
|
83
|
+
/**
|
|
84
|
+
* End between Min and Max, read as the number, or "20 – 80" in range mode; the step is the arrow key's. The name is
|
|
85
|
+
* the app's AccessibilityLabel only (C# used to make the value the name, drawnui-cross 6c).
|
|
86
|
+
*/
|
|
87
|
+
get AccessibilityValue(): import("../core/Accessibility").AccessibilityValue;
|
|
84
88
|
}
|
|
@@ -280,5 +280,11 @@ export class SkiaSlider extends SkiaControl {
|
|
|
280
280
|
this.End = Math.min(this.Max, Math.max(low, value));
|
|
281
281
|
return true;
|
|
282
282
|
}
|
|
283
|
-
|
|
283
|
+
/**
|
|
284
|
+
* End between Min and Max, read as the number, or "20 – 80" in range mode; the step is the arrow key's. The name is
|
|
285
|
+
* the app's AccessibilityLabel only (C# used to make the value the name, drawnui-cross 6c).
|
|
286
|
+
*/
|
|
287
|
+
get AccessibilityValue() {
|
|
288
|
+
return { Now: this.end, Min: this.Min, Max: this.Max, Step: this.Step > 0 ? this.Step : (this.Max - this.Min) / 100, Text: this.EnableRange ? `${this.start} – ${this.end}` : "" };
|
|
289
|
+
}
|
|
284
290
|
}
|
|
@@ -86,6 +86,7 @@ export declare class SkiaSprite extends AnimatedFramesRenderer {
|
|
|
86
86
|
/** Transparent borders of a frame, scanned once (C# ScanFrameContentBounds over the bitmap alpha). */
|
|
87
87
|
private GetFrameContentBounds;
|
|
88
88
|
protected OnAnimatorUpdated(value: number): void;
|
|
89
|
+
protected NextChangeMs(value: number): number | undefined;
|
|
89
90
|
protected OnAnimatorSeeking(time: number): void;
|
|
90
91
|
protected OnAnimatorInitializing(): void;
|
|
91
92
|
protected ApplySpeed(): void;
|
|
@@ -227,6 +227,10 @@ export class SkiaSprite extends AnimatedFramesRenderer {
|
|
|
227
227
|
}
|
|
228
228
|
// ---- animator ----
|
|
229
229
|
OnAnimatorUpdated(value) { this.Seek(value); }
|
|
230
|
+
NextChangeMs(value) {
|
|
231
|
+
const fd = this.FrameDurationMs;
|
|
232
|
+
return fd > 0 ? (Math.floor(value / fd) + 1) * fd : undefined;
|
|
233
|
+
}
|
|
230
234
|
OnAnimatorSeeking(time) {
|
|
231
235
|
if (!this.SpriteSheet)
|
|
232
236
|
return;
|
|
@@ -61,6 +61,11 @@ export declare abstract class SnappingLayout extends SkiaLayout {
|
|
|
61
61
|
protected FindNearestAnchorInternal(current: SKPoint, _velocity: SKPoint): SKPoint;
|
|
62
62
|
/** The closest anchor lying in the direction of the velocity (dot product), else the origin. */
|
|
63
63
|
SelectNextAnchor(origin: SKPoint, velocity: SKPoint): SKPoint;
|
|
64
|
+
/**
|
|
65
|
+
* Release speed, points per second along an axis, under which the snap ignores the velocity and goes to the nearest
|
|
66
|
+
* anchor by distance (C# SnapVelocityThreshold, drawnui-cross 6o): a finger held still before lifting is not a flick.
|
|
67
|
+
*/
|
|
68
|
+
SnapVelocityThreshold: number;
|
|
64
69
|
ScrollToNearestAnchor(location: SKPoint, velocity: SKPoint): void;
|
|
65
70
|
get CanAnimate(): boolean;
|
|
66
71
|
protected GetAutoVelocity(displacement: SKPoint): SKPoint;
|
|
@@ -138,9 +138,17 @@ export class SnappingLayout extends SkiaLayout {
|
|
|
138
138
|
}
|
|
139
139
|
return origin;
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Release speed, points per second along an axis, under which the snap ignores the velocity and goes to the nearest
|
|
143
|
+
* anchor by distance (C# SnapVelocityThreshold, drawnui-cross 6o): a finger held still before lifting is not a flick.
|
|
144
|
+
*/
|
|
145
|
+
SnapVelocityThreshold = 100;
|
|
141
146
|
ScrollToNearestAnchor(location, velocity) {
|
|
142
147
|
if (this.SnapPoints.length === 0)
|
|
143
148
|
return;
|
|
149
|
+
if (Math.abs(velocity.X) < this.SnapVelocityThreshold || Math.abs(velocity.Y) < this.SnapVelocityThreshold) {
|
|
150
|
+
velocity = new SKPoint(Math.abs(velocity.X) < this.SnapVelocityThreshold ? 0 : velocity.X, Math.abs(velocity.Y) < this.SnapVelocityThreshold ? 0 : velocity.Y);
|
|
151
|
+
}
|
|
144
152
|
const origin = this.FindNearestAnchorInternal(location, velocity);
|
|
145
153
|
const target = this.SelectNextAnchor(origin, velocity);
|
|
146
154
|
if (SnappingLayout.Dist(location, target) >= 0.5)
|
|
@@ -70,6 +70,17 @@ export interface AccessibilityTextLine {
|
|
|
70
70
|
FontWeight: number;
|
|
71
71
|
FontSize: number;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The value of a range control (a slider, a progress bar): what a screen reader reads and adjusts (drawnui-cross 6c,
|
|
75
|
+
* C# AccessibilityValue): aria-valuenow / min / max, and aria-valuetext when Text is not empty ("65%", "20 – 80").
|
|
76
|
+
*/
|
|
77
|
+
export interface AccessibilityValue {
|
|
78
|
+
Now: number;
|
|
79
|
+
Min: number;
|
|
80
|
+
Max: number;
|
|
81
|
+
Step: number;
|
|
82
|
+
Text: string;
|
|
83
|
+
}
|
|
73
84
|
/** One entry of the accessibility snapshot (DrawnUi AccessibilityNode). Rect is CSS pixels relative to the canvas. */
|
|
74
85
|
export interface AccessibilityNode {
|
|
75
86
|
Id: number;
|
|
@@ -83,6 +94,8 @@ export interface AccessibilityNode {
|
|
|
83
94
|
IsPressed?: boolean;
|
|
84
95
|
Live?: string;
|
|
85
96
|
Source: SkiaControl;
|
|
97
|
+
/** A range control's value (aria-valuenow / min / max / valuetext); its name stays the app's label. */
|
|
98
|
+
Value?: AccessibilityValue;
|
|
86
99
|
/** Real text lines rendered into the overlay so the browser can select / copy them (opt-in per control). */
|
|
87
100
|
TextLines?: AccessibilityTextLine[];
|
|
88
101
|
}
|
|
@@ -80,6 +80,7 @@ export class SkiaAccessibilityManager {
|
|
|
80
80
|
if (!node || !node.AccessibilityCanInteract)
|
|
81
81
|
return false;
|
|
82
82
|
node.OnAccessibilityActivated();
|
|
83
|
+
node.Superview?.AccessibilityManager.ForceRebuildOnNextFrame(); // a screen reader reads the new state right back
|
|
83
84
|
return true;
|
|
84
85
|
}
|
|
85
86
|
/**
|
|
@@ -90,8 +91,10 @@ export class SkiaAccessibilityManager {
|
|
|
90
91
|
static Key(node, key) {
|
|
91
92
|
if (!node)
|
|
92
93
|
return false;
|
|
93
|
-
if (node.CanReceiveGesture("Panning") && node.OnAccessibilityKey(key))
|
|
94
|
+
if (node.CanReceiveGesture("Panning") && node.OnAccessibilityKey(key)) {
|
|
95
|
+
node.Superview?.AccessibilityManager.ForceRebuildOnNextFrame(); // the new value is read right back, not a second later
|
|
94
96
|
return true;
|
|
97
|
+
}
|
|
95
98
|
return node.Superview?.AccessibilityManager.MoveInGroup(node, key) ?? false;
|
|
96
99
|
}
|
|
97
100
|
// ---- arrow-key groups (C# SkiaAccessibilityManager, "Arrow-key groups") ----
|
|
@@ -255,7 +258,7 @@ export class SkiaAccessibilityManager {
|
|
|
255
258
|
for (const cb of this.liveUpdated)
|
|
256
259
|
cb(node);
|
|
257
260
|
}
|
|
258
|
-
ForceRebuildOnNextFrame() { this.lastRebuild =
|
|
261
|
+
ForceRebuildOnNextFrame() { this.lastRebuild = -Infinity; this.dirty = true; }
|
|
259
262
|
Unregister(node) {
|
|
260
263
|
if (this.nodes.delete(node)) {
|
|
261
264
|
node.OnAccessibilityUnregistered();
|
|
@@ -311,7 +314,7 @@ export class SkiaAccessibilityManager {
|
|
|
311
314
|
if (px.Right < -canvasWidthPx || px.Bottom < -canvasHeightPx || px.Left > 2 * canvasWidthPx || px.Top > 2 * canvasHeightPx)
|
|
312
315
|
continue;
|
|
313
316
|
list.push({
|
|
314
|
-
Id: n.AccessibilityId, Label: n.AccessibilityLabel, Hint: n.AccessibilityHint, Role: n.AccessibilityRole,
|
|
317
|
+
Id: n.AccessibilityId, Label: n.AccessibilityLabel, Hint: n.AccessibilityHint, Role: n.AccessibilityRole, Value: n.AccessibilityValue,
|
|
315
318
|
Rect: new SKRect(px.Left / scale, px.Top / scale, px.Right / scale, px.Bottom / scale),
|
|
316
319
|
CanInteract: n.AccessibilityCanInteract, IsPressed: n.AccessibilityIsPressed, Live: n.AccessibilityLive, Source: n,
|
|
317
320
|
Disabled: !n.AccessibilityCanInteract && Aria.IsInteractiveRole(n.AccessibilityRole),
|
|
@@ -347,7 +350,7 @@ export class SkiaAccessibilityManager {
|
|
|
347
350
|
for (let i = 0; i < a.length; i++) {
|
|
348
351
|
const x = a[i], y = b[i];
|
|
349
352
|
if (x.Source !== y.Source || x.Label !== y.Label || x.Hint !== y.Hint || x.Role !== y.Role || x.CanInteract !== y.CanInteract || x.Disabled !== y.Disabled
|
|
350
|
-
|| x.IsPressed !== y.IsPressed || x.Live !== y.Live
|
|
353
|
+
|| x.IsPressed !== y.IsPressed || x.Live !== y.Live || !SameValue(x.Value, y.Value)
|
|
351
354
|
|| Math.abs(x.Rect.Left - y.Rect.Left) > 0.5 || Math.abs(x.Rect.Top - y.Rect.Top) > 0.5
|
|
352
355
|
|| Math.abs(x.Rect.Right - y.Rect.Right) > 0.5 || Math.abs(x.Rect.Bottom - y.Rect.Bottom) > 0.5)
|
|
353
356
|
return false;
|
|
@@ -389,6 +392,9 @@ function SayNamesOnce(list) {
|
|
|
389
392
|
}
|
|
390
393
|
return said.size ? list.filter((x) => !said.has(x)) : list;
|
|
391
394
|
}
|
|
395
|
+
function SameValue(a, b) {
|
|
396
|
+
return a === b || (!!a && !!b && a.Now === b.Now && a.Min === b.Min && a.Max === b.Max && a.Step === b.Step && a.Text === b.Text);
|
|
397
|
+
}
|
|
392
398
|
// ---- group helpers (duck-typed layouts and scrolls) ----
|
|
393
399
|
/** Children of a control (a layout's Views, a scroll's Content). */
|
|
394
400
|
function ViewsOf(c) {
|
package/dist/core/Animators.d.ts
CHANGED
|
@@ -18,11 +18,28 @@ export declare class AnimatorBase {
|
|
|
18
18
|
StartFrameTimeNanos: number;
|
|
19
19
|
OnStart?: () => void;
|
|
20
20
|
OnStop?: () => void;
|
|
21
|
+
/** Paused: the canvas does not tick it and it wakes no frames; Resume goes on from where it was (C# Pause / Resume). */
|
|
22
|
+
IsPaused: boolean;
|
|
23
|
+
/** Paused by the canvas because its control is hidden; resumed when it shows again. */
|
|
24
|
+
PausedWhileHidden: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Frame time (ns) before which nothing it shows changes: the canvas does not tick it and it keeps no frames coming
|
|
27
|
+
* until then, a timer wakes the frame loop at that time (a GIF or a sprite between two frames; DrawnUi.Rust frame
|
|
28
|
+
* animators sleep between changes). 0 = tick every frame.
|
|
29
|
+
*/
|
|
30
|
+
SleepUntilNanos: number;
|
|
21
31
|
private delayHandle;
|
|
32
|
+
private delayEnd;
|
|
33
|
+
private delayLeft;
|
|
34
|
+
private pausedAt;
|
|
22
35
|
constructor(parent: SkiaControl);
|
|
23
36
|
protected Register(): boolean;
|
|
24
37
|
protected Unregister(): void;
|
|
25
38
|
Start(delayMs?: number): void;
|
|
39
|
+
/** Freezes the run (and what is left of a start delay); Resume goes on from there. */
|
|
40
|
+
Pause(): void;
|
|
41
|
+
/** Goes on from where Pause froze it: the paused time is not counted. */
|
|
42
|
+
Resume(): void;
|
|
26
43
|
Stop(): void;
|
|
27
44
|
/** Returns true when there is nothing more to do (not running / finished). */
|
|
28
45
|
TickFrame(frameTimeNanos: number): boolean;
|
|
@@ -55,10 +72,20 @@ export declare class SkiaValueAnimator extends AnimatorBase {
|
|
|
55
72
|
TickFrame(frameTimeNanos: number): boolean;
|
|
56
73
|
/** Passed over mValue; subclasses derive their own reported value here. */
|
|
57
74
|
protected TransformReportedValue(_deltaT: number): number;
|
|
58
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Updates mValue from elapsed time; true when the run is over. The eased value is not clamped to the range: a spring
|
|
77
|
+
* easing goes past its ends, and a run whose mMinValue is above mMaxValue (a ping-pong's way back) goes down.
|
|
78
|
+
*/
|
|
59
79
|
protected UpdateValue(_deltaT: number, deltaFromStart: number): boolean;
|
|
60
80
|
protected FinishedRunning(): boolean;
|
|
61
81
|
}
|
|
82
|
+
/** DrawnUi PingPongAnimator: every repeat runs the other way (Repeat -1 = forever by default). */
|
|
83
|
+
export declare class PingPongAnimator extends SkiaValueAnimator {
|
|
84
|
+
/** One way done, the next starts (C# CycleFInished). */
|
|
85
|
+
CycleFinished?: () => void;
|
|
86
|
+
constructor(parent: SkiaControl);
|
|
87
|
+
protected FinishedRunning(): boolean;
|
|
88
|
+
}
|
|
62
89
|
/** DrawnUi RenderingAnimator: a value animator that paints itself above its Parent (IOverlayEffect). */
|
|
63
90
|
export declare class RenderingAnimator extends SkiaValueAnimator implements IOverlayEffect {
|
|
64
91
|
constructor(parent: SkiaControl);
|
package/dist/core/Animators.js
CHANGED
|
@@ -16,7 +16,20 @@ export class AnimatorBase {
|
|
|
16
16
|
StartFrameTimeNanos = 0;
|
|
17
17
|
OnStart;
|
|
18
18
|
OnStop;
|
|
19
|
+
/** Paused: the canvas does not tick it and it wakes no frames; Resume goes on from where it was (C# Pause / Resume). */
|
|
20
|
+
IsPaused = false;
|
|
21
|
+
/** Paused by the canvas because its control is hidden; resumed when it shows again. */
|
|
22
|
+
PausedWhileHidden = false;
|
|
23
|
+
/**
|
|
24
|
+
* Frame time (ns) before which nothing it shows changes: the canvas does not tick it and it keeps no frames coming
|
|
25
|
+
* until then, a timer wakes the frame loop at that time (a GIF or a sprite between two frames; DrawnUi.Rust frame
|
|
26
|
+
* animators sleep between changes). 0 = tick every frame.
|
|
27
|
+
*/
|
|
28
|
+
SleepUntilNanos = 0;
|
|
19
29
|
delayHandle = 0;
|
|
30
|
+
delayEnd = 0;
|
|
31
|
+
delayLeft = 0;
|
|
32
|
+
pausedAt = 0;
|
|
20
33
|
constructor(parent) { this.Parent = parent; }
|
|
21
34
|
Register() {
|
|
22
35
|
const parent = this.Parent;
|
|
@@ -60,6 +73,8 @@ export class AnimatorBase {
|
|
|
60
73
|
this.delayHandle = 0;
|
|
61
74
|
}
|
|
62
75
|
if (delayMs > 0) {
|
|
76
|
+
// the delay waits without frames
|
|
77
|
+
this.delayEnd = performance.now() + delayMs;
|
|
63
78
|
this.delayHandle = window.setTimeout(() => { this.delayHandle = 0; this.Start(); }, delayMs);
|
|
64
79
|
return;
|
|
65
80
|
}
|
|
@@ -72,7 +87,43 @@ export class AnimatorBase {
|
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
89
|
}
|
|
90
|
+
/** Freezes the run (and what is left of a start delay); Resume goes on from there. */
|
|
91
|
+
Pause() {
|
|
92
|
+
if (this.IsPaused)
|
|
93
|
+
return;
|
|
94
|
+
this.IsPaused = true;
|
|
95
|
+
this.pausedAt = performance.now();
|
|
96
|
+
if (this.delayHandle) {
|
|
97
|
+
clearTimeout(this.delayHandle);
|
|
98
|
+
this.delayHandle = 0;
|
|
99
|
+
this.delayLeft = Math.max(0, this.delayEnd - this.pausedAt);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Goes on from where Pause froze it: the paused time is not counted. */
|
|
103
|
+
Resume() {
|
|
104
|
+
if (!this.IsPaused)
|
|
105
|
+
return;
|
|
106
|
+
this.IsPaused = false;
|
|
107
|
+
const paused = (performance.now() - this.pausedAt) * 1_000_000;
|
|
108
|
+
if (this.StartFrameTimeNanos !== 0) {
|
|
109
|
+
this.StartFrameTimeNanos += paused;
|
|
110
|
+
this.LastFrameTimeNanos += paused;
|
|
111
|
+
}
|
|
112
|
+
if (this.SleepUntilNanos !== 0)
|
|
113
|
+
this.SleepUntilNanos += paused;
|
|
114
|
+
if (this.delayLeft > 0) {
|
|
115
|
+
const left = this.delayLeft;
|
|
116
|
+
this.delayLeft = 0;
|
|
117
|
+
this.Start(left);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.Parent?.Repaint(); // frames again
|
|
121
|
+
}
|
|
75
122
|
Stop() {
|
|
123
|
+
this.IsPaused = false;
|
|
124
|
+
this.PausedWhileHidden = false;
|
|
125
|
+
this.SleepUntilNanos = 0;
|
|
126
|
+
this.delayLeft = 0;
|
|
76
127
|
if (this.delayHandle) {
|
|
77
128
|
clearTimeout(this.delayHandle);
|
|
78
129
|
this.delayHandle = 0;
|
|
@@ -155,23 +206,21 @@ export class SkiaValueAnimator extends AnimatorBase {
|
|
|
155
206
|
}
|
|
156
207
|
/** Passed over mValue; subclasses derive their own reported value here. */
|
|
157
208
|
TransformReportedValue(_deltaT) { return this.mValue; }
|
|
158
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* Updates mValue from elapsed time; true when the run is over. The eased value is not clamped to the range: a spring
|
|
211
|
+
* easing goes past its ends, and a run whose mMinValue is above mMaxValue (a ping-pong's way back) goes down.
|
|
212
|
+
*/
|
|
159
213
|
UpdateValue(_deltaT, deltaFromStart) {
|
|
160
214
|
const elapsedMs = deltaFromStart / 1_000_000;
|
|
161
215
|
const progress = this.Speed > 0 ? elapsedMs / this.Speed : 1;
|
|
162
216
|
const deltaValue = this.mMaxValue - this.mMinValue;
|
|
163
217
|
const eased = this.Easing.Ease(Math.min(progress, 1));
|
|
164
218
|
this.Progress = eased;
|
|
165
|
-
|
|
166
|
-
if (value < this.mMinValue) {
|
|
167
|
-
this.mValue = this.mMinValue;
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
if (value >= this.mMaxValue || progress >= 1) {
|
|
219
|
+
if (progress >= 1) {
|
|
171
220
|
this.mValue = this.mMaxValue;
|
|
172
221
|
return true;
|
|
173
222
|
}
|
|
174
|
-
this.mValue =
|
|
223
|
+
this.mValue = deltaValue * eased + this.mMinValue;
|
|
175
224
|
return false;
|
|
176
225
|
}
|
|
177
226
|
FinishedRunning() {
|
|
@@ -188,6 +237,25 @@ export class SkiaValueAnimator extends AnimatorBase {
|
|
|
188
237
|
return true;
|
|
189
238
|
}
|
|
190
239
|
}
|
|
240
|
+
/** DrawnUi PingPongAnimator: every repeat runs the other way (Repeat -1 = forever by default). */
|
|
241
|
+
export class PingPongAnimator extends SkiaValueAnimator {
|
|
242
|
+
/** One way done, the next starts (C# CycleFInished). */
|
|
243
|
+
CycleFinished;
|
|
244
|
+
constructor(parent) { super(parent); this.Repeat = -1; }
|
|
245
|
+
FinishedRunning() {
|
|
246
|
+
if (this.Repeat !== 0) {
|
|
247
|
+
this.CycleFinished?.();
|
|
248
|
+
if (this.Repeat > 0)
|
|
249
|
+
this.Repeat--;
|
|
250
|
+
[this.mMinValue, this.mMaxValue] = [this.mMaxValue, this.mMinValue];
|
|
251
|
+
this.mValue = this.mMinValue;
|
|
252
|
+
this.LastFrameTimeNanos = 0;
|
|
253
|
+
this.StartFrameTimeNanos = 0;
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
return super.FinishedRunning();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
191
259
|
/** DrawnUi RenderingAnimator: a value animator that paints itself above its Parent (IOverlayEffect). */
|
|
192
260
|
export class RenderingAnimator extends SkiaValueAnimator {
|
|
193
261
|
constructor(parent) { super(parent); }
|