drawnui-react 0.1.0-preview.2 → 0.1.0-preview.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,11 +67,13 @@ the showcase for other frameworks on this engine.
67
67
 
68
68
  ## Context menu (right click)
69
69
 
70
- A right click on the canvas normally opens the browser's own menu ("Save image as…"). Only the primary mouse
71
- button starts a gesture, as in DrawnUi.Blazor, so a right click never fires `Tapped`; to take it, handle
72
- `ContextMenu` on any control, exactly like `Tapped`:
70
+ A right click on the canvas normally opens the browser's own menu ("Save image as…"). Every mouse button goes
71
+ through the gesture pipeline as in DrawnUi.Net (`e.Parameters.Event.Pointer.Button` says which), so a right click is
72
+ a `Tapped` with `Button` `"Right"` for games and custom controls; a handler that should act on the primary button only
73
+ checks it. To take the browser menu request, handle `ContextMenu` on any control, exactly like `Tapped`:
73
74
 
74
75
  ```tsx
76
+ <SkiaShape Tapped={(s, e) => { if ((e.Parameters.Event.Pointer?.Button ?? "Left") === "Left") open(); }} />
75
77
  <SkiaShape ContextMenu={(sender, e) => { openMyMenu(e.Location); return true; }} />
76
78
  <Canvas ContextMenu={(canvas, e) => true /* nothing hit: swallow the browser menu everywhere */} />
77
79
  ```
package/SKIPPED.md CHANGED
@@ -9,7 +9,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
9
9
  |---|---|---|
10
10
  | `UseCache` | partial | `None`, `Operations` / `OperationsFull` (SkPicture replay), `Image` (offscreen surface snapshot, GPU-backed on WebGL, nearest-sampled blit), `ImageDoubleBuffered` (`RenderObjectPrevious` kept and drawn when a new cache cannot be produced yet, `DrawPlaceholder(ctx)` hook when there is none; recording itself is synchronous — no background thread, same as DrawnUi.Blazor with `CanUseCacheDoubleBuffering = false`), `ImageComposite` / `ImageCompositeGPU` (per-child composite, see the `UseCache="ImageComposite"` row below). `GPU` resolves to `Image` and `ImageCompositeGPU` to `ImageComposite`: no separate GPU-only path. `Super.CacheEnabled`, `RenderObject`/`CachedObject`, `InvalidateCache`, `DestroyRenderingObject`, `Canvas.DisposeObject` (deleted after flush). Defaults per C#: `SkiaLabel`/`SkiaSvg` Operations, everything else None. Not ported: `CacheSharing`, `AllowCaching`, offscreen bake threads, cache validity by GRContext. |
11
11
  | Incremental invalidation | partial | `Measure` returns the previous size when `!NeedMeasure` and constraints+scale are unchanged; `Update()`/`InvalidateMeasure()` bubble up and stale every ancestor cache; `Repaint()` keeps caches. Arrange still runs per frame for the whole tree; no dirty regions, no `DirtyChildrenTracker`. Post-animators on a control nested INSIDE a cached ancestor are drawn only when that ancestor re-records. |
