@effindomv2/fui-as 0.1.35 → 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.
- package/browser/src/common-harness/managed-harness-canvas-host.ts +72 -0
- package/browser/src/common-harness/managed-harness.ts +21 -0
- package/browser/src/common-harness/types.ts +1 -0
- package/browser/src/common-harness/ui-imports.ts +6 -0
- package/package.json +5 -5
- package/scripts/build.sh +4 -1
- package/src/Fui.ts +6 -0
- package/src/FuiExports.ts +1 -0
- package/src/bindings/ui.ts +8 -0
- package/src/controls/ContextMenu.ts +1 -1
- package/src/core/Application.ts +2 -1
- package/src/core/Bitmap.ts +29 -0
- package/src/core/FrameScheduler.ts +27 -0
- package/src/core/Node.ts +3 -1
- package/src/core/ffi.ts +119 -0
- package/src/drawing/DrawCallback.ts +18 -0
- package/src/drawing/DrawContext.ts +81 -0
- package/src/drawing/Paint.ts +34 -0
- package/src/drawing/Path.ts +40 -0
- package/src/nodes/CustomDrawable.ts +54 -0
- package/src/nodes/FlexBox.ts +17 -0
- package/src/nodes/index.ts +2 -0
|
@@ -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.
|
|
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.
|
|
89
|
-
"@
|
|
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.
|
|
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
package/src/bindings/ui.ts
CHANGED
|
@@ -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,
|
package/src/core/Application.ts
CHANGED
|
@@ -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
|
}
|
package/src/core/Bitmap.ts
CHANGED
|
@@ -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/Node.ts
CHANGED
|
@@ -991,6 +991,8 @@ export abstract class Node implements DragGestureHost, Disposable {
|
|
|
991
991
|
return this;
|
|
992
992
|
}
|
|
993
993
|
|
|
994
|
+
/// Removes `node` from this parent. Does NOT call `dispose()` — the C++ handle
|
|
995
|
+
/// stays alive so callers can re-parent with `addChildNode()`.
|
|
994
996
|
removeChildNode(node: Node): this {
|
|
995
997
|
const index = this.indexOfChild(node);
|
|
996
998
|
if (index < 0) {
|
|
@@ -1737,4 +1739,4 @@ export abstract class Node implements DragGestureHost, Disposable {
|
|
|
1737
1739
|
}
|
|
1738
1740
|
}
|
|
1739
1741
|
|
|
1740
|
-
}
|
|
1742
|
+
}
|
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
|
+
}
|
package/src/nodes/FlexBox.ts
CHANGED
|
@@ -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
|
}
|
package/src/nodes/index.ts
CHANGED