@effindomv2/fui-rs 0.1.3
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/COMMERCIAL.md +7 -0
- package/Cargo.toml +34 -0
- package/LICENSE.md +9 -0
- package/README.md +88 -0
- package/package.json +54 -0
- package/scripts/build.sh +227 -0
- package/scripts/check-runtime-dependency.sh +41 -0
- package/scripts/framework-host-services.ts +40 -0
- package/scripts/generate-host-events.ts +17 -0
- package/scripts/generate-host-services.ts +28 -0
- package/scripts/hostgen/common.ts +73 -0
- package/scripts/hostgen/registry.ts +159 -0
- package/scripts/hostgen/rust-host-events.ts +171 -0
- package/scripts/hostgen/rust-host-services.ts +257 -0
- package/src/animation.rs +625 -0
- package/src/app.rs +324 -0
- package/src/assets.rs +572 -0
- package/src/bindings/mod.rs +1 -0
- package/src/bindings/ui.rs +705 -0
- package/src/bitmap.rs +317 -0
- package/src/bridge_callbacks.rs +242 -0
- package/src/context_menu_manager.rs +332 -0
- package/src/controls/anti_selection_area.rs +54 -0
- package/src/controls/button.rs +542 -0
- package/src/controls/checkbox.rs +372 -0
- package/src/controls/combobox.rs +1574 -0
- package/src/controls/context_menu.rs +1367 -0
- package/src/controls/control_template_set.rs +46 -0
- package/src/controls/control_tokens.rs +596 -0
- package/src/controls/dialog.rs +590 -0
- package/src/controls/dropdown.rs +1010 -0
- package/src/controls/form.rs +229 -0
- package/src/controls/internal/button_presenter.rs +142 -0
- package/src/controls/internal/checkbox_indicator_presenter.rs +210 -0
- package/src/controls/internal/dropdown_chevron_presenter.rs +134 -0
- package/src/controls/internal/dropdown_field_presenter.rs +269 -0
- package/src/controls/internal/dropdown_option_row_presenter.rs +184 -0
- package/src/controls/internal/mod.rs +13 -0
- package/src/controls/internal/pressable_indicator_presenter.rs +26 -0
- package/src/controls/internal/pressable_labeled_control.rs +492 -0
- package/src/controls/internal/radio_indicator_presenter.rs +208 -0
- package/src/controls/internal/selectable_popup_list.rs +597 -0
- package/src/controls/internal/slider_presenter.rs +282 -0
- package/src/controls/internal/switch_indicator_presenter.rs +235 -0
- package/src/controls/internal/text_input_core.rs +1074 -0
- package/src/controls/internal/text_input_presenter.rs +108 -0
- package/src/controls/mod.rs +235 -0
- package/src/controls/nav_link.rs +395 -0
- package/src/controls/popup.rs +191 -0
- package/src/controls/progress_bar.rs +287 -0
- package/src/controls/radio_button.rs +348 -0
- package/src/controls/radio_group.rs +283 -0
- package/src/controls/selection_area.rs +82 -0
- package/src/controls/shared.rs +68 -0
- package/src/controls/slider.rs +701 -0
- package/src/controls/switch.rs +293 -0
- package/src/controls/templating.rs +49 -0
- package/src/controls/tests.rs +3905 -0
- package/src/controls/text_area.rs +207 -0
- package/src/controls/text_input.rs +192 -0
- package/src/debug.rs +146 -0
- package/src/drag_drop.rs +424 -0
- package/src/drag_gesture.rs +258 -0
- package/src/drawing.rs +385 -0
- package/src/event.rs +1603 -0
- package/src/external_drop.rs +467 -0
- package/src/fetch.rs +500 -0
- package/src/ffi.rs +3542 -0
- package/src/file.rs +1677 -0
- package/src/focus_adorner.rs +307 -0
- package/src/focus_visibility.rs +121 -0
- package/src/frame_scheduler.rs +159 -0
- package/src/frame_signal.rs +64 -0
- package/src/generated/ffi.rs +798 -0
- package/src/generated/framework_host_services.rs +74 -0
- package/src/generated/mod.rs +2 -0
- package/src/host_services.rs +162 -0
- package/src/image_sampling.rs +78 -0
- package/src/keyboard_scroll.rs +137 -0
- package/src/keyboard_scroll_tracker.rs +385 -0
- package/src/lib.rs +547 -0
- package/src/logger.rs +56 -0
- package/src/mobile_text_selection_toolbar.rs +1034 -0
- package/src/navigation.rs +48 -0
- package/src/node/core.rs +2210 -0
- package/src/node/custom_drawable.rs +149 -0
- package/src/node/flex_box.rs +874 -0
- package/src/node/grid.rs +304 -0
- package/src/node/helpers.rs +442 -0
- package/src/node/image.rs +506 -0
- package/src/node/mod.rs +315 -0
- package/src/node/scroll_bar.rs +845 -0
- package/src/node/scroll_box.rs +514 -0
- package/src/node/scroll_state.rs +121 -0
- package/src/node/scroll_view.rs +557 -0
- package/src/node/svg_node.rs +474 -0
- package/src/node/text_node.rs +633 -0
- package/src/node/virtual_list.rs +678 -0
- package/src/panic_hook.rs +36 -0
- package/src/persisted.rs +274 -0
- package/src/platform.rs +556 -0
- package/src/popup_presenter.rs +344 -0
- package/src/selection_handle_adorner.rs +838 -0
- package/src/signal.rs +134 -0
- package/src/text.rs +1065 -0
- package/src/theme.rs +571 -0
- package/src/timers.rs +106 -0
- package/src/tool_tip.rs +167 -0
- package/src/tool_tip_manager.rs +691 -0
- package/src/transitions.rs +41 -0
- package/src/typography.rs +520 -0
- package/src/viewport.rs +108 -0
- package/src/worker.rs +308 -0
- package/src/worker_job.rs +82 -0
- package/src/worker_runtime.rs +172 -0
package/src/file.rs
ADDED
|
@@ -0,0 +1,1677 @@
|
|
|
1
|
+
use crate::ffi;
|
|
2
|
+
use std::cell::RefCell;
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
use std::rc::Rc;
|
|
5
|
+
|
|
6
|
+
const FILE_STATUS_SUCCESS: u32 = 1;
|
|
7
|
+
const FILE_STATUS_CANCELLED: u32 = 2;
|
|
8
|
+
|
|
9
|
+
const FILE_CAPABILITY_OPEN: u32 = 1 << 0;
|
|
10
|
+
const FILE_CAPABILITY_READ: u32 = 1 << 1;
|
|
11
|
+
const FILE_CAPABILITY_SAVE: u32 = 1 << 2;
|
|
12
|
+
const FILE_CAPABILITY_CHUNKED_READ: u32 = 1 << 3;
|
|
13
|
+
const FILE_CAPABILITY_CHUNKED_WRITE: u32 = 1 << 4;
|
|
14
|
+
const FILE_CAPABILITY_NATIVE_SAVE_PICKER: u32 = 1 << 5;
|
|
15
|
+
const FILE_CAPABILITY_PROCESS_WORKER_SAVE: u32 = 1 << 6;
|
|
16
|
+
|
|
17
|
+
type FileOpenCallback = Rc<dyn Fn(FileOpenEventArgs)>;
|
|
18
|
+
type FileErrorCallback = Rc<dyn Fn(FileErrorEventArgs)>;
|
|
19
|
+
type FileReadCallback = Rc<dyn Fn(FileReadChunk)>;
|
|
20
|
+
type FileSaveCallback = Rc<dyn Fn(FileSaveResult)>;
|
|
21
|
+
type FileWriteCallback = Rc<dyn Fn(FileWriteProgress)>;
|
|
22
|
+
type FileWriterCreatedCallback = Rc<dyn Fn(BrowserFileWriter)>;
|
|
23
|
+
type FileWorkerChunkCallback = Rc<dyn Fn(FileReadChunk)>;
|
|
24
|
+
type FileWorkerProgressCallback = Rc<dyn Fn(FileWorkerProcessProgress)>;
|
|
25
|
+
type FileWorkerCompleteCallback = Rc<dyn Fn(FileWorkerProcessResult)>;
|
|
26
|
+
|
|
27
|
+
thread_local! {
|
|
28
|
+
static NEXT_FILE_REQUEST_ID: RefCell<u32> = const { RefCell::new(1) };
|
|
29
|
+
static BROWSER_FILES: RefCell<HashMap<String, Rc<RefCell<BrowserFileState>>>> = RefCell::new(HashMap::new());
|
|
30
|
+
static PENDING_OPEN_REQUESTS: RefCell<HashMap<u32, PendingOpenRequest>> = RefCell::new(HashMap::new());
|
|
31
|
+
static PENDING_READ_REQUESTS: RefCell<HashMap<u32, PendingReadRequest>> = RefCell::new(HashMap::new());
|
|
32
|
+
static PENDING_SAVE_REQUESTS: RefCell<HashMap<u32, PendingSaveRequest>> = RefCell::new(HashMap::new());
|
|
33
|
+
static PENDING_WRITER_CREATE_REQUESTS: RefCell<HashMap<u32, PendingWriterCreateRequest>> = RefCell::new(HashMap::new());
|
|
34
|
+
static PENDING_WRITER_WRITE_REQUESTS: RefCell<HashMap<u32, PendingWriterWriteRequest>> = RefCell::new(HashMap::new());
|
|
35
|
+
static PENDING_WRITER_FINISH_REQUESTS: RefCell<HashMap<u32, PendingWriterFinishRequest>> = RefCell::new(HashMap::new());
|
|
36
|
+
static ACTIVE_WORKER_PROCESS_REQUESTS: RefCell<HashMap<u32, Rc<RefCell<FileWorkerProcessRequestState>>>> = RefCell::new(HashMap::new());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn with_utf8(value: &str, callback: impl FnOnce(usize, u32)) {
|
|
40
|
+
let bytes = value.as_bytes();
|
|
41
|
+
callback(
|
|
42
|
+
if bytes.is_empty() {
|
|
43
|
+
0
|
|
44
|
+
} else {
|
|
45
|
+
bytes.as_ptr() as usize
|
|
46
|
+
},
|
|
47
|
+
bytes.len() as u32,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn read_utf8(ptr: *const u8, len: u32) -> String {
|
|
52
|
+
if ptr.is_null() || len == 0 {
|
|
53
|
+
return String::new();
|
|
54
|
+
}
|
|
55
|
+
let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
|
|
56
|
+
String::from_utf8_lossy(bytes).into_owned()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn next_request_id() -> u32 {
|
|
60
|
+
NEXT_FILE_REQUEST_ID.with(|slot| {
|
|
61
|
+
let mut slot = slot.borrow_mut();
|
|
62
|
+
let id = *slot;
|
|
63
|
+
*slot += 1;
|
|
64
|
+
id
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn describe_file_failure(status: u32, fallback: &str) -> String {
|
|
69
|
+
if status == FILE_STATUS_CANCELLED {
|
|
70
|
+
return "File operation was cancelled.".to_string();
|
|
71
|
+
}
|
|
72
|
+
fallback.to_string()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn dispatch_file_error(callback: Option<FileErrorCallback>, message: String) {
|
|
76
|
+
if let Some(callback) = callback {
|
|
77
|
+
callback(FileErrorEventArgs { message });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
crate::logger::warn("File", &message);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn decode_length_prefixed_text(bytes: &[u8], cursor: &mut usize) -> Option<String> {
|
|
84
|
+
if *cursor + 4 > bytes.len() {
|
|
85
|
+
return None;
|
|
86
|
+
}
|
|
87
|
+
let len = u32::from_le_bytes(bytes[*cursor..*cursor + 4].try_into().ok()?) as usize;
|
|
88
|
+
*cursor += 4;
|
|
89
|
+
if *cursor + len > bytes.len() {
|
|
90
|
+
return None;
|
|
91
|
+
}
|
|
92
|
+
let value = String::from_utf8_lossy(&bytes[*cursor..*cursor + len]).into_owned();
|
|
93
|
+
*cursor += len;
|
|
94
|
+
Some(value)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fn decode_file_list_payload(bytes: &[u8]) -> Vec<BrowserFile> {
|
|
98
|
+
if bytes.len() < 4 {
|
|
99
|
+
return Vec::new();
|
|
100
|
+
}
|
|
101
|
+
let mut cursor = 0usize;
|
|
102
|
+
let count = u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().unwrap_or([0; 4])) as usize;
|
|
103
|
+
cursor += 4;
|
|
104
|
+
let mut files = Vec::with_capacity(count);
|
|
105
|
+
for _ in 0..count {
|
|
106
|
+
let Some(id) = decode_length_prefixed_text(bytes, &mut cursor) else {
|
|
107
|
+
break;
|
|
108
|
+
};
|
|
109
|
+
if cursor + 16 > bytes.len() {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
let size_bytes = u64::from_le_bytes(bytes[cursor..cursor + 8].try_into().unwrap_or([0; 8]));
|
|
113
|
+
cursor += 8;
|
|
114
|
+
let last_modified_ms =
|
|
115
|
+
u64::from_le_bytes(bytes[cursor..cursor + 8].try_into().unwrap_or([0; 8]));
|
|
116
|
+
cursor += 8;
|
|
117
|
+
let Some(name) = decode_length_prefixed_text(bytes, &mut cursor) else {
|
|
118
|
+
break;
|
|
119
|
+
};
|
|
120
|
+
let Some(mime_type) = decode_length_prefixed_text(bytes, &mut cursor) else {
|
|
121
|
+
break;
|
|
122
|
+
};
|
|
123
|
+
files.push(register_browser_file(
|
|
124
|
+
id,
|
|
125
|
+
name,
|
|
126
|
+
if mime_type.is_empty() {
|
|
127
|
+
None
|
|
128
|
+
} else {
|
|
129
|
+
Some(mime_type)
|
|
130
|
+
},
|
|
131
|
+
size_bytes,
|
|
132
|
+
last_modified_ms,
|
|
133
|
+
));
|
|
134
|
+
}
|
|
135
|
+
files
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn decode_writer_payload(bytes: &[u8]) -> Option<(FileSaveMode, String, Option<String>)> {
|
|
139
|
+
if bytes.len() < 4 {
|
|
140
|
+
return None;
|
|
141
|
+
}
|
|
142
|
+
let mut cursor = 0usize;
|
|
143
|
+
let mode_raw = u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().ok()?);
|
|
144
|
+
cursor += 4;
|
|
145
|
+
let first = decode_length_prefixed_text(bytes, &mut cursor)?;
|
|
146
|
+
let second = if cursor < bytes.len() {
|
|
147
|
+
decode_length_prefixed_text(bytes, &mut cursor)
|
|
148
|
+
} else {
|
|
149
|
+
None
|
|
150
|
+
};
|
|
151
|
+
Some((FileSaveMode::from_raw(mode_raw), first, second))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn split_worker_process_complete_payload(text: String) -> (Option<String>, Option<String>) {
|
|
155
|
+
let mut parts = text.splitn(2, '\0');
|
|
156
|
+
let output_file_name = parts.next().unwrap_or_default().to_string();
|
|
157
|
+
let worker_result = parts.next().unwrap_or_default().to_string();
|
|
158
|
+
(
|
|
159
|
+
if output_file_name.is_empty() {
|
|
160
|
+
None
|
|
161
|
+
} else {
|
|
162
|
+
Some(output_file_name)
|
|
163
|
+
},
|
|
164
|
+
if worker_result.is_empty() {
|
|
165
|
+
None
|
|
166
|
+
} else {
|
|
167
|
+
Some(worker_result)
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
173
|
+
enum FileRequestKind {
|
|
174
|
+
Open,
|
|
175
|
+
Read,
|
|
176
|
+
Save,
|
|
177
|
+
WriterCreate,
|
|
178
|
+
WriterWrite,
|
|
179
|
+
WriterFinish,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[must_use = "dropping the guard unregisters the pending file callback"]
|
|
183
|
+
pub struct FileRequestGuard {
|
|
184
|
+
kind: FileRequestKind,
|
|
185
|
+
request_id: u32,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
impl FileRequestGuard {
|
|
189
|
+
fn inactive(kind: FileRequestKind) -> Self {
|
|
190
|
+
Self {
|
|
191
|
+
kind,
|
|
192
|
+
request_id: 0,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
pub fn cancel(&mut self) {
|
|
197
|
+
if self.request_id == 0 {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let request_id = self.request_id;
|
|
201
|
+
self.request_id = 0;
|
|
202
|
+
match self.kind {
|
|
203
|
+
FileRequestKind::Open => {
|
|
204
|
+
PENDING_OPEN_REQUESTS.with(|requests| {
|
|
205
|
+
requests.borrow_mut().remove(&request_id);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
FileRequestKind::Read => {
|
|
209
|
+
PENDING_READ_REQUESTS.with(|requests| {
|
|
210
|
+
requests.borrow_mut().remove(&request_id);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
FileRequestKind::Save => {
|
|
214
|
+
PENDING_SAVE_REQUESTS.with(|requests| {
|
|
215
|
+
requests.borrow_mut().remove(&request_id);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
FileRequestKind::WriterCreate => {
|
|
219
|
+
PENDING_WRITER_CREATE_REQUESTS.with(|requests| {
|
|
220
|
+
requests.borrow_mut().remove(&request_id);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
FileRequestKind::WriterWrite => {
|
|
224
|
+
PENDING_WRITER_WRITE_REQUESTS.with(|requests| {
|
|
225
|
+
requests.borrow_mut().remove(&request_id);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
FileRequestKind::WriterFinish => {
|
|
229
|
+
PENDING_WRITER_FINISH_REQUESTS.with(|requests| {
|
|
230
|
+
requests.borrow_mut().remove(&request_id);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
impl Drop for FileRequestGuard {
|
|
238
|
+
fn drop(&mut self) {
|
|
239
|
+
self.cancel();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
244
|
+
pub enum FileSaveMode {
|
|
245
|
+
Download = 1,
|
|
246
|
+
NativePicker = 2,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
impl FileSaveMode {
|
|
250
|
+
fn from_raw(value: u32) -> Self {
|
|
251
|
+
match value {
|
|
252
|
+
2 => Self::NativePicker,
|
|
253
|
+
_ => Self::Download,
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
259
|
+
pub struct FileCapabilities {
|
|
260
|
+
pub can_pick_open: bool,
|
|
261
|
+
pub can_read: bool,
|
|
262
|
+
pub can_save: bool,
|
|
263
|
+
pub can_read_chunks: bool,
|
|
264
|
+
pub can_write_chunks: bool,
|
|
265
|
+
pub can_use_native_save_picker: bool,
|
|
266
|
+
pub can_process_in_worker_to_picked_file: bool,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
impl FileCapabilities {
|
|
270
|
+
fn from_bits(bits: u32) -> Self {
|
|
271
|
+
Self {
|
|
272
|
+
can_pick_open: (bits & FILE_CAPABILITY_OPEN) != 0,
|
|
273
|
+
can_read: (bits & FILE_CAPABILITY_READ) != 0,
|
|
274
|
+
can_save: (bits & FILE_CAPABILITY_SAVE) != 0,
|
|
275
|
+
can_read_chunks: (bits & FILE_CAPABILITY_CHUNKED_READ) != 0,
|
|
276
|
+
can_write_chunks: (bits & FILE_CAPABILITY_CHUNKED_WRITE) != 0,
|
|
277
|
+
can_use_native_save_picker: (bits & FILE_CAPABILITY_NATIVE_SAVE_PICKER) != 0,
|
|
278
|
+
can_process_in_worker_to_picked_file: (bits & FILE_CAPABILITY_PROCESS_WORKER_SAVE) != 0,
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
284
|
+
pub struct FileOpenEventArgs {
|
|
285
|
+
pub files: Vec<BrowserFile>,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
289
|
+
pub struct FileErrorEventArgs {
|
|
290
|
+
pub message: String,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
294
|
+
pub struct FileReadChunk {
|
|
295
|
+
pub offset_bytes: u64,
|
|
296
|
+
pub file_size_bytes: u64,
|
|
297
|
+
pub bytes: Vec<u8>,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
impl FileReadChunk {
|
|
301
|
+
pub fn next_offset_bytes(&self) -> u64 {
|
|
302
|
+
self.offset_bytes + self.bytes.len() as u64
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
pub fn reached_eof(&self) -> bool {
|
|
306
|
+
self.next_offset_bytes() >= self.file_size_bytes
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
311
|
+
pub struct FileWriteProgress {
|
|
312
|
+
pub written_bytes: u64,
|
|
313
|
+
pub total_written_bytes: u64,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
317
|
+
pub struct FileSaveResult {
|
|
318
|
+
pub file_name: String,
|
|
319
|
+
pub mode: FileSaveMode,
|
|
320
|
+
pub written_bytes: u64,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
324
|
+
pub struct FileWorkerProcessProgress {
|
|
325
|
+
pub processed_bytes: u64,
|
|
326
|
+
pub total_bytes: u64,
|
|
327
|
+
pub output_file_name: Option<String>,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
331
|
+
pub struct FileWorkerProcessResult {
|
|
332
|
+
pub processed_bytes: u64,
|
|
333
|
+
pub output_file_name: Option<String>,
|
|
334
|
+
pub worker_result: Option<String>,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
338
|
+
struct BrowserFileState {
|
|
339
|
+
id: String,
|
|
340
|
+
name: String,
|
|
341
|
+
mime_type: Option<String>,
|
|
342
|
+
size_bytes: u64,
|
|
343
|
+
last_modified_ms: u64,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#[derive(Clone, Debug)]
|
|
347
|
+
pub struct BrowserFile {
|
|
348
|
+
inner: Rc<RefCell<BrowserFileState>>,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
impl Default for BrowserFile {
|
|
352
|
+
fn default() -> Self {
|
|
353
|
+
Self {
|
|
354
|
+
inner: Rc::new(RefCell::new(BrowserFileState {
|
|
355
|
+
id: String::new(),
|
|
356
|
+
name: String::new(),
|
|
357
|
+
mime_type: None,
|
|
358
|
+
size_bytes: 0,
|
|
359
|
+
last_modified_ms: 0,
|
|
360
|
+
})),
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
impl PartialEq for BrowserFile {
|
|
366
|
+
fn eq(&self, other: &Self) -> bool {
|
|
367
|
+
self.id() == other.id()
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
impl Eq for BrowserFile {}
|
|
372
|
+
|
|
373
|
+
impl BrowserFile {
|
|
374
|
+
pub fn id(&self) -> String {
|
|
375
|
+
self.inner.borrow().id.clone()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
pub fn name(&self) -> String {
|
|
379
|
+
self.inner.borrow().name.clone()
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
pub fn mime_type(&self) -> Option<String> {
|
|
383
|
+
self.inner.borrow().mime_type.clone()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
pub fn size_bytes(&self) -> u64 {
|
|
387
|
+
self.inner.borrow().size_bytes
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
pub fn last_modified_ms(&self) -> u64 {
|
|
391
|
+
self.inner.borrow().last_modified_ms
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
pub fn read_bytes_chunk(
|
|
395
|
+
&self,
|
|
396
|
+
offset_bytes: u64,
|
|
397
|
+
max_bytes: u32,
|
|
398
|
+
on_complete: impl Fn(FileReadChunk) + 'static,
|
|
399
|
+
) -> FileRequestGuard {
|
|
400
|
+
self.read_bytes_chunk_with_error(
|
|
401
|
+
offset_bytes,
|
|
402
|
+
max_bytes,
|
|
403
|
+
on_complete,
|
|
404
|
+
None::<fn(FileErrorEventArgs)>,
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
pub fn read_bytes_chunk_with_error(
|
|
409
|
+
&self,
|
|
410
|
+
offset_bytes: u64,
|
|
411
|
+
max_bytes: u32,
|
|
412
|
+
on_complete: impl Fn(FileReadChunk) + 'static,
|
|
413
|
+
on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
|
|
414
|
+
) -> FileRequestGuard {
|
|
415
|
+
if max_bytes == 0 {
|
|
416
|
+
dispatch_file_error(
|
|
417
|
+
on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
|
|
418
|
+
"BrowserFile.read_bytes_chunk: max_bytes must be greater than zero.".to_string(),
|
|
419
|
+
);
|
|
420
|
+
return FileRequestGuard::inactive(FileRequestKind::Read);
|
|
421
|
+
}
|
|
422
|
+
let request_id = next_request_id();
|
|
423
|
+
PENDING_READ_REQUESTS.with(|requests| {
|
|
424
|
+
requests.borrow_mut().insert(
|
|
425
|
+
request_id,
|
|
426
|
+
PendingReadRequest {
|
|
427
|
+
on_complete: Rc::new(on_complete),
|
|
428
|
+
on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
let file_id = self.id();
|
|
433
|
+
with_utf8(&file_id, |file_id_ptr, file_id_len| unsafe {
|
|
434
|
+
ffi::fui_file_read_chunk(
|
|
435
|
+
request_id,
|
|
436
|
+
file_id_ptr,
|
|
437
|
+
file_id_len,
|
|
438
|
+
offset_bytes,
|
|
439
|
+
max_bytes,
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
FileRequestGuard {
|
|
443
|
+
kind: FileRequestKind::Read,
|
|
444
|
+
request_id,
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
struct PendingOpenRequest {
|
|
450
|
+
on_complete: FileOpenCallback,
|
|
451
|
+
on_error: Option<FileErrorCallback>,
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
struct PendingReadRequest {
|
|
455
|
+
on_complete: FileReadCallback,
|
|
456
|
+
on_error: Option<FileErrorCallback>,
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
struct PendingSaveRequest {
|
|
460
|
+
on_complete: FileSaveCallback,
|
|
461
|
+
on_error: Option<FileErrorCallback>,
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
struct PendingWriterCreateRequest {
|
|
465
|
+
on_complete: FileWriterCreatedCallback,
|
|
466
|
+
on_error: Option<FileErrorCallback>,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
struct PendingWriterWriteRequest {
|
|
470
|
+
on_complete: FileWriteCallback,
|
|
471
|
+
on_error: Option<FileErrorCallback>,
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
struct PendingWriterFinishRequest {
|
|
475
|
+
on_complete: FileSaveCallback,
|
|
476
|
+
on_error: Option<FileErrorCallback>,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
480
|
+
pub struct BrowserFileWriter {
|
|
481
|
+
writer_id: String,
|
|
482
|
+
pub file_name: String,
|
|
483
|
+
pub mode: FileSaveMode,
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
impl BrowserFileWriter {
|
|
487
|
+
pub fn write_text_chunk(
|
|
488
|
+
&self,
|
|
489
|
+
text: impl Into<String>,
|
|
490
|
+
on_complete: impl Fn(FileWriteProgress) + 'static,
|
|
491
|
+
) -> FileRequestGuard {
|
|
492
|
+
self.write_text_chunk_with_error(text, on_complete, None::<fn(FileErrorEventArgs)>)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
pub fn write_text_chunk_with_error(
|
|
496
|
+
&self,
|
|
497
|
+
text: impl Into<String>,
|
|
498
|
+
on_complete: impl Fn(FileWriteProgress) + 'static,
|
|
499
|
+
on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
|
|
500
|
+
) -> FileRequestGuard {
|
|
501
|
+
let text = text.into();
|
|
502
|
+
let request_id = next_request_id();
|
|
503
|
+
PENDING_WRITER_WRITE_REQUESTS.with(|requests| {
|
|
504
|
+
requests.borrow_mut().insert(
|
|
505
|
+
request_id,
|
|
506
|
+
PendingWriterWriteRequest {
|
|
507
|
+
on_complete: Rc::new(on_complete),
|
|
508
|
+
on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
|
|
509
|
+
},
|
|
510
|
+
);
|
|
511
|
+
});
|
|
512
|
+
with_utf8(&self.writer_id, |writer_id_ptr, writer_id_len| {
|
|
513
|
+
with_utf8(&text, |text_ptr, text_len| unsafe {
|
|
514
|
+
ffi::fui_file_writer_write_text(
|
|
515
|
+
request_id,
|
|
516
|
+
writer_id_ptr,
|
|
517
|
+
writer_id_len,
|
|
518
|
+
text_ptr,
|
|
519
|
+
text_len,
|
|
520
|
+
);
|
|
521
|
+
})
|
|
522
|
+
});
|
|
523
|
+
FileRequestGuard {
|
|
524
|
+
kind: FileRequestKind::WriterWrite,
|
|
525
|
+
request_id,
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
pub fn write_bytes_chunk(
|
|
530
|
+
&self,
|
|
531
|
+
bytes: &[u8],
|
|
532
|
+
on_complete: impl Fn(FileWriteProgress) + 'static,
|
|
533
|
+
) -> FileRequestGuard {
|
|
534
|
+
self.write_bytes_chunk_with_error(bytes, on_complete, None::<fn(FileErrorEventArgs)>)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
pub fn write_bytes_chunk_with_error(
|
|
538
|
+
&self,
|
|
539
|
+
bytes: &[u8],
|
|
540
|
+
on_complete: impl Fn(FileWriteProgress) + 'static,
|
|
541
|
+
on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
|
|
542
|
+
) -> FileRequestGuard {
|
|
543
|
+
let owned = bytes.to_vec();
|
|
544
|
+
let request_id = next_request_id();
|
|
545
|
+
PENDING_WRITER_WRITE_REQUESTS.with(|requests| {
|
|
546
|
+
requests.borrow_mut().insert(
|
|
547
|
+
request_id,
|
|
548
|
+
PendingWriterWriteRequest {
|
|
549
|
+
on_complete: Rc::new(on_complete),
|
|
550
|
+
on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
|
|
551
|
+
},
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
with_utf8(&self.writer_id, |writer_id_ptr, writer_id_len| unsafe {
|
|
555
|
+
ffi::fui_file_writer_write_bytes(
|
|
556
|
+
request_id,
|
|
557
|
+
writer_id_ptr,
|
|
558
|
+
writer_id_len,
|
|
559
|
+
if owned.is_empty() {
|
|
560
|
+
0
|
|
561
|
+
} else {
|
|
562
|
+
owned.as_ptr() as usize
|
|
563
|
+
},
|
|
564
|
+
owned.len() as u32,
|
|
565
|
+
);
|
|
566
|
+
});
|
|
567
|
+
FileRequestGuard {
|
|
568
|
+
kind: FileRequestKind::WriterWrite,
|
|
569
|
+
request_id,
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
pub fn finish(&self, on_complete: impl Fn(FileSaveResult) + 'static) -> FileRequestGuard {
|
|
574
|
+
self.finish_with_error(on_complete, None::<fn(FileErrorEventArgs)>)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
pub fn finish_with_error(
|
|
578
|
+
&self,
|
|
579
|
+
on_complete: impl Fn(FileSaveResult) + 'static,
|
|
580
|
+
on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
|
|
581
|
+
) -> FileRequestGuard {
|
|
582
|
+
let request_id = next_request_id();
|
|
583
|
+
PENDING_WRITER_FINISH_REQUESTS.with(|requests| {
|
|
584
|
+
requests.borrow_mut().insert(
|
|
585
|
+
request_id,
|
|
586
|
+
PendingWriterFinishRequest {
|
|
587
|
+
on_complete: Rc::new(on_complete),
|
|
588
|
+
on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
|
|
589
|
+
},
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
with_utf8(&self.writer_id, |writer_id_ptr, writer_id_len| unsafe {
|
|
593
|
+
ffi::fui_file_writer_finish(request_id, writer_id_ptr, writer_id_len);
|
|
594
|
+
});
|
|
595
|
+
FileRequestGuard {
|
|
596
|
+
kind: FileRequestKind::WriterFinish,
|
|
597
|
+
request_id,
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#[derive(Clone, Debug, Default)]
|
|
603
|
+
pub struct FileOpenRequest {
|
|
604
|
+
accept: String,
|
|
605
|
+
multiple: bool,
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
impl FileOpenRequest {
|
|
609
|
+
pub fn accept(mut self, value: impl Into<String>) -> Self {
|
|
610
|
+
self.accept = value.into();
|
|
611
|
+
self
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
pub fn multiple(mut self, flag: bool) -> Self {
|
|
615
|
+
self.multiple = flag;
|
|
616
|
+
self
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
pub fn pick(self, on_complete: impl Fn(FileOpenEventArgs) + 'static) -> FileRequestGuard {
|
|
620
|
+
self.pick_with_error(on_complete, None::<fn(FileErrorEventArgs)>)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
pub fn pick_with_error(
|
|
624
|
+
self,
|
|
625
|
+
on_complete: impl Fn(FileOpenEventArgs) + 'static,
|
|
626
|
+
on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
|
|
627
|
+
) -> FileRequestGuard {
|
|
628
|
+
let request_id = next_request_id();
|
|
629
|
+
PENDING_OPEN_REQUESTS.with(|requests| {
|
|
630
|
+
requests.borrow_mut().insert(
|
|
631
|
+
request_id,
|
|
632
|
+
PendingOpenRequest {
|
|
633
|
+
on_complete: Rc::new(on_complete),
|
|
634
|
+
on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
|
|
635
|
+
},
|
|
636
|
+
);
|
|
637
|
+
});
|
|
638
|
+
with_utf8(&self.accept, |accept_ptr, accept_len| unsafe {
|
|
639
|
+
ffi::fui_file_pick(request_id, accept_ptr, accept_len, self.multiple);
|
|
640
|
+
});
|
|
641
|
+
FileRequestGuard {
|
|
642
|
+
kind: FileRequestKind::Open,
|
|
643
|
+
request_id,
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
#[derive(Clone, Debug, Default)]
|
|
649
|
+
pub struct FileSaveRequest {
|
|
650
|
+
suggested_name: String,
|
|
651
|
+
mime_type: String,
|
|
652
|
+
file_extension: String,
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
impl FileSaveRequest {
|
|
656
|
+
pub fn suggested_name(mut self, value: impl Into<String>) -> Self {
|
|
657
|
+
self.suggested_name = value.into();
|
|
658
|
+
self
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
pub fn mime_type(mut self, value: impl Into<String>) -> Self {
|
|
662
|
+
self.mime_type = value.into();
|
|
663
|
+
self
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
pub fn file_extension(mut self, value: impl Into<String>) -> Self {
|
|
667
|
+
self.file_extension = value.into();
|
|
668
|
+
self
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
pub fn save_text(
|
|
672
|
+
self,
|
|
673
|
+
text: impl Into<String>,
|
|
674
|
+
on_complete: impl Fn(FileSaveResult) + 'static,
|
|
675
|
+
) -> FileRequestGuard {
|
|
676
|
+
self.save_text_with_error(text, on_complete, None::<fn(FileErrorEventArgs)>)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
pub fn save_text_with_error(
|
|
680
|
+
self,
|
|
681
|
+
text: impl Into<String>,
|
|
682
|
+
on_complete: impl Fn(FileSaveResult) + 'static,
|
|
683
|
+
on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
|
|
684
|
+
) -> FileRequestGuard {
|
|
685
|
+
let text = text.into();
|
|
686
|
+
let request_id = next_request_id();
|
|
687
|
+
PENDING_SAVE_REQUESTS.with(|requests| {
|
|
688
|
+
requests.borrow_mut().insert(
|
|
689
|
+
request_id,
|
|
690
|
+
PendingSaveRequest {
|
|
691
|
+
on_complete: Rc::new(on_complete),
|
|
692
|
+
on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
|
|
693
|
+
},
|
|
694
|
+
);
|
|
695
|
+
});
|
|
696
|
+
with_utf8(
|
|
697
|
+
&self.suggested_name,
|
|
698
|
+
|suggested_name_ptr, suggested_name_len| {
|
|
699
|
+
with_utf8(&self.mime_type, |mime_type_ptr, mime_type_len| {
|
|
700
|
+
with_utf8(
|
|
701
|
+
&self.file_extension,
|
|
702
|
+
|file_extension_ptr, file_extension_len| {
|
|
703
|
+
with_utf8(&text, |text_ptr, text_len| unsafe {
|
|
704
|
+
ffi::fui_file_save_text(
|
|
705
|
+
request_id,
|
|
706
|
+
suggested_name_ptr,
|
|
707
|
+
suggested_name_len,
|
|
708
|
+
mime_type_ptr,
|
|
709
|
+
mime_type_len,
|
|
710
|
+
file_extension_ptr,
|
|
711
|
+
file_extension_len,
|
|
712
|
+
text_ptr,
|
|
713
|
+
text_len,
|
|
714
|
+
);
|
|
715
|
+
})
|
|
716
|
+
},
|
|
717
|
+
)
|
|
718
|
+
})
|
|
719
|
+
},
|
|
720
|
+
);
|
|
721
|
+
FileRequestGuard {
|
|
722
|
+
kind: FileRequestKind::Save,
|
|
723
|
+
request_id,
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
pub fn save_bytes(
|
|
728
|
+
self,
|
|
729
|
+
bytes: &[u8],
|
|
730
|
+
on_complete: impl Fn(FileSaveResult) + 'static,
|
|
731
|
+
) -> FileRequestGuard {
|
|
732
|
+
self.save_bytes_with_error(bytes, on_complete, None::<fn(FileErrorEventArgs)>)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
pub fn save_bytes_with_error(
|
|
736
|
+
self,
|
|
737
|
+
bytes: &[u8],
|
|
738
|
+
on_complete: impl Fn(FileSaveResult) + 'static,
|
|
739
|
+
on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
|
|
740
|
+
) -> FileRequestGuard {
|
|
741
|
+
let owned = bytes.to_vec();
|
|
742
|
+
let request_id = next_request_id();
|
|
743
|
+
PENDING_SAVE_REQUESTS.with(|requests| {
|
|
744
|
+
requests.borrow_mut().insert(
|
|
745
|
+
request_id,
|
|
746
|
+
PendingSaveRequest {
|
|
747
|
+
on_complete: Rc::new(on_complete),
|
|
748
|
+
on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
|
|
749
|
+
},
|
|
750
|
+
);
|
|
751
|
+
});
|
|
752
|
+
with_utf8(
|
|
753
|
+
&self.suggested_name,
|
|
754
|
+
|suggested_name_ptr, suggested_name_len| {
|
|
755
|
+
with_utf8(&self.mime_type, |mime_type_ptr, mime_type_len| {
|
|
756
|
+
with_utf8(
|
|
757
|
+
&self.file_extension,
|
|
758
|
+
|file_extension_ptr, file_extension_len| unsafe {
|
|
759
|
+
ffi::fui_file_save_bytes(
|
|
760
|
+
request_id,
|
|
761
|
+
suggested_name_ptr,
|
|
762
|
+
suggested_name_len,
|
|
763
|
+
mime_type_ptr,
|
|
764
|
+
mime_type_len,
|
|
765
|
+
file_extension_ptr,
|
|
766
|
+
file_extension_len,
|
|
767
|
+
if owned.is_empty() {
|
|
768
|
+
0
|
|
769
|
+
} else {
|
|
770
|
+
owned.as_ptr() as usize
|
|
771
|
+
},
|
|
772
|
+
owned.len() as u32,
|
|
773
|
+
);
|
|
774
|
+
},
|
|
775
|
+
)
|
|
776
|
+
})
|
|
777
|
+
},
|
|
778
|
+
);
|
|
779
|
+
FileRequestGuard {
|
|
780
|
+
kind: FileRequestKind::Save,
|
|
781
|
+
request_id,
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
pub fn create_writer(
|
|
786
|
+
self,
|
|
787
|
+
on_complete: impl Fn(BrowserFileWriter) + 'static,
|
|
788
|
+
) -> FileRequestGuard {
|
|
789
|
+
self.create_writer_with_error(on_complete, None::<fn(FileErrorEventArgs)>)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
pub fn create_writer_with_error(
|
|
793
|
+
self,
|
|
794
|
+
on_complete: impl Fn(BrowserFileWriter) + 'static,
|
|
795
|
+
on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
|
|
796
|
+
) -> FileRequestGuard {
|
|
797
|
+
let request_id = next_request_id();
|
|
798
|
+
PENDING_WRITER_CREATE_REQUESTS.with(|requests| {
|
|
799
|
+
requests.borrow_mut().insert(
|
|
800
|
+
request_id,
|
|
801
|
+
PendingWriterCreateRequest {
|
|
802
|
+
on_complete: Rc::new(on_complete),
|
|
803
|
+
on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
|
|
804
|
+
},
|
|
805
|
+
);
|
|
806
|
+
});
|
|
807
|
+
with_utf8(
|
|
808
|
+
&self.suggested_name,
|
|
809
|
+
|suggested_name_ptr, suggested_name_len| {
|
|
810
|
+
with_utf8(&self.mime_type, |mime_type_ptr, mime_type_len| {
|
|
811
|
+
with_utf8(
|
|
812
|
+
&self.file_extension,
|
|
813
|
+
|file_extension_ptr, file_extension_len| unsafe {
|
|
814
|
+
ffi::fui_file_create_writer(
|
|
815
|
+
request_id,
|
|
816
|
+
suggested_name_ptr,
|
|
817
|
+
suggested_name_len,
|
|
818
|
+
mime_type_ptr,
|
|
819
|
+
mime_type_len,
|
|
820
|
+
file_extension_ptr,
|
|
821
|
+
file_extension_len,
|
|
822
|
+
);
|
|
823
|
+
},
|
|
824
|
+
)
|
|
825
|
+
})
|
|
826
|
+
},
|
|
827
|
+
);
|
|
828
|
+
FileRequestGuard {
|
|
829
|
+
kind: FileRequestKind::WriterCreate,
|
|
830
|
+
request_id,
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
#[derive(Default)]
|
|
836
|
+
struct FileWorkerProcessRequestState {
|
|
837
|
+
file: BrowserFile,
|
|
838
|
+
worker_wasm_path: String,
|
|
839
|
+
worker_entry_name: String,
|
|
840
|
+
suggested_name: String,
|
|
841
|
+
save_to_picked_file: bool,
|
|
842
|
+
chunk_bytes: u32,
|
|
843
|
+
on_chunk: Option<FileWorkerChunkCallback>,
|
|
844
|
+
on_progress: Option<FileWorkerProgressCallback>,
|
|
845
|
+
on_complete: Option<FileWorkerCompleteCallback>,
|
|
846
|
+
on_error: Option<FileErrorCallback>,
|
|
847
|
+
request_id: u32,
|
|
848
|
+
started: bool,
|
|
849
|
+
finished: bool,
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
#[derive(Clone, Default)]
|
|
853
|
+
pub struct FileWorkerProcessRequest {
|
|
854
|
+
inner: Rc<RefCell<FileWorkerProcessRequestState>>,
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
impl FileWorkerProcessRequest {
|
|
858
|
+
fn new(file: BrowserFile) -> Self {
|
|
859
|
+
Self {
|
|
860
|
+
inner: Rc::new(RefCell::new(FileWorkerProcessRequestState {
|
|
861
|
+
suggested_name: file.name(),
|
|
862
|
+
file,
|
|
863
|
+
chunk_bytes: 64 * 1024,
|
|
864
|
+
..FileWorkerProcessRequestState::default()
|
|
865
|
+
})),
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
pub fn suggested_name(self, value: impl Into<String>) -> Self {
|
|
870
|
+
self.inner.borrow_mut().suggested_name = value.into();
|
|
871
|
+
self
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
pub fn worker(self, wasm_path: impl Into<String>, entry_name: impl Into<String>) -> Self {
|
|
875
|
+
let mut inner = self.inner.borrow_mut();
|
|
876
|
+
inner.worker_wasm_path = wasm_path.into();
|
|
877
|
+
inner.worker_entry_name = entry_name.into();
|
|
878
|
+
drop(inner);
|
|
879
|
+
self
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
pub fn save_to_picked_file(self, value: impl Into<String>) -> Self {
|
|
883
|
+
let mut inner = self.inner.borrow_mut();
|
|
884
|
+
inner.save_to_picked_file = true;
|
|
885
|
+
inner.suggested_name = value.into();
|
|
886
|
+
drop(inner);
|
|
887
|
+
self
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
pub fn chunk_bytes(self, value: u32) -> Self {
|
|
891
|
+
self.inner.borrow_mut().chunk_bytes = value;
|
|
892
|
+
self
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
pub fn on_chunk(self, handler: impl Fn(FileReadChunk) + 'static) -> Self {
|
|
896
|
+
self.inner.borrow_mut().on_chunk = Some(Rc::new(handler));
|
|
897
|
+
self
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
pub fn on_progress(self, handler: impl Fn(FileWorkerProcessProgress) + 'static) -> Self {
|
|
901
|
+
self.inner.borrow_mut().on_progress = Some(Rc::new(handler));
|
|
902
|
+
self
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
pub fn on_complete(self, handler: impl Fn(FileWorkerProcessResult) + 'static) -> Self {
|
|
906
|
+
self.inner.borrow_mut().on_complete = Some(Rc::new(handler));
|
|
907
|
+
self
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
pub fn on_error(self, handler: impl Fn(FileErrorEventArgs) + 'static) -> Self {
|
|
911
|
+
self.inner.borrow_mut().on_error = Some(Rc::new(handler));
|
|
912
|
+
self
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
pub fn start(self) -> Self {
|
|
916
|
+
let (
|
|
917
|
+
request_id,
|
|
918
|
+
worker_wasm_path,
|
|
919
|
+
worker_entry_name,
|
|
920
|
+
file_id,
|
|
921
|
+
suggested_name,
|
|
922
|
+
chunk_bytes,
|
|
923
|
+
save_to_picked_file,
|
|
924
|
+
on_chunk,
|
|
925
|
+
on_error,
|
|
926
|
+
already_finished,
|
|
927
|
+
already_started,
|
|
928
|
+
) = {
|
|
929
|
+
let inner = self.inner.borrow();
|
|
930
|
+
(
|
|
931
|
+
inner.request_id,
|
|
932
|
+
inner.worker_wasm_path.clone(),
|
|
933
|
+
inner.worker_entry_name.clone(),
|
|
934
|
+
inner.file.id(),
|
|
935
|
+
inner.suggested_name.clone(),
|
|
936
|
+
inner.chunk_bytes,
|
|
937
|
+
inner.save_to_picked_file,
|
|
938
|
+
inner.on_chunk.is_some(),
|
|
939
|
+
inner.on_error.clone(),
|
|
940
|
+
inner.finished,
|
|
941
|
+
inner.started,
|
|
942
|
+
)
|
|
943
|
+
};
|
|
944
|
+
if already_finished {
|
|
945
|
+
crate::logger::warn(
|
|
946
|
+
"File",
|
|
947
|
+
"FileWorkerProcessRequest.start ignored after the worker process already finished.",
|
|
948
|
+
);
|
|
949
|
+
return self;
|
|
950
|
+
}
|
|
951
|
+
if already_started {
|
|
952
|
+
crate::logger::warn(
|
|
953
|
+
"File",
|
|
954
|
+
"FileWorkerProcessRequest.start ignored because the request already started.",
|
|
955
|
+
);
|
|
956
|
+
return self;
|
|
957
|
+
}
|
|
958
|
+
if chunk_bytes == 0 {
|
|
959
|
+
dispatch_file_error(
|
|
960
|
+
on_error,
|
|
961
|
+
"FileWorkerProcessRequest.start: chunk_bytes must be greater than zero."
|
|
962
|
+
.to_string(),
|
|
963
|
+
);
|
|
964
|
+
return self;
|
|
965
|
+
}
|
|
966
|
+
if worker_wasm_path.is_empty() || worker_entry_name.is_empty() {
|
|
967
|
+
dispatch_file_error(
|
|
968
|
+
on_error,
|
|
969
|
+
"FileWorkerProcessRequest.start: worker(wasm_path, entry_name) is required."
|
|
970
|
+
.to_string(),
|
|
971
|
+
);
|
|
972
|
+
return self;
|
|
973
|
+
}
|
|
974
|
+
if !save_to_picked_file && !on_chunk {
|
|
975
|
+
dispatch_file_error(
|
|
976
|
+
on_error,
|
|
977
|
+
"FileWorkerProcessRequest.start: either save_to_picked_file(...) or on_chunk(...) is required.".to_string(),
|
|
978
|
+
);
|
|
979
|
+
return self;
|
|
980
|
+
}
|
|
981
|
+
let request_id = if request_id == 0 {
|
|
982
|
+
let request_id = next_request_id();
|
|
983
|
+
let mut inner = self.inner.borrow_mut();
|
|
984
|
+
inner.request_id = request_id;
|
|
985
|
+
inner.started = true;
|
|
986
|
+
drop(inner);
|
|
987
|
+
ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| {
|
|
988
|
+
requests.borrow_mut().insert(request_id, self.inner.clone());
|
|
989
|
+
});
|
|
990
|
+
request_id
|
|
991
|
+
} else {
|
|
992
|
+
request_id
|
|
993
|
+
};
|
|
994
|
+
with_utf8(
|
|
995
|
+
&worker_wasm_path,
|
|
996
|
+
|worker_wasm_path_ptr, worker_wasm_path_len| {
|
|
997
|
+
with_utf8(&worker_entry_name, |worker_entry_ptr, worker_entry_len| {
|
|
998
|
+
with_utf8(&file_id, |file_id_ptr, file_id_len| {
|
|
999
|
+
with_utf8(
|
|
1000
|
+
&suggested_name,
|
|
1001
|
+
|suggested_name_ptr, suggested_name_len| unsafe {
|
|
1002
|
+
ffi::fui_file_process_worker_start(
|
|
1003
|
+
request_id,
|
|
1004
|
+
worker_wasm_path_ptr,
|
|
1005
|
+
worker_wasm_path_len,
|
|
1006
|
+
worker_entry_ptr,
|
|
1007
|
+
worker_entry_len,
|
|
1008
|
+
file_id_ptr,
|
|
1009
|
+
file_id_len,
|
|
1010
|
+
suggested_name_ptr,
|
|
1011
|
+
suggested_name_len,
|
|
1012
|
+
chunk_bytes,
|
|
1013
|
+
save_to_picked_file,
|
|
1014
|
+
);
|
|
1015
|
+
},
|
|
1016
|
+
)
|
|
1017
|
+
})
|
|
1018
|
+
})
|
|
1019
|
+
},
|
|
1020
|
+
);
|
|
1021
|
+
self
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
pub fn cancel(&self) {
|
|
1025
|
+
let request_id = {
|
|
1026
|
+
let mut inner = self.inner.borrow_mut();
|
|
1027
|
+
if inner.finished {
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
let request_id = inner.request_id;
|
|
1031
|
+
if request_id == 0 {
|
|
1032
|
+
inner.finished = true;
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
inner.request_id = 0;
|
|
1036
|
+
inner.started = false;
|
|
1037
|
+
inner.finished = true;
|
|
1038
|
+
inner.on_chunk = None;
|
|
1039
|
+
inner.on_progress = None;
|
|
1040
|
+
inner.on_complete = None;
|
|
1041
|
+
inner.on_error = None;
|
|
1042
|
+
request_id
|
|
1043
|
+
};
|
|
1044
|
+
ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| {
|
|
1045
|
+
requests.borrow_mut().remove(&request_id);
|
|
1046
|
+
});
|
|
1047
|
+
unsafe {
|
|
1048
|
+
ffi::fui_file_process_worker_cancel(request_id);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
impl Drop for FileWorkerProcessRequest {
|
|
1054
|
+
fn drop(&mut self) {
|
|
1055
|
+
self.cancel();
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
pub struct File;
|
|
1060
|
+
|
|
1061
|
+
impl File {
|
|
1062
|
+
pub fn open() -> FileOpenRequest {
|
|
1063
|
+
FileOpenRequest::default()
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
pub fn save() -> FileSaveRequest {
|
|
1067
|
+
FileSaveRequest::default()
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
pub fn process_file_in_worker(file: BrowserFile) -> FileWorkerProcessRequest {
|
|
1071
|
+
FileWorkerProcessRequest::new(file)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
pub fn capabilities() -> FileCapabilities {
|
|
1075
|
+
FileCapabilities::from_bits(unsafe { ffi::fui_file_capabilities() })
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
pub fn try_get_file(id: &str) -> Option<BrowserFile> {
|
|
1079
|
+
BROWSER_FILES.with(|files| {
|
|
1080
|
+
files
|
|
1081
|
+
.borrow()
|
|
1082
|
+
.get(id)
|
|
1083
|
+
.cloned()
|
|
1084
|
+
.map(|inner| BrowserFile { inner })
|
|
1085
|
+
})
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
pub(crate) fn register_browser_file(
|
|
1090
|
+
id: String,
|
|
1091
|
+
name: String,
|
|
1092
|
+
mime_type: Option<String>,
|
|
1093
|
+
size_bytes: u64,
|
|
1094
|
+
last_modified_ms: u64,
|
|
1095
|
+
) -> BrowserFile {
|
|
1096
|
+
BROWSER_FILES.with(|files| {
|
|
1097
|
+
let mut files = files.borrow_mut();
|
|
1098
|
+
if let Some(existing) = files.get(&id).cloned() {
|
|
1099
|
+
let mut existing = existing.borrow_mut();
|
|
1100
|
+
existing.name = name;
|
|
1101
|
+
existing.mime_type = mime_type;
|
|
1102
|
+
existing.size_bytes = size_bytes;
|
|
1103
|
+
existing.last_modified_ms = last_modified_ms;
|
|
1104
|
+
drop(existing);
|
|
1105
|
+
return BrowserFile {
|
|
1106
|
+
inner: files.get(&id).unwrap().clone(),
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
let inner = Rc::new(RefCell::new(BrowserFileState {
|
|
1110
|
+
id: id.clone(),
|
|
1111
|
+
name,
|
|
1112
|
+
mime_type,
|
|
1113
|
+
size_bytes,
|
|
1114
|
+
last_modified_ms,
|
|
1115
|
+
}));
|
|
1116
|
+
files.insert(id, inner.clone());
|
|
1117
|
+
BrowserFile { inner }
|
|
1118
|
+
})
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
fn finish_worker_process(
|
|
1122
|
+
request_id: u32,
|
|
1123
|
+
) -> Option<(
|
|
1124
|
+
Option<FileWorkerCompleteCallback>,
|
|
1125
|
+
Option<FileErrorCallback>,
|
|
1126
|
+
)> {
|
|
1127
|
+
let Some(request) =
|
|
1128
|
+
ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id))
|
|
1129
|
+
else {
|
|
1130
|
+
return None;
|
|
1131
|
+
};
|
|
1132
|
+
let mut inner = request.borrow_mut();
|
|
1133
|
+
if inner.finished {
|
|
1134
|
+
return None;
|
|
1135
|
+
}
|
|
1136
|
+
inner.started = false;
|
|
1137
|
+
inner.finished = true;
|
|
1138
|
+
inner.request_id = 0;
|
|
1139
|
+
let on_complete = inner.on_complete.take();
|
|
1140
|
+
let on_error = inner.on_error.take();
|
|
1141
|
+
inner.on_chunk = None;
|
|
1142
|
+
inner.on_progress = None;
|
|
1143
|
+
Some((on_complete, on_error))
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
pub fn reset_file_runtime() {
|
|
1147
|
+
let active_requests = ACTIVE_WORKER_PROCESS_REQUESTS
|
|
1148
|
+
.with(|requests| requests.borrow().values().cloned().collect::<Vec<_>>());
|
|
1149
|
+
for request in active_requests {
|
|
1150
|
+
FileWorkerProcessRequest { inner: request }.cancel();
|
|
1151
|
+
}
|
|
1152
|
+
BROWSER_FILES.with(|files| files.borrow_mut().clear());
|
|
1153
|
+
PENDING_OPEN_REQUESTS.with(|requests| requests.borrow_mut().clear());
|
|
1154
|
+
PENDING_READ_REQUESTS.with(|requests| requests.borrow_mut().clear());
|
|
1155
|
+
PENDING_SAVE_REQUESTS.with(|requests| requests.borrow_mut().clear());
|
|
1156
|
+
PENDING_WRITER_CREATE_REQUESTS.with(|requests| requests.borrow_mut().clear());
|
|
1157
|
+
PENDING_WRITER_WRITE_REQUESTS.with(|requests| requests.borrow_mut().clear());
|
|
1158
|
+
PENDING_WRITER_FINISH_REQUESTS.with(|requests| requests.borrow_mut().clear());
|
|
1159
|
+
ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| requests.borrow_mut().clear());
|
|
1160
|
+
NEXT_FILE_REQUEST_ID.with(|slot| *slot.borrow_mut() = 1);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
#[no_mangle]
|
|
1164
|
+
pub extern "C" fn __fui_on_file_pick_result(
|
|
1165
|
+
request_id: u32,
|
|
1166
|
+
status: u32,
|
|
1167
|
+
payload_ptr: *const u8,
|
|
1168
|
+
payload_len: u32,
|
|
1169
|
+
) {
|
|
1170
|
+
let request = PENDING_OPEN_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
|
|
1171
|
+
let Some(request) = request else {
|
|
1172
|
+
return;
|
|
1173
|
+
};
|
|
1174
|
+
if status == FILE_STATUS_SUCCESS {
|
|
1175
|
+
let payload = if payload_ptr.is_null() || payload_len == 0 {
|
|
1176
|
+
&[][..]
|
|
1177
|
+
} else {
|
|
1178
|
+
unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
|
|
1179
|
+
};
|
|
1180
|
+
let files = decode_file_list_payload(payload);
|
|
1181
|
+
(request.on_complete)(FileOpenEventArgs { files });
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
dispatch_file_error(
|
|
1185
|
+
request.on_error,
|
|
1186
|
+
if payload_ptr.is_null() || payload_len == 0 {
|
|
1187
|
+
describe_file_failure(status, "File picker failed.")
|
|
1188
|
+
} else {
|
|
1189
|
+
read_utf8(payload_ptr, payload_len)
|
|
1190
|
+
},
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
#[no_mangle]
|
|
1195
|
+
pub extern "C" fn __fui_on_file_read_result(
|
|
1196
|
+
request_id: u32,
|
|
1197
|
+
status: u32,
|
|
1198
|
+
offset_bytes: u64,
|
|
1199
|
+
file_size_bytes: u64,
|
|
1200
|
+
payload_ptr: *const u8,
|
|
1201
|
+
payload_len: u32,
|
|
1202
|
+
) {
|
|
1203
|
+
let request = PENDING_READ_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
|
|
1204
|
+
let Some(request) = request else {
|
|
1205
|
+
return;
|
|
1206
|
+
};
|
|
1207
|
+
if status == FILE_STATUS_SUCCESS {
|
|
1208
|
+
let bytes = if payload_ptr.is_null() || payload_len == 0 {
|
|
1209
|
+
Vec::new()
|
|
1210
|
+
} else {
|
|
1211
|
+
unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }.to_vec()
|
|
1212
|
+
};
|
|
1213
|
+
(request.on_complete)(FileReadChunk {
|
|
1214
|
+
offset_bytes,
|
|
1215
|
+
file_size_bytes,
|
|
1216
|
+
bytes,
|
|
1217
|
+
});
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
dispatch_file_error(
|
|
1221
|
+
request.on_error,
|
|
1222
|
+
if payload_ptr.is_null() || payload_len == 0 {
|
|
1223
|
+
describe_file_failure(status, "File read failed.")
|
|
1224
|
+
} else {
|
|
1225
|
+
read_utf8(payload_ptr, payload_len)
|
|
1226
|
+
},
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
#[no_mangle]
|
|
1231
|
+
pub extern "C" fn __fui_on_file_save_result(
|
|
1232
|
+
request_id: u32,
|
|
1233
|
+
status: u32,
|
|
1234
|
+
written_bytes: u64,
|
|
1235
|
+
payload_ptr: *const u8,
|
|
1236
|
+
payload_len: u32,
|
|
1237
|
+
) {
|
|
1238
|
+
let request = PENDING_SAVE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
|
|
1239
|
+
let Some(request) = request else {
|
|
1240
|
+
return;
|
|
1241
|
+
};
|
|
1242
|
+
if status == FILE_STATUS_SUCCESS {
|
|
1243
|
+
let payload = if payload_ptr.is_null() || payload_len == 0 {
|
|
1244
|
+
&[][..]
|
|
1245
|
+
} else {
|
|
1246
|
+
unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
|
|
1247
|
+
};
|
|
1248
|
+
if let Some((mode, file_name, _)) = decode_writer_payload(payload) {
|
|
1249
|
+
(request.on_complete)(FileSaveResult {
|
|
1250
|
+
file_name,
|
|
1251
|
+
mode,
|
|
1252
|
+
written_bytes,
|
|
1253
|
+
});
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
dispatch_file_error(
|
|
1258
|
+
request.on_error,
|
|
1259
|
+
if payload_ptr.is_null() || payload_len == 0 {
|
|
1260
|
+
describe_file_failure(status, "File save failed.")
|
|
1261
|
+
} else {
|
|
1262
|
+
read_utf8(payload_ptr, payload_len)
|
|
1263
|
+
},
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
#[no_mangle]
|
|
1268
|
+
pub extern "C" fn __fui_on_file_writer_created(
|
|
1269
|
+
request_id: u32,
|
|
1270
|
+
status: u32,
|
|
1271
|
+
payload_ptr: *const u8,
|
|
1272
|
+
payload_len: u32,
|
|
1273
|
+
) {
|
|
1274
|
+
let request =
|
|
1275
|
+
PENDING_WRITER_CREATE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
|
|
1276
|
+
let Some(request) = request else {
|
|
1277
|
+
return;
|
|
1278
|
+
};
|
|
1279
|
+
if status == FILE_STATUS_SUCCESS {
|
|
1280
|
+
let payload = if payload_ptr.is_null() || payload_len == 0 {
|
|
1281
|
+
&[][..]
|
|
1282
|
+
} else {
|
|
1283
|
+
unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
|
|
1284
|
+
};
|
|
1285
|
+
if let Some((mode, writer_id, Some(file_name))) = decode_writer_payload(payload) {
|
|
1286
|
+
(request.on_complete)(BrowserFileWriter {
|
|
1287
|
+
writer_id,
|
|
1288
|
+
file_name,
|
|
1289
|
+
mode,
|
|
1290
|
+
});
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
dispatch_file_error(
|
|
1295
|
+
request.on_error,
|
|
1296
|
+
if payload_ptr.is_null() || payload_len == 0 {
|
|
1297
|
+
describe_file_failure(status, "Creating a file writer failed.")
|
|
1298
|
+
} else {
|
|
1299
|
+
read_utf8(payload_ptr, payload_len)
|
|
1300
|
+
},
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
#[no_mangle]
|
|
1305
|
+
pub extern "C" fn __fui_on_file_write_result(
|
|
1306
|
+
request_id: u32,
|
|
1307
|
+
status: u32,
|
|
1308
|
+
written_bytes: u64,
|
|
1309
|
+
total_written_bytes: u64,
|
|
1310
|
+
payload_ptr: *const u8,
|
|
1311
|
+
payload_len: u32,
|
|
1312
|
+
) {
|
|
1313
|
+
let request =
|
|
1314
|
+
PENDING_WRITER_WRITE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
|
|
1315
|
+
let Some(request) = request else {
|
|
1316
|
+
return;
|
|
1317
|
+
};
|
|
1318
|
+
if status == FILE_STATUS_SUCCESS {
|
|
1319
|
+
(request.on_complete)(FileWriteProgress {
|
|
1320
|
+
written_bytes,
|
|
1321
|
+
total_written_bytes,
|
|
1322
|
+
});
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
dispatch_file_error(
|
|
1326
|
+
request.on_error,
|
|
1327
|
+
if payload_ptr.is_null() || payload_len == 0 {
|
|
1328
|
+
describe_file_failure(status, "File write failed.")
|
|
1329
|
+
} else {
|
|
1330
|
+
read_utf8(payload_ptr, payload_len)
|
|
1331
|
+
},
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
#[no_mangle]
|
|
1336
|
+
pub extern "C" fn __fui_on_file_finish_result(
|
|
1337
|
+
request_id: u32,
|
|
1338
|
+
status: u32,
|
|
1339
|
+
written_bytes: u64,
|
|
1340
|
+
payload_ptr: *const u8,
|
|
1341
|
+
payload_len: u32,
|
|
1342
|
+
) {
|
|
1343
|
+
let request =
|
|
1344
|
+
PENDING_WRITER_FINISH_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
|
|
1345
|
+
let Some(request) = request else {
|
|
1346
|
+
return;
|
|
1347
|
+
};
|
|
1348
|
+
if status == FILE_STATUS_SUCCESS {
|
|
1349
|
+
let payload = if payload_ptr.is_null() || payload_len == 0 {
|
|
1350
|
+
&[][..]
|
|
1351
|
+
} else {
|
|
1352
|
+
unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
|
|
1353
|
+
};
|
|
1354
|
+
if let Some((mode, file_name, _)) = decode_writer_payload(payload) {
|
|
1355
|
+
(request.on_complete)(FileSaveResult {
|
|
1356
|
+
file_name,
|
|
1357
|
+
mode,
|
|
1358
|
+
written_bytes,
|
|
1359
|
+
});
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
dispatch_file_error(
|
|
1364
|
+
request.on_error,
|
|
1365
|
+
if payload_ptr.is_null() || payload_len == 0 {
|
|
1366
|
+
describe_file_failure(status, "Finishing the file writer failed.")
|
|
1367
|
+
} else {
|
|
1368
|
+
read_utf8(payload_ptr, payload_len)
|
|
1369
|
+
},
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
#[no_mangle]
|
|
1374
|
+
pub extern "C" fn __fui_on_file_worker_process_progress(
|
|
1375
|
+
request_id: u32,
|
|
1376
|
+
processed_bytes: u64,
|
|
1377
|
+
total_bytes: u64,
|
|
1378
|
+
payload_ptr: *const u8,
|
|
1379
|
+
payload_len: u32,
|
|
1380
|
+
) {
|
|
1381
|
+
let text = if payload_ptr.is_null() || payload_len == 0 {
|
|
1382
|
+
String::new()
|
|
1383
|
+
} else {
|
|
1384
|
+
read_utf8(payload_ptr, payload_len)
|
|
1385
|
+
};
|
|
1386
|
+
let request =
|
|
1387
|
+
ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| requests.borrow().get(&request_id).cloned());
|
|
1388
|
+
let Some(request) = request else {
|
|
1389
|
+
return;
|
|
1390
|
+
};
|
|
1391
|
+
let callback = request.borrow().on_progress.clone();
|
|
1392
|
+
if let Some(callback) = callback {
|
|
1393
|
+
callback(FileWorkerProcessProgress {
|
|
1394
|
+
processed_bytes,
|
|
1395
|
+
total_bytes,
|
|
1396
|
+
output_file_name: if text.is_empty() { None } else { Some(text) },
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
#[no_mangle]
|
|
1402
|
+
pub extern "C" fn __fui_on_file_worker_process_chunk(
|
|
1403
|
+
request_id: u32,
|
|
1404
|
+
offset_bytes: u64,
|
|
1405
|
+
file_size_bytes: u64,
|
|
1406
|
+
payload_ptr: *const u8,
|
|
1407
|
+
payload_len: u32,
|
|
1408
|
+
) {
|
|
1409
|
+
let request =
|
|
1410
|
+
ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| requests.borrow().get(&request_id).cloned());
|
|
1411
|
+
let Some(request) = request else {
|
|
1412
|
+
return;
|
|
1413
|
+
};
|
|
1414
|
+
let callback = request.borrow().on_chunk.clone();
|
|
1415
|
+
if let Some(callback) = callback {
|
|
1416
|
+
let bytes = if payload_ptr.is_null() || payload_len == 0 {
|
|
1417
|
+
Vec::new()
|
|
1418
|
+
} else {
|
|
1419
|
+
unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }.to_vec()
|
|
1420
|
+
};
|
|
1421
|
+
callback(FileReadChunk {
|
|
1422
|
+
offset_bytes,
|
|
1423
|
+
file_size_bytes,
|
|
1424
|
+
bytes,
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
#[no_mangle]
|
|
1430
|
+
pub extern "C" fn __fui_on_file_worker_process_complete(
|
|
1431
|
+
request_id: u32,
|
|
1432
|
+
processed_bytes: u64,
|
|
1433
|
+
payload_ptr: *const u8,
|
|
1434
|
+
payload_len: u32,
|
|
1435
|
+
) {
|
|
1436
|
+
let text = if payload_ptr.is_null() || payload_len == 0 {
|
|
1437
|
+
String::new()
|
|
1438
|
+
} else {
|
|
1439
|
+
read_utf8(payload_ptr, payload_len)
|
|
1440
|
+
};
|
|
1441
|
+
let (output_file_name, worker_result) = split_worker_process_complete_payload(text);
|
|
1442
|
+
let Some((on_complete, _)) = finish_worker_process(request_id) else {
|
|
1443
|
+
return;
|
|
1444
|
+
};
|
|
1445
|
+
if let Some(callback) = on_complete {
|
|
1446
|
+
callback(FileWorkerProcessResult {
|
|
1447
|
+
processed_bytes,
|
|
1448
|
+
output_file_name,
|
|
1449
|
+
worker_result,
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
#[no_mangle]
|
|
1455
|
+
pub extern "C" fn __fui_on_file_worker_process_error(
|
|
1456
|
+
request_id: u32,
|
|
1457
|
+
status: u32,
|
|
1458
|
+
payload_ptr: *const u8,
|
|
1459
|
+
payload_len: u32,
|
|
1460
|
+
) {
|
|
1461
|
+
let message = if payload_ptr.is_null() || payload_len == 0 {
|
|
1462
|
+
describe_file_failure(status, "Worker file processing failed.")
|
|
1463
|
+
} else {
|
|
1464
|
+
read_utf8(payload_ptr, payload_len)
|
|
1465
|
+
};
|
|
1466
|
+
let Some((_, on_error)) = finish_worker_process(request_id) else {
|
|
1467
|
+
return;
|
|
1468
|
+
};
|
|
1469
|
+
dispatch_file_error(on_error, message);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
#[cfg(test)]
|
|
1473
|
+
mod tests {
|
|
1474
|
+
use super::*;
|
|
1475
|
+
use crate::ffi::{self, Call};
|
|
1476
|
+
use std::cell::RefCell;
|
|
1477
|
+
use std::rc::Rc;
|
|
1478
|
+
|
|
1479
|
+
fn writer_payload(mode: u32, first: &str, second: Option<&str>) -> Vec<u8> {
|
|
1480
|
+
let mut bytes = Vec::new();
|
|
1481
|
+
bytes.extend_from_slice(&mode.to_le_bytes());
|
|
1482
|
+
bytes.extend_from_slice(&(first.len() as u32).to_le_bytes());
|
|
1483
|
+
bytes.extend_from_slice(first.as_bytes());
|
|
1484
|
+
if let Some(second) = second {
|
|
1485
|
+
bytes.extend_from_slice(&(second.len() as u32).to_le_bytes());
|
|
1486
|
+
bytes.extend_from_slice(second.as_bytes());
|
|
1487
|
+
}
|
|
1488
|
+
bytes
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
#[test]
|
|
1492
|
+
fn open_request_emits_host_call_and_pick_result_registers_files() {
|
|
1493
|
+
reset_file_runtime();
|
|
1494
|
+
ffi::test::reset();
|
|
1495
|
+
let picked = Rc::new(RefCell::new(Vec::<BrowserFile>::new()));
|
|
1496
|
+
let picked_clone = picked.clone();
|
|
1497
|
+
let _guard = File::open()
|
|
1498
|
+
.accept(".txt")
|
|
1499
|
+
.multiple(true)
|
|
1500
|
+
.pick(move |event| {
|
|
1501
|
+
picked_clone.replace(event.files);
|
|
1502
|
+
});
|
|
1503
|
+
let calls = ffi::test::take_calls();
|
|
1504
|
+
assert!(calls.iter().any(|call| matches!(call, Call::FilePick { request_id, accept, multiple } if *request_id == 1 && accept == ".txt" && *multiple)));
|
|
1505
|
+
|
|
1506
|
+
let mut payload = Vec::new();
|
|
1507
|
+
payload.extend_from_slice(&1u32.to_le_bytes());
|
|
1508
|
+
payload.extend_from_slice(&5u32.to_le_bytes());
|
|
1509
|
+
payload.extend_from_slice(b"file-");
|
|
1510
|
+
payload.extend_from_slice(&12u64.to_le_bytes());
|
|
1511
|
+
payload.extend_from_slice(&34u64.to_le_bytes());
|
|
1512
|
+
payload.extend_from_slice(&8u32.to_le_bytes());
|
|
1513
|
+
payload.extend_from_slice(b"note.txt");
|
|
1514
|
+
payload.extend_from_slice(&10u32.to_le_bytes());
|
|
1515
|
+
payload.extend_from_slice(b"text/plain");
|
|
1516
|
+
super::__fui_on_file_pick_result(
|
|
1517
|
+
1,
|
|
1518
|
+
FILE_STATUS_SUCCESS,
|
|
1519
|
+
payload.as_ptr(),
|
|
1520
|
+
payload.len() as u32,
|
|
1521
|
+
);
|
|
1522
|
+
|
|
1523
|
+
let picked = picked.borrow();
|
|
1524
|
+
assert_eq!(picked.len(), 1);
|
|
1525
|
+
assert_eq!(picked[0].name(), "note.txt");
|
|
1526
|
+
assert_eq!(picked[0].mime_type(), Some("text/plain".to_string()));
|
|
1527
|
+
assert_eq!(picked[0].size_bytes(), 12);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
#[test]
|
|
1531
|
+
fn browser_file_read_chunk_emits_host_call_and_callback_receives_bytes() {
|
|
1532
|
+
reset_file_runtime();
|
|
1533
|
+
ffi::test::reset();
|
|
1534
|
+
let file =
|
|
1535
|
+
register_browser_file("picked-1".to_string(), "demo.bin".to_string(), None, 100, 0);
|
|
1536
|
+
let chunk = Rc::new(RefCell::new(None::<FileReadChunk>));
|
|
1537
|
+
let chunk_clone = chunk.clone();
|
|
1538
|
+
let _guard = file.read_bytes_chunk(10, 8, move |value| {
|
|
1539
|
+
chunk_clone.replace(Some(value));
|
|
1540
|
+
});
|
|
1541
|
+
let calls = ffi::test::take_calls();
|
|
1542
|
+
assert!(calls.iter().any(|call| matches!(call, Call::FileReadChunk { request_id, file_id, offset_bytes, max_bytes } if *request_id == 1 && file_id == "picked-1" && *offset_bytes == 10 && *max_bytes == 8)));
|
|
1543
|
+
|
|
1544
|
+
let bytes = b"1234".to_vec();
|
|
1545
|
+
super::__fui_on_file_read_result(
|
|
1546
|
+
1,
|
|
1547
|
+
FILE_STATUS_SUCCESS,
|
|
1548
|
+
10,
|
|
1549
|
+
100,
|
|
1550
|
+
bytes.as_ptr(),
|
|
1551
|
+
bytes.len() as u32,
|
|
1552
|
+
);
|
|
1553
|
+
let chunk = chunk.borrow().clone().expect("chunk");
|
|
1554
|
+
assert_eq!(chunk.offset_bytes, 10);
|
|
1555
|
+
assert_eq!(chunk.file_size_bytes, 100);
|
|
1556
|
+
assert_eq!(chunk.bytes, b"1234");
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
#[test]
|
|
1560
|
+
fn save_text_emits_host_call() {
|
|
1561
|
+
reset_file_runtime();
|
|
1562
|
+
ffi::test::reset();
|
|
1563
|
+
let _guard = File::save()
|
|
1564
|
+
.suggested_name("report")
|
|
1565
|
+
.mime_type("text/plain")
|
|
1566
|
+
.file_extension(".txt")
|
|
1567
|
+
.save_text("hello", |_| {});
|
|
1568
|
+
let calls = ffi::test::take_calls();
|
|
1569
|
+
assert!(calls.iter().any(|call| matches!(call, Call::FileSaveText { request_id, suggested_name, mime_type, file_extension, text } if *request_id == 1 && suggested_name == "report" && mime_type == "text/plain" && file_extension == ".txt" && text == "hello")));
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
#[test]
|
|
1573
|
+
fn writer_created_and_finish_callbacks_decode_payload() {
|
|
1574
|
+
reset_file_runtime();
|
|
1575
|
+
ffi::test::reset();
|
|
1576
|
+
let writer = Rc::new(RefCell::new(None::<BrowserFileWriter>));
|
|
1577
|
+
let writer_clone = writer.clone();
|
|
1578
|
+
let _guard = File::save()
|
|
1579
|
+
.suggested_name("report")
|
|
1580
|
+
.create_writer(move |value| {
|
|
1581
|
+
writer_clone.replace(Some(value));
|
|
1582
|
+
});
|
|
1583
|
+
let payload = writer_payload(
|
|
1584
|
+
FileSaveMode::NativePicker as u32,
|
|
1585
|
+
"writer-1",
|
|
1586
|
+
Some("report.txt"),
|
|
1587
|
+
);
|
|
1588
|
+
super::__fui_on_file_writer_created(
|
|
1589
|
+
1,
|
|
1590
|
+
FILE_STATUS_SUCCESS,
|
|
1591
|
+
payload.as_ptr(),
|
|
1592
|
+
payload.len() as u32,
|
|
1593
|
+
);
|
|
1594
|
+
let writer = writer.borrow().clone().expect("writer");
|
|
1595
|
+
assert_eq!(writer.file_name, "report.txt");
|
|
1596
|
+
assert_eq!(writer.mode, FileSaveMode::NativePicker);
|
|
1597
|
+
|
|
1598
|
+
ffi::test::reset();
|
|
1599
|
+
let finished = Rc::new(RefCell::new(None::<FileSaveResult>));
|
|
1600
|
+
let finished_clone = finished.clone();
|
|
1601
|
+
let _guard = writer.finish(move |value| {
|
|
1602
|
+
finished_clone.replace(Some(value));
|
|
1603
|
+
});
|
|
1604
|
+
let calls = ffi::test::take_calls();
|
|
1605
|
+
assert!(calls.iter().any(|call| matches!(call, Call::FileWriterFinish { request_id, writer_id } if *request_id == 2 && writer_id == "writer-1")));
|
|
1606
|
+
let payload = writer_payload(FileSaveMode::NativePicker as u32, "report.txt", None);
|
|
1607
|
+
super::__fui_on_file_finish_result(
|
|
1608
|
+
2,
|
|
1609
|
+
FILE_STATUS_SUCCESS,
|
|
1610
|
+
44,
|
|
1611
|
+
payload.as_ptr(),
|
|
1612
|
+
payload.len() as u32,
|
|
1613
|
+
);
|
|
1614
|
+
let finished = finished.borrow().clone().expect("finish");
|
|
1615
|
+
assert_eq!(finished.file_name, "report.txt");
|
|
1616
|
+
assert_eq!(finished.written_bytes, 44);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
#[test]
|
|
1620
|
+
fn worker_process_requires_worker_and_sink() {
|
|
1621
|
+
reset_file_runtime();
|
|
1622
|
+
ffi::test::reset();
|
|
1623
|
+
let file =
|
|
1624
|
+
register_browser_file("picked-1".to_string(), "demo.bin".to_string(), None, 100, 0);
|
|
1625
|
+
let error = Rc::new(RefCell::new(String::new()));
|
|
1626
|
+
let error_clone = error.clone();
|
|
1627
|
+
let _request = File::process_file_in_worker(file)
|
|
1628
|
+
.on_error(move |event| {
|
|
1629
|
+
error_clone.replace(event.message);
|
|
1630
|
+
})
|
|
1631
|
+
.start();
|
|
1632
|
+
assert!(error
|
|
1633
|
+
.borrow()
|
|
1634
|
+
.contains("worker(wasm_path, entry_name) is required"));
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
#[test]
|
|
1638
|
+
fn worker_process_start_emits_host_call_and_callbacks_receive_payloads() {
|
|
1639
|
+
reset_file_runtime();
|
|
1640
|
+
ffi::test::reset();
|
|
1641
|
+
let file =
|
|
1642
|
+
register_browser_file("picked-1".to_string(), "demo.bin".to_string(), None, 100, 0);
|
|
1643
|
+
let progress = Rc::new(RefCell::new(None::<FileWorkerProcessProgress>));
|
|
1644
|
+
let result = Rc::new(RefCell::new(None::<FileWorkerProcessResult>));
|
|
1645
|
+
let progress_clone = progress.clone();
|
|
1646
|
+
let result_clone = result.clone();
|
|
1647
|
+
let _request = File::process_file_in_worker(file)
|
|
1648
|
+
.worker("./workers/file_worker.wasm", "entry")
|
|
1649
|
+
.save_to_picked_file("copy.bin")
|
|
1650
|
+
.on_progress(move |value| {
|
|
1651
|
+
progress_clone.replace(Some(value));
|
|
1652
|
+
})
|
|
1653
|
+
.on_complete(move |value| {
|
|
1654
|
+
result_clone.replace(Some(value));
|
|
1655
|
+
})
|
|
1656
|
+
.start();
|
|
1657
|
+
let calls = ffi::test::take_calls();
|
|
1658
|
+
assert!(calls.iter().any(|call| matches!(call, Call::FileProcessWorkerStart { request_id, worker_wasm_path, worker_entry_name, file_id, suggested_name, chunk_bytes, save_to_picked_file } if *request_id == 1 && worker_wasm_path == "./workers/file_worker.wasm" && worker_entry_name == "entry" && file_id == "picked-1" && suggested_name == "copy.bin" && *chunk_bytes == 64 * 1024 && *save_to_picked_file)));
|
|
1659
|
+
|
|
1660
|
+
super::__fui_on_file_worker_process_progress(1, 20, 100, b"copy.bin".as_ptr(), 8);
|
|
1661
|
+
let progress = progress.borrow().clone().expect("progress");
|
|
1662
|
+
assert_eq!(progress.processed_bytes, 20);
|
|
1663
|
+
assert_eq!(progress.output_file_name, Some("copy.bin".to_string()));
|
|
1664
|
+
|
|
1665
|
+
let payload = b"copy.bin\0sha256".to_vec();
|
|
1666
|
+
super::__fui_on_file_worker_process_complete(
|
|
1667
|
+
1,
|
|
1668
|
+
100,
|
|
1669
|
+
payload.as_ptr(),
|
|
1670
|
+
payload.len() as u32,
|
|
1671
|
+
);
|
|
1672
|
+
let result = result.borrow().clone().expect("result");
|
|
1673
|
+
assert_eq!(result.processed_bytes, 100);
|
|
1674
|
+
assert_eq!(result.output_file_name, Some("copy.bin".to_string()));
|
|
1675
|
+
assert_eq!(result.worker_result, Some("sha256".to_string()));
|
|
1676
|
+
}
|
|
1677
|
+
}
|