12
- | Gestures core: `Canvas.Gestures` (Disabled/Enabled/Lock), `ProcessGestures(args, apply)`, `ConsumeGestures`, `Tapped`/`ChildTapped`, `InputTransparent`, `BlockGesturesBelow`, `LockChildrenGestures`, `HitBoxAuto`/`HitIsInside`/`IsGestureForChild`, `SkiaGesturesParameters`/`GestureEventProcessingInfo`/`SkiaGesturesInfo`/`ControlTappedEventArgs` | ported | Pointer events -> per-pointer `OnTouchAction` state machine (port of DrawnUi.Blazor) -> Down/Panning/Tapped/Up, queued and processed at frame start. Tap slop = 16pt like AppoMobi TouchEffect. Only the primary mouse button starts a gesture (right / middle clicks are ignored, like DrawnUi.Blazor). |
12
+ | Gestures core: `Canvas.Gestures` (Disabled/Enabled/Lock), `ProcessGestures(args, apply)`, `ConsumeGestures`, `Tapped`/`ChildTapped`, `InputTransparent`, `BlockGesturesBelow`, `LockChildrenGestures`, `HitBoxAuto`/`HitIsInside`/`IsGestureForChild`, `SkiaGesturesParameters`/`GestureEventProcessingInfo`/`SkiaGesturesInfo`/`ControlTappedEventArgs` | ported | Pointer events -> per-pointer `OnTouchAction` state machine (port of DrawnUi.Blazor) -> Down/Panning/Tapped/Up, queued and processed at frame start. Tap slop = 16pt like AppoMobi TouchEffect. Every mouse button is delivered (`Event.Pointer`: `Button`, `ButtonNumber`, `DeviceType`, `PressedButtons`, the AppoMobi.Gestures PointerData): a right click is Down / Up / Tapped with `Button` "Right" plus a `ContextMenu`; handlers that must be primary-only check `e.Parameters.Event.Pointer?.Button`. |
13
13
  | Gestures: `LongPressing`, `Pointer` (hover), multi-touch pinch, `SoftLock`, `AddGestures` attached props, `OnGestures` delegate | skipped | The enum members exist for parity and are never produced; there is no pinch member at all. Ported instead of skipped: velocity (`Gestures.ts` fills `Distance.Velocity` per move, the scroll / snapping physics run on it) and transform-aware mapping — a child receives the point through the inverse `RenderTransformMatrix` (`TransformPointToLocalSpace`), so rotated / scaled / translated controls hit-test correctly. `ChildOffset` stays zero because that inverse matrix replaces the C# cache-offset math (`TranslateInputCoords`), not because transforms are missing. `Wheel` IS produced (`Canvas.onWheel`). |
14
14
  | C# multi-subscriber `event`s | changed shape | One callback per event prop (`Tapped={fn}`), same names. `Command*` (ICommand) variants not ported. |
15
15
  | `Opacity`, `TranslationX/Y`, `Rotation`, `ScaleX/Y` (+`Scale`), `SkewX/Y`, `AnchorX/Y`, `HasTransform`, `RenderTransformMatrix`, `TransformPointToLocalSpace` | ported | Same matrix order as C# `ApplyTransforms` (pivot → rotation → scale/skew → translation), opacity = `saveLayer` alpha over the subtree, gestures mapped through the inverse matrix (`IsGestureForChild`, child receives the local point), accessibility rect = transformed bounds. Animations: `AnimateAsync`, `AnimateRangeAsync`, `FadeToAsync`, `ScaleToAsync`, `TranslateToAsync`, `RotateToAsync` as Promises with `AbortSignal` (one running per kind, like the C# per-property cancellation). Not ported: `RotationX/Y/Z`, `TranslationZ`, `Perspective1/2`, `CameraAngle`, `LinkTransforms`, `AddTranslationX/Y`, `UseTranslation*` overrides, `CustomizeLayerPaint`, `SkipRenderingOutOfBounds`. |
