drawnui-react 0.1.0-preview.1 → 0.1.0-preview.10

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 (45) hide show
  1. package/README.md +87 -29
  2. package/SKIPPED.md +20 -15
  3. package/dist/controls/SkiaButton.d.ts +4 -1
  4. package/dist/controls/SkiaButton.js +20 -7
  5. package/dist/controls/SkiaCarousel.d.ts +6 -0
  6. package/dist/controls/SkiaCarousel.js +7 -1
  7. package/dist/controls/SkiaCheckbox.d.ts +0 -1
  8. package/dist/controls/SkiaCheckbox.js +0 -6
  9. package/dist/controls/SkiaEditor.js +8 -0
  10. package/dist/controls/SkiaLabel.d.ts +2 -0
  11. package/dist/controls/SkiaLabel.js +9 -0
  12. package/dist/controls/SkiaLayout.d.ts +10 -0
  13. package/dist/controls/SkiaLayout.js +64 -1
  14. package/dist/controls/SkiaRadioButton.js +1 -2
  15. package/dist/controls/SkiaScroll.d.ts +9 -0
  16. package/dist/controls/SkiaScroll.js +120 -13
  17. package/dist/controls/SkiaScrollBar.d.ts +27 -0
  18. package/dist/controls/SkiaScrollBar.js +51 -2
  19. package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
  20. package/dist/controls/SkiaShaderCarousel.js +5 -0
  21. package/dist/controls/SkiaSwitch.d.ts +0 -1
  22. package/dist/controls/SkiaSwitch.js +0 -6
  23. package/dist/controls/SkiaToggle.d.ts +18 -4
  24. package/dist/controls/SkiaToggle.js +45 -3
  25. package/dist/core/Canvas.d.ts +82 -4
  26. package/dist/core/Canvas.js +255 -9
  27. package/dist/core/Gestures.d.ts +47 -1
  28. package/dist/core/Gestures.js +43 -1
  29. package/dist/core/SkiaControl.d.ts +30 -1
  30. package/dist/core/SkiaControl.js +59 -3
  31. package/dist/core/Styles.d.ts +51 -0
  32. package/dist/core/Styles.js +92 -0
  33. package/dist/core/Super.d.ts +10 -1
  34. package/dist/core/Super.js +20 -3
  35. package/dist/core/Types.d.ts +7 -2
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/react/SkiaShell.js +5 -2
  39. package/dist/react/index.d.ts +22 -4
  40. package/dist/react/index.js +25 -4
  41. package/dist/react/reconciler.js +2 -0
  42. package/dist/vite/index.d.ts +74 -0
  43. package/dist/vite/index.js +204 -0
  44. package/package.json +35 -4
  45. package/PARITY.md +0 -287
