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            // Fill any declared field that wasn't provided with the type
128            // default so downstream consumers always see a complete struct.
129            for (k, field_ty) in s.fields.iter() {
130                if str_value.get_field(k.as_str()).is_none() {
131                    str_value
132                        .set_field(k.to_string(), crate::eval::default_value_for_type(field_ty));
133                }
134            }
135            true
136        }
137        Type::Enumeration(en) => {
138            matches!(value, Value::EnumerationValue(name, _) if name == en.name.as_str())
139        }
140        Type::Keys => matches!(value, Value::Keys(_)),
141        Type::LayoutCache => matches!(value, Value::LayoutCache(_)),
142        Type::ArrayOfU16 => matches!(value, Value::ArrayOfU16(_)),
143        Type::ComponentFactory => matches!(value, Value::ComponentFactory(_)),
144        Type::StyledText => matches!(value, Value::StyledText(_)),
145    }
146}
147
148/// Invoke a public callback or function by name.
149pub fn invoke(
150    instance: &VRc<ItemTreeVTable, Instance>,
151    name: &str,
152    args: &[Value],
153) -> Option<Value> {
154    use i_slint_compiler::langtype::Type;
155    let (public, sub) = resolve(instance)?;
156    let prop = find_public_property(public, name)?;
157    // Only callbacks and functions are callable; propagate a miss for
158    // anything else so the public API surfaces a `NoSuchCallable` error.
159    if !matches!(&prop.ty, Type::Callback(_) | Type::Function(_)) {
160        return None;
161    }
162    let ctx = EvalContext::new(sub);
163    Some(if matches!(&prop.ty, Type::Function(_)) || prop.prop.is_function() {
164        invoke_function(&ctx, &prop.prop, args.to_vec())
165    } else {
166        invoke_callback(&ctx, &prop.prop, args)
167    })
168}
169
170/// Install a host-side handler on a public callback.
171///
172/// Host handlers take the callback args as a flat `&[Value]` and return a
173/// `Value`; they're adapted to the sub-component's
174/// `Callback<[Value], Value>` shape before being installed.
175pub fn set_callback(
176    instance: &VRc<ItemTreeVTable, Instance>,
177    name: &str,
178    handler: Box<dyn Fn(&[Value]) -> Value>,
179) -> Result<(), ()> {
180    let (public, sub) = resolve(instance).ok_or(())?;
181    let prop = find_public_property(public, name).ok_or(())?;
182    match &prop.prop {
183        MemberReference::Relative { parent_level, local_reference } => {
184            let target = walk_to(sub, *parent_level, &local_reference.sub_component_path);
185            match &local_reference.reference {
186                i_slint_compiler::llr::LocalMemberIndex::Callback(idx) => {
187                    let cb = Pin::as_ref(&target.callbacks[*idx]);
188                    cb.set_handler(handler);
189                    if let Some(tracker) = target.callback_trackers[*idx].as_ref() {
190                        Pin::as_ref(tracker).mark_dirty();
191                    }
192                    Ok(())
193                }
194                i_slint_compiler::llr::LocalMemberIndex::Native {
195                    item_index, prop_name, ..
196                } => {
197                    Pin::as_ref(&target.items[*item_index]).set_callback_handler(prop_name, handler)
198                }
199                _ => Err(()),
200            }
201        }
202        MemberReference::Global { global_index, member } => {
203            // An alias like `callback foo <=> Glo.bar` surfaces as a
204            // public property whose `prop` is a global reference. Route
205            // directly to the matching `GlobalInstance::callbacks` slot.
206            let global_inst = instance.globals.get(*global_index).ok_or(())?;
207            let i_slint_compiler::llr::LocalMemberIndex::Callback(idx) = member else {
208                return Err(());
209            };
210            let cb = Pin::as_ref(&global_inst.callbacks[*idx]);
211            cb.set_handler(handler);
212            if let Some(tracker) = global_inst.callback_trackers[*idx].as_ref() {
213                Pin::as_ref(tracker).mark_dirty();
214            }
215            Ok(())
216        }
217    }
218}
219
220fn resolve(
221    instance: &VRc<ItemTreeVTable, Instance>,
222) -> Option<(&PublicComponent, Pin<Rc<SubComponentInstance>>)> {
223    let cu = &instance.root_sub_component.compilation_unit;
224    let public_index = instance.public_component_index?;
225    let public = cu.public_components.get(public_index)?;
226    Some((public, instance.root_sub_component.clone()))
227}
228
229/// Name-based lookup of a public property on an exported global singleton.
230/// Returns the looked-up property plus the runtime `GlobalInstance`.
231fn resolve_global<'a>(
232    instance: &'a VRc<ItemTreeVTable, Instance>,
233    global_name: &str,
234    prop_name: &str,
235) -> Option<(&'a PublicProperty, Rc<crate::globals::GlobalInstance>)> {
236    let cu = &instance.root_sub_component.compilation_unit;
237    let (_global, global_instance) = instance.globals.find_by_name(cu, global_name)?;
238    let global_instance = global_instance.clone();
239    let needle = i_slint_compiler::parser::normalize_identifier(prop_name);
240    let global = &cu.globals[global_instance.global_idx];
241    let prop = global.public_properties.get(needle.as_str())?;
242    Some((prop, global_instance))
243}
244
245/// Resolve a public global property to its underlying `(GlobalInstance,
246/// LocalMemberIndex)`. A `data <=> G1.data` alias surfaces as
247/// `MemberReference::Global` pointing at a *different* global from the one
248/// whose `public_properties` map carries the entry, so the member index
249/// must be resolved against the target global, not the source.
250fn resolve_global_property(
251    instance: &VRc<ItemTreeVTable, Instance>,
252    source_inst: Rc<crate::globals::GlobalInstance>,
253    prop: &PublicProperty,
254) -> Option<(Rc<crate::globals::GlobalInstance>, i_slint_compiler::llr::LocalMemberIndex)> {
255    match &prop.prop {
256        MemberReference::Global { global_index, member } => {
257            let target = instance.globals.get(*global_index)?.clone();
258            Some((target, member.clone()))
259        }
260        MemberReference::Relative { local_reference, .. } => {
261            Some((source_inst, local_reference.reference.clone()))
262        }
263    }
264}
265
266/// Read a property on a public global singleton.
267pub fn get_global(
268    instance: &VRc<ItemTreeVTable, Instance>,
269    global_name: &str,
270    prop_name: &str,
271) -> Option<Value> {
272    let (prop, source_inst) = resolve_global(instance, global_name, prop_name)?;
273    let (target_inst, member) = resolve_global_property(instance, source_inst, prop)?;
274    match member {
275        i_slint_compiler::llr::LocalMemberIndex::Property(idx) => {
276            Some(Pin::as_ref(&target_inst.properties[idx]).get())
277        }
278        _ => None,
279    }
280}
281
282/// Write a property on a public global singleton.
283pub fn set_global(
284    instance: &VRc<ItemTreeVTable, Instance>,
285    global_name: &str,
286    prop_name: &str,
287    mut value: Value,
288) -> Result<(), SetPropertyError> {
289    let (prop, source_inst) =
290        resolve_global(instance, global_name, prop_name).ok_or(SetPropertyError::NoSuchProperty)?;
291    if prop.read_only() {
292        return Err(SetPropertyError::AccessDenied);
293    }
294    if !check_and_coerce(&mut value, &prop.ty) {
295        return Err(SetPropertyError::WrongType);
296    }
297    let (target_inst, member) = resolve_global_property(instance, source_inst, prop)
298        .ok_or(SetPropertyError::NoSuchProperty)?;
299    match member {
300        i_slint_compiler::llr::LocalMemberIndex::Property(_) => {
301            crate::eval::store_global(&target_inst, &member, value);
302            Ok(())
303        }
304        _ => Err(SetPropertyError::NoSuchProperty),
305    }
306}
307
308/// Install a handler on a public callback declared on an exported global
309/// singleton.
310pub fn set_global_callback(
311    instance: &VRc<ItemTreeVTable, Instance>,
312    global_name: &str,
313    callback_name: &str,
314    handler: Box<dyn Fn(&[Value]) -> Value>,
315) -> Result<(), ()> {
316    let (prop, source_inst) = resolve_global(instance, global_name, callback_name).ok_or(())?;
317    let (target_inst, member) = resolve_global_property(instance, source_inst, prop).ok_or(())?;
318    match member {
319        i_slint_compiler::llr::LocalMemberIndex::Callback(idx) => {
320            if let Some(native) = &target_inst.native {
321                let g = &target_inst.compilation_unit.globals[target_inst.global_idx];
322                return native.as_ref().set_callback_handler(&g.callbacks[idx].name, handler);
323            }
324            let cb = Pin::as_ref(&target_inst.callbacks[idx]);
325            cb.set_handler(handler);
326            if let Some(tracker) = target_inst.callback_trackers[idx].as_ref() {
327                Pin::as_ref(tracker).mark_dirty();
328            }
329            Ok(())
330        }
331        _ => Err(()),
332    }
333}
334
335/// Invoke a public callback or function on an exported global singleton.
336pub fn invoke_global(
337    instance: &VRc<ItemTreeVTable, Instance>,
338    global_name: &str,
339    name: &str,
340    args: &[Value],
341) -> Option<Value> {
342    use i_slint_compiler::llr::LocalMemberIndex;
343    let (prop, source_inst) = resolve_global(instance, global_name, name)?;
344    let (target_inst, member) = resolve_global_property(instance, source_inst, prop)?;
345    match member {
346        LocalMemberIndex::Callback(idx) => {
347            let cu = &target_inst.compilation_unit;
348            let cb_decl = &cu.globals[target_inst.global_idx].callbacks[idx];
349            if let Some(native) = &target_inst.native {
350                let res =
351                    native.as_ref().invoke_callback(&cb_decl.name, args).unwrap_or(Value::Void);
352                return Some(crate::eval::ensure_typed_default(res, &cb_decl.ret_ty));
353            }
354            let cb = Pin::as_ref(&target_inst.callbacks[idx]);
355            Some(crate::eval::ensure_typed_default(cb.call(args), &cb_decl.ret_ty))
356        }
357        LocalMemberIndex::Function(fn_idx) => {
358            let cu = &instance.root_sub_component.compilation_unit;
359            let global = &cu.globals[target_inst.global_idx];
360            let function = &global.functions[fn_idx];
361            let expr = function.code.borrow().clone();
362            let mut ctx = crate::eval::EvalContext::for_global(
363                std::rc::Rc::downgrade(&instance.globals),
364                cu.clone(),
365            );
366            ctx.function_arg_types = function.args.clone();
367            ctx.function_arguments = args.to_vec();
368            Some(crate::eval::eval_expression(&mut ctx, &expr))
369        }
370        _ => None,
371    }
372}
373
374fn walk_to(
375    start: Pin<Rc<SubComponentInstance>>,
376    parent_level: usize,
377    path: &[i_slint_compiler::llr::SubComponentInstanceIdx],
378) -> Pin<Rc<SubComponentInstance>> {
379    crate::eval::walk_sub_path(crate::eval::walk_parent(&start, parent_level), path)
380}