@@ -30,7 +30,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
30
30
  | `ZIndex` | ported | Static children of a layout are drawn in ZIndex order (stable for equal values), gestures go to the top-most first; the sorted list is cached until a child or a ZIndex changes (C# `GetOrderedSubviews`). Templated cells keep index order. |
31
31
  | `FillGradient`, `SkiaShape.StrokeGradient` | ported | `SkiaGradient` as a plain object: `Type` Linear (Start/End ratios or CSS `Angle`), Circular / Conical (radius = half the smaller side), Oval (radial scaled to the box), Sweep (around the center, `Value1`/`Value2` angles), `ColorPositions`, `TileMode`, `Light` (< 1 darker, > 1 lighter — HSL lightness, C# uses `MakeDarker/MakeLighter`, exact curve unverified), `Opacity`, `BlendMode`; C# `SetupGradient` paint setup (white base); shaders cached per gradient object and rect (C# `Version` cache: a new React literal replaces the old shaders). `SkiaLabel.FillGradient` paints the glyphs (`GradientByLines` default true = per line bounds, false = the text block) and the background only when `BackgroundColor` is set, like the C# label override. `SkiaLabel` text stroke (`StrokeColor`, `StrokeWidth` points, `StrokeGradient`) and drop shadow (`DropShadowColor`, `DropShadowSize`, `DropShadowOffsetX/Y`) in the C# order shadow → stroke → fill, measurement inflated by stroke × 2 and shadow size + offset like C#. Not ported: `Background` MAUI brushes, `FillBlendMode`. |
32
32
  | `SkiaShell` | partial (React-level) | `Routes` (route -> JSX page factory), `GoToAsync(route, animated)` / `GoBackAsync(animated)` / `PopToRootAsync` with the SkiaViewSwitcher slide (`PagesAnimationSpeed` 200 ms, pushed pages opaque over the page below, lower pages hidden when idle), `NavigationStack`, `CanGoBack`, `Route`, nav bar with Back + title, `useShell()`; `OpenPopupAsync(content, { animated, closeWhenBackgroundTapped, showOverlay, backgroundColor })` (C# PopupWrapper: dimmed backdrop, scale 0.5→1 + fade over `PopupsAnimationSpeed`, tap outside the content closes), `ClosePopupAsync`, `CloseAllPopups`; `PushModalAsync(content, { useGestures, animated, freezeBackground })` (C# ModalWrapper: full-screen `SkiaDrawer` FromBottom HeaderSize 0, drag-to-close with `useGestures`), `PopModalAsync`; `ShowToast(text | content, ms)` (C# layout: bottom banner, slide + fade 300/250 ms, `ShellDefaults.Toast*`), `CloseAllToasts`; `PopupsCount`/`ModalsCount`/`ToastsCount`; `ShellDefaults` = the C# statics (`PopupBackgroundColor`, `PopupsBackgroundBlur`, `PopupsAnimationSpeed`, `PopupsCancelAnimationsAfterMs`, `ZIndex*`, `Toast*`, tab colors); popups/modals sit on a `SkiaBackdrop` (blur + tint); `Tabs` (C# SkiaViewSwitcher tabs: bottom tab bar, per-tab page stacks, `SelectedTab`, `SelectTabAsync`, `PopTabToRootAsync`); `GoBackAsync` follows C# `GoBackDefault` (top popup, then modal, then page); React extension `UseBrowserHistory` (default true): pages in the URL hash `#/a/b`, one history entry per page / popup / modal, browser Back unwinds in the same order, deep links restore the page stack. Not ported: engine-level shell / `NavigationLayout` view switcher, frozen-screenshot backgrounds, `pixelsScaleInFrom`, `Navigating`/`Navigated`/`RouteChanged` events, `RegisterRoute` by type with arguments, `IHandleGoBack`, insets, `PushAsync(page instance)`; `AnimateTabs` + `TabsAnimationSpeed` (150) ported with the C# SelectLeftTab/SelectRightTab slide (0.75 width, fade, back-ease 0.55) — only the tab roots animate, pages pushed inside the leaving tab drop instantly. Browser history: a `popstate` at the same depth with another hash (a plain `<a href="#/route">` link, a typed hash) rebuilds the stack like a forward navigation. |
33
- | Styles / `ConfigureStyles` | skipped | |
33
+ | Styles / `ConfigureStyles` | ported | `Super.UseDrawnUi().ConfigureStyles(styles => styles.AddStyle({ TargetType, ApplyToDerivedTypes, Setters }))`, the C# `Style` shape with the `Setter` list written as a plain object (`Setters: { FontFamily: "FontText" }`) because controls hold plain properties instead of BindableProperty. Per-class resolution cached, base-type styles applied furthest-first so the nearest wins, exact type last (C# `ResolveStylesForType`). Applied by the reconciler between `new` and the JSX props, so props override; a control built in code-behind is styled on its first `Measure` and keeps values that differ from its class defaults (the React stand-in for C# `ExplicitPropertiesSet`, which needs BindableProperty). Not ported: `BasedOn`, `Class`, `Triggers`, `Behaviors`, the per-control `Style` property, `ConditionalStyle`. |
34
34
  | Animators core: `AnimatorBase` / `SkiaValueAnimator` / `RenderingAnimator` (`IOverlayEffect`), `Canvas.RegisterAnimator`/`AnimatingControls`, `PostAnimators` + `ExecutePostAnimators`, `Easing` (Linear/Cubic*) | ported | Frame-driven: a running animator keeps frames coming, idle canvas draws nothing. |
35
35
  | Touch feedback: `AnimationTapped="Ripple"` + `TouchEffectColor` + `AnimationTappedSpeed` on any control, `SkiaButton.ApplyEffect="Ripple"` (on Down), `PlayRippleAnimation`, `RippleAnimator`, `ClipEffects`/`CreateClip` | ported | Same numbers as C#: 500ms CubicIn, radius 300pt, opacity 0.20 fading over 1.15x progress. |
36
36
  | `Shimmer` touch animation, `ShimmerAnimator`, `ClippedEffectsWith`, `TransformView`, `DelayCallbackMs`, `removePrevious`, `Pause`/`Resume`, `UseInterpolator`, spring/deceleration timing, `AnimateExtensions` (`FadeToAsync`, `TranslateToAsync`, ...) | skipped | |
@@ -92,7 +92,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
92
92
  |---|---|---|
93
93
  | `Super.UseDrawnUi().ConfigureFonts(f => f.AddFont(src, alias)).BuildAsync()` | ported | Same shape as DrawnUi.Net/OpenTK. |
94
94
  | `AddFont(src, alias, weight)`, font fallback, emoji | skipped | |
95
- | `PreloadAssets`, `ConfigureStyles` | skipped | |
95
+ | `PreloadAssets` | skipped | `ConfigureStyles` is ported, see the Engine table. |
96
96
 
97
97
  ## React layer
98
98
 
@@ -101,7 +101,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
101
101
  | Custom `react-reconciler` renderer, PascalCase props mapped 1:1 to control properties | ported | |
102
102
  | `SkiaToggle` / `SkiaSwitch` / `SkiaCheckbox` / `SkiaRadioButton` | ported | Same names: `IsToggled`, `Toggled`, `DefaultValue`, `IsAnimated`, `RespondsToGestures`, `ColorThumbOn/Off`, `ColorFrameOn/Off` (+ `ColorCheckOn`), `ControlStyle` with the Default / Cupertino / Material / Material3 / Windows looks and geometry of the C# style builders (`Frame`/`Thumb`, `FrameOff`/`FrameOn`/`ViewCheckOn`, `On` ring + `Text`), thumb animated with `TranslateToAsync` (`AnimationSpeed` 200 ms), radio groups by `GroupName` or parent. Not ported: `CommandToggled`/`CommandTapped`, hover (`CheckHovered`), `TransformView` hotspot (the toggle itself takes the tap), `Platform` style resolves from the browser UA. |
103
103
  | `SkiaProgress` / `SkiaSlider` | ported (painted, not composed) | `Value`/`Min`/`Max`, `TrackColor`/`ProgressColor`; slider `Min`/`Max`/`Step`/`Start`/`End`/`EnableRange`/`RangeMin`/`SliderHeight`/`ThumbColor`/`TrackColor`/`TrackSelectedColor`/`ClickOnTrailEnabled`/`StartChanged`/`EndChanged`, `StartThumbX`/`EndThumbX`, the C# position<->value math (`SliderHeight` = thumb box, `Step` rounding, `RangeMin` clamps), drag + click-on-trail + per-style looks (track heights, thumb sizes, palettes, Material3 gap + stop dot). Track/trail/thumbs are painted directly instead of child shapes (`SliderTrail`/`SliderThumb` not exposed). Not ported: `Orientation` Vertical, `Invert`, `ValueStringFormat`/`MinMaxStringFormat` + `StartDesc`/`EndDesc`, `IgnoreWrongDirection`, `AvailableWidthAdjustment`, `Trail*Offset`, `Cupertino*` sizing props, hover. |
104
- | `SkiaButton.ControlStyle` | ported | Cupertino / Material / Material3 / Windows accent, corner radius, font size/weight and minimum height from the C# style builders, applied only when the user left `BackgroundColor`/`CornerRadius`(8)/`FontSize`(15)/`MinimumHeightRequest` unset; default look is now the C# Crimson accent. Not ported: `Background` brushes, pressed-state shadow changes. |
104
+ | `SkiaButton.ControlStyle` | ported | Cupertino / Material / Material3 / Windows accent, corner radius, font size/weight and minimum content size from the C# style builders, applied only when the user left `BackgroundColor`/`CornerRadius`(8)/`FontSize`(15)/`MinimumWidthRequest`/`MinimumHeightRequest` unset; default look is now the C# Crimson accent. `SetDefaultMinimumContentSize` is mirrored for BOTH axes (every C# style passes width 100, heights 41/36/40/40/32) and for the default `Unset` style too, skipped on a `Fill` axis or when a `WidthRequest`/`HeightRequest` is set, so short captions clamp to 100pt as in C#; the C# value adds `Margins` because it compares against a margin-inclusive size, here Measure applies it margin-exclusive and adds the margin after. Not ported: `Background` brushes, pressed-state shadow changes. |
105
105
  | `SkiaButton.FontFamilyFallback` | React extension | Pass-through to the inner label so icon-glyph buttons render with `AddSymbols()` faces. |
106
106
  | Accessibility (`ISkiaAccessibilityNode` + `SkiaAccessibilityManager` + Blazor ARIA overlay) | ported | `AccessibilityRole/Label/Hint/CanInteract/IsPressed/Live`, `Aria` constants, `IsAccessibilityElement`, `GetAccessibilityPixelRect`, `NotifyAccessibility`, `OnAccessibilityActivated` (synthetic Tapped), `OnAccessibilityFocused`, snapshot rate-limited by `MinUpdateIntervalMs`, top-left reading order, `SkiaButton` hides its inner label. Extensions: per-class `DefaultAccessibilityRole`, label/interaction defaults derived from text / `Tapped`, `pointer-events:none` overlay, detached/off-canvas nodes pruned at rebuild (no explicit unregister on removal). Not ported: `WithAccessibility*` fluent helpers, `FocusChanged` event consumers, UIA/AT-SPI (browser only), `aria-level` for headings. |
107
107
  | Static HTML for crawlers (`drawnui-react/vite` `drawnUiStatic()`, React extension) | ported | Build-time only: after `vite build` the plugin serves the build, opens it in headless Chrome (`playwright-core`, optional peer), reads the accessibility snapshot of the root page and of every page a root button opens (or `routes`), and writes visible semantic HTML (heading → heading, button → `<a href="#/route">`, text → paragraph, other controls → list items; `img` and `presentation` dropped) after the mount element or at `<!-- drawnui-static -->`, with `DEFAULT_STATIC_STYLE` (`render.style` to override / disable). Runtime: the first `<Canvas>` mount removes every `[data-drawnui-static]` element right after its engine drew frame 1 (`removeStaticContent`, one querySelectorAll, nothing in the frame loop). Not in DrawnUi.Net (Blazor sites hand-write static SEO content, see the drawnui-blazor notes). Not done: per-route documents (pages live in the hash, one URL), `<title>` / meta per page, sitemap entries. |
@@ -44,7 +44,10 @@ export declare class SkiaButton extends SkiaLayout {
44
44
  /** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
45
45
  protected PaintBackground(): void;
46
46
  get UsingControlStyle(): ResolvedControlStyle;
47
- /** C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size. */
47
+ /**
48
+ * C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size.
49
+ * Every C# style calls SetDefaultMinimumContentSize(100, minH), so the width floor is 100 everywhere.
50
+ */
48
51
  private static Look;
49
52
  protected MeasureAbsolute(w: number, h: number, scale: number): ScaledSize;
50
53
  /** The frame sits under the padding box: arrange children in the full rect, the label centered inside padding. */
@@ -49,6 +49,11 @@ export class SkiaButton extends SkiaLayout {
49
49
  this.label.VerticalOptions = "Center";
50
50
  this.AddSubView(this.frame);
51
51
  this.AddSubView(this.label);
52
+ // C# ButtonLabel is styled when the button builds its content, and ApplyProperties then overwrites FontFamily with
53
+ // the button's own (empty by default) value, which the style no longer touches. Same order here: style the caption
54
+ // now, MeasureAbsolute below pushes SkiaButton.FontFamily over it every measure.
55
+ this.frame.ApplyInitialStyles(true);
56
+ this.label.ApplyInitialStyles(true);
52
57
  }
53
58
  /** Set to `Aria.RoleButton` to expose every button (React extension). */
54
59
  static DefaultAccessibilityRole;
@@ -57,14 +62,17 @@ export class SkiaButton extends SkiaLayout {
57
62
  /** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
58
63
  PaintBackground() { }
59
64
  get UsingControlStyle() { return ResolveControlStyle(this.ControlStyle); }
60
- /** C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size. */
65
+ /**
66
+ * C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size.
67
+ * Every C# style calls SetDefaultMinimumContentSize(100, minH), so the width floor is 100 everywhere.
68
+ */
61
69
  static Look(s) {
62
70
  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 };
71
+ case "Cupertino": return { bg: "#007AFF", radius: 8, font: 17, weight: 600, minW: 100, minH: 36, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 2, Opacity: 0.2, Color: Colors.Black }) };
72
+ case "Material": return { bg: "#2196F3", radius: 4, font: 14, weight: 0, minW: 100, minH: 40, shadow: new SkiaShadow({ X: 0, Y: 2, Blur: 4, Opacity: 0.3, Color: Colors.Black }) };
73
+ case "Material3": return { bg: "#6750A4", radius: 20, font: 14, weight: 0, minW: 100, minH: 40 };
74
+ case "Windows": return { bg: "#0078D7", radius: 4, font: 15, weight: 500, minW: 100, minH: 32, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 1, Opacity: 0.2, Color: Colors.Black }) };
75
+ default: return { bg: "#DC143C", radius: 8, font: 15, weight: 0, minW: 100, minH: 41 };
68
76
  }
