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 | use i_slint_compiler::langtype::Type; |
5 | use i_slint_core::window::WindowInner; |
6 | use napi::{Env, Error, JsFunction, JsUnknown, NapiRaw, NapiValue, Ref, Result}; |
7 | use slint_interpreter::{ComponentHandle, ComponentInstance, Value}; |
8 | |
9 | use crate::JsWindow; |
10 | |
11 | use super::JsComponentDefinition; |
12 | |
13 | #[napi (js_name = "ComponentInstance" )] |
14 | pub struct JsComponentInstance { |
15 | inner: ComponentInstance, |
16 | } |
17 | |
18 | impl From<ComponentInstance> for JsComponentInstance { |
19 | fn from(instance: ComponentInstance) -> Self { |
20 | Self { inner: instance } |
21 | } |
22 | } |
23 | |
24 | #[napi ] |
25 | impl JsComponentInstance { |
26 | #[napi (constructor)] |
27 | pub fn new() -> napi::Result<Self> { |
28 | Err(napi::Error::from_reason( |
29 | "ComponentInstance can only be created by using ComponentCompiler." .to_string(), |
30 | )) |
31 | } |
32 | |
33 | #[napi ] |
34 | pub fn definition(&self) -> JsComponentDefinition { |
35 | self.inner.definition().into() |
36 | } |
37 | |
38 | #[napi ] |
39 | pub fn get_property(&self, env: Env, name: String) -> Result<JsUnknown> { |
40 | let value = self |
41 | .inner |
42 | .get_property(name.as_ref()) |
43 | .map_err(|e| Error::from_reason(e.to_string()))?; |
44 | super::value::to_js_unknown(&env, &value) |
45 | } |
46 | |
47 | #[napi ] |
48 | pub fn set_property(&self, env: Env, prop_name: String, js_value: JsUnknown) -> Result<()> { |
49 | let ty = self |
50 | .inner |
51 | .definition() |
52 | .properties_and_callbacks() |
53 | .find_map(|(name, proptype)| if name == prop_name { Some(proptype) } else { None }) |
54 | .ok_or(()) |
55 | .map_err(|_| { |
56 | napi::Error::from_reason(format!("Property {prop_name} not found in the component" )) |
57 | })?; |
58 | |
59 | self.inner |
60 | .set_property(&prop_name, super::value::to_value(&env, js_value, &ty)?) |
61 | .map_err(|e| Error::from_reason(format!("{e}" )))?; |
62 | |
63 | Ok(()) |
64 | } |
65 | |
66 | #[napi ] |
67 | pub fn get_global_property( |
68 | &self, |
69 | env: Env, |
70 | global_name: String, |
71 | name: String, |
72 | ) -> Result<JsUnknown> { |
73 | if !self.definition().globals().contains(&global_name) { |
74 | return Err(napi::Error::from_reason(format!("Global {global_name} not found" ))); |
75 | } |
76 | let value = self |
77 | .inner |
78 | .get_global_property(global_name.as_ref(), name.as_ref()) |
79 | .map_err(|e| Error::from_reason(e.to_string()))?; |
80 | super::value::to_js_unknown(&env, &value) |
81 | } |
82 | |
83 | #[napi ] |
84 | pub fn set_global_property( |
85 | &self, |
86 | env: Env, |
87 | global_name: String, |
88 | prop_name: String, |
89 | js_value: JsUnknown, |
90 | ) -> Result<()> { |
91 | let ty = self |
92 | .inner |
93 | .definition() |
94 | .global_properties_and_callbacks(global_name.as_str()) |
95 | .ok_or(napi::Error::from_reason(format!("Global {global_name} not found" )))? |
96 | .find_map(|(name, proptype)| if name == prop_name { Some(proptype) } else { None }) |
97 | .ok_or(()) |
98 | .map_err(|_| { |
99 | napi::Error::from_reason(format!( |
100 | "Property {prop_name} of global {global_name} not found in the component" |
101 | )) |
102 | })?; |
103 | |
104 | self.inner |
105 | .set_global_property( |
106 | global_name.as_str(), |
107 | &prop_name, |
108 | super::value::to_value(&env, js_value, &ty)?, |
109 | ) |
110 | .map_err(|e| Error::from_reason(format!("{e}" )))?; |
111 | |
112 | Ok(()) |
113 | } |
114 | |
115 | #[napi ] |
116 | pub fn set_callback( |
117 | &self, |
118 | env: Env, |
119 | callback_name: String, |
120 | callback: JsFunction, |
121 | ) -> Result<()> { |
122 | let function_ref = RefCountedReference::new(&env, callback)?; |
123 | |
124 | let ty = self |
125 | .inner |
126 | .definition() |
127 | .properties_and_callbacks() |
128 | .find_map(|(name, proptype)| if name == callback_name { Some(proptype) } else { None }) |
129 | .ok_or(()) |
130 | .map_err(|_| { |
131 | napi::Error::from_reason(format!( |
132 | "Callback {callback_name} not found in the component" |
133 | )) |
134 | })?; |
135 | |
136 | if let Type::Callback { return_type, .. } = ty { |
137 | self.inner |
138 | .set_callback(callback_name.as_str(), { |
139 | let return_type = return_type.clone(); |
140 | |
141 | move |args| { |
142 | let callback: JsFunction = function_ref.get().unwrap(); |
143 | let result = callback |
144 | .call( |
145 | None, |
146 | args.iter() |
147 | .map(|v| super::value::to_js_unknown(&env, v).unwrap()) |
148 | .collect::<Vec<JsUnknown>>() |
149 | .as_ref(), |
150 | ) |
151 | .unwrap(); |
152 | |
153 | if let Some(return_type) = &return_type { |
154 | super::to_value(&env, result, return_type).unwrap() |
155 | } else { |
156 | Value::Void |
157 | } |
158 | } |
159 | }) |
160 | .map_err(|_| napi::Error::from_reason("Cannot set callback." ))?; |
161 | |
162 | return Ok(()); |
163 | } |
164 | |
165 | Err(napi::Error::from_reason(format!("{} is not a callback" , callback_name).as_str())) |
166 | } |
167 | |
168 | #[napi ] |
169 | pub fn set_global_callback( |
170 | &self, |
171 | env: Env, |
172 | global_name: String, |
173 | callback_name: String, |
174 | callback: JsFunction, |
175 | ) -> Result<()> { |
176 | let function_ref = RefCountedReference::new(&env, callback)?; |
177 | |
178 | let ty = self |
179 | .inner |
180 | .definition() |
181 | .global_properties_and_callbacks(global_name.as_str()) |
182 | .ok_or(napi::Error::from_reason(format!("Global {global_name} not found" )))? |
183 | .find_map(|(name, proptype)| if name == callback_name { Some(proptype) } else { None }) |
184 | .ok_or(()) |
185 | .map_err(|_| { |
186 | napi::Error::from_reason(format!( |
187 | "Callback {callback_name} of global {global_name} not found in the component" |
188 | )) |
189 | })?; |
190 | |
191 | if let Type::Callback { return_type, .. } = ty { |
192 | self.inner |
193 | .set_global_callback(global_name.as_str(), callback_name.as_str(), { |
194 | let return_type = return_type.clone(); |
195 | |
196 | move |args| { |
197 | let callback: JsFunction = function_ref.get().unwrap(); |
198 | let result = callback |
199 | .call( |
200 | None, |
201 | args.iter() |
202 | .map(|v| super::value::to_js_unknown(&env, v).unwrap()) |
203 | .collect::<Vec<JsUnknown>>() |
204 | .as_ref(), |
205 | ) |
206 | .unwrap(); |
207 | |
208 | if let Some(return_type) = &return_type { |
209 | super::to_value(&env, result, return_type).unwrap() |
210 | } else { |
211 | Value::Void |
212 | } |
213 | } |
214 | }) |
215 | .map_err(|_| napi::Error::from_reason("Cannot set callback." ))?; |
216 | |
217 | return Ok(()); |
218 | } |
219 | |
220 | Err(napi::Error::from_reason(format!("{} is not a callback" , callback_name).as_str())) |
221 | } |
222 | |
223 | #[napi ] |
224 | pub fn invoke( |
225 | &self, |
226 | env: Env, |
227 | callback_name: String, |
228 | arguments: Vec<JsUnknown>, |
229 | ) -> Result<JsUnknown> { |
230 | let ty = self |
231 | .inner |
232 | .definition() |
233 | .properties_and_callbacks() |
234 | .find_map(|(name, proptype)| if name == callback_name { Some(proptype) } else { None }) |
235 | .ok_or(()) |
236 | .map_err(|_| { |
237 | napi::Error::from_reason( |
238 | format!("Callback {} not found in the component" , callback_name).as_str(), |
239 | ) |
240 | })?; |
241 | |
242 | let args = if let Type::Callback { args, .. } = ty { |
243 | let count = args.len(); |
244 | let args = arguments |
245 | .into_iter() |
246 | .zip(args.into_iter()) |
247 | .map(|(a, ty)| super::value::to_value(&env, a, &ty)) |
248 | .collect::<Result<Vec<_>, _>>()?; |
249 | if args.len() != count { |
250 | return Err(napi::Error::from_reason( |
251 | format!( |
252 | "{} expect {} arguments, but {} where provided" , |
253 | callback_name, |
254 | count, |
255 | args.len() |
256 | ) |
257 | .as_str(), |
258 | )); |
259 | } |
260 | args |
261 | } else { |
262 | return Err(napi::Error::from_reason( |
263 | format!("{} is not a callback" , callback_name).as_str(), |
264 | )); |
265 | }; |
266 | |
267 | let result = self |
268 | .inner |
269 | .invoke(callback_name.as_str(), args.as_slice()) |
270 | .map_err(|_| napi::Error::from_reason("Cannot invoke callback." ))?; |
271 | super::to_js_unknown(&env, &result) |
272 | } |
273 | |
274 | #[napi ] |
275 | pub fn invoke_global( |
276 | &self, |
277 | env: Env, |
278 | global_name: String, |
279 | callback_name: String, |
280 | arguments: Vec<JsUnknown>, |
281 | ) -> Result<JsUnknown> { |
282 | let ty = self |
283 | .inner |
284 | .definition() |
285 | .global_properties_and_callbacks(global_name.as_str()) |
286 | .ok_or(napi::Error::from_reason(format!("Global {global_name} not found" )))? |
287 | .find_map(|(name, proptype)| if name == callback_name { Some(proptype) } else { None }) |
288 | .ok_or(()) |
289 | .map_err(|_| { |
290 | napi::Error::from_reason( |
291 | format!( |
292 | "Callback {} of global {global_name} not found in the component" , |
293 | callback_name |
294 | ) |
295 | .as_str(), |
296 | ) |
297 | })?; |
298 | |
299 | let args = if let Type::Callback { args, .. } = ty { |
300 | let count = args.len(); |
301 | let args = arguments |
302 | .into_iter() |
303 | .zip(args.into_iter()) |
304 | .map(|(a, ty)| super::value::to_value(&env, a, &ty)) |
305 | .collect::<Result<Vec<_>, _>>()?; |
306 | if args.len() != count { |
307 | return Err(napi::Error::from_reason( |
308 | format!( |
309 | "{} expect {} arguments, but {} where provided" , |
310 | callback_name, |
311 | count, |
312 | args.len() |
313 | ) |
314 | .as_str(), |
315 | )); |
316 | } |
317 | args |
318 | } else { |
319 | return Err(napi::Error::from_reason( |
320 | format!("{} is not a callback on global {}" , callback_name, global_name).as_str(), |
321 | )); |
322 | }; |
323 | |
324 | let result = self |
325 | .inner |
326 | .invoke_global(global_name.as_str(), callback_name.as_str(), args.as_slice()) |
327 | .map_err(|_| napi::Error::from_reason("Cannot invoke callback." ))?; |
328 | super::to_js_unknown(&env, &result) |
329 | } |
330 | |
331 | #[napi ] |
332 | pub fn send_mouse_click(&self, x: f64, y: f64) { |
333 | slint_interpreter::testing::send_mouse_click(&self.inner, x as f32, y as f32); |
334 | } |
335 | |
336 | #[napi ] |
337 | pub fn send_keyboard_string_sequence(&self, sequence: String) { |
338 | slint_interpreter::testing::send_keyboard_string_sequence(&self.inner, sequence.into()); |
339 | } |
340 | |
341 | #[napi ] |
342 | pub fn window(&self) -> Result<JsWindow> { |
343 | Ok(JsWindow { inner: WindowInner::from_pub(self.inner.window()).window_adapter() }) |
344 | } |
345 | } |
346 | |
347 | // Wrapper around Ref<>, which requires manual ref-counting. |
348 | pub struct RefCountedReference { |
349 | env: Env, |
350 | reference: Ref<()>, |
351 | } |
352 | |
353 | impl RefCountedReference { |
354 | pub fn new<T: NapiRaw>(env: &Env, value: T) -> Result<Self> { |
355 | Ok(Self { env: env.clone(), reference: env.create_reference(value)? }) |
356 | } |
357 | |
358 | pub fn get<T: NapiValue>(&self) -> Result<T> { |
359 | self.env.get_reference_value(&self.reference) |
360 | } |
361 | } |
362 | |
363 | impl Drop for RefCountedReference { |
364 | fn drop(&mut self) { |
365 | self.reference.unref(self.env).unwrap(); |
366 | } |
367 | } |
368 | |