@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.
Files changed (45) hide show
  1. package/Cargo.toml +1 -1
  2. package/README.md +69 -0
  3. package/package.json +3 -2
  4. package/scripts/build.sh +6 -1
  5. package/scripts/hostgen/rust-host-events.ts +43 -13
  6. package/src/app.rs +7 -0
  7. package/src/assets.rs +2 -2
  8. package/src/bindings/ui.rs +2 -0
  9. package/src/bitmap.rs +39 -3
  10. package/src/bridge_callbacks.rs +27 -17
  11. package/src/controls/button.rs +1 -0
  12. package/src/controls/checkbox.rs +1 -0
  13. package/src/controls/combobox.rs +5 -3
  14. package/src/controls/dropdown.rs +3 -1
  15. package/src/controls/internal/pressable_labeled_control.rs +1 -0
  16. package/src/controls/internal/text_input_core.rs +7 -3
  17. package/src/controls/radio_button.rs +1 -0
  18. package/src/controls/slider.rs +1 -0
  19. package/src/controls/switch.rs +1 -0
  20. package/src/controls/tests.rs +19 -7
  21. package/src/debug.rs +8 -5
  22. package/src/drag_drop.rs +1 -3
  23. package/src/drawing.rs +55 -1
  24. package/src/event.rs +39 -27
  25. package/src/fetch.rs +21 -7
  26. package/src/ffi.rs +3 -1
  27. package/src/file.rs +88 -59
  28. package/src/host_events.rs +41 -0
  29. package/src/lib.rs +52 -9
  30. package/src/node/core.rs +7 -21
  31. package/src/node/helpers.rs +1 -1
  32. package/src/node/mod.rs +1 -1
  33. package/src/node/scroll_bar.rs +101 -0
  34. package/src/node/scroll_box.rs +7 -0
  35. package/src/node/scroll_view.rs +20 -14
  36. package/src/node/text_node.rs +4 -0
  37. package/src/node/virtual_list.rs +120 -33
  38. package/src/platform.rs +3 -0
  39. package/src/popup_presenter.rs +1 -0
  40. package/src/text.rs +41 -4
  41. package/src/theme.rs +2 -2
  42. package/src/timers.rs +1 -1
  43. package/src/typography.rs +15 -15
  44. package/src/worker.rs +25 -9
  45. package/src/worker_runtime.rs +61 -17
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fui-rs"
3
- version = "0.1.16"
3
+ version = "0.1.18"
4
4
  edition = "2021"
5
5
  license = "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial"
6
6
 
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.16",
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 "Box<dyn Fn()>";
41
+ return "Rc<dyn Fn()>";
42
42
  }
43
- return `Box<dyn Fn(${args.map((arg) => rustValueType(arg)).join(", ")})>`;
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 std::cell::RefCell;",
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<${callbackTy}>> = const { RefCell::new(None) };`,
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(Box::new(callback)));`,
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(" let handler = slot.borrow();");
156
- lines.push(" let Some(callback) = handler.as_ref() else {");
157
- lines.push(" return;");
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 > 0 || remaining < 0 || pinned {
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 > 0 || remaining < 0 || pinned {
355
+ if remaining != 0 || pinned {
356
356
  return;
357
357
  }
358
358
  let url = get_texture_record(texture_id).borrow().url.clone();
@@ -1,3 +1,5 @@
1
+ #![allow(clippy::too_many_arguments)]
2
+
1
3
  use crate::ffi;
2
4
  use crate::ffi::{GridUnit, ImageSamplingKind};
3
5
 
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
- on_loaded(move |_| {
149
- Self::prepare_text(&node);
150
- callback(BitmapTextReadyEventArgs::EMPTY);
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
  }
@@ -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
- pub extern "C" fn __fui_on_route_changed(route_ptr: *const u8, route_len: u32) {
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
- pub extern "C" fn __fui_on_svg_failed(svg_id: u32, error_ptr: *const u8, error_len: u32) {
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
- pub extern "C" fn __fui_on_texture_failed(texture_id: u32, error_ptr: *const u8, error_len: u32) {
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();
@@ -374,6 +374,7 @@ impl LabeledControlTextStyle for Button {
374
374
  }
375
375
  }
376
376
 
377
+ #[allow(clippy::too_many_arguments)]
377
378
  fn sync_button_visual_state(
378
379
  root: &FlexBox,
379
380
  presenter: &Rc<RefCell<Rc<dyn ButtonPresenter>>>,
@@ -409,6 +409,7 @@ impl CheckboxEventTarget {
409
409
  }
410
410
  }
411
411
 
412
+ #[allow(clippy::too_many_arguments)]
412
413
  fn apply_checkbox_state(
413
414
  presenter: &Rc<RefCell<Rc<dyn CheckboxIndicatorPresenter>>>,
414
415
  weak_root: &Rc<WeakNodeRef>,
@@ -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
- RefCell<Option<Rc<dyn Fn(crate::controls::ComboBoxChangedEventArgs<ComboBoxItem>)>>>,
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
  }
@@ -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<Rc<dyn Fn(DropdownChangedEventArgs<DropdownItem>)>>>,
115
+ changed_callback: RefCell<Option<DropdownChangedCallback>>,
114
116
  theme_guard: RefCell<Option<SubscriptionGuard>>,
115
117
  focus_visibility_guard: RefCell<Option<SubscriptionGuard>>,
116
118
  }
@@ -372,6 +372,7 @@ impl WeakPressableLabeledControl {
372
372
  }
373
373
  }
374
374
 
375
+ #[allow(clippy::too_many_arguments)]
375
376
  fn sync_base_theme_parts(
376
377
  root: &FlexBox,
377
378
  label_node: &TextCore,
@@ -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<Rc<dyn Fn(TextChangedEventArgs)>>>,
144
- selection_changed_callback: RefCell<Option<Rc<dyn Fn(SelectionChangedEventArgs)>>>,
145
- focus_changed_callback: RefCell<Option<Rc<dyn Fn(FocusChangedEventArgs)>>>,
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
  }
@@ -383,6 +383,7 @@ impl RadioEventTarget {
383
383
  }
384
384
  }
385
385
 
386
+ #[allow(clippy::too_many_arguments)]
386
387
  fn apply_radio_checked(
387
388
  presenter: &Rc<RefCell<Rc<dyn RadioIndicatorPresenter>>>,
388
389
  weak_root: &Rc<WeakNodeRef>,
@@ -673,6 +673,7 @@ impl SliderEventTarget {
673
673
  }
674
674
  }
675
675
 
676
+ #[allow(clippy::too_many_arguments)]
676
677
  fn create_slider_visual_state(
677
678
  value: f32,
678
679
  min: f32,
@@ -324,6 +324,7 @@ impl SwitchEventTarget {
324
324
  }
325
325
  }
326
326
 
327
+ #[allow(clippy::too_many_arguments)]
327
328
  fn apply_switch_checked(
328
329
  presenter: &Rc<RefCell<Rc<dyn SwitchIndicatorPresenter>>>,
329
330
  weak_root: &Rc<WeakNodeRef>,