@effindomv2/fui-as 0.1.36 → 0.1.37

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.
@@ -0,0 +1,72 @@
1
+ import type { BridgeRuntime } from '@effindomv2/runtime';
2
+ import { normalizePointerForWasm, pointerToHeapOffset } from '@effindomv2/runtime';
3
+
4
+ interface ManagedHarnessCanvasHostDependencies {
5
+ getRuntime(): BridgeRuntime;
6
+ writeAppBytes(ptr: number, capacity: number, bytes: Uint8Array, context: string): number;
7
+ }
8
+
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ type AnyFn = (...args: any[]) => any;
11
+
12
+ export function createManagedHarnessCanvasHost(deps: ManagedHarnessCanvasHostDependencies) {
13
+ const surfaces = new Map<number, number>();
14
+ const core = () => deps.getRuntime().core as unknown as Record<string, AnyFn>;
15
+ const ptr = (p: number) => normalizePointerForWasm(deps.getRuntime().core, p);
16
+
17
+ const I: Record<string, AnyFn> = {};
18
+
19
+ I.fui_canvas_save = (p: number) => core()._ed_canvas_save(ptr(p));
20
+ I.fui_canvas_restore = (p: number) => core()._ed_canvas_restore(ptr(p));
21
+ I.fui_canvas_translate = (p: number, x: number, y: number) => core()._ed_canvas_translate(ptr(p), x, y);
22
+ I.fui_canvas_scale = (p: number, sx: number, sy: number) => core()._ed_canvas_scale(ptr(p), sx, sy);
23
+ I.fui_canvas_rotate = (p: number, d: number) => core()._ed_canvas_rotate(ptr(p), d);
24
+ I.fui_canvas_clip_rect = (p: number, x: number, y: number, w: number, h: number) => core()._ed_canvas_clip_rect(ptr(p), x, y, w, h);
25
+ I.fui_canvas_draw_rect = (p: number, x: number, y: number, w: number, h: number, fc: number, sc: number, sw: number) =>
26
+ core()._ed_canvas_draw_rect(ptr(p), x, y, w, h, fc, sc, sw);
27
+ I.fui_canvas_draw_circle = (p: number, cx: number, cy: number, r: number, fc: number, sc: number, sw: number) =>
28
+ core()._ed_canvas_draw_circle(ptr(p), cx, cy, r, fc, sc, sw);
29
+ I.fui_canvas_draw_line = (p: number, x1: number, y1: number, x2: number, y2: number, c: number, sw: number) =>
30
+ core()._ed_canvas_draw_line(ptr(p), x1, y1, x2, y2, c, sw);
31
+ I.fui_canvas_draw_round_rect = (p: number, x: number, y: number, w: number, h: number, rx: number, ry: number, fc: number, sc: number, sw: number) =>
32
+ core()._ed_canvas_draw_round_rect(ptr(p), x, y, w, h, rx, ry, fc, sc, sw);
33
+ I.fui_canvas_draw_path = (p: number, pid: number, fc: number, sc: number, sw: number) =>
34
+ core()._ed_canvas_draw_path(ptr(p), pid, fc, sc, sw);
35
+ I.fui_canvas_draw_text = (p: number, up: number, ul: number, x: number, y: number, fid: number, fs: number, c: number) =>
36
+ core()._ed_canvas_draw_text(ptr(p), up, ul, x, y, fid, fs, c);
37
+ I.fui_canvas_draw_image = (p: number, tid: number, x: number, y: number, w: number, h: number) =>
38
+ core()._ed_canvas_draw_image(ptr(p), tid, x, y, w, h);
39
+ I.fui_canvas_draw_svg = (p: number, sid: number, x: number, y: number, w: number, h: number) =>
40
+ core()._ed_canvas_draw_svg(ptr(p), sid, x, y, w, h);
41
+
42
+ I.fui_path_create = () => core()._ed_path_create();
43
+ I.fui_path_destroy = (id: number) => core()._ed_path_destroy(id);
44
+ I.fui_path_move_to = (id: number, x: number, y: number) => core()._ed_path_move_to(id, x, y);
45
+ I.fui_path_line_to = (id: number, x: number, y: number) => core()._ed_path_line_to(id, x, y);
46
+ I.fui_path_quad_to = (id: number, cx: number, cy: number, x: number, y: number) => core()._ed_path_quad_to(id, cx, cy, x, y);
47
+ I.fui_path_cubic_to = (id: number, cx1: number, cy1: number, cx2: number, cy2: number, x: number, y: number) =>
48
+ core()._ed_path_cubic_to(id, cx1, cy1, cx2, cy2, x, y);
49
+ I.fui_path_close = (id: number) => core()._ed_path_close(id);
50
+ I.fui_path_add_rect = (id: number, x: number, y: number, w: number, h: number) => core()._ed_path_add_rect(id, x, y, w, h);
51
+ I.fui_path_add_circle = (id: number, cx: number, cy: number, r: number) => core()._ed_path_add_circle(id, cx, cy, r);
52
+
53
+ I.fui_canvas_create_offscreen = (w: number, h: number) => {
54
+ const id = core()._ed_canvas_create_offscreen(w, h) as number;
55
+ if (id !== 0) surfaces.set(id, id);
56
+ return id;
57
+ };
58
+ I.fui_canvas_get_offscreen_ptr = (id: number) => core()._ed_canvas_get_offscreen_canvas(id);
59
+ I.fui_canvas_read_offscreen_pixels = (id: number, outPtr: number, w: number, h: number) => {
60
+ const bytesLen = w * h * 4;
61
+ const c = core();
62
+ const bufPtr = c._malloc(bytesLen);
63
+ try {
64
+ c._ed_canvas_read_offscreen_pixels(id, pointerToHeapOffset(bufPtr));
65
+ const off = pointerToHeapOffset(bufPtr);
66
+ deps.writeAppBytes(outPtr, bytesLen, (c.HEAPU8 as unknown as Uint8Array).slice(off, off + bytesLen), 'canvas-read');
67
+ } finally { c._free(bufPtr); }
68
+ };
69
+ I.fui_canvas_destroy_offscreen = (id: number) => { surfaces.delete(id); core()._ed_canvas_destroy_offscreen(id); };
70
+
71
+ return { imports: I };
72
+ }
@@ -14,6 +14,7 @@ import {
14
14
  } from './interop';
