Skip to main content

slint_interpreter/
public_api.rs

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
4//! Name-based bridge between the public API (`get_property`, `invoke`,
5//! `set_callback`, …) and the LLR's index-based `MemberReference`s.
6//!
7//! Each `PublicComponent::public_properties` entry carries a
8//! `MemberReference`; dispatch forwards to the evaluator helpers in
9//! [`crate::eval`].
10
11use crate::Value;
12use crate::api::SetPropertyError;
13use crate::eval::{EvalContext, invoke_callback, invoke_function, load_property, store_property};
14use crate::instance::{Instance, SubComponentInstance};
15use i_slint_compiler::langtype::Type;
16use i_slint_compiler::llr::{MemberReference, PublicComponent, PublicProperty};
17use i_slint_core::item_tree::ItemTreeVTable;
18use i_slint_core::model::Model;
19use std::pin::Pin;
20use std::rc::Rc;
21use vtable::VRc;
22
23/// Look up a public property by name on the given public component.
24/// Normalizes `name` through `normalize_identifier` so
25/// snake_case and kebab-case both work.
26pub fn find_public_property<'a>(
27    public: &'a PublicComponent,
28    name: &str,
29) -> Option<&'a PublicProperty> {
30    let normalized = i_slint_compiler::parser::normalize_identifier(name);
31    public.public_properties.get(normalized.as_str())
32}
33
34/// Read the value of a public property on `instance`.
35pub fn get(instance: &VRc<ItemTreeVTable, Instance>, name: &str) -> Option<Value> {
36    let (public, sub) = resolve(instance)?;
37    let prop = find_public_property(public, name)?;
38    if !prop.ty.is_property_type() {
39        return None;
40    }
41    let ctx = EvalContext::new(sub);
42    Some(load_property(&ctx, &prop.prop))
43}
44
45/// Write a public property on `instance`.
46pub fn set(
47    instance: &VRc<ItemTreeVTable, Instance>,
48    name: &str,
49    mut value: Value,
50) -> Result<(), SetPropertyError> {
51    let (public, sub) = resolve(instance).ok_or(SetPropertyError::NoSuchProperty)?;
52    let prop = find_public_property(public, name).ok_or(SetPropertyError::NoSuchProperty)?;
53    if !prop.ty.is_property_type() {
54        return Err(SetPropertyError::NoSuchProperty);
55    }
56    if prop.read_only() {
57        return Err(SetPropertyError::AccessDenied);
58    }
59    if !check_and_coerce(&mut value, &prop.ty) {
60        return Err(SetPropertyError::WrongType);
61    }
62    let ctx = EvalContext::new(sub);
63    store_property(&ctx, &prop.prop, value);
64    Ok(())
65}
66
67/// Return true if `value` matches `ty` — and coerce it in place when useful
68/// (struct values get missing fields filled with the type's defaults).
69pub(crate) fn check_and_coerce(value: &mut Value, ty: &Type) -> bool {
70    match ty {
71        Type::Void => true,
72        Type::Invalid
73        | Type::InferredProperty
74        | Type::InferredCallback
75        | Type::Callback(_)
76        | Type::Function(_)
77        | Type::ElementReference
78        | Type::Closure => false,
79        Type::Float32 | Type::Int32 => matches!(value, Value::Number(_)),
80        Type::String => matches!(value, Value::String(_)),
81        Type::Color | Type::Brush => matches!(value, Value::Brush(_)),
82        Type::UnitProduct(_)
83        | Type::Duration
84        | Type::PhysicalLength
85        | Type::LogicalLength
86        | Type::Rem
87        | Type::Angle
88        | Type::Percent => matches!(value, Value::Number(_)),
89        Type::Image => matches!(value, Value::Image(_)),
90        Type::Bool => matches!(value, Value::Bool(_)),
91        Type::Model => matches!(value, Value::Model(_) | Value::Bool(_) | Value::Number(_)),
92        Type::PathData => matches!(value, Value::PathData(_)),
93        Type::DataTransfer => matches!(value, Value::DataTransfer(_)),
94        Type::Easing => matches!(value, Value::EasingCurve(_)),
95        Type::MouseCursor => matches!(value, Value::MouseCursorInner(_)),
96        Type::Array(inner) => match value {
97            Value::Model(m) => {
98                let mut ok = true;
99                for i in 0..m.row_count() {
100                    if let Some(mut v) = m.row_data(i)
101                        && !check_and_coerce(&mut v, inner)
102                    {
103                        ok = false;
104                        break;
105                    }
106                }
107                ok
108            }
109            _ => false,
110        },
111        Type::Struct(s) => {
112            let Value::Struct(str_value) = value else { return false };
113            // Every provided key must be declared on the struct and have the
114            // right type.
115            let keys: Vec<String> = str_value.iter().map(|(k, _)| k.to_string()).collect();
116            for k in keys {
117                let Some(field_ty) = s.fields.get(k.as_str()) else {
118                    return false;
119                };
120                let Some(v) = str_value.get_field(&k).cloned() else { continue };
121                let mut v = v;
122                if !check_and_coerce(&mut v, field_ty) {
123                    return false;
124                }
125                str_value.set_field(k, v);
126            }
127            crate::eval::fill_missing_struct_fields(str_value, s);
128            true
129        }
130        Type::Enumeration(en) => {
131            matches!(value, Value::EnumerationValue(name, _) if name == en.name.as_str())
132        }
133        Type::Keys => matches!(value, Value::Keys(_)),
134        Type::LayoutCache => matches!(value, Value::LayoutCache(_)),
135        Type::ArrayOfU16 => matches!(value, Value::ArrayOfU16(_)),
136        Type::ComponentFactory => matches!(value, Value::ComponentFactory(_)),
137        Type::StyledText => matches!(value, Value::StyledText(_)),
138    }
139}
140
141/// Invoke a public callback or function by name.
142pub fn invoke(
143    instance: &VRc<ItemTreeVTable, Instance>,
144    name: &str,
145    args: &[Value],
146) -> Option<Value> {
147    use i_slint_compiler::langtype::Type;
148    let (public, sub) = resolve(instance)?;
149    let prop = find_public_property(public, name)?;
150    // Only callbacks and functions are callable; propagate a miss for
151    // anything else so the public API surfaces a `NoSuchCallable` error.
152    if !matches!(&prop.ty, Type::Callback(_) | Type::Function(_)) {
153        return None;
154    }
155    let ctx = EvalContext::new(sub);
156    Some(if matches!(&prop.ty, Type::Function(_)) || prop.prop.is_function() {
157        invoke_function(&ctx, &prop.prop, args.to_vec())
158    } else {
159        invoke_callback(&ctx, &prop.prop, args)
160    })
161}
162
163/// Install a host-side handler on a public callback.
164///
165/// Host handlers take the callback args as a flat `&[Value]` and return a
166/// `Value`; they're adapted to the sub-component's
167/// `Callback<[Value], Value>` shape before being installed.
168pub fn set_callback(
169    instance: &VRc<ItemTreeVTable, Instance>,
170    name: &str,
171    handler: Box<dyn Fn(&[Value]) -> Value>,
172) -> Result<(), ()> {
173    let (public, sub) = resolve(instance).ok_or(())?;
174    let prop = find_public_property(public, name).ok_or(())?;
175    match &prop.prop {
176        MemberReference::Relative { parent_level, local_reference } => {
177            let target = walk_to(sub, *parent_level, local_reference).ok_or(())?;
178            match &local_reference.reference {
179                i_slint_compiler::llr::LocalMemberIndex::Callback(idx) => {
180                    let cb = Pin::as_ref(&target.callbacks[*idx]);
181                    cb.set_handler(handler);
182                    if let Some(tracker) = target.callback_trackers[*idx].as_ref() {
183                        Pin::as_ref(tracker).mark_dirty();
184                    }
185                    Ok(())
186                }
187                i_slint_compiler::llr::LocalMemberIndex::Native {
188                    item_index, prop_name, ..
189                } => {
190                    Pin::as_ref(&target.items[*item_index]).set_callback_handler(prop_name, handler)
191                }
192                _ => Err(()),
193            }
194        }
195        MemberReference::Global { global_index, member } => {
196            // An alias like `callback foo <=> Glo.bar` surfaces as a
197            // public property whose `prop` is a global reference. Route
198            // directly to the matching `GlobalInstance::callbacks` slot.
199            let global_inst = instance.globals.get(*global_index).ok_or(())?;
200            let i_slint_compiler::llr::LocalMemberIndex::Callback(idx) = member else {
201                return Err(());
202            };
203            let cb = Pin::as_ref(&global_inst.callbacks[*idx]);
204            cb.set_handler(handler);
205            if let Some(tracker) = global_inst.callback_trackers[*idx].as_ref() {
206                Pin::as_ref(tracker).mark_dirty();
207            }
208            Ok(())
209        }
210    }
211}
212
213fn resolve(
214    instance: &VRc<ItemTreeVTable, Instance>,
215) -> Option<(&PublicComponent, Pin<Rc<SubComponentInstance>>)> {
216    let cu = &instance.root_sub_component.compilation_unit;
217    let public_index = instance.public_component_index?;
218    let public = cu.public_components.get(public_index)?;
219    Some((public, instance.root_sub_component.clone()))
220}
221
222/// Name-based lookup of a public property on an exported global singleton.
223/// Returns the looked-up property plus the runtime `GlobalInstance`.
224fn resolve_global<'a>(
225    instance: &'a VRc<ItemTreeVTable, Instance>,
226    global_name: &str,
227    prop_name: &str,
228) -> Option<(&'a PublicProperty, Rc<crate::globals::GlobalInstance>)> {
229    let cu = &instance.root_sub_component.compilation_unit;
230    let (_global, global_instance) = instance.globals.find_by_name(cu, global_name)?;
231    let global_instance = global_instance.clone();
232    let needle = i_slint_compiler::parser::normalize_identifier(prop_name);
233    let global = &cu.globals[global_instance.global_idx];
234    let prop = global.public_properties.get(needle.as_str())?;
235    Some((prop, global_instance))
236}
237
238/// Resolve a public global property to its underlying `(GlobalInstance,
239/// LocalMemberIndex)`. A `data <=> G1.data` alias surfaces as
240/// `MemberReference::Global` pointing at a *different* global from the one
241/// whose `public_properties` map carries the entry, so the member index
242/// must be resolved against the target global, not the source.
243fn resolve_global_property(
244    instance: &VRc<ItemTreeVTable, Instance>,
245    source_inst: Rc<crate::globals::GlobalInstance>,
246    prop: &PublicProperty,
247) -> Option<(Rc<crate::globals::GlobalInstance>, i_slint_compiler::llr::LocalMemberIndex)> {
248    match &prop.prop {
249        MemberReference::Global { global_index, member } => {
250            let target = instance.globals.get(*global_index)?.clone();
251            Some((target, member.clone()))
252        }
253        MemberReference::Relative { local_reference, .. } => {
254            Some((source_inst, local_reference.reference.clone()))
255        }
256    }
257}
258
259/// Read a property on a public global singleton.
260pub fn get_global(
261    instance: &VRc<ItemTreeVTable, Instance>,
262    global_name: &str,
263    prop_name: &str,
264) -> Option<Value> {
265    let (prop, source_inst) = resolve_global(instance, global_name, prop_name)?;
266    let (target_inst, member) = resolve_global_property(instance, source_inst, prop)?;
267    match member {
268        i_slint_compiler::llr::LocalMemberIndex::Property(idx) => {
269            Some(Pin::as_ref(&target_inst.properties[idx]).get())
270        }
271        _ => None,
272    }
273}
274
275/// Write a property on a public global singleton.
276pub fn set_global(
277    instance: &VRc<ItemTreeVTable, Instance>,
278    global_name: &str,
279    prop_name: &str,
280    mut value: Value,
281) -> Result<(), SetPropertyError> {
282    let (prop, source_inst) =
283        resolve_global(instance, global_name, prop_name).ok_or(SetPropertyError::NoSuchProperty)?;
284    if prop.read_only() {
285        return Err(SetPropertyError::AccessDenied);
286    }
287    if !check_and_coerce(&mut value, &prop.ty) {
288        return Err(SetPropertyError::WrongType);
289    }
290    let (target_inst, member) = resolve_global_property(instance, source_inst, prop)
291        .ok_or(SetPropertyError::NoSuchProperty)?;
292    match member {
293        i_slint_compiler::llr::LocalMemberIndex::Property(_) => {
294            crate::eval::store_global(&target_inst, &member, value);
295            Ok(())
296        }
297        _ => Err(SetPropertyError::NoSuchProperty),
298    }
299}
300
301/// Install a handler on a public callback declared on an exported global
302/// singleton.
303pub fn set_global_callback(
304    instance: &VRc<ItemTreeVTable, Instance>,
305    global_name: &str,
306    callback_name: &str,
307    handler: Box<dyn Fn(&[Value]) -> Value>,
308) -> Result<(), ()> {
309    let (prop, source_inst) = resolve_global(instance, global_name, callback_name).ok_or(())?;
310    let (target_inst, member) = resolve_global_property(instance, source_inst, prop).ok_or(())?;
311    match member {
312        i_slint_compiler::llr::LocalMemberIndex::Callback(idx) => {
313            if let Some(native) = &target_inst.native {
314                let g = &target_inst.compilation_unit.globals[target_inst.global_idx];
315                return native.as_ref().set_callback_handler(&g.callbacks[idx].name, handler);
316            }
317            let cb = Pin::as_ref(&target_inst.callbacks[idx]);
318            cb.set_handler(handler);
319            if let Some(tracker) = target_inst.callback_trackers[idx].as_ref() {
320                Pin::as_ref(tracker).mark_dirty();
321            }
322            Ok(())
323        }
324        _ => Err(()),
325    }
326}
327
328/// Invoke a public callback or function on an exported global singleton.
329pub fn invoke_global(
330    instance: &VRc<ItemTreeVTable, Instance>,
331    global_name: &str,
332    name: &str,
333    args: &[Value],
334) -> Option<Value> {
335    use i_slint_compiler::llr::LocalMemberIndex;
336    let (prop, source_inst) = resolve_global(instance, global_name, name)?;
337    let (target_inst, member) = resolve_global_property(instance, source_inst, prop)?;
338    match member {
339        LocalMemberIndex::Callback(idx) => {
340            let cu = &target_inst.compilation_unit;
341            let cb_decl = &cu.globals[target_inst.global_idx].callbacks[idx];
342            if let Some(native) = &target_inst.native {
343                let res =
344                    native.as_ref().invoke_callback(&cb_decl.name, args).unwrap_or(Value::Void);
345                return Some(crate::eval::ensure_typed_default(res, &cb_decl.ret_ty));
346            }
347            let cb = Pin::as_ref(&target_inst.callbacks[idx]);
348            Some(crate::eval::ensure_typed_default(cb.call(args), &cb_decl.ret_ty))
349        }
350        LocalMemberIndex::Function(fn_idx) => {
351            let cu = &instance.root_sub_component.compilation_unit;
352            let global = &cu.globals[target_inst.global_idx];
353            let function = &global.functions[fn_idx];
354            let expr = function.code.borrow().clone();
355            let mut ctx = crate::eval::EvalContext::for_global(
356                std::rc::Rc::downgrade(&instance.globals),
357                cu.clone(),
358            );
359            ctx.function_arg_types = function.args.clone();
360            ctx.function_arguments = args.to_vec();
361            Some(crate::eval::eval_expression(&mut ctx, &expr))
362        }
363        _ => None,
364    }
365}
366
367fn walk_to(
368    start: Pin<Rc<SubComponentInstance>>,
369    parent_level: usize,
370    local_reference: &i_slint_compiler::llr::LocalMemberReference,
371) -> Option<Pin<Rc<SubComponentInstance>>> {
372    let base = crate::eval::try_walk_parent(&start, parent_level)?;
373    Some(crate::eval::walk_sub_path(base, &local_reference.sub_component_path))
374}