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

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 (49) hide show
  1. package/README.md +98 -29
  2. package/SKIPPED.md +23 -16
  3. package/dist/controls/DrawnGame.d.ts +37 -0
  4. package/dist/controls/DrawnGame.js +60 -0
  5. package/dist/controls/SkiaButton.d.ts +4 -1
  6. package/dist/controls/SkiaButton.js +20 -7
  7. package/dist/controls/SkiaCarousel.d.ts +6 -0
  8. package/dist/controls/SkiaCarousel.js +7 -1
  9. package/dist/controls/SkiaCheckbox.d.ts +0 -1
  10. package/dist/controls/SkiaCheckbox.js +0 -6
  11. package/dist/controls/SkiaEditor.js +8 -0
  12. package/dist/controls/SkiaLabel.d.ts +2 -0
  13. package/dist/controls/SkiaLabel.js +9 -0
  14. package/dist/controls/SkiaLayout.d.ts +10 -0
  15. package/dist/controls/SkiaLayout.js +64 -1
  16. package/dist/controls/SkiaRadioButton.js +1 -2
  17. package/dist/controls/SkiaScroll.d.ts +9 -0
  18. package/dist/controls/SkiaScroll.js +120 -13
  19. package/dist/controls/SkiaScrollBar.d.ts +27 -0
  20. package/dist/controls/SkiaScrollBar.js +51 -2
  21. package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
  22. package/dist/controls/SkiaShaderCarousel.js +5 -0
  23. package/dist/controls/SkiaSwitch.d.ts +0 -1
  24. package/dist/controls/SkiaSwitch.js +0 -6
  25. package/dist/controls/SkiaToggle.d.ts +18 -4
  26. package/dist/controls/SkiaToggle.js +45 -3
  27. package/dist/core/Animators.d.ts +9 -0
  28. package/dist/core/Animators.js +26 -1
  29. package/dist/core/Canvas.d.ts +82 -4
  30. package/dist/core/Canvas.js +261 -10
  31. package/dist/core/Gestures.d.ts +47 -1
  32. package/dist/core/Gestures.js +43 -1
  33. package/dist/core/SkiaControl.d.ts +38 -2
  34. package/dist/core/SkiaControl.js +76 -3
  35. package/dist/core/Styles.d.ts +51 -0
  36. package/dist/core/Styles.js +92 -0
  37. package/dist/core/Super.d.ts +10 -1
  38. package/dist/core/Super.js +20 -3
  39. package/dist/core/Types.d.ts +7 -2
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.js +2 -0
  42. package/dist/react/SkiaShell.js +5 -2
  43. package/dist/react/index.d.ts +22 -4
  44. package/dist/react/index.js +25 -4
  45. package/dist/react/reconciler.js +2 -0
  46. package/dist/vite/index.d.ts +74 -0
  47. package/dist/vite/index.js +204 -0
  48. package/package.json +35 -4
  49. package/PARITY.md +0 -287
@@ -18,9 +18,20 @@ export class DistanceInfo {
18
18
  Total = SKPoint.Empty;
19
19
  Velocity = SKPoint.Empty;
20
20
  }
21
+ /** AppoMobi.Gestures PointerData: the device and button behind a Down / Up / Tapped (every button is delivered). */
22
+ export class PointerData {
23
+ Button = "Left";
24
+ /** 1 = Left, 2 = Right, 3 = Middle, 4+ = extended, like AppoMobi.Gestures. */
25
+ ButtonNumber = 1;
26
+ DeviceType = "Mouse";
27
+ /** DOM `buttons` bitmask of the buttons held (1 left, 2 right, 4 middle, 8 back, 16 forward). */
28
+ PressedButtons = 0;
29
+ }
21
30
  /** AppoMobi.Gestures TouchActionEventArgs. */