@@ -3,7 +3,7 @@ import { type Color, type LayoutOptions, SKRect, ScaledSize, type SkiaCacheType,
3
3
  import { type IOverlayEffect } from "./Animators";
4
4
  import { Easing } from "./Easing";
5
5
  import type { Canvas } from "./Canvas";
6
- import { ControlTappedEventArgs, GestureEventProcessingInfo, type LockTouch, SKPoint, SkiaGesturesInfo, SkiaGesturesParameters } from "./Gestures";
6
+ import { ContextMenuEventArgs, ControlTappedEventArgs, GestureEventProcessingInfo, type LockTouch, SKPoint, SkiaGesturesInfo, SkiaGesturesParameters } from "./Gestures";
7
7
  import { type CachedTexture, type IPostRendererEffect, type SkiaEffect } from "./SkiaEffect";
8
8
  /** Mirrors DrawnUi DrawingContext: ctx.Context.Canvas / Surface, ctx.Destination (pixels), ctx.Scale. */
9
9
  export interface DrawingContext {
@@ -180,11 +180,24 @@ export declare class SkiaControl {
180
180
  /** Clip overlay effects to the control's shape (CreateClip). */
181
181
  ClipEffects: boolean;
182
182
  Tapped?: (sender: SkiaControl, e: ControlTappedEventArgs) => void;
183
+ /**
184
+ * Context-menu request over this control (right click, long press on touch, Menu key). Return true to handle it:
185
+ * the browser's own canvas menu is suppressed. Routed deepest child first, then parents, then Canvas.ContextMenu.
186
+ * Not in DrawnUi.Net (a web concept).
187
+ */
188
+ ContextMenu?: (sender: SkiaControl, e: ContextMenuEventArgs) => boolean | void;
183
189
  ChildTapped?: (sender: SkiaControl, e: ControlTappedEventArgs) => void;
184
190
  /** Raw gesture hook: set e.Consumed = true to stop propagation (not for Up). */
185
191
  ConsumeGestures?: (sender: SkiaControl, e: SkiaGesturesInfo) => void;
186
192
  Parent?: SkiaControl;
187
193
  /** Containers override; a leaf control cannot host children. */
194
+ private styledInitially;
195
+ /**
196
+ * C# ApplyInitialStyles: takes the property defaults registered with `ConfigureStyles` for this control's class.
197
+ * The reconciler calls it with `atConstruction` right after `new`, before the JSX props, so props win; a control
198
+ * built in code-behind is styled on its first measure and keeps whatever the code already set.
199
+ */
200
+ ApplyInitialStyles(atConstruction?: boolean): void;
188
201
  AddSubView(_control: SkiaControl): void;
189
202
  InsertSubView(_index: number, control: SkiaControl): void;
190
203
  RemoveSubView(_control: SkiaControl): void;
@@ -235,6 +248,11 @@ export declare class SkiaControl {
235
248
  get AccessibilityCanInteract(): boolean;
236
249
  set AccessibilityCanInteract(v: boolean);
237
250
  protected DefaultAccessibilityCanInteract(): boolean;
251
+ /**
252
+ * Whether the mouse at this point (pixels, relative to DrawingRect's origin) is over something tappable, for the
253
+ * host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
254
+ */
255
+ WantsPointerCursor(_x: number, _y: number): boolean;
238
256
  /** aria-pressed for toggles; undefined = not a toggle. */
239
257
  get AccessibilityIsPressed(): boolean | undefined;
240
258
  set AccessibilityIsPressed(v: boolean | undefined);
@@ -364,12 +382,23 @@ export declare class SkiaControl {
364
382
  * cache holds its composited output, so those are staled before redrawing (DrawnUi RedrawCanvas + parent invalidation).
365
383
  */
366
384
  RepaintComposition(): void;
385
+ /**
386
+ * DrawnUi IsParentIndependent: this control's size never changes the layout around it, so its own invalidation
387
+ * stops here (ancestors composite again, nothing above re-measures). The parent re-measures it on draw when
388
+ * NeedMeasure is set. Used by SkiaScroll for its scroll bars.
389
+ */
390
+ IsParentIndependent: boolean;
367
391
  /** Size or content may have changed: this control and all ancestors remeasure and re-record. */
368
392
  InvalidateMeasure(): void;
369
393
  /** Hit rect in pixels (no transforms in this port, so it is DrawingRect). */
370
394
  get HitBoxAuto(): SKRect;
371
395
  HitIsInside(x: number, y: number): boolean;
372
396
  IsGestureForChild(child: SkiaControl, point: SKPoint): boolean;
397
+ /**
398
+ * Routes a context-menu request like a tap: visible, non-transparent children under the point first (top-most
399
+ * first, each in its own transformed space), then this control's ContextMenu handler. True = handled.
400
+ */
401
+ ProcessContextMenu(point: SKPoint, e: ContextMenuEventArgs): boolean;
373
402
  /** Children that may receive gestures, top-most LAST (layouts return their Views). */
374
403
  protected GetGestureListeners(): readonly SkiaControl[];
375
404
  private static readonly NoListeners;
@@ -1,4 +1,5 @@
1
1
  import { Super } from "./Super";
2
+ import { SkiaStyles } from "./Styles";
2
3
  import { Colors, SKRect, ScaledSize, Thickness } from "./Types";
3
4
  import { RippleAnimator, SkiaValueAnimator } from "./Animators";
4
5
  import { Easing } from "./Easing";
@@ -238,12 +239,29 @@ export class SkiaControl {
238
239
  ClipEffects = true;
239
240
  // ---- gesture events (single handler each; C# events map to one callback prop) ----
240
241
  Tapped;
242
+ /**
243
+ * Context-menu request over this control (right click, long press on touch, Menu key). Return true to handle it:
244
+ * the browser's own canvas menu is suppressed. Routed deepest child first, then parents, then Canvas.ContextMenu.
245
+ * Not in DrawnUi.Net (a web concept).
246
+ */
247
+ ContextMenu;
241
248
  ChildTapped;
242
249
  /** Raw gesture hook: set e.Consumed = true to stop propagation (not for Up). */
243
250
  ConsumeGestures;
244
251
  // ---- tree ----
245
252
  Parent;
246
253
  /** Containers override; a leaf control cannot host children. */
254
+ styledInitially = false;
255
+ /**
256
+ * C# ApplyInitialStyles: takes the property defaults registered with `ConfigureStyles` for this control's class.
257
+ * The reconciler calls it with `atConstruction` right after `new`, before the JSX props, so props win; a control
258
+ * built in code-behind is styled on its first measure and keeps whatever the code already set.
259
+ */
260
+ ApplyInitialStyles(atConstruction = false) {
261
+ this.styledInitially = true;
262
+ if (!SkiaStyles.IsEmpty)
263
+ SkiaStyles.Apply(this, atConstruction);
264
+ }
247
265
  AddSubView(_control) { throw new Error(`DrawnUi: ${this.constructor.name} cannot host children`); }
248
266
  InsertSubView(_index, control) { this.AddSubView(control); }
249
267
  RemoveSubView(_control) { }
@@ -265,6 +283,8 @@ export class SkiaControl {
265
283
  * this is what keeps a cached tree from re-measuring text every frame.
266
284
  */
267
285
  Measure(widthConstraint, heightConstraint, scale) {
286
+ if (!this.styledInitially)
287
+ this.ApplyInitialStyles(false);
268
288
  if (!this.NeedMeasure && this.RenderingScale === scale
269
289
  && Object.is(this.lastWidthConstraint, widthConstraint) && Object.is(this.lastHeightConstraint, heightConstraint)) {
270
290
  return this.MeasuredSize;
@@ -388,6 +408,11 @@ export class SkiaControl {
388
408
  this.AccessibilityChanged();
389
409
  } }
390
410
  DefaultAccessibilityCanInteract() { return !!this.Tapped; }
411
+ /**
412
+ * Whether the mouse at this point (pixels, relative to DrawingRect's origin) is over something tappable, for the
413
+ * host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
414
+ */
415
+ WantsPointerCursor(_x, _y) { return this.AccessibilityCanInteract; }
391
416
  /** aria-pressed for toggles; undefined = not a toggle. */
392
417
  get AccessibilityIsPressed() { return this.accessibilityIsPressed; }
393
418
  set AccessibilityIsPressed(v) { if (this.accessibilityIsPressed !== v) {
@@ -1057,13 +1082,21 @@ export class SkiaControl {
1057
1082
  }
1058
1083
  this.Repaint();
1059
1084
  }
1085
+ /**
1086
+ * DrawnUi IsParentIndependent: this control's size never changes the layout around it, so its own invalidation
1087
+ * stops here (ancestors composite again, nothing above re-measures). The parent re-measures it on draw when
1088
+ * NeedMeasure is set. Used by SkiaScroll for its scroll bars.
1089
+ */
1090
+ IsParentIndependent = false;
1060
1091
  /** Size or content may have changed: this control and all ancestors remeasure and re-record. */
1061
1092
  InvalidateMeasure() {
1062
1093
  this.NeedMeasure = true;
1063
1094
  this.cacheDirty = true;
1064
1095
  this.compositeFull = true; // structure may change: a composite cannot patch it
1065
1096
  this.effectsMarginCache = undefined;
1066
- if (this.Parent)
1097
+ if (this.Parent && this.IsParentIndependent)
1098
+ this.RepaintComposition();
1099
+ else if (this.Parent)
1067
1100
  this.Parent.InvalidateMeasure();
1068
1101
  else
1069
1102
  this.Superview?.Update();
@@ -1079,6 +1112,25 @@ export class SkiaControl {
1079
1112
  const local = child.TransformPointToLocalSpace(point);
1080
1113
  return child.HitIsInside(local.X, local.Y);
1081
1114
  }
1115
+ /**
1116
+ * Routes a context-menu request like a tap: visible, non-transparent children under the point first (top-most
1117
+ * first, each in its own transformed space), then this control's ContextMenu handler. True = handled.
1118
+ */
1119
+ ProcessContextMenu(point, e) {
1120
+ const listeners = this.GetGestureListeners();
1121
+ for (let i = listeners.length - 1; i >= 0; i--) {
1122
+ const listener = listeners[i];
1123
+ if (!listener.IsVisible || listener.InputTransparent || !this.IsGestureForChild(listener, point))
1124
+ continue;
1125
+ if (listener.ProcessContextMenu(listener.TransformPointToLocalSpace(point), e))
1126
+ return true;
1127
+ }
1128
+ if (!this.ContextMenu)
1129
+ return false;
1130
+ e.Control = this;
1131
+ e.Local = new SKPoint(point.X - this.DrawingRect.Left, point.Y - this.DrawingRect.Top);
1132
+ return this.ContextMenu(this, e) === true;
1133
+ }
1082
1134
  /** Children that may receive gestures, top-most LAST (layouts return their Views). */
1083
1135
  GetGestureListeners() { return SkiaControl.NoListeners; }
1084
1136
  static NoListeners = [];
@@ -1106,15 +1158,19 @@ export class SkiaControl {
1106
1158
  if (this.ConsumeGestures) {
1107
1159
  const sent = new SkiaGesturesInfo(args, apply);
1108
1160
  this.ConsumeGestures(this, sent);
1109
- if (args.Type !== "Up" && sent.Consumed)
1161
+ if (args.Type !== "Up" && sent.Consumed) {
1162
+ args.Event.Handled = true;
1110
1163
  return this;
1164
+ }
1111
1165
  }
1112
1166
  // C# EffectsGestureProcessors: attached effects that process gestures (ISkiaGestureProcessor) see them first
1113
1167
  if (!this.DisableEffects)
1114
1168
  for (const e of this.visualEffects) {
1115
1169
  const p = e.ProcessGestures;
1116
- if (typeof p === "function" && p.call(e, args, apply) && args.Type !== "Up")
1170
+ if (typeof p === "function" && p.call(e, args, apply) && args.Type !== "Up") {
1171
+ args.Event.Handled = true;
1117
1172
  return this;
1173
+ }
1118
1174
  }
1119
1175
  if (this.CheckChildrenGesturesLocked(args.Type))
1120
1176
  return consumedDefault;
@@ -0,0 +1,51 @@
1
+ import type { SkiaControl } from "./SkiaControl";
2
+ /**
3
+ * Mirrors DrawnUi.Net `Style` (SharedNet/Internals/Core/Style.cs): a set of property defaults for one control type,
4
+ * registered once at startup through `Super.UseDrawnUi().ConfigureStyles(...)`.
5
+ *
6
+ * The C# `Setters` list of `Setter { Property, Value }` becomes a plain object here, because React controls are
7
+ * classes with plain properties instead of BindableProperty: `Setters: { FontFamily: "FontText", FontSize: 14 }`.
8
+ */
9
+ export interface Style {
10
+ /** The control class the style targets, e.g. `SkiaLabel`. */
11
+ TargetType: Function;
12
+ /** Also style classes deriving from TargetType (C# Style.ApplyToDerivedTypes). Default false. */
13
+ ApplyToDerivedTypes?: boolean;
14
+ /** Property defaults, by property name. Anything the app sets itself wins over these. */
15
+ Setters: Record<string, unknown>;
16
+ }
17
+ /** Mirrors DrawnUi.Net `IStylesCollection` / `StylesCollection`: the startup registry, static like the C# one. */
18
+ export declare class StylesCollection {
19
+ /** Every style registered through ConfigureStyles, in registration order (C# StylesCollection.Styles). */
20
+ static readonly Styles: Style[];
21
+ AddStyle(style: Style): StylesCollection;
22
+ }
23
+ /**
24
+ * Applies registered styles to controls. Port of the C# `SkiaControl` styles partial
25
+ * (SharedNet/Draw/SkiaControl.NetShared.cs) with one difference forced by the language: C# knows which properties the
26
+ * app set explicitly (BindableProperty + ExplicitPropertiesSet) and skips those; here a property counts as "not set by
27
+ * the app" while it still equals the value the control's own constructor left, captured once per class.
28
+ */
29
+ export declare class SkiaStyles {
30
+ private static readonly resolved;
31
+ private static readonly defaults;
32
+ /** True while no style is registered: lets Measure skip the whole feature at zero cost. */
33
+ static get IsEmpty(): boolean;
34
+ /** C# InvalidateStylesCache: drop the per-type resolution after the registry changed. */
35
+ static InvalidateStylesCache(): void;
36
+ /**
37
+ * Setters that apply to a control class: the exact-type style plus every ApplyToDerivedTypes style of a base class,
38
+ * furthest base first so a nearer style overrides it, exact type last (C# sorts by inheritance distance).
39
+ */
40
+ private static For;
41
+ /** The values a freshly constructed control of this class carries, for the properties a style touches. */
42
+ private static SnapshotDefaults;
43
+ /**
44
+ * C# ApplyInitialStyles: applies the registered defaults to one control. `atConstruction` is true when nothing has
45
+ * touched the control yet (the reconciler calls it between `new` and the JSX props), so every setter applies and the
46
+ * pre-style values are kept as this class's defaults; a control styled later only gets the properties that still
47
+ * hold those defaults, which is how an explicit assignment wins — the C# ExplicitPropertiesSet rule without
48
+ * property accessors.
49
+ */
50
+ static Apply(control: SkiaControl, atConstruction: boolean): void;
51
+ }
@@ -0,0 +1,92 @@
1
+ /** Mirrors DrawnUi.Net `IStylesCollection` / `StylesCollection`: the startup registry, static like the C# one. */
2
+ export class StylesCollection {
3
+ /** Every style registered through ConfigureStyles, in registration order (C# StylesCollection.Styles). */
4
+ static Styles = [];
5
+ AddStyle(style) {
6
+ StylesCollection.Styles.push(style);
7
+ SkiaStyles.InvalidateStylesCache();
8
+ return this;
9
+ }
10
+ }
11
+ /**
12
+ * Applies registered styles to controls. Port of the C# `SkiaControl` styles partial
13
+ * (SharedNet/Draw/SkiaControl.NetShared.cs) with one difference forced by the language: C# knows which properties the
14
+ * app set explicitly (BindableProperty + ExplicitPropertiesSet) and skips those; here a property counts as "not set by
15
+ * the app" while it still equals the value the control's own constructor left, captured once per class.
16
+ */
17
+ export class SkiaStyles {
18
+ static resolved = new Map();
19
+ static defaults = new Map();
20
+ /** True while no style is registered: lets Measure skip the whole feature at zero cost. */
21
+ static get IsEmpty() { return StylesCollection.Styles.length === 0; }
22
+ /** C# InvalidateStylesCache: drop the per-type resolution after the registry changed. */
23
+ static InvalidateStylesCache() { SkiaStyles.resolved.clear(); SkiaStyles.defaults.clear(); }
24
+ /**
25
+ * Setters that apply to a control class: the exact-type style plus every ApplyToDerivedTypes style of a base class,
26
+ * furthest base first so a nearer style overrides it, exact type last (C# sorts by inheritance distance).
27
+ */
28
+ static For(type) {
29
+ let hit = SkiaStyles.resolved.get(type);
30
+ if (hit)
31
+ return hit;
32
+ const inherited = [];
33
+ let exact;
34
+ for (const style of StylesCollection.Styles) {
35
+ if (style.TargetType === type) {
36
+ exact = style;
37
+ continue;
38
+ }
39
+ if (!style.ApplyToDerivedTypes)
40
+ continue;
41
+ let distance = 0;
42
+ for (let base = Object.getPrototypeOf(type); base; base = Object.getPrototypeOf(base)) {
43
+ distance++;
44
+ if (base === style.TargetType) {
45
+ inherited.push({ Distance: distance, Style: style });
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ inherited.sort((a, b) => b.Distance - a.Distance);
51
+ const setters = {};
52
+ for (const i of inherited)
53
+ Object.assign(setters, i.Style.Setters);
54
+ if (exact)
55
+ Object.assign(setters, exact.Setters);
56
+ hit = { Setters: setters, Empty: Object.keys(setters).length === 0 };
57
+ SkiaStyles.resolved.set(type, hit);
58
+ return hit;
59
+ }
60
+ /** The values a freshly constructed control of this class carries, for the properties a style touches. */
61
+ static SnapshotDefaults(type, sample, keys) {
62
+ if (SkiaStyles.defaults.has(type))
63
+ return;
64
+ const known = {};
65
+ for (const key of keys)
66
+ known[key] = sample[key];
67
+ SkiaStyles.defaults.set(type, known);
68
+ }
69
+ /**
70
+ * C# ApplyInitialStyles: applies the registered defaults to one control. `atConstruction` is true when nothing has
71
+ * touched the control yet (the reconciler calls it between `new` and the JSX props), so every setter applies and the
72
+ * pre-style values are kept as this class's defaults; a control styled later only gets the properties that still
73
+ * hold those defaults, which is how an explicit assignment wins — the C# ExplicitPropertiesSet rule without
74
+ * property accessors.
75
+ */
76
+ static Apply(control, atConstruction) {
77
+ const type = control.constructor;
78
+ const resolved = SkiaStyles.For(type);
79
+ if (resolved.Empty)
80
+ return;
81
+ const keys = Object.keys(resolved.Setters);
82
+ const target = control;
83
+ if (atConstruction)
84
+ SkiaStyles.SnapshotDefaults(type, control, keys);
85
+ const defaults = atConstruction ? undefined : SkiaStyles.defaults.get(type);
86
+ for (const key of keys) {
87
+ // no snapshot for this class (nothing of it ever went through the reconciler): apply as a plain default
88
+ if (!defaults || Object.is(target[key], defaults[key]))
89
+ target[key] = resolved.Setters[key];
90
+ }
91
+ }
92
+ }
@@ -1,4 +1,5 @@
1
1
  import { Thickness } from "./Types";
2
+ import { StylesCollection } from "./Styles";
2
3
  import type { CanvasKit, Font, Typeface } from "canvaskit-wasm";
3
4
  /** Mirrors DrawnUi.Net IFontCollection: fonts.AddFont(source, alias[, weight]), plus the Blazor-head AddSymbols/AddEmojis. */
4
5
  export declare class FontCollection {
@@ -16,10 +17,16 @@ export declare class FontCollection {
16
17
  /** Registers a face under an alias; weight 100..900 lets FontWeight / FontAttributes=Bold pick the right file. */
17
18
  AddFont(source: string, alias: string, weight?: number): FontCollection;
18
19
  }
19
- /** Mirrors DrawnUi.Net DrawnUiBuilder: Super.UseDrawnUi().ConfigureFonts(...).BuildAsync(). */
20
+ /** Mirrors DrawnUi.Net DrawnUiBuilder: Super.UseDrawnUi().ConfigureFonts(...).ConfigureStyles(...).BuildAsync(). */
20
21
  export declare class DrawnUiBuilder {
21
22
  private readonly fonts;
22
23
  ConfigureFonts(configure: (fonts: FontCollection) => void): DrawnUiBuilder;
24
+ /**
25
+ * Mirrors DrawnUi.Net ConfigureStyles: property defaults per control type, applied to every control unless the app
26
+ * sets that property itself. The usual one, same as the .NET Blazor / Fiddle hosts:
27
+ * `styles.AddStyle({ TargetType: SkiaLabel, ApplyToDerivedTypes: true, Setters: { FontFamily: "FontText" } })`.
28
+ */
29
+ ConfigureStyles(configure: (styles: StylesCollection) => void): DrawnUiBuilder;
23
30
  /** Loads CanvasKit + registered fonts. Must complete before the first Canvas is created. */
24
31
  BuildAsync(): Promise<void>;
25
32
  }
@@ -39,6 +46,8 @@ export declare class Super {
39
46
  static readonly Fonts: Map<string, Map<number, Typeface>>;
40
47
  /** First registered font, or CanvasKit's built-in one. */
41
48
  static DefaultTypeface?: Typeface;
49
+ /** CanvasKit's built-in face: what an empty FontFamily resolves to, like C# SKTypeface.CreateDefault(). */
50
+ static SkiaDefaultTypeface?: Typeface;
42
51
  private static readonly fontCache;
43
52
  static UseDrawnUi(): DrawnUiBuilder;
44
53
  /** Master switch: false makes every control render uncached (DrawnUi Super.CacheEnabled). */
@@ -1,4 +1,5 @@
1
1
  import { Thickness } from "./Types";
2
+ import { StylesCollection } from "./Styles";
2
3
  // the "full" build: same API plus Skottie (SkiaLottie) and the paragraph module; +0.9 MB raw over the default build
3
4
  import CanvasKitInit from "canvaskit-wasm/bin/full/canvaskit.js";
4
5
  import wasmUrl from "canvaskit-wasm/bin/full/canvaskit.wasm?url";
@@ -22,13 +23,22 @@ export class FontCollection {
22
23
  return this;
23
24
  }
24
25
  }
25
- /** Mirrors DrawnUi.Net DrawnUiBuilder: Super.UseDrawnUi().ConfigureFonts(...).BuildAsync(). */
26
+ /** Mirrors DrawnUi.Net DrawnUiBuilder: Super.UseDrawnUi().ConfigureFonts(...).ConfigureStyles(...).BuildAsync(). */
26
27
  export class DrawnUiBuilder {
27
28
  fonts = new FontCollection();
28
29
  ConfigureFonts(configure) {
29
30
  configure(this.fonts);
30
31
  return this;
31
32
  }
33
+ /**
34
+ * Mirrors DrawnUi.Net ConfigureStyles: property defaults per control type, applied to every control unless the app
35
+ * sets that property itself. The usual one, same as the .NET Blazor / Fiddle hosts:
36
+ * `styles.AddStyle({ TargetType: SkiaLabel, ApplyToDerivedTypes: true, Setters: { FontFamily: "FontText" } })`.
37
+ */
38
+ ConfigureStyles(configure) {
39
+ configure(new StylesCollection());
40
+ return this;
41
+ }
32
42
  /** Loads CanvasKit + registered fonts. Must complete before the first Canvas is created. */
33
43
  async BuildAsync() {
34
44
  Super.CK = await CanvasKitInit({ locateFile: () => wasmUrl });
@@ -61,7 +71,8 @@ export class DrawnUiBuilder {
61
71
  if (!Super.DefaultFontAlias)
62
72
  Super.DefaultFontAlias = f.Alias;
63
73
  }
64
- Super.DefaultTypeface ??= Super.CK.Typeface.GetDefault() ?? undefined;
74
+ Super.SkiaDefaultTypeface ??= Super.CK.Typeface.GetDefault() ?? undefined;
75
+ Super.DefaultTypeface ??= Super.SkiaDefaultTypeface;
65
76
  }
66
77
  }
67
78
  /** Mirrors DrawnUi static Super: global engine state. */
@@ -105,6 +116,8 @@ export class Super {
105
116
  static Fonts = new Map();
106
117
  /** First registered font, or CanvasKit's built-in one. */
107
118
  static DefaultTypeface;
119
+ /** CanvasKit's built-in face: what an empty FontFamily resolves to, like C# SKTypeface.CreateDefault(). */
120
+ static SkiaDefaultTypeface;
108
121
  static fontCache = new Map();
109
122
  static UseDrawnUi() { return new DrawnUiBuilder(); }
110
123
  /** Master switch: false makes every control render uncached (DrawnUi Super.CacheEnabled). */
@@ -145,7 +158,11 @@ export class Super {
145
158
  }
146
159
  /** Nearest registered weight of the alias (empty alias = the first registered family); reports the weight actually used. */
147
160
  static ResolveTypeface(alias, weight) {
148
- const weights = Super.Fonts.get(alias || Super.DefaultFontAlias);
161
+ // No family: the Skia built-in face, like C# SkiaFontManager.GetFont("") -> SKTypeface.CreateDefault(). An app that
162
+ // wants its own font everywhere registers it as a style default (ConfigureStyles), exactly as the .NET hosts do.
163
+ if (!alias)
164
+ return { Typeface: Super.SkiaDefaultTypeface ?? Super.DefaultTypeface ?? null, Weight: 400 };
165
+ const weights = Super.Fonts.get(alias);
149
166
  const target = weight > 0 ? weight : 400;
150
167
  if (weights && weights.size > 0) {
151
168
  if (weights.has(target))
@@ -134,9 +134,14 @@ export type DrawImageAlignment = "Start" | "Center" | "End";
134
134
  /** MAUI/DrawnUi ScrollOrientation. */
135
135
  export type ScrollOrientation = "Vertical" | "Horizontal" | "Both" | "Neither";
136
136
  /**
137
- * DrawnUi SkiaCacheType. Operations = recorded draw commands (SkPicture) replayed each frame;
137
+ * DrawnUi SkiaCacheType. Operations / OperationsFull = recorded draw commands (SkPicture) replayed each frame.
138
138
  * Image = offscreen surface snapshot blitted each frame (GPU-backed when the canvas is WebGL).
139
- * GPU / ImageDoubleBuffered / ImageComposite / ImageCompositeGPU are accepted and currently resolve to Image.
139
+ * ImageDoubleBuffered = an Image cache that keeps the previous one and draws it — or DrawPlaceholder when there is
140
+ * none — while a new one cannot be produced yet.
141
+ * ImageComposite / ImageCompositeGPU = an Image cache whose offscreen surface survives between records: when only
142
+ * children changed, their old and new bounds plus the siblings those overlap are erased and only those children are
143
+ * painted again.
144
+ * GPU resolves to Image and ImageCompositeGPU to ImageComposite; the browser has no separate GPU-only path.
140
145
  */
141
146
  export type SkiaCacheType = "None" | "Operations" | "OperationsFull" | "Image" | "ImageDoubleBuffered" | "ImageComposite" | "ImageCompositeGPU" | "GPU";
142
147
  /** DrawnUi GradientType (Conical accepted, drawn as Circular). */
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./core/Types";
2
2
  export * from "./core/Gestures";
3
+ export * from "./core/Styles";
3
4
  export * from "./core/Easing";
4
5
  export * from "./core/Animators";
5
6
  export * from "./core/ScrollAnimators";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./core/Types";
2
2
  export * from "./core/Gestures";
3
+ export * from "./core/Styles";
3
4
  export * from "./core/Easing";
4
5
  export * from "./core/Animators";
5
6
  export * from "./core/ScrollAnimators";
@@ -440,9 +440,12 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
440
440
  }
441
441
  })();
442
442
  }
443
- else if (depth > history.current.length) {
444
- // forward: the hash names the pages, rebuild the stack without animation (overlays cannot be restored)
443
+ else {
444
+ // forward (depth grew), or the same depth with another hash = a plain <a href="#/route"> link or a typed hash:
445
+ // the hash names the pages, rebuild the stack without animation (overlays cannot be restored)
445
446
  const target = parseHash();
447
+ if (depth === history.current.length && target.join("/") === live.current.stack.join("/"))
448
+ return;
446
449
  history.current = target.map(() => "page");
447
450
  window.history.replaceState({ [HISTORY_KEY]: history.current.length }, "", hashFor(target));
448
451
  setStacks((all) => { const next = all.slice(); next[live.current.selectedTab] = target; return next; });
@@ -31,9 +31,17 @@ import { SkiaScroll as SkiaScrollCtrl } from "../controls/SkiaScroll";
31
31
  import type { SkiaShape as SkiaShapeCtrl } from "../controls/SkiaShape";
32
32
  import type { Color, RenderingModeType } from "../core/Types";
33
33
  import type { GesturesMode } from "../core/Gestures";
34
- /** Public settable properties of a control become its JSX props, same PascalCase names as C#. */
34
+ /**
35
+ * Public settable properties of a control become its JSX props, same PascalCase names as C#.
36
+ *
37
+ * Function-typed members split in two: a METHOD (`Update(): void`) is not a prop and is dropped,
38
+ * while an EVENT is declared optional (`Tapped?: (sender, e) => void`) and is exactly the thing a
39
+ * caller passes in JSX. Optionality is the discriminator; without it every handler was missing
40
+ * from the props type, so `<SkiaButton Tapped={…} />` worked at runtime but failed to typecheck
41
+ * and never appeared in completion.
42
+ */
35
43
  type PropsOf<T> = Partial<{
36
- [K in keyof T as T[K] extends Function ? never : K extends "Children" | "Views" | "Parent" | "Spans" | "GridStructure" | "AccessibilityId" | "IsAccessibilityElement" | "HasTransform" | "RenderTransformMatrix" | "RenderObjectPrevious" | "LastMeasuredIndex" | "ItemsInsertedAtStart" | "UsingControlStyle" | "Track" | "Thumb" | "FrameOn" | "FrameOff" | "ViewCheckOn" | "ViewOn" | "ViewText" | "Ratio" | "StartThumbX" | "EndThumbX" | "SnapPoints" | "CurrentPosition" | "CurrentSnap" | "ContentOffsetBounds" | "InTransition" | "CanAnimate" | "MaxIndex" | "ChildrenTotal" | "IsAtStart" | "IsAtEnd" | "ScrollProgress" | "ScrollAmount" | "TransitionProgress" | "LastIndex" | "ChildrenCount" | "Horizontal" | "Animator" | "Animation" | "IsPlaying" | "PlayWhenAvailable" | "TotalFrames" | "HasEffects" | "IsDisposed" | "Label" | "IsMultiline" | "HasSelection" | "LayoutVersion" | "LinesCount" | "MeasuredLineHeight" | "Focused" | "SpriteSheet" | "FrameWidth" | "FrameHeight" | "DurationMs" | "FrameDurationMs" | "CurrentSprite" | "Rects" | "HasTapHandler" | "HasDecorations" | "LinesCount" | "Superview" | "DrawingRect" | "MeasuredSize" | "RenderingScale" | "NeedMeasure" | "_superview" | "HitBoxAuto" | "TotalDown" | "TotalTapped" | "TouchDown" | "PostAnimators" | "LoadedSource" | "IsLoading" | "DisplayRect" | "AspectScale" | "Content" | "ContentSize" | "ContentOffsetBounds" | "OverscrollDistance" | "OverScrolled" | "IsUserPanning" | "IsUserFocused" | "IsScrolling" | "IsTemplated" | "FirstVisibleIndex" | "LastVisibleIndex" | "DebugString" | "ChildrenFactory" | "ContextIndex" | "RenderObject" | "UsingCacheType" | "TransitionEffect" | "TransitionFromIndex" | "TransitionToIndex" | "EffectPostRenderers" | "CachedImage" | "ImageBitmap" | "ShouldSubmitOnEnter" | "Header" | "Footer" | "RefreshIndicator" | "ScrollBar" | "ScrollBarHorizontal" | "IsRunning" | "VisibleRatio" | "CurrentIndex" | "LastCompositeRecord" | "DirtyChildrenInternal" | "IsRenderingWithComposition" ? never : K]: T[K];
44
+ [K in keyof T as T[K] extends Function ? (undefined extends T[K] ? K : never) : K extends "Children" | "Views" | "Parent" | "Spans" | "GridStructure" | "AccessibilityId" | "IsAccessibilityElement" | "HasTransform" | "RenderTransformMatrix" | "RenderObjectPrevious" | "LastMeasuredIndex" | "ItemsInsertedAtStart" | "UsingControlStyle" | "Track" | "Thumb" | "FrameOn" | "FrameOff" | "ViewCheckOn" | "ViewOn" | "ViewText" | "Ratio" | "StartThumbX" | "EndThumbX" | "SnapPoints" | "CurrentPosition" | "CurrentSnap" | "ContentOffsetBounds" | "InTransition" | "CanAnimate" | "MaxIndex" | "ChildrenTotal" | "IsAtStart" | "IsAtEnd" | "ScrollProgress" | "ScrollAmount" | "TransitionProgress" | "LastIndex" | "ChildrenCount" | "Horizontal" | "Animator" | "Animation" | "IsPlaying" | "PlayWhenAvailable" | "TotalFrames" | "HasEffects" | "IsDisposed" | "Label" | "IsMultiline" | "HasSelection" | "LayoutVersion" | "LinesCount" | "MeasuredLineHeight" | "Focused" | "SpriteSheet" | "FrameWidth" | "FrameHeight" | "DurationMs" | "FrameDurationMs" | "CurrentSprite" | "Rects" | "HasTapHandler" | "HasDecorations" | "LinesCount" | "Superview" | "DrawingRect" | "MeasuredSize" | "RenderingScale" | "NeedMeasure" | "_superview" | "HitBoxAuto" | "TotalDown" | "TotalTapped" | "TouchDown" | "PostAnimators" | "LoadedSource" | "IsLoading" | "DisplayRect" | "AspectScale" | "Content" | "ContentSize" | "ContentOffsetBounds" | "OverscrollDistance" | "OverScrolled" | "IsUserPanning" | "IsUserFocused" | "IsScrolling" | "IsTemplated" | "FirstVisibleIndex" | "LastVisibleIndex" | "DebugString" | "ChildrenFactory" | "ContextIndex" | "RenderObject" | "UsingCacheType" | "TransitionEffect" | "TransitionFromIndex" | "TransitionToIndex" | "EffectPostRenderers" | "CachedImage" | "ImageBitmap" | "ShouldSubmitOnEnter" | "Header" | "Footer" | "RefreshIndicator" | "ScrollBar" | "ScrollBarHorizontal" | "IsRunning" | "VisibleRatio" | "CurrentIndex" | "LastCompositeRecord" | "DirtyChildrenInternal" | "IsRenderingWithComposition" ? never : K]: T[K];
37
45
  }>;
38
46
  /** `ref` receives the engine control instance (react-reconciler getPublicInstance). */
39
47
  type LeafProps<T> = PropsOf<T> & {
@@ -92,19 +100,29 @@ export declare const SkiaDrawer: FC<LayoutProps<SkiaDrawerCtrl>>;
92
100
  export interface CanvasProps {
93
101
  BackgroundColor?: Color;
94
102
  RenderingMode?: RenderingModeType;
95
- /** Disabled (default) / Enabled / Lock, like DrawnUi Canvas.Gestures. */
103
+ /**
104
+ * Disabled (default) / Enabled / Lock, like DrawnUi Canvas.Gestures. Enabled shares input with the page like MAUI's
105
+ * Enabled inside a native scroll view: a touch pan along an axis the page can scroll scrolls the page, and the wheel
106
+ * scrolls it unless a control used the wheel. A page that cannot scroll keeps every touch. Lock keeps all input.
107
+ */
96
108
  Gestures?: GesturesMode;
97
109
  children?: ReactNode;
98
110
  style?: CSSProperties;
99
111
  className?: string;
100
112
  /** Receives the engine Canvas (FPS, FrameTime, RenderingScale...). */
101
113
  ref?: Ref<CanvasView>;
114
+ /** Context-menu request (right click / long press / Menu key) no control handled; return true to suppress the browser menu. */
115
+ ContextMenu?: CanvasView["ContextMenu"];
116
+ /** DrawnView.WillFirstTimeDraw: once, right before the first frame is drawn, with its drawing context. */
117
+ WillFirstTimeDraw?: CanvasView["WillFirstTimeDraw"];
118
+ /** DrawnView.WasDrawn: after every drawn frame. */
119
+ WasDrawn?: CanvasView["WasDrawn"];
102
120
  }
103
121
  /**
104
122
  * Mirrors DrawnUi Canvas: the bridge between the DOM (react-dom) and the drawn tree (DrawnUi reconciler).
105
123
  * Requires Super.UseDrawnUi()...BuildAsync() to have completed.
106
124
  */
107
- export declare function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef }: CanvasProps): import("react").JSX.Element;
125
+ export declare function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef, ContextMenu, WillFirstTimeDraw, WasDrawn }: CanvasProps): import("react").JSX.Element;
108
126
  export type { SkiaControl };
109
127
  export * from "../index";
110
128
  export * from "./SkiaShell";
@@ -49,22 +49,40 @@ export const SkiaCarousel = "SkiaCarousel";
49
49
  /** Carousel whose slide changes are rendered by an SkSL transition (C# SkiaShaderCarousel); slides need UseCache="Image". */
50
50
  export const SkiaShaderCarousel = "SkiaShaderCarousel";
51
51
  export const SkiaDrawer = "SkiaDrawer";
52
+ /**
53
+ * One-shot, at the first Canvas mount: removes the HTML the `drawnUiStatic` Vite plugin generated for crawlers
54
+ * (`[data-drawnui-static]`, placed after the mount element). A querySelectorAll once per page, never again; an
55
+ * app built without the plugin finds nothing.
56
+ */
57
+ let staticRemoved = false;
58
+ function removeStaticContent() {
59
+ if (staticRemoved || typeof document === "undefined")
60
+ return;
61
+ staticRemoved = true;
62
+ for (const el of document.querySelectorAll("[data-drawnui-static]"))
63
+ el.remove();
64
+ }
52
65
  /**
53
66
  * Mirrors DrawnUi Canvas: the bridge between the DOM (react-dom) and the drawn tree (DrawnUi reconciler).
54
67
  * Requires Super.UseDrawnUi()...BuildAsync() to have completed.
55
68
  */
56
- export function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef }) {
69
+ export function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef, ContextMenu, WillFirstTimeDraw, WasDrawn }) {
57
70
  const ref = useRef(null);
58
71
  const view = useRef(null);
59
72
  const root = useRef(null);
60
73
  const [engine, setEngine] = useState(null);
61
74
  useLayoutEffect(() => {
62
- const v = new CanvasView(ref.current);
63
- if (RenderingMode)
64
- v.RenderingMode = RenderingMode;
75
+ // the engine draws frame 1 inside its constructor: the draw events and the rendering mode must be on it before that
76
+ const v = new CanvasView(ref.current, (c) => {
77
+ if (RenderingMode)
78
+ c.RenderingMode = RenderingMode;
79
+ c.WillFirstTimeDraw = WillFirstTimeDraw;
80
+ c.WasDrawn = WasDrawn;
81
+ });
65
82
  view.current = v;
66
83
  root.current = createDrawnRoot(v);
67
84
  setEngine(v);
85
+ removeStaticContent(); // the constructor drew frame 1: the crawlable block (drawnUiStatic) is duplicate content from here on
68
86
  return () => { root.current?.unmount(); v.Dispose(); view.current = null; root.current = null; setEngine(null); };
69
87
  // RenderingMode is read once at surface creation, like DrawnUi.
70
88
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -78,6 +96,9 @@ export function Canvas({ BackgroundColor, RenderingMode, Gestures, children, sty
78
96
  v.Update();
79
97
  }
80
98
  v.Gestures = Gestures ?? "Disabled";
99
+ v.ContextMenu = ContextMenu;
100
+ v.WillFirstTimeDraw = WillFirstTimeDraw;
101
+ v.WasDrawn = WasDrawn;
81
102
  root.current.render(children);
82
103
  });
83
104
  // The drawn surface is aria-hidden; the overlay is the accessibility tree (DrawnUi.Blazor pattern).
@@ -85,6 +85,8 @@ const hostConfig = {
85
85
  if (!ctor)
86
86
  throw new Error(`DrawnUi: unknown control <${type}>`);
87
87
  const inst = new ctor();
88
+ // ConfigureStyles defaults first, the JSX props below override them (a TextSpan is not a control, it has none)
89
+ inst.ApplyInitialStyles?.(true);
88
90
  applyProps(inst, null, props);
89
91
  return inst;
90
92
  },