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
7use std::path::Path;
8use wasm_bindgen::prelude::*;
9
10use slint_interpreter::ComponentHandle;
11
12#[wasm_bindgen]
13#[allow(dead_code)]
14pub struct CompilationResult {
15 component: Option<WrappedCompiledComp>,
16 diagnostics: js_sys::Array,
17 error_string: String,
18}
19
20#[wasm_bindgen]
21impl 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)]
37const CALLBACK_FUNCTION_SECTION: &'static str = r#"
38type ImportCallbackFunction = (url: string) => Promise<string>;
39type CurrentElementInformationCallbackFunction = (url: string, start_line: number, start_column: number, end_line: number, end_column: number) => void;
40"#;
41
42#[wasm_bindgen]
43extern "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]
57pub 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]
67pub 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)]
148pub struct WrappedCompiledComp(slint_interpreter::ComponentDefinition);
149
150#[wasm_bindgen]
151impl 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]
222pub struct WrappedInstance(slint_interpreter::ComponentInstance);
223
224impl Clone for WrappedInstance {
225 fn clone(&self) -> Self {
226 Self(self.0.clone_strong())
227 }
228}
229
230#[wasm_bindgen]
231impl 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]
312pub fn run_event_loop() -> Result<(), JsValue> {
313 slint_interpreter::spawn_event_loop().map_err(|e| -> JsValue { format!("{e}").into() })?;
314 Ok(())
315}
316