@effindomv2/fui-as 0.1.51 → 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.51",
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,7 +217,7 @@ 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
  }
@@ -269,15 +275,26 @@ copy_runtime_assets() {
269
275
  rm -rf "${destination}/runtime"
270
276
  mkdir -p "${destination}/runtime/dist"
271
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
272
289
  if [ -f "${RUNTIME_DIST_DIR}/icu-asset.json" ]; then
273
290
  cp "${RUNTIME_DIST_DIR}/icu-asset.json" "${destination}/runtime/dist/icu-asset.json"
274
291
  fi
275
292
  cp -R "${RUNTIME_DIST_DIR}/runtime" "${destination}/runtime/dist/runtime"
276
- mkdir -p "${destination}/runtime/fonts"
293
+ mkdir -p "${destination}/runtime/dist/fonts"
277
294
  if [ -d "${REPO_ROOT}/public/v2/fonts" ]; then
278
- cp -R "${REPO_ROOT}/public/v2/fonts/." "${destination}/runtime/fonts/"
295
+ cp -R "${REPO_ROOT}/public/v2/fonts/." "${destination}/runtime/dist/fonts/"
279
296
  elif [ -d "${RUNTIME_DIST_DIR}/fonts" ]; then
280
- cp -R "${RUNTIME_DIST_DIR}/fonts/." "${destination}/runtime/fonts/"
297
+ cp -R "${RUNTIME_DIST_DIR}/fonts/." "${destination}/runtime/dist/fonts/"
281
298
  fi
282
299
  }
