drawnui-react 0.1.0-preview.1

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/PARITY.md +287 -0
  3. package/README.md +92 -0
  4. package/SKIPPED.md +107 -0
  5. package/dist/controls/AnimatedFramesRenderer.d.ts +58 -0
  6. package/dist/controls/AnimatedFramesRenderer.js +133 -0
  7. package/dist/controls/GridStructure.d.ts +79 -0
  8. package/dist/controls/GridStructure.js +403 -0
  9. package/dist/controls/RefreshIndicator.d.ts +35 -0
  10. package/dist/controls/RefreshIndicator.js +82 -0
  11. package/dist/controls/SkiaBackdrop.d.ts +25 -0
  12. package/dist/controls/SkiaBackdrop.js +80 -0
  13. package/dist/controls/SkiaButton.d.ts +58 -0
  14. package/dist/controls/SkiaButton.js +148 -0
  15. package/dist/controls/SkiaCarousel.d.ts +133 -0
  16. package/dist/controls/SkiaCarousel.js +603 -0
  17. package/dist/controls/SkiaCheckbox.d.ts +21 -0
  18. package/dist/controls/SkiaCheckbox.js +118 -0
  19. package/dist/controls/SkiaDecoratedGrid.d.ts +15 -0
  20. package/dist/controls/SkiaDecoratedGrid.js +46 -0
  21. package/dist/controls/SkiaDrawer.d.ts +46 -0
  22. package/dist/controls/SkiaDrawer.js +212 -0
  23. package/dist/controls/SkiaDynamicDrawnCell.d.ts +11 -0
  24. package/dist/controls/SkiaDynamicDrawnCell.js +17 -0
  25. package/dist/controls/SkiaEditor.d.ts +163 -0
  26. package/dist/controls/SkiaEditor.js +803 -0
  27. package/dist/controls/SkiaGif.d.ts +54 -0
  28. package/dist/controls/SkiaGif.js +206 -0
  29. package/dist/controls/SkiaHotspot.d.ts +13 -0
  30. package/dist/controls/SkiaHotspot.js +36 -0
  31. package/dist/controls/SkiaImage.d.ts +75 -0
  32. package/dist/controls/SkiaImage.js +222 -0
  33. package/dist/controls/SkiaImageTiles.d.ts +27 -0
  34. package/dist/controls/SkiaImageTiles.js +76 -0
  35. package/dist/controls/SkiaLabel.d.ts +160 -0
  36. package/dist/controls/SkiaLabel.js +580 -0
  37. package/dist/controls/SkiaLayout.d.ts +163 -0
  38. package/dist/controls/SkiaLayout.js +723 -0
  39. package/dist/controls/SkiaLottie.d.ts +75 -0
  40. package/dist/controls/SkiaLottie.js +290 -0
  41. package/dist/controls/SkiaProgress.d.ts +40 -0
  42. package/dist/controls/SkiaProgress.js +80 -0
  43. package/dist/controls/SkiaRadioButton.d.ts +32 -0
  44. package/dist/controls/SkiaRadioButton.js +157 -0
  45. package/dist/controls/SkiaRichLabel.d.ts +77 -0
  46. package/dist/controls/SkiaRichLabel.js +274 -0
  47. package/dist/controls/SkiaScroll.d.ts +231 -0
  48. package/dist/controls/SkiaScroll.js +958 -0
  49. package/dist/controls/SkiaScrollBar.d.ts +56 -0
  50. package/dist/controls/SkiaScrollBar.js +158 -0
  51. package/dist/controls/SkiaShaderCarousel.d.ts +67 -0
  52. package/dist/controls/SkiaShaderCarousel.js +246 -0
  53. package/dist/controls/SkiaShape.d.ts +63 -0
  54. package/dist/controls/SkiaShape.js +317 -0
  55. package/dist/controls/SkiaSlider.d.ts +74 -0
  56. package/dist/controls/SkiaSlider.js +243 -0
  57. package/dist/controls/SkiaSprite.d.ts +99 -0
  58. package/dist/controls/SkiaSprite.js +324 -0
  59. package/dist/controls/SkiaSpriteSet.d.ts +27 -0
  60. package/dist/controls/SkiaSpriteSet.js +75 -0
  61. package/dist/controls/SkiaSvg.d.ts +43 -0
  62. package/dist/controls/SkiaSvg.js +181 -0
  63. package/dist/controls/SkiaSwitch.d.ts +26 -0
  64. package/dist/controls/SkiaSwitch.js +147 -0
  65. package/dist/controls/SkiaToggle.d.ts +50 -0
  66. package/dist/controls/SkiaToggle.js +71 -0
  67. package/dist/controls/SnappingLayout.d.ts +78 -0
  68. package/dist/controls/SnappingLayout.js +217 -0
  69. package/dist/controls/TextSpan.d.ts +70 -0
  70. package/dist/controls/TextSpan.js +76 -0
  71. package/dist/core/Accessibility.d.ts +104 -0
  72. package/dist/core/Accessibility.js +150 -0
  73. package/dist/core/Animators.d.ts +82 -0
  74. package/dist/core/Animators.js +247 -0
  75. package/dist/core/Canvas.d.ts +89 -0
  76. package/dist/core/Canvas.js +328 -0
  77. package/dist/core/ControlStyle.d.ts +6 -0
  78. package/dist/core/ControlStyle.js +11 -0
  79. package/dist/core/Easing.d.ts +13 -0
  80. package/dist/core/Easing.js +15 -0
  81. package/dist/core/Gestures.d.ts +74 -0
  82. package/dist/core/Gestures.js +104 -0
  83. package/dist/core/ImageEffects.d.ts +29 -0
  84. package/dist/core/ImageEffects.js +55 -0
  85. package/dist/core/KeyboardManager.d.ts +25 -0
  86. package/dist/core/KeyboardManager.js +54 -0
  87. package/dist/core/ScrollAnimators.d.ts +90 -0
  88. package/dist/core/ScrollAnimators.js +244 -0
  89. package/dist/core/SkiaControl.d.ts +401 -0
  90. package/dist/core/SkiaControl.js +1239 -0
  91. package/dist/core/SkiaEffect.d.ts +216 -0
  92. package/dist/core/SkiaEffect.js +571 -0
  93. package/dist/core/SkiaImageManager.d.ts +43 -0
  94. package/dist/core/SkiaImageManager.js +122 -0
  95. package/dist/core/Super.d.ts +61 -0
  96. package/dist/core/Super.js +178 -0
  97. package/dist/core/TextInputProxy.d.ts +31 -0
  98. package/dist/core/TextInputProxy.js +144 -0
  99. package/dist/core/Types.d.ts +177 -0
  100. package/dist/core/Types.js +122 -0
  101. package/dist/core/ViewsAdapter.d.ts +41 -0
  102. package/dist/core/ViewsAdapter.js +113 -0
  103. package/dist/index.d.ts +48 -0
  104. package/dist/index.js +48 -0
  105. package/dist/react/SkiaShell.d.ts +144 -0
  106. package/dist/react/SkiaShell.js +490 -0
  107. package/dist/react/index.d.ts +110 -0
  108. package/dist/react/index.js +158 -0
  109. package/dist/react/reconciler.d.ts +14 -0
  110. package/dist/react/reconciler.js +160 -0
  111. package/package.json +60 -0
