Skip to content
37 changes: 36 additions & 1 deletion api/cpp/include/private/slint_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,13 @@ struct Property
},
new BindingMapper { t2_binding, self->map_to, self->map_from },
[](void *user_data) { delete reinterpret_cast<BindingMapper *>(user_data); },
nullptr, nullptr);
[](void *user_data, const void *value) {
auto self = reinterpret_cast<BindingMapper *>(user_data);
T2 sub_value = self->map_to(*reinterpret_cast<const T *>(value));
return cbindgen_private::slint_property_intercept_set_binding(
self->t2_binding, &sub_value);
},
nullptr);
return true;
};

Expand All @@ -215,6 +221,35 @@ struct Property
del_fn, intercept_fn, intercept_binding_fn);
}

/// Bind `prop` two-way to a value stored in a model row. `getter` reads
/// the current row value; `setter` writes a new value back into the row.
template<typename Getter, typename Setter>
static void link_two_way_to_model_data(const Property<T> *prop, Getter getter, Setter setter)
{
struct ModelTwoWayBinding
{
Getter getter;
Setter setter;
};
cbindgen_private::slint_property_set_binding(
&prop->inner,
[](void *user_data, void *value) {
auto self = reinterpret_cast<ModelTwoWayBinding *>(user_data);
*reinterpret_cast<T *>(value) = self->getter();
},
new ModelTwoWayBinding { std::move(getter), std::move(setter) },
[](void *user_data) { delete reinterpret_cast<ModelTwoWayBinding *>(user_data); },
[](void *user_data, const void *value) {
auto self = reinterpret_cast<ModelTwoWayBinding *>(user_data);
self->setter(*reinterpret_cast<const T *>(value));
return true;
},
[](void *, void *) -> bool {
// Cannot rebind a property already two-way bound to a model.
std::abort();
});
}

