@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 +1 -1
- package/package.json +3 -2
- package/scripts/build.sh +6 -1
- package/src/bitmap.rs +39 -3
- package/src/drawing.rs +72 -22
- package/src/lib.rs +4 -4
- package/src/node/core.rs +5 -0
- package/src/node/custom_drawable.rs +36 -9
- package/src/node/mod.rs +1 -1
- package/src/node/text_node.rs +4 -0
- package/src/text.rs +41 -4
- package/src/typography.rs +9 -1
package/Cargo.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effindomv2/fui-rs",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
|
|
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/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
|
-
|
|
66
|
+
struct PathResource {
|
|
67
67
|
id: u32,
|
|
68
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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::*;
|
package/src/node/text_node.rs
CHANGED
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
|
-
|
|
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
|
-
|
|
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)
|