15
15
  import { createHostImportModule } from './host-imports';
16
16
  import { createManagedHarnessBitmapHost } from './managed-harness-bitmap-host';
17
+ import { createManagedHarnessCanvasHost } from './managed-harness-canvas-host';
17
18
  import { createManagedHarnessFileHost } from './managed-harness-file-host';
18
19
  import {
19
20
  EXTERNAL_DRAG_EVENT_DROP,
@@ -313,6 +314,11 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
313
314
  });
314
315
  bitmapHost.installReplay(runtime);
315
316
 
317
+ const canvasHost = createManagedHarnessCanvasHost({
318
+ getRuntime: () => runtime,
319
+ writeAppBytes,
320
+ });
321
+
316
322
  async function settleCurrentSessionAfterRestore(context: string): Promise<void> {
317
323
  const session = currentSession;
318
324
  if (session === null) {
@@ -896,6 +902,7 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
896
902
  notifyTextureFailed,
897
903
  }),
898
904
  ...bitmapHost.imports,
905
+ ...canvasHost.imports,
899
906
  ...fileHost.imports,
900
907
  },
901
908
  fui_fetch_host: fetchHost.imports,
@@ -1387,6 +1394,20 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
1387
1394
  },
1388
1395
  };
1389
1396
  currentSession = session;
1397
+
1398
+ // Wire the immediate-mode custom draw callback (Tier 1 → JS → Tier 3)
1399
+ (window as unknown as Record<string, unknown>).__effindomV2CustomDraw = (
1400
+ handleLo: number,
1401
+ handleHi: number,
1402
+ canvasPtr: number,
1403
+ ): void => {
1404
+ const handle = (BigInt(handleHi >>> 0) << 32n) | BigInt(handleLo >>> 0);
1405
+ const rawExports = exports as unknown as Record<string, unknown>;
1406
+ if (typeof rawExports.fui_dispatch_custom_draw === 'function') {
1407
+ (rawExports.fui_dispatch_custom_draw as (h: bigint, p: number) => void)(handle, canvasPtr);
1408
+ }
1409
+ };
1410
+
1390
1411
  notifyRouteForCurrentLocation(session);