/// Internal (private) constructor used by link_two_way
explicit Property(cbindgen_private::PropertyHandleOpaque inner, T value)
: inner(inner), value(std::move(value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ together and always contain the same value. Also known as bidirectional or bi-di

The right hand side of the `<=>` must be a reference to a property of the same type,
or a field of the same type within a property of struct type.
Inside a `for` loop, it can also be the value of a model, which is kept in sync.
The property type is optional with two-way bindings, it will be inferred if not specified.
The initial value of a linked property will be the value of the right hand side of the binding.
The two linked properties must be compatible in terms of input/output.
Expand Down
6 changes: 1 addition & 5 deletions examples/todo/ui/todo.slint
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,8 @@ export component MainWindow inherits Window {
list-view := ListView {
for todo in root.todo-model: HorizontalLayout {
CheckBox {
toggled => {
todo.checked = self.checked;
}

text: todo.title;
checked: todo.checked;
checked <=> todo.checked;
}
}
}
Expand Down
47 changes: 38 additions & 9 deletions internal/compiler/expression_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1655,17 +1655,32 @@ fn model_inner_type(model: &Expression) -> Type {

/// The right hand side of a two way binding
#[derive(Clone, Debug)]
pub struct TwoWayBinding {
/// The property being linked
pub property: NamedReference,
/// If property is a struct, this is the fields.
/// So if you have `foo <=> element.property.baz.xyz`, then `field_access` is `vec!["baz", "xyz"]`
pub field_access: Vec<SmolStr>,
pub enum TwoWayBinding {
Property {
/// The property being linked
property: NamedReference,
/// If property is a struct, this is the fields.
/// So if you have `foo <=> element.property.baz.xyz`, then `field_access` is `vec!["baz", "xyz"]`
field_access: Vec<SmolStr>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite part of this PR, but I'm inclined to suggest handling this as another enum variant to make the code more readable (instead of the extra ifs after the match)

},
ModelData {
/// The model being linked
repeated_element: ElementWeak,
/// same as `Self::Property::field_access`
field_access: Vec<SmolStr>,
},
}
impl TwoWayBinding {
pub fn ty(&self) -> Type {
let mut ty = self.property.ty();
for x in &self.field_access {
let (mut ty, field_access) = match self {
Self::Property { property, field_access } => (property.ty(), field_access),
Self::ModelData { repeated_element, field_access } => {
let ty =
Expression::RepeaterModelReference { element: repeated_element.clone() }.ty();
(ty, field_access)
}
};
for x in field_access {
ty = match ty {
Type::InferredProperty | Type::InferredCallback => return ty,
Type::Struct(s) => s.fields.get(x).cloned().unwrap_or_default(),
Expand All @@ -1674,11 +1689,25 @@ impl TwoWayBinding {
}
ty
}

pub fn is_constant(&self) -> bool {
match self {
Self::Property { property, .. } => property.is_constant(),
Self::ModelData { .. } => false,
}
}

pub fn property(&self) -> Option<&NamedReference> {
match self {
Self::Property { property, .. } => Some(property),
Self::ModelData { .. } => None,
}
}
}

impl From<NamedReference> for TwoWayBinding {
fn from(nr: NamedReference) -> Self {
Self { property: nr, field_access: Vec::new() }
Self::Property { property: nr, field_access: Vec::new() }
}
}

Expand Down
91 changes: 73 additions & 18 deletions internal/compiler/generator/cpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,68 @@ fn property_set_value_code(
prop.then(|prop| format!("{prop}.set({value_expr})"))
}

/// Walk `field_access` on `root_ty`, prepending each access to `base` to
/// produce a C++ expression (e.g. `base.foo.bar`), and return the leaf type.
fn lower_field_access_chain(
mut base: String,
root_ty: &Type,
field_access: &[SmolStr],
) -> (String, Type) {
let mut ty = root_ty.clone();
for f in field_access {
let Type::Struct(s) = &ty else { panic!("Field of two way binding on a non-struct type") };
base = struct_field_access(base, s, f);
ty = s.fields.get(f).unwrap().clone();
}
(base, ty)
}

/// Emit a `link_two_way_to_model_data` call wiring `p1` to a row of the
/// model described by `info`, optionally through a struct `field_access`.
fn generate_model_two_way_binding(
ctx: &EvaluationContext,
info: &llr::ResolvedModelTwoWayBinding,
p1: &str,
field_access: &[SmolStr],
) -> String {
let body_sc = &ctx.compilation_unit.sub_components[info.body_sub_component];
let data_prop_name = ident(&body_sc.properties[info.data_prop].name);
let index_prop_name = ident(&body_sc.properties[info.index_prop].name);
let repeater_index = usize::from(info.repeater_index);

// Walk the parent chain in a single expression so the intermediate
// `lock().value()` temporaries live until we assign to `body_rc`.
let (body_setup, body) = if info.parent_level == 0 {
(String::new(), "self")
} else {
let chain: String = (0..info.parent_level).map(|_| "->parent.lock().value()").collect();
(format!("auto body_rc = self{chain}; "), "body_rc")
};

let (getter_expr, ty) = lower_field_access_chain(
format!("{body}->{data_prop_name}.get()"),
info.data_prop_ty,
field_access,
);
let (setter_lvalue, _) =
lower_field_access_chain("data".into(), info.data_prop_ty, field_access);
let cpp_ty = ty.cpp_type().unwrap();

format!(
"slint::private_api::Property<{cpp_ty}>::link_two_way_to_model_data(&{p1}, \
[self]() -> {cpp_ty} {{ {body_setup}return {getter_expr}; }}, \
[self](const {cpp_ty} &value) {{ \
{body_setup}\
if (auto parent_opt = {body}->parent.lock()) {{ \
auto data = {body}->{data_prop_name}.get(); \
{setter_lvalue} = value; \
(*parent_opt)->repeater_{repeater_index}.model_set_row_data(\
static_cast<size_t>({body}->{index_prop_name}.get()), data); \
}} \
}});"
)
}

fn handle_property_init(
prop: &llr::MemberReference,
binding_expression: &llr::BindingExpression,
Expand Down Expand Up @@ -2147,30 +2209,23 @@ fn generate_sub_component(
));
}

for (prop1, prop2, fields) in &component.two_way_bindings {
if fields.is_empty() {
let ty = ctx.property_ty(prop1).cpp_type().unwrap();
let p1 = access_member(prop1, &ctx).unwrap();
for twb in &component.two_way_bindings {
let p1 = access_local_member(&twb.prop1, &ctx);
if let Some(info) = twb.resolve_model(&ctx) {
init.push(generate_model_two_way_binding(&ctx, &info, &p1, &twb.field_access));
} else if twb.field_access.is_empty() {
let ty = ctx.relative_property_ty(&twb.prop1, 0).cpp_type().unwrap();
init.push(
access_member(prop2, &ctx).then(|p2| {
access_member(&twb.prop2, &ctx).then(|p2| {
format!("slint::private_api::Property<{ty}>::link_two_way(&{p1}, &{p2})",)
}) + ";",
);
} else {
let mut access = "x".to_string();
let mut ty = ctx.property_ty(prop2);
let cpp_ty = ty.cpp_type().unwrap();
for f in fields {
let Type::Struct(s) = &ty else {
panic!("Field of two way binding on a non-struct type")
};
access = struct_field_access(access, s, f);
ty = s.fields.get(f).unwrap();
}

let p1 = access_member(prop1, &ctx).unwrap();
let prop2_ty = ctx.property_ty(&twb.prop2);
let cpp_ty = prop2_ty.cpp_type().unwrap();
let (access, _) = lower_field_access_chain("x".into(), prop2_ty, &twb.field_access);
init.push(
access_member(prop2, &ctx).then(|p2|
access_member(&twb.prop2, &ctx).then(|p2|
format!("slint::private_api::Property<{cpp_ty}>::link_two_way_with_map(&{p2}, &{p1}, [](const auto &x){{ return {access}; }}, [](auto &x, const auto &v){{ {access} = v; }})")
) + ";",
);
Expand Down
126 changes: 97 additions & 29 deletions internal/compiler/generator/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,84 @@ fn generate_enum(en: &std::rc::Rc<Enumeration>) -> TokenStream {
}
}

/// Walk `field_access` on `root_ty`, producing the Rust access suffix
/// (e.g. `.foo.bar`) and the type of the leaf field.
fn lower_field_access_chain(root_ty: &Type, field_access: &[SmolStr]) -> (TokenStream, Type) {
let mut access = quote!();
let mut ty = root_ty;
for f in field_access {
let Type::Struct(s) = ty else { panic!("Field of two way binding on a non-struct type") };
let a = struct_field_access(s, f);
access.extend(quote!(.#a));
ty = s.fields.get(f).unwrap();
}
(access, ty.clone())
}

/// Emit a `link_two_way_to_model_data` call wiring `p1` to a row of the
/// model described by `info`, optionally through a struct `field_access`.
fn generate_model_two_way_binding(
ctx: &EvaluationContext,
info: &llr::ResolvedModelTwoWayBinding,
p1: &TokenStream,
field_access: &[SmolStr],
) -> TokenStream {
let body_sc = &ctx.compilation_unit.sub_components[info.body_sub_component];
let parent_sc = &ctx.compilation_unit.sub_components[info.parent_sub_component];
let body_id = self::inner_component_id(body_sc);
let data_f =
access_component_field_offset(&body_id, &ident(&body_sc.properties[info.data_prop].name));
let index_f =
access_component_field_offset(&body_id, &ident(&body_sc.properties[info.index_prop].name));
let repeater = access_component_field_offset(
&self::inner_component_id(parent_sc),
&format_ident!("repeater{}", usize::from(info.repeater_index)),
);

let item_tree_weak = if info.parent_level == 0 {
quote!(sp::VRcMapped::downgrade(&self_rc))
} else {
let mut e = quote!(_self.parent.clone());
for _ in 1..info.parent_level {
e = quote!(#e.upgrade().unwrap().parent.clone());
}
e
};

// The leaf field type may differ from the property type (e.g. struct
// field `f32` vs property `LogicalLength`); apply the usual conversions.
let (access_model, getter_value) = if field_access.is_empty() {
(quote!(let data = value.clone();), quote!(#data_f.apply_pin(x.as_pin_ref()).get()))
} else {
let (access, ty) = lower_field_access_chain(info.data_prop_ty, field_access);
let to_struct_value = primitive_value_from_property_value(&ty, quote!(value.clone()));
let to_property_value = set_primitive_property_value(
&ty,
quote!(#data_f.apply_pin(x.as_pin_ref()).get() #access .clone()),
);
(
quote! {
let mut data = #data_f.apply_pin(x.as_pin_ref()).get();
data #access = #to_struct_value;
},
to_property_value,
)
};

quote! { sp::Property::link_two_way_to_model_data(#p1, #item_tree_weak,
|item_tree_weak| item_tree_weak.upgrade().map(|x| #getter_value),
|item_tree_weak, value| {
if let Some(x) = item_tree_weak.upgrade() {
if let Some(parent) = x.parent.upgrade() {
let index = #index_f.apply_pin(x.as_pin_ref()).get();
#access_model
#repeater.apply_pin(parent.as_pin_ref()).model_set_row_data(index as usize, data);
}
}
}
)}
}

fn handle_property_init(
prop: &llr::MemberReference,
binding_expression: &llr::BindingExpression,
Expand Down Expand Up @@ -1167,36 +1245,26 @@ fn generate_sub_component(
let popup_id_names =
component.popup_windows.iter().enumerate().map(|(i, _)| internal_popup_id(i));

for (prop1, prop2, fields) in &component.two_way_bindings {
let p1 = access_member(prop1, &ctx).unwrap();
let p2 = access_member(prop2, &ctx);
let r = p2.then(|p2| {
if fields.is_empty() {
quote!(sp::Property::link_two_way(#p1, #p2))
} else {
let mut access = quote!();
let mut ty = ctx.property_ty(prop2);
for f in fields {
let Type::Struct(s) = &ty else {
panic!("Field of two way binding on a non-struct type")
};
let a = struct_field_access(s, f);
access.extend(quote!(.#a));
ty = s.fields.get(f).unwrap();
for twb in &component.two_way_bindings {
let p1 = access_local_member(&twb.prop1, &ctx);
let r = if let Some(info) = twb.resolve_model(&ctx) {
generate_model_two_way_binding(&ctx, &info, &p1, &twb.field_access)
} else {
let p2 = access_member(&twb.prop2, &ctx);
p2.then(|p2| {
if twb.field_access.is_empty() {
quote!(sp::Property::link_two_way(#p1, #p2))
} else {
let (access, ty) =
lower_field_access_chain(ctx.property_ty(&twb.prop2), &twb.field_access);
let to_property_value =
set_primitive_property_value(&ty, quote!(s #access .clone()));
let to_struct_value =
primitive_value_from_property_value(&ty, quote!((*v).clone()));
quote!(sp::Property::link_two_way_with_map(#p2, #p1, |s| #to_property_value, |s, v| s #access = #to_struct_value))
}
let to_property_value =
set_primitive_property_value(ty, quote!(s #access .clone()));
let to_struct_value = primitive_value_from_property_value(ty, quote!((*v).clone()));
quote!(
sp::Property::link_two_way_with_map(
#p2,
#p1,
|s| #to_property_value,
|s, v| s #access = #to_struct_value,
)
)
}
});
})
};
init.push(quote!(#r;))
}

Expand Down
Loading
Loading