@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 +36 -0
- package/browser/favicon.ico +0 -0
- package/browser/src/assemblyscript-harness.ts +39 -0
- package/browser/src/index.ts +3 -0
- package/browser/src/routed-harness.ts +25 -1
- package/browser/src/shared-browser.ts +4 -1
- package/package.json +14 -5
- package/scripts/build.sh +31 -9
- package/scripts/framework-host-services.ts +12 -0
- package/src/Fui.ts +12 -1
- package/src/controls/Button.ts +3 -61
- package/src/controls/Checkbox.ts +16 -1
- package/src/controls/ContextMenu.ts +29 -0
- package/src/controls/RadioButton.ts +16 -1
- package/src/controls/Switch.ts +16 -1
- package/src/controls/internal/PressableLabeledControl.ts +36 -1
- package/src/core/Application.ts +6 -1
- package/src/core/ContextMenuManager.ts +89 -58
- package/src/core/EventRouter.ts +44 -0
- package/src/core/Node.ts +84 -8
- package/src/core/Platform.ts +64 -7
- package/src/core/ffi.ts +2 -0
- package/src/core/generated/FrameworkHostServices.ts +14 -0
- package/src/core/generated/HostAbi.ts +9 -0
- package/src/core/generated/UiEnums.ts +24 -0
- package/src/nodes/FlexBox.ts +10 -0
- package/src/nodes/Grid.ts +10 -0
- package/src/nodes/ScrollView.ts +10 -0
- package/src/nodes/TextCore.ts +10 -0
- package/browser/loading-overlay-body.html +0 -13
- package/browser/loading-overlay-styles.html +0 -52
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`.
|
package/browser/favicon.ico
CHANGED
|
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
|
+
}
|
package/browser/src/index.ts
CHANGED
|
@@ -1 +1,25 @@
|
|
|
1
|
-
|
|
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
|
|
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.
|
|
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
|
-
"@
|
|
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="${
|
|
24
|
-
LOADING_OVERLAY_BODY_FILE="${
|
|
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
|
|
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 "
|
|
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 "
|
|
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
|
-
|
|
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 {
|
|
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,
|
package/src/controls/Button.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
442
|
+
this.endPress(true);
|
|
501
443
|
return true;
|
|
502
444
|
}
|
|
503
445
|
return callbackHandled;
|
package/src/controls/Checkbox.ts
CHANGED
|
@@ -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);
|
package/src/controls/Switch.ts
CHANGED
|
@@ -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
|
}
|
package/src/core/Application.ts
CHANGED
|
@@ -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) {
|