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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +17 -4
  2. package/SKIPPED.md +33 -27
  3. package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
  4. package/dist/controls/AnimatedFramesRenderer.js +16 -1
  5. package/dist/controls/DrawnGame.d.ts +40 -0
  6. package/dist/controls/DrawnGame.js +64 -0
  7. package/dist/controls/GridStructure.d.ts +16 -2
  8. package/dist/controls/GridStructure.js +40 -4
  9. package/dist/controls/SkiaBackdrop.d.ts +8 -2
  10. package/dist/controls/SkiaBackdrop.js +48 -8
  11. package/dist/controls/SkiaButton.d.ts +19 -2
  12. package/dist/controls/SkiaButton.js +41 -11
  13. package/dist/controls/SkiaCarousel.d.ts +9 -1
  14. package/dist/controls/SkiaCarousel.js +16 -3
  15. package/dist/controls/SkiaCheckbox.d.ts +0 -1
  16. package/dist/controls/SkiaCheckbox.js +0 -6
  17. package/dist/controls/SkiaDrawer.d.ts +2 -0
  18. package/dist/controls/SkiaDrawer.js +2 -0
  19. package/dist/controls/SkiaEditor.d.ts +12 -1
  20. package/dist/controls/SkiaEditor.js +46 -19
  21. package/dist/controls/SkiaGif.d.ts +3 -0
  22. package/dist/controls/SkiaGif.js +12 -2
  23. package/dist/controls/SkiaImage.js +15 -10
  24. package/dist/controls/SkiaLabel.d.ts +54 -3
  25. package/dist/controls/SkiaLabel.js +235 -52
  26. package/dist/controls/SkiaLabelFps.d.ts +16 -0
  27. package/dist/controls/SkiaLabelFps.js +41 -0
  28. package/dist/controls/SkiaLayout.d.ts +64 -7
  29. package/dist/controls/SkiaLayout.js +316 -19
  30. package/dist/controls/SkiaLottie.js +4 -2
  31. package/dist/controls/SkiaProgress.d.ts +2 -1
  32. package/dist/controls/SkiaProgress.js +6 -3
  33. package/dist/controls/SkiaRadioButton.js +4 -3
  34. package/dist/controls/SkiaScroll.d.ts +35 -5
  35. package/dist/controls/SkiaScroll.js +233 -23
  36. package/dist/controls/SkiaScrollBar.d.ts +27 -0
  37. package/dist/controls/SkiaScrollBar.js +51 -2
  38. package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
  39. package/dist/controls/SkiaShaderCarousel.js +5 -0
  40. package/dist/controls/SkiaShape.d.ts +13 -2
  41. package/dist/controls/SkiaShape.js +50 -10
  42. package/dist/controls/SkiaSlider.d.ts +20 -2
  43. package/dist/controls/SkiaSlider.js +59 -5
  44. package/dist/controls/SkiaSprite.d.ts +1 -0
  45. package/dist/controls/SkiaSprite.js +4 -0
  46. package/dist/controls/SkiaSvg.js +1 -1
  47. package/dist/controls/SkiaSwitch.d.ts +0 -1
  48. package/dist/controls/SkiaSwitch.js +0 -6
  49. package/dist/controls/SkiaToggle.d.ts +25 -5
  50. package/dist/controls/SkiaToggle.js +58 -5
  51. package/dist/controls/SnappingLayout.d.ts +12 -1
  52. package/dist/controls/SnappingLayout.js +19 -1
  53. package/dist/core/Accessibility.d.ts +80 -0
  54. package/dist/core/Accessibility.js +365 -8
  55. package/dist/core/Animators.d.ts +37 -1
  56. package/dist/core/Animators.js +102 -9
  57. package/dist/core/Canvas.d.ts +108 -7
  58. package/dist/core/Canvas.js +363 -35
  59. package/dist/core/Easing.d.ts +12 -1
  60. package/dist/core/Easing.js +25 -1
  61. package/dist/core/Gestures.d.ts +48 -2
  62. package/dist/core/Gestures.js +95 -3
  63. package/dist/core/Hover.d.ts +47 -0
  64. package/dist/core/Hover.js +134 -0
  65. package/dist/core/KeyboardManager.d.ts +13 -0
  66. package/dist/core/KeyboardManager.js +18 -0
  67. package/dist/core/ScrollAnimators.js +9 -0
  68. package/dist/core/SkiaControl.d.ts +216 -7
  69. package/dist/core/SkiaControl.js +516 -59
  70. package/dist/core/SkiaEffect.d.ts +17 -0
  71. package/dist/core/SkiaEffect.js +43 -8
  72. package/dist/core/SkiaImageManager.js +1 -1
  73. package/dist/core/Styles.d.ts +51 -0
  74. package/dist/core/Styles.js +92 -0
  75. package/dist/core/Super.d.ts +17 -1
  76. package/dist/core/Super.js +35 -4
  77. package/dist/core/ViewsAdapter.d.ts +7 -2
  78. package/dist/core/ViewsAdapter.js +10 -3
  79. package/dist/index.d.ts +4 -0
  80. package/dist/index.js +4 -0
  81. package/dist/react/SkiaShell.js +33 -14
  82. package/dist/react/index.d.ts +15 -4
  83. package/dist/react/index.js +102 -15
  84. package/dist/react/reconciler.js +34 -12
  85. package/dist/vite/index.d.ts +9 -0
  86. package/dist/vite/index.js +36 -3
  87. package/package.json +33 -3
@@ -389,6 +389,12 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
389
389
  prev ? prev.TranslateToAsync(-dir * w, 0, TabsAnimationSpeed, TabsEasing) : Promise.resolve(),
390
390
  ]), TabsAnimationSpeed + 500);
391
391
  }
392
+ // the tab left stays mounted (its state kept): hidden first, then back in place for its next turn
393
+ if (prev) {
394
+ prev.IsVisible = false;
395
+ prev.TranslationX = 0;
396
+ prev.TranslationY = 0;
397
+ }
392
398
  setTabTransition(null);
393
399
  }, [hasTabs, Tabs, AnimateTabs, TabsAnimationSpeed]);
394
400
  // popstate: the browser popped OUR last entry (back) — or moved forward to a hash we can rebuild
@@ -405,7 +411,13 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
405
411
  }
