@effindomv2/fui-rs 0.1.16 → 0.1.18
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/Cargo.toml +1 -1
- package/README.md +69 -0
- package/package.json +3 -2
- package/scripts/build.sh +6 -1
- package/scripts/hostgen/rust-host-events.ts +43 -13
- package/src/app.rs +7 -0
- package/src/assets.rs +2 -2
- package/src/bindings/ui.rs +2 -0
- package/src/bitmap.rs +39 -3
- package/src/bridge_callbacks.rs +27 -17
- package/src/controls/button.rs +1 -0
- package/src/controls/checkbox.rs +1 -0
- package/src/controls/combobox.rs +5 -3
- package/src/controls/dropdown.rs +3 -1
- package/src/controls/internal/pressable_labeled_control.rs +1 -0
- package/src/controls/internal/text_input_core.rs +7 -3
- package/src/controls/radio_button.rs +1 -0
- package/src/controls/slider.rs +1 -0
- package/src/controls/switch.rs +1 -0
- package/src/controls/tests.rs +19 -7
- package/src/debug.rs +8 -5
- package/src/drag_drop.rs +1 -3
- package/src/drawing.rs +55 -1
- package/src/event.rs +39 -27
- package/src/fetch.rs +21 -7
- package/src/ffi.rs +3 -1
- package/src/file.rs +88 -59
- package/src/host_events.rs +41 -0
- package/src/lib.rs +52 -9
- package/src/node/core.rs +7 -21
- package/src/node/helpers.rs +1 -1
- package/src/node/mod.rs +1 -1
- package/src/node/scroll_bar.rs +101 -0
- package/src/node/scroll_box.rs +7 -0
- package/src/node/scroll_view.rs +20 -14
- package/src/node/text_node.rs +4 -0
- package/src/node/virtual_list.rs +120 -33
- package/src/platform.rs +3 -0
- package/src/popup_presenter.rs +1 -0
- package/src/text.rs +41 -4
- package/src/theme.rs +2 -2
- package/src/timers.rs +1 -1
- package/src/typography.rs +15 -15
- package/src/worker.rs +25 -9
- package/src/worker_runtime.rs +61 -17
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -74,6 +74,75 @@ fui_app!(FlexBox, build_page);
|
|
|
74
74
|
| Browser file/fetch/worker bridges | Available |
|
|
75
75
|
| Host services/events generator support | Available |
|
|
76
76
|
|
|
77
|
+
## Recycled virtual-list rows
|
|
78
|
+
|
|
79
|
+
`VirtualList` creates a fixed retained row pool. Use `item_template` once to
|
|
80
|
+
construct typed row state, then update that state from `on_bind_item` whenever a
|
|
81
|
+
pool slot is assigned a new item index:
|
|
82
|
+
|
|
83
|
+
```rust
|
|
84
|
+
struct ContactRow {
|
|
85
|
+
name: TextNode,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let contacts = virtual_list(10_000, 28.0)
|
|
89
|
+
.item_template(|container| {
|
|
90
|
+
let name = text("");
|
|
91
|
+
container.child(&name);
|
|
92
|
+
ContactRow { name }
|
|
93
|
+
});
|
|
94
|
+
contacts.on_bind_item(|row, index| {
|
|
95
|
+
row.name.text(format!("Contact {index}"));
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The template is not rerun while scrolling. Do not key recycled rows by pointer
|
|
100
|
+
or create controls inside `on_bind_item`.
|
|
101
|
+
|
|
102
|
+
## Scrollbar styling
|
|
103
|
+
|
|
104
|
+
Apply common scrollbar chrome without leaving fluent `ScrollBox` construction:
|
|
105
|
+
|
|
106
|
+
```rust
|
|
107
|
+
let content = scroll_box().scrollbar_style(
|
|
108
|
+
ScrollBarStyle::new()
|
|
109
|
+
.track_width(10.0)
|
|
110
|
+
.thumb_width(7.0)
|
|
111
|
+
.thumb_corner_radius(3.5),
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use `vertical_scrollbar()` or `horizontal_scrollbar()` afterward for an
|
|
116
|
+
axis-specific override.
|
|
117
|
+
|
|
118
|
+
## Host-event lifetime
|
|
119
|
+
|
|
120
|
+
Generated `on_*` host-event functions return `HostEventSubscription`. Retain
|
|
121
|
+
the guard for exactly as long as the handler should remain active; dropping it
|
|
122
|
+
unsubscribes automatically. Replacing a handler is generation-safe, so dropping
|
|
123
|
+
an older guard cannot remove its replacement.
|
|
124
|
+
|
|
125
|
+
## Worker entrypoints
|
|
126
|
+
|
|
127
|
+
Enable the `worker-runtime` feature, implement `Default + WorkerJob`, and let
|
|
128
|
+
the SDK emit resumable entries plus the shared callback-buffer ABI:
|
|
129
|
+
|
|
130
|
+
```rust
|
|
131
|
+
use fui::prelude::*;
|
|
132
|
+
|
|
133
|
+
#[derive(Default)]
|
|
134
|
+
struct PrimeJob {
|
|
135
|
+
state: WorkerJobState,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
impl WorkerJob for PrimeJob {
|
|
139
|
+
fn state(&mut self) -> &mut WorkerJobState { &mut self.state }
|
|
140
|
+
fn run(&mut self) { self.complete("done"); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fui_worker!(primeWorker => PrimeJob);
|
|
144
|
+
```
|
|
145
|
+
|
|
77
146
|
## Architecture
|
|
78
147
|
|
|
79
148
|
FUI-RS builds Rust retained UI objects into the EffinDom v2 runtime through the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effindomv2/fui-rs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial",
|
|
6
6
|
"description": "EffinDom v2 Rust frontend framework SDK and host generators",
|
|
@@ -27,9 +27,10 @@
|
|
|
27
27
|
"generate:host-events:workbench": "mkdir -p build && esbuild scripts/generate-host-events.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-events.mjs && node build/generate-host-events.mjs demo/src/host-events.ts demoHostEvents crates/demo-workbench/src/generated/host_events.rs",
|
|
28
28
|
"generate:host-events:stage4": "mkdir -p build && esbuild scripts/generate-host-events.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-events.mjs && node build/generate-host-events.mjs demo/src/host-events.ts demoHostEvents crates/demo-stage4/src/generated/host_events.rs",
|
|
29
29
|
"generate:host-events:stage5": "mkdir -p build && esbuild scripts/generate-host-events.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-events.mjs && node build/generate-host-events.mjs demo/src/host-events.ts demoHostEvents crates/demo-stage5/src/generated/host_events.rs",
|
|
30
|
+
"generate:host-events:immediate-drawing": "mkdir -p build && esbuild scripts/generate-host-events.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-events.mjs && node build/generate-host-events.mjs demo/src/host-events.ts demoHostEvents crates/demo-immediate-drawing/src/generated/host_events.rs",
|
|
30
31
|
"generate:worker-host-services": "mkdir -p build && esbuild scripts/generate-host-services.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-services.mjs && node build/generate-host-services.mjs demo/src/worker-host-services.ts demoWorkerHostServices crates/demo-shared/src/generated/worker_host_services.rs fui::host_services fui_host_service",
|
|
31
32
|
"generate:framework-host-services": "mkdir -p build && esbuild scripts/generate-host-services.ts --bundle --format=esm --platform=node --target=node20 --packages=external --outfile=build/generate-host-services.mjs && node build/generate-host-services.mjs scripts/framework-host-services.ts frameworkHostServices src/generated/framework_host_services.rs crate::host_services fui_host",
|
|
32
|
-
"generate:host": "npm run generate:host-services && npm run generate:host-events:home && npm run generate:host-events:workbench && npm run generate:host-events:stage4 && npm run generate:host-events:stage5 && npm run generate:worker-host-services && npm run generate:framework-host-services",
|
|
33
|
+
"generate:host": "npm run generate:host-services && npm run generate:host-events:home && npm run generate:host-events:workbench && npm run generate:host-events:stage4 && npm run generate:host-events:stage5 && npm run generate:host-events:immediate-drawing && npm run generate:worker-host-services && npm run generate:framework-host-services",
|
|
33
34
|
"guard:runtime-dependency": "bash scripts/check-runtime-dependency.sh",
|
|
34
35
|
"lint": "cd ../.. && npm run lint:v2",
|
|
35
36
|
"build": "npm run guard:runtime-dependency && npm run check:abi && bash scripts/build.sh",
|
package/scripts/build.sh
CHANGED
|
@@ -15,7 +15,7 @@ HOST_EVENT_GENERATOR_BUILD="${PACKAGE_DIR}/build/generate-host-events.mjs"
|
|
|
15
15
|
WORKER_BOOTSTRAP_BUILD="${PACKAGE_DIR}/build/worker-bootstrap.js"
|
|
16
16
|
WORKER_BOOTSTRAP_MAP_BUILD="${PACKAGE_DIR}/build/worker-bootstrap.js.map"
|
|
17
17
|
|
|
18
|
-
mkdir -p "${PACKAGE_DIR}/build" "${OUT_DIR}" "${DEMO_OUT_DIR}" "${DEMO_OUT_DIR}/workbench" "${DEMO_OUT_DIR}/stage4" "${DEMO_OUT_DIR}/stage5"
|
|
18
|
+
mkdir -p "${PACKAGE_DIR}/build" "${OUT_DIR}" "${DEMO_OUT_DIR}" "${DEMO_OUT_DIR}/workbench" "${DEMO_OUT_DIR}/stage4" "${DEMO_OUT_DIR}/stage5" "${DEMO_OUT_DIR}/immediate-drawing"
|
|
19
19
|
|
|
20
20
|
cd "${PACKAGE_DIR}"
|
|
21
21
|
if ! command -v cargo >/dev/null 2>&1 && [ -f "${HOME}/.cargo/env" ]; then
|
|
@@ -127,6 +127,7 @@ generate_host_events "demo/src/host-events.ts" "demoHostEvents" "crates/demo-hom
|
|
|
127
127
|
generate_host_events "demo/src/host-events.ts" "demoHostEvents" "crates/demo-workbench/src/generated/host_events.rs"
|
|
128
128
|
generate_host_events "demo/src/host-events.ts" "demoHostEvents" "crates/demo-stage4/src/generated/host_events.rs"
|
|
129
129
|
generate_host_events "demo/src/host-events.ts" "demoHostEvents" "crates/demo-stage5/src/generated/host_events.rs"
|
|
130
|
+
generate_host_events "demo/src/host-events.ts" "demoHostEvents" "crates/demo-immediate-drawing/src/generated/host_events.rs"
|
|
130
131
|
generate_host_services "demo/src/worker-host-services.ts" "demoWorkerHostServices" "crates/demo-shared/src/generated/worker_host_services.rs" "fui::host_services" "fui_host_service"
|
|
131
132
|
generate_host_services "demo/src/worker-host-services.ts" "demoWorkerHostServices" "crates/demo-worker/src/generated/worker_host_services.rs" "" "fui_host_service"
|
|
132
133
|
generate_host_services "scripts/framework-host-services.ts" "frameworkHostServices" "src/generated/framework_host_services.rs" "crate::host_services" "fui_host"
|
|
@@ -147,10 +148,12 @@ build_demo_route "fui-rs-demo-home" "fui_rs_demo_home" "${DEMO_OUT_DIR}/home.was
|
|
|
147
148
|
build_demo_route "fui-rs-demo-workbench" "fui_rs_demo_workbench" "${DEMO_OUT_DIR}/workbench.wasm"
|
|
148
149
|
build_demo_route "fui-rs-demo-stage4" "fui_rs_demo_stage4" "${DEMO_OUT_DIR}/stage4.wasm"
|
|
149
150
|
build_demo_route "fui-rs-demo-stage5" "fui_rs_demo_stage5" "${DEMO_OUT_DIR}/stage5.wasm"
|
|
151
|
+
build_demo_route "fui-rs-demo-immediate-drawing" "fui_rs_demo_immediate_drawing" "${DEMO_OUT_DIR}/immediate-drawing.wasm"
|
|
150
152
|
build_demo_route "fui-rs-demo-worker" "fui_rs_demo_worker" "${DEMO_OUT_DIR}/workers.wasm"
|
|
151
153
|
cp "${DEMO_OUT_DIR}/workers.wasm" "${DEMO_OUT_DIR}/workbench/workers.wasm"
|
|
152
154
|
cp "${DEMO_OUT_DIR}/workers.wasm" "${DEMO_OUT_DIR}/stage4/workers.wasm"
|
|
153
155
|
cp "${DEMO_OUT_DIR}/workers.wasm" "${DEMO_OUT_DIR}/stage5/workers.wasm"
|
|
156
|
+
cp "${DEMO_OUT_DIR}/workers.wasm" "${DEMO_OUT_DIR}/immediate-drawing/workers.wasm"
|
|
154
157
|
|
|
155
158
|
npx esbuild "${PACKAGE_DIR}/demo/harness.ts" \
|
|
156
159
|
--bundle \
|
|
@@ -187,6 +190,7 @@ cp "${PACKAGE_DIR}/demo/index.html" "${DEMO_OUT_DIR}/index.html"
|
|
|
187
190
|
cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/workbench/index.html"
|
|
188
191
|
cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/stage4/index.html"
|
|
189
192
|
cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/stage5/index.html"
|
|
193
|
+
cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/immediate-drawing/index.html"
|
|
190
194
|
cp "${PACKAGE_DIR}/demo/routes.json" "${DEMO_OUT_DIR}/routes.json"
|
|
191
195
|
rm -f "${DEMO_OUT_DIR}/worker-manifest.json"
|
|
192
196
|
if [ -f "${SHARED_DEMO_TEXTURE}" ]; then
|
|
@@ -195,6 +199,7 @@ if [ -f "${SHARED_DEMO_TEXTURE}" ]; then
|
|
|
195
199
|
cp "${SHARED_DEMO_TEXTURE}" "${DEMO_OUT_DIR}/workbench/demo-texture.png"
|
|
196
200
|
cp "${SHARED_DEMO_TEXTURE}" "${DEMO_OUT_DIR}/stage4/demo-texture.png"
|
|
197
201
|
cp "${SHARED_DEMO_TEXTURE}" "${DEMO_OUT_DIR}/stage5/demo-texture.png"
|
|
202
|
+
cp "${SHARED_DEMO_TEXTURE}" "${DEMO_OUT_DIR}/immediate-drawing/demo-texture.png"
|
|
198
203
|
fi
|
|
199
204
|
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")"
|
|
200
205
|
cat > "${OUT_DIR}/effindom-runtime-config.js" << RUNTIMECFG
|
|
@@ -38,9 +38,9 @@ function rustValueType(type: HostServiceTypeName): string {
|
|
|
38
38
|
|
|
39
39
|
function callbackType(args: readonly HostServiceTypeName[]): string {
|
|
40
40
|
if (args.length === 0) {
|
|
41
|
-
return "
|
|
41
|
+
return "Rc<dyn Fn()>";
|
|
42
42
|
}
|
|
43
|
-
return `
|
|
43
|
+
return `Rc<dyn Fn(${args.map((arg) => rustValueType(arg)).join(", ")})>`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function emitExportArgs(args: readonly HostServiceTypeName[]): string {
|
|
@@ -125,7 +125,21 @@ export async function generateRustHostEventsFile(
|
|
|
125
125
|
"#![allow(dead_code)]",
|
|
126
126
|
"#![allow(non_snake_case)]",
|
|
127
127
|
"",
|
|
128
|
-
"use
|
|
128
|
+
"use fui::HostEventSubscription;",
|
|
129
|
+
"use std::cell::{Cell, RefCell};",
|
|
130
|
+
"use std::rc::Rc;",
|
|
131
|
+
"",
|
|
132
|
+
"thread_local! {",
|
|
133
|
+
" static NEXT_HOST_EVENT_SUBSCRIPTION_ID: Cell<u64> = const { Cell::new(1) };",
|
|
134
|
+
"}",
|
|
135
|
+
"",
|
|
136
|
+
"fn next_host_event_subscription_id() -> u64 {",
|
|
137
|
+
" NEXT_HOST_EVENT_SUBSCRIPTION_ID.with(|next| {",
|
|
138
|
+
" let id = next.get();",
|
|
139
|
+
" next.set(id.wrapping_add(1).max(1));",
|
|
140
|
+
" id",
|
|
141
|
+
" })",
|
|
142
|
+
"}",
|
|
129
143
|
"",
|
|
130
144
|
];
|
|
131
145
|
methods.forEach((method) => {
|
|
@@ -133,36 +147,52 @@ export async function generateRustHostEventsFile(
|
|
|
133
147
|
const callbackTy = callbackType(method.args);
|
|
134
148
|
lines.push("thread_local! {");
|
|
135
149
|
lines.push(
|
|
136
|
-
` static ${handlerName.toUpperCase()}_HANDLER: RefCell<Option
|
|
150
|
+
` static ${handlerName.toUpperCase()}_HANDLER: RefCell<Option<(u64, ${callbackTy})>> = const { RefCell::new(None) };`,
|
|
137
151
|
);
|
|
138
152
|
lines.push("}");
|
|
139
153
|
lines.push("");
|
|
140
154
|
lines.push(
|
|
141
|
-
`pub fn on_${handlerName}(callback: impl Fn(${method.args.map((arg) => rustValueType(arg)).join(", ")}) + 'static) {`,
|
|
155
|
+
`pub fn on_${handlerName}(callback: impl Fn(${method.args.map((arg) => rustValueType(arg)).join(", ")}) + 'static) -> HostEventSubscription {`,
|
|
142
156
|
);
|
|
157
|
+
lines.push(" let subscription_id = next_host_event_subscription_id();");
|
|
143
158
|
lines.push(
|
|
144
|
-
` ${handlerName.toUpperCase()}_HANDLER.with(|slot| *slot.borrow_mut() = Some(
|
|
159
|
+
` ${handlerName.toUpperCase()}_HANDLER.with(|slot| *slot.borrow_mut() = Some((subscription_id, Rc::new(callback))));`,
|
|
145
160
|
);
|
|
161
|
+
lines.push(" HostEventSubscription::new(move || {");
|
|
162
|
+
lines.push(` ${handlerName.toUpperCase()}_HANDLER.with(|slot| {`);
|
|
163
|
+
lines.push(" let should_clear = slot.borrow().as_ref().is_some_and(|(id, _)| *id == subscription_id);");
|
|
164
|
+
lines.push(" if should_clear {");
|
|
165
|
+
lines.push(" *slot.borrow_mut() = None;");
|
|
166
|
+
lines.push(" }");
|
|
167
|
+
lines.push(" });");
|
|
168
|
+
lines.push(" })");
|
|
146
169
|
lines.push("}");
|
|
147
170
|
lines.push("");
|
|
148
171
|
lines.push(`pub fn clear_${handlerName}() {`);
|
|
149
172
|
lines.push(` ${handlerName.toUpperCase()}_HANDLER.with(|slot| *slot.borrow_mut() = None);`);
|
|
150
173
|
lines.push("}");
|
|
151
174
|
lines.push("");
|
|
175
|
+
const hasPointerArgs = method.args.some((type) =>
|
|
176
|
+
type === "string" || type === "bytes" || type.endsWith("_array")
|
|
177
|
+
);
|
|
178
|
+
if (hasPointerArgs) {
|
|
179
|
+
lines.push("/// # Safety");
|
|
180
|
+
lines.push("/// Pointer arguments must reference readable WASM memory for their declared lengths.");
|
|
181
|
+
}
|
|
152
182
|
lines.push("#[no_mangle]");
|
|
153
|
-
lines.push(`pub extern "C" fn ${method.exportName}(${emitExportArgs(method.args)}) {`);
|
|
154
|
-
lines.push(` ${handlerName.toUpperCase()}_HANDLER.with(|slot| {`);
|
|
155
|
-
lines.push("
|
|
156
|
-
lines.push("
|
|
157
|
-
lines.push("
|
|
158
|
-
lines.push("
|
|
183
|
+
lines.push(`pub ${hasPointerArgs ? "unsafe " : ""}extern "C" fn ${method.exportName}(${emitExportArgs(method.args)}) {`);
|
|
184
|
+
lines.push(` let callback = ${handlerName.toUpperCase()}_HANDLER.with(|slot| {`);
|
|
185
|
+
lines.push(" slot.borrow().as_ref().map(|(_, callback)| callback.clone())");
|
|
186
|
+
lines.push(" });");
|
|
187
|
+
lines.push(" let Some(callback) = callback else {");
|
|
188
|
+
lines.push(" return;");
|
|
189
|
+
lines.push(" };");
|
|
159
190
|
lines.push(...emitDecodedArgs(method.args));
|
|
160
191
|
if (method.args.length === 0) {
|
|
161
192
|
lines.push(" callback();");
|
|
162
193
|
} else {
|
|
163
194
|
lines.push(` callback(${method.args.map((_arg, index) => `arg${String(index)}`).join(", ")});`);
|
|
164
195
|
}
|
|
165
|
-
lines.push(" });");
|
|
166
196
|
lines.push("}");
|
|
167
197
|
lines.push("");
|
|
168
198
|
});
|
package/src/app.rs
CHANGED
|
@@ -102,6 +102,12 @@ pub struct ApplicationRegistration {
|
|
|
102
102
|
build_page_fn: Rc<dyn Fn() -> FlexBox>,
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
impl Default for ApplicationRegistration {
|
|
106
|
+
fn default() -> Self {
|
|
107
|
+
Self::new()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
105
111
|
impl ApplicationRegistration {
|
|
106
112
|
pub fn new() -> Self {
|
|
107
113
|
Self {
|
|
@@ -318,6 +324,7 @@ impl Node for NodeRefMount {
|
|
|
318
324
|
fn build_self(&self) {}
|
|
319
325
|
}
|
|
320
326
|
|
|
327
|
+
#[cfg(not(feature = "worker-runtime"))]
|
|
321
328
|
#[no_mangle]
|
|
322
329
|
pub extern "C" fn __flushRenders() {
|
|
323
330
|
Application::flush_renders();
|
package/src/assets.rs
CHANGED
|
@@ -332,7 +332,7 @@ pub fn release_svg_asset(svg_id: u32) {
|
|
|
332
332
|
}
|
|
333
333
|
let remaining = SVG_REF_COUNTS.with(|ref_counts| decrement_ref_count(ref_counts, svg_id));
|
|
334
334
|
let pinned = PINNED_SVG_IDS.with(|ids| ids.borrow().contains(&svg_id));
|
|
335
|
-
if remaining
|
|
335
|
+
if remaining != 0 || pinned {
|
|
336
336
|
return;
|
|
337
337
|
}
|
|
338
338
|
let url = get_svg_record(svg_id).borrow().url.clone();
|
|
@@ -352,7 +352,7 @@ pub fn release_texture_asset(texture_id: u32) {
|
|
|
352
352
|
let remaining =
|
|
353
353
|
TEXTURE_REF_COUNTS.with(|ref_counts| decrement_ref_count(ref_counts, texture_id));
|
|
354
354
|
let pinned = PINNED_TEXTURE_IDS.with(|ids| ids.borrow().contains(&texture_id));
|
|
355
|
-
if remaining
|
|
355
|
+
if remaining != 0 || pinned {
|
|
356
356
|
return;
|
|
357
357
|
}
|
|
358
358
|
let url = get_texture_record(texture_id).borrow().url.clone();
|
package/src/bindings/ui.rs
CHANGED
package/src/bitmap.rs
CHANGED
|
@@ -5,6 +5,7 @@ use crate::frame_scheduler::on_loaded;
|
|
|
5
5
|
use crate::logger::error;
|
|
6
6
|
use crate::node::Node;
|
|
7
7
|
use crate::text::TextLayout;
|
|
8
|
+
use crate::typography::FontFace;
|
|
8
9
|
use std::cell::RefCell;
|
|
9
10
|
use std::rc::Rc;
|
|
10
11
|
|
|
@@ -136,6 +137,7 @@ impl Bitmap {
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
pub fn prepare_text<T: Node>(node: &T) {
|
|
140
|
+
node.build();
|
|
139
141
|
crate::bindings::ui::prepare_node(node.handle().raw());
|
|
140
142
|
}
|
|
141
143
|
|
|
@@ -145,9 +147,17 @@ impl Bitmap {
|
|
|
145
147
|
callback: impl FnOnce(BitmapTextReadyEventArgs) + 'static,
|
|
146
148
|
) -> &Self {
|
|
147
149
|
let node = node.clone();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
let required_font_ids = node.required_font_ids_for_preparation();
|
|
151
|
+
let callback = Rc::new(RefCell::new(Some(callback)));
|
|
152
|
+
FontFace::when_fonts_loaded(&required_font_ids, move |_| {
|
|
153
|
+
let node = node.clone();
|
|
154
|
+
let callback = callback.clone();
|
|
155
|
+
on_loaded(move |_| {
|
|
156
|
+
Self::prepare_text(&node);
|
|
157
|
+
if let Some(callback) = callback.borrow_mut().take() {
|
|
158
|
+
callback(BitmapTextReadyEventArgs::EMPTY);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
151
161
|
});
|
|
152
162
|
self
|
|
153
163
|
}
|
|
@@ -255,6 +265,10 @@ mod tests {
|
|
|
255
265
|
use super::Bitmap;
|
|
256
266
|
use crate::drawing::Paint;
|
|
257
267
|
use crate::ffi::{self, Call};
|
|
268
|
+
use crate::frame_scheduler;
|
|
269
|
+
use crate::node::{Node, TextNode};
|
|
270
|
+
use std::cell::Cell;
|
|
271
|
+
use std::rc::Rc;
|
|
258
272
|
|
|
259
273
|
#[test]
|
|
260
274
|
fn bitmap_canvas_commit_flushes_offscreen_and_marks_asset_ready() {
|
|
@@ -314,4 +328,26 @@ mod tests {
|
|
|
314
328
|
}
|
|
315
329
|
)));
|
|
316
330
|
}
|
|
331
|
+
|
|
332
|
+
#[test]
|
|
333
|
+
fn bitmap_text_ready_builds_detached_text_before_preparing_it() {
|
|
334
|
+
ffi::test::reset();
|
|
335
|
+
frame_scheduler::reset_commit_state();
|
|
336
|
+
let bitmap = Bitmap::new(64, 32);
|
|
337
|
+
let text = TextNode::new("Detached bitmap text");
|
|
338
|
+
let fired = Rc::new(Cell::new(false));
|
|
339
|
+
bitmap.on_text_ready(&text, {
|
|
340
|
+
let fired = fired.clone();
|
|
341
|
+
move |_| fired.set(true)
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
frame_scheduler::fire_loaded_callbacks();
|
|
345
|
+
|
|
346
|
+
assert!(fired.get());
|
|
347
|
+
assert!(text.has_built_handle());
|
|
348
|
+
let calls = ffi::test::take_calls();
|
|
349
|
+
assert!(calls
|
|
350
|
+
.iter()
|
|
351
|
+
.any(|call| matches!(call, Call::PrepareNode { .. })));
|
|
352
|
+
}
|
|
317
353
|
}
|
package/src/bridge_callbacks.rs
CHANGED
|
@@ -105,25 +105,27 @@ pub fn persisted_restore_count() -> u32 {
|
|
|
105
105
|
PERSIST_RESTORE_COUNT.with(Cell::get)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
#[no_mangle]
|
|
108
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
109
109
|
pub extern "C" fn __fui_on_viewport_changed(width: f32, height: f32) {
|
|
110
110
|
ui::resize_window(width, height);
|
|
111
111
|
viewport::set_viewport_size(width, height);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
#[no_mangle]
|
|
114
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
115
115
|
pub extern "C" fn __fui_on_frame(timestamp_ms: f64) {
|
|
116
116
|
frame_signal::set_frame_time(timestamp_ms);
|
|
117
117
|
animation::tick_animations(timestamp_ms);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
#[no_mangle]
|
|
121
|
-
|
|
120
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
121
|
+
/// # Safety
|
|
122
|
+
/// `route_ptr` must be null for an empty route or point to `route_len` readable bytes.
|
|
123
|
+
pub unsafe extern "C" fn __fui_on_route_changed(route_ptr: *const u8, route_len: u32) {
|
|
122
124
|
let route = read_utf8(route_ptr, route_len);
|
|
123
125
|
CURRENT_ROUTE.with(|slot| slot.replace(route));
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
#[no_mangle]
|
|
128
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
127
129
|
pub extern "C" fn __fui_on_scroll(
|
|
128
130
|
handle: u64,
|
|
129
131
|
offset_x: f32,
|
|
@@ -155,25 +157,25 @@ pub extern "C" fn __fui_on_scroll(
|
|
|
155
157
|
);
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
#[no_mangle]
|
|
160
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
159
161
|
pub extern "C" fn __fui_can_show_context_menu(handle: u64) -> bool {
|
|
160
162
|
context_menu_manager::can_show_for_handle(handle)
|
|
161
163
|
}
|
|
162
164
|
|
|
163
|
-
#[no_mangle]
|
|
165
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
164
166
|
pub extern "C" fn __fui_on_context_menu(handle: u64, x: f32, y: f32) {
|
|
165
167
|
LAST_CONTEXT_MENU.with(|slot| slot.replace(Some(ContextMenuRequest { handle, x, y })));
|
|
166
168
|
let shown = context_menu_manager::show_for_current_selection(handle, x, y);
|
|
167
169
|
CONTEXT_MENU_VISIBLE.with(|visible| visible.set(shown));
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
#[no_mangle]
|
|
172
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
171
173
|
pub extern "C" fn __fui_hide_active_context_menu() {
|
|
172
174
|
context_menu_manager::hide_active_menu();
|
|
173
175
|
CONTEXT_MENU_VISIBLE.with(|visible| visible.set(false));
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
#[no_mangle]
|
|
178
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
177
179
|
pub extern "C" fn __fui_on_font_loaded(font_id: u32) {
|
|
178
180
|
LAST_FONT_LOADED.with(|slot| slot.set(Some(font_id)));
|
|
179
181
|
assets::on_font_loaded(font_id);
|
|
@@ -182,7 +184,7 @@ pub extern "C" fn __fui_on_font_loaded(font_id: u32) {
|
|
|
182
184
|
crate::frame_scheduler::mark_needs_commit();
|
|
183
185
|
}
|
|
184
186
|
|
|
185
|
-
#[no_mangle]
|
|
187
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
186
188
|
pub extern "C" fn __fui_on_svg_loaded(svg_id: u32, width: f32, height: f32) {
|
|
187
189
|
LAST_SVG_LOADED.with(|slot| {
|
|
188
190
|
slot.replace(Some(AssetReady {
|
|
@@ -194,8 +196,10 @@ pub extern "C" fn __fui_on_svg_loaded(svg_id: u32, width: f32, height: f32) {
|
|
|
194
196
|
assets::on_svg_loaded(svg_id, width, height);
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
#[no_mangle]
|
|
198
|
-
|
|
199
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
200
|
+
/// # Safety
|
|
201
|
+
/// `error_ptr` must be null for an empty message or point to `error_len` readable bytes.
|
|
202
|
+
pub unsafe extern "C" fn __fui_on_svg_failed(svg_id: u32, error_ptr: *const u8, error_len: u32) {
|
|
199
203
|
let error = read_utf8(error_ptr, error_len);
|
|
200
204
|
LAST_SVG_FAILED.with(|slot| {
|
|
201
205
|
slot.replace(Some(AssetFailure {
|
|
@@ -206,7 +210,7 @@ pub extern "C" fn __fui_on_svg_failed(svg_id: u32, error_ptr: *const u8, error_l
|
|
|
206
210
|
assets::on_svg_failed(svg_id, error);
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
#[no_mangle]
|
|
213
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
210
214
|
pub extern "C" fn __fui_on_texture_loaded(texture_id: u32, width: f32, height: f32) {
|
|
211
215
|
LAST_TEXTURE_LOADED.with(|slot| {
|
|
212
216
|
slot.replace(Some(AssetReady {
|
|
@@ -218,8 +222,14 @@ pub extern "C" fn __fui_on_texture_loaded(texture_id: u32, width: f32, height: f
|
|
|
218
222
|
assets::on_texture_loaded(texture_id, width, height);
|
|
219
223
|
}
|
|
220
224
|
|
|
221
|
-
#[no_mangle]
|
|
222
|
-
|
|
225
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
226
|
+
/// # Safety
|
|
227
|
+
/// `error_ptr` must be null for an empty message or point to `error_len` readable bytes.
|
|
228
|
+
pub unsafe extern "C" fn __fui_on_texture_failed(
|
|
229
|
+
texture_id: u32,
|
|
230
|
+
error_ptr: *const u8,
|
|
231
|
+
error_len: u32,
|
|
232
|
+
) {
|
|
223
233
|
let error = read_utf8(error_ptr, error_len);
|
|
224
234
|
LAST_TEXTURE_FAILED.with(|slot| {
|
|
225
235
|
slot.replace(Some(AssetFailure {
|
|
@@ -230,13 +240,13 @@ pub extern "C" fn __fui_on_texture_failed(texture_id: u32, error_ptr: *const u8,
|
|
|
230
240
|
assets::on_texture_failed(texture_id, error);
|
|
231
241
|
}
|
|
232
242
|
|
|
233
|
-
#[no_mangle]
|
|
243
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
234
244
|
pub extern "C" fn __fui_capture_persisted_ui_state() {
|
|
235
245
|
PERSIST_CAPTURE_COUNT.with(|count| count.set(count.get() + 1));
|
|
236
246
|
Application::capture_persisted_ui_state();
|
|
237
247
|
}
|
|
238
248
|
|
|
239
|
-
#[no_mangle]
|
|
249
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
240
250
|
pub extern "C" fn __fui_restore_persisted_ui_state() {
|
|
241
251
|
PERSIST_RESTORE_COUNT.with(|count| count.set(count.get() + 1));
|
|
242
252
|
Application::restore_persisted_ui_state();
|
package/src/controls/button.rs
CHANGED
package/src/controls/checkbox.rs
CHANGED
package/src/controls/combobox.rs
CHANGED
|
@@ -28,6 +28,9 @@ use crate::{app, frame_scheduler, ThemeBindable};
|
|
|
28
28
|
use std::cell::{Cell, RefCell};
|
|
29
29
|
use std::rc::{Rc, Weak};
|
|
30
30
|
|
|
31
|
+
type ComboBoxChangedCallback = Rc<dyn Fn(crate::controls::ComboBoxChangedEventArgs<ComboBoxItem>)>;
|
|
32
|
+
type ComboBoxTextChangedCallback = Rc<dyn Fn(TextChangedEventArgs)>;
|
|
33
|
+
|
|
31
34
|
const DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA: f32 = 10.0;
|
|
32
35
|
|
|
33
36
|
fn strings_equal_ignore_case(left: &str, right: &str) -> bool {
|
|
@@ -211,9 +214,8 @@ struct ComboBoxShared {
|
|
|
211
214
|
popup_panel_background_blur_overridden: Cell<bool>,
|
|
212
215
|
suppress_editor_changed: Cell<bool>,
|
|
213
216
|
last_auto_complete_text_value: RefCell<String>,
|
|
214
|
-
changed_callback:
|
|
215
|
-
|
|
216
|
-
text_changed_callback: RefCell<Option<Rc<dyn Fn(TextChangedEventArgs)>>>,
|
|
217
|
+
changed_callback: RefCell<Option<ComboBoxChangedCallback>>,
|
|
218
|
+
text_changed_callback: RefCell<Option<ComboBoxTextChangedCallback>>,
|
|
217
219
|
theme_guard: RefCell<Option<SubscriptionGuard>>,
|
|
218
220
|
focus_visibility_guard: RefCell<Option<SubscriptionGuard>>,
|
|
219
221
|
}
|
package/src/controls/dropdown.rs
CHANGED
|
@@ -26,6 +26,8 @@ use crate::ThemeBindable;
|
|
|
26
26
|
use std::cell::{Cell, RefCell};
|
|
27
27
|
use std::rc::{Rc, Weak};
|
|
28
28
|
|
|
29
|
+
type DropdownChangedCallback = Rc<dyn Fn(DropdownChangedEventArgs<DropdownItem>)>;
|
|
30
|
+
|
|
29
31
|
const DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA: f32 = 10.0;
|
|
30
32
|
|
|
31
33
|
fn is_activation_key(event: &KeyEventArgs) -> bool {
|
|
@@ -110,7 +112,7 @@ struct DropdownShared {
|
|
|
110
112
|
popup_panel_background_blur_sigma_value: Cell<f32>,
|
|
111
113
|
popup_panel_color_overridden: Cell<bool>,
|
|
112
114
|
popup_panel_background_blur_overridden: Cell<bool>,
|
|
113
|
-
changed_callback: RefCell<Option<
|
|
115
|
+
changed_callback: RefCell<Option<DropdownChangedCallback>>,
|
|
114
116
|
theme_guard: RefCell<Option<SubscriptionGuard>>,
|
|
115
117
|
focus_visibility_guard: RefCell<Option<SubscriptionGuard>>,
|
|
116
118
|
}
|
|
@@ -25,6 +25,10 @@ use std::rc::{Rc, Weak};
|
|
|
25
25
|
|
|
26
26
|
const UNLIMITED_TEXT_LENGTH: i32 = i32::MAX;
|
|
27
27
|
|
|
28
|
+
type TextChangedCallback = Rc<dyn Fn(TextChangedEventArgs)>;
|
|
29
|
+
type SelectionChangedCallback = Rc<dyn Fn(SelectionChangedEventArgs)>;
|
|
30
|
+
type FocusChangedCallback = Rc<dyn Fn(FocusChangedEventArgs)>;
|
|
31
|
+
|
|
28
32
|
#[derive(Clone, Copy)]
|
|
29
33
|
pub(crate) struct TextInputProfile {
|
|
30
34
|
max_lines: i32,
|
|
@@ -140,9 +144,9 @@ pub struct TextInputCore {
|
|
|
140
144
|
selection_start_bytes: Cell<u32>,
|
|
141
145
|
selection_end_bytes: Cell<u32>,
|
|
142
146
|
focused_state: Cell<bool>,
|
|
143
|
-
changed_callback: RefCell<Option<
|
|
144
|
-
selection_changed_callback: RefCell<Option<
|
|
145
|
-
focus_changed_callback: RefCell<Option<
|
|
147
|
+
changed_callback: RefCell<Option<TextChangedCallback>>,
|
|
148
|
+
selection_changed_callback: RefCell<Option<SelectionChangedCallback>>,
|
|
149
|
+
focus_changed_callback: RefCell<Option<FocusChangedCallback>>,
|
|
146
150
|
theme_guard: RefCell<Option<SubscriptionGuard>>,
|
|
147
151
|
focus_visibility_guard: RefCell<Option<SubscriptionGuard>>,
|
|
148
152
|
}
|
package/src/controls/slider.rs
CHANGED