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
4use i_slint_compiler::langtype::Type;
5use i_slint_core::window::WindowInner;
6use napi::{Env, Error, JsFunction, JsUnknown, NapiRaw, NapiValue, Ref, Result};
7use slint_interpreter::{ComponentHandle, ComponentInstance, Value};
8
9use crate::JsWindow;
10
11use super::JsComponentDefinition;
12
13#[napi(js_name = "ComponentInstance")]
14pub struct JsComponentInstance {
15 inner: ComponentInstance,
16}
17
18impl From<ComponentInstance> for JsComponentInstance {
19 fn from(instance: ComponentInstance) -> Self {
20 Self { inner: instance }
21 }
22}
23
24#[napi]
25impl 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.
348pub struct RefCountedReference {
349 env: Env,
350 reference: Ref<()>,
351}
352
353impl 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
363impl Drop for RefCountedReference {
364 fn drop(&mut self) {
365 self.reference.unref(self.env).unwrap();
366 }
367}
368