22
31
  export class TouchActionEventArgs {
23
32
  Id = 0;
33
+ /** Device and button of this event (mouse, pen, touch); undefined for wheel. */
34
+ Pointer;
24
35
  Type = "Pressed";
25
36
  /** Pixels. */
26
37
  Location = SKPoint.Empty;
@@ -34,6 +45,11 @@ export class TouchActionEventArgs {
34
45
  DeltaTimeMs = 0;
35
46
  /** Mouse wheel: Delta > 0 = wheel down (browser deltaY sign). */
36
47
  Wheel = { Delta: 0 };
48
+ /**
49
+ * AppoMobi TouchActionEventArgs.Handled: set by a control that actually used the event. The canvas prevents the
50
+ * browser default (the page scrolling under the wheel) only for a Handled event in `Gestures="Enabled"`.
51
+ */
52
+ Handled = false;
37
53
  /** Same as the .NET helper: derives Start/End/Delta/Total from the previous event of the same pointer. */
38
54
  static FillDistanceInfo(current, previous) {
39
55
  if (!previous) {
@@ -91,7 +107,33 @@ export class SkiaGesturesInfo {
91
107
  this.Info = Info;
92
108
  }
93
109
  }
94
- /** DrawnUi ControlTappedEventArgs. */
110
+ /**
111
+ * Arguments of SkiaControl.ContextMenu / Canvas.ContextMenu: a browser `contextmenu` request on the canvas (right
112
+ * click, long press on touch, the Menu key). Handlers return true to take it: the browser's own menu ("Save image")
113
+ * is then suppressed; otherwise it shows as usual.
114
+ */
115
+ export class ContextMenuEventArgs {
116
+ Location;
117
+ Pixels;
118
+ Source;
119
+ Native;
120
+ /** Deepest control under the point that had a ContextMenu handler (set while routing). */
121
+ Control;
122
+ /** Point inside Control, in pixels relative to its DrawingRect origin (set while routing). */
123
+ Local = SKPoint.Empty;
124
+ constructor(
125
+ /** Point on the canvas, in points (CSS px). */
126
+ Location,
127
+ /** Same point in pixels (canvas space). */
128
+ Pixels, Source,
129
+ /** The DOM event: modifiers, target, preventDefault if you need it yourself. */
130
+ Native) {
131
+ this.Location = Location;
132
+ this.Pixels = Pixels;
133
+ this.Source = Source;
134
+ this.Native = Native;
135
+ }
136
+ }
95
137
  export class ControlTappedEventArgs {
96
138
  Control;
97
139
  Parameters;
@@ -1,9 +1,9 @@
1
1
  import type { Canvas as SkCanvas, Image, Path, SkPicture, Surface } from "canvaskit-wasm";
2
2
  import { type Color, type LayoutOptions, SKRect, ScaledSize, type SkiaCacheType, type SkiaGradient, type SkiaTouchAnimation, Thickness } from "./Types";
3
- import { type IOverlayEffect } from "./Animators";
3
+ import { type AnimatorBase, 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 {
@@ -164,6 +164,11 @@ export declare class SkiaControl {
164
164
  AnimationTappedSpeed: number;
165
165
  /** Overlay effects drawn above this control's content every frame (ripple etc). */
166
166
  readonly PostAnimators: IOverlayEffect[];
167
+ /**
168
+ * C# PendingUnattachedAnimators: animators started while no Canvas was above this control yet (e.g. a game loop
169
+ * started in a constructor). They start on the control's first arrange, where C# starts them in SuperViewChanged.
170
+ */
171
+ PendingUnattachedAnimators?: AnimatorBase[];
167
172
  private visualEffects;
168
173
  /** Post-renderer effects among VisualEffects (C# EffectPostRenderers). */
169
174
  EffectPostRenderers: (SkiaEffect & IPostRendererEffect)[];
@@ -180,11 +185,24 @@ export declare class SkiaControl {
180
185
  /** Clip overlay effects to the control's shape (CreateClip). */
181
186
  ClipEffects: boolean;
182
187
  Tapped?: (sender: SkiaControl, e: ControlTappedEventArgs) => void;
188
+ /**
189
+ * Context-menu request over this control (right click, long press on touch, Menu key). Return true to handle it:
190
+ * the browser's own canvas menu is suppressed. Routed deepest child first, then parents, then Canvas.ContextMenu.
191
+ * Not in DrawnUi.Net (a web concept).
192
+ */
193
+ ContextMenu?: (sender: SkiaControl, e: ContextMenuEventArgs) => boolean | void;
183
194
  ChildTapped?: (sender: SkiaControl, e: ControlTappedEventArgs) => void;
184
195
  /** Raw gesture hook: set e.Consumed = true to stop propagation (not for Up). */
185
196
  ConsumeGestures?: (sender: SkiaControl, e: SkiaGesturesInfo) => void;
186
197
  Parent?: SkiaControl;
187
198
  /** Containers override; a leaf control cannot host children. */
199
+ private styledInitially;
200
+ /**
201
+ * C# ApplyInitialStyles: takes the property defaults registered with `ConfigureStyles` for this control's class.
202
+ * The reconciler calls it with `atConstruction` right after `new`, before the JSX props, so props win; a control
203
+ * built in code-behind is styled on its first measure and keeps whatever the code already set.
204
+ */
205
+ ApplyInitialStyles(atConstruction?: boolean): void;
188
206
  AddSubView(_control: SkiaControl): void;
189
207
  InsertSubView(_index: number, control: SkiaControl): void;
190
208
  RemoveSubView(_control: SkiaControl): void;
@@ -207,6 +225,8 @@ export declare class SkiaControl {
207
225
  protected MeasureAbsolute(_widthConstraint: number, _heightConstraint: number, scale: number): ScaledSize;
208
226
  /** Places MeasuredSize inside destination per Margin + Horizontal/VerticalOptions -> DrawingRect. */
209
227
  Arrange(destination: SKRect, _widthRequest: number, _heightRequest: number, scale: number): void;
228
+ /** C# SuperViewChanged: starts the animators queued before this control was attached. */
229
+ private StartPendingAnimators;
210
230
  private static nextAccessibilityId;
211
231
  /** Stable identity for the DOM overlay. */
212
232
  readonly AccessibilityId: number;
@@ -235,6 +255,11 @@ export declare class SkiaControl {
235
255
  get AccessibilityCanInteract(): boolean;
236
256
  set AccessibilityCanInteract(v: boolean);
237
257
  protected DefaultAccessibilityCanInteract(): boolean;
258
+ /**
259
+ * Whether the mouse at this point (pixels, relative to DrawingRect's origin) is over something tappable, for the
260
+ * host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
261
+ */
262
+ WantsPointerCursor(_x: number, _y: number): boolean;
238
263
  /** aria-pressed for toggles; undefined = not a toggle. */
239
264
  get AccessibilityIsPressed(): boolean | undefined;
240
265
  set AccessibilityIsPressed(v: boolean | undefined);
@@ -364,12 +389,23 @@ export declare class SkiaControl {
364
389
  * cache holds its composited output, so those are staled before redrawing (DrawnUi RedrawCanvas + parent invalidation).
365
390
  */
366
391
  RepaintComposition(): void;
392
+ /**
393
+ * DrawnUi IsParentIndependent: this control's size never changes the layout around it, so its own invalidation
394
+ * stops here (ancestors composite again, nothing above re-measures). The parent re-measures it on draw when
395
+ * NeedMeasure is set. Used by SkiaScroll for its scroll bars.
396
+ */
397
+ IsParentIndependent: boolean;
367
398
  /** Size or content may have changed: this control and all ancestors remeasure and re-record. */
368
399
  InvalidateMeasure(): void;
369
400
  /** Hit rect in pixels (no transforms in this port, so it is DrawingRect). */
370
401
  get HitBoxAuto(): SKRect;
371
402
  HitIsInside(x: number, y: number): boolean;
372
403
  IsGestureForChild(child: SkiaControl, point: SKPoint): boolean;
404
+ /**
405
+ * Routes a context-menu request like a tap: visible, non-transparent children under the point first (top-most
406
+ * first, each in its own transformed space), then this control's ContextMenu handler. True = handled.
407
+ */
408
+ ProcessContextMenu(point: SKPoint, e: ContextMenuEventArgs): boolean;
373
409
  /** Children that may receive gestures, top-most LAST (layouts return their Views). */
374
410
  protected GetGestureListeners(): readonly SkiaControl[];
375
411
  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";
@@ -198,6 +199,11 @@ export class SkiaControl {
198
199
  AnimationTappedSpeed = 0;
199
200
  /** Overlay effects drawn above this control's content every frame (ripple etc). */
200
201
  PostAnimators = [];
202
+ /**
203
+ * C# PendingUnattachedAnimators: animators started while no Canvas was above this control yet (e.g. a game loop
204
+ * started in a constructor). They start on the control's first arrange, where C# starts them in SuperViewChanged.
205
+ */
206
+ PendingUnattachedAnimators;
201
207
  // ---- visual effects (C# VisualEffects: attached SkiaEffect objects) ----
202
208
  visualEffects = [];
203
209
  /** Post-renderer effects among VisualEffects (C# EffectPostRenderers). */
@@ -238,12 +244,29 @@ export class SkiaControl {
238
244
  ClipEffects = true;
239
245
  // ---- gesture events (single handler each; C# events map to one callback prop) ----
240
246
  Tapped;
247
+ /**
248
+ * Context-menu request over this control (right click, long press on touch, Menu key). Return true to handle it:
249
+ * the browser's own canvas menu is suppressed. Routed deepest child first, then parents, then Canvas.ContextMenu.
250
+ * Not in DrawnUi.Net (a web concept).
251
+ */
252
+ ContextMenu;
241
253
  ChildTapped;
242
254
  /** Raw gesture hook: set e.Consumed = true to stop propagation (not for Up). */
243
255
  ConsumeGestures;
244
256
  // ---- tree ----
245
257
  Parent;
246
258
  /** Containers override; a leaf control cannot host children. */
259
+ styledInitially = false;
260
+ /**
261
+ * C# ApplyInitialStyles: takes the property defaults registered with `ConfigureStyles` for this control's class.
262
+ * The reconciler calls it with `atConstruction` right after `new`, before the JSX props, so props win; a control
263
+ * built in code-behind is styled on its first measure and keeps whatever the code already set.
264
+ */
265
+ ApplyInitialStyles(atConstruction = false) {
266
+ this.styledInitially = true;
267
+ if (!SkiaStyles.IsEmpty)
268
+ SkiaStyles.Apply(this, atConstruction);
269
+ }
247
270
  AddSubView(_control) { throw new Error(`DrawnUi: ${this.constructor.name} cannot host children`); }
248
271
  InsertSubView(_index, control) { this.AddSubView(control); }
249
272
  RemoveSubView(_control) { }
@@ -265,6 +288,8 @@ export class SkiaControl {
265
288
  * this is what keeps a cached tree from re-measuring text every frame.
266
289
  */
267
290
  Measure(widthConstraint, heightConstraint, scale) {
291
+ if (!this.styledInitially)
292
+ this.ApplyInitialStyles(false);
268
293
  if (!this.NeedMeasure && this.RenderingScale === scale
269
294
  && Object.is(this.lastWidthConstraint, widthConstraint) && Object.is(this.lastHeightConstraint, heightConstraint)) {
270
295
  return this.MeasuredSize;
@@ -343,10 +368,21 @@ export class SkiaControl {
343
368
  const y = availT + SkiaControl.Align(this.VerticalOptions, fullH, h);
344
369
  this.DrawingRect = SKRect.Create(x, y, w, h);
345
370
  this.OnLayoutChanged();
371
+ if (this.PendingUnattachedAnimators)
372
+ this.StartPendingAnimators();
346
373
  // DrawnUi OnLayoutReady: an accessible control registers itself on its first layout
347
374
  if (!this.registeredWithAccessibility && this.IsAccessibilityElement)
348
375
  this.NotifyAccessibility();
349
376
  }
377
+ /** C# SuperViewChanged: starts the animators queued before this control was attached. */
378
+ StartPendingAnimators() {
379
+ if (!this.Superview)
380
+ return;
381
+ const pending = this.PendingUnattachedAnimators;
382
+ this.PendingUnattachedAnimators = undefined;
383
+ for (const animator of pending)
384
+ animator.Start();
385
+ }
350
386
  // ---- accessibility (DrawnUi ISkiaAccessibilityNode) ----
351
387
  static nextAccessibilityId = 1;
352
388
  /** Stable identity for the DOM overlay. */
@@ -388,6 +424,11 @@ export class SkiaControl {
388
424
  this.AccessibilityChanged();
389
425
  } }
390
426
  DefaultAccessibilityCanInteract() { return !!this.Tapped; }
427
+ /**
428
+ * Whether the mouse at this point (pixels, relative to DrawingRect's origin) is over something tappable, for the
429
+ * host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
430
+ */
431
+ WantsPointerCursor(_x, _y) { return this.AccessibilityCanInteract; }
391
432
  /** aria-pressed for toggles; undefined = not a toggle. */
392
433
  get AccessibilityIsPressed() { return this.accessibilityIsPressed; }
393
434
  set AccessibilityIsPressed(v) { if (this.accessibilityIsPressed !== v) {
@@ -1013,6 +1054,7 @@ export class SkiaControl {
1013
1054
  this.ownAnimations.clear();
1014
1055
  for (const e of [...this.PostAnimators])
1015
1056
  e.Stop?.();
1057
+ this.PendingUnattachedAnimators = undefined;
1016
1058
  this.PostAnimators.length = 0;
1017
1059
  this.DestroyRenderingObject();
1018
1060
  this.compositeSurface?.delete();
@@ -1057,13 +1099,21 @@ export class SkiaControl {
1057
1099
  }
1058
1100
  this.Repaint();
1059
1101
  }
1102
+ /**
1103
+ * DrawnUi IsParentIndependent: this control's size never changes the layout around it, so its own invalidation
1104
+ * stops here (ancestors composite again, nothing above re-measures). The parent re-measures it on draw when
1105
+ * NeedMeasure is set. Used by SkiaScroll for its scroll bars.
1106
+ */
1107
+ IsParentIndependent = false;
1060
1108
  /** Size or content may have changed: this control and all ancestors remeasure and re-record. */
1061
1109
  InvalidateMeasure() {
1062
1110
  this.NeedMeasure = true;
1063
1111
  this.cacheDirty = true;
1064
1112
  this.compositeFull = true; // structure may change: a composite cannot patch it
1065
1113
  this.effectsMarginCache = undefined;
1066
- if (this.Parent)
1114
+ if (this.Parent && this.IsParentIndependent)
1115
+ this.RepaintComposition();
1116
+ else if (this.Parent)
1067
1117
  this.Parent.InvalidateMeasure();
1068
1118
  else
1069
1119
  this.Superview?.Update();
@@ -1079,6 +1129,25 @@ export class SkiaControl {
1079
1129
  const local = child.TransformPointToLocalSpace(point);
1080
1130
  return child.HitIsInside(local.X, local.Y);
1081
1131
  }
1132
+ /**
1133
+ * Routes a context-menu request like a tap: visible, non-transparent children under the point first (top-most
1134
+ * first, each in its own transformed space), then this control's ContextMenu handler. True = handled.
1135
+ */
1136
+ ProcessContextMenu(point, e) {
1137
+ const listeners = this.GetGestureListeners();
1138
+ for (let i = listeners.length - 1; i >= 0; i--) {
1139
+ const listener = listeners[i];
1140
+ if (!listener.IsVisible || listener.InputTransparent || !this.IsGestureForChild(listener, point))
1141
+ continue;
1142
+ if (listener.ProcessContextMenu(listener.TransformPointToLocalSpace(point), e))
1143
+ return true;
1144
+ }
1145
+ if (!this.ContextMenu)
1146
+ return false;
1147
+ e.Control = this;
1148
+ e.Local = new SKPoint(point.X - this.DrawingRect.Left, point.Y - this.DrawingRect.Top);
1149
+ return this.ContextMenu(this, e) === true;
1150
+ }
1082
1151
  /** Children that may receive gestures, top-most LAST (layouts return their Views). */
1083
1152
  GetGestureListeners() { return SkiaControl.NoListeners; }
1084
1153
  static NoListeners = [];
@@ -1106,15 +1175,19 @@ export class SkiaControl {
1106
1175
  if (this.ConsumeGestures) {
1107
1176
  const sent = new SkiaGesturesInfo(args, apply);
1108
1177
  this.ConsumeGestures(this, sent);
1109
- if (args.Type !== "Up" && sent.Consumed)
1178
+ if (args.Type !== "Up" && sent.Consumed) {
1179
+ args.Event.Handled = true;
1110
1180
  return this;
1181
+ }
1111
1182
  }
1112
1183
  // C# EffectsGestureProcessors: attached effects that process gestures (ISkiaGestureProcessor) see them first
1113
1184
  if (!this.DisableEffects)
1114
1185
  for (const e of this.visualEffects) {
1115
1186
  const p = e.ProcessGestures;
1116
- if (typeof p === "function" && p.call(e, args, apply) && args.Type !== "Up")
1187
+ if (typeof p === "function" && p.call(e, args, apply) && args.Type !== "Up") {
1188
+ args.Event.Handled = true;
1117
1189
  return this;
1190
+ }
1118
1191
  }
1119
1192
  if (this.CheckChildrenGesturesLocked(args.Type))
1120
1193
  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";
@@ -39,6 +40,7 @@ export * from "./controls/SkiaBackdrop";
39
40
  export * from "./controls/SkiaEditor";
40
41
  export * from "./controls/SkiaSprite";
41
42
  export * from "./controls/SkiaSpriteSet";
43
+ export * from "./controls/DrawnGame";
42
44
  export * from "./core/KeyboardManager";
43
45
  export * from "./controls/AnimatedFramesRenderer";
44
46
  export * from "./controls/SkiaLottie";
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";
@@ -39,6 +40,7 @@ export * from "./controls/SkiaBackdrop";
39
40
  export * from "./controls/SkiaEditor";
40
41
  export * from "./controls/SkiaSprite";
41
42
  export * from "./controls/SkiaSpriteSet";
43
+ export * from "./controls/DrawnGame";
42
44
  export * from "./core/KeyboardManager";
43
45
  export * from "./controls/AnimatedFramesRenderer";
44
46
  export * from "./controls/SkiaLottie";
@@ -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; });