diff --git a/api/node/rust/types/model.rs b/api/node/rust/types/model.rs index aff8889f658..eea3d930e77 100644 --- a/api/node/rust/types/model.rs +++ b/api/node/rust/types/model.rs @@ -203,6 +203,108 @@ impl Model for JsModel { } } + fn push_row(&self, data: Self::Data) { + let Ok(model_unknown) = self.js_impl.get_unknown() else { + eprintln!("Node.js: JavaScript Model's pushRow threw an exception"); + return; + }; + + let Ok(model) = model_unknown.coerce_to_object() else { + eprintln!("Node.js: JavaScript Model is not an object"); + return; + }; + + let push_row_fn: Function, Unknown> = match model.get_named_property("pushRow") + { + Ok(f) => f, + Err(e) => { + eprintln!("{}", e.to_string()); + eprintln!( + "Node.js: JavaScript Model implementation is missing pushRow property" + ); + return; + } + }; + + let Ok(js_data) = to_js_unknown(&self.env, &data) else { + eprintln!( + "Node.js: Model's push_row called by Rust with data type that can't be represented in JavaScript" + ); + return; + }; + + if let Err(exception) = push_row_fn.apply(model, js_data) { + eprintln!( + "Node.js: JavaScript Model's pushRow function threw an exception: {exception}" + ); + } + } + + fn remove_row(&self, row: usize) { + let Ok(model_unknown) = self.js_impl.get_unknown() else { + eprintln!("Node.js: JavaScript Model's removeRow threw an exception"); + return; + }; + + let Ok(model) = model_unknown.coerce_to_object() else { + eprintln!("Node.js: JavaScript Model is not an object"); + return; + }; + + let remove_row_fn: Function = match model.get_named_property("removeRow") { + Ok(f) => f, + Err(e) => { + eprintln!("{}", e.to_string()); + eprintln!( + "Node.js: JavaScript Model implementation is missing removeRow property" + ); + return; + } + }; + + if let Err(exception) = remove_row_fn.apply(model, row as f64) { + eprintln!( + "Node.js: JavaScript Model's removeRow function threw an exception: {exception}" + ); + } + } + + fn insert_row(&self, row: usize, data: Self::Data) { + let Ok(model_unknown) = self.js_impl.get_unknown() else { + eprintln!("Node.js: JavaScript Model's insertRow threw an exception"); + return; + }; + + let Ok(model) = model_unknown.coerce_to_object() else { + eprintln!("Node.js: JavaScript Model is not an object"); + return; + }; + + let insert_row_fn: Function)>, Unknown> = + match model.get_named_property("insertRow") { + Ok(f) => f, + Err(_) => { + eprintln!( + "Node.js: JavaScript Model implementation is missing insertRow property" + ); + return; + } + }; + + let Ok(js_data) = to_js_unknown(&self.env, &data) else { + eprintln!( + "Node.js: Model's insert_row called by Rust with data type that can't be represented in JavaScript" + ); + return; + }; + + if let Err(exception) = insert_row_fn.apply(model, FnArgs::from((row as f64, js_data))) { + eprintln!( + "Node.js: JavaScript Model's insertRow function threw an exception: {exception}" + ); + } + } + fn model_tracker(&self) -> &dyn i_slint_core::model::ModelTracker { &**self.shared_model_notify } diff --git a/api/node/typescript/models.ts b/api/node/typescript/models.ts index f13a53977f5..81e707bfc4a 100644 --- a/api/node/typescript/models.ts +++ b/api/node/typescript/models.ts @@ -135,6 +135,38 @@ export abstract class Model implements Iterable { ); } + /** + * Implementations of this function must add a line to the model with the provided data. + * @param _data new data item to store in a new row. + */ + pushRow(_data: T): void { + console.log( + "pushRow called on a model which does not re-implement this method. This happens when trying to modify a read-only model", + ); + } + + /** + * Implementations of this function must remove the row at the specified index. + * @param _index index of the row to remove. + */ + removeRow(_index: number): void { + console.log( + "removeRow called on a model which does not re-implement this method. This happens when trying to modify a read-only model", + ); + } + + /** + * Implementations of this function must add a row at the specified index, pushing all next + * rows to the right. + * @param _index index of the row to insert. + * @param _data new data item to store in a new row. + */ + insertRow(_index: number, _data: T): void { + console.log( + "insertRow called on a model which does not re-implement this method. This happens when trying to modify a read-only model", + ); + } + [Symbol.iterator](): Iterator { return new ModelIterator(this); } @@ -226,6 +258,34 @@ export class ArrayModel extends Model { this.notifyRowDataChanged(row); } + /** + * Add a new row to the array backing the model and notifies run-time about the added row. + * @param data new data item to store in a new row. + */ + pushRow(data: T) { + this.#array.push(data); + this.notifyRowAdded(this.#array.length - 1, 1); + } + + /** + * Remove a row from the array backing the model and notifies run-time about the removed row. + * @param _index index of the row to remove. + */ + removeRow(_index: number) { + this.#array.splice(_index, 1); + this.notifyRowRemoved(_index, 1); + } + + /** + * Insert a new row into the array backing the model at the specified index and notifies run-time about the added row. + * @param _index index at which to insert the new row. + * @param _data data item to store in the new row. + */ + insertRow(_index: number, _data: T) { + this.#array.splice(_index, 0, _data); + this.notifyRowAdded(_index, 1); + } + /** * Pushes new values to the array that's backing the model and notifies * the run-time about the added rows. diff --git a/api/python/slint/models.rs b/api/python/slint/models.rs index a4522c46ed3..67264625569 100644 --- a/api/python/slint/models.rs +++ b/api/python/slint/models.rs @@ -210,6 +210,80 @@ impl i_slint_core::model::Model for PyModelShared { }); } + fn push_row(&self, data: Self::Data) { + Python::try_attach(|py| { + let obj = self.self_ref.borrow(); + let Some(obj) = obj.as_ref() else { + eprintln!("Python: Model implementation is lacking self object (in push_row)"); + return; + }; + + let Some(type_collection) = self.type_collection.borrow().as_ref().cloned() else { + eprintln!("Python: Model implementation is lacking type collection (in push_row)"); + return; + }; + + let element_type = self.element_type.borrow().clone(); + if let Err(err) = + obj.call_method1(py, "push_row", (type_collection.to_py_value(data, element_type),)) + { + crate::handle_unraisable( + py, + "Python: Model implementation of push_row() threw an exception".into(), + err, + ); + }; + }); + } + + fn remove_row(&self, row: usize) { + Python::try_attach(|py| { + let obj = self.self_ref.borrow(); + let Some(obj) = obj.as_ref() else { + eprintln!("Python: Model implementation is lacking self object (in remove_row)"); + return; + }; + + if let Err(err) = obj.call_method1(py, "remove_row", (row,)) { + crate::handle_unraisable( + py, + "Python: Model implementation of remove_row() threw an exception".into(), + err, + ); + }; + }); + } + + fn insert_row(&self, row: usize, data: Self::Data) { + Python::try_attach(|py| { + let obj = self.self_ref.borrow(); + let Some(obj) = obj.as_ref() else { + eprintln!("Python: Model implementation is lacking self object (in insert_row)"); + return; + }; + + let Some(type_collection) = self.type_collection.borrow().as_ref().cloned() else { + eprintln!( + "Python: Model implementation is lacking type collection (in insert_row)" + ); + return; + }; + + let element_type = self.element_type.borrow().clone(); + if let Err(err) = obj.call_method1( + py, + "insert_row", + (row, type_collection.to_py_value(data, element_type)), + ) { + crate::handle_unraisable( + py, + "Python: Model implementation of insert_row() threw an exception".into(), + err, + ); + }; + }); + } + fn model_tracker(&self) -> &dyn i_slint_core::model::ModelTracker { &self.notify } diff --git a/api/python/slint/slint/models.py b/api/python/slint/slint/models.py index 61b55c896bb..0e7ff709c29 100644 --- a/api/python/slint/slint/models.py +++ b/api/python/slint/slint/models.py @@ -45,6 +45,21 @@ def row_data(self, row: int) -> typing.Optional[T]: Re-implement this method in a sub-class to provide the data.""" return cast(T, super().row_data(row)) + def push_row(self, value: T) -> None: + """Add a new row to the model with the provided value. + Re-implement this method in a sub-class to handle the change.""" + super().push_row(value) + + def remove_row(self, row: int) -> None: + """Remove the row at the given index. + Re-implement this method in a sub-class to handle the change.""" + super().remove_row(row) + + def insert_row(self, row: int, value: T) -> None: + """Insert a new row at the given index. + Re-implement this method in a sub-class to handle the change.""" + super().insert_row(row, value) + def notify_row_changed(self, row: int) -> None: """Call this method from a sub-class to notify the views that a row has changed.""" super().notify_row_changed(row) @@ -91,6 +106,18 @@ def set_row_data(self, row: int, value: T) -> None: self.list[row] = value super().notify_row_changed(row) + def push_row(self, value: T) -> None: + self.list.append(value) + super().notify_row_added(len(self.list) - 1, 1) + + def remove_row(self, row: int) -> None: + del self.list[row] + super().notify_row_removed(row, 1) + + def insert_row(self, row: int, value: T) -> None: + self.list.insert(row, value) + self.notify_row_added(row, 1) + def __delitem__(self, key: int | slice) -> None: if isinstance(key, slice): start, stop, step = key.indices(len(self.list)) diff --git a/api/python/slint/slint/slint.pyi b/api/python/slint/slint/slint.pyi index 7dc3f22e3bd..afe316d4b8e 100644 --- a/api/python/slint/slint/slint.pyi +++ b/api/python/slint/slint/slint.pyi @@ -169,6 +169,9 @@ class PyModelBase: def row_count(self) -> int: ... def row_data(self, row: int) -> typing.Optional[Any]: ... def set_row_data(self, row: int, value: Any) -> None: ... + def push_row(self, value: Any) -> None: ... + def remove_row(self, row: int) -> None: ... + def insert_row(self, row: int, value: Any) -> None: ... def notify_row_changed(self, row: int) -> None: ... def notify_row_removed(self, row: int, count: int) -> None: ... def notify_row_added(self, row: int, count: int) -> None: ... diff --git a/api/python/slint/tests/test_models.py b/api/python/slint/tests/test_models.py index 7b3c2e7a60f..b0a029f7bc8 100644 --- a/api/python/slint/tests/test_models.py +++ b/api/python/slint/tests/test_models.py @@ -157,3 +157,40 @@ def test_model_writeback() -> None: assert list(instance.get_property("model")) == [100, 42] instance.invoke("write-to-model", 0, 25) assert list(instance.get_property("model")) == [25, 42] + + +def test_model_modifications() -> None: + compiler = native.Compiler() + compdef = compiler.build_from_source( + """ + export component App { + in-out property<[int]> ints; + public function push-one(value: int) { ints.push(value) } + public function remove-one(index: int) { ints.remove(index) } + public function insert-one(index: int, value: int) { ints.insert(index, value) } + } + """, + Path(""), + ).component("App") + + assert compdef is not None + + instance = compdef.create() + assert instance is not None + + model = models.ListModel([1, 2, 3]) + instance.set_property("ints", model) + + assert instance.get_property("ints").row_count() == 3 + + instance.invoke("push-one", 10) + assert instance.get_property("ints").row_count() == 4 + assert instance.get_property("ints").row_data(3) == 10 + + instance.invoke("remove-one", 1) + assert instance.get_property("ints").row_count() == 3 + assert instance.get_property("ints").row_data(2) == 10 + + instance.invoke("insert-one", 1, 20) + assert instance.get_property("ints").row_count() == 4 + assert instance.get_property("ints").row_data(1) == 20 diff --git a/internal/compiler/builtin_macros.rs b/internal/compiler/builtin_macros.rs index 7d08bc0a41c..9a2c783611b 100644 --- a/internal/compiler/builtin_macros.rs +++ b/internal/compiler/builtin_macros.rs @@ -88,6 +88,9 @@ pub fn lower_macro( BuiltinMacroFunction::Rgb => rgb_macro(n, sub_expr.collect(), diag), BuiltinMacroFunction::Hsv => hsv_macro(n, sub_expr.collect(), diag), BuiltinMacroFunction::Oklch => oklch_macro(n, sub_expr.collect(), diag), + BuiltinMacroFunction::ArrayPush => array_push_macro(n, sub_expr.collect(), diag), + BuiltinMacroFunction::ArrayRemove => array_remove_macro(n, sub_expr.collect(), diag), + BuiltinMacroFunction::ArrayInsert => array_insert_macro(n, sub_expr.collect(), diag), } } @@ -379,6 +382,107 @@ fn debug_macro( } } +fn array_push_macro( + node: &dyn Spanned, + mut args: Vec<(Expression, Option)>, + diag: &mut BuildDiagnostics, +) -> Expression { + if args.len() != 2 { + diag.push_error( + format!( + "This method needs 1 argument but {} were provided", + if args.len() == 1 { 0 } else { args.len() - 1 } // Avoid counting the model argument. + ), + node, + ); + return Expression::Invalid; + } + + let element_type = match args[0].0.ty() { + Type::Array(t) => (*t).clone(), + _ => { + diag.push_error(format!("push() was called on a non-array: {:?}", args[0].0), node); + return Expression::Invalid; + } + }; + + let (model_expr, _) = args.remove(0); + let (value_expr, value_node) = args.remove(0); + let value = value_expr.maybe_convert_to(element_type, &value_node, diag); + Expression::FunctionCall { + function: Callable::Builtin(BuiltinFunction::ArrayPush), + arguments: vec![model_expr, value], + source_location: Some(node.to_source_location()), + } +} + +fn array_remove_macro( + node: &dyn Spanned, + mut args: Vec<(Expression, Option)>, + diag: &mut BuildDiagnostics, +) -> Expression { + if args.len() != 2 { + diag.push_error( + format!( + "This method needs 1 argument but {} were provided", + if args.len() == 1 { 0 } else { args.len() - 1 } // Avoid counting the model argument. + ), + node, + ); + return Expression::Invalid; + } + + if !matches!(args[0].0.ty(), Type::Array(_)) { + diag.push_error(format!("remove() was called on a non-array: {:?}", args[0].0), node); + return Expression::Invalid; + } + + let (model_expr, _) = args.remove(0); + let (index_expr, index_node) = args.remove(0); + let index = index_expr.maybe_convert_to(Type::Int32, &index_node, diag); + Expression::FunctionCall { + function: Callable::Builtin(BuiltinFunction::ArrayRemove), + arguments: vec![model_expr, index], + source_location: Some(node.to_source_location()), + } +} + +fn array_insert_macro( + node: &dyn Spanned, + mut args: Vec<(Expression, Option)>, + diag: &mut BuildDiagnostics, +) -> Expression { + if args.len() != 3 { + diag.push_error( + format!( + "This method needs 2 argument but {} were provided", + if args.len() == 1 { 0 } else { args.len() - 1 } // Avoid counting the model argument. + ), + node, + ); + return Expression::Invalid; + } + + let element_type = match args[0].0.ty() { + Type::Array(t) => (*t).clone(), + _ => { + diag.push_error(format!("push() was called on a non-array: {:?}", args[0].0), node); + return Expression::Invalid; + } + }; + + let (model_expr, _) = args.remove(0); + let (index_expr, index_node) = args.remove(0); + let (value_expr, value_node) = args.remove(0); + let index = index_expr.maybe_convert_to(Type::Int32, &index_node, diag); + let value = value_expr.maybe_convert_to(element_type, &value_node, diag); + Expression::FunctionCall { + function: Callable::Builtin(BuiltinFunction::ArrayInsert), + arguments: vec![model_expr, index, value], + source_location: Some(node.to_source_location()), + } +} + fn to_debug_string( expr: Expression, node: &dyn Spanned, diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index 9c14a69e6a7..d172c69e39e 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -84,6 +84,9 @@ pub enum BuiltinFunction { ColorWithAlpha, ImageSize, ArrayLength, + ArrayPush, + ArrayRemove, + ArrayInsert, Rgb, Hsv, Oklch, @@ -149,6 +152,9 @@ pub enum BuiltinMacroFunction { Oklch, /// transform `debug(a, b, c)` into debug `a + " " + b + " " + c` Debug, + ArrayPush, + ArrayRemove, + ArrayInsert, } macro_rules! declare_builtin_function_types { @@ -261,6 +267,9 @@ declare_builtin_function_types!( name: crate::langtype::BuiltinPrivateStruct::Size.into(), })), ArrayLength: (Type::Model) -> Type::Int32, + ArrayPush: (Type::Model) -> Type::Void, + ArrayRemove: (Type::Model, Type::Int32) -> Type::Void, + ArrayInsert: (Type::Model, Type::Int32) -> Type::Void, Rgb: (Type::Int32, Type::Int32, Type::Int32, Type::Float32) -> Type::Color, Hsv: (Type::Float32, Type::Float32, Type::Float32, Type::Float32) -> Type::Color, Oklch: (Type::Float32, Type::Float32, Type::Float32, Type::Float32) -> Type::Color, @@ -384,6 +393,9 @@ impl BuiltinFunction { #[cfg(target_arch = "wasm32")] BuiltinFunction::ImageSize => false, BuiltinFunction::ArrayLength => true, + BuiltinFunction::ArrayPush + | BuiltinFunction::ArrayRemove + | BuiltinFunction::ArrayInsert => false, BuiltinFunction::Rgb => true, BuiltinFunction::Hsv => true, BuiltinFunction::Oklch => true, @@ -468,6 +480,9 @@ impl BuiltinFunction { | BuiltinFunction::ColorWithAlpha => true, BuiltinFunction::ImageSize => true, BuiltinFunction::ArrayLength => true, + BuiltinFunction::ArrayPush + | BuiltinFunction::ArrayRemove + | BuiltinFunction::ArrayInsert => false, BuiltinFunction::Rgb => true, BuiltinFunction::Hsv => true, BuiltinFunction::Oklch => true, diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index b98c729ce56..edd4d8e9441 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -4415,6 +4415,15 @@ fn compile_builtin_function_call( BuiltinFunction::ArrayLength => { format!("slint::private_api::model_length({})", a.next().unwrap()) } + BuiltinFunction::ArrayPush => { + todo!() + } + BuiltinFunction::ArrayRemove => { + todo!() + } + BuiltinFunction::ArrayInsert => { + todo!() + } BuiltinFunction::Rgb => { format!("slint::Color::from_argb_uint8(std::clamp(static_cast({a}) * 255., 0., 255.), std::clamp(static_cast({r}), 0, 255), std::clamp(static_cast({g}), 0, 255), std::clamp(static_cast({b}), 0, 255))", r = a.next().unwrap(), diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index a3d44473eae..df26ac7fbf1 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -3753,7 +3753,41 @@ fn compile_builtin_function_call( x.row_count() as i32 }}) } + BuiltinFunction::ArrayPush => { + let model = a.next().unwrap(); + let value = a.next().unwrap(); + quote!({ + let model = &#model; + let value = #value; + model.push_row(value); + }) + } + BuiltinFunction::ArrayRemove => { + let model = a.next().unwrap(); + let index = a.next().unwrap(); + quote!({ + let model = &#model; + let index = #index as usize; + + if index < model.row_count() { + model.remove_row(index); + } + }) + } + BuiltinFunction::ArrayInsert => { + let model = a.next().unwrap(); + let index = a.next().unwrap(); + let value = a.next().unwrap(); + quote!({ + let model = &#model; + let index = #index as usize; + let value = #value; + if index <= model.row_count() { + model.insert_row(index, value); + } + }) + } BuiltinFunction::Rgb => { let (r, g, b, a) = (a.next().unwrap(), a.next().unwrap(), a.next().unwrap(), a.next().unwrap()); diff --git a/internal/compiler/llr/optim_passes/inline_expressions.rs b/internal/compiler/llr/optim_passes/inline_expressions.rs index bd71a0a779a..0d3aebfbff0 100644 --- a/internal/compiler/llr/optim_passes/inline_expressions.rs +++ b/internal/compiler/llr/optim_passes/inline_expressions.rs @@ -136,6 +136,9 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize { BuiltinFunction::ColorWithAlpha => 50, BuiltinFunction::ImageSize => 50, BuiltinFunction::ArrayLength => 50, + BuiltinFunction::ArrayPush + | BuiltinFunction::ArrayRemove + | BuiltinFunction::ArrayInsert => ALLOC_COST, BuiltinFunction::Rgb => 50, BuiltinFunction::Hsv => 50, BuiltinFunction::Oklch => 50, diff --git a/internal/compiler/lookup.rs b/internal/compiler/lookup.rs index 74910abff2c..a1fe3223c46 100644 --- a/internal/compiler/lookup.rs +++ b/internal/compiler/lookup.rs @@ -1120,16 +1120,20 @@ impl LookupObject for ArrayExpression<'_> { ctx: &LookupCtx, f: &mut impl FnMut(&SmolStr, LookupResult) -> Option, ) -> Option { - let member_function = |f: BuiltinFunction| { + let function_call = |f: BuiltinFunction| { LookupResult::from(Expression::FunctionCall { function: Callable::Builtin(f), source_location: ctx.current_token.as_ref().map(|t| t.to_source_location()), arguments: vec![self.0.clone()], }) }; - None.or_else(|| { - f(&SmolStr::new_static("length"), member_function(BuiltinFunction::ArrayLength)) - }) + let mut member_macro = member_macro_generator(self.0.clone(), ctx.current_token.clone()); + + let mut f = |s, res| f(&SmolStr::new_static(s), res); + None.or_else(|| f("length", function_call(BuiltinFunction::ArrayLength))) + .or_else(|| f("push", member_macro(BuiltinMacroFunction::ArrayPush))) + .or_else(|| f("remove", member_macro(BuiltinMacroFunction::ArrayRemove))) + .or_else(|| f("insert", member_macro(BuiltinMacroFunction::ArrayInsert))) } } diff --git a/internal/core/model.rs b/internal/core/model.rs index b85c846d0fe..a6c3d601648 100644 --- a/internal/core/model.rs +++ b/internal/core/model.rs @@ -144,6 +144,54 @@ pub trait Model { ); } + /// Add a new row to the model. + /// + /// If the model cannot support data changes, then it is ok to do nothing. + /// The default implementation will print a warning to stderr. + /// + /// If the model can update the data, it should also call [`ModelNotify::row_changed`] on its + /// internal [`ModelNotify`]. + fn push_row(&self, _data: Self::Data) { + #[cfg(feature = "std")] + crate::debug_log!( + "Model::push_row called on a model of type {} which does not re-implement this method. \ + This happens when trying to modify a read-only model", + core::any::type_name::() + ); + } + + /// Remove a row from the model at the specified index. + /// + /// If the model cannot support data changes, then it is ok to do nothing. + /// The default implementation will print a warning to stderr. + /// + /// If the model can update the data, it should also call [`ModelNotify::row_changed`] on its + /// internal [`ModelNotify`]. + fn remove_row(&self, _row: usize) { + #[cfg(feature = "std")] + crate::debug_log!( + "Model::remove_row called on a model of type {} which does not re-implement this method. \ + This happens when trying to modify a read-only model", + core::any::type_name::() + ); + } + + /// Insert a new row at the specified index and move the next rows by 1 step to the right. + /// + /// If the model cannot support data changes, then it is ok to do nothing. + /// The default implementation will print a warning to stderr. + /// + /// If the model can update the data, it should also call [`ModelNotify::row_changed`] on its + /// internal [`ModelNotify`]. + fn insert_row(&self, _row: usize, _data: Self::Data) { + #[cfg(feature = "std")] + crate::debug_log!( + "Model::insert_row called on a model of type {} which does not re-implement this method. \ + This happens when trying to modify a read-only model", + core::any::type_name::() + ); + } + /// The implementation should return a reference to its [`ModelNotify`] field. /// /// You can return `&()` if you your `Model` is constant and does not have a ModelNotify field. @@ -482,6 +530,18 @@ impl Model for VecModel { } } + fn push_row(&self, data: Self::Data) { + self.push(data); + } + + fn remove_row(&self, row: usize) { + self.remove(row); + } + + fn insert_row(&self, row: usize, data: Self::Data) { + self.insert(row, data); + } + fn model_tracker(&self) -> &dyn ModelTracker { &self.notify } @@ -535,6 +595,25 @@ impl Model for SharedVectorModel { self.notify.row_changed(row); } + fn push_row(&self, data: Self::Data) { + self.array.borrow_mut().push(data); + self.notify.row_added(self.array.borrow().len() - 1, 1); + } + + fn remove_row(&self, row: usize) { + let mut array = self.array.borrow_mut(); + array.make_mut_slice()[row..].rotate_left(1); + array.pop(); + self.notify.row_removed(row, 1); + } + + fn insert_row(&self, row: usize, data: Self::Data) { + let mut array = self.array.borrow_mut(); + array.push(data); + array.make_mut_slice()[row..].rotate_right(1); + self.notify.row_added(row, 1); + } + fn model_tracker(&self) -> &dyn ModelTracker { &self.notify } @@ -820,6 +899,24 @@ impl Model for ModelRc { } } + fn push_row(&self, data: Self::Data) { + if let Some(model) = self.0.as_ref() { + model.push_row(data); + } + } + + fn remove_row(&self, row: usize) { + if let Some(model) = self.0.as_ref() { + model.remove_row(row); + } + } + + fn insert_row(&self, row: usize, data: Self::Data) { + if let Some(model) = self.0.as_ref() { + model.insert_row(row, data); + } + } + fn model_tracker(&self) -> &dyn ModelTracker { self.0.as_ref().map_or(&(), |model| model.model_tracker()) } diff --git a/internal/interpreter/eval.rs b/internal/interpreter/eval.rs index f8e600ac56e..737488cb2e3 100644 --- a/internal/interpreter/eval.rs +++ b/internal/interpreter/eval.rs @@ -1392,6 +1392,65 @@ fn call_builtin_function( } } } + BuiltinFunction::ArrayPush => { + if arguments.len() != 2 { + panic!("internal error: incorrect argument count to ArrayPush") + } + + let model = match eval_expression(&arguments[0], local_context) { + Value::Model(m) => m, + _ => panic!("First argument not an array: {:?}", arguments[0]), + }; + let value = eval_expression(&arguments[1], local_context); + + model.push_row(value); + + Value::Void + } + BuiltinFunction::ArrayRemove => { + if arguments.len() != 2 { + panic!("internal error: incorrect argument count to ArrayRemove") + } + + let model = match eval_expression(&arguments[0], local_context) { + Value::Model(m) => m, + _ => panic!("First argument not an array: {:?}", arguments[0]), + }; + let index = match eval_expression(&arguments[1], local_context) { + Value::Number(i) => i as usize, + _ => panic!("Second argument not an integer: {:?}", arguments[0]), + }; + + if index < model.row_count() { + model.remove_row(index); + } + + Value::Void + } + + BuiltinFunction::ArrayInsert => { + if arguments.len() != 3 { + panic!("internal error: incorrect argument count to ArrayInsert") + } + + let model = match eval_expression(&arguments[0], local_context) { + Value::Model(m) => m, + _ => panic!("First argument not an array: {:?}", arguments[0]), + }; + let index = match eval_expression(&arguments[1], local_context) { + Value::Number(i) => i as usize, + _ => panic!("Second argument not an integer: {:?}", arguments[0]), + }; + + if index > model.row_count() { + return Value::Void; + } + + let value = eval_expression(&arguments[2], local_context); + model.insert_row(index, value); + + Value::Void + } BuiltinFunction::Rgb => { let r: i32 = eval_expression(&arguments[0], local_context).try_into().unwrap(); let g: i32 = eval_expression(&arguments[1], local_context).try_into().unwrap(); diff --git a/tests/cases/models/array.slint b/tests/cases/models/array.slint index 55ce716f740..7a9983e50ee 100644 --- a/tests/cases/models/array.slint +++ b/tests/cases/models/array.slint @@ -20,6 +20,10 @@ export component TestCase { out property decimal_check: ints[-0.5] == 1 && ints[2.9] == 3 && ints[-1.3] == 0; out property hello_world: [{t: "hello"}, {t: "world"}][1].t; + public function push_one(val: int) { ints.push(val) } + public function remove_one(index: int) { ints.remove(index) } + public function insert_one(index: int, value: int) { ints.insert(index, value) } + for xxx in (third_int == 0) ? ints : [] : Rectangle {} } @@ -51,6 +55,19 @@ assert_eq(instance.get_ninth_int(), 9); assert_eq(instance.get_hello_world(), "world"); + +model->set_vector(std::vector{1, 2, 3}); +instance.invoke_push_one(10); +assert_eq(model->row_count(), 4); +assert_eq(model->row_data(3).unwrap(), 10); + +instance.invoke_remove_one(2); +assert_eq(model->row_count(), 3); +assert_eq(model->row_data(2).unwrap(), 10); + +instance.invoke_insert_one(1, 20); +assert_eq(model->row_count(), 4); +assert_eq(model->row_data(1).unwrap(), 20); ``` @@ -81,6 +98,19 @@ assert_eq!(instance.get_ninth_int(), 9); assert_eq!(instance.get_hello_world(), slint::SharedString::from("world")); + +model.set_vec([1, 2, 3]); +instance.invoke_push_one(10); +assert_eq!(model.row_count(), 4); +assert_eq!(model.row_data(3).unwrap(), 10); + +instance.invoke_remove_one(2); +assert_eq!(model.row_count(), 3); +assert_eq!(model.row_data(2).unwrap(), 10); + +instance.invoke_insert_one(1, 20); +assert_eq!(model.row_count(), 4); +assert_eq!(model.row_data(1).unwrap(), 20); ``` ```js @@ -108,5 +138,20 @@ model.push(9); assert.equal(instance.ninth_int, 9); assert.equal(instance.hello_world, "world"); + +model = new slintlib.ArrayModel([1, 2, 3]); +instance.ints = model; + +instance.push_one(10); +assert.equal(model.rowCount(), 4); +assert.equal(model.rowData(3), 10); + +instance.remove_one(2); +assert.equal(model.rowCount(), 3); +assert.equal(model.rowData(2), 10); + +instance.insert_one(1, 20); +assert.equal(model.rowCount(), 4); +assert.equal(model.rowData(1), 20); ``` */ diff --git a/tools/lsp/preview/eval.rs b/tools/lsp/preview/eval.rs index 02864f0ec83..038b83c8186 100644 --- a/tools/lsp/preview/eval.rs +++ b/tools/lsp/preview/eval.rs @@ -621,6 +621,65 @@ fn handle_builtin_function( _ => Value::Void, } } + BuiltinFunction::ArrayPush => { + if arguments.len() != 2 { + panic!("internal error: incorrect argument count to ArrayPush") + } + + let model = match eval_expression(&arguments[0], local_context, None) { + Value::Model(m) => m, + _ => panic!("First argument not an array: {:?}", arguments[0]), + }; + let value = eval_expression(&arguments[1], local_context, None); + + model.push_row(value); + + Value::Void + } + BuiltinFunction::ArrayRemove => { + if arguments.len() != 2 { + panic!("internal error: incorrect argument count to ArrayRemove") + } + + let model = match eval_expression(&arguments[0], local_context, None) { + Value::Model(m) => m, + _ => panic!("First argument not an array: {:?}", arguments[0]), + }; + + let index = match eval_expression(&arguments[1], local_context, None) { + Value::Number(i) => i as usize, + _ => panic!("Second argument not an integer: {:?}", arguments[0]), + }; + + if index < model.row_count() { + model.remove_row(index); + } + + Value::Void + } + BuiltinFunction::ArrayInsert => { + if arguments.len() != 3 { + panic!("internal error: incorrect argument count to ArrayInsert") + } + + let model = match eval_expression(&arguments[0], local_context, None) { + Value::Model(m) => m, + _ => panic!("First argument not an array: {:?}", arguments[0]), + }; + let index = match eval_expression(&arguments[1], local_context, None) { + Value::Number(i) => i as usize, + _ => panic!("Second argument not an integer: {:?}", arguments[0]), + }; + + if index > model.row_count() { + return Value::Void; + } + + let value = eval_expression(&arguments[2], local_context, None); + model.insert_row(index, value); + + Value::Void + } BuiltinFunction::Rgb => { let r: i32 = eval_expression(&arguments[0], local_context, None).try_into().unwrap_or_default();