@@ -0,0 +1,122 @@
1
+ import { Super } from "./Super";
2
+ const PRIORITY_RANK = { High: 0, Normal: 1, Low: 2 };
3
+ /**
4
+ * Mirrors DrawnUi SkiaImageManager: loads + decodes image sources once and shares the decoded CanvasKit Image
5
+ * between all SkiaImage instances using the same source (`ReuseBitmaps`). `LoadImageManagedAsync` goes through the
6
+ * priority queue (High > Normal > Low, at most `MaxParallelLoads` fetches in flight, C# semaphore of 5; the same
7
+ * source requested twice is fetched once), `LoadImageAsync` loads directly. `PreloadImages` warms the cache,
8
+ * `CancelAll` drops what is still queued. Sources are URLs (absolute, or relative to the page like "images/photo.jpg").
9
+ */
10
+ export class SkiaImageManager {
11
+ static Instance = new SkiaImageManager();
12
+ /** Keep decoded images for reuse across controls (DrawnUi ReuseBitmaps). */
13
+ static ReuseBitmaps = true;
14
+ /** Fetches in flight at once through the managed queue (C# CreateSemaphoreForLocalFiles: 5). */
15
+ static MaxParallelLoads = 5;
16
+ /** C# CanReload: raised when a queued load failed so the caller may retry later. */
17
+ CanReload;
18
+ cache = new Map();
19
+ pending = new Map();
20
+ queue = [];
21
+ waiting = new Map();
22
+ running = 0;
23
+ /** Queue diagnostics. */
24
+ get QueuedCount() { return this.queue.length; }
25
+ get RunningCount() { return this.running; }
26
+ /** Cached decoded image, if loaded already (C# GetFromCache). */
27
+ GetFromCache(source) { return this.cache.get(source); }
28
+ /** Direct load without the queue (C# LoadImageAsync); cached and de-duplicated. Rejects on network/decode failure. */
29
+ LoadImageAsync(source, signal) {
30
+ const cached = this.cache.get(source);
31
+ if (cached)
32
+ return Promise.resolve(cached);
33
+ let inflight = this.pending.get(source);
34
+ if (inflight)
35
+ return inflight;
36
+ inflight = this.Fetch(source, signal);
37
+ this.pending.set(source, inflight);
38
+ inflight.finally(() => this.pending.delete(source)).catch(() => undefined);
39
+ return inflight;
40
+ }
41
+ async Fetch(source, signal) {
42
+ const response = await fetch(source, signal ? { signal } : undefined);
43
+ if (!response.ok)
44
+ throw new Error(`DrawnUi: ${response.status} loading image '${source}'`);
45
+ const image = Super.CK.MakeImageFromEncoded(await response.arrayBuffer());
46
+ if (!image)
47
+ throw new Error(`DrawnUi: cannot decode image '${source}'`);
48
+ if (SkiaImageManager.ReuseBitmaps)
49
+ this.cache.set(source, image);
50
+ return image;
51
+ }
52
+ /** Queued load (C# LoadImageManagedAsync): cached images resolve at once, the rest wait their turn by priority. */
53
+ LoadImageManagedAsync(source, signal, priority = "Normal") {
54
+ const cached = this.cache.get(source);
55
+ if (cached)
56
+ return Promise.resolve(cached);
57
+ const inflight = this.pending.get(source);
58
+ if (inflight)
59
+ return inflight;
60
+ return new Promise((resolve, reject) => {
61
+ const item = { source, priority, signal, resolve, reject };
62
+ const same = this.waiting.get(source);
63
+ if (same) {
64
+ same.push(item);
65
+ return;
66
+ } // already queued: one fetch serves every requester
67
+ this.waiting.set(source, [item]);
68
+ const rank = PRIORITY_RANK[priority];
69
+ let at = this.queue.length;
70
+ while (at > 0 && PRIORITY_RANK[this.queue[at - 1].priority] > rank)
71
+ at--;
72
+ this.queue.splice(at, 0, item);
73
+ this.Pump();
74
+ });
75
+ }
76
+ Pump() {
77
+ while (this.running < SkiaImageManager.MaxParallelLoads && this.queue.length > 0) {
78
+ const item = this.queue.shift();
79
+ const requesters = this.waiting.get(item.source) ?? [item];
80
+ this.waiting.delete(item.source);
81
+ if (item.signal?.aborted) {
82
+ for (const r of requesters)
83
+ r.reject(new Error("DrawnUi: image load cancelled"));
84
+ continue;
85
+ }
86
+ this.running++;
87
+ const task = this.LoadImageAsync(item.source, item.signal);
88
+ task.then((image) => { for (const r of requesters)
89
+ r.resolve(image); }, (error) => { this.CanReload?.(item.source, error); for (const r of requesters)
90
+ r.reject(error); }).finally(() => { this.running--; this.Pump(); });
91
+ }
92
+ }
93
+ /** C# PreloadImage: warms the cache for one source; failures are ignored. */
94
+ async PreloadImage(source, priority = "Normal") {
95
+ await this.LoadImageManagedAsync(source, undefined, priority).catch(() => undefined);
96
+ }
97
+ /** Warms the cache so first paint of these images is instant (DrawnUi PreloadImages). Failures are ignored. */
98
+ async PreloadImages(sources, priority = "Normal") {
99
+ await Promise.all(sources.map((s) => this.PreloadImage(s, priority)));
100
+ }
101
+ /** Drops everything still waiting in the queue (C# CancelAll); loads already in flight finish. */
102
+ CancelAll() {
103
+ const items = this.queue.splice(0);
104
+ for (const item of items) {
105
+ const requesters = this.waiting.get(item.source) ?? [item];
106
+ this.waiting.delete(item.source);
107
+ for (const r of requesters)
108
+ r.reject(new Error("DrawnUi: image load cancelled"));
109
+ }
110
+ }
111
+ /** Drops a cached image (or everything) — decoded images hold GPU/WASM memory. */
112
+ Clear(source) {
113
+ if (source) {
114
+ this.cache.get(source)?.delete();
115
+ this.cache.delete(source);
116
+ return;
117
+ }
118
+ for (const img of this.cache.values())
119
+ img.delete();
120
+ this.cache.clear();
121
+ }
122
+ }
@@ -0,0 +1,61 @@
1
+ import { Thickness } from "./Types";
2
+ import type { CanvasKit, Font, Typeface } from "canvaskit-wasm";
3
+ /** Mirrors DrawnUi.Net IFontCollection: fonts.AddFont(source, alias[, weight]), plus the Blazor-head AddSymbols/AddEmojis. */
4
+ export declare class FontCollection {
5
+ /** Where the shipped subset fonts are served from (the demo copies them to public/fonts). */
6
+ static ContentRoot: string;
7
+ readonly Fonts: {
8
+ Source: string;
9
+ Alias: string;
10
+ Weight: number;
11
+ }[];
12
+ /** Noto Sans Math + Symbols 2 subsets as "FontSymbols" / "FontSymbols2" (arrows, math, misc symbols) — DrawnUi.Blazor AddSymbols. */
13
+ AddSymbols(): FontCollection;
14
+ /** Noto Color Emoji subset (faces + hands) as "FontEmoji" — DrawnUi.Blazor AddEmojis. */
15
+ AddEmojis(): FontCollection;
16
+ /** Registers a face under an alias; weight 100..900 lets FontWeight / FontAttributes=Bold pick the right file. */
17
+ AddFont(source: string, alias: string, weight?: number): FontCollection;
18
+ }
19
+ /** Mirrors DrawnUi.Net DrawnUiBuilder: Super.UseDrawnUi().ConfigureFonts(...).BuildAsync(). */
20
+ export declare class DrawnUiBuilder {
21
+ private readonly fonts;
22
+ ConfigureFonts(configure: (fonts: FontCollection) => void): DrawnUiBuilder;
23
+ /** Loads CanvasKit + registered fonts. Must complete before the first Canvas is created. */
24
+ BuildAsync(): Promise<void>;
25
+ }
26
+ /** Mirrors DrawnUi static Super: global engine state. */
27
+ export declare class Super {
28
+ private static insets?;
29
+ private static insetsProbe?;
30
+ private static readonly insetsChanged;
31
+ /** Safe-area insets in CSS px (points), measured once and on resize; zero outside a browser. */
32
+ static get Insets(): Thickness;
33
+ /** Subscribe to inset changes (orientation / resize); returns the unsubscribe. */
34
+ static OnInsetsChanged(handler: () => void): () => void;
35
+ static MeasureInsets(): void;
36
+ /** CanvasKit instance, valid after BuildAsync(). */
37
+ static CK: CanvasKit;
38
+ /** Registered typefaces by alias (FontFamily) and weight. */
39
+ static readonly Fonts: Map<string, Map<number, Typeface>>;
40
+ /** First registered font, or CanvasKit's built-in one. */
41
+ static DefaultTypeface?: Typeface;
42
+ private static readonly fontCache;
43
+ static UseDrawnUi(): DrawnUiBuilder;
44
+ /** Master switch: false makes every control render uncached (DrawnUi Super.CacheEnabled). */
45
+ static CacheEnabled: boolean;
46
+ private static readonly colorCache;
47
+ /**
48
+ * Color string -> CanvasKit color. Hex follows the MAUI/DrawnUi convention: #RGB, #ARGB, #RRGGBB, #AARRGGBB
49
+ * (alpha FIRST — CSS puts it last, CanvasKit's own parser would read "#22FFFFFF" as opaque cyan).
50
+ * rgb()/rgba() strings are passed to CanvasKit as is.
51
+ */
52
+ static ParseColor(color: string): Float32Array;
53
+ /** Typeface for an alias at a weight: exact, else the nearest registered weight, else the default face. */
54
+ /** Alias of the first registered font: what an empty FontFamily resolves to (weights included). */
55
+ static DefaultFontAlias: string;
56
+ static GetTypeface(alias?: string, weight?: number): Typeface | null;
57
+ /** Nearest registered weight of the alias (empty alias = the first registered family); reports the weight actually used. */
58
+ private static ResolveTypeface;
59
+ /** Shared Font (typeface + pixel size + synthetic italic), cached across all labels. */
60
+ static GetFont(alias: string, weight: number, italic: boolean, sizePx: number): Font;
61
+ }
@@ -0,0 +1,178 @@
1
+ import { Thickness } from "./Types";
2
+ // the "full" build: same API plus Skottie (SkiaLottie) and the paragraph module; +0.9 MB raw over the default build
3
+ import CanvasKitInit from "canvaskit-wasm/bin/full/canvaskit.js";
4
+ import wasmUrl from "canvaskit-wasm/bin/full/canvaskit.wasm?url";
5
+ /** Mirrors DrawnUi.Net IFontCollection: fonts.AddFont(source, alias[, weight]), plus the Blazor-head AddSymbols/AddEmojis. */
6
+ export class FontCollection {
7
+ /** Where the shipped subset fonts are served from (the demo copies them to public/fonts). */
8
+ static ContentRoot = "fonts/";
9
+ Fonts = [];
10
+ /** Noto Sans Math + Symbols 2 subsets as "FontSymbols" / "FontSymbols2" (arrows, math, misc symbols) — DrawnUi.Blazor AddSymbols. */
11
+ AddSymbols() {
12
+ return this.AddFont(FontCollection.ContentRoot + "NotoSansMathSymbols-Subset.ttf", "FontSymbols")
13
+ .AddFont(FontCollection.ContentRoot + "NotoSansSymbols2-Subset.ttf", "FontSymbols2");
14
+ }
15
+ /** Noto Color Emoji subset (faces + hands) as "FontEmoji" — DrawnUi.Blazor AddEmojis. */
16
+ AddEmojis() {
17
+ return this.AddFont(FontCollection.ContentRoot + "NotoColorEmoji-Subset.ttf", "FontEmoji");
18
+ }
19
+ /** Registers a face under an alias; weight 100..900 lets FontWeight / FontAttributes=Bold pick the right file. */
20
+ AddFont(source, alias, weight = 400) {
21
+ this.Fonts.push({ Source: source, Alias: alias, Weight: weight });
22
+ return this;
23
+ }
24
+ }
25
+ /** Mirrors DrawnUi.Net DrawnUiBuilder: Super.UseDrawnUi().ConfigureFonts(...).BuildAsync(). */
26
+ export class DrawnUiBuilder {
27
+ fonts = new FontCollection();
28
+ ConfigureFonts(configure) {
29
+ configure(this.fonts);
30
+ return this;
31
+ }
32
+ /** Loads CanvasKit + registered fonts. Must complete before the first Canvas is created. */
33
+ async BuildAsync() {
34
+ Super.CK = await CanvasKitInit({ locateFile: () => wasmUrl });
35
+ const loaded = new Map(); // same file registered twice = one face
36
+ for (const f of this.fonts.Fonts) {
37
+ let face = loaded.get(f.Source);
38
+ if (!face) {
39
+ const data = await (await fetch(f.Source)).arrayBuffer();
40
+ face = Super.CK.Typeface.MakeFreeTypeFaceFromData(data) ?? undefined;
41
+ if (!face)
42
+ throw new Error(`DrawnUi: cannot load font '${f.Source}'`);
43
+ loaded.set(f.Source, face);
44
+ // the same face as a CSS font under the alias: the accessibility overlay's selectable text lines up with the drawn glyphs
45
+ if (typeof FontFace !== "undefined" && typeof document !== "undefined") {
46
+ try {
47
+ const css = new FontFace(f.Alias, data, { weight: String(f.Weight) });
48
+ await css.load();
49
+ document.fonts.add(css);
50
+ }
51
+ catch { /* selection text falls back to a system font */ }
52
+ }
53
+ }
54
+ let weights = Super.Fonts.get(f.Alias);
55
+ if (!weights) {
56
+ weights = new Map();
57
+ Super.Fonts.set(f.Alias, weights);
58
+ }
59
+ weights.set(f.Weight, face);
60
+ Super.DefaultTypeface ??= face;
61
+ if (!Super.DefaultFontAlias)
62
+ Super.DefaultFontAlias = f.Alias;
63
+ }
64
+ Super.DefaultTypeface ??= Super.CK.Typeface.GetDefault() ?? undefined;
65
+ }
66
+ }
67
+ /** Mirrors DrawnUi static Super: global engine state. */
68
+ export class Super {
69
+ // ---- insets (C# Super.Insets / InsetsChanged): the browser's safe area from env(safe-area-inset-*) ----
70
+ static insets;
71
+ static insetsProbe;
72
+ static insetsChanged = new Set();
73
+ /** Safe-area insets in CSS px (points), measured once and on resize; zero outside a browser. */
74
+ static get Insets() {
75
+ if (!Super.insets)
76
+ Super.MeasureInsets();
77
+ return Super.insets ?? Thickness.Zero;
78
+ }
79
+ /** Subscribe to inset changes (orientation / resize); returns the unsubscribe. */
80
+ static OnInsetsChanged(handler) { Super.insetsChanged.add(handler); return () => Super.insetsChanged.delete(handler); }
81
+ static MeasureInsets() {
82
+ if (typeof document === "undefined") {
83
+ Super.insets = Thickness.Zero;
84
+ return;
85
+ }
86
+ let probe = Super.insetsProbe;
87
+ if (!probe) {
88
+ probe = document.createElement("div");
89
+ probe.style.cssText = "position:fixed;left:0;top:0;width:0;height:0;visibility:hidden;pointer-events:none;padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);";
90
+ document.body.appendChild(probe);
91
+ Super.insetsProbe = probe;
92
+ window.addEventListener("resize", () => Super.MeasureInsets());
93
+ }
94
+ const cs = getComputedStyle(probe);
95
+ const next = new Thickness(parseFloat(cs.paddingLeft) || 0, parseFloat(cs.paddingTop) || 0, parseFloat(cs.paddingRight) || 0, parseFloat(cs.paddingBottom) || 0);
96
+ const prev = Super.insets;
97
+ Super.insets = next;
98
+ if (prev && (prev.Left !== next.Left || prev.Top !== next.Top || prev.Right !== next.Right || prev.Bottom !== next.Bottom))
99
+ for (const h of [...Super.insetsChanged])
100
+ h();
101
+ }
102
+ /** CanvasKit instance, valid after BuildAsync(). */
103
+ static CK;
104
+ /** Registered typefaces by alias (FontFamily) and weight. */
105
+ static Fonts = new Map();
106
+ /** First registered font, or CanvasKit's built-in one. */
107
+ static DefaultTypeface;
108
+ static fontCache = new Map();
109
+ static UseDrawnUi() { return new DrawnUiBuilder(); }
110
+ /** Master switch: false makes every control render uncached (DrawnUi Super.CacheEnabled). */
111
+ static CacheEnabled = true;
112
+ static colorCache = new Map();
113
+ /**
114
+ * Color string -> CanvasKit color. Hex follows the MAUI/DrawnUi convention: #RGB, #ARGB, #RRGGBB, #AARRGGBB
115
+ * (alpha FIRST — CSS puts it last, CanvasKit's own parser would read "#22FFFFFF" as opaque cyan).
116
+ * rgb()/rgba() strings are passed to CanvasKit as is.
117
+ */
118
+ static ParseColor(color) {
119
+ let c = Super.colorCache.get(color);
120
+ if (c)
121
+ return c;
122
+ if (color.startsWith("#")) {
123
+ let h = color.slice(1);
124
+ if (h.length === 3 || h.length === 4)
125
+ h = [...h].map((ch) => ch + ch).join("");
126
+ let a = 1, rgb = h;
127
+ if (h.length === 8) {
128
+ a = parseInt(h.slice(0, 2), 16) / 255;
129
+ rgb = h.slice(2);
130
+ }
131
+ const r = parseInt(rgb.slice(0, 2), 16) / 255, g = parseInt(rgb.slice(2, 4), 16) / 255, b = parseInt(rgb.slice(4, 6), 16) / 255;
132
+ c = Super.CK.Color4f(r, g, b, a);
133
+ }
134
+ else {
135
+ c = Super.CK.parseColorString(color);
136
+ }
137
+ Super.colorCache.set(color, c);
138
+ return c;
139
+ }
140
+ /** Typeface for an alias at a weight: exact, else the nearest registered weight, else the default face. */
141
+ /** Alias of the first registered font: what an empty FontFamily resolves to (weights included). */
142
+ static DefaultFontAlias = "";
143
+ static GetTypeface(alias, weight = 0) {
144
+ return Super.ResolveTypeface(alias, weight).Typeface;
145
+ }
146
+ /** Nearest registered weight of the alias (empty alias = the first registered family); reports the weight actually used. */
147
+ static ResolveTypeface(alias, weight) {
148
+ const weights = Super.Fonts.get(alias || Super.DefaultFontAlias);
149
+ const target = weight > 0 ? weight : 400;
150
+ if (weights && weights.size > 0) {
151
+ if (weights.has(target))
152
+ return { Typeface: weights.get(target), Weight: target };
153
+ let best;
154
+ for (const w of weights.keys())
155
+ if (best === undefined || Math.abs(w - target) < Math.abs(best - target))
156
+ best = w;
157
+ return { Typeface: weights.get(best), Weight: best };
158
+ }
159
+ return { Typeface: Super.DefaultTypeface ?? null, Weight: 400 };
160
+ }
161
+ /** Shared Font (typeface + pixel size + synthetic italic), cached across all labels. */
162
+ static GetFont(alias, weight, italic, sizePx) {
163
+ const key = `${alias}|${weight}|${italic ? 1 : 0}|${sizePx}`;
164
+ let font = Super.fontCache.get(key);
165
+ if (!font) {
166
+ const resolved = Super.ResolveTypeface(alias, weight);
167
+ font = new Super.CK.Font(resolved.Typeface, sizePx);
168
+ font.setSubpixel(true);
169
+ if (italic)
170
+ font.setSkewX(-0.25);
171
+ // no bold face registered: synthetic bold, like C# Font.Embolden = IsBold
172
+ if (weight >= 600 && resolved.Weight < 600)
173
+ font.setEmbolden(true);
174
+ Super.fontCache.set(key, font);
175
+ }
176
+ return font;
177
+ }
178
+ }
@@ -0,0 +1,31 @@
1
+ import type { SkiaEditor } from "../controls/SkiaEditor";
2
+ /**
3
+ * Hidden DOM textarea standing in for the focused SkiaEditor so IME composition, mobile soft keyboards,
4
+ * autocorrect, native copy / cut / paste and undo reach the drawn editor. Not part of DrawnUi.Blazor (which has no
5
+ * DOM input): the textarea mirrors the editor's text + selection, and every `input` event is diffed against the
6
+ * editor and applied through the same stub methods the physical keyboard uses (`StubSelectRange` + `StubTypeText`
7
+ * / `StubBackspace`, a line break through `StubPressEnter`). Keys that the textarea turns into input events
8
+ * (characters, Backspace, Delete, Enter, clipboard shortcuts) are skipped by the KeyboardManager path while the
9
+ * proxy is active; navigation keys keep going through `KeyDown`.
10
+ */
11
+ export declare class TextInputProxy {
12
+ private static element?;
13
+ private static owner?;
14
+ private static composing;
15
+ private static applying;
16
+ /** The textarea (created on first use); KeyboardManager recognizes it through `data-drawnui-text-input`. */
17
+ static get Element(): HTMLTextAreaElement | undefined;
18
+ static IsActive(editor: SkiaEditor): boolean;
19
+ static IsProxyTarget(target: EventTarget | null): boolean;
20
+ private static Create;
21
+ /** The editor took focus: the textarea mirrors it and gets DOM focus (soft keyboard, IME). */
22
+ static Attach(editor: SkiaEditor): void;
23
+ static Detach(editor: SkiaEditor): void;
24
+ private static Configure;
25
+ /** Editor text / selection changed: mirror it (never while an IME composition is in flight). */
26
+ static Sync(editor: SkiaEditor): void;
27
+ /** Puts the textarea over the editor so mobile browsers scroll the right spot into view. */
28
+ private static Place;
29
+ /** The textarea changed (typing, IME commit, paste, undo): diff against the editor and replay through the stubs. */
30
+ private static ApplyFromElement;
31
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Hidden DOM textarea standing in for the focused SkiaEditor so IME composition, mobile soft keyboards,
3
+ * autocorrect, native copy / cut / paste and undo reach the drawn editor. Not part of DrawnUi.Blazor (which has no
4
+ * DOM input): the textarea mirrors the editor's text + selection, and every `input` event is diffed against the
5
+ * editor and applied through the same stub methods the physical keyboard uses (`StubSelectRange` + `StubTypeText`
6
+ * / `StubBackspace`, a line break through `StubPressEnter`). Keys that the textarea turns into input events
7
+ * (characters, Backspace, Delete, Enter, clipboard shortcuts) are skipped by the KeyboardManager path while the
8
+ * proxy is active; navigation keys keep going through `KeyDown`.
9
+ */
10
+ export class TextInputProxy {
11
+ static element;
12
+ static owner;
13
+ static composing = false;
14
+ static applying = false;
15
+ /** The textarea (created on first use); KeyboardManager recognizes it through `data-drawnui-text-input`. */
16
+ static get Element() { return TextInputProxy.element; }
17
+ static IsActive(editor) { return TextInputProxy.owner === editor; }
18
+ static IsProxyTarget(target) { return !!target && target === TextInputProxy.element; }
19
+ static Create() {
20
+ if (typeof document === "undefined")
21
+ return undefined;
22
+ const el = document.createElement("textarea");
23
+ el.dataset.drawnuiTextInput = "1";
24
+ el.setAttribute("aria-hidden", "true");
25
+ el.tabIndex = -1;
26
+ el.autocomplete = "off";
27
+ el.setAttribute("autocorrect", "off");
28
+ el.setAttribute("autocapitalize", "off");
29
+ el.rows = 1;
30
+ // fixed 1x1 box over the caret: invisible, still focusable, 16px font keeps iOS from zooming the page
31
+ el.style.cssText = "position:fixed;left:0;top:0;width:1px;height:1px;padding:0;border:0;margin:0;opacity:0.01;font-size:16px;line-height:1;resize:none;overflow:hidden;outline:none;background:transparent;color:transparent;caret-color:transparent;pointer-events:none;z-index:-1;";
32
+ el.addEventListener("compositionstart", () => { TextInputProxy.composing = true; });
33
+ el.addEventListener("compositionend", () => { TextInputProxy.composing = false; TextInputProxy.ApplyFromElement(); });
34
+ el.addEventListener("input", () => TextInputProxy.ApplyFromElement());
35
+ el.addEventListener("blur", () => {
36
+ // focus moved to another DOM text field (a page input): the drawn editor loses focus like C# BlurExternalTextInput
37
+ queueMicrotask(() => {
38
+ const a = document.activeElement;
39
+ const owner = TextInputProxy.owner;
40
+ if (owner && a && a !== el && (a.tagName === "INPUT" || a.tagName === "TEXTAREA" || a.isContentEditable))
41
+ owner.IsFocused = false;
42
+ });
43
+ });
44
+ // a tap on the canvas moves DOM focus to the body: give it back to the proxy synchronously (keeps the soft keyboard)
45
+ window.addEventListener("pointerup", (e) => {
46
+ const owner = TextInputProxy.owner;
47
+ if (owner && owner.IsFocused && e.target?.tagName === "CANVAS")
48
+ el.focus({ preventScroll: true });
49
+ }, true);
50
+ document.body.appendChild(el);
51
+ return el;
52
+ }
53
+ /** The editor took focus: the textarea mirrors it and gets DOM focus (soft keyboard, IME). */
54
+ static Attach(editor) {
55
+ const el = TextInputProxy.element ?? (TextInputProxy.element = TextInputProxy.Create());
56
+ if (!el)
57
+ return;
58
+ TextInputProxy.owner = editor;
59
+ TextInputProxy.composing = false;
60
+ TextInputProxy.Configure(editor, el);
61
+ TextInputProxy.Sync(editor);
62
+ el.focus({ preventScroll: true });
63
+ }
64
+ static Detach(editor) {
65
+ if (TextInputProxy.owner !== editor)
66
+ return;
67
+ TextInputProxy.owner = undefined;
68
+ const el = TextInputProxy.element;
69
+ if (el && document.activeElement === el)
70
+ el.blur();
71
+ }
72
+ static Configure(editor, el) {
73
+ const modes = { Default: "text", Numeric: "numeric", Decimal: "decimal", Phone: "tel", Email: "email" };
74
+ el.inputMode = modes[editor.KeyboardType] ?? "text";
75
+ const hints = { Done: "done", Go: "go", Next: "next", Search: "search", Send: "send" };
76
+ el.enterKeyHint = hints[editor.ReturnType] ?? (editor.IsMultiline ? "enter" : "done");
77
+ el.spellcheck = editor.IsSpellCheckEnabled;
78
+ }
79
+ /** Editor text / selection changed: mirror it (never while an IME composition is in flight). */
80
+ static Sync(editor) {
81
+ const el = TextInputProxy.element;
82
+ if (!el || TextInputProxy.owner !== editor || TextInputProxy.composing || TextInputProxy.applying)
83
+ return;
84
+ if (el.value !== editor.Text)
85
+ el.value = editor.Text;
86
+ const start = editor.CursorPosition, end = start + editor.SelectionLength;
87
+ if (el.selectionStart !== start || el.selectionEnd !== end)
88
+ el.setSelectionRange(start, end);
89
+ TextInputProxy.Place(editor, el);
90
+ }
91
+ /** Puts the textarea over the editor so mobile browsers scroll the right spot into view. */
92
+ static Place(editor, el) {
93
+ const canvas = editor.Superview?.Element;
94
+ if (!canvas)
95
+ return;
96
+ const b = canvas.getBoundingClientRect();
97
+ const scale = editor.RenderingScale || 1;
98
+ const r = editor.DrawingRect;
99
+ el.style.left = `${Math.round(b.left + r.Left / scale)}px`;
100
+ el.style.top = `${Math.round(b.top + r.Top / scale)}px`;
101
+ }
102
+ /** The textarea changed (typing, IME commit, paste, undo): diff against the editor and replay through the stubs. */
103
+ static ApplyFromElement() {
104
+ const el = TextInputProxy.element, editor = TextInputProxy.owner;
105
+ if (!el || !editor || TextInputProxy.composing)
106
+ return;
107
+ const next = el.value, prev = editor.Text;
108
+ if (next === prev) {
109
+ TextInputProxy.Sync(editor);
110
+ return;
111
+ }
112
+ let start = 0;
113
+ const max = Math.min(prev.length, next.length);
114
+ while (start < max && prev.charCodeAt(start) === next.charCodeAt(start))
115
+ start++;
116
+ let endPrev = prev.length, endNext = next.length;
117
+ while (endPrev > start && endNext > start && prev.charCodeAt(endPrev - 1) === next.charCodeAt(endNext - 1)) {
118
+ endPrev--;
119
+ endNext--;
120
+ }
121
+ const inserted = next.slice(start, endNext);
122
+ TextInputProxy.applying = true;
123
+ try {
124
+ editor.StubSelectRange(start, endPrev - start);
125
+ if (inserted.includes("\n") && (!editor.IsMultiline || editor.ShouldSubmitOnEnter)) {
126
+ // Enter on a single-line editor (or a Send editor): submit instead of inserting the break
127
+ const text = inserted.replace(/\n/g, "");
128
+ if (text)
129
+ editor.StubTypeText(text);
130
+ else if (endPrev > start)
131
+ editor.StubBackspace();
132
+ editor.StubPressEnter(false, false);
133
+ }
134
+ else if (inserted)
135
+ editor.StubTypeText(inserted);
136
+ else
137
+ editor.StubBackspace();
138
+ }
139
+ finally {
140
+ TextInputProxy.applying = false;
141
+ }
142
+ TextInputProxy.Sync(editor);
143
+ }
144
+ }