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 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, JsExternal, 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 .and_then(|shared_model_notify: JsExternal| {
40 env.get_value_external::<SharedModelNotify>(&shared_model_notify).cloned()
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 model: Object = self.js_impl.get().unwrap();
94
95 let Ok(row_count_property) = model.get::<&str, JsFunction>("rowCount") else {
96 eprintln!("Node.js: JavaScript Model<T> implementation is missing rowCount property");
97 return 0;
98 };
99
100 let Some(row_count_property_fn) = row_count_property else {
101 eprintln!("Node.js: JavaScript Model<T> implementation's rowCount property is not a callable function");
102 return 0;
103 };
104
105 let Ok(row_count_result) = row_count_property_fn.call_without_args(Some(&model)) else {
106 eprintln!("Node.js: JavaScript Model<T>'s rowCount implementation call failed");
107 return 0;
108 };
109
110 let Ok(row_count_number) = row_count_result.coerce_to_number() else {
111 eprintln!("Node.js: JavaScript Model<T>'s rowCount function returned a value that cannot be coerced to a number");
112 return 0;
113 };
114
115 let Ok(row_count) = row_count_number.get_uint32() else {
116 eprintln!("Node.js: JavaScript Model<T>'s rowCount function returned a number that cannot be mapped to a uint32");
117 return 0;
118 };
119
120 row_count as usize
121 }
122
123 fn row_data(&self, row: usize) -> Option<Self::Data> {
124 let model: Object = self.js_impl.get().unwrap();
125 let Ok(row_data_property) = model.get::<&str, JsFunction>("rowData") else {
126 eprintln!("Node.js: JavaScript Model<T> implementation is missing rowData property");
127 return None;
128 };
129
130 let Some(row_data_fn) = row_data_property else {
131 eprintln!("Node.js: Model<T> implementation's rowData property is not a function");
132 return None;
133 };
134
135 let Ok(row_data) = row_data_fn
136 .call::<JsNumber>(Some(&model), &[self.env.create_double(row as f64).unwrap()])
137 else {
138 eprintln!("Node.js: JavaScript Model<T>'s rowData function threw an exception");
139 return None;
140 };
141
142 if row_data.get_type().unwrap() == ValueType::Undefined {
143 debug_assert!(row >= self.row_count());
144 None
145 } else {
146 let Ok(js_value) = to_value(&self.env, row_data, &self.row_data_type) else {
147 eprintln!("Node.js: JavaScript Model<T>'s rowData function returned data type that cannot be represented in Rust");
148 return None;
149 };
150 Some(js_value)
151 }
152 }
153
154 fn set_row_data(&self, row: usize, data: Self::Data) {
155 let model: Object = self.js_impl.get().unwrap();
156
157 let Ok(set_row_data_property) = model.get::<&str, JsFunction>("setRowData") else {
158 eprintln!("Node.js: JavaScript Model<T> implementation is missing setRowData property");
159 return;
160 };
161
162 let Some(set_row_data_fn) = set_row_data_property else {
163 eprintln!("Node.js: Model<T> implementation's setRowData property is not a function");
164 return;
165 };
166
167 let Ok(js_data) = to_js_unknown(&self.env, &data) else {
168 eprintln!("Node.js: Model<T>'s set_row_data called by Rust with data type that can't be represented in JavaScript");
169 return;
170 };
171
172 if let Err(exception) = set_row_data_fn.call::<JsUnknown>(
173 Some(&model),
174 &[self.env.create_double(row as f64).unwrap().into_unknown(), js_data],
175 ) {
176 eprintln!(
177 "Node.js: JavaScript Model<T>'s setRowData function threw an exception: {}",
178 exception
179 );
180 }
181 }
182
183 fn model_tracker(&self) -> &dyn i_slint_core::model::ModelTracker {
184 &**self.shared_model_notify
185 }
186
187 fn as_any(&self) -> &dyn core::any::Any {
188 self
189 }
190}
191
192#[napi]
193pub struct ReadOnlyRustModel(ModelRc<slint_interpreter::Value>);
194
195impl From<ModelRc<slint_interpreter::Value>> for ReadOnlyRustModel {
196 fn from(model: ModelRc<slint_interpreter::Value>) -> Self {
197 Self(model)
198 }
199}
200
201// Implement minimal Model<T> interface
202#[napi]
203impl ReadOnlyRustModel {
204 #[napi]
205 pub fn row_count(&self, env: Env) -> Result<JsNumber> {
206 env.create_uint32(self.0.row_count() as u32)
207 }
208
209 #[napi]
210 pub fn row_data(&self, env: Env, row: u32) -> Result<JsUnknown> {
211 let Some(data) = self.0.row_data(row as usize) else {
212 return env.get_undefined().map(|v| v.into_unknown());
213 };
214 to_js_unknown(&env, &data)
215 }
216
217 #[napi]
218 pub fn set_row_data(&self, _env: Env, _row: u32, _data: JsUnknown) {
219 eprintln!("setRowData called on a model which does not re-implement this method. This happens when trying to modify a read-only model")
220 }
221
222 pub fn into_js(self, env: &Env) -> Result<JsUnknown> {
223 let model = self.0.clone();
224 let iterator_env = env.clone();
225
226 let mut obj = self.into_instance(*env)?.as_object(*env);
227
228 // Implement Iterator protocol by hand until it's stable in napi-rs
229 let iterator_symbol = env
230 .get_global()
231 .and_then(|global| global.get_named_property::<JsFunction>("Symbol"))
232 .and_then(|symbol_function| symbol_function.coerce_to_object())
233 .and_then(|symbol_obj| symbol_obj.get::<&str, JsSymbol>("iterator"))?
234 .expect("fatal: Unable to find Symbol.iterator");
235
236 obj.set_property(
237 iterator_symbol,
238 env.create_function_from_closure("rust model iterator", move |_| {
239 Ok(ModelIterator { model: model.clone(), row: 0, env: iterator_env }
240 .into_instance(iterator_env)?
241 .as_object(iterator_env))
242 })?,
243 )?;
244
245 Ok(obj.into_unknown())
246 }
247}
248
249#[napi]
250pub struct ModelIterator {
251 model: ModelRc<slint_interpreter::Value>,
252 row: usize,
253 env: Env,
254}
255
256#[napi]
257impl ModelIterator {
258 #[napi]
259 pub fn next(&mut self) -> Result<JsUnknown> {
260 let mut result: JsObject = self.env.create_object()?;
261 if self.row >= self.model.row_count() {
262 result.set_named_property(name:"done", value:true)?;
263 } else {
264 let row: usize = self.row;
265 self.row += 1;
266 result.set_named_property(
267 name:"value",
268 self.model.row_data(row).and_then(|value: Value| to_js_unknown(&self.env, &value).ok()),
269 )?
270 }
271 return Ok(result.into_unknown());
272 }
273}
274