406
412
  const onPop = () => {
407
413
  const depth = window.history.state?.[HISTORY_KEY] ?? 0;
408
- if (depth < history.current.length) {
414
+ const target = parseHash();
415
+ // a Back lands on one of OUR entries: its hash is the stack it was pushed with (the first N pages, N = the page
416
+ // entries below it). A typed hash or a plain <a href="#/route"> makes a new entry with no state (depth 0):
417
+ // that is a navigation to the hash, not a Back to the root
418
+ const pagesBelow = history.current.slice(0, depth).filter((k) => k === "page").length;
419
+ const back = depth < history.current.length && target.join("/") === live.current.stack.slice(0, pagesBelow).join("/");
420
+ if (back) {
409
421
  // back: unwind our entries down to depth
410
422
  void (async () => {
411
423
  while (history.current.length > depth) {
@@ -441,9 +453,8 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
441
453
  })();
442
454
  }
443
455
  else {
444
- // forward (depth grew), or the same depth with another hash = a plain <a href="#/route"> link or a typed hash:
456
+ // forward (depth grew), or another hash = a plain <a href="#/route"> link or a typed hash:
445
457
  // the hash names the pages, rebuild the stack without animation (overlays cannot be restored)
446
- const target = parseHash();
447
458
  if (depth === history.current.length && target.join("/") === live.current.stack.join("/"))
448
459
  return;
449
460
  history.current = target.map(() => "page");
@@ -466,28 +477,36 @@ export const SkiaShell = forwardRef(function SkiaShell({ Routes, children, NavBa
466
477
  const transitioning = transitions > 0 || leaving !== null;
467
478
  const rootBottom = hasTabs ? TabBarHeight + insets.Bottom : 0;
468
479
  const navTop = NavBarHeight + insets.Top;
469
- const rootNode = hasTabs ? RenderRoute(Tabs[selectedTab]?.route ?? "") : children;
480
+ // every tab keeps its root and its pages mounted (state, scroll positions), only the selected one shows (C# tab
481
+ // views, DrawnUi.Rust "tabs keep their stacks")
482
+ const tabIndexes = hasTabs ? Tabs.map((_, i) => i) : [0];
470
483
  const entering = tabTransition ? { TranslationX: tabTransition.dir * 0.75 * (typeof window !== "undefined" ? window.innerWidth : 0), Opacity: 0.001, ZIndex: 1 } : {};
471
- return (_jsx(ShellContext.Provider, { value: nav, children: _jsxs(SkiaLayer, { VerticalOptions: "Fill", children: [tabTransition && (_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, 0, 0, rootBottom), ZIndex: 0, BackgroundColor: PageBackgroundColor, ref: (c) => { if (c)
472
- tabRoots.current.set(tabTransition.from, c); }, children: RenderRoute(Tabs[tabTransition.from]?.route ?? "") }, `tab-leaving-${tabTransition.from}`)), _jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, 0, 0, rootBottom), IsVisible: visiblePages.length === 0 || transitioning, BackgroundColor: hasTabs ? PageBackgroundColor : undefined, ...entering, ref: (c) => { if (c)
473
- tabRoots.current.set(selectedTab, c); }, children: rootNode }, hasTabs ? `tab-${selectedTab}` : "root"), visiblePages.map((r, i) => (_jsxs(PageHost, { route: r, speed: PagesAnimationSpeed, visible: r === topRoute || transitioning, background: PageBackgroundColor, bottom: rootBottom, register: (c) => { if (c)
484
+ return (_jsx(ShellContext.Provider, { value: nav, children: _jsxs(SkiaLayer, { VerticalOptions: "Fill", children: [tabIndexes.map((t) => {
485
+ const selected = t === selectedTab;
486
+ return (_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, 0, 0, rootBottom), BackgroundColor: hasTabs ? PageBackgroundColor : undefined, IsVisible: selected ? visiblePages.length === 0 || transitioning : tabTransition?.from === t, ZIndex: 0, ...(selected ? entering : {}), ref: (c) => { if (c)
487
+ tabRoots.current.set(t, c); }, children: hasTabs ? RenderRoute(Tabs[t]?.route ?? "") : children }, hasTabs ? `tab-${t}` : "root"));
488
+ }), tabIndexes.flatMap((t) => (t === selectedTab ? visiblePages : stacks[t] ?? []).map((r, i) => (_jsxs(PageHost, { route: r, speed: PagesAnimationSpeed, active: t === selectedTab, visible: t === selectedTab && (r === topRoute || transitioning), background: PageBackgroundColor, bottom: rootBottom, register: (c) => { if (c)
474
489
  pageCtrls.current.set(r, c);
475
490
  else
476
- pageCtrls.current.delete(r); }, children: [_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, navTop, 0, 0), children: RenderRoute(r) }), _jsxs(SkiaLayer, { HeightRequest: navTop, BackgroundColor: NavBarColor, Padding: new Thickness(0, insets.Top, 0, 0), children: [_jsx(SkiaButton, { Text: "\u2039 Back", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, VerticalOptions: "Center", Margin: new Thickness(8, 0), ApplyEffect: "Ripple", Tapped: () => void GoBackAsync(), AccessibilityRole: "button", AccessibilityLabel: "Back" }), _jsx(SkiaLabel, { Text: Titles?.[RouteName(r)] ?? RouteName(r), FontSize: 18, FontFamily: "FontTextBold", TextColor: Colors.White, HorizontalOptions: "Fill", HorizontalTextAlignment: "Center", VerticalOptions: "Center", MaxLines: 1, Margin: new Thickness(96, 0), AccessibilityRole: "heading" }), _jsx(SkiaButton, { Text: "Home", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, HorizontalOptions: "End", VerticalOptions: "Center", Margin: new Thickness(0, 0, 8, 0), ApplyEffect: "Ripple", Tapped: () => void PopToRootAsync(), AccessibilityRole: "button", AccessibilityLabel: "Home" }), _jsx(SkiaLayer, { HeightRequest: 1, VerticalOptions: "End", BackgroundColor: "#343A40" })] })] }, `${selectedTab}:${r}#${i}`))), hasTabs && (_jsxs(SkiaLayer, { HeightRequest: TabBarHeight + insets.Bottom, VerticalOptions: "End", BackgroundColor: TabBarColor, ZIndex: ShellDefaults.ZIndexModals - 1, BlockGesturesBelow: true, Padding: new Thickness(0, 0, 0, insets.Bottom), children: [_jsx(SkiaLayer, { HeightRequest: 1, BackgroundColor: "#343A40" }), _jsx(SkiaGrid, { ColumnDefinitions: Tabs.map(() => "*").join(","), HorizontalOptions: "Fill", VerticalOptions: "Fill", children: Tabs.map((t, i) => (_jsxs(SkiaLayer, { Column: i, HorizontalOptions: "Fill", VerticalOptions: "Fill", AnimationTapped: "Ripple", Tapped: () => void SelectTabAsync(i), AccessibilityRole: "button", AccessibilityLabel: t.title, children: [_jsx(SkiaLabel, { Text: t.title, FontSize: 13, FontFamily: i === selectedTab ? "FontTextBold" : undefined, TextColor: i === selectedTab ? ShellDefaults.TabSelectedColor : ShellDefaults.TabColor, HorizontalOptions: "Center", VerticalOptions: "Center", AccessibilityRole: "presentation" }), i === selectedTab && _jsx(SkiaShape, { Type: "Rectangle", HeightRequest: 3, WidthRequest: 36, CornerRadius: 2, BackgroundColor: ShellDefaults.TabSelectedColor, HorizontalOptions: "Center", VerticalOptions: "End" })] }, t.route))) })] })), modals.map((m) => m.node), popups.map((p) => p.node), toasts.map((t) => t.node)] }) }));
491
+ pageCtrls.current.delete(r); }, children: [_jsx(SkiaLayer, { VerticalOptions: "Fill", Margin: new Thickness(0, navTop, 0, 0), children: RenderRoute(r) }), _jsxs(SkiaLayer, { HeightRequest: navTop, BackgroundColor: NavBarColor, Padding: new Thickness(0, insets.Top, 0, 0), children: [_jsx(SkiaButton, { Text: "\u2039 Back", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, VerticalOptions: "Center", Margin: new Thickness(8, 0), ApplyEffect: "Ripple", Tapped: () => void GoBackAsync(), AccessibilityRole: "button", AccessibilityLabel: "Back" }), _jsx(SkiaLabel, { Text: Titles?.[RouteName(r)] ?? RouteName(r), FontSize: 18, FontFamily: "FontTextBold", TextColor: Colors.White, HorizontalOptions: "Fill", HorizontalTextAlignment: "Center", VerticalOptions: "Center", MaxLines: 1, Margin: new Thickness(96, 0), AccessibilityRole: "heading" }), _jsx(SkiaButton, { Text: "Home", BackgroundColor: "#00000000", TextColor: "#6EA8FE", FontSize: 16, HorizontalOptions: "End", VerticalOptions: "Center", Margin: new Thickness(0, 0, 8, 0), ApplyEffect: "Ripple", Tapped: () => void PopToRootAsync(), AccessibilityRole: "button", AccessibilityLabel: "Home" }), _jsx(SkiaLayer, { HeightRequest: 1, VerticalOptions: "End", BackgroundColor: "#343A40" })] })] }, `${t}:${r}#${i}`)))), hasTabs && (_jsxs(SkiaLayer, { HeightRequest: TabBarHeight + insets.Bottom, VerticalOptions: "End", BackgroundColor: TabBarColor, ZIndex: ShellDefaults.ZIndexModals - 1, BlockGesturesBelow: true, Padding: new Thickness(0, 0, 0, insets.Bottom), children: [_jsx(SkiaLayer, { HeightRequest: 1, BackgroundColor: "#343A40" }), _jsx(SkiaGrid, { ColumnDefinitions: Tabs.map(() => "*").join(","), HorizontalOptions: "Fill", VerticalOptions: "Fill", children: Tabs.map((t, i) => (_jsxs(SkiaLayer, { Column: i, HorizontalOptions: "Fill", VerticalOptions: "Fill", AnimationTapped: "Ripple", Tapped: () => void SelectTabAsync(i), AccessibilityRole: "button", AccessibilityLabel: t.title, children: [_jsx(SkiaLabel, { Text: t.title, FontSize: 13, FontFamily: i === selectedTab ? "FontTextBold" : undefined, TextColor: i === selectedTab ? ShellDefaults.TabSelectedColor : ShellDefaults.TabColor, HorizontalOptions: "Center", VerticalOptions: "Center", AccessibilityRole: "presentation" }), i === selectedTab && _jsx(SkiaShape, { Type: "Rectangle", HeightRequest: 3, WidthRequest: 36, CornerRadius: 2, BackgroundColor: ShellDefaults.TabSelectedColor, HorizontalOptions: "Center", VerticalOptions: "End" })] }, t.route))) })] })), modals.map((m) => m.node), popups.map((p) => p.node), toasts.map((t) => t.node)] }) }));
477
492
  });
