Skip to main content

slint_interpreter/
debug_hook.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//! Runtime side of the `debug_hooks` compiler feature: evaluating an
5//! `Expression::DebugHook` calls the callback installed with
6//! `ComponentInstance::set_debug_hook_callback`, which may override the value.
7
8use crate::Value;
9use crate::eval::EvalContext;
10
11use smol_str::SmolStr;
12
13pub type DebugHookCallback = Box<dyn Fn(&str) -> Option<Value>>;
14
15#[cfg(feature = "internal")]
16pub(crate) fn set_debug_hook_callback(
17    instance: &vtable::VRc<i_slint_core::item_tree::ItemTreeVTable, crate::instance::Instance>,
18    func: Option<DebugHookCallback>,
19) {
20    *instance.globals.debug_hook_callback.borrow_mut() = func;
21}
22
23/// `Some` when the installed callback overrides the value, `None` to evaluate the binding.
24pub(crate) fn trigger_debug_hook(ctx: &EvalContext, id: &SmolStr) -> Option<Value> {
25    let globals = ctx.globals.upgrade()?;
26    let callback = globals.debug_hook_callback.borrow();
27    callback.as_ref().and_then(|callback| callback(id))
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33    use crate::{Compiler, ComponentInstance};
34    use i_slint_compiler::object_tree::Element;
35    use i_slint_core::{Property, graphics::ApproxEq};
36    use std::{cell::RefCell, collections::HashMap, path::PathBuf, pin::Pin, rc::Rc};
37
38    fn compile_with_debug_hooks(code: &str) -> ComponentInstance {
39        i_slint_backend_testing::init_no_event_loop();
40
41        let mut compiler = Compiler::default();
42        compiler.compiler_configuration(i_slint_core::InternalToken).debug_hooks =
43            Some(std::hash::RandomState::new());
44        let compile_result =
45            spin_on::spin_on(compiler.build_from_source(code.to_string(), test_path()));
46        assert!(!compile_result.has_errors(), "{:?}", compile_result.diagnostics);
47        compile_result.components().next().unwrap().create().unwrap()
48    }
49
50    fn install_debug_hook_store(instance: &ComponentInstance) -> Store {
51        let store: Store = Default::default();
52        {
53            let store = Rc::clone(&store);
54            instance.set_debug_hook_callback(Some(Box::new(move |id: &str| -> Option<Value> {
55                let mut m = (*store).borrow_mut();
56                let p = m.entry(SmolStr::from(id)).or_insert_with(|| Box::pin(Property::new(None)));
57                p.as_ref().get()
58            })));
59        }
60        store
61    }
62
63    fn set_override(store: &Store, element_hash: u64, name: &str, value: Option<Value>) {
64        let id = i_slint_compiler::passes::property_id(element_hash, &SmolStr::from(name));
65        let mut store = (*store).borrow_mut();
66        let override_property = store.entry(id).or_insert_with(|| Box::pin(Property::new(None)));
67        (&**override_property).set(value);
68    }
69
70    // Make sure to not actually write this file, it's just a synthetic path
71    fn test_path() -> PathBuf {
72        PathBuf::from("/tmp/test.slint")
73    }
74
75    fn find_element(
76        instance: &ComponentInstance,
77        code: &str,
78        search_term: &str,
79    ) -> (Rc<RefCell<Element>>, u64) {
80        let offset = code.find(search_term).unwrap() as u32;
81        let (element, debug_index) = instance
82            .element_node_at_source_code_position(&test_path(), offset)
83            .first()
84            .cloned()
85            .expect("element resolved");
86        let element_hash = element.borrow().debug[debug_index].element_hash;
87        assert_ne!(element_hash, 0, "debug_hooks should populate element_hash");
88        (element, element_hash)
89    }
90
91    // Editor-style override store + callback (must be installed before the first evaluation so
92    // the hooked bindings register a dependency on the override properties while still `None`).
93    type Store = Rc<RefCell<HashMap<SmolStr, Pin<Box<Property<Option<Value>>>>>>>;
94
95    // Validates the "live drag" mechanism the visual editor relies on: with `debug_hooks`
96    // enabled, a `set_debug_hook_callback` that reads a per-id `Property<Option<Value>>` lets the
97    // editor reactively override a property's value (and revert it) without touching the source.
98    // Setting the override property re-evaluates the hooked binding via Slint's dependency tracker.
99    #[test]
100    fn debug_hook_live_override() {
101        let code = r#"
102export component Win inherits Window {
103    width: 300px;
104    height: 300px;
105    rect := Rectangle {
106        x: 10px;
107        y: 20px;
108        width: 30px;
109        height: 40px;
110    }
111}"#;
112
113        let instance = compile_with_debug_hooks(code);
114
115        let (element, element_hash) = find_element(&instance, code, "Rectangle");
116
117        let store = install_debug_hook_store(&instance);
118
119        let base = instance.element_positions(&element).first().expect("geometry").rect;
120
121        set_override(&store, element_hash, "x", Some(Value::Number(100.0)));
122        set_override(&store, element_hash, "width", Some(Value::Number(70.0)));
123        let after = instance.element_positions(&element).first().expect("geometry").rect;
124        assert!(
125            after.origin.x.approx_eq(&(base.origin.x + 90.0)),
126            "x override should shift the element by 90px (base {}, after {})",
127            base.origin.x,
128            after.origin.x
129        );
130        assert!(
131            after.size.width.approx_eq(&(base.size.width + 40.0)),
132            "width override should grow the element by 40px (base {}, after {})",
133            base.size.width,
134            after.size.width
135        );
136
137        set_override(&store, element_hash, "x", None);
138        set_override(&store, element_hash, "width", None);
139        let reverted = instance.element_positions(&element).first().expect("geometry").rect;
140        assert!(reverted.origin.x.approx_eq(&base.origin.x), "x should revert");
141        assert!(reverted.size.width.approx_eq(&base.size.width), "width should revert");
142    }
143
144    // Component-instance elements are hooked too: their unbound properties get synthetic hooks
145    // that must be upgraded with the definition's default bindings during inlining (keeping the
146    // *instance* element's hook id). Verifies that the defaults are preserved (regression: they
147    // used to be clobbered, rendering repeated items transparent) and that instance properties
148    // are live-overridable through the hook callback.
149    #[test]
150    fn debug_hook_component_instance_override() {
151        let code = r#"
152component Sub inherits Rectangle {
153    in property <color> tint: blue;
154    background: tint;
155}
156export component Win inherits Window {
157    width: 300px;
158    height: 300px;
159    sub := Sub { x: 10px; y: 20px; width: 50px; height: 50px; }
160    for _idx in 2: Sub { width: 10px; height: 10px; }
161    out property <brush> sub-background: sub.background;
162}"#;
163        let instance = compile_with_debug_hooks(code);
164
165        let store = install_debug_hook_store(&instance);
166
167        let blue = Value::Brush(i_slint_core::Brush::SolidColor(i_slint_core::Color::from_rgb_u8(
168            0, 0, 255,
169        )));
170        assert_eq!(instance.get_property("sub-background").unwrap(), blue);
171
172        let (element, element_hash) = find_element(&instance, code, "Sub {");
173
174        let red = Value::Brush(i_slint_core::Brush::SolidColor(i_slint_core::Color::from_rgb_u8(
175            255, 0, 0,
176        )));
177        set_override(&store, element_hash, "background", Some(red.clone()));
178        assert_eq!(instance.get_property("sub-background").unwrap(), red);
179        set_override(&store, element_hash, "background", None);
180        assert_eq!(instance.get_property("sub-background").unwrap(), blue);
181
182        let base = instance.element_positions(&element).first().expect("geometry").rect;
183        set_override(&store, element_hash, "x", Some(Value::Number(110.0)));
184        let after = instance.element_positions(&element).first().expect("geometry").rect;
185        assert!(
186            (after.origin.x - base.origin.x - 100.0).abs() < 0.5,
187            "x override should shift the instance by 100px (base {}, after {})",
188            base.origin.x,
189            after.origin.x
190        );
191        set_override(&store, element_hash, "x", None);
192
193        // Overriding the injected transform-rotation hook must be possible (the Transform
194        // wrapper element is reified around the instance) and must not affect the geometry.
195        set_override(&store, element_hash, "transform-rotation", Some(Value::Number(45.0)));
196        let rotated = instance.element_positions(&element).first().expect("geometry").rect;
197        assert!(
198            (rotated.origin.x - base.origin.x).abs() < 0.5,
199            "rotation must not move the origin"
200        );
201        set_override(&store, element_hash, "transform-rotation", None);
202    }
203
204    // Regression test: debug hooks inject bindings for properties the element may not have
205    // natively (geometry, transform-rotation). Every injected binding must end up on a property
206    // that actually exists at runtime, for every kind of element — otherwise instantiation
207    // aborts with "unknown property ... in ...". Exercise the special cases: the root element,
208    // plain items, elements that become component roots later (PopupWindow — plain or through
209    // component inheritance), non-item types (Timer), repeated and conditional elements,
210    // layouts, menus, style widgets, and tooltips with custom content.
211    #[test]
212    fn debug_hooks_instantiate_special_elements() {
213        let code = r#"
214import { Button } from "std-widgets.slint";
215
216component MyPopup inherits PopupWindow {
217    Rectangle { background: yellow; }
218}
219
220export component Win inherits Window {
221    width: 300px;
222    height: 300px;
223
224    MenuBar {
225        Menu {
226            title: "File";
227            MenuItem { title: "Quit"; }
228        }
229    }
230
231    rect := Rectangle {
232        rotated := Rectangle { transform-rotation: 45deg; }
233        scaled := Rectangle { transform-scale: 150%; }
234        plain := Rectangle { }
235    }
236
237    covered := Rectangle {
238        Tooltip {
239            Rectangle { background: #222; }
240        }
241    }
242
243    Button { text: "a widget"; }
244
245    popup := PopupWindow {
246        Text { text: "popup content"; }
247    }
248    my-popup := MyPopup { }
249    callback show-the-popups();
250    show-the-popups() => { popup.show(); my-popup.show(); }
251
252    Timer { interval: 1s; running: false; }
253
254    for _ in 3: Rectangle { width: 10px; }
255    if true: Rectangle { height: 5px; }
256
257    VerticalLayout {
258        Rectangle { }
259    }
260}"#;
261
262        let instance = compile_with_debug_hooks(code);
263
264        // Showing the popups instantiates the popup components (their bindings are only set up then).
265        instance.invoke("show-the-popups", &[]).unwrap();
266    }
267
268    // Enabling debug_hooks now also materializes hooked default bindings for unbound properties.
269    // This must NOT change the rendered result when no override is set: wrapping default-geometry
270    // bindings must preserve fill/implicit sizing, and injecting the type-default for unbound props
271    // must equal their unbound value (e.g. the font sentinel that drives Window inheritance).
272    #[test]
273    fn debug_hooks_preserve_geometry() {
274        i_slint_backend_testing::init_no_event_loop();
275
276        let code = r#"
277export component Win inherits Window {
278    width: 300px;
279    height: 200px;
280    rect := Rectangle { }            // no explicit geometry -> fills the parent
281    txt := Text { text: "Hello"; }   // implicit (font-dependent) size, inherited font
282}"#;
283        let geometries = |debug_hooks: bool| -> Vec<(f32, f32, f32, f32)> {
284            let mut compiler = Compiler::default();
285            if debug_hooks {
286                compiler.compiler_configuration(i_slint_core::InternalToken).debug_hooks =
287                    Some(std::hash::RandomState::new());
288            }
289            let r = spin_on::spin_on(compiler.build_from_source(code.to_string(), test_path()));
290            assert!(!r.has_errors(), "{:?}", r.diagnostics);
291            let instance = r.components().next().unwrap().create().unwrap();
292            [code.find("Rectangle").unwrap(), code.find("Text").unwrap()]
293                .into_iter()
294                .map(|off| {
295                    let (elem, _) = instance
296                        .element_node_at_source_code_position(&test_path(), off as u32)
297                        .first()
298                        .cloned()
299                        .expect("element");
300                    let g = instance.element_positions(&elem).first().expect("geometry").rect;
301                    (g.origin.x, g.origin.y, g.size.width, g.size.height)
302                })
303                .collect()
304        };
305
306        let without = geometries(false);
307        let with = geometries(true);
308        for (a, b) in without.iter().zip(with.iter()) {
309            assert!(
310                (a.0 - b.0).abs() < 0.5
311                    && (a.1 - b.1).abs() < 0.5
312                    && (a.2 - b.2).abs() < 0.5
313                    && (a.3 - b.3).abs() < 0.5,
314                "geometry differs with vs without debug_hooks: {a:?} vs {b:?}"
315            );
316        }
317        // Sanity: the Rectangle actually filled the 300x200 window (so we know we compared real sizes).
318        assert!((with[0].2 - 300.0).abs() < 0.5);
319        assert!((with[0].3 - 200.0).abs() < 0.5);
320    }
321}