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.
- package/README.md +17 -4
- package/SKIPPED.md +33 -27
- package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
- package/dist/controls/AnimatedFramesRenderer.js +16 -1
- package/dist/controls/DrawnGame.d.ts +40 -0
- package/dist/controls/DrawnGame.js +64 -0
- package/dist/controls/GridStructure.d.ts +16 -2
- package/dist/controls/GridStructure.js +40 -4
- package/dist/controls/SkiaBackdrop.d.ts +8 -2
- package/dist/controls/SkiaBackdrop.js +48 -8
- package/dist/controls/SkiaButton.d.ts +19 -2
- package/dist/controls/SkiaButton.js +41 -11
- package/dist/controls/SkiaCarousel.d.ts +9 -1
- package/dist/controls/SkiaCarousel.js +16 -3
- package/dist/controls/SkiaCheckbox.d.ts +0 -1
- package/dist/controls/SkiaCheckbox.js +0 -6
- package/dist/controls/SkiaDrawer.d.ts +2 -0
- package/dist/controls/SkiaDrawer.js +2 -0
- package/dist/controls/SkiaEditor.d.ts +12 -1
- package/dist/controls/SkiaEditor.js +46 -19
- package/dist/controls/SkiaGif.d.ts +3 -0
- package/dist/controls/SkiaGif.js +12 -2
- package/dist/controls/SkiaImage.js +15 -10
- package/dist/controls/SkiaLabel.d.ts +54 -3
- package/dist/controls/SkiaLabel.js +235 -52
- package/dist/controls/SkiaLabelFps.d.ts +16 -0
- package/dist/controls/SkiaLabelFps.js +41 -0
- package/dist/controls/SkiaLayout.d.ts +64 -7
- package/dist/controls/SkiaLayout.js +316 -19
- package/dist/controls/SkiaLottie.js +4 -2
- package/dist/controls/SkiaProgress.d.ts +2 -1
- package/dist/controls/SkiaProgress.js +6 -3
- package/dist/controls/SkiaRadioButton.js +4 -3
- package/dist/controls/SkiaScroll.d.ts +35 -5
- package/dist/controls/SkiaScroll.js +233 -23
- package/dist/controls/SkiaScrollBar.d.ts +27 -0
- package/dist/controls/SkiaScrollBar.js +51 -2
- package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
- package/dist/controls/SkiaShaderCarousel.js +5 -0
- package/dist/controls/SkiaShape.d.ts +13 -2
- package/dist/controls/SkiaShape.js +50 -10
- package/dist/controls/SkiaSlider.d.ts +20 -2
- package/dist/controls/SkiaSlider.js +59 -5
- package/dist/controls/SkiaSprite.d.ts +1 -0
- package/dist/controls/SkiaSprite.js +4 -0
- package/dist/controls/SkiaSvg.js +1 -1
- package/dist/controls/SkiaSwitch.d.ts +0 -1
- package/dist/controls/SkiaSwitch.js +0 -6
- package/dist/controls/SkiaToggle.d.ts +25 -5
- package/dist/controls/SkiaToggle.js +58 -5
- package/dist/controls/SnappingLayout.d.ts +12 -1
- package/dist/controls/SnappingLayout.js +19 -1
- package/dist/core/Accessibility.d.ts +80 -0
- package/dist/core/Accessibility.js +365 -8
- package/dist/core/Animators.d.ts +37 -1
- package/dist/core/Animators.js +102 -9
- package/dist/core/Canvas.d.ts +108 -7
- package/dist/core/Canvas.js +363 -35
- package/dist/core/Easing.d.ts +12 -1
- package/dist/core/Easing.js +25 -1
- package/dist/core/Gestures.d.ts +48 -2
- package/dist/core/Gestures.js +95 -3
- package/dist/core/Hover.d.ts +47 -0
- package/dist/core/Hover.js +134 -0
- package/dist/core/KeyboardManager.d.ts +13 -0
- package/dist/core/KeyboardManager.js +18 -0
- package/dist/core/ScrollAnimators.js +9 -0
- package/dist/core/SkiaControl.d.ts +216 -7
- package/dist/core/SkiaControl.js +516 -59
- package/dist/core/SkiaEffect.d.ts +17 -0
- package/dist/core/SkiaEffect.js +43 -8
- package/dist/core/SkiaImageManager.js +1 -1
- package/dist/core/Styles.d.ts +51 -0
- package/dist/core/Styles.js +92 -0
- package/dist/core/Super.d.ts +17 -1
- package/dist/core/Super.js +35 -4
- package/dist/core/ViewsAdapter.d.ts +7 -2
- package/dist/core/ViewsAdapter.js +10 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/react/SkiaShell.js +33 -14
- package/dist/react/index.d.ts +15 -4
- package/dist/react/index.js +102 -15
- package/dist/react/reconciler.js +34 -12
- package/dist/vite/index.d.ts +9 -0
- package/dist/vite/index.js +36 -3
- package/package.json +33 -3
package/dist/react/SkiaShell.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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: [
|
|
472
|
-
|
|
473
|
-
|
|
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" })] })] }, `${
|
|
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 (
|
|
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
|
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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";
|
package/dist/react/index.js
CHANGED
|
@@ -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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
114
|
-
*
|
|
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
|
-
|
|
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
|
|
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) => {
|
|
160
|
-
e.
|
|
161
|
-
|
|
162
|
-
|
|
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...).
|
package/dist/react/reconciler.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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; },
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -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"...).
|
package/dist/vite/index.js
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
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.
|
|
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": {
|
|
69
|
-
|
|
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",
|