478
493
  /** A pushed page: slides in from the right on mount (C# SkiaViewSwitcher PushView), covers what is below. */
479
- function PageHost({ route, speed, visible, background, bottom, register, children }) {
494
+ function PageHost({ route, speed, active, visible, background, bottom, register, children }) {
480
495
  const ctrl = useRef(null);
496
+ // slides in once, when pushed; a page of another tab stays mounted and is registered only while its tab is selected
481
497
  useEffect(() => {
482
498
  const c = ctrl.current;
483
- if (!c)
484
- return;
485
- register(c);
486
- if (speed > 0) {
499
+ if (c && speed > 0) {
487
500
  c.TranslationX = CanvasWidthPts(c);
488
501
  void c.TranslateToAsync(0, 0, speed);
489
502
  }
490
- return () => register(null);
491
503
  }, [route]); // eslint-disable-line react-hooks/exhaustive-deps
504
+ useEffect(() => {
505
+ const c = ctrl.current;
506
+ if (!c || !active)
507
+ return;
508
+ register(c);
509
+ return () => register(null);
510
+ }, [route, active]); // eslint-disable-line react-hooks/exhaustive-deps
492
511
  return (_jsx(SkiaLayer, { ref: (c) => { ctrl.current = c; }, VerticalOptions: "Fill", HorizontalOptions: "Fill", Margin: new Thickness(0, 0, 0, bottom), BackgroundColor: background, IsVisible: visible, BlockGesturesBelow: true, children: children }));
493
512
  }
@@ -4,6 +4,7 @@ import type { SkiaControl } from "../core/SkiaControl";
4
4
  import type { SkiaLabel as SkiaLabelCtrl } from "../controls/SkiaLabel";
5
5
  import type { TextSpan as TextSpanCtrl } from "../controls/TextSpan";
6
6
  import type { SkiaRichLabel as SkiaRichLabelCtrl } from "../controls/SkiaRichLabel";
7
+ import type { SkiaLabelFps as SkiaLabelFpsCtrl } from "../controls/SkiaLabelFps";
7
8
  import type { SkiaSwitch as SkiaSwitchCtrl } from "../controls/SkiaSwitch";
8
9
  import type { SkiaCheckbox as SkiaCheckboxCtrl } from "../controls/SkiaCheckbox";
9
10
  import type { SkiaRadioButton as SkiaRadioButtonCtrl } from "../controls/SkiaRadioButton";
@@ -22,7 +23,7 @@ import type { SkiaScrollBar as SkiaScrollBarCtrl } from "../controls/SkiaScrollB
22
23
  import type { RefreshIndicator as RefreshIndicatorCtrl } from "../controls/RefreshIndicator";
23
24
  import type { SkiaSvg as SkiaSvgCtrl } from "../controls/SkiaSvg";
24
25
  import type { SkiaBackdrop as SkiaBackdropCtrl } from "../controls/SkiaBackdrop";
25
- import type { SkiaEditor as SkiaEditorCtrl } from "../controls/SkiaEditor";
26
+ import { SkiaEditor as SkiaEditorCtrl } from "../controls/SkiaEditor";
26
27
  import type { SkiaSprite as SkiaSpriteCtrl } from "../controls/SkiaSprite";
27
28
  import type { SkiaSpriteSet as SkiaSpriteSetCtrl } from "../controls/SkiaSpriteSet";
28
29
  import type { SkiaLottie as SkiaLottieCtrl } from "../controls/SkiaLottie";
@@ -41,7 +42,7 @@ import type { GesturesMode } from "../core/Gestures";
41
42
  * and never appeared in completion.
42
43
  */
43
44
  type PropsOf<T> = Partial<{
44
- [K in keyof T as T[K] extends Function ? (undefined extends T[K] ? K : never) : K extends "Children" | "Views" | "Parent" | "Spans" | "GridStructure" | "AccessibilityId" | "IsAccessibilityElement" | "HasTransform" | "RenderTransformMatrix" | "RenderObjectPrevious" | "LastMeasuredIndex" | "ItemsInsertedAtStart" | "UsingControlStyle" | "Track" | "Thumb" | "FrameOn" | "FrameOff" | "ViewCheckOn" | "ViewOn" | "ViewText" | "Ratio" | "StartThumbX" | "EndThumbX" | "SnapPoints" | "CurrentPosition" | "CurrentSnap" | "ContentOffsetBounds" | "InTransition" | "CanAnimate" | "MaxIndex" | "ChildrenTotal" | "IsAtStart" | "IsAtEnd" | "ScrollProgress" | "ScrollAmount" | "TransitionProgress" | "LastIndex" | "ChildrenCount" | "Horizontal" | "Animator" | "Animation" | "IsPlaying" | "PlayWhenAvailable" | "TotalFrames" | "HasEffects" | "IsDisposed" | "Label" | "IsMultiline" | "HasSelection" | "LayoutVersion" | "LinesCount" | "MeasuredLineHeight" | "Focused" | "SpriteSheet" | "FrameWidth" | "FrameHeight" | "DurationMs" | "FrameDurationMs" | "CurrentSprite" | "Rects" | "HasTapHandler" | "HasDecorations" | "LinesCount" | "Superview" | "DrawingRect" | "MeasuredSize" | "RenderingScale" | "NeedMeasure" | "_superview" | "HitBoxAuto" | "TotalDown" | "TotalTapped" | "TouchDown" | "PostAnimators" | "LoadedSource" | "IsLoading" | "DisplayRect" | "AspectScale" | "Content" | "ContentSize" | "ContentOffsetBounds" | "OverscrollDistance" | "OverScrolled" | "IsUserPanning" | "IsUserFocused" | "IsScrolling" | "IsTemplated" | "FirstVisibleIndex" | "LastVisibleIndex" | "DebugString" | "ChildrenFactory" | "ContextIndex" | "RenderObject" | "UsingCacheType" | "TransitionEffect" | "TransitionFromIndex" | "TransitionToIndex" | "EffectPostRenderers" | "CachedImage" | "ImageBitmap" | "ShouldSubmitOnEnter" | "Header" | "Footer" | "RefreshIndicator" | "ScrollBar" | "ScrollBarHorizontal" | "IsRunning" | "VisibleRatio" | "CurrentIndex" | "LastCompositeRecord" | "DirtyChildrenInternal" | "IsRenderingWithComposition" ? never : K]: T[K];
45
+ [K in keyof T as T[K] extends Function ? (undefined extends T[K] ? K : never) : K extends "Children" | "Views" | "Parent" | "Spans" | "GridStructure" | "AccessibilityId" | "IsAccessibilityElement" | "HasTransform" | "RenderTransformMatrix" | "RenderObjectPrevious" | "LastMeasuredIndex" | "ItemsInsertedAtStart" | "UsingControlStyle" | "Track" | "Thumb" | "FrameOn" | "FrameOff" | "ViewCheckOn" | "ViewOn" | "ViewText" | "Ratio" | "StartThumbX" | "EndThumbX" | "SnapPoints" | "CurrentPosition" | "CurrentSnap" | "ContentOffsetBounds" | "InTransition" | "CanAnimate" | "MaxIndex" | "ChildrenTotal" | "IsAtStart" | "IsAtEnd" | "ScrollProgress" | "ScrollAmount" | "TransitionProgress" | "LastIndex" | "ChildrenCount" | "Horizontal" | "Animator" | "Animation" | "IsPlaying" | "PlayWhenAvailable" | "TotalFrames" | "HasEffects" | "IsDisposed" | "Label" | "IsMultiline" | "HasSelection" | "LayoutVersion" | "LinesCount" | "MeasuredLineHeight" | "Focused" | "SpriteSheet" | "FrameWidth" | "FrameHeight" | "DurationMs" | "FrameDurationMs" | "CurrentSprite" | "Rects" | "HasTapHandler" | "HasDecorations" | "LinesCount" | "Superview" | "DrawingRect" | "MeasuredSize" | "RenderingScale" | "NeedMeasure" | "_superview" | "HitBoxAuto" | "TotalDown" | "TotalTapped" | "TouchDown" | "PostAnimators" | "LoadedSource" | "IsLoading" | "DisplayRect" | "AspectScale" | "Content" | "ContentSize" | "ContentOffsetBounds" | "OverscrollDistance" | "OverScrolled" | "IsUserPanning" | "IsUserFocused" | "IsScrolling" | "IsTemplated" | "FirstVisibleIndex" | "LastVisibleIndex" | "DebugString" | "ChildrenFactory" | "ContextIndex" | "RenderObject" | "UsingCacheType" | "TransitionEffect" | "TransitionFromIndex" | "TransitionToIndex" | "EffectPostRenderers" | "CachedImage" | "ImageBitmap" | "ShouldSubmitOnEnter" | "Header" | "Footer" | "RefreshIndicator" | "ScrollBar" | "ScrollBarHorizontal" | "IsRunning" | "VisibleRatio" | "CurrentIndex" | "LastCompositeRecord" | "DirtyChildrenInternal" | "IsRenderingWithComposition" | "IsHovered" ? never : K]: T[K];
45
46
  }>;
46
47
  /** `ref` receives the engine control instance (react-reconciler getPublicInstance). */
47
48
  type LeafProps<T> = PropsOf<T> & {
@@ -61,6 +62,8 @@ export declare const SkiaGrid: FC<LayoutProps<SkiaLayoutCtrl>>;
61
62
  /** Grid drawing gradient separator lines in its spacing (C# SkiaDecoratedGrid). */
62
63
  export declare const SkiaDecoratedGrid: FC<LayoutProps<SkiaDecoratedGridCtrl>>;
63
64
  export declare const SkiaLabel: FC<LayoutProps<SkiaLabelCtrl>>;
65
+ /** Canvas FPS counter (C# SkiaLabelFps); its value changes only on frames running animators request anyway. */
66
+ export declare const SkiaLabelFps: FC<LeafProps<SkiaLabelFpsCtrl>>;
64
67
  /** Markdown label (C# SkiaRichLabel): Text is markdown, rendered as spans; LinkTapped for [text](url). */
65
68
  export declare const SkiaRichLabel: FC<LeafProps<SkiaRichLabelCtrl>>;
66
69
  /** Child of <SkiaLabel>: a styled fragment (C# TextSpan). */
@@ -100,7 +103,11 @@ export declare const SkiaDrawer: FC<LayoutProps<SkiaDrawerCtrl>>;
100
103
  export interface CanvasProps {
101
104
  BackgroundColor?: Color;
102
105
  RenderingMode?: RenderingModeType;
103
- /** Disabled (default) / Enabled / Lock, like DrawnUi Canvas.Gestures. */
106
+ /**
107
+ * Disabled (default) / Enabled / Lock, like DrawnUi Canvas.Gestures. Enabled shares input with the page like MAUI's
108
+ * Enabled inside a native scroll view: a touch pan along an axis the page can scroll scrolls the page, and the wheel
109
+ * scrolls it unless a control used the wheel. A page that cannot scroll keeps every touch. Lock keeps all input.
110
+ */
104
111
  Gestures?: GesturesMode;
105
112
  children?: ReactNode;
106
113
  style?: CSSProperties;
@@ -109,12 +116,16 @@ export interface CanvasProps {
109
116
  ref?: Ref<CanvasView>;
110
117
  /** Context-menu request (right click / long press / Menu key) no control handled; return true to suppress the browser menu. */
111
118
  ContextMenu?: CanvasView["ContextMenu"];
119
+ /** DrawnView.WillFirstTimeDraw: once, right before the first frame is drawn, with its drawing context. */
120
+ WillFirstTimeDraw?: CanvasView["WillFirstTimeDraw"];
121
+ /** DrawnView.WasDrawn: after every drawn frame. */
122
+ WasDrawn?: CanvasView["WasDrawn"];
112
123
  }
113
124
  /**
114
125
  * Mirrors DrawnUi Canvas: the bridge between the DOM (react-dom) and the drawn tree (DrawnUi reconciler).
115
126
  * Requires Super.UseDrawnUi()...BuildAsync() to have completed.
116
127
  */
117
- export declare function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef, ContextMenu }: CanvasProps): import("react").JSX.Element;
128
+ export declare function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef, ContextMenu, WillFirstTimeDraw, WasDrawn }: CanvasProps): import("react").JSX.Element;
118
129
  export type { SkiaControl };
