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.
- package/LICENSE +21 -0
- package/PARITY.md +287 -0
- package/README.md +92 -0
- package/SKIPPED.md +107 -0
- package/dist/controls/AnimatedFramesRenderer.d.ts +58 -0
- package/dist/controls/AnimatedFramesRenderer.js +133 -0
- package/dist/controls/GridStructure.d.ts +79 -0
- package/dist/controls/GridStructure.js +403 -0
- package/dist/controls/RefreshIndicator.d.ts +35 -0
- package/dist/controls/RefreshIndicator.js +82 -0
- package/dist/controls/SkiaBackdrop.d.ts +25 -0
- package/dist/controls/SkiaBackdrop.js +80 -0
- package/dist/controls/SkiaButton.d.ts +58 -0
- package/dist/controls/SkiaButton.js +148 -0
- package/dist/controls/SkiaCarousel.d.ts +133 -0
- package/dist/controls/SkiaCarousel.js +603 -0
- package/dist/controls/SkiaCheckbox.d.ts +21 -0
- package/dist/controls/SkiaCheckbox.js +118 -0
- package/dist/controls/SkiaDecoratedGrid.d.ts +15 -0
- package/dist/controls/SkiaDecoratedGrid.js +46 -0
- package/dist/controls/SkiaDrawer.d.ts +46 -0
- package/dist/controls/SkiaDrawer.js +212 -0
- package/dist/controls/SkiaDynamicDrawnCell.d.ts +11 -0
- package/dist/controls/SkiaDynamicDrawnCell.js +17 -0
- package/dist/controls/SkiaEditor.d.ts +163 -0
- package/dist/controls/SkiaEditor.js +803 -0
- package/dist/controls/SkiaGif.d.ts +54 -0
- package/dist/controls/SkiaGif.js +206 -0
- package/dist/controls/SkiaHotspot.d.ts +13 -0
- package/dist/controls/SkiaHotspot.js +36 -0
- package/dist/controls/SkiaImage.d.ts +75 -0
- package/dist/controls/SkiaImage.js +222 -0
- package/dist/controls/SkiaImageTiles.d.ts +27 -0
- package/dist/controls/SkiaImageTiles.js +76 -0
- package/dist/controls/SkiaLabel.d.ts +160 -0
- package/dist/controls/SkiaLabel.js +580 -0
- package/dist/controls/SkiaLayout.d.ts +163 -0
- package/dist/controls/SkiaLayout.js +723 -0
- package/dist/controls/SkiaLottie.d.ts +75 -0
- package/dist/controls/SkiaLottie.js +290 -0
- package/dist/controls/SkiaProgress.d.ts +40 -0
- package/dist/controls/SkiaProgress.js +80 -0
- package/dist/controls/SkiaRadioButton.d.ts +32 -0
- package/dist/controls/SkiaRadioButton.js +157 -0
- package/dist/controls/SkiaRichLabel.d.ts +77 -0
- package/dist/controls/SkiaRichLabel.js +274 -0
- package/dist/controls/SkiaScroll.d.ts +231 -0
- package/dist/controls/SkiaScroll.js +958 -0
- package/dist/controls/SkiaScrollBar.d.ts +56 -0
- package/dist/controls/SkiaScrollBar.js +158 -0
- package/dist/controls/SkiaShaderCarousel.d.ts +67 -0
- package/dist/controls/SkiaShaderCarousel.js +246 -0
- package/dist/controls/SkiaShape.d.ts +63 -0
- package/dist/controls/SkiaShape.js +317 -0
- package/dist/controls/SkiaSlider.d.ts +74 -0
- package/dist/controls/SkiaSlider.js +243 -0
- package/dist/controls/SkiaSprite.d.ts +99 -0
- package/dist/controls/SkiaSprite.js +324 -0
- package/dist/controls/SkiaSpriteSet.d.ts +27 -0
- package/dist/controls/SkiaSpriteSet.js +75 -0
- package/dist/controls/SkiaSvg.d.ts +43 -0
- package/dist/controls/SkiaSvg.js +181 -0
- package/dist/controls/SkiaSwitch.d.ts +26 -0
- package/dist/controls/SkiaSwitch.js +147 -0
- package/dist/controls/SkiaToggle.d.ts +50 -0
- package/dist/controls/SkiaToggle.js +71 -0
- package/dist/controls/SnappingLayout.d.ts +78 -0
- package/dist/controls/SnappingLayout.js +217 -0
- package/dist/controls/TextSpan.d.ts +70 -0
- package/dist/controls/TextSpan.js +76 -0
- package/dist/core/Accessibility.d.ts +104 -0
- package/dist/core/Accessibility.js +150 -0
- package/dist/core/Animators.d.ts +82 -0
- package/dist/core/Animators.js +247 -0
- package/dist/core/Canvas.d.ts +89 -0
- package/dist/core/Canvas.js +328 -0
- package/dist/core/ControlStyle.d.ts +6 -0
- package/dist/core/ControlStyle.js +11 -0
- package/dist/core/Easing.d.ts +13 -0
- package/dist/core/Easing.js +15 -0
- package/dist/core/Gestures.d.ts +74 -0
- package/dist/core/Gestures.js +104 -0
- package/dist/core/ImageEffects.d.ts +29 -0
- package/dist/core/ImageEffects.js +55 -0
- package/dist/core/KeyboardManager.d.ts +25 -0
- package/dist/core/KeyboardManager.js +54 -0
- package/dist/core/ScrollAnimators.d.ts +90 -0
- package/dist/core/ScrollAnimators.js +244 -0
- package/dist/core/SkiaControl.d.ts +401 -0
- package/dist/core/SkiaControl.js +1239 -0
- package/dist/core/SkiaEffect.d.ts +216 -0
- package/dist/core/SkiaEffect.js +571 -0
- package/dist/core/SkiaImageManager.d.ts +43 -0
- package/dist/core/SkiaImageManager.js +122 -0
- package/dist/core/Super.d.ts +61 -0
- package/dist/core/Super.js +178 -0
- package/dist/core/TextInputProxy.d.ts +31 -0
- package/dist/core/TextInputProxy.js +144 -0
- package/dist/core/Types.d.ts +177 -0
- package/dist/core/Types.js +122 -0
- package/dist/core/ViewsAdapter.d.ts +41 -0
- package/dist/core/ViewsAdapter.js +113 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +48 -0
- package/dist/react/SkiaShell.d.ts +144 -0
- package/dist/react/SkiaShell.js +490 -0
- package/dist/react/index.d.ts +110 -0
- package/dist/react/index.js +158 -0
- package/dist/react/reconciler.d.ts +14 -0
- package/dist/react/reconciler.js +160 -0
- package/package.json +60 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ScrollOrientation } from "../core/Types";
|
|
2
|
+
import { SkiaLayout } from "./SkiaLayout";
|
|
3
|
+
/** C# IRefreshIndicator: the pull-to-refresh view a SkiaScroll drives while overscrolling. */
|
|
4
|
+
export interface IRefreshIndicator {
|
|
5
|
+
/** ratio = overscroll / RefreshShowDistance (not clamped by the caller), ptsScrollOffset = current offset in points. */
|
|
6
|
+
SetDragRatio(ratio: number, ptsScrollOffset: number, ptsLimit: number, ptsTrigger: number): void;
|
|
7
|
+
IsVisible: boolean;
|
|
8
|
+
IsRunning: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Mirrors DrawnUi RefreshIndicator: a layout parked above (or left of) the viewport that `SetDragRatio` pulls into
|
|
12
|
+
* view with the C# position curve and fades in; `IsRunning` becomes true once fully shown (the refresh is in
|
|
13
|
+
* progress) — override `OnIsRunningChanged` to start / stop your animation. Put the visuals inside as children.
|
|
14
|
+
*/
|
|
15
|
+
export declare class RefreshIndicator extends SkiaLayout implements IRefreshIndicator {
|
|
16
|
+
private orientation;
|
|
17
|
+
get Orientation(): ScrollOrientation;
|
|
18
|
+
set Orientation(v: ScrollOrientation);
|
|
19
|
+
private isRunning;
|
|
20
|
+
/** Read-only: the indicator is fully shown (refresh in progress). */
|
|
21
|
+
get IsRunning(): boolean;
|
|
22
|
+
set IsRunning(v: boolean);
|
|
23
|
+
VisibleRatio: number;
|
|
24
|
+
constructor();
|
|
25
|
+
/** Only swaps alignments still at the previous orientation's defaults (C# UpdateOrientation). */
|
|
26
|
+
protected UpdateOrientation(previous: ScrollOrientation): void;
|
|
27
|
+
/**
|
|
28
|
+
* C# SetDragRatio: 0..1 (clamped here) -> position + opacity; running once fully shown. The view slides in with the
|
|
29
|
+
* pull: its far edge follows the overscroll until ptsLimit (RefreshShowDistance), where it stays while refreshing.
|
|
30
|
+
*/
|
|
31
|
+
SetDragRatio(ratio: number, ptsScrollOffset: number, ptsLimit: number, _ptsTrigger: number): void;
|
|
32
|
+
/** C# SetAnimationState: shown while it has any opacity. */
|
|
33
|
+
SetAnimationState(running: boolean): void;
|
|
34
|
+
protected OnIsRunningChanged(_value: boolean): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { SkiaLayout } from "./SkiaLayout";
|
|
2
|
+
/**
|
|
3
|
+
* Mirrors DrawnUi RefreshIndicator: a layout parked above (or left of) the viewport that `SetDragRatio` pulls into
|
|
4
|
+
* view with the C# position curve and fades in; `IsRunning` becomes true once fully shown (the refresh is in
|
|
5
|
+
* progress) — override `OnIsRunningChanged` to start / stop your animation. Put the visuals inside as children.
|
|
6
|
+
*/
|
|
7
|
+
export class RefreshIndicator extends SkiaLayout {
|
|
8
|
+
orientation = "Vertical";
|
|
9
|
+
get Orientation() { return this.orientation; }
|
|
10
|
+
set Orientation(v) {
|
|
11
|
+
if (this.orientation === v)
|
|
12
|
+
return;
|
|
13
|
+
if (v === "Both")
|
|
14
|
+
throw new Error("DrawnUi: RefreshIndicator supports Vertical or Horizontal");
|
|
15
|
+
const prev = this.orientation;
|
|
16
|
+
this.orientation = v;
|
|
17
|
+
this.UpdateOrientation(prev);
|
|
18
|
+
}
|
|
19
|
+
isRunning = false;
|
|
20
|
+
/** Read-only: the indicator is fully shown (refresh in progress). */
|
|
21
|
+
get IsRunning() { return this.isRunning; }
|
|
22
|
+
set IsRunning(v) { if (this.isRunning !== v) {
|
|
23
|
+
this.isRunning = v;
|
|
24
|
+
this.OnIsRunningChanged(v);
|
|
25
|
+
} }
|
|
26
|
+
VisibleRatio = 0;
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.Type = "Absolute";
|
|
30
|
+
this.InputTransparent = true;
|
|
31
|
+
this.HorizontalOptions = "Fill";
|
|
32
|
+
this.VerticalOptions = "Start";
|
|
33
|
+
this.IsVisible = false;
|
|
34
|
+
}
|
|
35
|
+
/** Only swaps alignments still at the previous orientation's defaults (C# UpdateOrientation). */
|
|
36
|
+
UpdateOrientation(previous) {
|
|
37
|
+
const [oldH, oldV] = previous === "Horizontal" ? ["Start", "Fill"] : ["Fill", "Start"];
|
|
38
|
+
const [newH, newV] = this.orientation === "Horizontal" ? ["Start", "Fill"] : ["Fill", "Start"];
|
|
39
|
+
if (this.HorizontalOptions === oldH)
|
|
40
|
+
this.HorizontalOptions = newH;
|
|
41
|
+
if (this.VerticalOptions === oldV)
|
|
42
|
+
this.VerticalOptions = newV;
|
|
43
|
+
this.InvalidateMeasure();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* C# SetDragRatio: 0..1 (clamped here) -> position + opacity; running once fully shown. The view slides in with the
|
|
47
|
+
* pull: its far edge follows the overscroll until ptsLimit (RefreshShowDistance), where it stays while refreshing.
|
|
48
|
+
*/
|
|
49
|
+
SetDragRatio(ratio, ptsScrollOffset, ptsLimit, _ptsTrigger) {
|
|
50
|
+
ratio = Math.max(0, Math.min(1, ratio));
|
|
51
|
+
this.VisibleRatio = ratio;
|
|
52
|
+
let opacity = ratio;
|
|
53
|
+
const scale = this.RenderingScale || 1;
|
|
54
|
+
const size = (this.orientation === "Horizontal" ? this.MeasuredSize.Pixels.Width : this.MeasuredSize.Pixels.Height) / scale;
|
|
55
|
+
if (ratio <= 0) {
|
|
56
|
+
// parked just outside the viewport (C#: dividing by 0 poisoned the transform, so it is placed explicitly)
|
|
57
|
+
if (this.orientation === "Horizontal")
|
|
58
|
+
this.TranslationX = -size;
|
|
59
|
+
else
|
|
60
|
+
this.TranslationY = -size;
|
|
61
|
+
opacity = 0;
|
|
62
|
+
}
|
|
63
|
+
else if (size > 0) {
|
|
64
|
+
const shown = Math.min(Math.max(0, ptsScrollOffset), ptsLimit);
|
|
65
|
+
const pos = shown - size + Math.max(0, (ptsLimit - size) / 2); // centered in the gap when the gap is taller than the view
|
|
66
|
+
if (this.orientation === "Horizontal")
|
|
67
|
+
this.TranslationX = pos;
|
|
68
|
+
else
|
|
69
|
+
this.TranslationY = pos;
|
|
70
|
+
opacity = ratio;
|
|
71
|
+
}
|
|
72
|
+
this.IsRunning = opacity >= 1;
|
|
73
|
+
if (this.IsRunning)
|
|
74
|
+
opacity = 1;
|
|
75
|
+
this.Opacity = opacity;
|
|
76
|
+
this.SetAnimationState(opacity !== 0);
|
|
77
|
+
this.RepaintComposition();
|
|
78
|
+
}
|
|
79
|
+
/** C# SetAnimationState: shown while it has any opacity. */
|
|
80
|
+
SetAnimationState(running) { this.IsVisible = running; }
|
|
81
|
+
OnIsRunningChanged(_value) { }
|
|
82
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Image } from "canvaskit-wasm";
|
|
2
|
+
import type { DrawingContext } from "../core/SkiaControl";
|
|
3
|
+
import { SkiaLayout } from "./SkiaLayout";
|
|
4
|
+
/**
|
|
5
|
+
* Mirrors DrawnUi SkiaBackdrop: draws what is already painted beneath its box through `Blur` (points, default 5)
|
|
6
|
+
* and `Brightness` (gamma, 1 = unchanged), tinted by `BackgroundColor`; children draw first and are blurred too.
|
|
7
|
+
* The snapshot comes from the surface being drawn (`UseContext`) or the on-screen surface. Not cached (re-blurs
|
|
8
|
+
* on every frame it is drawn in; inside a cached parent it is re-recorded with that parent).
|
|
9
|
+
*/
|
|
10
|
+
export declare class SkiaBackdrop extends SkiaLayout {
|
|
11
|
+
Blur: number;
|
|
12
|
+
Brightness: number;
|
|
13
|
+
/** Snapshot the surface currently drawn into (true) or the on-screen surface (false). */
|
|
14
|
+
UseContext: boolean;
|
|
15
|
+
private snapshot?;
|
|
16
|
+
constructor();
|
|
17
|
+
get HasEffects(): boolean;
|
|
18
|
+
/** The last snapshot taken (C# GetImage hands it over: the caller owns it afterwards). */
|
|
19
|
+
GetImage(): Image | undefined;
|
|
20
|
+
/** Background is painted inside Paint(), after the children and before the snapshot (C# order). */
|
|
21
|
+
protected PaintBackground(_ctx: DrawingContext): void;
|
|
22
|
+
protected PaintsBackgroundWithoutColor(): boolean;
|
|
23
|
+
protected Paint(ctx: DrawingContext): void;
|
|
24
|
+
protected OnDisposing(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Super } from "../core/Super";
|
|
2
|
+
import { SkiaImageEffects } from "../core/ImageEffects";
|
|
3
|
+
import { SkiaLayout } from "./SkiaLayout";
|
|
4
|
+
/**
|
|
5
|
+
* Mirrors DrawnUi SkiaBackdrop: draws what is already painted beneath its box through `Blur` (points, default 5)
|
|
6
|
+
* and `Brightness` (gamma, 1 = unchanged), tinted by `BackgroundColor`; children draw first and are blurred too.
|
|
7
|
+
* The snapshot comes from the surface being drawn (`UseContext`) or the on-screen surface. Not cached (re-blurs
|
|
8
|
+
* on every frame it is drawn in; inside a cached parent it is re-recorded with that parent).
|
|
9
|
+
*/
|
|
10
|
+
export class SkiaBackdrop extends SkiaLayout {
|
|
11
|
+
Blur = 5;
|
|
12
|
+
Brightness = 1;
|
|
13
|
+
/** Snapshot the surface currently drawn into (true) or the on-screen surface (false). */
|
|
14
|
+
UseContext = true;
|
|
15
|
+
snapshot;
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.HorizontalOptions = "Fill";
|
|
19
|
+
this.VerticalOptions = "Fill";
|
|
20
|
+
}
|
|
21
|
+
get HasEffects() { return this.Blur !== 0 || this.Brightness !== 1; }
|
|
22
|
+
/** The last snapshot taken (C# GetImage hands it over: the caller owns it afterwards). */
|
|
23
|
+
GetImage() { const i = this.snapshot; this.snapshot = undefined; return i; }
|
|
24
|
+
/** Background is painted inside Paint(), after the children and before the snapshot (C# order). */
|
|
25
|
+
PaintBackground(_ctx) { }
|
|
26
|
+
PaintsBackgroundWithoutColor() { return false; }
|
|
27
|
+
Paint(ctx) {
|
|
28
|
+
const d = ctx.Destination;
|
|
29
|
+
if (d.Width <= 0 || d.Height <= 0)
|
|
30
|
+
return;
|
|
31
|
+
const CK = Super.CK;
|
|
32
|
+
const canvas = ctx.Context.Canvas;
|
|
33
|
+
super.Paint(ctx); // children
|
|
34
|
+
if (this.BackgroundColor) {
|
|
35
|
+
const paint = this.CreateBackgroundPaint(d);
|
|
36
|
+
canvas.drawRect(CK.LTRBRect(d.Left, d.Top, d.Right, d.Bottom), paint);
|
|
37
|
+
paint.delete();
|
|
38
|
+
}
|
|
39
|
+
if (!this.HasEffects)
|
|
40
|
+
return;
|
|
41
|
+
// C# UseContext: snapshot the surface the pixels end up on — the on-screen surface or the nearest Image cache
|
|
42
|
+
// surface (Origin = its top-left in canvas pixels). Content beneath is already there because caches are recorded
|
|
43
|
+
// inline while the parent paints, so this also works while an Operations picture is being recorded.
|
|
44
|
+
const surface = this.UseContext ? ctx.Context.Surface : this.Superview?.Surface;
|
|
45
|
+
if (!surface)
|
|
46
|
+
return;
|
|
47
|
+
surface.flush();
|
|
48
|
+
const origin = this.UseContext ? ctx.Context.Origin : undefined;
|
|
49
|
+
const ox = origin?.X ?? 0, oy = origin?.Y ?? 0;
|
|
50
|
+
const l = Math.floor(d.Left - ox), t = Math.floor(d.Top - oy), r = Math.ceil(d.Right - ox), b = Math.ceil(d.Bottom - oy);
|
|
51
|
+
// whole-surface snapshot, sub-rect on draw: a bounded snapshot of a GPU surface is not origin-safe in CanvasKit
|
|
52
|
+
const image = surface.makeImageSnapshot();
|
|
53
|
+
if (!image)
|
|
54
|
+
return;
|
|
55
|
+
const paint = new CK.Paint();
|
|
56
|
+
const blur = this.Blur > 0 ? CK.ImageFilter.MakeBlur(this.Blur * ctx.Scale, this.Blur * ctx.Scale, CK.TileMode.Mirror, null) : null;
|
|
57
|
+
if (blur)
|
|
58
|
+
paint.setImageFilter(blur);
|
|
59
|
+
const gamma = this.Brightness !== 1 ? SkiaImageEffects.Gamma(this.Brightness) : null;
|
|
60
|
+
if (gamma)
|
|
61
|
+
paint.setColorFilter(gamma);
|
|
62
|
+
const saved = canvas.save();
|
|
63
|
+
canvas.clipRect(CK.LTRBRect(d.Left, d.Top, d.Right, d.Bottom), CK.ClipOp.Intersect, true);
|
|
64
|
+
canvas.drawImageRectOptions(image, CK.LTRBRect(l, t, r, b), CK.LTRBRect(d.Left, d.Top, d.Right, d.Bottom), CK.FilterMode.Linear, CK.MipmapMode.None, paint);
|
|
65
|
+
canvas.restoreToCount(saved);
|
|
66
|
+
blur?.delete();
|
|
67
|
+
gamma?.delete();
|
|
68
|
+
paint.delete();
|
|
69
|
+
const kill = this.snapshot;
|
|
70
|
+
this.snapshot = image;
|
|
71
|
+
kill?.delete();
|
|
72
|
+
// What is beneath may change without invalidating this branch (an image that loads later, a sibling animating):
|
|
73
|
+
// stale the ancestors' caches for the NEXT frame, after the current record has finished, without asking for one.
|
|
74
|
+
queueMicrotask(() => { let p = this.Parent; while (p) {
|
|
75
|
+
p.InvalidateCache();
|
|
76
|
+
p = p.Parent;
|
|
77
|
+
} });
|
|
78
|
+
}
|
|
79
|
+
OnDisposing() { this.snapshot?.delete(); this.snapshot = undefined; }
|
|
80
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Path } from "canvaskit-wasm";
|
|
2
|
+
import type { SkiaControl } from "../core/SkiaControl";
|
|
3
|
+
import { type Color, type CornerRadius, ScaledSize, type SkiaTouchAnimation } from "../core/Types";
|
|
4
|
+
import { type GestureEventProcessingInfo, type SkiaGesturesParameters } from "../core/Gestures";
|
|
5
|
+
import { SkiaLayout } from "./SkiaLayout";
|
|
6
|
+
import { type PrebuiltControlStyle, type ResolvedControlStyle } from "../core/ControlStyle";
|
|
7
|
+
/**
|
|
8
|
+
* Mirrors DrawnUi SkiaButton (default style): a SkiaShape frame tagged "BtnShape" + a centered SkiaLabel
|
|
9
|
+
* tagged "BtnText". Consumes Down/Up/Tapped inside its rect; releases the pressed state when a pan exceeds
|
|
10
|
+
* PanThreshold. Press feedback = ApplyEffect (ripple), no darkening.
|
|
11
|
+
*/
|
|
12
|
+
export declare class SkiaButton extends SkiaLayout {
|
|
13
|
+
static PanThreshold: number;
|
|
14
|
+
Text: string;
|
|
15
|
+
/** Look of the default content (C# SkiaButton.ControlStyle): background, corners, font, minimum size when left unset. */
|
|
16
|
+
ControlStyle: PrebuiltControlStyle;
|
|
17
|
+
TextColor: Color;
|
|
18
|
+
FontSize: number;
|
|
19
|
+
FontFamily: string;
|
|
20
|
+
/** Glyph fallback chain for the label (React extension, same semantics as SkiaLabel.FontFamilyFallback). */
|
|
21
|
+
FontFamilyFallback: string;
|
|
22
|
+
/** Frame corner radius, points (DrawnUi default look uses 8). */
|
|
23
|
+
CornerRadius: CornerRadius | number;
|
|
24
|
+
StrokeColor: Color;
|
|
25
|
+
StrokeWidth: number;
|
|
26
|
+
IsPressed: boolean;
|
|
27
|
+
IsDisabled: boolean;
|
|
28
|
+
LockPanning: boolean;
|
|
29
|
+
/** Touch feedback played on Down (DrawnUi SkiaButton.ApplyEffect). */
|
|
30
|
+
ApplyEffect: SkiaTouchAnimation;
|
|
31
|
+
TotalDown: number;
|
|
32
|
+
TotalTapped: number;
|
|
33
|
+
Down?: (sender: SkiaButton, args: SkiaGesturesParameters) => void;
|
|
34
|
+
Up?: (sender: SkiaButton, args: SkiaGesturesParameters) => void;
|
|
35
|
+
private readonly frame;
|
|
36
|
+
private readonly label;
|
|
37
|
+
private lastDownPts;
|
|
38
|
+
private hadDown;
|
|
39
|
+
constructor();
|
|
40
|
+
/** Set to `Aria.RoleButton` to expose every button (React extension). */
|
|
41
|
+
static DefaultAccessibilityRole?: string;
|
|
42
|
+
protected DefaultAccessibilityLabel(): string | undefined;
|
|
43
|
+
protected DefaultAccessibilityCanInteract(): boolean;
|
|
44
|
+
/** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
|
|
45
|
+
protected PaintBackground(): void;
|
|
46
|
+
get UsingControlStyle(): ResolvedControlStyle;
|
|
47
|
+
/** C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size. */
|
|
48
|
+
private static Look;
|
|
49
|
+
protected MeasureAbsolute(w: number, h: number, scale: number): ScaledSize;
|
|
50
|
+
/** The frame sits under the padding box: arrange children in the full rect, the label centered inside padding. */
|
|
51
|
+
protected OnLayoutChanged(): void;
|
|
52
|
+
/** Ripple clipped to the rounded frame. */
|
|
53
|
+
CreateClip(): Path;
|
|
54
|
+
/** DrawnUi SkiaButton.OnDown: press feedback; true = consume Down. */
|
|
55
|
+
protected OnDown(_args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): boolean;
|
|
56
|
+
private SetUp;
|
|
57
|
+
ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
|
|
58
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Colors, SKRect, ScaledSize, SkiaShadow, Thickness } from "../core/Types";
|
|
2
|
+
import { SKPoint } from "../core/Gestures";
|
|
3
|
+
import { SkiaLayout } from "./SkiaLayout";
|
|
4
|
+
import { SkiaLabel } from "./SkiaLabel";
|
|
5
|
+
import { SkiaShape } from "./SkiaShape";
|
|
6
|
+
import { ResolveControlStyle } from "../core/ControlStyle";
|
|
7
|
+
/**
|
|
8
|
+
* Mirrors DrawnUi SkiaButton (default style): a SkiaShape frame tagged "BtnShape" + a centered SkiaLabel
|
|
9
|
+
* tagged "BtnText". Consumes Down/Up/Tapped inside its rect; releases the pressed state when a pan exceeds
|
|
10
|
+
* PanThreshold. Press feedback = ApplyEffect (ripple), no darkening.
|
|
11
|
+
*/
|
|
12
|
+
export class SkiaButton extends SkiaLayout {
|
|
13
|
+
static PanThreshold = 5;
|
|
14
|
+
Text = "";
|
|
15
|
+
/** Look of the default content (C# SkiaButton.ControlStyle): background, corners, font, minimum size when left unset. */
|
|
16
|
+
ControlStyle = "Unset";
|
|
17
|
+
TextColor = Colors.White;
|
|
18
|
+
FontSize = 15;
|
|
19
|
+
FontFamily = "";
|
|
20
|
+
/** Glyph fallback chain for the label (React extension, same semantics as SkiaLabel.FontFamilyFallback). */
|
|
21
|
+
FontFamilyFallback = "";
|
|
22
|
+
/** Frame corner radius, points (DrawnUi default look uses 8). */
|
|
23
|
+
CornerRadius = 8;
|
|
24
|
+
StrokeColor = Colors.Transparent;
|
|
25
|
+
StrokeWidth = 0;
|
|
26
|
+
IsPressed = false;
|
|
27
|
+
IsDisabled = false;
|
|
28
|
+
LockPanning = false;
|
|
29
|
+
/** Touch feedback played on Down (DrawnUi SkiaButton.ApplyEffect). */
|
|
30
|
+
ApplyEffect = "Ripple"; // C# default
|
|
31
|
+
TotalDown = 0;
|
|
32
|
+
TotalTapped = 0;
|
|
33
|
+
Down;
|
|
34
|
+
Up;
|
|
35
|
+
frame = new SkiaShape();
|
|
36
|
+
label = new SkiaLabel();
|
|
37
|
+
lastDownPts = SKPoint.Empty;
|
|
38
|
+
hadDown = false;
|
|
39
|
+
constructor() {
|
|
40
|
+
super();
|
|
41
|
+
this.Type = "Absolute";
|
|
42
|
+
this.Padding = new Thickness(16, 10);
|
|
43
|
+
this.frame.Tag = "BtnShape";
|
|
44
|
+
this.frame.HorizontalOptions = "Fill";
|
|
45
|
+
this.frame.VerticalOptions = "Fill";
|
|
46
|
+
this.label.Tag = "BtnText";
|
|
47
|
+
this.label.AccessibilityRole = "presentation"; // the button is the accessible node, not its inner label (C# MainLabel.AccessibilityRole = null)
|
|
48
|
+
this.label.HorizontalOptions = "Center";
|
|
49
|
+
this.label.VerticalOptions = "Center";
|
|
50
|
+
this.AddSubView(this.frame);
|
|
51
|
+
this.AddSubView(this.label);
|
|
52
|
+
}
|
|
53
|
+
/** Set to `Aria.RoleButton` to expose every button (React extension). */
|
|
54
|
+
static DefaultAccessibilityRole;
|
|
55
|
+
DefaultAccessibilityLabel() { return this.Text || undefined; }
|
|
56
|
+
DefaultAccessibilityCanInteract() { return !this.IsDisabled; }
|
|
57
|
+
/** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
|
|
58
|
+
PaintBackground() { }
|
|
59
|
+
get UsingControlStyle() { return ResolveControlStyle(this.ControlStyle); }
|
|
60
|
+
/** C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size. */
|
|
61
|
+
static Look(s) {
|
|
62
|
+
switch (s) {
|
|
63
|
+
case "Cupertino": return { bg: "#007AFF", radius: 8, font: 17, weight: 600, minH: 36, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 2, Opacity: 0.2, Color: Colors.Black }) };
|
|
64
|
+
case "Material": return { bg: "#2196F3", radius: 4, font: 14, weight: 0, minH: 40, shadow: new SkiaShadow({ X: 0, Y: 2, Blur: 4, Opacity: 0.3, Color: Colors.Black }) };
|
|
65
|
+
case "Material3": return { bg: "#6750A4", radius: 20, font: 14, weight: 0, minH: 40 };
|
|
66
|
+
case "Windows": return { bg: "#0078D7", radius: 4, font: 15, weight: 500, minH: 32, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 1, Opacity: 0.2, Color: Colors.Black }) };
|
|
67
|
+
default: return { bg: "#DC143C", radius: 8, font: 15, weight: 0, minH: 41 };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
MeasureAbsolute(w, h, scale) {
|
|
71
|
+
const look = SkiaButton.Look(this.UsingControlStyle);
|
|
72
|
+
this.frame.BackgroundColor = this.BackgroundColor ?? look.bg;
|
|
73
|
+
this.frame.CornerRadius = this.CornerRadius === 8 ? look.radius : this.CornerRadius; // C#: 8 = "not customized"
|
|
74
|
+
this.frame.StrokeColor = this.StrokeColor;
|
|
75
|
+
this.frame.StrokeWidth = this.StrokeWidth;
|
|
76
|
+
if (this.frame.Shadows[0] !== look.shadow)
|
|
77
|
+
this.frame.Shadows = look.shadow ? [look.shadow] : [];
|
|
78
|
+
if (this.MinimumHeightRequest < 0 && this.UsingControlStyle !== "Unset")
|
|
79
|
+
this.MinimumHeightRequest = look.minH;
|
|
80
|
+
this.label.Text = this.Text;
|
|
81
|
+
this.label.TextColor = this.TextColor;
|
|
82
|
+
this.label.FontSize = this.FontSize === 15 ? look.font : this.FontSize;
|
|
83
|
+
this.label.FontWeight = look.weight;
|
|
84
|
+
this.label.FontFamily = this.FontFamily;
|
|
85
|
+
this.label.FontFamilyFallback = this.FontFamilyFallback;
|
|
86
|
+
// Size comes from the label + Padding; the Fill frame follows whatever the button is arranged to.
|
|
87
|
+
const px = this.Padding.HorizontalThickness * scale, py = this.Padding.VerticalThickness * scale;
|
|
88
|
+
const l = this.label.Measure(isFinite(w) ? w - px : w, isFinite(h) ? h - py : h, scale);
|
|
89
|
+
return ScaledSize.FromPixels(l.Pixels.Width + px, l.Pixels.Height + py, scale);
|
|
90
|
+
}
|
|
91
|
+
/** The frame sits under the padding box: arrange children in the full rect, the label centered inside padding. */
|
|
92
|
+
OnLayoutChanged() {
|
|
93
|
+
const scale = this.RenderingScale;
|
|
94
|
+
const r = this.DrawingRect;
|
|
95
|
+
this.frame.Arrange(r, -1, -1, scale);
|
|
96
|
+
const p = this.Padding;
|
|
97
|
+
this.label.Arrange(new SKRect(r.Left + p.Left * scale, r.Top + p.Top * scale, r.Right - p.Right * scale, r.Bottom - p.Bottom * scale), -1, -1, scale);
|
|
98
|
+
}
|
|
99
|
+
/** Ripple clipped to the rounded frame. */
|
|
100
|
+
CreateClip() { return this.frame.CreateClip(); }
|
|
101
|
+
/** DrawnUi SkiaButton.OnDown: press feedback; true = consume Down. */
|
|
102
|
+
OnDown(_args, apply) {
|
|
103
|
+
if (this.ApplyEffect === "Ripple") {
|
|
104
|
+
const pts = this.GetOffsetInsideControlInPoints(apply.MappedLocation, apply.ChildOffset);
|
|
105
|
+
this.PlayRippleAnimation(this.TouchEffectColor, pts.X, pts.Y);
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
SetUp(args) {
|
|
110
|
+
this.IsPressed = false;
|
|
111
|
+
this.hadDown = false;
|
|
112
|
+
this.Up?.(this, args);
|
|
113
|
+
this.Repaint();
|
|
114
|
+
}
|
|
115
|
+
ProcessGestures(args, apply) {
|
|
116
|
+
if (this.IsDisabled)
|
|
117
|
+
return null;
|
|
118
|
+
const point = args.Event.Location;
|
|
119
|
+
if (args.Type === "Down") {
|
|
120
|
+
this.IsPressed = true;
|
|
121
|
+
this.lastDownPts = point;
|
|
122
|
+
this.hadDown = true;
|
|
123
|
+
this.TotalDown++;
|
|
124
|
+
this.Down?.(this, args);
|
|
125
|
+
this.Repaint();
|
|
126
|
+
return this.OnDown(args, apply) ? this : null;
|
|
127
|
+
}
|
|
128
|
+
if (args.Type === "Panning") {
|
|
129
|
+
if (this.LockPanning)
|
|
130
|
+
return this;
|
|
131
|
+
const t = SkiaButton.PanThreshold * this.RenderingScale;
|
|
132
|
+
if (Math.abs(point.X - this.lastDownPts.X) > t || Math.abs(point.Y - this.lastDownPts.Y) > t) {
|
|
133
|
+
if (this.hadDown)
|
|
134
|
+
this.SetUp(args);
|
|
135
|
+
this.hadDown = false;
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (args.Type === "Up") {
|
|
140
|
+
this.SetUp(args);
|
|
141
|
+
}
|
|
142
|
+
else if (args.Type === "Tapped") {
|
|
143
|
+
this.TotalTapped++;
|
|
144
|
+
return this.SendTapped(args, apply) ? this : null;
|
|
145
|
+
}
|
|
146
|
+
return this.hadDown ? this : null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { DrawingContext, SkiaControl } from "../core/SkiaControl";
|
|
2
|
+
import { type GestureEventProcessingInfo, SKPoint, type SkiaGesturesParameters } from "../core/Gestures";
|
|
3
|
+
import { Spring } from "../core/ScrollAnimators";
|
|
4
|
+
import { ScaledSize } from "../core/Types";
|
|
5
|
+
import { SnappingLayout } from "./SnappingLayout";
|
|
6
|
+
export interface SnapPoint {
|
|
7
|
+
Id: number;
|
|
8
|
+
Location: SKPoint;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Mirrors DrawnUi SkiaCarousel: every child (static `Children` or a recycled `ItemsSource` + `ItemTemplate` cell)
|
|
12
|
+
* is a full-size slide laid along the axis, `SidesOffset` peeks the neighbours, `Spacing` separates slides; a
|
|
13
|
+
* swipe / release snaps to the nearest slide by velocity, `SelectedIndex` drives and reports the position,
|
|
14
|
+
* `IsLooped` wraps around through virtual anchors, `PreloadNeighboors` draws the hidden neighbours,
|
|
15
|
+
* `DynamicSize` sizes an auto-sized carousel from the selected slide, `SwipeSpeed` / `LinearSpeedMs` tune the snap.
|
|
16
|
+
*/
|
|
17
|
+
export declare class SkiaCarousel extends SnappingLayout {
|
|
18
|
+
IsVertical: boolean;
|
|
19
|
+
/** Side padding in points so the previous/next slides peek in. */
|
|
20
|
+
SidesOffset: number;
|
|
21
|
+
/** Accepted for parity; C# marks it TODO. */
|
|
22
|
+
IsRightToLeft: boolean;
|
|
23
|
+
/** Wrap from the last slide to the first and back. */
|
|
24
|
+
IsLooped: boolean;
|
|
25
|
+
/** Draw the hidden neighbour slides too (preloads images etc.), C# default true. */
|
|
26
|
+
PreloadNeighboors: boolean;
|
|
27
|
+
/** An auto-sized carousel takes the size of the selected slide instead of the largest one. */
|
|
28
|
+
DynamicSize: boolean;
|
|
29
|
+
/** Multiplies the snap velocity / spring stiffness (C# SwipeSpeed). */
|
|
30
|
+
SwipeSpeed: number;
|
|
31
|
+
/** Milliseconds a whole slide takes when snapping without Bounces; 0 = derived from the velocity (C# LinearSpeedMs). */
|
|
32
|
+
LinearSpeedMs: number;
|
|
33
|
+
SelectedIndexChanged?: (sender: SkiaCarousel, index: number) => void;
|
|
34
|
+
ItemAppearing?: (sender: SkiaCarousel, index: number) => void;
|
|
35
|
+
ItemDisappearing?: (sender: SkiaCarousel, index: number) => void;
|
|
36
|
+
private selectedIndex;
|
|
37
|
+
private lastIndex;
|
|
38
|
+
private childrenInitialized;
|
|
39
|
+
private layoutKey;
|
|
40
|
+
private lastLooped;
|
|
41
|
+
private cellSize;
|
|
42
|
+
/** Slide size in points (C# CellSize). */
|
|
43
|
+
protected get CellSize(): {
|
|
44
|
+
W: number;
|
|
45
|
+
H: number;
|
|
46
|
+
};
|
|
47
|
+
private panningOffset;
|
|
48
|
+
private panningStart;
|
|
49
|
+
private wrongDirection;
|
|
50
|
+
private snapIfNoPanOnUp;
|
|
51
|
+
private hadDown;
|
|
52
|
+
private itemsVisibility;
|
|
53
|
+
private snapPointsVirtual?;
|
|
54
|
+
private scrollAmount;
|
|
55
|
+
/** Slides drawn on screen by the last frame: the only gesture targets (C# rendering tree). */
|
|
56
|
+
private visibleViews;
|
|
57
|
+
constructor();
|
|
58
|
+
get SelectedIndex(): number;
|
|
59
|
+
set SelectedIndex(v: number);
|
|
60
|
+
/** Previously selected index (C# LastIndex). */
|
|
61
|
+
get LastIndex(): number;
|
|
62
|
+
get MaxIndex(): number;
|
|
63
|
+
get ChildrenTotal(): number;
|
|
64
|
+
get ChildrenCount(): number;
|
|
65
|
+
get IsAtStart(): boolean;
|
|
66
|
+
get IsAtEnd(): boolean;
|
|
67
|
+
/** 0..1 progress of the content along all slides (C# ScrollProgress). */
|
|
68
|
+
get ScrollProgress(): number;
|
|
69
|
+
/** Scroll amount of the selected slide, 0..1 of the track (C# ScrollAmount). */
|
|
70
|
+
get ScrollAmount(): number;
|
|
71
|
+
/** Fraction of the current transition between two slides (C# TransitionProgress). */
|
|
72
|
+
get TransitionProgress(): number;
|
|
73
|
+
private OnSelectedIndexChanged;
|
|
74
|
+
/** Index reached by scrolling: state only, no snap (C# SelectedIndex set from UpdateReportedPosition). */
|
|
75
|
+
private ReportIndex;
|
|
76
|
+
GoNext(): void;
|
|
77
|
+
GoPrev(): void;
|
|
78
|
+
/** C# ScrollTo(index, animate). */
|
|
79
|
+
ScrollTo(index: number, animate?: boolean): void;
|
|
80
|
+
private get ApplyLoopedLogic();
|
|
81
|
+
/** Step between slides in points: slide size + Spacing - 2 * SidesOffset (C# InitializeChildren). */
|
|
82
|
+
private Step;
|
|
83
|
+
/**
|
|
84
|
+
* C# InterruptSnapping: stops an in-flight snap before a programmatic index change and continues from the visual
|
|
85
|
+
* position; for IsLooped the position is shifted by whole strips into the neighbourhood of the current index.
|
|
86
|
+
*/
|
|
87
|
+
private InterruptSnapping;
|
|
88
|
+
private ApplyIndex;
|
|
89
|
+
UpdateReportedPosition(): void;
|
|
90
|
+
ApplyPosition(position: SKPoint): void;
|
|
91
|
+
/** The position changed (C# OnScrollProgressChanged); SkiaShaderCarousel drives its transition here. */
|
|
92
|
+
protected OnScrollProgressChanged(): void;
|
|
93
|
+
/** Snap points were (re)built (C# OnChildrenInitialized). */
|
|
94
|
+
protected OnChildrenInitialized(): void;
|
|
95
|
+
/** Where a drawn slide goes for its scroll offset; SkiaShaderCarousel keeps every slide in place (C# AnimateVisibleChild). */
|
|
96
|
+
protected SlideOffset(offset: SKPoint): SKPoint;
|
|
97
|
+
/** An already realized slide (C# ChildrenFactory.GetViewForIndex): no cell is created. */
|
|
98
|
+
protected GetExistingView(index: number): SkiaControl | undefined;
|
|
99
|
+
private GetVirtualSnapPoints;
|
|
100
|
+
protected GetVirtualAnchor(current: SKPoint): SnapPoint;
|
|
101
|
+
protected FindNearestAnchorInternal(current: SKPoint, velocity: SKPoint): SKPoint;
|
|
102
|
+
/** C# SkiaCarousel.ScrollToNearestAnchor: velocity below 100 counts as none; looped snaps around virtual anchors. */
|
|
103
|
+
ScrollToNearestAnchor(location: SKPoint, velocity: SKPoint): void;
|
|
104
|
+
/** C# SkiaCarousel.SelectNextAnchor: base choice, plus a wrap to the virtual anchor at the strip borders when looped. */
|
|
105
|
+
SelectNextAnchor(origin: SKPoint, velocity: SKPoint): SKPoint;
|
|
106
|
+
/** C# FixIndex: a virtual anchor becomes the real one, the position keeps its offset (visually identical, looped drawing wraps). */
|
|
107
|
+
private FixIndex;
|
|
108
|
+
private FixPosition;
|
|
109
|
+
protected OnTransitionChanged(): void;
|
|
110
|
+
protected ScaleSnapVelocity(velocity: SKPoint): SKPoint;
|
|
111
|
+
protected CreateSnapSpring(): Spring;
|
|
112
|
+
protected SpringVelocity(velocity: SKPoint): SKPoint;
|
|
113
|
+
protected GetSnapDurationSeconds(start: SKPoint, end: SKPoint, velocity: SKPoint, displacement: SKPoint): number;
|
|
114
|
+
private InnerRect;
|
|
115
|
+
/** C# AdaptTemplate: slides fill the carousel (Start only when the carousel auto-sizes on that axis), SidesOffset as margin. */
|
|
116
|
+
private AdaptTemplate;
|
|
117
|
+
private GetChild;
|
|
118
|
+
protected MeasureAbsolute(w: number, h: number, scale: number): ScaledSize;
|
|
119
|
+
protected OnLayoutChanged(): void;
|
|
120
|
+
/** C# InitializeChildren: snap points from the slide size, bounds, and the current index applied instantly. */
|
|
121
|
+
private InitializeChildren;
|
|
122
|
+
/** C# GetContentOffsetBounds: the snap extent, or unbounded along the axis when looped. */
|
|
123
|
+
private GetContentOffsetBounds;
|
|
124
|
+
/** C# CalculateChildPosition: slide offset in points, visibility, and "next to screen"; looped draws the edge slides on the other side. */
|
|
125
|
+
private CalculateChildPosition;
|
|
126
|
+
private SendVisibility;
|
|
127
|
+
/** C# RenderViewsList: position every slide, draw the visible ones (+ neighbours when PreloadNeighboors), release the rest. */
|
|
128
|
+
protected Paint(ctx: DrawingContext): void;
|
|
129
|
+
private SlideRect;
|
|
130
|
+
protected GetGestureListeners(): readonly SkiaControl[];
|
|
131
|
+
ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
|
|
132
|
+
protected DefaultAccessibilityLabel(): string | undefined;
|
|
133
|
+
}
|