1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | //! This wasm library can be loaded from JS to load and display the content of .slint files |
5 | #![cfg (target_arch = "wasm32" )] |
6 | |
7 | use std::path::Path; |
8 | use wasm_bindgen::prelude::*; |
9 | |
10 | use slint_interpreter::ComponentHandle; |
11 | |
12 | #[wasm_bindgen] |
13 | #[allow (dead_code)] |
14 | pub struct CompilationResult { |
15 | component: Option<WrappedCompiledComp>, |
16 | diagnostics: js_sys::Array, |
17 | error_string: String, |
18 | } |
19 | |
20 | #[wasm_bindgen] |
21 | impl CompilationResult { |
22 | #[wasm_bindgen(getter)] |
23 | pub fn component(&self) -> Option<WrappedCompiledComp> { |
24 | self.component.clone() |
25 | } |
26 | #[wasm_bindgen(getter)] |
27 | pub fn diagnostics(&self) -> js_sys::Array { |
28 | self.diagnostics.clone() |
29 | } |
30 | #[wasm_bindgen(getter)] |
31 | pub fn error_string(&self) -> String { |
32 | self.error_string.clone() |
33 | } |
34 | } |
35 | |
36 | #[wasm_bindgen(typescript_custom_section)] |
37 | const CALLBACK_FUNCTION_SECTION: &'static str = r#" |
38 | type ImportCallbackFunction = (url: string) => Promise<string>; |
39 | type CurrentElementInformationCallbackFunction = (url: string, start_line: number, start_column: number, end_line: number, end_column: number) => void; |
40 | "# ; |
41 | |
42 | #[wasm_bindgen] |
43 | extern "C" { |
44 | #[wasm_bindgen(typescript_type = "ImportCallbackFunction" )] |
45 | pub type ImportCallbackFunction; |
46 | |
47 | #[wasm_bindgen(typescript_type = "CurrentElementInformationCallbackFunction" )] |
48 | pub type CurrentElementInformationCallbackFunction; |
49 | #[wasm_bindgen(typescript_type = "Promise<WrappedInstance>" )] |
50 | pub type InstancePromise; |
51 | } |
52 | |
53 | /// Compile the content of a string. |
54 | /// |
55 | /// Returns a promise to a compiled component which can be run with ".run()" |
56 | #[wasm_bindgen] |
57 | pub async fn compile_from_string( |
58 | source: String, |
59 | base_url: String, |
60 | optional_import_callback: Option<ImportCallbackFunction>, |
61 | ) -> Result<CompilationResult, JsValue> { |
62 | compile_from_string_with_style(source, base_url, String::new(), optional_import_callback).await |
63 | } |
64 | |
65 | /// Same as [`compile_from_string`], but also takes a style parameter |
66 | #[wasm_bindgen] |
67 | pub async fn compile_from_string_with_style( |
68 | source: String, |
69 | base_url: String, |
70 | style: String, |
71 | optional_import_callback: Option<ImportCallbackFunction>, |
72 | ) -> Result<CompilationResult, JsValue> { |
73 | #[cfg (feature = "console_error_panic_hook" )] |
74 | console_error_panic_hook::set_once(); |
75 | |
76 | let mut compiler = slint_interpreter::ComponentCompiler::default(); |
77 | if !style.is_empty() { |
78 | compiler.set_style(style) |
79 | } |
80 | |
81 | if let Some(load_callback) = optional_import_callback { |
82 | let open_import_fallback = move |file_name: &Path| -> core::pin::Pin< |
83 | Box<dyn core::future::Future<Output = Option<std::io::Result<String>>>>, |
84 | > { |
85 | Box::pin({ |
86 | let load_callback = js_sys::Function::from(load_callback.clone()); |
87 | let file_name: String = file_name.to_string_lossy().into(); |
88 | async move { |
89 | let result = load_callback.call1(&JsValue::UNDEFINED, &file_name.into()); |
90 | let promise: js_sys::Promise = result.unwrap().into(); |
91 | let future = wasm_bindgen_futures::JsFuture::from(promise); |
92 | match future.await { |
93 | Ok(js_ok) => Some(Ok(js_ok.as_string().unwrap_or_default())), |
94 | Err(js_err) => Some(Err(std::io::Error::new( |
95 | std::io::ErrorKind::Other, |
96 | js_err.as_string().unwrap_or_default(), |
97 | ))), |
98 | } |
99 | } |
100 | }) |
101 | }; |
102 | compiler.set_file_loader(open_import_fallback); |
103 | } |
104 | |
105 | let c = compiler.build_from_source(source, base_url.into()).await; |
106 | |
107 | let line_key = JsValue::from_str("lineNumber" ); |
108 | let column_key = JsValue::from_str("columnNumber" ); |
109 | let message_key = JsValue::from_str("message" ); |
110 | let file_key = JsValue::from_str("fileName" ); |
111 | let level_key = JsValue::from_str("level" ); |
112 | let mut error_as_string = String::new(); |
113 | let array = js_sys::Array::new(); |
114 | for d in compiler.diagnostics().into_iter() { |
115 | let filename = |
116 | d.source_file().as_ref().map_or(String::new(), |sf| sf.to_string_lossy().into()); |
117 | |
118 | let filename_js = JsValue::from_str(&filename); |
119 | let (line, column) = d.line_column(); |
120 | |
121 | if d.level() == slint_interpreter::DiagnosticLevel::Error { |
122 | if !error_as_string.is_empty() { |
123 | error_as_string.push_str(" \n" ); |
124 | } |
125 | use std::fmt::Write; |
126 | |
127 | write!(&mut error_as_string, "{}:{}:{}" , filename, line, d).unwrap(); |
128 | } |
129 | |
130 | let error_obj = js_sys::Object::new(); |
131 | js_sys::Reflect::set(&error_obj, &message_key, &JsValue::from_str(&d.message()))?; |
132 | js_sys::Reflect::set(&error_obj, &line_key, &JsValue::from_f64(line as f64))?; |
133 | js_sys::Reflect::set(&error_obj, &column_key, &JsValue::from_f64(column as f64))?; |
134 | js_sys::Reflect::set(&error_obj, &file_key, &filename_js)?; |
135 | js_sys::Reflect::set(&error_obj, &level_key, &JsValue::from_f64(d.level() as i8 as f64))?; |
136 | array.push(&error_obj); |
137 | } |
138 | |
139 | Ok(CompilationResult { |
140 | component: c.map(|c| WrappedCompiledComp(c)), |
141 | diagnostics: array, |
142 | error_string: error_as_string, |
143 | }) |
144 | } |
145 | |
146 | #[wasm_bindgen] |
147 | #[derive(Clone)] |
148 | pub struct WrappedCompiledComp(slint_interpreter::ComponentDefinition); |
149 | |
150 | #[wasm_bindgen] |
151 | impl WrappedCompiledComp { |
152 | /// Run this compiled component in a canvas. |
153 | /// The HTML must contains a <canvas> element with the given `canvas_id` |
154 | /// where the result is gonna be rendered |
155 | #[wasm_bindgen] |
156 | pub fn run(&self, canvas_id: String) { |
157 | let component = self.0.create_with_canvas_id(&canvas_id).unwrap(); |
158 | component.show().unwrap(); |
159 | slint_interpreter::spawn_event_loop().unwrap(); |
160 | } |
161 | /// Creates this compiled component in a canvas, wrapped in a promise. |
162 | /// The HTML must contains a <canvas> element with the given `canvas_id` |
163 | /// where the result is gonna be rendered. |
164 | /// You need to call `show()` on the returned instance for rendering. |
165 | /// |
166 | /// Note that the promise will only be resolved after calling `slint.run_event_loop()`. |
167 | #[wasm_bindgen] |
168 | pub fn create(&self, canvas_id: String) -> Result<InstancePromise, JsValue> { |
169 | Ok(JsValue::from(js_sys::Promise::new(&mut |resolve, reject| { |
170 | let comp = send_wrapper::SendWrapper::new(self.0.clone()); |
171 | let canvas_id = canvas_id.clone(); |
172 | let resolve = send_wrapper::SendWrapper::new(resolve); |
173 | if let Err(e) = slint_interpreter::invoke_from_event_loop(move || { |
174 | let instance = |
175 | WrappedInstance(comp.take().create_with_canvas_id(&canvas_id).unwrap()); |
176 | resolve.take().call1(&JsValue::UNDEFINED, &JsValue::from(instance)).unwrap_throw(); |
177 | }) { |
178 | reject |
179 | .call1( |
180 | &JsValue::UNDEFINED, |
181 | &JsValue::from( |
182 | format!("internal error: Failed to queue closure for event loop invocation: {e}" ), |
183 | ), |
184 | ) |
185 | .unwrap_throw(); |
186 | } |
187 | })).unchecked_into::<InstancePromise>()) |
188 | } |
189 | /// Creates this compiled component in the canvas of the provided instance, wrapped in a promise. |
190 | /// For this to work, the provided instance needs to be visible (show() must've been |
191 | /// called) and the event loop must be running (`slint.run_event_loop()`). After this |
192 | /// call the provided instance is not rendered anymore and can be discarded. |
193 | /// |
194 | /// Note that the promise will only be resolved after calling `slint.run_event_loop()`. |
195 | #[wasm_bindgen] |
196 | pub fn create_with_existing_window( |
197 | &self, |
198 | instance: WrappedInstance, |
199 | ) -> Result<InstancePromise, JsValue> { |
200 | Ok(JsValue::from(js_sys::Promise::new(&mut |resolve, reject| { |
201 | let params = send_wrapper::SendWrapper::new((self.0.clone(), instance.0.clone_strong(), resolve)); |
202 | if let Err(e) = slint_interpreter::invoke_from_event_loop(move || { |
203 | let (comp, instance, resolve) = params.take(); |
204 | let instance = |
205 | WrappedInstance(comp.create_with_existing_window(instance.window()).unwrap()); |
206 | resolve.call1(&JsValue::UNDEFINED, &JsValue::from(instance)).unwrap_throw(); |
207 | }) { |
208 | reject |
209 | .call1( |
210 | &JsValue::UNDEFINED, |
211 | &JsValue::from( |
212 | format!("internal error: Failed to queue closure for event loop invocation: {e}" ), |
213 | ), |
214 | ) |
215 | .unwrap_throw(); |
216 | } |
217 | })).unchecked_into::<InstancePromise>()) |
218 | } |
219 | } |
220 | |
221 | #[wasm_bindgen] |
222 | pub struct WrappedInstance(slint_interpreter::ComponentInstance); |
223 | |
224 | impl Clone for WrappedInstance { |
225 | fn clone(&self) -> Self { |
226 | Self(self.0.clone_strong()) |
227 | } |
228 | } |
229 | |
230 | #[wasm_bindgen] |
231 | impl WrappedInstance { |
232 | /// Marks this instance for rendering and input handling. |
233 | /// |
234 | /// Note that the promise will only be resolved after calling `slint.run_event_loop()`. |
235 | #[wasm_bindgen] |
236 | pub fn show(&self) -> Result<js_sys::Promise, JsValue> { |
237 | self.invoke_from_event_loop_wrapped_in_promise(|instance| instance.show()) |
238 | } |
239 | /// Hides this instance and prevents further updates of the canvas element. |
240 | /// |
241 | /// Note that the promise will only be resolved after calling `slint.run_event_loop()`. |
242 | #[wasm_bindgen] |
243 | pub fn hide(&self) -> Result<js_sys::Promise, JsValue> { |
244 | self.invoke_from_event_loop_wrapped_in_promise(|instance| instance.hide()) |
245 | } |
246 | |
247 | fn invoke_from_event_loop_wrapped_in_promise( |
248 | &self, |
249 | callback: impl FnOnce( |
250 | &slint_interpreter::ComponentInstance, |
251 | ) -> Result<(), slint_interpreter::PlatformError> |
252 | + 'static, |
253 | ) -> Result<js_sys::Promise, JsValue> { |
254 | let callback = std::cell::RefCell::new(Some(callback)); |
255 | Ok(js_sys::Promise::new(&mut |resolve, reject| { |
256 | let inst_weak = self.0.as_weak(); |
257 | |
258 | if let Err(e) = slint_interpreter::invoke_from_event_loop({ |
259 | let params = send_wrapper::SendWrapper::new(( |
260 | resolve, |
261 | reject.clone(), |
262 | callback.take().unwrap(), |
263 | )); |
264 | move || { |
265 | let (resolve, reject, callback) = params.take(); |
266 | match inst_weak.upgrade() { |
267 | Some(instance) => match callback(&instance) { |
268 | Ok(()) => { |
269 | resolve.call0(&JsValue::UNDEFINED).unwrap_throw(); |
270 | } |
271 | Err(e) => { |
272 | reject |
273 | .call1( |
274 | &JsValue::UNDEFINED, |
275 | &JsValue::from(format!( |
276 | "Invocation on ComponentInstance from within event loop failed: {e}" |
277 | )), |
278 | ) |
279 | .unwrap_throw(); |
280 | } |
281 | }, |
282 | None => { |
283 | reject |
284 | .call1( |
285 | &JsValue::UNDEFINED, |
286 | &JsValue::from(format!( |
287 | "Invocation on ComponentInstance failed because instance was deleted too soon" |
288 | )), |
289 | ) |
290 | .unwrap_throw(); |
291 | } |
292 | } |
293 | } |
294 | }) { |
295 | reject |
296 | .call1( |
297 | &JsValue::UNDEFINED, |
298 | &JsValue::from( |
299 | format!("internal error: Failed to queue closure for event loop invocation: {e}" ), |
300 | ), |
301 | ) |
302 | .unwrap_throw(); |
303 | } |
304 | })) |
305 | } |
306 | } |
307 | |
308 | /// Register DOM event handlers on all instance and set up the event loop for that. |
309 | /// You can call this function only once. It will throw an exception but that is safe |
310 | /// to ignore. |
311 | #[wasm_bindgen] |
312 | pub fn run_event_loop() -> Result<(), JsValue> { |
313 | slint_interpreter::spawn_event_loop().map_err(|e| -> JsValue { format!("{e}" ).into() })?; |
314 | Ok(()) |
315 | } |
316 | |