119
130
  export * from "../index";
120
131
  export * from "./SkiaShell";
@@ -1,6 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
3
+ import { SkiaAccessibilityManager } from "../core/Accessibility";
4
+ import { TextInputProxy } from "../core/TextInputProxy";
3
5
  import { Canvas as CanvasView } from "../core/Canvas";
6
+ import { SkiaEditor as SkiaEditorCtrl } from "../controls/SkiaEditor";
4
7
  import { SkiaScroll as SkiaScrollCtrl } from "../controls/SkiaScroll";
5
8
  import { createDrawnRoot } from "./reconciler";
6
9
  /** Typed JSX tags resolved by the reconciler Registry. */
@@ -13,6 +16,8 @@ export const SkiaGrid = "SkiaGrid";
13
16
  /** Grid drawing gradient separator lines in its spacing (C# SkiaDecoratedGrid). */
14
17
  export const SkiaDecoratedGrid = "SkiaDecoratedGrid";
15
18
  export const SkiaLabel = "SkiaLabel";
19
+ /** Canvas FPS counter (C# SkiaLabelFps); its value changes only on frames running animators request anyway. */
20
+ export const SkiaLabelFps = "SkiaLabelFps";
16
21
  /** Markdown label (C# SkiaRichLabel): Text is markdown, rendered as spans; LinkTapped for [text](url). */
