@effindomv2/fui-rs 0.1.15 → 0.1.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fui-rs"
3
- version = "0.1.15"
3
+ version = "0.1.17"
4
4
  edition = "2021"
5
5
  license = "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial"
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effindomv2/fui-rs",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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
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
  }
package/src/drawing.rs CHANGED
@@ -2,7 +2,7 @@ use crate::ffi;
2
2
  use crate::image_sampling::ImageSampling;
3
3
  use crate::logger::error;
4
4
  use crate::node::Node;
5
- use crate::text::TextLayout;
5
+ use crate::text::{DynamicTextLayout, TextLayout};
6
6
  use std::cell::RefCell;
7
7
  use std::rc::Rc;
8
8
 
@@ -63,36 +63,45 @@ impl Paint {
63
63
  }
64
64
  }
65
65
 
66
- pub struct Path {
66
+ struct PathResource {
67
67
  id: u32,
68
- disposed: bool,
68
+ }
69
+
70
+ impl Drop for PathResource {
71
+ fn drop(&mut self) {
72
+ unsafe { ffi::fui_path_destroy(self.id) };
73
+ }
74
+ }
75
+
76
+ #[derive(Clone)]
77
+ pub struct Path {
78
+ resource: Rc<PathResource>,
69
79
  }
70
80
 
71
81
  impl Path {
72
82
  pub fn new() -> Self {
73
83
  let id = unsafe { ffi::fui_path_create() };
74
84
  Self {
75
- id,
76
- disposed: false,
85
+ resource: Rc::new(PathResource { id }),
77
86
  }
78
87
  }
79
88
 
80
89
  pub fn id(&self) -> u32 {
81
- self.id
90
+ self.resource.id
82
91
  }
83
92
 
84
93
  pub fn move_to(&mut self, x: f32, y: f32) -> &mut Self {
85
- unsafe { ffi::fui_path_move_to(self.id, x, y) };
94
+ unsafe { ffi::fui_path_move_to(self.id(), x, y) };
86
95
  self
87
96
  }
88
97
 
89
98
  pub fn line_to(&mut self, x: f32, y: f32) -> &mut Self {
90
- unsafe { ffi::fui_path_line_to(self.id, x, y) };
99
+ unsafe { ffi::fui_path_line_to(self.id(), x, y) };
91
100
  self
92
101
  }
93
102
 
94
103
  pub fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) -> &mut Self {
95
- unsafe { ffi::fui_path_quad_to(self.id, cx, cy, x, y) };
104
+ unsafe { ffi::fui_path_quad_to(self.id(), cx, cy, x, y) };
96
105
  self
97
106
  }
98
107
 
@@ -105,39 +114,31 @@ impl Path {
105
114
  x: f32,
106
115
  y: f32,
107
116
  ) -> &mut Self {
108
- unsafe { ffi::fui_path_cubic_to(self.id, cx1, cy1, cx2, cy2, x, y) };
117
+ unsafe { ffi::fui_path_cubic_to(self.id(), cx1, cy1, cx2, cy2, x, y) };
109
118
  self
110
119
  }
111
120
 
112
121
  pub fn close(&mut self) -> &mut Self {
113
- unsafe { ffi::fui_path_close(self.id) };
122
+ unsafe { ffi::fui_path_close(self.id()) };
114
123
  self
115
124
  }
116
125
 
117
126
  pub fn add_rect(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Self {
118
- unsafe { ffi::fui_path_add_rect(self.id, x, y, w, h) };
127
+ unsafe { ffi::fui_path_add_rect(self.id(), x, y, w, h) };
119
128
  self
120
129
  }
121
130
 
122
131
  pub fn add_circle(&mut self, cx: f32, cy: f32, r: f32) -> &mut Self {
123
- unsafe { ffi::fui_path_add_circle(self.id, cx, cy, r) };
132
+ unsafe { ffi::fui_path_add_circle(self.id(), cx, cy, r) };
124
133
  self
125
134
  }
126
135
  }
127
136
 
128
- impl Drop for Path {
129
- fn drop(&mut self) {
130
- if !self.disposed {
131
- unsafe { ffi::fui_path_destroy(self.id) };
132
- self.disposed = true;
133
- }
134
- }
135
- }
136
-
137
137
  #[derive(Default)]
138
138
  struct DrawContextState {
139
139
  canvas_ptr: usize,
140
140
  words: Vec<u32>,
141
+ retained_paths: Vec<Path>,
141
142
  }
142
143
 
143
144
  #[derive(Clone, Default)]
@@ -151,6 +152,7 @@ impl DrawContext {
151
152
  inner: Rc::new(RefCell::new(DrawContextState {
152
153
  canvas_ptr,
153
154
  words: Vec::new(),
155
+ retained_paths: Vec::new(),
154
156
  })),
155
157
  }
156
158
  }
