@effindomv2/fui-as 0.1.50 → 0.2.0-alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @effindomv2/fui-as
2
+
3
+ EffinDOM's retained-mode UI SDK for AssemblyScript and the browser.
4
+
5
+ ## Quick start
6
+
7
+ Create a new application with the official scaffold:
8
+
9
+ ```bash
10
+ npx @effindomv2/create-fui-as-app my-app
11
+ cd my-app
12
+ npm install
13
+ npm run dev
14
+ ```
15
+
16
+ The scaffold includes host-service generation, a routed application option, the EffinDOM browser harness, and a production build.
17
+
18
+ ## Install in an existing application
19
+
20
+ ```bash
21
+ npm install @effindomv2/fui-as
22
+ ```
23
+
24
+ Build an application WASM module with AssemblyScript, then mount it with the generated FUI-AS browser harness. See the package documentation and scaffolded application for the complete setup.
25
+
26
+ `@effindomv2/fui-as` uses an exact, tested `@effindomv2/runtime` dependency. The runtime first attempts to reuse immutable assets from `https://runtimes.effindom.dev`, then falls back to the runtime assets published with the application.
27
+
28
+ ## Links
29
+
30
+ - [FUI-AS repository](https://github.com/zion-sati/fui-as)
31
+ - [EffinDOM runtime](https://www.npmjs.com/package/@effindomv2/runtime)
32
+ - [Create FUI-AS App](https://www.npmjs.com/package/@effindomv2/create-fui-as-app)
33
+
34
+ ## License
35
+
36
+ AGPL-3.0-only or a commercial EffinDOM license. See `LICENSE.md` and `COMMERCIAL.md`.
Binary file
@@ -0,0 +1,39 @@
1
+ import {
2
+ startHarness as startRuntimeHarness,
3
+ startManagedHarness as startRuntimeManagedHarness,
4
+ type HarnessExports,
5
+ type HarnessOptions,
6
+ type ManagedHarnessOptions,
7
+ type WasmAppInstantiator,
8
+ } from '@effindomv2/runtime/managed-harness';
9
+
10
+ /**
11
+ * Instantiates an AssemblyScript application with its language-level abort
12
+ * import. This belongs to FUI-AS rather than the framework-agnostic runtime.
13
+ */
14
+ export const instantiateAssemblyScriptApp: WasmAppInstantiator = async (module, imports) => {
15
+ const environment = imports.env;
16
+ return WebAssembly.instantiate(module, {
17
+ ...imports,
18
+ env: {
19
+ ...environment,
20
+ abort(_message?: number, _fileName?: number, line?: number, column?: number): never {
21
+ throw new Error(`AssemblyScript application aborted at ${String(line ?? 0)}:${String(column ?? 0)}.`);
22
+ },
23
+ },
24
+ });
25
+ };
26
+
27
+ export function startHarness<Exports extends HarnessExports>(options: HarnessOptions<Exports>): void {
28
+ startRuntimeHarness({
29
+ ...options,
30
+ instantiateApp: instantiateAssemblyScriptApp,
31
+ });
32
+ }
33
+
34
+ export function startManagedHarness(options: ManagedHarnessOptions): void {
35
+ startRuntimeManagedHarness({
36
+ ...options,
37
+ instantiateApp: instantiateAssemblyScriptApp,
38
+ });
39
+ }
@@ -1,6 +1,9 @@
1
1
  export {
2
2
  startHarness,
3
3
  startManagedHarness,
4
+ } from './assemblyscript-harness';
5
+
6
+ export {
4
7
  canManagedNavigateBack,
5
8
  canManagedNavigateForward,
6
9
  pushManagedHistoryEntry,
@@ -1 +1,25 @@
1
- export * from './shared-browser';
1
+ import {
2
+ startRoutedHarness as startRuntimeRoutedHarness,
3
+ } from '@effindomv2/runtime/routed-harness';
4
+ import type { HarnessExports } from '@effindomv2/runtime/managed-harness';
5
+ import type {
6
+ RoutedHarnessConfig,
7
+ RoutedHarnessRoute,
8
+ } from '@effindomv2/runtime/routed-harness';
9
+ import { instantiateAssemblyScriptApp } from './assemblyscript-harness';
10
+
11
+ export type {
12
+ RoutedHarnessConfig,
13
+ RoutedHarnessManagerState,
14
+ RoutedHarnessRoute,
15
+ } from '@effindomv2/runtime/routed-harness';
16
+
17
+ export function startRoutedHarness<
18
+ TExports extends HarnessExports,
19
+ TRoute extends RoutedHarnessRoute = RoutedHarnessRoute,
20
+ >(config: RoutedHarnessConfig<TExports, TRoute>): void {
21
+ startRuntimeRoutedHarness({
22
+ ...config,
23
+ instantiateApp: instantiateAssemblyScriptApp,
24
+ });
25
+ }
@@ -1,3 +1,6 @@
1
+ // Preserve the complete runtime browser API for FUI-AS consumers. The two
2
+ // application mount entry points are explicitly replaced below so
3
+ // AssemblyScript apps always receive their language-level imports.
1
4
  export * from '@effindomv2/runtime/managed-harness';
2
5
  export * from '@effindomv2/runtime/routed-app-conventions';
3
- export * from '@effindomv2/runtime/routed-harness';
6
+ export { startHarness, startManagedHarness } from './assemblyscript-harness';
package/package.json CHANGED
@@ -1,9 +1,21 @@
1
1
  {
2
2
  "name": "@effindomv2/fui-as",
3
- "version": "0.1.50",
3
+ "version": "0.2.0-alpha1",
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",
7
+ "repository": "https://github.com/zion-sati/fui-as",
8
+ "keywords": [
9
+ "effindom",
10
+ "assemblyscript",
11
+ "webassembly",
12
+ "wasm",
13
+ "retained-mode",
14
+ "ui-framework",
15
+ "canvas-ui",
16
+ "skia",
17
+ "accessibility"
18
+ ],
7
19
  "type": "module",
8
20
  "publishConfig": {
9
21
  "access": "public"
@@ -70,8 +82,6 @@
70
82
  "src",
71
83
  "browser/routed-app-conventions.ts",
72
84
  "browser/favicon.ico",
73
- "browser/loading-overlay-styles.html",
74
- "browser/loading-overlay-body.html",
75
85
  "browser/src",
76
86
  "scripts",
77
87
  "LICENSE.md",
@@ -95,8 +105,7 @@
95
105
  "typecheck": "npm run guard:runtime-dependency && npm run check:abi && tsc -p tsconfig.json --noEmit"
96
106
  },
97
107
  "dependencies": {
98
- "@assemblyscript/loader": "^0.28.19",
99
- "@effindomv2/runtime": "0.1.26"
108
+ "@effindomv2/runtime": "0.2.2"
100
109
  },
101
110
  "devDependencies": {
102
111
  "@as-pect/assembly": "9.0.0",
package/scripts/build.sh CHANGED
@@ -20,8 +20,8 @@ HOST_EVENT_GENERATOR_BUILD="${PACKAGE_DIR}/build/generate-host-events.mjs"
20
20
  ABI_GENERATOR_BUILD="${PACKAGE_DIR}/build/generate-abi.mjs"
21
21
  RUNTIME_CONFIG_FILE="effindom-runtime-config.js"
22
22
  DEFAULT_MANIFEST_PATH="./runtime/dist/effindom.v2.manifest.json"
23
- LOADING_OVERLAY_STYLES_FILE="${PACKAGE_DIR}/browser/loading-overlay-styles.html"
24
- LOADING_OVERLAY_BODY_FILE="${PACKAGE_DIR}/browser/loading-overlay-body.html"
23
+ LOADING_OVERLAY_STYLES_FILE="${REPO_ROOT}/v2/browser-bridge/src/managed-harness/loading-overlay-styles.css"
24
+ LOADING_OVERLAY_BODY_FILE="${REPO_ROOT}/v2/browser-bridge/src/managed-harness/loading-overlay-body.html"
25
25
  RUNTIME_PACKAGE_DIR="${PACKAGE_DIR}/node_modules/@effindomv2/runtime"
26
26
  FILE_PROCESSING_WORKER_SOURCE="${REPO_ROOT}/v2/browser-bridge/src/managed-harness/file-processing-worker.ts"
27
27
  WORKER_BOOTSTRAP_SOURCE="${REPO_ROOT}/v2/browser-bridge/src/managed-harness/worker-bootstrap.ts"
@@ -29,6 +29,12 @@ WORKER_BOOTSTRAP_SOURCE="${REPO_ROOT}/v2/browser-bridge/src/managed-harness/work
29
29
  if [ ! -d "${RUNTIME_PACKAGE_DIR}" ]; then
30
30
  RUNTIME_PACKAGE_DIR="${REPO_ROOT}/node_modules/@effindomv2/runtime"
31
31
  fi
32
+ if [ ! -f "${LOADING_OVERLAY_STYLES_FILE}" ]; then
33
+ LOADING_OVERLAY_STYLES_FILE="${RUNTIME_PACKAGE_DIR}/src/managed-harness/loading-overlay-styles.css"
34
+ fi
35
+ if [ ! -f "${LOADING_OVERLAY_BODY_FILE}" ]; then
36
+ LOADING_OVERLAY_BODY_FILE="${RUNTIME_PACKAGE_DIR}/src/managed-harness/loading-overlay-body.html"
37
+ fi
32
38
  if [ ! -f "${FILE_PROCESSING_WORKER_SOURCE}" ]; then
33
39
  FILE_PROCESSING_WORKER_SOURCE="${RUNTIME_PACKAGE_DIR}/src/managed-harness/file-processing-worker.ts"
34
40
  fi
@@ -192,9 +198,9 @@ resolve_runtime_dist_dir() {
192
198
  )
193
199
 
194
200
  if [ -f "${public_manifest}" ] && [ -f "${package_manifest}" ] && ! cmp -s "${public_manifest}" "${package_manifest}"; then
195
- echo "Note: using ${public_runtime_dir} for monorepo runtime assets." >&2
201
+ echo "Note: using ${public_runtime_dir} for local runtime assets." >&2
196
202
  echo " ${package_runtime_dir} is a staged package copy and may be stale." >&2
197
- echo " For ABI changes, run repo-root ./build.sh (or npm run build:v2:abi)." >&2
203
+ echo " Rebuild the local runtime assets before changing the ABI." >&2
198
204
  fi
199
205
 
200
206
  for candidate in "${candidates[@]}"; do
@@ -211,12 +217,13 @@ resolve_runtime_dist_dir() {
211
217
  echo " - ${REPO_ROOT}/node_modules/@effindomv2/runtime/dist" >&2
212
218
  echo " - ${REPO_ROOT}/public/v2/browser-bridge" >&2
213
219
  echo " - ${REPO_ROOT}/v2/browser-bridge/dist" >&2
214
- echo "Monorepo note: repo-root ./build.sh (or npm run build:v2:abi) refreshes the runtime assets used here." >&2
220
+ echo "Build the local runtime assets or install @effindomv2/runtime first." >&2
215
221
  echo "Install @effindomv2/runtime or build runtime assets first." >&2
216
222
  exit 1
217
223
  }
218
224
 
219
225
  RUNTIME_DIST_DIR="$(resolve_runtime_dist_dir)"
226
+ RUNTIME_SET_HASH="$(node -e 'const fs=require("fs"); const value=JSON.parse(fs.readFileSync(process.argv[1], "utf8")).runtime_set_hash; if (typeof value !== "string" || value.length === 0) process.exit(1); process.stdout.write(value);' "${RUNTIME_DIST_DIR}/effindom.v2.manifest.json")"
220
227
 
221
228
  write_runtime_config() {
222
229
  local destination="$1"
@@ -224,7 +231,11 @@ write_runtime_config() {
224
231
 
225
232
  cat > "${destination}/${RUNTIME_CONFIG_FILE}" <<EOF
226
233
  window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, {
227
- manifestUrl: new URL('${manifest_url}', document.currentScript && document.currentScript.src ? document.currentScript.src : document.baseURI).toString(),
234
+ manifestUrls: [
235
+ 'https://runtimes.effindom.dev/v2/manifests/${RUNTIME_SET_HASH}.json',
236
+ new URL('${manifest_url}', document.currentScript && document.currentScript.src ? document.currentScript.src : document.baseURI).toString(),
237
+ ],
238
+ expectedRuntimeSetHash: '${RUNTIME_SET_HASH}',
228
239
  buildMode: 'debug',
229
240
  });
230
241
  EOF
@@ -264,15 +275,26 @@ copy_runtime_assets() {
264
275
  rm -rf "${destination}/runtime"
265
276
  mkdir -p "${destination}/runtime/dist"
266
277
  cp "${RUNTIME_DIST_DIR}/effindom.v2.manifest.json" "${destination}/runtime/dist/effindom.v2.manifest.json"
278
+ MANIFEST_PATH="${destination}/runtime/dist/effindom.v2.manifest.json" node <<'NODE'
279
+ const fs = require('node:fs');
280
+ const manifestPath = process.env.MANIFEST_PATH;
281
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
282
+ for (const font of Object.values(manifest.assets?.fonts ?? {})) {
283
+ if (typeof font.url === 'string' && font.url.startsWith('../fonts/')) {
284
+ font.url = `./fonts/${font.url.slice('../fonts/'.length)}`;
285
+ }
286
+ }
287
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
288
+ NODE
267
289
  if [ -f "${RUNTIME_DIST_DIR}/icu-asset.json" ]; then
268
290
  cp "${RUNTIME_DIST_DIR}/icu-asset.json" "${destination}/runtime/dist/icu-asset.json"
269
291
  fi
270
292
  cp -R "${RUNTIME_DIST_DIR}/runtime" "${destination}/runtime/dist/runtime"
271
- mkdir -p "${destination}/runtime/fonts"
293
+ mkdir -p "${destination}/runtime/dist/fonts"
272
294
  if [ -d "${REPO_ROOT}/public/v2/fonts" ]; then
273
- cp -R "${REPO_ROOT}/public/v2/fonts/." "${destination}/runtime/fonts/"
295
+ cp -R "${REPO_ROOT}/public/v2/fonts/." "${destination}/runtime/dist/fonts/"
274
296
  elif [ -d "${RUNTIME_DIST_DIR}/fonts" ]; then
275
- cp -R "${RUNTIME_DIST_DIR}/fonts/." "${destination}/runtime/fonts/"
297
+ cp -R "${RUNTIME_DIST_DIR}/fonts/." "${destination}/runtime/dist/fonts/"
276
298
  fi
277
299
  }
278
300
 
@@ -30,6 +30,18 @@ export const frameworkHostServices = defineHostServices({
30
30
  returns: "u32",
31
31
  implementation: () => unsupported(),
32
32
  }),
33
+ hostEnvironment: hostService({
34
+ importName: "fui_get_host_environment",
35
+ args: [] as const,
36
+ returns: "u32",
37
+ implementation: () => unsupported(),
38
+ }),
39
+ hostCapabilities: hostService({
40
+ importName: "fui_get_host_capabilities",
41
+ args: [] as const,
42
+ returns: "u32",
43
+ implementation: () => unsupported(),
44
+ }),
33
45
  isCoarsePointer: hostService({
34
46
  importName: "fui_is_coarse_pointer",
35
47
  args: [] as const,
package/src/Fui.ts CHANGED
@@ -173,7 +173,18 @@ export { Worker, WorkerCompletedEventArgs, WorkerErrorEventArgs, WorkerProgressE
173
173
  export { Worker as WorkerRuntime } from "./worker/Worker";
174
174
  export { WorkerJob } from "./worker/WorkerJob";
175
175
  export { currentRoute, navigateTo } from "./core/Navigation";
176
- export { getPlatformFamily, hasPrimaryShortcutModifier, PlatformFamily, resolvePrimaryShortcutModifier } from "./core/Platform";
176
+ export {
177
+ getHostContext,
178
+ getHostEnvironment,
179
+ getPlatformFamily,
180
+ hasHostCapability,
181
+ hasPrimaryShortcutModifier,
182
+ HostCapability,
183
+ HostContext,
184
+ HostEnvironment,
185
+ PlatformFamily,
186
+ resolvePrimaryShortcutModifier,
187
+ } from "./core/Platform";
177
188
  export {
178
189
  Colors,
179
190
  ContextMenuItemTheme,
@@ -74,10 +74,6 @@ export class Button extends FlexBox {
74
74
  private readonly disposables: Array<Disposable> = new Array<Disposable>();
75
75
  private action: ((event: ClickEventArgs) => void) | null = null;
76
76
  private actionBinding: Callback1<ClickEventArgs> | null = null;
77
- private doubleAction: ((event: ClickEventArgs) => void) | null = null;
78
- private doubleActionBinding: Callback1<ClickEventArgs> | null = null;
79
- private tripleAction: ((event: ClickEventArgs) => void) | null = null;
80
- private tripleActionBinding: Callback1<ClickEventArgs> | null = null;
81
77
  private hoverChanged: ((event: HoverChangedEventArgs) => void) | null = null;
82
78
  private hoverChangedBinding: Callback1<HoverChangedEventArgs> | null = null;
83
79
  private disposed: bool = false;
@@ -171,40 +167,6 @@ export class Button extends FlexBox {
171
167
  return this;
172
168
  }
173
169
 
174
- onDoubleClick(cb: (event: ClickEventArgs) => void): this {
175
- this.doubleAction = cb;
176
- this.doubleActionBinding = null;
177
- return this;
178
- }
179
-
180
- bindDoubleClick<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
181
- this.doubleAction = null;
182
- this.doubleActionBinding = bind1<Owner, ClickEventArgs>(owner, handler);
183
- return this;
184
- }
185
-
186
- onDoubleClickWith<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
187
- this.bindDoubleClick(owner, handler);
188
- return this;
189
- }
190
-
191
- onTripleClick(cb: (event: ClickEventArgs) => void): this {
192
- this.tripleAction = cb;
193
- this.tripleActionBinding = null;
194
- return this;
195
- }
196
-
197
- bindTripleClick<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
198
- this.tripleAction = null;
199
- this.tripleActionBinding = bind1<Owner, ClickEventArgs>(owner, handler);
200
- return this;
201
- }
202
-
203
- onTripleClickWith<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
204
- this.bindTripleClick(owner, handler);
205
- return this;
206
- }
207
-
208
170
  onHoverChanged(cb: (event: HoverChangedEventArgs) => void): this {
209
171
  this.hoverChanged = cb;
210
172
  this.hoverChangedBinding = null;
@@ -369,7 +331,7 @@ export class Button extends FlexBox {
369
331
  this.pressed.value = true;
370
332
  }
371
333
 
372
- endPress(clickCount: i32 = 1, fireAction: bool = true): void {
334
+ endPress(fireAction: bool = true): void {
373
335
  const wasPressed = this.pressed.value;
374
336
  this.pressed.value = false;
375
337
  if (!wasPressed || !fireAction) {
@@ -383,26 +345,6 @@ export class Button extends FlexBox {
383
345
  if (binding !== null) {
384
346
  binding.invoke(ClickEventArgs.Empty);
385
347
  }
386
- if (clickCount == 2) {
387
- const doubleCallback = this.doubleAction;
388
- if (doubleCallback !== null) {
389
- doubleCallback(ClickEventArgs.Empty);
390
- }
391
- const doubleBinding = this.doubleActionBinding;
392
- if (doubleBinding !== null) {
393
- doubleBinding.invoke(ClickEventArgs.Empty);
394
- }
395
- }
396
- if (clickCount == 3) {
397
- const tripleCallback = this.tripleAction;
398
- if (tripleCallback !== null) {
399
- tripleCallback(ClickEventArgs.Empty);
400
- }
401
- const tripleBinding = this.tripleActionBinding;
402
- if (tripleBinding !== null) {
403
- tripleBinding.invoke(ClickEventArgs.Empty);
404
- }
405
- }
406
348
  }
407
349
 
408
350
  cancelPress(): void {
@@ -459,7 +401,7 @@ export class Button extends FlexBox {
459
401
  return;
460
402
  }
461
403
  if (eventType == PointerEventType.Up && this.pressed.value) {
462
- this.endPress(event === null || event.clickCount <= 0 ? 1 : event.clickCount, true);
404
+ this.endPress(true);
463
405
  }
464
406
  }
465
407
 
@@ -497,7 +439,7 @@ export class Button extends FlexBox {
497
439
  }
498
440
  if (eventType == KeyEventType.Up && this.keyboardArmedKey !== null && changetype<string>(this.keyboardArmedKey) == key) {
499
441
  this.keyboardArmedKey = null;
500
- this.endPress(1, true);
442
+ this.endPress(true);
501
443
  return true;
502
444
  }
503
445
  return callbackHandled;
@@ -1,6 +1,6 @@
1
1
  import { Callback1, Handler1 } from "../core/BoundCallback";
2
2
  import { SemanticCheckedState, SemanticRole } from "../core/ffi";
3
- import { CheckboxChangedEventArgs } from "../core/Node";
3
+ import { CheckboxChangedEventArgs, ClickEventArgs } from "../core/Node";
4
4
  import { PersistedInt32Codec, PersistedValueState } from "../core/PersistedState";
5
5
  import { bind1 } from "../core/bind";
6
6
  import { LabeledControlSizing } from "./ControlSizing";
@@ -124,6 +124,21 @@ export class Checkbox extends PressableLabeledControl {
124
124
  return this;
125
125
  }
126
126
 
127
+ onClick(callback: (event: ClickEventArgs) => void): this {
128
+ super.onClick(callback);
129
+ return this;
130
+ }
131
+
132
+ bindClick<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
133
+ super.bindClick(owner, handler);
134
+ return this;
135
+ }
136
+
137
+ onClickWith<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
138
+ super.onClickWith(owner, handler);
139
+ return this;
140
+ }
141
+
127
142
  bindChanged<Owner>(owner: Owner, handler: Handler1<Owner, CheckboxChangedEventArgs>): this {
128
143
  this.changedCallback = null;
129
144
  this.changedBinding = bind1<Owner, CheckboxChangedEventArgs>(owner, handler);
@@ -22,6 +22,7 @@ import { warn } from "../core/Logger";
22
22
  import { FontFamily, FontStyle, FontWeight } from "../core/Typography";
23
23
  import { Border, FlexBox, Grid, Portal, TextCore } from "../nodes";
24
24
  import { PopupPresenter } from "./internal/PopupPresenter";
25
+ import { getHostContext, HostCapability } from "../core/Platform";
25
26
 
26
27
  const MENU_WIDTH: f32 = 220.0;
27
28
  const MENU_SEPARATOR_HEIGHT: f32 = 9.0;
@@ -304,6 +305,17 @@ function commitFocusedTextActionIfNeeded(item: MenuItem): void {
304
305
  }
305
306
 
306
307
  export function runContextMenuAction(item: MenuItem): void {
308
+ const host = getHostContext();
309
+ if (
310
+ ((item.action == ContextMenuAction.NavigateBack || item.action == ContextMenuAction.NavigateForward) && !host.supports(HostCapability.BrowserHistory)) ||
311
+ (item.action == ContextMenuAction.ReloadPage && !host.supports(HostCapability.Reload)) ||
312
+ ((item.action == ContextMenuAction.OpenLinkInNewTab || item.action == ContextMenuAction.OpenImageInNewTab) && !host.supports(HostCapability.NewBrowsingContext)) ||
313
+ ((item.action == ContextMenuAction.OpenLink || item.action == ContextMenuAction.OpenImage) && !host.supports(HostCapability.OpenExternalUri)) ||
314
+ ((item.action == ContextMenuAction.CopyCurrentSelection || item.action == ContextMenuAction.CutTextSelection) && !host.supports(HostCapability.ClipboardWrite)) ||
315
+ (item.action == ContextMenuAction.PasteText && !host.supports(HostCapability.ClipboardRead))
316
+ ) {
317
+ return;
318
+ }
307
319
  if (item.disabled) {
308
320
  const actionNeedsLiveSelection =
309
321
  item.targetHandle != <u64>HandleValue.Invalid &&
@@ -512,6 +524,23 @@ export class ContextMenu extends Portal implements GlobalKeyHandler {
512
524
  }
513
525
  }
514
526
 
527
+ static dismissForOutsidePointerDown(sceneX: f32, sceneY: f32): bool {
528
+ const menu = ContextMenu.activeInstance;
529
+ if (menu === null || !menu.isMenuVisible) {
530
+ return false;
531
+ }
532
+ const bounds = menu.panel.getBounds();
533
+ const left = unchecked(bounds[0]);
534
+ const top = unchecked(bounds[1]);
535
+ const right = left + unchecked(bounds[2]);
536
+ const bottom = top + unchecked(bounds[3]);
537
+ if (sceneX >= left && sceneX <= right && sceneY >= top && sceneY <= bottom) {
538
+ return false;
539
+ }
540
+ menu.hide();
541
+ return true;
542
+ }
543
+
515
544
  static invokeActiveSlot(slot: i32): void {
516
545
  const menu = ContextMenu.activeInstance;
517
546
  if (menu !== null) {
@@ -1,6 +1,6 @@
1
1
  import { Callback1, Handler1 } from "../core/BoundCallback";
2
2
  import { KeyEventType, SemanticCheckedState, SemanticRole } from "../core/ffi";
3
- import { RadioButtonChangedEventArgs } from "../core/Node";
3
+ import { ClickEventArgs, RadioButtonChangedEventArgs } from "../core/Node";
4
4
  import { activeTheme } from "../core/Theme";
5
5
  import { bind1 } from "../core/bind";
6
6
  import { LabeledControlSizing } from "./ControlSizing";
@@ -87,6 +87,21 @@ export class RadioButton extends PressableLabeledControl {
87
87
  return this;
88
88
  }
89
89
 
90
+ onClick(callback: (event: ClickEventArgs) => void): this {
91
+ super.onClick(callback);
92
+ return this;
93
+ }
94
+
95
+ bindClick<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
96
+ super.bindClick(owner, handler);
97
+ return this;
98
+ }
99
+
100
+ onClickWith<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
101
+ super.onClickWith(owner, handler);
102
+ return this;
103
+ }
104
+
90
105
  bindChanged<Owner>(owner: Owner, handler: Handler1<Owner, RadioButtonChangedEventArgs>): this {
91
106
  this.changedCallback = null;
92
107
  this.changedBinding = bind1<Owner, RadioButtonChangedEventArgs>(owner, handler);
@@ -1,6 +1,6 @@
1
1
  import { Callback1, Handler1 } from "../core/BoundCallback";
2
2
  import { SemanticCheckedState, SemanticRole } from "../core/ffi";
3
- import { SwitchChangedEventArgs } from "../core/Node";
3
+ import { ClickEventArgs, SwitchChangedEventArgs } from "../core/Node";
4
4
  import { PersistedBoolCodec, PersistedValueState } from "../core/PersistedState";
5
5
  import { bind1 } from "../core/bind";
6
6
  import { activeTheme } from "../core/Theme";
@@ -92,6 +92,21 @@ export class Switch extends PressableLabeledControl {
92
92
  return this;
93
93
  }
94
94
 
95
+ onClick(callback: (event: ClickEventArgs) => void): this {
96
+ super.onClick(callback);
97
+ return this;
98
+ }
99
+
100
+ bindClick<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
101
+ super.bindClick(owner, handler);
102
+ return this;
103
+ }
104
+
105
+ onClickWith<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
106
+ super.onClickWith(owner, handler);
107
+ return this;
108
+ }
109
+
95
110
  bindChanged<Owner>(owner: Owner, handler: Handler1<Owner, SwitchChangedEventArgs>): this {
96
111
  this.changedCallback = null;
97
112
  this.changedBinding = bind1<Owner, SwitchChangedEventArgs>(owner, handler);
@@ -1,9 +1,11 @@
1
1
  import { rgba } from "../../color";
2
2
  import { HandlerAction } from "../../core/Action";
3
+ import { Callback1, Handler1 } from "../../core/BoundCallback";
4
+ import { bind1 } from "../../core/bind";
3
5
  import { Disposable, disposeAll } from "../../core/Disposable";
4
6
  import { FocusAdornerManager } from "../../core/FocusAdornerManager";
5
7
  import { keyboardFocusVisible } from "../../core/FocusVisibility";
6
- import { Node } from "../../core/Node";
8
+ import { ClickEventArgs, Node } from "../../core/Node";
7
9
  import {
8
10
  AlignItems,
9
11
  CursorStyle,
@@ -37,6 +39,8 @@ export class PressableLabeledControl extends FlexBox {
37
39
  protected focusedState: bool = false;
38
40
  private keyPressedState: bool = false;
39
41
  private pointerPressedState: bool = false;
42
+ private clickCallback: ((event: ClickEventArgs) => void) | null = null;
43
+ private clickBinding: Callback1<ClickEventArgs> | null = null;
40
44
 
41
45
  constructor(role: SemanticRole, label: string, indicatorPresenterRoot: FlexBox) {
42
46
  super();
@@ -106,6 +110,7 @@ export class PressableLabeledControl extends FlexBox {
106
110
  this.pressedState = false;
107
111
  this.syncVisualState();
108
112
  this.handleActivated();
113
+ this.fireSemanticClick();
109
114
  }
110
115
  }
111
116
 
@@ -125,6 +130,7 @@ export class PressableLabeledControl extends FlexBox {
125
130
  this.pressedState = false;
126
131
  this.syncVisualState();
127
132
  this.handleActivated();
133
+ this.fireSemanticClick();
128
134
  return true;
129
135
  }
130
136
  return callbackHandled;
@@ -157,6 +163,23 @@ export class PressableLabeledControl extends FlexBox {
157
163
 
158
164
  protected handleActivated(): void {}
159
165
 
166
+ onClick(callback: (event: ClickEventArgs) => void): this {
167
+ this.clickCallback = callback;
168
+ this.clickBinding = null;
169
+ return this;
170
+ }
171
+
172
+ bindClick<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
173
+ this.clickCallback = null;
174
+ this.clickBinding = bind1<Owner, ClickEventArgs>(owner, handler);
175
+ return this;
176
+ }
177
+
178
+ onClickWith<Owner>(owner: Owner, handler: Handler1<Owner, ClickEventArgs>): this {
179
+ this.bindClick(owner, handler);
180
+ return this;
181
+ }
182
+
160
183
  protected syncVisualState(): void {}
161
184
 
162
185
  protected replaceIndicatorRoot(nextRoot: FlexBox): void {
@@ -245,4 +268,16 @@ export class PressableLabeledControl extends FlexBox {
245
268
  }
246
269
  FocusAdornerManager.hideOwner(this);
247
270
  }
271
+
272
+ private fireSemanticClick(): void {
273
+ const callback = this.clickCallback;
274
+ if (callback !== null) {
275
+ callback(ClickEventArgs.Empty);
276
+ return;
277
+ }
278
+ const binding = this.clickBinding;
279
+ if (binding !== null) {
280
+ binding.invoke(ClickEventArgs.Empty);
281
+ }
282
+ }
248
283
  }
@@ -10,7 +10,7 @@ import { SelectionHandleAdornerManager } from "./SelectionHandleAdornerManager";
10
10
  import { ToolTipManager } from "./ToolTipManager";
11
11
  import * as ui from "../bindings/ui";
12
12
  import { flushCommit, fireLoadedCallbacks, markNeedsCommit, resetCommitState } from "./FrameScheduler";
13
- import { HandleValue, Unit } from "./ffi";
13
+ import { fui_set_application_caption, HandleValue, Unit } from "./ffi";
14
14
  import { Node } from "./Node";
15
15
  import { cancelAllTimers } from "./Timers";
16
16
  import {
@@ -247,6 +247,11 @@ export class Application<TPage> {
247
247
  return root;
248
248
  }
249
249
 
250
+ static caption(value: string): void {
251
+ const bytes = Uint8Array.wrap(String.UTF8.encode(value, false));
252
+ fui_set_application_caption(bytes.length > 0 ? bytes.dataStart : 0, <u32>bytes.length);
253
+ }
254
+
250
255
  static unmount(): void {
251
256
  const previous = mountedShell;
252
257
  if (previous === null) {