17
22
  export const SkiaRichLabel = "SkiaRichLabel";
18
23
  /** Child of <SkiaLabel>: a styled fragment (C# TextSpan). */
@@ -66,15 +71,19 @@ function removeStaticContent() {
66
71
  * Mirrors DrawnUi Canvas: the bridge between the DOM (react-dom) and the drawn tree (DrawnUi reconciler).
67
72
  * Requires Super.UseDrawnUi()...BuildAsync() to have completed.
68
73
  */
69
- export function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef, ContextMenu }) {
74
+ export function Canvas({ BackgroundColor, RenderingMode, Gestures, children, style, className, ref: viewRef, ContextMenu, WillFirstTimeDraw, WasDrawn }) {
70
75
  const ref = useRef(null);
71
76
  const view = useRef(null);
72
77
  const root = useRef(null);
73
78
  const [engine, setEngine] = useState(null);
74
79
  useLayoutEffect(() => {
75
- const v = new CanvasView(ref.current);
76
- if (RenderingMode)
77
- v.RenderingMode = RenderingMode;
80
+ // the engine draws frame 1 inside its constructor: the draw events and the rendering mode must be on it before that
81
+ const v = new CanvasView(ref.current, (c) => {
82
+ if (RenderingMode)
83
+ c.RenderingMode = RenderingMode;
84
+ c.WillFirstTimeDraw = WillFirstTimeDraw;
85
+ c.WasDrawn = WasDrawn;
86
+ });
78
87
  view.current = v;
79
88
  root.current = createDrawnRoot(v);
80
89
  setEngine(v);
@@ -93,6 +102,8 @@ export function Canvas({ BackgroundColor, RenderingMode, Gestures, children, sty
93
102
  }
94
103
  v.Gestures = Gestures ?? "Disabled";
95
104
  v.ContextMenu = ContextMenu;
105
+ v.WillFirstTimeDraw = WillFirstTimeDraw;
106
+ v.WasDrawn = WasDrawn;
96
107
  root.current.render(children);
97
108
  });
98
109
  // The drawn surface is aria-hidden; the overlay is the accessibility tree (DrawnUi.Blazor pattern).
@@ -108,19 +119,74 @@ const A11Y_CSS = `
108
119
  .drawnui-a11y-text>span{position:absolute;white-space:pre;color:transparent;user-select:text;-webkit-user-select:text;pointer-events:auto;cursor:text;line-height:1;font-kerning:none;font-feature-settings:"kern" 0,"liga" 0,"calt" 0;text-rendering:geometricPrecision}
109
120
  .drawnui-a11y-text>span::selection{background:rgba(110,168,254,.55);color:transparent}
110
121
  `;
122
+ const NAVIGATION_KEYS = new Set(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"]);
111
123
  /**
112
- * Invisible ARIA elements mirroring the accessible drawn controls, positioned over the canvas.
113
- * Improvement over DrawnUi.Blazor: `pointer-events:none`, so hover and every pointer gesture still reach the canvas —
114
- * keyboard (Tab / Enter / Space) and screen-reader activation arrive as DOM events and are routed back as a Tapped.
124
+ * Invisible ARIA elements mirroring the accessible drawn controls, positioned over the canvas (the DrawnUi.Blazor
125
+ * overlay). Improvement over DrawnUi.Blazor: `pointer-events:none`, so hover and every pointer gesture still reach the
126
+ * canvas. Keyboard and screen-reader input arrive as DOM events: Enter / Space activate (a Tapped), the navigation keys
127
+ * go to the focused control and then to its arrow-key group; a group is one Tab stop (roving tabindex).
115
128
  */
129
+ /** aria-valuenow / min / max / valuetext of a range control (drawnui-cross 6c); a slider reads as horizontal. */
130
+ function valueAttributes(n) {
131
+ const v = n.Value;
132
+ if (!v)
133
+ return undefined;
134
+ return { "aria-valuenow": v.Now, "aria-valuemin": v.Min, "aria-valuemax": v.Max, "aria-valuetext": v.Text || undefined, "aria-orientation": n.Role === "slider" ? "horizontal" : undefined };
135
+ }
116
136
  function AccessibilityOverlay({ view }) {
117
137
  const [nodes, setNodes] = useState(() => view.AccessibilityManager.Snapshot);
138
+ // keyboard focus moved by the manager (arrow keys in a group): focus that node's element once it is rendered
139
+ const [focusRequest, setFocusRequest] = useState();
140
+ // the Tab stop of a group moves with keyboard focus: re-render on focus
141
+ const [, setFocusTick] = useState(0);
118
142
  useEffect(() => {
119
143
  const mgr = view.AccessibilityManager;
120
144
  setNodes(mgr.Snapshot);
121
145
  const off = mgr.OnChanged(() => setNodes(mgr.Snapshot));
122
146
  const offLive = mgr.OnLiveRegionUpdated(() => { mgr.ForceRebuildOnNextFrame(); view.Update(); });
123
- return () => { off(); offLive(); };
147
+ const offFocus = mgr.OnKeyboardFocusRequested((node) => { setNodes(mgr.Snapshot); setFocusRequest({ id: node.AccessibilityId }); });
148
+ return () => { off(); offLive(); offFocus(); };
149
+ }, [view]);
150
+ const elementOf = (id) => overlayRef.current?.querySelector(`[data-a11y-id="${id}"]`) ?? undefined;
151
+ useLayoutEffect(() => {
152
+ if (!focusRequest)
153
+ return;
154
+ setFocusRequest(undefined);
155
+ elementOf(focusRequest.id)?.focus({ preventScroll: true });
156
+ }, [focusRequest]);
157
+ // the focused node went away (its page or popup closed): the focus goes to the first Tab stop on screen in reading
158
+ // order instead of falling back to the page (drawnui-cross 6c refocus); a focus the user moved away stays away
159
+ const focusedId = useRef(0);
160
+ useLayoutEffect(() => {
161
+ const id = focusedId.current;
162
+ if (!id || nodes.some((n) => n.Id === id))
163
+ return;
164
+ focusedId.current = 0;
165
+ const active = document.activeElement;
166
+ if (active && active !== document.body)
167
+ return;
168
+ const box = overlayRef.current?.getBoundingClientRect();
169
+ const first = nodes.find((n) => n.CanInteract && view.AccessibilityManager.IsTabStop(n.Source)
170
+ && (!box || (n.Rect.Bottom > 0 && n.Rect.Top < box.height && n.Rect.Right > 0 && n.Rect.Left < box.width)));
171
+ if (first)
172
+ elementOf(first.Id)?.focus({ preventScroll: true });
173
+ }, [nodes]);
174
+ // Tab / Shift+Tab in a text field leave it for the next node, also after a click into it: DOM focus goes to the
175
+ // field's own element (without taking the caret again) and the browser's Tab moves on from there
176
+ const tabbingOut = useRef(0);
177
+ useEffect(() => {
178
+ const onKey = (e) => {
179
+ const editor = SkiaEditorCtrl.Focused;
180
+ if (e.key !== "Tab" || !editor || editor.Superview !== view || !TextInputProxy.IsProxyTarget(e.target))
181
+ return;
182
+ const el = elementOf(editor.AccessibilityId);
183
+ if (!el)
184
+ return;
185
+ tabbingOut.current = editor.AccessibilityId;
186
+ el.focus({ preventScroll: true });
187
+ };
188
+ document.addEventListener("keydown", onKey, true);
189
+ return () => document.removeEventListener("keydown", onKey, true);
124
190
  }, [view]);
125
191
  // selectable text spans take pointer events: the wheel over them is re-dispatched to the canvas so drawn scrolls keep working
126
192
  const overlayRef = useRef(null);
@@ -155,15 +221,36 @@ function AccessibilityOverlay({ view }) {
155
221
  const pin = (e) => { e.currentTarget.scrollTop = 0; e.currentTarget.scrollLeft = 0; };
156
222
  return (_jsxs("div", { className: "drawnui-a11y-overlay", onScroll: pin, ref: overlayRef, children: [_jsx("style", { children: A11Y_CSS }), nodes.map((n) => {
157
223
  const pos = { left: n.Rect.Left, top: n.Rect.Top, width: n.Rect.Width, height: n.Rect.Height };
158
- const activate = () => n.Source.OnAccessibilityActivated();
159
- return n.CanInteract ? (_jsx("div", { role: n.Role, "aria-label": n.Label, title: n.Hint, "aria-pressed": n.Role === "button" ? n.IsPressed : undefined, "aria-checked": n.Role === "switch" || n.Role === "checkbox" || n.Role === "radio" ? n.IsPressed : undefined, "aria-live": n.Live, tabIndex: 0, className: "drawnui-a11y-node", style: { ...pos, fontSize: 0, userSelect: "none" }, onClick: activate, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") {
160
- e.preventDefault();
161
- activate();
162
- } }, onFocus: (e) => { pin({ currentTarget: e.currentTarget.parentElement }); SkiaScrollCtrl.EnsureVisible(n.Source); n.Source.OnAccessibilityFocused(true); n.Source.NotifyAccessibilityFocused(true); }, onBlur: () => { n.Source.OnAccessibilityFocused(false); n.Source.NotifyAccessibilityFocused(false); }, children: n.Label }, n.Id)) : n.TextLines ? (
224
+ const activate = () => SkiaAccessibilityManager.Activate(n.Source);
225
+ return n.CanInteract ? (_jsx("div", { "data-a11y-id": n.Id, role: n.Role, "aria-label": n.Label, title: n.Hint, ...valueAttributes(n), "aria-pressed": n.Role === "button" ? n.IsPressed : undefined, "aria-checked": n.Role === "switch" || n.Role === "checkbox" || n.Role === "radio" ? n.IsPressed : undefined, "aria-live": n.Live, tabIndex: view.AccessibilityManager.IsTabStop(n.Source) ? 0 : -1, className: "drawnui-a11y-node", style: { ...pos, fontSize: 0, userSelect: "none" }, onClick: activate, onKeyDown: (e) => {
226
+ if (e.key === "Enter" || e.key === " ") {
227
+ e.preventDefault();
228
+ activate();
229
+ }
230
+ else if (NAVIGATION_KEYS.has(e.key) && SkiaAccessibilityManager.Key(n.Source, e.key))
231
+ e.preventDefault();
232
+ }, onFocus: (e) => {
233
+ focusedId.current = n.Id;
234
+ pin({ currentTarget: e.currentTarget.parentElement });
235
+ SkiaScrollCtrl.EnsureVisible(n.Source); // inside every enclosing scroll
236
+ if (tabbingOut.current === n.Id)
237
+ tabbingOut.current = 0; // leaving a text field: it keeps no caret
238
+ else
239
+ n.Source.OnAccessibilityFocused(true); // a text field takes the caret, as after a click
240
+ n.Source.NotifyAccessibilityFocused(true);
241
+ setFocusTick((t) => t + 1);
242
+ }, onBlur: (e) => {
243
+ if (e.currentTarget.isConnected && focusedId.current === n.Id)
244
+ focusedId.current = 0; // moved away on purpose
245
+ if (TextInputProxy.IsProxyTarget(e.relatedTarget))
246
+ return; // a text field moved DOM focus to its own input
247
+ n.Source.OnAccessibilityFocused(false);
248
+ n.Source.NotifyAccessibilityFocused(false);
249
+ }, children: n.Label }, n.Id)) : n.TextLines ? (
163
250
  // AccessibilityTextSelectable: one positioned span per drawn line, in the drawn font, so the browser selects and copies it like HTML
164
- _jsx("div", { role: n.Role, title: n.Hint, "aria-live": n.Live, className: "drawnui-a11y-node drawnui-a11y-text", style: pos, children: n.TextLines.map((l, i) => (_jsxs("span", { "data-w": l.Width, style: { left: l.Left, top: l.Top, height: l.Height, fontFamily: l.FontFamily, fontWeight: l.FontWeight, fontSize: l.FontSize, lineHeight: `${l.Height}px` }, children: [l.Text, i < n.TextLines.length - 1 ? "\n" : ""] }, i))) }, n.Id)) : (
251
+ _jsx("div", { role: n.Role, title: n.Hint, "aria-disabled": n.Disabled || undefined, "aria-live": n.Live, className: "drawnui-a11y-node drawnui-a11y-text", style: pos, children: n.TextLines.map((l, i) => (_jsxs("span", { "data-w": l.Width, style: { left: l.Left, top: l.Top, height: l.Height, fontFamily: l.FontFamily, fontWeight: l.FontWeight, fontSize: l.FontSize, lineHeight: `${l.Height}px` }, children: [l.Text, i < n.TextLines.length - 1 ? "\n" : ""] }, i))) }, n.Id)) : (
165
252
  // static text is exposed as real (transparent) text content; aria-label only for roles that need a name
166
- _jsx("div", { role: n.Role, "aria-label": n.Role === "text" ? undefined : n.Label, title: n.Hint, "aria-live": n.Live, className: "drawnui-a11y-node", style: pos, children: n.Label }, n.Id));
253
+ _jsx("div", { role: n.Role, "aria-label": n.Role === "text" ? undefined : n.Label, title: n.Hint, ...valueAttributes(n), "aria-disabled": n.Disabled || undefined, "aria-live": n.Live, className: "drawnui-a11y-node", style: pos, children: n.Label }, n.Id));
167
254
  })] }));
168
255
  }
169
256
  // One import for apps: React tags + every engine type (Colors, Thickness, Super, gestures, animators...).
@@ -1,9 +1,11 @@
1
1
  import { createContext } from "react";
2
2
  import Reconciler from "react-reconciler";
3
3
  import { ConcurrentRoot, DefaultEventPriority } from "react-reconciler/constants";
4
+ import { SkiaControl } from "../core/SkiaControl";
4
5
  import { SkiaLabel } from "../controls/SkiaLabel";
5
6
  import { TextSpan } from "../controls/TextSpan";
6
7
  import { SkiaRichLabel } from "../controls/SkiaRichLabel";
8
+ import { SkiaLabelFps } from "../controls/SkiaLabelFps";
7
9
  import { SkiaSwitch } from "../controls/SkiaSwitch";
8
10
  import { SkiaCheckbox } from "../controls/SkiaCheckbox";
9
11
  import { SkiaRadioButton } from "../controls/SkiaRadioButton";
@@ -31,18 +33,20 @@ import { SkiaScroll } from "../controls/SkiaScroll";
31
33
  import { SkiaFrame, SkiaShape } from "../controls/SkiaShape";
32
34
  /** JSX tag name -> engine class. Add a control here to expose it to React. */
33
35
  export const Registry = {
34
- SkiaLayout, SkiaStack, SkiaRow, SkiaLayer, SkiaWrap, SkiaGrid, SkiaDecoratedGrid, SkiaBackdrop, SkiaEditor, SkiaSprite, SkiaSpriteSet, SkiaLabel, SkiaRichLabel, TextSpan, SkiaHotspot, SkiaButton, SkiaImage, SkiaImageTiles, SkiaSvg, SkiaLottie, SkiaGif, SkiaScroll, SkiaScrollBar, RefreshIndicator, SkiaShape, SkiaFrame,
36
+ SkiaLayout, SkiaStack, SkiaRow, SkiaLayer, SkiaWrap, SkiaGrid, SkiaDecoratedGrid, SkiaBackdrop, SkiaEditor, SkiaSprite, SkiaSpriteSet, SkiaLabel, SkiaLabelFps, SkiaRichLabel, TextSpan, SkiaHotspot, SkiaButton, SkiaImage, SkiaImageTiles, SkiaSvg, SkiaLottie, SkiaGif, SkiaScroll, SkiaScrollBar, RefreshIndicator, SkiaShape, SkiaFrame,
35
37
  SkiaSwitch, SkiaCheckbox, SkiaRadioButton, SkiaProgress, SkiaSlider, SkiaCarousel, SkiaShaderCarousel, SkiaDrawer,
36
38
  };
37
39
  const SKIP = new Set(["children", "key", "ref"]);
38
40
  /** Props that change how the subtree is composited, not what it contains: repaint, keep caches (DrawnUi RedrawCanvas). */
39
- const REPAINT_ONLY = new Set(["TranslationX", "TranslationY", "Rotation", "ScaleX", "ScaleY", "Scale", "SkewX", "SkewY", "AnchorX", "AnchorY", "Opacity"]);
41
+ const REPAINT_ONLY = new Set(["TranslationX", "TranslationY", "TranslationZ", "Rotation", "RotationX", "RotationY", "RotationZ", "ScaleX", "ScaleY", "Scale", "SkewX", "SkewY", "AnchorX", "AnchorY", "Opacity"]);
42
+ /** Props that change only how a control looks, never its size (C# NeedDraw): an ImageComposite above redraws only its area. */
43
+ const isDrawOnly = (k) => k.endsWith("Color") || k === "FillGradient" || k === "StrokeGradient";
40
44
  /**
41
45
  * Assigns changed props straight onto the control (same names as the C# properties).
42
46
  * Handler props (functions) are swapped without invalidating: inline arrows change identity every render.
43
47
  */
44
48
  function applyProps(inst, prev, next) {
45
- let changed = false, repaint = false;
49
+ let changed = false, repaint = false, layout = false;
46
50
  for (const k in next) {
47
51
  if (SKIP.has(k) || (prev && prev[k] === next[k]))
48
52
  continue;
@@ -51,23 +55,36 @@ function applyProps(inst, prev, next) {
51
55
  continue;
52
56
  if (REPAINT_ONLY.has(k))
53
57
  repaint = true;
54
- else
58
+ else {
55
59
  changed = true;
60
+ if (!isDrawOnly(k))
61
+ layout = true;
62
+ }
56
63
  }
57
64
  if (prev)
58
65
  for (const k in prev)
59
66
  if (!SKIP.has(k) && !(k in next)) {
60
67
  inst[k] = new inst.constructor()[k];
61
- changed = true;
68
+ changed = layout = true;
62
69
  }
63
- if (changed && prev)
64
- inst.Update();
70
+ if (changed && prev) {
71
+ if (layout || !(inst instanceof SkiaControl))
72
+ inst.Update();
73
+ else
74
+ inst.UpdateDraw();
75
+ }
65
76
  else if (repaint && prev)
66
77
  inst.RepaintComposition?.();
67
78
  }
68
79
  let currentUpdatePriority = DefaultEventPriority;
69
80
  const noop = () => { };
70
81
  const hostContext = {};
82
+ function appendChild(parent, child) {
83
+ if (parent instanceof SkiaLayout)
84
+ return parent.InsertChild(child);
85
+ child.IsChildrenItem = true;
86
+ parent.AddSubView(child);
87
+ }
71
88
  // Record<string, unknown> absorbs members that react-reconciler 0.33 reads but @types 0.32 does not declare yet.
72
89
  const hostConfig = {
73
90
  supportsMutation: true,
@@ -85,18 +102,23 @@ const hostConfig = {
85
102
  if (!ctor)
86
103
  throw new Error(`DrawnUi: unknown control <${type}>`);
87
104
  const inst = new ctor();
105
+ // ConfigureStyles defaults first, the JSX props below override them (a TextSpan is not a control, it has none)
106
+ inst.ApplyInitialStyles?.(true);
88
107
  applyProps(inst, null, props);
89
108
  return inst;
90
109
  },
91
110
  createTextInstance(text) {
92
111
  throw new Error(`DrawnUi: raw text "${text}" is not allowed, use <SkiaLabel Text="..." />`);
93
112
  },
94
- appendInitialChild: (parent, child) => parent.AddSubView(child),
95
- appendChild: (parent, child) => parent.AddSubView(child),
113
+ // JSX children are the app's children (C# IsChildrenItem): a layout keeps them in JSX order inside the slots they
114
+ // occupy, so its own parts keep their place; an existing child passed again is a move
115
+ appendInitialChild: (parent, child) => appendChild(parent, child),
116
+ appendChild: (parent, child) => appendChild(parent, child),
96
117
  insertBefore: (parent, child, before) => {
97
- const index = parent instanceof SkiaLayout ? parent.Views.indexOf(before)
98
- : parent instanceof SkiaLabel ? parent.Spans.indexOf(before) : 0;
99
- parent.InsertSubView(index, child);
118
+ if (parent instanceof SkiaLayout)
119
+ return parent.InsertChild(child, before);
120
+ child.IsChildrenItem = true;
121
+ parent.InsertSubView(parent instanceof SkiaLabel ? parent.Spans.indexOf(before) : 0, child);
100
122
  },
101
123
  removeChild: (parent, child) => parent.RemoveSubView(child),
102
124
  appendChildToContainer: (canvas, child) => { canvas.Content = child; },
@@ -1,4 +1,13 @@
1
1
  import type { Plugin } from "vite";
2
+ /**
3
+ * Versioned addresses for the files of the public folder (fonts, images, sprites, shaders, Lottie, favicon). Their
4
+ * names never change, and a browser or CDN may keep them for hours, so after a publish a returning visitor could
5
+ * get new code with an old file. On build every public file gets a short content hash: index.html receives the map
6
+ * as `globalThis.DrawnUiAssetStamps` (read by `Super.ResolveAssetUrl`, which every engine loader fetches through)
7
+ * and its own `href` / `src` links to public files get `?v=<hash>`. A changed file gets a new address, an unchanged
8
+ * one keeps its address and stays cached. Bundled files (hashed names) need nothing. The dev server is left alone.
9
+ */
10
+ export declare function drawnUiAssetStamps(): Plugin;
2
11
  export interface DrawnUiStaticOptions {
3
12
  /**
4
13
  * Pages to generate, as URL hashes / paths appended to the site root ("" = the root page, "#/shapes"...).
@@ -1,9 +1,42 @@
1
1
  /**
2
- * Vite helpers for DrawnUi.React apps: `import { drawnUiStatic } from "drawnui-react/vite"`.
2
+ * Vite helpers for DrawnUi.React apps: `import { drawnUiStatic, drawnUiAssetStamps } from "drawnui-react/vite"`.
3
3
  * Build-time only; the runtime bundle never imports this module and the frame loop knows nothing about it.
4
4
  */
5
- import { readFileSync, writeFileSync } from "node:fs";
6
- import { resolve } from "node:path";
5
+ import { createHash } from "node:crypto";
6
+ import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
7
+ import { join, resolve } from "node:path";
8
+ /**
9
+ * Versioned addresses for the files of the public folder (fonts, images, sprites, shaders, Lottie, favicon). Their
10
+ * names never change, and a browser or CDN may keep them for hours, so after a publish a returning visitor could
11
+ * get new code with an old file. On build every public file gets a short content hash: index.html receives the map
12
+ * as `globalThis.DrawnUiAssetStamps` (read by `Super.ResolveAssetUrl`, which every engine loader fetches through)
13
+ * and its own `href` / `src` links to public files get `?v=<hash>`. A changed file gets a new address, an unchanged
14
+ * one keeps its address and stays cached. Bundled files (hashed names) need nothing. The dev server is left alone.
15
+ */
16
+ export function drawnUiAssetStamps() {
17
+ let stamps = {};
18
+ return {
19
+ name: "drawnui-asset-stamps",
20
+ apply: "build",
21
+ configResolved(config) {
22
+ const dir = config.publicDir;
23
+ if (!dir)
24
+ return;
25
+ for (const rel of readdirSync(dir, { recursive: true })) {
26
+ const file = join(dir, rel);
27
+ if (statSync(file).isFile())
28
+ stamps[rel.replace(/\\/g, "/")] = createHash("sha256").update(readFileSync(file)).digest("hex").slice(0, 10);
29
+ }
30
+ },
31
+ transformIndexHtml(html) {
32
+ const stamped = html.replace(/\b(href|src)="([^"?#:]+)"/g, (m, attr, url) => {
33
+ const v = stamps[url.replace(/^\.?\//, "")];
34
+ return v ? `${attr}="${url}?v=${v}"` : m;
35
+ });
36
+ return { html: stamped, tags: [{ tag: "script", children: `globalThis.DrawnUiAssetStamps=${JSON.stringify(stamps)}`, injectTo: "head-prepend" }] };
37
+ },
38
+ };
39
+ }
7
40
  /**
8
41
  * Generates visible, semantic HTML for crawlers and AI agents from the app's own accessibility tree, at build time.
9
42
  *
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.21",
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)",
@@ -53,6 +53,32 @@
53
53
  "typecheck": "tsc --noEmit",
54
54
  "build": "tsc --noEmit && node dev/build-samples.mjs",
55
55
  "sync:skills": "node dev/sync-skills.mjs",
56
+ "check:recycling": "node dev/check.mjs dev/check-recycling.ts",
57
+ "check:keyboard": "node dev/check.mjs dev/check-keyboard.ts",
58
+ "check:wheel": "node dev/check.mjs dev/check-wheel.ts",
59
+ "check:children": "node dev/check.mjs dev/check-children.tsx",
60
+ "check:composite": "node dev/check.mjs dev/check-composite.tsx",
61
+ "check:hover": "node dev/check.mjs dev/check-hover.ts",
62
+ "check:grid-label": "node dev/check.mjs dev/check-grid-label.ts",
63
+ "check:snap": "node dev/check.mjs dev/check-snap.ts",
64
+ "check:game": "node dev/check.mjs dev/check-game.ts",
65
+ "check:animation": "node dev/check.mjs dev/check-animation.ts",
66
+ "check:shape": "node dev/check.mjs dev/check-shape.ts",
67
+ "check:label": "node dev/check.mjs dev/check-label.ts",
68
+ "check:image": "node dev/check.mjs dev/check-image.ts",
69
+ "check:longpress": "node dev/check.mjs dev/check-longpress.ts",
70
+ "check:hidden": "node dev/check.mjs dev/check-hidden.ts",
71
+ "check:shell": "node dev/check.mjs dev/check-shell.tsx",
72
+ "check:wheel-routing": "node dev/check.mjs dev/check-wheel-routing.ts",
73
+ "check:velocity": "node dev/check.mjs dev/check-velocity.ts",
74
+ "check:scroll-index": "node dev/check.mjs dev/check-scroll-index.ts",
75
+ "check:culling": "node dev/check.mjs dev/check-culling.ts",
76
+ "check:responds": "node dev/check.mjs dev/check-responds.ts",
77
+ "check:3d": "node dev/check.mjs dev/check-3d.ts",
78
+ "check:gesture-owner": "node dev/check.mjs dev/check-gesture-owner.ts",
79
+ "check:shader-cache": "node dev/check.mjs dev/check-shader-cache.ts",
80
+ "check:shader-content": "node dev/check.mjs dev/check-shader-content.ts",
81
+ "check:backdrop": "node dev/check.mjs dev/check-backdrop.ts",
56
82
  "site:extras": "node dev/site-extras.mjs dist/demo",
57
83
  "build:lib": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
58
84
  "prepublishOnly": "npm run build:lib",
@@ -65,8 +91,12 @@
65
91
  "vite": ">=5"
66
92
  },
67
93
  "peerDependenciesMeta": {
68
- "playwright-core": { "optional": true },
69
- "vite": { "optional": true }
94
+ "playwright-core": {
95
+ "optional": true
96
+ },
97
+ "vite": {
98
+ "optional": true
99
+ }
70
100
  },
71
101
  "dependencies": {
72
102
  "canvaskit-wasm": "^0.42.0",