drawnui-react 0.1.0-preview.2 → 0.1.0-preview.4
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 +5 -3
- package/SKIPPED.md +5 -5
- package/dist/controls/SkiaButton.d.ts +4 -1
- package/dist/controls/SkiaButton.js +20 -7
- package/dist/controls/SkiaLayout.d.ts +10 -0
- package/dist/controls/SkiaLayout.js +64 -1
- package/dist/core/Canvas.d.ts +9 -0
- package/dist/core/Canvas.js +38 -7
- package/dist/core/Gestures.d.ts +14 -0
- package/dist/core/Gestures.js +11 -0
- package/dist/core/SkiaControl.d.ts +7 -0
- package/dist/core/SkiaControl.js +14 -0
- package/dist/core/Styles.d.ts +51 -0
- package/dist/core/Styles.js +92 -0
- package/dist/core/Super.d.ts +10 -1
- package/dist/core/Super.js +20 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/react/reconciler.js +2 -0
- package/package.json +7 -3
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…").
|
|
71
|
-
|
|
72
|
-
`
|
|
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.
|
|
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. The control that consumes a gesture keeps receiving it until it lets go (DrawnUi `Canvas.HadInput`, one pointer here instead of C#'s set): without it a drag handle, a slider thumb or a held button stops getting `Panning` the moment the finger leaves its box, and a control that stops consuming hands routing straight back to the tree (a button whose press turns into a scroll pan). 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` |
|
|
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 | |
|
|
@@ -61,7 +61,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
61
61
|
| `SkiaLayout` Absolute / Column / Row (+ `SkiaStack`, `SkiaRow`, `SkiaLayer`) | ported | `Spacing`, `Padding`, `Children`/`Views`, `AddSubView`/`RemoveSubView`/`InsertSubView`. |
|
|
62
62
|
| `SkiaLayout` templated (Column only): `ItemsSource` (array), `ItemTemplate` (factory `() => SkiaControl`), `RecyclingTemplate` Enabled/Disabled, `MeasureItemsStrategy` `MeasureFirst` (uniform, O(1) for 100k items) / `MeasureAll`, `ChildrenFactory` (ViewsAdapter pool: `GetOrCreateViewForIndex`, `ReleaseViewAt`), `FirstVisibleIndex`/`LastVisibleIndex`, `DebugString`, `ApplyItemsSource`, `VirtualisationInflated`, `BindingContext`/`ContextIndex` on every control, `SkiaDynamicDrawnCell.SetContent` | ported | Cells realized/bound/arranged/drawn only for the visible viewport (+inflation) each frame. |
|
|
63
63
|
| `LoadMoreCommand` / `LoadMoreTopCommand` / `LoadMoreOffset` / `LoadMoreTopOffset` | ported | Callbacks instead of ICommand. Bottom fires once per content extent when within the offset of the end (or when the content underfills the viewport), re-arms when the content grows or the user moves away (>offset+100 pt, >2 s) — C# CheckNeedToLoadMore. Top arms only after the offset left the top edge, fires when it comes back. Not ported: opposite-direction cooldown (`IsOppositeLoadMoreBlocked`), `IInsideViewport.ShouldTriggerLoadMore`. |
|
|
64
|
-
| Structure-preserving `ItemsSource` updates | ported (array diff) | C# listens to `ObservableCollection` Add/Insert; React apps replace the array, so the setter detects **append** (old is a prefix: measured heights kept, tail measured lazily) and **prepend** (old is a suffix: view indices shifted without rebinding, new head measured synchronously up to 200 items, `ItemsInsertedAtStart(px)` lets the `SkiaScroll` move its offset so visible rows stay put — C# head-insert rebase). Any other change rebuilds the structure (heights re-measured, offset kept). Not ported: removals
|
|
64
|
+
| Structure-preserving `ItemsSource` updates | ported (array diff) | C# listens to `ObservableCollection` Add/Insert; React apps replace the array, so the setter detects **append** (old is a prefix: measured heights kept, tail measured lazily) and **prepend** (old is a suffix: view indices shifted without rebinding, new head measured synchronously up to 200 items, `ItemsInsertedAtStart(px)` lets the `SkiaScroll` move its offset so visible rows stay put — C# head-insert rebase). and **reorder** (same items permuted, the C# `ObservableCollection.Move` case: contexts rebound, the index-keyed measured heights permuted with their items, nothing re-measured, extent and scroll offset unchanged). Any other change rebuilds the structure (heights re-measured, offset kept). Not ported: removals in place, `Replace`. |
|
|
65
65
|
| Templated Row/Grid/Wrap, `Split`, `ItemTemplateSelector`, `ItemTemplatePoolSize`/`ReserveTemplates`, `UsePreparedViews`, `SkiaCachedStack`, measure memo | skipped | |
|
|
66
66
|
| `ItemsSourceWindow` / `WindowSourceThreshold` (built-in window over huge sources) | not needed | The C# window bounds pipeline work (structures, pools, measures) for `MeasureFirst`/`MeasureAll` over big collections. React `MeasureVisible` measures on demand and keeps only visible cells realized, so the pipeline is already O(visible); memory of the array itself belongs to the app (paginate with `LoadMore*`). Revisit only if `MeasureAll` over 100k+ items becomes a requirement. |
|
|
67
67
|
| `MeasureItemsStrategy="MeasureVisible"` + background measurement | ported | Initial pass measures enough items to fill the viewport (≥ `BackgroundMeasurementBatchSize`), content extent = exact prefix + average × remaining (C# MeasureList estimate), visible cells measured on demand and laid out with their real heights, the prefix grows in `requestIdleCallback` slices (time-budgeted, then one `InvalidateMeasure`), `LastMeasuredIndex`, `DebugString` shows `measured n/N`. Not ported: cancellation/`_pendingStructureChanges` interplay, `UsePreparedViews`, windowed sources, anchor correction when an estimate above the viewport changes (content may shift by the delta). |
|
|
@@ -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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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;
|
|
@@ -70,6 +70,16 @@ export declare class SkiaLayout extends SkiaControl {
|
|
|
70
70
|
* anything else rebuilds. Returns false when a full rebuild is needed.
|
|
71
71
|
*/
|
|
72
72
|
private ApplyIncrementalChange;
|
|
73
|
+
/**
|
|
74
|
+
* Same items in another order — a drag-to-reorder, or any code writing a permuted array. DrawnUi handles the
|
|
75
|
+
* matching `ObservableCollection.Move` by keeping the structure and only rebinding the contexts, because its
|
|
76
|
+
* arrange re-flows each row from its bound view (`HandleStructurePreservingMove` / `ApplyMoveChange`, 2026-09-08).
|
|
77
|
+
* The measured heights here are index-keyed and authoritative, so they travel with their items instead: nothing is
|
|
78
|
+
* remeasured, the extent cannot change, and the scroll keeps its offset. Rebuilding instead would reset
|
|
79
|
+
* `MeasureVisible` to "exact for the first screen, estimated after" and throw a scrolled list back towards the top.
|
|
80
|
+
* Returns false when the two arrays are not a permutation of each other, so a real content change still rebuilds.
|
|
81
|
+
*/
|
|
82
|
+
private ApplyReorderChange;
|
|
73
83
|
/** Factory creating one cell (DrawnUi DataTemplate). Cells receive the item as BindingContext. */
|
|
74
84
|
get ItemTemplate(): (() => SkiaControl) | undefined;
|
|
75
85
|
set ItemTemplate(value: (() => SkiaControl) | undefined);
|
|
@@ -87,7 +87,11 @@ export class SkiaLayout extends SkiaControl {
|
|
|
87
87
|
*/
|
|
88
88
|
ApplyIncrementalChange(old, items) {
|
|
89
89
|
const n = items.length, o = old.length;
|
|
90
|
-
if (
|
|
90
|
+
if (o === 0)
|
|
91
|
+
return false;
|
|
92
|
+
if (n === o)
|
|
93
|
+
return this.ApplyReorderChange(old, items);
|
|
94
|
+
if (n < o)
|
|
91
95
|
return false;
|
|
92
96
|
const k = n - o;
|
|
93
97
|
const scale = this.RenderingScale, gap = this.Spacing * scale, w = this.measuredWidthPx;
|
|
@@ -143,6 +147,65 @@ export class SkiaLayout extends SkiaControl {
|
|
|
143
147
|
}
|
|
144
148
|
return false;
|
|
145
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Same items in another order — a drag-to-reorder, or any code writing a permuted array. DrawnUi handles the
|
|
152
|
+
* matching `ObservableCollection.Move` by keeping the structure and only rebinding the contexts, because its
|
|
153
|
+
* arrange re-flows each row from its bound view (`HandleStructurePreservingMove` / `ApplyMoveChange`, 2026-09-08).
|
|
154
|
+
* The measured heights here are index-keyed and authoritative, so they travel with their items instead: nothing is
|
|
155
|
+
* remeasured, the extent cannot change, and the scroll keeps its offset. Rebuilding instead would reset
|
|
156
|
+
* `MeasureVisible` to "exact for the first screen, estimated after" and throw a scrolled list back towards the top.
|
|
157
|
+
* Returns false when the two arrays are not a permutation of each other, so a real content change still rebuilds.
|
|
158
|
+
*/
|
|
159
|
+
ApplyReorderChange(old, items) {
|
|
160
|
+
const n = items.length;
|
|
161
|
+
if (this.MeasureItemsStrategy === "MeasureAll" && this.itemHeights.length !== n)
|
|
162
|
+
return false;
|
|
163
|
+
if (this.MeasureItemsStrategy === "MeasureVisible" && this.mvHeights.length !== n)
|
|
164
|
+
return false;
|
|
165
|
+
const moved = [];
|
|
166
|
+
for (let i = 0; i < n; i++)
|
|
167
|
+
if (old[i] !== items[i])
|
|
168
|
+
moved.push(i);
|
|
169
|
+
if (moved.length === 0) {
|
|
170
|
+
this.ChildrenFactory.UpdateItems(items);
|
|
171
|
+
return true;
|
|
172
|
+
} // same order, new array reference
|
|
173
|
+
// every slot that changed must hold an item that was in one of those slots before, otherwise this is not a reorder
|
|
174
|
+
const free = new Map();
|
|
175
|
+
for (const i of moved) {
|
|
176
|
+
const q = free.get(old[i]);
|
|
177
|
+
if (q)
|
|
178
|
+
q.push(i);
|
|
179
|
+
else
|
|
180
|
+
free.set(old[i], [i]);
|
|
181
|
+
}
|
|
182
|
+
const from = new Int32Array(n);
|
|
183
|
+
for (let i = 0; i < n; i++)
|
|
184
|
+
from[i] = i;
|
|
185
|
+
for (const i of moved) {
|
|
186
|
+
const q = free.get(items[i]);
|
|
187
|
+
if (!q || q.length === 0)
|
|
188
|
+
return false;
|
|
189
|
+
from[i] = q.shift();
|
|
190
|
+
}
|
|
191
|
+
if (this.MeasureItemsStrategy === "MeasureAll") {
|
|
192
|
+
const was = this.itemHeights;
|
|
193
|
+
this.itemHeights = Array.from({ length: n }, (_, i) => was[from[i]]);
|
|
194
|
+
}
|
|
195
|
+
else if (this.MeasureItemsStrategy === "MeasureVisible") {
|
|
196
|
+
const was = this.mvHeights, heights = new Float64Array(n);
|
|
197
|
+
for (let i = 0; i < n; i++)
|
|
198
|
+
heights[i] = was[from[i]];
|
|
199
|
+
this.mvHeights = heights;
|
|
200
|
+
// an item that was never measured can land inside the exact prefix: rebuild it over the leading measured run
|
|
201
|
+
this.mvPrefix = new Float64Array(n + 1);
|
|
202
|
+
this.mvMeasured = 0;
|
|
203
|
+
this.MvExtendPrefix(this.Spacing * this.RenderingScale);
|
|
204
|
+
}
|
|
205
|
+
// MeasureFirst has one height for every row, so a reorder is a pure rebind there (same as the C# note).
|
|
206
|
+
this.ChildrenFactory.UpdateItems(items);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
146
209
|
/** Factory creating one cell (DrawnUi DataTemplate). Cells receive the item as BindingContext. */
|
|
147
210
|
get ItemTemplate() { return this.itemTemplate; }
|
|
148
211
|
set ItemTemplate(value) {
|
package/dist/core/Canvas.d.ts
CHANGED
|
@@ -109,6 +109,15 @@ export declare class Canvas {
|
|
|
109
109
|
OnTouchAction(args: TouchActionEventArgs): void;
|
|
110
110
|
private OnGestureEvent;
|
|
111
111
|
private ProcessPendingGestures;
|
|
112
|
+
/**
|
|
113
|
+
* The control that consumed the current gesture keeps it until it lets go (DrawnUi Canvas.HadInput). Without this a
|
|
114
|
+
* gesture is re-routed by the pointer position on every event, so anything the finger drags away from — a drag
|
|
115
|
+
* handle, a slider thumb, a button being held — stops receiving Panning the moment the pointer leaves its box.
|
|
116
|
+
* Single pointer, which is what the web gives us here; C# keeps a set for multi-touch.
|
|
117
|
+
*/
|
|
118
|
+
private gestureOwner;
|
|
119
|
+
/** DrawnUi IsSavedGesture: the types replayed to the owner instead of being routed again. */
|
|
120
|
+
private static IsSavedGesture;
|
|
112
121
|
/** Entry into the control tree, same shape as DrawnUi Canvas.ProcessGestures. */
|
|
113
122
|
protected ProcessGestures(root: SkiaControl, args: SkiaGesturesParameters): SkiaControl | null;
|
|
114
123
|
}
|
package/dist/core/Canvas.js
CHANGED
|
@@ -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();
|
|
@@ -418,8 +422,35 @@ export class Canvas {
|
|
|
418
422
|
for (const args of batch)
|
|
419
423
|
this.ProcessGestures(root, args);
|
|
420
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* The control that consumed the current gesture keeps it until it lets go (DrawnUi Canvas.HadInput). Without this a
|
|
427
|
+
* gesture is re-routed by the pointer position on every event, so anything the finger drags away from — a drag
|
|
428
|
+
* handle, a slider thumb, a button being held — stops receiving Panning the moment the pointer leaves its box.
|
|
429
|
+
* Single pointer, which is what the web gives us here; C# keeps a set for multi-touch.
|
|
430
|
+
*/
|
|
431
|
+
gestureOwner = null;
|
|
432
|
+
/** DrawnUi IsSavedGesture: the types replayed to the owner instead of being routed again. */
|
|
433
|
+
static IsSavedGesture(type) {
|
|
434
|
+
return type === "Panning" || type === "Wheel" || type === "Up";
|
|
435
|
+
}
|
|
421
436
|
/** Entry into the control tree, same shape as DrawnUi Canvas.ProcessGestures. */
|
|
422
437
|
ProcessGestures(root, args) {
|
|
423
|
-
|
|
438
|
+
const info = () => new GestureEventProcessingInfo(args.Event.Location, SKPoint.Empty, SKPoint.Empty, null);
|
|
439
|
+
if (args.Type === "Down")
|
|
440
|
+
this.gestureOwner = null;
|
|
441
|
+
else if (this.gestureOwner && Canvas.IsSavedGesture(args.Type)) {
|
|
442
|
+
const owner = this.gestureOwner;
|
|
443
|
+
const alive = !!owner.Superview && owner.IsVisible && !owner.InputTransparent;
|
|
444
|
+
const consumed = alive ? owner.OnSkiaGestureEvent(args, info()) : null;
|
|
445
|
+
if (consumed) {
|
|
446
|
+
// it still wants the gesture: nobody else sees this one (C# skips the tree pass for a saved gesture)
|
|
447
|
+
this.gestureOwner = args.Type === "Up" ? null : consumed;
|
|
448
|
+
return consumed;
|
|
449
|
+
}
|
|
450
|
+
this.gestureOwner = null; // it let go (a button whose press turned into a pan): route normally again
|
|
451
|
+
}
|
|
452
|
+
const consumed = root.ProcessGestures(args, info());
|
|
453
|
+
this.gestureOwner = consumed && args.Type !== "Up" ? consumed : null;
|
|
454
|
+
return consumed;
|
|
424
455
|
}
|
|
425
456
|
}
|
package/dist/core/Gestures.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/Gestures.js
CHANGED
|
@@ -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;
|
package/dist/core/SkiaControl.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/core/Super.d.ts
CHANGED
|
@@ -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). */
|
package/dist/core/Super.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
package/dist/index.js
CHANGED
package/dist/react/reconciler.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.0-preview.4",
|
|
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)",
|
|
@@ -65,8 +65,12 @@
|
|
|
65
65
|
"vite": ">=5"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
|
68
|
-
"playwright-core": {
|
|
69
|
-
|
|
68
|
+
"playwright-core": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"vite": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
70
74
|
},
|
|
71
75
|
"dependencies": {
|
|
72
76
|
"canvaskit-wasm": "^0.42.0",
|