1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use std::rc::Rc;
5
6use i_slint_compiler::langtype::Type;
7use i_slint_core::model::{Model, ModelNotify, ModelRc};
8use napi::{bindgen_prelude::*, JsSymbol};
9use napi::{Env, JsFunction, JsNumber, JsObject, JsUnknown, Result, ValueType};
10
11use crate::{to_js_unknown, to_value, RefCountedReference};
12
13#[napi]
14#[derive(Clone, Default)]
15pub struct SharedModelNotify(Rc<ModelNotify>);
16
17impl core::ops::Deref for SharedModelNotify {
18 type Target = Rc<ModelNotify>;
19
20 fn deref(&self) -> &Self::Target {
21 &self.0
22 }
23}
24
25pub(crate) fn js_into_rust_model(
26 env: &Env,
27 maybe_js_impl: &JsObject,
28 row_data_type: &Type,
29) -> Result<ModelRc<slint_interpreter::Value>> {
30 let shared_model_notify: SharedModelNotify = maybe_js_impl
31 .get("modelNotify")
32 .and_then(|prop| {
33 prop.ok_or_else(|| {
34 napi::Error::from_reason(
35 "Could not convert value to slint model: missing modelNotify property",
36 )
37 })
38 })
39 .map(|shared_model_notify: External<SharedModelNotify>| {
40 shared_model_notify.as_ref().clone()
41 })?;
42 Ok(RcRc::new(JsModel {
43 shared_model_notify,
44 env: env.clone(),
45 js_impl: RefCountedReference::new(env, value:maybe_js_impl)?,
46 row_data_type: row_data_type.clone(),
47 })
48 .into())
49}
50
51pub(crate) fn rust_into_js_model(
52 model: &ModelRc<slint_interpreter::Value>,
53) -> Option<Result<JsUnknown>> {
54 model.as_any().downcast_ref::<JsModel>().map(|rust_model: &JsModel| rust_model.js_impl.get())
55}
56
57struct JsModel {
58 shared_model_notify: SharedModelNotify,
59 env: Env,
60 js_impl: RefCountedReference,
61 row_data_type: Type,
62}
63
64#[napi]
65pub fn js_model_notify_new() -> Result<External<SharedModelNotify>> {
66 Ok(External::new(Default::default()))
67}
68
69#[napi]
70pub fn js_model_notify_row_data_changed(notify: External<SharedModelNotify>, row: u32) {
71 notify.row_changed(row as usize);
72}
73
74#[napi]
75pub fn js_model_notify_row_added(notify: External<SharedModelNotify>, row: u32, count: u32) {
76 notify.row_added(index:row as usize, count as usize);
77}
78
79#[napi]
80pub fn js_model_notify_row_removed(notify: External<SharedModelNotify>, row: u32, count: u32) {
81 notify.row_removed(index:row as usize, count as usize);
82}
83
84#[napi]
85pub fn js_model_notify_reset(notify: External<SharedModelNotify>) {
86 notify.reset();
87}
88
89impl Model for JsModel {
90 type Data = slint_interpreter::Value;
91
92 fn row_count(&self) -> usize {
93 let Ok(model) = self.js_impl.get::<Object>() else {
94 eprintln!("Node.js: JavaScript Model<T>'s rowCount threw an exception");
95 return 0;
96 };
97
98 let Ok(row_count_property) = model.get::<&str, JsFunction>("rowCount") else {
99 eprintln!("Node.js: JavaScript Model<T> implementation is missing rowCount property");
100 return 0;
101 };
102
103 let Some(row_count_property_fn) = row_count_property else {
104 eprintln!("Node.js: JavaScript Model<T> implementation's rowCount property is not a callable function");
105 return 0;
106 };
107
108 let Ok(row_count_result) = row_count_property_fn.call_without_args(Some(&model)) else {
109 eprintln!("Node.js: JavaScript Model<T>'s rowCount implementation call failed");
110 return 0;
111 };
112
113 let Ok(row_count_number) = row_count_result.coerce_to_number() else {
114 eprintln!("Node.js: JavaScript Model<T>'s rowCount function returned a value that cannot be coerced to a number");
115 return 0;
116 };
117
118 let Ok(row_count) = row_count_number.get_uint32() else {
119 eprintln!("Node.js: JavaScript Model<T>'s rowCount function returned a number that cannot be mapped to a uint32");
120 return 0;
121 };
122
123 row_count as usize
124 }
125
126 fn row_data(&self, row: usize) -> Option<Self::Data> {
127 let Ok(model) = self.js_impl.get::<Object>() else {
128 eprintln!("Node.js: JavaScript Model<T>'s rowData threw an exception");
129 return None;
130 };
131
132 let Ok(row_data_property) = model.get::<&str, JsFunction>("rowData") else {
133 eprintln!("Node.js: JavaScript Model<T> implementation is missing rowData property");
134 return None;
135 };
136
137 let Some(row_data_fn) = row_data_property else {
138 eprintln!("Node.js: Model<T> implementation's rowData property is not a function");
139 return None;
140 };
141
142 let Ok(row_data) = row_data_fn
143 .call::<JsNumber>(Some(&model), &[self.env.create_double(row as f64).unwrap()])
144 else {
145 eprintln!("Node.js: JavaScript Model<T>'s rowData function threw an exception");
146 return None;
147 };
148
149 if row_data.get_type().unwrap() == ValueType::Undefined {
150 debug_assert!(row >= self.row_count());
151 None
152 } else {
153 let Ok(js_value) = to_value(&self.env, row_data, &self.row_data_type) else {
154 eprintln!("Node.js: JavaScript Model<T>'s rowData function returned data type that cannot be represented in Rust");
155 return None;
156 };
157 Some(js_value)
158 }
159 }
160
161 fn set_row_data(&self, row: usize, data: Self::Data) {
162 let Ok(model) = self.js_impl.get::<Object>() else {
163 eprintln!("Node.js: JavaScript Model<T>'s setRowData threw an exception");
164 return;
165 };
166
167 let Ok(set_row_data_property) = model.get::<&str, JsFunction>("setRowData") else {
168 eprintln!("Node.js: JavaScript Model<T> implementation is missing setRowData property");
169 return;
170 };
171
172 let Some(set_row_data_fn) = set_row_data_property else {
173 eprintln!("Node.js: Model<T> implementation's setRowData property is not a function");
174 return;
175 };
176
177 let Ok(js_data) = to_js_unknown(&self.env, &data) else {
178 eprintln!("Node.js: Model<T>'s set_row_data called by Rust with data type that can't be represented in JavaScript");
179 return;
180 };
181
182 if let Err(exception) = set_row_data_fn.call::<JsUnknown>(
183 Some(&model),
184 &[self.env.create_double(row as f64).unwrap().into_unknown(), js_data],
185 ) {
186 eprintln!(
187 "Node.js: JavaScript Model<T>'s setRowData function threw an exception: {exception}"
188 );
189 }
190 }
191
192 fn model_tracker(&self) -> &dyn i_slint_core::model::ModelTracker {
193 &**self.shared_model_notify
194 }
195
196 fn as_any(&self) -> &dyn core::any::Any {
197 self
198 }
199}
200
201#[napi]
202pub struct ReadOnlyRustModel(ModelRc<slint_interpreter::Value>);
203
204impl From<ModelRc<slint_interpreter::Value>> for ReadOnlyRustModel {
205 fn from(model: ModelRc<slint_interpreter::Value>) -> Self {
206 Self(model)
207 }
208}
209
210// Implement minimal Model<T> interface
211#[napi]
212impl ReadOnlyRustModel {
213 #[napi]
214 pub fn row_count(&self, env: Env) -> Result<JsNumber> {
215 env.create_uint32(self.0.row_count() as u32)
216 }
217
218 #[napi]
219 pub fn row_data(&self, env: Env, row: u32) -> Result<JsUnknown> {
220 let Some(data) = self.0.row_data(row as usize) else {
221 return env.get_undefined().map(|v| v.into_unknown());
222 };
223 to_js_unknown(&env, &data)
224 }
225
226 #[napi]
227 pub fn set_row_data(&self, _env: Env, _row: u32, _data: JsUnknown) {
228 eprintln!("setRowData called on a model which does not re-implement this method. This happens when trying to modify a read-only model")
229 }
230
231 pub fn into_js(self, env: &Env) -> Result<JsUnknown> {
232 let model = self.0.clone();
233 let iterator_env = env.clone();
234
235 let mut obj = self.into_instance(*env)?.as_object(*env);
236
237 // Implement Iterator protocol by hand until it's stable in napi-rs
238 let iterator_symbol = env
239 .get_global()
240 .and_then(|global| global.get_named_property::<JsFunction>("Symbol"))
241 .and_then(|symbol_function| symbol_function.coerce_to_object())
242 .and_then(|symbol_obj| symbol_obj.get::<&str, JsSymbol>("iterator"))?
243 .expect("fatal: Unable to find Symbol.iterator");
244
245 obj.set_property(
246 iterator_symbol,
247 env.create_function_from_closure("rust model iterator", move |_| {
248 Ok(ModelIterator { model: model.clone(), row: 0, env: iterator_env }
249 .into_instance(iterator_env)?
250 .as_object(iterator_env))
251 })?,
252 )?;
253
254 Ok(obj.into_unknown())
255 }
256}
257
258#[napi]
259pub struct ModelIterator {
260 model: ModelRc<slint_interpreter::Value>,
261 row: usize,
262 env: Env,
263}
264
265#[napi]
266impl ModelIterator {
267 #[napi]
268 pub fn next(&mut self) -> Result<JsUnknown> {
269 let mut result: JsObject = self.env.create_object()?;
270 if self.row >= self.model.row_count() {
271 result.set_named_property(name:"done", value:true)?;
272 } else {
273 let row: usize = self.row;
274 self.row += 1;
275 result.set_named_property(
276 name:"value",
277 self.model.row_data(row).and_then(|value: Value| to_js_unknown(&self.env, &value).ok()),
278 )?
279 }
280 Ok(result.into_unknown())
281 }
282}
283