@@ -172,6 +174,7 @@ impl DrawContext {
172
174
  )
173
175
  };
174
176
  state.words.clear();
177
+ state.retained_paths.clear();
175
178
  }
176
179
 
177
180
  pub fn save(&self) {
@@ -289,6 +292,7 @@ impl DrawContext {
289
292
  state.words.push(paint.fill_color);
290
293
  state.words.push(paint.stroke_color);
291
294
  Self::push_float(&mut state.words, paint.stroke_width);
295
+ state.retained_paths.push(path.clone());
292
296
  }
293
297
 
294
298
  pub fn draw_text_node<T: Node>(&self, node: &T, x: f32, y: f32) {
@@ -313,6 +317,18 @@ impl DrawContext {
313
317
  self.draw_text_node(&node, x, y);
314
318
  }
315
319
 
320
+ pub fn draw_dynamic_text_layout(&self, layout: &DynamicTextLayout, x: f32, y: f32) {
321
+ if !layout.is_ready() {
322
+ error(
323
+ "DynamicTextLayout",
324
+ "DrawContext.draw_dynamic_text_layout() called before the DynamicTextLayout was ready; register on_ready and draw after the callback.",
325
+ );
326
+ return;
327
+ }
328
+ let node = layout.draw_node();
329
+ self.draw_text_node(&node, x, y);
330
+ }
331
+
316
332
  pub fn draw_image(&self, texture_id: u32, x: f32, y: f32, w: f32, h: f32) {
317
333
  self.draw_image_sampling(texture_id, x, y, w, h, ImageSampling::linear());
318
334
  }
@@ -351,8 +367,13 @@ impl DrawContext {
351
367
  #[cfg(test)]
352
368
  mod tests {
353
369
  use super::{DrawContext, Paint, Path};
370
+ use crate::assets;
354
371
  use crate::ffi::{self, Call};
372
+ use crate::frame_scheduler;
355
373
  use crate::image_sampling::ImageSampling;
374
+ use crate::text::DynamicTextLayout;
375
+ use crate::typography::FontStack;
376
+ use crate::Unit;
356
377
 
357
378
  #[test]
358
379
  fn path_and_draw_context_emit_batched_host_calls() {
@@ -382,4 +403,33 @@ mod tests {
382
403
  Call::CanvasDrawBatch { canvas_ptr: 77, words } if !words.is_empty()
383
404
  )));
384
405
  }
406
+
407
+ #[test]
408
+ fn draw_dynamic_text_layout_emits_text_command_without_exposing_draw_node() {
409
+ ffi::test::reset();
410
+ frame_scheduler::reset_commit_state();
411
+ assets::test_reset();
412
+
413
+ let layout = DynamicTextLayout::fixed_charset("0123456789");
414
+ layout
415
+ .font_stack(FontStack::from_id(1), 14.0)
416
+ .width(72.0, Unit::Pixel)
417
+ .height(20.0, Unit::Pixel)
418
+ .set_text("42");
419
+ layout.on_ready(|_| {});
420
+ frame_scheduler::fire_loaded_callbacks();
421
+ assert!(layout.is_ready());
422
+ ffi::test::take_calls();
423
+
424
+ let ctx = DrawContext::new(88);
425
+ ctx.draw_dynamic_text_layout(&layout, 5.0, 7.0);
426
+ ctx.flush();
427
+
428
+ let calls = ffi::test::take_calls();
429
+ assert!(calls.iter().any(|call| matches!(
430
+ call,
431
+ Call::CanvasDrawBatch { canvas_ptr: 88, words }
432
+ if words.first() == Some(&super::OP_DRAW_TEXT_NODE)
433
+ )));
434
+ }
385
435
  }
package/src/lib.rs CHANGED
@@ -441,8 +441,8 @@ pub mod prelude {
441
441
  pub use crate::node::{
442
442
  auto, column, custom_drawable, fill, flex_box, grid, image, pct, portal, px, row,
443
443
  scroll_box, scroll_view, svg, text, viewport_height, viewport_width, virtual_list, Border,
444
- Child, ContextMenuEventArgs, Corners, CustomDrawable, EdgeInsets, FlexBox, FlexBoxSurface,
445
- GradientStop, Grid, GridTrack, HasFlexBoxRoot, Image, ImageNode, Length, Node,
444
+ Child, ContextMenuEventArgs, Corners, CustomDrawable, DrawableInvalidator, EdgeInsets,
445
+ FlexBox, FlexBoxSurface, GradientStop, Grid, GridTrack, HasFlexBoxRoot, Image, ImageNode, Length, Node,
446
446
  PresenterHostStyle, ScrollBar, ScrollBarVisibility, ScrollBox, ScrollState, ScrollView,
447
447
  Shadow, Svg, SvgNode, Text, TextCore, TextNode, ThemeBindable, VirtualList,
448
448
  };
@@ -559,8 +559,8 @@ pub use navigation::*;
559
559
  pub use node::{
560
560
  auto, column, custom_drawable, fill, flex_box, grid, image, pct, portal, px, row, scroll_box,
561
561
  scroll_view, svg, text, viewport_height, viewport_width, virtual_list, Border, Child,
562
- ContextMenuEventArgs, Corners, CustomDrawable, EdgeInsets, FlexBox, FlexBoxSurface,
563
- GradientStop, Grid, GridTrack, HasFlexBoxRoot, Image, ImageNode, Length, Node,
562
+ ContextMenuEventArgs, Corners, CustomDrawable, DrawableInvalidator, EdgeInsets, FlexBox,
563
+ FlexBoxSurface, GradientStop, Grid, GridTrack, HasFlexBoxRoot, Image, ImageNode, Length, Node,
564
564
  PresenterHostStyle, ScrollBar, ScrollBarVisibility, ScrollBox, ScrollState, ScrollView, Shadow,
565
565
  Svg, SvgNode, Text, TextCore, TextNode, ThemeBindable, VirtualList,
566
566
  };
package/src/node/core.rs CHANGED
@@ -1577,6 +1577,11 @@ pub trait Node: Clone {
1577
1577
  #[doc(hidden)]
1578
1578
  fn build_self(&self);
1579
1579
 
1580
+ #[doc(hidden)]
1581
+ fn required_font_ids_for_preparation(&self) -> Vec<u32> {
1582
+ Vec::new()
1583
+ }
1584
+
1580
1585
  fn node_id(&self, node_id: impl Into<String>) -> &Self
1581
1586
  where
1582
1587
  Self: Sized,
@@ -7,6 +7,19 @@ pub struct CustomDrawable {
7
7
  draw_callback: DrawCallback,
8
8
  }
9
9
 
10
+ #[derive(Clone)]
11
+ pub struct DrawableInvalidator {
12
+ base: WeakFlexBox,
13
+ }
14
+
15
+ impl DrawableInvalidator {
16
+ pub fn mark_dirty(&self) {
17
+ if let Some(base) = self.base.upgrade() {
18
+ mark_base_dirty(&base);
19
+ }
20
+ }
21
+ }
22
+
10
23
  impl CustomDrawable {
11
24
  pub fn new(handler: impl Fn(&mut DrawContext) + 'static) -> Self {
12
25
  let base = FlexBox::default();
@@ -18,19 +31,29 @@ impl CustomDrawable {
18
31
  }
19
32
 
20
33
  pub fn mark_dirty(&self) {
21
- let handle = self.handle();
22
- if handle != NodeHandle::INVALID {
23
- let Some(bounds) = ui::get_visible_bounds(handle.raw()) else {
24
- return;
25
- };
26
- if bounds[2] <= 0.0 || bounds[3] <= 0.0 {
27
- return;
28
- }
34
+ mark_base_dirty(&self.base);
35
+ }
36
+
37
+ pub fn invalidator(&self) -> DrawableInvalidator {
38
+ DrawableInvalidator {
39
+ base: self.base.downgrade(),
29
40
  }
30
- crate::frame_scheduler::mark_needs_commit();
31
41
  }
32
42
  }
33
43
 
44
+ fn mark_base_dirty(base: &FlexBox) {
45
+ let handle = base.handle();
46
+ if handle != NodeHandle::INVALID {
47
+ let Some(bounds) = ui::get_visible_bounds(handle.raw()) else {
48
+ return;
49
+ };
50
+ if bounds[2] <= 0.0 || bounds[3] <= 0.0 {
51
+ return;
52
+ }
53
+ }
54
+ crate::frame_scheduler::mark_needs_commit();
55
+ }
56
+
34
57
  impl Node for CustomDrawable {
35
58
  fn retained_node_ref(&self) -> NodeRef {
36
59
  NodeRef::from_node(self.base.core.clone(), self.clone())
@@ -117,5 +140,9 @@ mod tests {
117
140
  .corner_radius(12.0)
118
141
  .bg_color(0x112233FF)
119
142
  .clip_to_bounds(true);
143
+
144
+ let invalidator = drawable.invalidator();
145
+ drop(drawable);
146
+ invalidator.mark_dirty();
120
147
  }
121
148
  }
package/src/node/mod.rs CHANGED
@@ -37,7 +37,7 @@ mod virtual_list;
37
37
  pub use core::NodeRef;
38
38
  pub use core::{ContextMenuEventArgs, Node, NodeHandle};
39
39
  pub(crate) use core::{WeakFlexBox, WeakNodeRef};
40
- pub use custom_drawable::CustomDrawable;
40
+ pub use custom_drawable::{CustomDrawable, DrawableInvalidator};
41
41
  pub use flex_box::{Border, FlexBox, GradientStop};
42
42
  pub use grid::{Grid, GridTrack};
43
43
  pub(crate) use helpers::*;
@@ -655,6 +655,10 @@ impl Node for TextNode {
655
655
  self.core.borrow().behavior.clone(),
656
656
  );
657
657
  }
658
+
659
+ fn required_font_ids_for_preparation(&self) -> Vec<u32> {
660
+ self.required_font_ids()
661
+ }
658
662
  }
659
663
 
660
664
  impl super::ThemeBindable for TextNode {
package/src/text.rs CHANGED
@@ -255,6 +255,10 @@ impl Node for RichText {
255
255
  fn build_self(&self) {
256
256
  self.node.build_self();
257
257
  }
258
+
259
+ fn required_font_ids_for_preparation(&self) -> Vec<u32> {
260
+ self.node.required_font_ids()
261
+ }
258
262
  }
259
263
 
260
264
  fn compile_rich_text_state(state: &RichTextState) -> CompiledRichText {
@@ -781,6 +785,8 @@ impl DynamicTextLayout {
781
785
  layout.set_text(value);
782
786
  if was_ready {
783
787
  layout.prepare_or_wait();
788
+ } else {
789
+ layout.schedule_ready();
784
790
  }
785
791
  true
786
792
  }
@@ -853,12 +859,15 @@ impl DynamicTextLayout {
853
859
  value.chars().all(|ch| charset.contains(ch))
854
860
  }
855
861
 
856
- fn include_in_charset(state: &mut DynamicTextLayoutState, value: &str) {
862
+ fn include_in_charset(state: &mut DynamicTextLayoutState, value: &str) -> bool {
863
+ let mut changed = false;
857
864
  for ch in value.chars() {
858
865
  if !state.charset.contains(ch) {
859
866
  state.charset.push(ch);
867
+ changed = true;
860
868
  }
861
869
  }
870
+ changed
862
871
  }
863
872
 
864
873
  fn refresh_numeric_text(&self) {
@@ -880,12 +889,14 @@ impl DynamicTextLayout {
880
889
  let mut state = self.inner.borrow_mut();
881
890
  let prefix = state.numeric_prefix.clone();
882
891
  let suffix = state.numeric_suffix.clone();
883
- Self::include_in_charset(&mut state, &prefix);
884
- Self::include_in_charset(&mut state, &suffix);
892
+ let charset_changed = Self::include_in_charset(&mut state, &prefix)
893
+ | Self::include_in_charset(&mut state, &suffix);
885
894
  let charset = state.charset.clone();
886
895
  let layout = state.layout.clone();
887
896
  drop(state);
888
- layout.set_dynamic_charset_internal(charset);
897
+ if charset_changed {
898
+ layout.set_dynamic_charset_internal(charset);
899
+ }
889
900
  format!("{prefix}{}{suffix}", self.format_numeric_value(value))
890
901
  }
891
902
 
@@ -1077,4 +1088,30 @@ mod tests {
1077
1088
  .iter()
1078
1089
  .any(|call| matches!(call, Call::SetDynamicTextCharset { charset, .. } if charset == "0123456789")));
1079
1090
  }
1091
+
1092
+ #[test]
1093
+ fn numeric_dynamic_text_layout_stays_ready_across_value_updates() {
1094
+ ffi::test::reset();
1095
+ frame_scheduler::reset_commit_state();
1096
+ assets::test_reset();
1097
+
1098
+ let layout = DynamicTextLayout::numeric();
1099
+ layout
1100
+ .precision(0)
1101
+ .font_stack(crate::typography::FontStack::from_id(1), 16.0)
1102
+ .width(120.0, Unit::Pixel)
1103
+ .height(24.0, Unit::Pixel)
1104
+ .on_ready(|_| {});
1105
+ frame_scheduler::fire_loaded_callbacks();
1106
+ assert!(layout.is_ready());
1107
+ ffi::test::take_calls();
1108
+
1109
+ assert!(layout.set_value(42.0));
1110
+ assert!(layout.is_ready());
1111
+ assert_eq!(layout.current_text(), "42");
1112
+ let calls = ffi::test::take_calls();
1113
+ assert!(calls
1114
+ .iter()
1115
+ .any(|call| matches!(call, Call::PrepareNode { .. })));
1116
+ }
1080
1117
  }
package/src/typography.rs CHANGED
@@ -158,7 +158,7 @@ impl FontFace {
158
158
  }
159
159
 
160
160
  pub(crate) fn is_font_loaded(font_id: u32) -> bool {
161
- font_id == 0 || assets::is_font_loaded(font_id)
161
+ font_id == 0 || (1..=6).contains(&font_id) || assets::is_font_loaded(font_id)
162
162
  }
163
163
 
164
164
  pub fn on_loaded(&self, callback: impl Fn(FontFaceLoadedEventArgs) + 'static) -> Self {
@@ -502,6 +502,14 @@ mod tests {
502
502
  assert_eq!(fired.get(), 1);
503
503
  }
504
504
 
505
+ #[test]
506
+ fn built_in_font_faces_are_preloaded_like_fui_as() {
507
+ assets::test_reset();
508
+ for font_id in 1..=6 {
509
+ assert!(FontFace::new(font_id).is_loaded());
510
+ }
511
+ }
512
+
505
513
  #[test]
506
514
  fn font_stack_reports_required_font_ids() {
507
515
  let stack = FontStack::from_id(1)