283
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,
@@ -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) {
@@ -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) {
@@ -11,7 +11,10 @@ import {
11
11
  formatRedoShortcutLabel,
12
12
  formatShortcutLabel,
13
13
  formatUndoShortcutLabel,
14
+ getHostContext,
14
15
  getPlatformFamily,
16
+ HostCapability,
17
+ HostContext,
15
18
  PlatformFamily,
16
19
  } from "./Platform";
17
20
  import { ContextMenuEventArgs, Node, VisibilityChangedEventArgs } from "./Node";
@@ -101,18 +104,21 @@ export class ContextMenuManager {
101
104
  if (this.invokeCustomContextMenuHandler(targetNode, x, y)) {
102
105
  return;
103
106
  }
104
- const items = this.buildBuiltInItems(handle, x, y, true);
107
+ const host = getHostContext();
108
+ const items = this.buildBuiltInItemsForHost(handle, x, y, true, host);
105
109
 
106
110
  const navigationItems = new Array<MenuItem>();
107
- if (ffi.fui_can_navigate_back()) {
111
+ if (host.supports(HostCapability.BrowserHistory) && ffi.fui_can_navigate_back()) {
108
112
  navigationItems.push(new MenuItem("Back", ContextMenuAction.NavigateBack, null, 0, resolveHistoryShortcutLabel(false)));
109
113
  }
110
- if (ffi.fui_can_navigate_forward()) {
114
+ if (host.supports(HostCapability.BrowserHistory) && ffi.fui_can_navigate_forward()) {
111
115
  navigationItems.push(new MenuItem("Forward", ContextMenuAction.NavigateForward, null, 0, resolveHistoryShortcutLabel(true)));
112
116
  }
113
- navigationItems.push(
114
- new MenuItem("Reload Page", ContextMenuAction.ReloadPage, null, 0, formatPrimaryShortcutLabel("r")),
115
- );
117
+ if (host.supports(HostCapability.Reload)) {
118
+ navigationItems.push(
119
+ new MenuItem("Reload Page", ContextMenuAction.ReloadPage, null, 0, formatPrimaryShortcutLabel("r")),
120
+ );
121
+ }
116
122
  appendMenuSection(items, navigationItems);
117
123
  if (items.length == 0) {
118
124
  this.releaseActiveMenuLinkPreview();
@@ -123,7 +129,7 @@ export class ContextMenuManager {
123
129
  }
124
130
 
125
131
  static showForLongPress(handle: u64, x: f32, y: f32): bool {
126
- const items = this.buildBuiltInItems(handle, x, y, false);
132
+ const items = this.buildBuiltInItemsForHost(handle, x, y, false, getHostContext());
127
133
  if (items.length == 0) {
128
134
  this.releaseActiveMenuLinkPreview();
129
135
  return false;
@@ -169,7 +175,7 @@ export class ContextMenuManager {
169
175
  }
170
176
  const handler = contextNode.contextMenuHandler;
171
177
  if (handler !== null) {
172
- handler(new ContextMenuEventArgs(targetNode, x, y));
178
+ handler(new ContextMenuEventArgs(targetNode, x, y, getHostContext()));
173
179
  return true;
174
180
  }
175
181
  contextNode = contextNode.parentNode;
@@ -177,7 +183,14 @@ export class ContextMenuManager {
177
183
  return false;
178
184
  }
179
185
 
180
- private static buildBuiltInItems(handle: u64, x: f32, y: f32, clearSelectionOnBackgroundMiss: bool): Array<MenuItem> {
186
+ /** @internal Exposed for deterministic host-policy tests; not part of the FUI public barrel. */
187
+ static buildBuiltInItemsForHost(
188
+ handle: u64,
189
+ x: f32,
190
+ y: f32,
191
+ clearSelectionOnBackgroundMiss: bool,
192
+ host: HostContext,
193
+ ): Array<MenuItem> {
181
194
  this.releaseActiveMenuLinkPreview();
182
195
  const targetNode = EventRouter.getRegisteredNode(handle);
183
196
  let contextNode = targetNode;
@@ -194,26 +207,34 @@ export class ContextMenuManager {
194
207
  link.pinPreviewForContextMenu();
195
208
  this.activeMenuLink = link;
196
209
  const linkItems = new Array<MenuItem>();
197
- linkItems.push(new MenuItem("New Tab", ContextMenuAction.OpenLinkInNewTab, link.href));
198
- linkItems.push(new MenuItem("Open", ContextMenuAction.OpenLink, link.href));
210
+ if (host.supports(HostCapability.NewBrowsingContext)) {
211
+ linkItems.push(new MenuItem("New Tab", ContextMenuAction.OpenLinkInNewTab, link.href));
212
+ }
213
+ if (host.supports(HostCapability.OpenExternalUri)) {
214
+ linkItems.push(new MenuItem("Open", ContextMenuAction.OpenLink, link.href));
215
+ }
199
216
  appendMenuSection(items, linkItems);
200
217
  }
201
218
 
202
219
  const textTarget = this.resolveTextTarget(targetNode);
203
220
  const selectionHit = ui.isPointInSelection(x, y) || this.selectionHintContainsHandle(handle);
204
221
  if (textTarget !== null) {
205
- appendMenuSection(items, this.buildTextSection(textTarget, selectionHit ? this.currentSelectionText : ""));
222
+ appendMenuSection(items, this.buildTextSection(textTarget, selectionHit ? this.currentSelectionText : "", host));
206
223
  }
207
224
 
208
225
  const imageUrl = this.resolveImageUrl(targetNode, x, y);
209
226
  if (imageUrl !== null && imageUrl.length > 0) {
210
227
  const imageItems = new Array<MenuItem>();
211
- imageItems.push(new MenuItem("New Tab", ContextMenuAction.OpenImageInNewTab, imageUrl));
212
- imageItems.push(new MenuItem("Open", ContextMenuAction.OpenImage, imageUrl));
228
+ if (host.supports(HostCapability.NewBrowsingContext)) {
229
+ imageItems.push(new MenuItem("New Tab", ContextMenuAction.OpenImageInNewTab, imageUrl));
230
+ }
231
+ if (host.supports(HostCapability.OpenExternalUri)) {
232
+ imageItems.push(new MenuItem("Open", ContextMenuAction.OpenImage, imageUrl));
233
+ }
213
234
  appendMenuSection(items, imageItems);
214
235
  }
215
236
 
216
- if (selectionHit && textTarget === null && this.currentSelectionText.length > 0) {
237
+ if (selectionHit && textTarget === null && this.currentSelectionText.length > 0 && host.supports(HostCapability.ClipboardWrite)) {
217
238
  const selectionItems = new Array<MenuItem>();
218
239
  selectionItems.push(new MenuItem("Copy", ContextMenuAction.CopyCurrentSelection, this.currentSelectionText));
219
240
  appendMenuSection(items, selectionItems);
@@ -266,7 +287,7 @@ export class ContextMenuManager {
266
287
  return null;
267
288
  }
268
289
 
269
- private static buildTextSection(target: TextCore, currentSelectionText: string = ""): Array<MenuItem> {
290
+ private static buildTextSection(target: TextCore, currentSelectionText: string, host: HostContext): Array<MenuItem> {
270
291
  const items = new Array<MenuItem>();
271
292
  const handle = target.builtHandle;
272
293
  if (handle == <u64>HandleValue.Invalid || !target.isSelectableText) {
@@ -313,48 +334,52 @@ export class ContextMenuManager {
313
334
  ),
314
335
  );
315
336
  items.push(MenuItem.separator());
316
- items.push(
317
- new MenuItem(
318
- "Cut",
319
- ContextMenuAction.CutTextSelection,
320
- selectedPayload,
321
- 0,
322
- formatPrimaryShortcutLabel("x"),
323
- !hasSelection,
324
- handle,
325
- 0,
326
- 0,
327
- true,
328
- ).withSelectionRange(selectionStartByte, selectionEndByte),
329
- );
330
- items.push(
331
- new MenuItem(
332
- "Copy",
333
- ContextMenuAction.CopyCurrentSelection,
334
- selectedPayload,
335
- 0,
336
- formatPrimaryShortcutLabel("c"),
337
- !hasSelection,
338
- handle,
339
- 0,
340
- 0,
341
- true,
342
- ),
343
- );
344
- items.push(
345
- new MenuItem(
346
- "Paste",
347
- ContextMenuAction.PasteText,
348
- null,
349
- 0,
350
- formatPrimaryShortcutLabel("v"),
351
- false,
352
- handle,
353
- 0,
354
- 0,
355
- true,
356
- ),
357
- );
337
+ if (host.supports(HostCapability.ClipboardWrite)) {
338
+ items.push(
339
+ new MenuItem(
340
+ "Cut",
341
+ ContextMenuAction.CutTextSelection,
342
+ selectedPayload,
343
+ 0,
344
+ formatPrimaryShortcutLabel("x"),
345
+ !hasSelection,
346
+ handle,
347
+ 0,
348
+ 0,
349
+ true,
350
+ ).withSelectionRange(selectionStartByte, selectionEndByte),
351
+ );
352
+ items.push(
353
+ new MenuItem(
354
+ "Copy",
355
+ ContextMenuAction.CopyCurrentSelection,
356
+ selectedPayload,
357
+ 0,
358
+ formatPrimaryShortcutLabel("c"),
359
+ !hasSelection,
360
+ handle,
361
+ 0,
362
+ 0,
363
+ true,
364
+ ),
365
+ );
366
+ }
367
+ if (host.supports(HostCapability.ClipboardRead)) {
368
+ items.push(
369
+ new MenuItem(
370
+ "Paste",
371
+ ContextMenuAction.PasteText,
372
+ null,
373
+ 0,
374
+ formatPrimaryShortcutLabel("v"),
375
+ false,
376
+ handle,
377
+ 0,
378
+ 0,
379
+ true,
380
+ ),
381
+ );
382
+ }
358
383
  items.push(
359
384
  new MenuItem(
360
385
  "Select All",
@@ -372,6 +397,12 @@ export class ContextMenuManager {
372
397
  return items;
373
398
  }
374
399
 
400
+ if (!host.supports(HostCapability.ClipboardWrite)) {
401
+ items.push(
402
+ new MenuItem("Select All", ContextMenuAction.SelectAllText, null, 0, formatPrimaryShortcutLabel("a"), !hasText, handle),
403
+ );
404
+ return items;
405
+ }
375
406
  if (currentSelectionText.length > 0) {
376
407
  items.push(
377
408
  new MenuItem(
@@ -24,6 +24,7 @@ import { NavLink } from "../controls/NavLink";
24
24
  import { TextInputCore } from "../controls/internal/TextInputCore";
25
25
  import { Image } from "../nodes/Image";
26
26
  import { Svg } from "../nodes/Svg";
27
+ import { ContextMenu } from "../controls/ContextMenu";
27
28
 
28
29
  export interface GlobalKeyHandler {
29
30
  handleGlobalKeyEvent(eventType: KeyEventType, key: string, modifiers: u32): bool;
@@ -44,6 +45,7 @@ export class EventRouter {
44
45
  private static readonly keyFilterHandlers: Array<GlobalKeyHandler> = new Array<GlobalKeyHandler>();
45
46
  private static readonly keyFilterTokens: Array<u32> = new Array<u32>();
46
47
  private static capturedPointerHandle: u64 = <u64>HandleValue.Invalid;
48
+ private static selectionCursorHandle: u64 = <u64>HandleValue.Invalid;
47
49
  private static currentCursorStyle: CursorStyle = CursorStyle.Default;
48
50
  private static nextKeyFilterToken: u32 = 1;
49
51
  private static focusedNode: Node | null = null;
@@ -70,6 +72,9 @@ export class EventRouter {
70
72
  if (this.capturedPointerHandle == handle) {
71
73
  this.capturedPointerHandle = <u64>HandleValue.Invalid;
72
74
  }
75
+ if (this.selectionCursorHandle == handle) {
76
+ this.selectionCursorHandle = <u64>HandleValue.Invalid;
77
+ }
73
78
  const index = getHandleIndex(handle);
74
79
  this.nodes.delete(index);
75
80
  this.generations.delete(index);
@@ -94,6 +99,15 @@ export class EventRouter {
94
99
  showKeyboardFocusForPointerEvent(eventType);
95
100
  SelectionHandleAdornerManager.recordPointerEvent(eventType, pointerType);
96
101
  const pointedNode = this.resolveNode(handle);
102
+ if (
103
+ eventType == PointerEventType.Down &&
104
+ (button == 0 || pointerType == PointerType.Touch || pointerType == PointerType.Pen) &&
105
+ ContextMenu.dismissForOutsidePointerDown(x, y)
106
+ ) {
107
+ DragDropManager.handlePointerEvent(null, eventType, x, y, modifiers);
108
+ this.applyCurrentCursor();
109
+ return true;
110
+ }
97
111
  if (
98
112
  eventType == PointerEventType.Down &&
99
113
  !this.preservesSelectionOnPointerDownForRouting(pointedNode) &&
@@ -103,6 +117,14 @@ export class EventRouter {
103
117
  this.applyCurrentCursor();
104
118
  return true;
105
119
  }
120
+ if (eventType == PointerEventType.Down && button == 0 && pointerType != PointerType.Touch) {
121
+ const selectionCursorNode = this.resolveSelectionCursorNode(pointedNode);
122
+ this.selectionCursorHandle = selectionCursorNode === null
123
+ ? <u64>HandleValue.Invalid
124
+ : changetype<Node>(selectionCursorNode).builtHandle;
125
+ } else if (eventType == PointerEventType.Up || eventType == PointerEventType.Cancel) {
126
+ this.selectionCursorHandle = <u64>HandleValue.Invalid;
127
+ }
106
128
  if (eventType == PointerEventType.Up) {
107
129
  trackKeyboardScrollPointerUp(pointedNode, x, y);
108
130
  }
@@ -553,6 +575,7 @@ export class EventRouter {
553
575
  this.nodes.clear();
554
576
  this.generations.clear();
555
577
  this.capturedPointerHandle = <u64>HandleValue.Invalid;
578
+ this.selectionCursorHandle = <u64>HandleValue.Invalid;
556
579
  this.hoverStack.length = 0;
557
580
  this.focusedNode = null;
558
581
  this.keyFilterHandlers.length = 0;
@@ -687,6 +710,22 @@ export class EventRouter {
687
710
  return this.resolveNode(this.capturedPointerHandle);
688
711
  }
689
712
 
713
+ private static resolveSelectionCursorNode(node: Node | null): Node | null {
714
+ if (node === null || !node.isEnabled || !node.isVisible) {
715
+ return null;
716
+ }
717
+ if (node.isSelectableText || node.isEditableText) {
718
+ return node;
719
+ }
720
+ if (node instanceof TextInputCore) {
721
+ const editor = changetype<TextInputCore>(node).editorNode;
722
+ if (editor.isEnabled && editor.isVisible && (editor.isSelectableText || editor.isEditableText)) {
723
+ return editor;
724
+ }
725
+ }
726
+ return null;
727
+ }
728
+
690
729
  private static applyCurrentCursor(): void {
691
730
  const dragCursor = DragDropManager.cursorOverrideStyle();
692
731
  if (dragCursor != CursorStyle.Default) {
@@ -698,6 +737,11 @@ export class EventRouter {
698
737
  this.applyCursor(captured.cursorStyle);
699
738
  return;
700
739
  }
740
+ const selectionCursorNode = this.resolveNode(this.selectionCursorHandle);
741
+ if (selectionCursorNode !== null) {
742
+ this.applyCursor(changetype<Node>(selectionCursorNode).cursorStyle);
743
+ return;
744
+ }
701
745
  if (this.hoverStack.length == 0) {
702
746
  this.applyCursor(CursorStyle.Default);
703
747
  return;
package/src/core/Node.ts CHANGED
@@ -23,6 +23,7 @@ import { storePersistedTextState, tryLoadPersistedTextState } from "./PersistedU
23
23
  import { ToolTip } from "./ToolTip";
24
24
  import { ToolTipManager } from "./ToolTipManager";
25
25
  import { throwNullArgument } from "./Errors";
26
+ import { HostContext } from "./Platform";
26
27
  import { bind0, bind1, bind2, bindResult0, bindResult1 } from "./bind";
27
28
  import {
28
29
  CursorStyle,
@@ -435,11 +436,13 @@ export class ContextMenuEventArgs {
435
436
  readonly target: Node | null;
436
437
  readonly x: f32;
437
438
  readonly y: f32;
439
+ readonly host: HostContext;
438
440
 
439
- constructor(target: Node | null, x: f32, y: f32) {
441
+ constructor(target: Node | null, x: f32, y: f32, host: HostContext) {
440
442
  this.target = target;
441
443
  this.x = x;
442
444
  this.y = y;
445
+ this.host = host;
443
446
  }
444
447
  }
445
448
 
@@ -1,13 +1,58 @@
1
- import { fui_get_platform_family, fui_is_coarse_pointer, KeyModifier } from "./ffi";
1
+ import {
2
+ fui_get_host_capabilities,
3
+ fui_get_host_environment,
4
+ fui_get_platform_family,
5
+ fui_is_coarse_pointer,
6
+ HostCapability,
7
+ HostEnvironment,
8
+ KeyModifier,
9
+ PlatformFamily,
10
+ } from "./ffi";
2
11
 
3
- export enum PlatformFamily {
4
- Unknown = 0,
5
- Apple = 1,
6
- Windows = 2,
7
- Linux = 3,
12
+ export { HostCapability, HostEnvironment, PlatformFamily } from "./ffi";
13
+
14
+ const KNOWN_HOST_CAPABILITIES: u32 =
15
+ HostCapability.BrowserHistory |
16
+ HostCapability.Reload |
17
+ HostCapability.NewBrowsingContext |
18
+ HostCapability.OpenExternalUri |
19
+ HostCapability.ClipboardRead |
20
+ HostCapability.ClipboardWrite |
21
+ HostCapability.FileDialogs;
22
+
23
+ function resolvePlatformFamily(value: u32): PlatformFamily {
24
+ if (value <= <u32>PlatformFamily.Linux) {
25
+ return <PlatformFamily>value;
26
+ }
27
+ return PlatformFamily.Unknown;
8
28
  }
9
29
 
10
- const detectedPlatformFamily = <PlatformFamily>fui_get_platform_family();
30
+ function resolveHostEnvironment(value: u32): HostEnvironment {
31
+ if (value <= <u32>HostEnvironment.Headless) {
32
+ return <HostEnvironment>value;
33
+ }
34
+ return HostEnvironment.Unknown;
35
+ }
36
+
37
+ const detectedPlatformFamily = resolvePlatformFamily(fui_get_platform_family());
38
+ const detectedHostEnvironment = resolveHostEnvironment(fui_get_host_environment());
39
+ const detectedHostCapabilities = fui_get_host_capabilities() & KNOWN_HOST_CAPABILITIES;
40
+
41
+ export class HostContext {
42
+ readonly platformFamily: PlatformFamily;
43
+ readonly environment: HostEnvironment;
44
+ private readonly capabilities: u32;
45
+
46
+ constructor(platformFamily: PlatformFamily, environment: HostEnvironment, capabilities: u32) {
47
+ this.platformFamily = resolvePlatformFamily(<u32>platformFamily);
48
+ this.environment = resolveHostEnvironment(<u32>environment);
49
+ this.capabilities = capabilities & KNOWN_HOST_CAPABILITIES;
50
+ }
51
+
52
+ supports(capability: HostCapability): bool {
53
+ return (this.capabilities & <u32>capability) != 0;
54
+ }
55
+ }
11
56
 
12
57
  class KeyboardPolicy {
13
58
  constructor(
@@ -108,6 +153,18 @@ export function getPlatformFamily(): PlatformFamily {
108
153
  return detectedPlatformFamily;
109
154
  }
110
155
 
156
+ export function getHostEnvironment(): HostEnvironment {
157
+ return detectedHostEnvironment;
158
+ }
159
+
160
+ export function getHostContext(): HostContext {
161
+ return new HostContext(detectedPlatformFamily, detectedHostEnvironment, detectedHostCapabilities);
162
+ }
163
+
164
+ export function hasHostCapability(capability: HostCapability): bool {
165
+ return (detectedHostCapabilities & <u32>capability) != 0;
166
+ }
167
+
111
168
  export function isCoarsePointer(): bool {
112
169
  return fui_is_coarse_pointer();
113
170
  }
package/src/core/ffi.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export {
2
2
  fui_get_accent_color,
3
+ fui_get_host_capabilities,
4
+ fui_get_host_environment,
3
5
  fui_get_platform_family,
4
6
  fui_is_coarse_pointer,
5
7
  fui_is_dark_mode,
@@ -8,6 +8,20 @@ export function fui_get_accent_color(): u32 {
8
8
  return __host_fui_get_accent_color();
9
9
  }
10
10
 
11
+ @external("fui_host", "fui_get_host_capabilities")
12
+ declare function __host_fui_get_host_capabilities(): u32;
13
+
14
+ export function fui_get_host_capabilities(): u32 {
15
+ return __host_fui_get_host_capabilities();
16
+ }
17
+
18
+ @external("fui_host", "fui_get_host_environment")
19
+ declare function __host_fui_get_host_environment(): u32;
20
+
21
+ export function fui_get_host_environment(): u32 {
22
+ return __host_fui_get_host_environment();
23
+ }
24
+
11
25
  @external("fui_host", "fui_get_platform_family")
12
26
  declare function __host_fui_get_platform_family(): u32;
13
27
 
@@ -15,6 +15,9 @@ export declare function get_viewport_height(): f32;
15
15
  @external("fui_host", "get_device_pixel_ratio")
16
16
  export declare function get_device_pixel_ratio(): f32;
17
17
 
18
+ @external("fui_host", "fui_set_application_caption")
19
+ export declare function fui_set_application_caption(captionPtr: usize, captionLen: u32): void;
20
+
18
21
  @external("fui_host", "fui_set_pointer_capture")
19
22
  export declare function fui_set_pointer_capture(handle: u64): void;
20
23
 
@@ -108,6 +111,12 @@ export declare function fui_get_accent_color(): u32;
108
111
  @external("fui_host", "fui_get_platform_family")
109
112
  export declare function fui_get_platform_family(): u32;
110
113
 
114
+ @external("fui_host", "fui_get_host_environment")
115
+ export declare function fui_get_host_environment(): u32;
116
+
117
+ @external("fui_host", "fui_get_host_capabilities")
118
+ export declare function fui_get_host_capabilities(): u32;
119
+
111
120
  @external("fui_host", "fui_is_coarse_pointer")
112
121
  export declare function fui_is_coarse_pointer(): bool;
113
122
 
@@ -166,6 +166,30 @@ export enum SemanticRole {
166
166
  ComboBox = 16,
167
167
  }
168
168
 
169
+ export enum PlatformFamily {
170
+ Unknown = 0,
171
+ Apple = 1,
172
+ Windows = 2,
173
+ Linux = 3,
174
+ }
175
+
176
+ export enum HostEnvironment {
177
+ Unknown = 0,
178
+ Browser = 1,
179
+ Desktop = 2,
180
+ Headless = 3,
181
+ }
182
+
183
+ export enum HostCapability {
184
+ BrowserHistory = 1,
185
+ Reload = 2,
186
+ NewBrowsingContext = 4,
187
+ OpenExternalUri = 8,
188
+ ClipboardRead = 16,
189
+ ClipboardWrite = 32,
190
+ FileDialogs = 64,
191
+ }
192
+
169
193
  export enum CursorStyle {
170
194
  Default = 0,
171
195
  Pointer = 1,
@@ -1,13 +0,0 @@
1
- <div
2
- class="effindom-loading-overlay"
3
- id="effindom-loading-overlay"
4
- data-state="loading"
5
- aria-live="polite"
6
- aria-hidden="true"
7
- hidden>
8
- <div class="effindom-loading-card">
9
- <p class="effindom-loading-kicker">EffinDom backstage</p>
10
- <h2 class="effindom-loading-title" id="effindom-loading-title">Teaching the pixels their lines...</h2>
11
- <p class="effindom-loading-detail" id="effindom-loading-detail">The runtime orchestra is tuning up behind the canvas.</p>
12
- </div>
13
- </div>
@@ -1,52 +0,0 @@
1
- .effindom-loading-overlay {
2
- position: absolute;
3
- inset: 0;
4
- display: grid;
5
- place-items: center;
6
- padding: 24px;
7
- box-sizing: border-box;
8
- background: linear-gradient(180deg, rgba(2, 6, 23, 0.72), rgba(10, 20, 34, 0.82));
9
- backdrop-filter: blur(10px);
10
- z-index: 2;
11
- }
12
-
13
- .effindom-loading-overlay[hidden] {
14
- display: none;
15
- }
16
-
17
- .effindom-loading-card {
18
- max-width: 420px;
19
- padding: 22px 24px;
20
- border: 1px solid rgba(148, 163, 184, 0.32);
21
- border-radius: 18px;
22
- background: rgba(6, 12, 21, 0.80);
23
- text-align: center;
24
- box-shadow: 0 18px 48px rgba(2, 6, 23, 0.30);
25
- }
26
-
27
- .effindom-loading-overlay[data-state="error"] .effindom-loading-card {
28
- border-color: rgba(248, 113, 113, 0.50);
29
- background: rgba(69, 10, 10, 0.78);
30
- }
31
-
32
- .effindom-loading-kicker {
33
- margin: 0 0 10px;
34
- font-size: 11px;
35
- font-weight: 700;
36
- letter-spacing: 0.12em;
37
- text-transform: uppercase;
38
- color: #7dd3fc;
39
- }
40
-
41
- .effindom-loading-title {
42
- margin: 0;
43
- font-size: 28px;
44
- line-height: 1.15;
45
- }
46
-
47
- .effindom-loading-detail {
48
- margin: 12px 0 0;
49
- font-size: 14px;
50
- line-height: 1.55;
51
- color: #cbd5e1;
52
- }