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 std::rc::Rc; |
5 | |
6 | use i_slint_compiler::langtype::Type; |
7 | use i_slint_core::model::{Model, ModelNotify, ModelRc}; |
8 | use napi::{bindgen_prelude::*, JsSymbol}; |
9 | use napi::{Env, JsExternal, JsFunction, JsNumber, JsObject, JsUnknown, Result, ValueType}; |
10 | |
11 | use crate::{to_js_unknown, to_value, RefCountedReference}; |
12 | |
13 | #[napi ] |
14 | #[derive (Clone, Default)] |
15 | pub struct SharedModelNotify(Rc<ModelNotify>); |
16 | |
17 | impl core::ops::Deref for SharedModelNotify { |
18 | type Target = Rc<ModelNotify>; |
19 | |
20 | fn deref(&self) -> &Self::Target { |
21 | &self.0 |
22 | } |
23 | } |
24 | |
25 | pub(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 | |
51 | pub(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 | |
57 | struct JsModel { |
58 | shared_model_notify: SharedModelNotify, |
59 | env: Env, |
60 | js_impl: RefCountedReference, |
61 | row_data_type: Type, |
62 | } |
63 | |
64 | #[napi ] |
65 | pub fn js_model_notify_new() -> Result<External<SharedModelNotify>> { |
66 | Ok(External::new(Default::default())) |
67 | } |
68 | |
69 | #[napi ] |
70 | pub fn js_model_notify_row_data_changed(notify: External<SharedModelNotify>, row: u32) { |
71 | notify.row_changed(row as usize); |
72 | } |
73 | |
74 | #[napi ] |
75 | pub 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 ] |
80 | pub 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 ] |
85 | pub fn js_model_notify_reset(notify: External<SharedModelNotify>) { |
86 | notify.reset(); |
87 | } |
88 | |
89 | impl 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 ] |
193 | pub struct ReadOnlyRustModel(ModelRc<slint_interpreter::Value>); |
194 | |
195 | impl 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 ] |
203 | impl 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 ] |
250 | pub struct ModelIterator { |
251 | model: ModelRc<slint_interpreter::Value>, |
252 | row: usize, |
253 | env: Env, |
254 | } |
255 | |
256 | #[napi ] |
257 | impl 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 | |