@effindomv2/fui-rs 0.1.17 → 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 +1 -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/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 +8 -0
- 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 +2 -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/virtual_list.rs +120 -33
- package/src/platform.rs +3 -0
- package/src/popup_presenter.rs +1 -0
- package/src/theme.rs +2 -2
- package/src/timers.rs +1 -1
- package/src/typography.rs +6 -14
- 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
|
@@ -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/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
package/src/controls/switch.rs
CHANGED
package/src/controls/tests.rs
CHANGED
|
@@ -213,7 +213,9 @@ fn press_enter(node_ref: &crate::node::NodeRef) {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
fn key_event(event_type: KeyEventType, key: &str, modifiers: u32) -> bool {
|
|
216
|
-
|
|
216
|
+
unsafe {
|
|
217
|
+
event::__fui_on_key_event(event_type as u32, key.as_ptr(), key.len() as u32, modifiers)
|
|
218
|
+
}
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
fn blur(node_ref: &crate::node::NodeRef) {
|
|
@@ -1407,7 +1409,7 @@ fn retained_dropdown_keeps_theme_subscription_after_wrapper_drop() {
|
|
|
1407
1409
|
let mut theme = current_theme();
|
|
1408
1410
|
theme.colors.surface = 0x135724FF;
|
|
1409
1411
|
theme.colors.border = 0x246813FF;
|
|
1410
|
-
theme.colors.text_primary =
|
|
1412
|
+
theme.colors.text_primary = 0xABCDEFFF;
|
|
1411
1413
|
use_custom_theme(theme.clone());
|
|
1412
1414
|
let calls = ffi::test::take_calls();
|
|
1413
1415
|
|
|
@@ -4212,7 +4214,9 @@ fn text_input_text_replaced_event_updates_value_and_emits_changed() {
|
|
|
4212
4214
|
})
|
|
4213
4215
|
.expect("expected text input editor node id to be applied");
|
|
4214
4216
|
|
|
4215
|
-
|
|
4217
|
+
unsafe {
|
|
4218
|
+
event::__fui_on_text_replaced(editor_handle, 0, 0, b"hello".as_ptr(), 5);
|
|
4219
|
+
}
|
|
4216
4220
|
|
|
4217
4221
|
assert_eq!(input.value(), "hello");
|
|
4218
4222
|
assert_eq!(&*last_text.borrow(), "hello");
|
|
@@ -4474,7 +4478,9 @@ fn text_input_text_replacement_clamps_selection_bytes_from_char_positions() {
|
|
|
4474
4478
|
})
|
|
4475
4479
|
.expect("expected text input editor node id to be applied");
|
|
4476
4480
|
|
|
4477
|
-
|
|
4481
|
+
unsafe {
|
|
4482
|
+
event::__fui_on_text_replaced(editor_handle, 1, 5, std::ptr::null(), 0);
|
|
4483
|
+
}
|
|
4478
4484
|
|
|
4479
4485
|
assert_eq!(input.value(), "ab");
|
|
4480
4486
|
assert_eq!(input.selection_start(), 0);
|
|
@@ -4660,16 +4666,22 @@ fn text_area_readonly_disabled_placeholder_and_line_height_follow_text_input_cor
|
|
|
4660
4666
|
if *handle == editor_handle && (*line_height - 26.0).abs() < f32::EPSILON
|
|
4661
4667
|
)));
|
|
4662
4668
|
|
|
4663
|
-
|
|
4669
|
+
unsafe {
|
|
4670
|
+
event::__fui_on_text_replaced(editor_handle, 0, 0, b"Hello\nworld".as_ptr(), 11);
|
|
4671
|
+
}
|
|
4664
4672
|
assert_eq!(input.value(), "Hello\nworld");
|
|
4665
4673
|
|
|
4666
|
-
|
|
4674
|
+
unsafe {
|
|
4675
|
+
event::__fui_on_text_changed(editor_handle, b"alpha\nbeta".as_ptr(), 10);
|
|
4676
|
+
}
|
|
4667
4677
|
assert_eq!(input.value(), "alpha\nbeta");
|
|
4668
4678
|
event::__fui_on_selection_changed(editor_handle, 6, 10);
|
|
4669
4679
|
assert_eq!(input.selection_start(), 6);
|
|
4670
4680
|
assert_eq!(input.selection_end(), 10);
|
|
4671
4681
|
|
|
4672
|
-
|
|
4682
|
+
unsafe {
|
|
4683
|
+
event::__fui_on_text_replaced(editor_handle, 0, 11, std::ptr::null(), 0);
|
|
4684
|
+
}
|
|
4673
4685
|
assert_eq!(input.value(), "");
|
|
4674
4686
|
Application::unmount();
|
|
4675
4687
|
}
|
package/src/debug.rs
CHANGED
|
@@ -2,6 +2,7 @@ use crate::event::{self, PointerType};
|
|
|
2
2
|
use crate::ffi::{self, KeyEventType, PointerEventType};
|
|
3
3
|
use crate::node::NodeHandle;
|
|
4
4
|
|
|
5
|
+
#[allow(clippy::too_many_arguments)]
|
|
5
6
|
pub fn pointer_event(
|
|
6
7
|
event_type: PointerEventType,
|
|
7
8
|
handle: NodeHandle,
|
|
@@ -42,7 +43,7 @@ pub fn key_event(event_type: KeyEventType, key: &str, modifiers: u32) -> bool {
|
|
|
42
43
|
event::dispatch_key_event(event_type, key.to_string(), modifiers)
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
#[no_mangle]
|
|
46
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
46
47
|
pub extern "C" fn __fui_debug_pointer_event(
|
|
47
48
|
event_type: u32,
|
|
48
49
|
handle: u64,
|
|
@@ -74,8 +75,10 @@ pub extern "C" fn __fui_debug_pointer_event(
|
|
|
74
75
|
);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
#[no_mangle]
|
|
78
|
-
|
|
78
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
79
|
+
/// # Safety
|
|
80
|
+
/// `key_ptr` must be null for an empty key or point to `key_len` readable bytes.
|
|
81
|
+
pub unsafe extern "C" fn __fui_debug_key_event(
|
|
79
82
|
event_type: u32,
|
|
80
83
|
key_ptr: *const u8,
|
|
81
84
|
key_len: u32,
|
|
@@ -97,12 +100,12 @@ pub extern "C" fn __fui_debug_key_event(
|
|
|
97
100
|
);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
#[no_mangle]
|
|
103
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
101
104
|
pub extern "C" fn __fui_debug_focus_changed(handle: u64, focused: bool) {
|
|
102
105
|
event::dispatch_focus_changed(NodeHandle::from_raw(handle), focused);
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
#[no_mangle]
|
|
108
|
+
#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
|
|
106
109
|
pub extern "C" fn __fui_debug_scroll(
|
|
107
110
|
handle: u64,
|
|
108
111
|
offset_x: f32,
|
package/src/drag_drop.rs
CHANGED
|
@@ -399,9 +399,7 @@ fn finish_session(
|
|
|
399
399
|
notify_target_leave: bool,
|
|
400
400
|
) {
|
|
401
401
|
let target = with_state(|state| {
|
|
402
|
-
let
|
|
403
|
-
return None;
|
|
404
|
-
};
|
|
402
|
+
let active = state.active_session.as_ref()?;
|
|
405
403
|
if !Rc::ptr_eq(&active.inner, &session.inner) {
|
|
406
404
|
return None;
|
|
407
405
|
}
|
package/src/drawing.rs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
#![allow(clippy::too_many_arguments)]
|
|
2
|
+
|
|
1
3
|
use crate::ffi;
|
|
2
4
|
use crate::image_sampling::ImageSampling;
|
|
3
5
|
use crate::logger::error;
|
|
@@ -78,6 +80,12 @@ pub struct Path {
|
|
|
78
80
|
resource: Rc<PathResource>,
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
impl Default for Path {
|
|
84
|
+
fn default() -> Self {
|
|
85
|
+
Self::new()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
impl Path {
|
|
82
90
|
pub fn new() -> Self {
|
|
83
91
|
let id = unsafe { ffi::fui_path_create() };
|