69
77
  }
70
78
  MeasureAbsolute(w, h, scale) {
@@ -75,7 +83,12 @@ export class SkiaButton extends SkiaLayout {
75
83
  this.frame.StrokeWidth = this.StrokeWidth;
76
84
  if (this.frame.Shadows[0] !== look.shadow)
77
85
  this.frame.Shadows = look.shadow ? [look.shadow] : [];
78
- if (this.MinimumHeightRequest < 0 && this.UsingControlStyle !== "Unset")
86
+ // C# SetDefaultMinimumContentSize(100, minH) from the style builder: a style default only where the app left the
87
+ // size unset, skipped on a Fill axis. C# adds Margins because it compares the minimum against a margin-inclusive
88
+ // size; here Measure applies it to the margin-excluded size and adds the margin after, so the plain value is used.
89
+ if (this.WidthRequest < 0 && this.MinimumWidthRequest < 0 && this.HorizontalOptions !== "Fill")
90
+ this.MinimumWidthRequest = look.minW;
91
+ if (this.HeightRequest < 0 && this.MinimumHeightRequest < 0 && this.VerticalOptions !== "Fill")
79
92
  this.MinimumHeightRequest = look.minH;
80
93
  this.label.Text = this.Text;
81
94
  this.label.TextColor = this.TextColor;
@@ -1,7 +1,7 @@
1
1
  import { Super } from "./Super";
2
2
  import { SkiaAccessibilityManager } from "./Accessibility";
3
3
  import { Colors, SKRect } from "./Types";
4
- import { ContextMenuEventArgs, GestureEventProcessingInfo, SKPoint, SkiaGesturesParameters, TouchActionEventArgs, } from "./Gestures";
4
+ import { ContextMenuEventArgs, GestureEventProcessingInfo, PointerData, SKPoint, SkiaGesturesParameters, TouchActionEventArgs, } from "./Gestures";
5
5
  /**
6
6
  * Mirrors DrawnUi Canvas (DrawnView): hosts one Content control on an HTML canvas element,
7
7
  * owns RenderingScale (devicePixelRatio), the surface, the on-demand frame loop and raw input.
@@ -260,17 +260,13 @@ export class Canvas {
260
260
  e.type === "pointercancel" ? "Cancelled" : undefined;
261
261
  if (!type)
262
262
  return;
263
- // only the primary mouse button is a touch (DrawnUi.Blazor): right / middle buttons never start a gesture, and
264
- // their Up / Cancel (pointer never pressed here) is ignored too, so a right click cannot end as a Tapped
265
- if (type === "Pressed" && e.pointerType === "mouse" && e.button !== 0)
266
- return;
267
263
  if (type === "Moved" && !this.activeTouchIds.has(e.pointerId)) {
268
264
  if (e.pointerType === "mouse")
269
265
  this.UpdateCursor(e.offsetX, e.offsetY);
270
266
  return;
271
267
  } // hover not ported (TouchActionResult.Pointer)
272
268
  if ((type === "Released" || type === "Cancelled") && !this.activeTouchIds.has(e.pointerId))
273
- return;
269
+ return; // Up of a pointer that never pressed here
274
270
  // Capture so Up outside the element still arrives; throws for synthetic events (tests) — harmless.
275
271
  if (type === "Pressed") {
276
272
  try {
@@ -284,6 +280,14 @@ export class Canvas {
284
280
  args.Type = type;
285
281
  args.Scale = this.RenderingScale;
286
282
  args.Location = new SKPoint((e.clientX - rect.left) * this.RenderingScale, (e.clientY - rect.top) * this.RenderingScale);
283
+ // every mouse button is delivered with its PointerData (AppoMobi.Gestures): a right click is a Down / Up / Tapped
284
+ // with Button "Right" for games and custom controls, and a ContextMenu on top
285
+ const pd = new PointerData();
286
+ pd.Button = e.button === 0 ? "Left" : e.button === 1 ? "Middle" : e.button === 2 ? "Right" : e.button === 3 ? "XButton1" : e.button === 4 ? "XButton2" : "Extended";
287
+ pd.ButtonNumber = e.button === 1 ? 3 : e.button === 2 ? 2 : e.button + 1;
288
+ pd.DeviceType = e.pointerType === "touch" ? "Touch" : e.pointerType === "pen" ? "Pen" : "Mouse";
289
+ pd.PressedButtons = e.buttons;
290
+ args.Pointer = pd;
287
291
  this.OnTouchAction(args);
288
292
  };
289
293
  preventTouch = (e) => e.preventDefault();
@@ -21,9 +21,23 @@ export declare class DistanceInfo {
21
21
  Total: SKPoint;
22
22
  Velocity: SKPoint;
23
23
  }
24
+ /** AppoMobi.Gestures MouseButton: which button pressed / released (DOM button 0..4). */
25
+ export type MouseButton = "Left" | "Middle" | "Right" | "XButton1" | "XButton2" | "Extended";
26
+ export type PointerDeviceType = "Mouse" | "Touch" | "Pen";
27
+ /** AppoMobi.Gestures PointerData: the device and button behind a Down / Up / Tapped (every button is delivered). */
28
+ export declare class PointerData {
29
+ Button: MouseButton;
30
+ /** 1 = Left, 2 = Right, 3 = Middle, 4+ = extended, like AppoMobi.Gestures. */
31
+ ButtonNumber: number;
32
+ DeviceType: PointerDeviceType;
33
+ /** DOM `buttons` bitmask of the buttons held (1 left, 2 right, 4 middle, 8 back, 16 forward). */
34
+ PressedButtons: number;
35
+ }
24
36
  /** AppoMobi.Gestures TouchActionEventArgs. */
25
37
  export declare class TouchActionEventArgs {
26
38
  Id: number;
39
+ /** Device and button of this event (mouse, pen, touch); undefined for wheel. */
40
+ Pointer?: PointerData;
27
41
  Type: TouchActionType;
28
42
  /** Pixels. */
29
43
  Location: SKPoint;
@@ -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;
@@ -191,6 +191,13 @@ export declare class SkiaControl {
191
191
  ConsumeGestures?: (sender: SkiaControl, e: SkiaGesturesInfo) => void;
192
192
  Parent?: SkiaControl;
193
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;
194
201
  AddSubView(_control: SkiaControl): void;
195
202
  InsertSubView(_index: number, control: SkiaControl): void;
196
203
  RemoveSubView(_control: SkiaControl): void;
@@ -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";
@@ -250,6 +251,17 @@ export class SkiaControl {
250
251
  // ---- tree ----
251
252
  Parent;
252
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
+ }
253
265
  AddSubView(_control) { throw new Error(`DrawnUi: ${this.constructor.name} cannot host children`); }
254
266
  InsertSubView(_index, control) { this.AddSubView(control); }
255
267
  RemoveSubView(_control) { }
@@ -271,6 +283,8 @@ export class SkiaControl {
271
283
  * this is what keeps a cached tree from re-measuring text every frame.
272
284
  */
273
285
  Measure(widthConstraint, heightConstraint, scale) {
286
+ if (!this.styledInitially)
287
+ this.ApplyInitialStyles(false);
274
288
  if (!this.NeedMeasure && this.RenderingScale === scale
275
289
  && Object.is(this.lastWidthConstraint, widthConstraint) && Object.is(this.lastHeightConstraint, heightConstraint)) {
276
290
  return this.MeasuredSize;
@@ -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))
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";
@@ -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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drawnui-react",
3
- "version": "0.1.0-preview.2",
3
+ "version": "0.1.0-preview.3",
4
4
  "description": "DrawnUi for React: the DrawnUi (.NET) drawn-UI engine in TypeScript on CanvasKit (Skia WASM). Same controls, same PascalCase API as DrawnUi.Net.",
5
5
  "license": "MIT",
6
6
  "author": "Nick Kovalsky (AppoMobi)",