1391
1412
  runtime.setAppFrameHandler((timestampMs: number) => {
1392
1413
  if (currentSession !== session) {
@@ -130,6 +130,7 @@ export interface HarnessExports {
130
130
  __fui_on_worker_complete(workerId: number, textPtr: number, textLen: number): void;
131
131
  __fui_on_worker_error(workerId: number, textPtr: number, textLen: number): void;
132
132
  __fui_restore_persisted_ui_state?(): void;
133
+ fui_dispatch_custom_draw?(handle: bigint, canvasPtr: number): void;
133
134
  }
134
135
 
135
136
  export interface HarnessContext<Exports extends HarnessExports> {
@@ -115,6 +115,12 @@ export function createUiImportModule(deps: UiImportDeps) {
115
115
  ui_set_is_shared_size_scope(handle: AppHandleLike, flag: number): void {
116
116
  deps.getRuntime().ui._ui_set_is_shared_size_scope(toBigIntHandle(handle), flag);
117
117
  },
118
+ ui_set_custom_drawable(handle: AppHandleLike, flag: number): void {
119
+ deps.getRuntime().ui._ui_set_custom_drawable(toBigIntHandle(handle), flag);
120
+ },
121
+ ui_set_flex_wrap(handle: AppHandleLike, wrap: number): void {
122
+ deps.getRuntime().ui._ui_set_flex_wrap(toBigIntHandle(handle), wrap);
123
+ },
118
124
  ui_set_root(handle: AppHandleLike): void {
119
125
  const runtime = deps.getRuntime();
120
126
  const rootHandle = toBigIntHandle(handle);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effindomv2/fui-as",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "private": false,
5
5
  "license": "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial",
6
6
  "description": "EffinDom v2 AssemblyScript frontend framework SDK and browser harness",
@@ -85,9 +85,8 @@
85
85
  "typecheck": "npm run guard:runtime-dependency && tsc -p tsconfig.json --noEmit"
86
86
  },
87
87
  "dependencies": {
88
- "@assemblyscript/loader": "^0.28.17",
89
- "@devcycle/assemblyscript-json": "^2.0.0",
90
- "@effindomv2/runtime": "0.1.17"
88
+ "@assemblyscript/loader": "^0.28.19",
89
+ "@effindomv2/runtime": "0.1.18"
91
90
  },
92
91
  "devDependencies": {
93
92
  "@as-pect/assembly": "8.1.0",
@@ -95,7 +94,8 @@
95
94
  "@as-pect/core": "8.1.0",
96
95
  "@playwright/test": "1.59.1",
97
96
  "@types/node": "^24.10.1",
98
- "assemblyscript": "0.28.17",
97
+ "assemblyscript": "0.28.19",
98
+ "@devcycle/assemblyscript-json": "2.0.0",
99
99
  "esbuild": "^0.27.7",
100
100
  "typescript": "^5.9.3"
101
101
  }
package/scripts/build.sh CHANGED
@@ -287,6 +287,7 @@ build_app "demo/src/routes/demo_home.ts" "${DEMO_OUT_DIR}/home.wasm"
287
287
  build_app "demo/src/routes/demo_advanced_controls.ts" "${DEMO_OUT_DIR}/advanced-controls.wasm"
288
288
  build_app "demo/src/routes/templated-controls.ts" "${DEMO_OUT_DIR}/templated-controls.wasm"
289
289
  build_app "demo/src/routes/demo_scrollbar_gutter.ts" "${DEMO_OUT_DIR}/scrollbar-gutter.wasm"
290
+ build_app "demo/src/routes/demo_immediate_drawing.ts" "${DEMO_OUT_DIR}/immediate-drawing.wasm"
290
291
  build_workers
291
292
  write_worker_manifest
292
293
 
@@ -346,10 +347,11 @@ cp "${PACKAGE_DIR}/browser/favicon.ico" "${REPO_ROOT}/public/favicon.ico"
346
347
  cp "${PACKAGE_DIR}/demo/demo-texture.png" "${DEMO_OUT_DIR}/demo-texture.png"
347
348
  cp "${PACKAGE_DIR}/demo/demo-secondary-texture.png" "${DEMO_OUT_DIR}/demo-secondary-texture.png"
348
349
 
349
- mkdir -p "${DEMO_OUT_DIR}/advanced-controls" "${DEMO_OUT_DIR}/templated-controls" "${DEMO_OUT_DIR}/scrollbar-gutter"
350
+ mkdir -p "${DEMO_OUT_DIR}/advanced-controls" "${DEMO_OUT_DIR}/templated-controls" "${DEMO_OUT_DIR}/scrollbar-gutter" "${DEMO_OUT_DIR}/immediate-drawing"
350
351
  render_html_with_loading_overlay "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/advanced-controls/index.html"
351
352
  render_html_with_loading_overlay "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/templated-controls/index.html"
352
353
  render_html_with_loading_overlay "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/scrollbar-gutter/index.html"
354
+ render_html_with_loading_overlay "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/immediate-drawing/index.html"
353
355
 
354
356
  copy_runtime_assets "${OUT_DIR}"
355
357
  copy_runtime_assets "${DEMO_OUT_DIR}"
@@ -360,3 +362,4 @@ write_runtime_config "${DEMO_OUT_DIR}" "${DEFAULT_MANIFEST_PATH}"
360
362
  write_runtime_config "${DEMO_OUT_DIR}/advanced-controls" "../runtime/dist/effindom.v2.manifest.json"
361
363
  write_runtime_config "${DEMO_OUT_DIR}/templated-controls" "../runtime/dist/effindom.v2.manifest.json"
362
364
  write_runtime_config "${DEMO_OUT_DIR}/scrollbar-gutter" "../runtime/dist/effindom.v2.manifest.json"
365
+ write_runtime_config "${DEMO_OUT_DIR}/immediate-drawing" "../runtime/dist/effindom.v2.manifest.json"
package/src/Fui.ts CHANGED
@@ -31,10 +31,14 @@ export {
31
31
  loadTexture,
32
32
  } from "./core/Assets";
33
33
  export { Bitmap } from "./core/Bitmap";
34
+ export { DrawContext } from "./drawing/DrawContext";
35
+ export { Paint } from "./drawing/Paint";
36
+ export { Path } from "./drawing/Path";
34
37
  export { Fetch, FetchRequest, FetchResponse } from "./core/Fetch";
35
38
  export { log } from "./core/Logger";
36
39
  export { disposeAll, Disposable } from "./core/Disposable";
37
40
  export { cancelAllTimers, cancelTimer, hasTimer, scheduleTimer } from "./core/Timers";
41
+ export { onLoaded } from "./core/FrameScheduler";
38
42
  export { SetBackgroundAction, SetTextAction } from "./core/Actions";
39
43
  export { hslToColor, mixColor, rgb, rgba } from "./color";
40
44
  export {
@@ -74,6 +78,7 @@ export {
74
78
  BorderStyle,
75
79
  CursorStyle,
76
80
  FlexDirection,
81
+ FlexWrap,
77
82
  GridUnit,
78
83
  HandleValue,
79
84
  JustifyContent,
@@ -201,6 +206,7 @@ export {
201
206
  export {
202
207
  FlexBox,
203
208
  FlexBoxProps,
209
+ CustomDrawable,
204
210
  GradientStop,
205
211
  Grid,
206
212
  Image,
package/src/FuiExports.ts CHANGED
@@ -53,3 +53,4 @@ export {
53
53
  __fui_on_texture_failed,
54
54
  __fui_on_texture_loaded,
55
55
  } from "./core/Assets";
56
+ export { fui_dispatch_custom_draw } from "./drawing/DrawCallback";
@@ -172,6 +172,14 @@ export function setIsSharedSizeScope(handle: u64, isScope: bool): void {
172
172
  ffi.ui_set_is_shared_size_scope(handle, isScope);
173
173
  }
174
174
 
175
+ export function setCustomDrawable(handle: u64, flag: bool): void {
176
+ ffi.ui_set_custom_drawable(handle, flag);
177
+ }
178
+
179
+ export function setFlexWrap(handle: u64, wrap: u32): void {
180
+ ffi.ui_set_flex_wrap(handle, wrap);
181
+ }
182
+
175
183
  export function gridSetColumns(handle: u64, values: Float32Array, types: Uint8Array): void {
176
184
  ffi.ui_grid_set_columns(
177
185
  handle,
@@ -5,7 +5,7 @@ import { disposeAllFileRequests } from "./File";
5
5
  import { FocusAdornerManager } from "./FocusAdornerManager";
6
6
  import { ToolTipManager } from "./ToolTipManager";
7
7
  import * as ui from "../bindings/ui";
8
- import { flushCommit, markNeedsCommit, resetCommitState } from "./FrameScheduler";
8
+ import { flushCommit, fireLoadedCallbacks, markNeedsCommit, resetCommitState } from "./FrameScheduler";
9
9
  import { HandleValue, Unit } from "./ffi";
10
10
  import { Node } from "./Node";
11
11
  import { cancelAllTimers } from "./Timers";
@@ -211,6 +211,7 @@ export class Application<TPage> {
211
211
  ui.setRoot(shell.builtHandle);
212
212
  mountedShell = shell;
213
213
  mountedRoot = root;
214
+ fireLoadedCallbacks();
214
215
  markNeedsCommit();
215
216
  return root;
216
217
  }
@@ -1,7 +1,9 @@
1
1
  import * as ui from "../bindings/ui";
2
+ import * as ffi from "../core/ffi";
2
3
  import { markTextureAssetReady } from "./Assets";
3
4
  import { Disposable } from "./Disposable";
4
5
  import { allocateDynamicTextureId } from "./DynamicAssetIds";
6
+ import { DrawContext } from "../drawing/DrawContext";
5
7
  import { throwObjectDisposed } from "./Errors";
6
8
  import { error } from "./Logger";
7
9
 
@@ -28,6 +30,8 @@ export class Bitmap implements Disposable {
28
30
  readonly height: u32;
29
31
  readonly textureId: u32;
30
32
  private pixelBytes: Uint8Array;
33
+ private offscreenId: u32 = 0;
34
+ private canvasUsed: bool = false;
31
35
  private disposed: bool = false;
32
36
 
33
37
  constructor(width: u32, height: u32) {
@@ -36,6 +40,7 @@ export class Bitmap implements Disposable {
36
40
  this.height = height;
37
41
  this.textureId = allocateDynamicTextureId();
38
42
  this.pixelBytes = new Uint8Array(<i32>byteLength);
43
+ this.offscreenId = ffi.fui_canvas_create_offscreen(width, height);
39
44
  }
40
45
 
41
46
  pixels(): Uint8Array {
@@ -48,8 +53,29 @@ export class Bitmap implements Disposable {
48
53
  return pixels.length > 0 ? pixels.dataStart : 0;
49
54
  }
50
55
 
56
+ /**
57
+ * Returns a `DrawContext` targeting this bitmap's off‑screen raster
58
+ * surface. Drawing commands issued on the context are accumulated on
59
+ * the off‑screen canvas and uploaded to the GPU texture on the next
60
+ * `commit()` call.
61
+ *
62
+ * The returned `DrawContext` is valid for the lifetime of the `Bitmap`.
63
+ * Do not store it beyond the `Bitmap`'s disposal.
64
+ */
65
+ canvas(): DrawContext {
66
+ this.ensureNotDisposed("Bitmap.canvas");
67
+ this.canvasUsed = true;
68
+ const ptr = ffi.fui_canvas_get_offscreen_ptr(this.offscreenId);
69
+ return new DrawContext(ptr);
70
+ }
71
+
51
72
  commit(): u32 {
52
73
  this.ensureNotDisposed("Bitmap.commit");
74
+ if (this.canvasUsed) {
75
+ // Read the offscreen raster surface back into the pixel buffer
76
+ ffi.fui_canvas_read_offscreen_pixels(
77
+ this.offscreenId, this.pixelBytes.dataStart, this.width, this.height);
78
+ }
53
79
  ui.bitmapCommit(this.textureId, this.pixelBytes, this.width, this.height);
54
80
  markTextureAssetReady(this.textureId, <f32>this.width, <f32>this.height);
55
81
  return this.textureId;
@@ -60,6 +86,9 @@ export class Bitmap implements Disposable {
60
86
  return;
61
87
  }
62
88
  this.disposed = true;
89
+ if (this.offscreenId !== 0) {
90
+ ffi.fui_canvas_destroy_offscreen(this.offscreenId);
91
+ }
63
92
  ui.bitmapRelease(this.textureId);
64
93
  this.pixelBytes = new Uint8Array(0);
65
94
  }
@@ -2,6 +2,10 @@ import * as ui from "../bindings/ui";
2
2
 
3
3
  let needsCommit: bool = false;
4
4
  let flushScheduled: bool = false;
5
+ let didFirstCommit: bool = false;
6
+
7
+ // WPF-style Loaded event — fires once after the tree is built and mounted.
8
+ const loadedCallbacks = new Array<() => void>();
5
9
 
6
10
  export function markNeedsCommit(): void {
7
11
  needsCommit = true;
@@ -12,6 +16,28 @@ export function markNeedsCommit(): void {
12
16
  ui.requestRender();
13
17
  }
14
18
 
19
+ /**
20
+ * Register a callback that fires once after the page has been mounted and
21
+ * the first frame has been committed. Safe to call during `buildPage()` —
22
+ * timers started here survive `cancelAllTimers()` in `Application.mount()`.
23
+ */
24
+ export function onLoaded(cb: () => void): void {
25
+ if (didFirstCommit) {
26
+ cb();
27
+ return;
28
+ }
29
+ loadedCallbacks.push(cb);
30
+ }
31
+
32
+ export function fireLoadedCallbacks(): void {
33
+ if (didFirstCommit) return;
34
+ didFirstCommit = true;
35
+ for (let i: i32 = 0; i < loadedCallbacks.length; i++) {
36
+ loadedCallbacks[i]();
37
+ }
38
+ loadedCallbacks.length = 0;
39
+ }
40
+
15
41
  export function flushCommit(): bool {
16
42
  flushScheduled = false;
17
43
  if (!needsCommit) {
@@ -25,4 +51,5 @@ export function flushCommit(): bool {
25
51
  export function resetCommitState(): void {
26
52
  needsCommit = false;
27
53
  flushScheduled = false;
54
+ didFirstCommit = false;
28
55
  }
package/src/core/ffi.ts CHANGED
@@ -47,6 +47,12 @@ export enum FlexDirection {
47
47
  Row = 1,
48
48
  }
49
49
 
50
+ export enum FlexWrap {
51
+ NoWrap = 0,
52
+ Wrap = 1,
53
+ WrapReverse = 2,
54
+ }
55
+
50
56
  export enum JustifyContent {
51
57
  Start = 0,
52
58
  Center = 2,
@@ -291,6 +297,12 @@ export declare function ui_set_position(handle: u64, left: f32, top: f32, right:
291
297
  @external("effindom_v2_ui", "ui_set_is_shared_size_scope")
292
298
  export declare function ui_set_is_shared_size_scope(handle: u64, isScope: bool): void;
293
299
 
300
+ @external("effindom_v2_ui", "ui_set_custom_drawable")
301
+ export declare function ui_set_custom_drawable(handle: u64, flag: bool): void;
302
+
303
+ @external("effindom_v2_ui", "ui_set_flex_wrap")
304
+ export declare function ui_set_flex_wrap(handle: u64, wrap: u32): void;
305
+
294
306
  @external("effindom_v2_ui", "ui_grid_set_columns")
295
307
  export declare function ui_grid_set_columns(handle: u64, count: u32, valuesPtr: usize, typesPtr: usize): void;
296
308
 
@@ -752,3 +764,110 @@ export declare function fui_file_process_worker_start(
752
764
 
753
765
  @external("fui_host", "fui_file_process_worker_cancel")
754
766
  export declare function fui_file_process_worker_cancel(requestId: u32): void;
767
+
768
+ /* ── Immediate-mode canvas drawing ───────────────────────── */
769
+
770
+ @external("fui_host", "fui_canvas_save")
771
+ export declare function fui_canvas_save(canvasPtr: usize): void;
772
+
773
+ @external("fui_host", "fui_canvas_restore")
774
+ export declare function fui_canvas_restore(canvasPtr: usize): void;
775
+
776
+ @external("fui_host", "fui_canvas_translate")
777
+ export declare function fui_canvas_translate(canvasPtr: usize, x: f32, y: f32): void;
778
+
779
+ @external("fui_host", "fui_canvas_scale")
780
+ export declare function fui_canvas_scale(canvasPtr: usize, sx: f32, sy: f32): void;
781
+
782
+ @external("fui_host", "fui_canvas_rotate")
783
+ export declare function fui_canvas_rotate(canvasPtr: usize, degrees: f32): void;
784
+
785
+ @external("fui_host", "fui_canvas_clip_rect")
786
+ export declare function fui_canvas_clip_rect(canvasPtr: usize, x: f32, y: f32, w: f32, h: f32): void;
787
+
788
+ @external("fui_host", "fui_canvas_draw_rect")
789
+ export declare function fui_canvas_draw_rect(
790
+ canvasPtr: usize, x: f32, y: f32, w: f32, h: f32,
791
+ fillColor: u32, strokeColor: u32, strokeWidth: f32,
792
+ ): void;
793
+
794
+ @external("fui_host", "fui_canvas_draw_circle")
795
+ export declare function fui_canvas_draw_circle(
796
+ canvasPtr: usize, cx: f32, cy: f32, radius: f32,
797
+ fillColor: u32, strokeColor: u32, strokeWidth: f32,
798
+ ): void;
799
+
800
+ @external("fui_host", "fui_canvas_draw_line")
801
+ export declare function fui_canvas_draw_line(
802
+ canvasPtr: usize, x1: f32, y1: f32, x2: f32, y2: f32,
803
+ color: u32, strokeWidth: f32,
804
+ ): void;
805
+
806
+ @external("fui_host", "fui_canvas_draw_round_rect")
807
+ export declare function fui_canvas_draw_round_rect(
808
+ canvasPtr: usize, x: f32, y: f32, w: f32, h: f32,
809
+ rx: f32, ry: f32, fillColor: u32, strokeColor: u32, strokeWidth: f32,
810
+ ): void;
811
+
812
+ @external("fui_host", "fui_path_create")
813
+ export declare function fui_path_create(): u32;
814
+
815
+ @external("fui_host", "fui_path_destroy")
816
+ export declare function fui_path_destroy(pathId: u32): void;
817
+
818
+ @external("fui_host", "fui_path_move_to")
819
+ export declare function fui_path_move_to(pathId: u32, x: f32, y: f32): void;
820
+
821
+ @external("fui_host", "fui_path_line_to")
822
+ export declare function fui_path_line_to(pathId: u32, x: f32, y: f32): void;
823
+
824
+ @external("fui_host", "fui_path_quad_to")
825
+ export declare function fui_path_quad_to(pathId: u32, cx: f32, cy: f32, x: f32, y: f32): void;
826
+
827
+ @external("fui_host", "fui_path_cubic_to")
828
+ export declare function fui_path_cubic_to(
829
+ pathId: u32, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32,
830
+ ): void;
831
+
832
+ @external("fui_host", "fui_path_close")
833
+ export declare function fui_path_close(pathId: u32): void;
834
+
835
+ @external("fui_host", "fui_path_add_rect")
836
+ export declare function fui_path_add_rect(pathId: u32, x: f32, y: f32, w: f32, h: f32): void;
837
+
838
+ @external("fui_host", "fui_path_add_circle")
839
+ export declare function fui_path_add_circle(pathId: u32, cx: f32, cy: f32, r: f32): void;
840
+
841
+ @external("fui_host", "fui_canvas_draw_path")
842
+ export declare function fui_canvas_draw_path(
843
+ canvasPtr: usize, pathId: u32,
844
+ fillColor: u32, strokeColor: u32, strokeWidth: f32,
845
+ ): void;
846
+
847
+ @external("fui_host", "fui_canvas_draw_text")
848
+ export declare function fui_canvas_draw_text(
849
+ canvasPtr: usize, utf8Ptr: usize, utf8Len: u32, x: f32, y: f32,
850
+ fontId: u32, fontSize: f32, color: u32,
851
+ ): void;
852
+
853
+ @external("fui_host", "fui_canvas_draw_image")
854
+ export declare function fui_canvas_draw_image(
855
+ canvasPtr: usize, textureId: u32, x: f32, y: f32, w: f32, h: f32,
856
+ ): void;
857
+
858
+ @external("fui_host", "fui_canvas_draw_svg")
859
+ export declare function fui_canvas_draw_svg(
860
+ canvasPtr: usize, svgId: u32, x: f32, y: f32, w: f32, h: f32,
861
+ ): void;
862
+
863
+ @external("fui_host", "fui_canvas_create_offscreen")
864
+ export declare function fui_canvas_create_offscreen(width: u32, height: u32): u32;
865
+
866
+ @external("fui_host", "fui_canvas_get_offscreen_ptr")
867
+ export declare function fui_canvas_get_offscreen_ptr(offscreenId: u32): usize;
868
+
869
+ @external("fui_host", "fui_canvas_read_offscreen_pixels")
870
+ export declare function fui_canvas_read_offscreen_pixels(offscreenId: u32, outPtr: usize, width: u32, height: u32): void;
871
+
872
+ @external("fui_host", "fui_canvas_destroy_offscreen")
873
+ export declare function fui_canvas_destroy_offscreen(offscreenId: u32): void;
@@ -0,0 +1,18 @@
1
+ import { DrawContext } from "./DrawContext";
2
+ import { CustomDrawable } from "../nodes/CustomDrawable";
3
+ import { EventRouter } from "../core/EventRouter";
4
+
5
+ /**
6
+ * Called from the JS bridge during the render pass when Tier 1
7
+ * encounters an OP_DRAW_CUSTOM instruction.
8
+ *
9
+ * @param handle - The u64 node handle (passed as i64 from JS).
10
+ * @param canvasPtr - The opaque Skia canvas pointer (usize).
11
+ */
12
+ export function fui_dispatch_custom_draw(handle: u64, canvasPtr: usize): void {
13
+ const node = EventRouter.getRegisteredNode(handle);
14
+ if (node !== null && node instanceof CustomDrawable) {
15
+ const ctx = new DrawContext(canvasPtr);
16
+ (node as CustomDrawable).draw(ctx);
17
+ }
18
+ }
@@ -0,0 +1,81 @@
1
+ import * as ffi from "../core/ffi";
2
+ import { Paint } from "./Paint";
3
+ import { Path } from "./Path";
4
+
5
+ /**
6
+ * Immediate-mode drawing context wrapping an opaque Skia canvas pointer.
7
+ *
8
+ * Obtained during the render pass via `CustomDrawable.draw(ctx)` or from
9
+ * `Bitmap.canvas()` for off-screen drawing. The canvas pointer is opaque —
10
+ * all drawing goes through the `fui_canvas_*` FFI functions.
11
+ */
12
+ export class DrawContext {
13
+ constructor(private canvasPtr: usize) {}
14
+
15
+ /* ── State ──────────────────────────────────────────────────── */
16
+
17
+ save(): void { ffi.fui_canvas_save(this.canvasPtr); }
18
+ restore(): void { ffi.fui_canvas_restore(this.canvasPtr); }
19
+ translate(x: f32, y: f32): void { ffi.fui_canvas_translate(this.canvasPtr, x, y); }
20
+ scale(sx: f32, sy: f32): void { ffi.fui_canvas_scale(this.canvasPtr, sx, sy); }
21
+ rotate(degrees: f32): void { ffi.fui_canvas_rotate(this.canvasPtr, degrees); }
22
+ clipRect(x: f32, y: f32, w: f32, h: f32): void {
23
+ ffi.fui_canvas_clip_rect(this.canvasPtr, x, y, w, h);
24
+ }
25
+
26
+ /* ── Primitives ─────────────────────────────────────────────── */
27
+
28
+ drawRect(x: f32, y: f32, w: f32, h: f32, paint: Paint): void {
29
+ ffi.fui_canvas_draw_rect(
30
+ this.canvasPtr, x, y, w, h,
31
+ paint.fillColor, paint.strokeColor, paint.strokeWidth,
32
+ );
33
+ }
34
+
35
+ drawCircle(cx: f32, cy: f32, radius: f32, paint: Paint): void {
36
+ ffi.fui_canvas_draw_circle(
37
+ this.canvasPtr, cx, cy, radius,
38
+ paint.fillColor, paint.strokeColor, paint.strokeWidth,
39
+ );
40
+ }
41
+
42
+ drawLine(x1: f32, y1: f32, x2: f32, y2: f32, color: u32, strokeWidth: f32): void {
43
+ ffi.fui_canvas_draw_line(this.canvasPtr, x1, y1, x2, y2, color, strokeWidth);
44
+ }
45
+
46
+ drawRoundRect(x: f32, y: f32, w: f32, h: f32, rx: f32, ry: f32, paint: Paint): void {
47
+ ffi.fui_canvas_draw_round_rect(
48
+ this.canvasPtr, x, y, w, h, rx, ry,
49
+ paint.fillColor, paint.strokeColor, paint.strokeWidth,
50
+ );
51
+ }
52
+
53
+ /* ── Path ───────────────────────────────────────────────────── */
54
+
55
+ drawPath(path: Path, paint: Paint): void {
56
+ ffi.fui_canvas_draw_path(
57
+ this.canvasPtr, path._pathId,
58
+ paint.fillColor, paint.strokeColor, paint.strokeWidth,
59
+ );
60
+ }
61
+
62
+ /* ── Text ───────────────────────────────────────────────────── */
63
+
64
+ drawText(text: string, x: f32, y: f32, fontId: u32, fontSize: f32, color: u32): void {
65
+ const encoded = Uint8Array.wrap(String.UTF8.encode(text, false));
66
+ ffi.fui_canvas_draw_text(
67
+ this.canvasPtr, encoded.dataStart, encoded.byteLength,
68
+ x, y, fontId, fontSize, color,
69
+ );
70
+ }
71
+
72
+ /* ── Image / SVG ─────────────────────────────────────────────── */
73
+
74
+ drawImage(textureId: u32, x: f32, y: f32, w: f32, h: f32): void {
75
+ ffi.fui_canvas_draw_image(this.canvasPtr, textureId, x, y, w, h);
76
+ }
77
+
78
+ drawSvg(svgId: u32, x: f32, y: f32, w: f32, h: f32): void {
79
+ ffi.fui_canvas_draw_svg(this.canvasPtr, svgId, x, y, w, h);
80
+ }
81
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Paint descriptor for DrawContext drawing operations.
3
+ *
4
+ * Models fill and stroke properties as a lightweight value type.
5
+ * Color encoding is 0xRRGGBBAA (matching the repo's Color type).
6
+ */
7
+ export class Paint {
8
+ constructor(
9
+ public fillColor: u32 = 0, // 0xRRGGBBAA; 0 = transparent (no fill)
10
+ public strokeColor: u32 = 0, // 0xRRGGBBAA; 0 = no stroke
11
+ public strokeWidth: f32 = 0, // stroke width in logical pixels
12
+ ) {}
13
+
14
+ static fill(color: u32): Paint {
15
+ return new Paint(color, 0, 0);
16
+ }
17
+
18
+ static stroke(color: u32, width: f32): Paint {
19
+ return new Paint(0, color, width);
20
+ }
21
+
22
+ static filledStroke(fillColor: u32, strokeColor: u32, strokeWidth: f32): Paint {
23
+ return new Paint(fillColor, strokeColor, strokeWidth);
24
+ }
25
+
26
+ /** Returns true if this paint produces any visible output. */
27
+ hasFill(): bool {
28
+ return (this.fillColor & 0xff) !== 0;
29
+ }
30
+
31
+ hasStroke(): bool {
32
+ return this.strokeWidth > 0 && (this.strokeColor & 0xff) !== 0;
33
+ }
34
+ }
@@ -0,0 +1,40 @@
1
+ import * as ffi from "../core/ffi";
2
+ import { Disposable } from "../core/Disposable";
3
+
4
+ /**
5
+ * Retained Skia path, built imperatively then drawn via DrawContext.
6
+ *
7
+ * Callers must `dispose()` the path when done to release the engine-side resource.
8
+ * Paths are pooled in Tier 1 and identified by an opaque u32 ID.
9
+ */
10
+ export class Path implements Disposable {
11
+ // @ts-ignore: assigned in constructor via FFI
12
+ private _id: u32;
13
+ private disposed: bool = false;
14
+
15
+ constructor() {
16
+ this._id = ffi.fui_path_create();
17
+ }
18
+
19
+ /** @internal — exposed for DrawContext.drawPath */
20
+ get _pathId(): u32 {
21
+ return this._id;
22
+ }
23
+
24
+ moveTo(x: f32, y: f32): Path { ffi.fui_path_move_to(this._id, x, y); return this; }
25
+ lineTo(x: f32, y: f32): Path { ffi.fui_path_line_to(this._id, x, y); return this; }
26
+ quadTo(cx: f32, cy: f32, x: f32, y: f32): Path { ffi.fui_path_quad_to(this._id, cx, cy, x, y); return this; }
27
+ cubicTo(cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32): Path {
28
+ ffi.fui_path_cubic_to(this._id, cx1, cy1, cx2, cy2, x, y); return this;
29
+ }
30
+ close(): Path { ffi.fui_path_close(this._id); return this; }
31
+ addRect(x: f32, y: f32, w: f32, h: f32): Path { ffi.fui_path_add_rect(this._id, x, y, w, h); return this; }
32
+ addCircle(cx: f32, cy: f32, r: f32): Path { ffi.fui_path_add_circle(this._id, cx, cy, r); return this; }
33
+
34
+ dispose(): void {
35
+ if (!this.disposed) {
36
+ ffi.fui_path_destroy(this._id);
37
+ this.disposed = true;
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,54 @@
1
+ import { FlexBox } from "./FlexBox";
2
+ import { DrawContext } from "../drawing/DrawContext";
3
+ import * as ui from "../bindings/ui";
4
+ import { markNeedsCommit } from "../core/FrameScheduler";
5
+
6
+ /**
7
+ * A retained-mode node that draws itself procedurally via a `DrawContext`
8
+ * during the render pass. Extends `FlexBox` for full layout, styling, and
9
+ * child support.
10
+ *
11
+ * Subclasses override `draw(ctx)` with immediate-mode Skia drawing.
12
+ *
13
+ * **Dirty model:** EffinDom only repaints when something is dirty — there
14
+ * is no automatic 60fps repaint loop. A `CustomDrawable` renders once on
15
+ * the initial frame, then only again when `markDirty()` is called. For
16
+ * animations, rebind a `Signal` or timer to call `markDirty()` each frame.
17
+ *
18
+ * ```
19
+ * class Gauge extends CustomDrawable {
20
+ * readonly value = new Signal<f32>(0);
21
+ *
22
+ * constructor() {
23
+ * super();
24
+ * this.width(200).height(200);
25
+ * this.value.bind(this, () => this.markDirty());
26
+ * }
27
+ *
28
+ * draw(ctx: DrawContext): void {
29
+ * // immediate-mode Skia drawing — called only when dirty
30
+ * }
31
+ * }
32
+ * ```
33
+ */
34
+ export abstract class CustomDrawable extends FlexBox {
35
+ abstract draw(ctx: DrawContext): void;
36
+
37
+ build(): u64 {
38
+ this.buildStyledNode(0); // 0 = NodeType.FlexBox
39
+ ui.setCustomDrawable(this.handle, true);
40
+ return this.handle;
41
+ }
42
+
43
+ /**
44
+ * Request a redraw on the next frame.
45
+ *
46
+ * Call this whenever the visual state changes (e.g. a bound `Signal`
47
+ * fires, a timer ticks, or a layout mutation occurs). Without this
48
+ * call the `draw()` method will not be invoked again after the initial
49
+ * render.
50
+ */
51
+ markDirty(): void {
52
+ markNeedsCommit();
53
+ }
54
+ }
@@ -14,6 +14,7 @@ import {
14
14
  AlignItems,
15
15
  BorderStyle,
16
16
  FlexDirection,
17
+ FlexWrap,
17
18
  HandleValue,
18
19
  JustifyContent,
19
20
  NodeType,
@@ -127,6 +128,8 @@ export class FlexBox extends Node {
127
128
  private borderDashOn: f32 = 0.0;
128
129
  private borderDashOff: f32 = 0.0;
129
130
  private flexDirectionValue: FlexDirection = FlexDirection.Column;
131
+ private hasFlexWrap: bool = false;
132
+ private flexWrapValue: FlexWrap = FlexWrap.NoWrap;
130
133
  private hasFlexDirection: bool = false;
131
134
  private justifyContentValue: JustifyContent = JustifyContent.Start;
132
135
  private hasJustifyContent: bool = false;
@@ -395,6 +398,17 @@ export class FlexBox extends Node {
395
398
  return this;
396
399
  }
397
400
 
401
+ flexWrap(wrap: FlexWrap): this {
402
+ this.flexWrapValue = wrap;
403
+ this.hasFlexWrap = true;
404
+ if (this.hasBuiltHandle()) {
405
+ ui.setFlexWrap(this.handle, <u32>wrap);
406
+ this.notifyRetainedLayoutMutation();
407
+ this.onRetainedChildLayoutChanged();
408
+ }
409
+ return this;
410
+ }
411
+
398
412
  justifyContent(justify: JustifyContent): this {
399
413
  this.justifyContentValue = justify;
400
414
  this.hasJustifyContent = true;
@@ -676,6 +690,9 @@ export class FlexBox extends Node {
676
690
  if (this.hasFlexDirection) {
677
691
  ui.setFlexDirection(this.handle, <u32>this.flexDirectionValue);
678
692
  }
693
+ if (this.hasFlexWrap) {
694
+ ui.setFlexWrap(this.handle, <u32>this.flexWrapValue);
695
+ }
679
696
  if (this.hasFlexBasis) {
680
697
  ui.setFlexBasis(this.handle, this.flexBasisValue);
681
698
  }
@@ -1,3 +1,5 @@
1
+ export { Column, Row, pct, px } from "./helpers";
2
+ export { CustomDrawable } from "./CustomDrawable";
1
3
  export { FlexBox, FlexBoxProps } from "./FlexBox";
2
4
  export { GradientStop } from "./GradientStop";
3
5
  export { Grid } from "./Grid";