1use 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
23pub(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)]
31pub(crate) mod 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 pub 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 pub 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 type Store = Rc<RefCell<HashMap<SmolStr, Pin<Box<Property<Option<Value>>>>>>>;
94
95 #[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 #[test]
145 fn debug_hook_component_instance_override() {
146 let code = r#"
147component Sub inherits Rectangle {
148 in property <color> tint: blue;
149 background: tint;
150}
151export component Win inherits Window {
152 width: 300px;
153 height: 300px;
154 sub := Sub { x: 10px; y: 20px; width: 50px; height: 50px; }
155 for _idx in 2: Sub { width: 10px; height: 10px; }
156 out property <brush> sub-background: sub.background;
157}"#;
158 let instance = compile_with_debug_hooks(code);
159
160 let store = install_debug_hook_store(&instance);
161
162 let blue = Value::Brush(i_slint_core::Brush::SolidColor(i_slint_core::Color::from_rgb_u8(
163 0, 0, 255,
164 )));
165 assert_eq!(instance.get_property("sub-background").unwrap(), blue);
166
167 let (element, element_hash) = find_element(&instance, code, "Sub {");
168
169 let red = Value::Brush(i_slint_core::Brush::SolidColor(i_slint_core::Color::from_rgb_u8(
170 255, 0, 0,
171 )));
172 set_override(&store, element_hash, "background", Some(red.clone()));
173 assert_eq!(instance.get_property("sub-background").unwrap(), red);
174 set_override(&store, element_hash, "background", None);
175 assert_eq!(instance.get_property("sub-background").unwrap(), blue);
176
177 let base = instance.element_positions(&element).first().expect("geometry").rect;
178 set_override(&store, element_hash, "x", Some(Value::Number(110.0)));
179 let after = instance.element_positions(&element).first().expect("geometry").rect;
180 assert!(
181 (after.origin.x - base.origin.x - 100.0).abs() < 0.5,
182 "x override should shift the instance by 100px (base {}, after {})",
183 base.origin.x,
184 after.origin.x
185 );
186 set_override(&store, element_hash, "x", None);
187
188 set_override(&store, element_hash, "transform-rotation", Some(Value::Number(45.0)));
191 let rotated = instance.element_positions(&element).first().expect("geometry").rect;
192 assert!(
193 (rotated.origin.x - base.origin.x).abs() < 0.5,
194 "rotation must not move the origin"
195 );
196 set_override(&store, element_hash, "transform-rotation", None);
197 }
198
199 #[test]
200 fn debug_hook_forwards_live_private_state_per_instance() {
201 let code = r#"
202component Sub inherits Rectangle {
203 in property <length> seed;
204 in property <bool> active;
205 in-out property <length> inherited-value: private-child.x;
206 private-child := Rectangle { x: root.seed; }
207 states [
208 active when root.active: {
209 private-child.x: 50px;
210 }
211 ]
212}
213export component Win inherits Window {
214 in-out property <length> first-seed: 10px;
215 in-out property <bool> first-active;
216 first := Sub { seed: root.first-seed; active: root.first-active; }
217 second := Sub { seed: 20px; }
218 out property <length> first-value: first.inherited-value;
219 out property <length> second-value: second.inherited-value;
220}"#;
221 let instance = compile_with_debug_hooks(code);
222 let store = install_debug_hook_store(&instance);
223 let (_, first_hash) = find_element(&instance, code, "Sub { seed: root.first-seed");
224
225 assert_eq!(instance.get_property("first-value").unwrap(), Value::Number(10.0));
226 assert_eq!(instance.get_property("second-value").unwrap(), Value::Number(20.0));
227
228 instance.set_property("first-seed", Value::Number(15.0)).unwrap();
229 assert_eq!(instance.get_property("first-value").unwrap(), Value::Number(15.0));
230 assert_eq!(instance.get_property("second-value").unwrap(), Value::Number(20.0));
231
232 instance.set_property("first-active", Value::Bool(true)).unwrap();
233 assert_eq!(instance.get_property("first-value").unwrap(), Value::Number(50.0));
234
235 set_override(&store, first_hash, "inherited-value", Some(Value::Number(90.0)));
236 instance.set_property("first-active", Value::Bool(false)).unwrap();
237 instance.set_property("first-seed", Value::Number(25.0)).unwrap();
238 assert_eq!(instance.get_property("first-value").unwrap(), Value::Number(90.0));
239 assert_eq!(instance.get_property("second-value").unwrap(), Value::Number(20.0));
240
241 set_override(&store, first_hash, "inherited-value", None);
242 assert_eq!(instance.get_property("first-value").unwrap(), Value::Number(25.0));
243 assert_eq!(instance.get_property("second-value").unwrap(), Value::Number(20.0));
244 }
245
246 #[test]
254 fn debug_hooks_instantiate_special_elements() {
255 let code = r#"
256import { Button } from "std-widgets.slint";
257
258component MyPopup inherits PopupWindow {
259 Rectangle { background: yellow; }
260}
261
262export component Win inherits Window {
263 width: 300px;
264 height: 300px;
265
266 MenuBar {
267 Menu {
268 title: "File";
269 MenuItem { title: "Quit"; }
270 }
271 }
272
273 rect := Rectangle {
274 rotated := Rectangle { transform-rotation: 45deg; }
275 scaled := Rectangle { transform-scale: 150%; }
276 plain := Rectangle { }
277 }
278
279 covered := Rectangle {
280 Tooltip {
281 Rectangle { background: #222; }
282 }
283 }
284
285 Button { text: "a widget"; }
286
287 popup := PopupWindow {
288 Text { text: "popup content"; }
289 }
290 my-popup := MyPopup { }
291 callback show-the-popups();
292 show-the-popups() => { popup.show(); my-popup.show(); }
293
294 Timer { interval: 1s; running: false; }
295
296 for _ in 3: Rectangle { width: 10px; }
297 if true: Rectangle { height: 5px; }
298
299 VerticalLayout {
300 Rectangle { }
301 }
302}"#;
303
304 let instance = compile_with_debug_hooks(code);
305
306 instance.invoke("show-the-popups", &[]).unwrap();
308 }
309
310 #[test]
315 fn debug_hooks_preserve_geometry() {
316 i_slint_backend_testing::init_no_event_loop();
317
318 let code = r#"
319export component Win inherits Window {
320 width: 300px;
321 height: 200px;
322 rect := Rectangle { } // no explicit geometry -> fills the parent
323 txt := Text { text: "Hello"; } // implicit (font-dependent) size, inherited font
324}"#;
325 let geometries = |debug_hooks: bool| -> Vec<(f32, f32, f32, f32)> {
326 let mut compiler = Compiler::default();
327 if debug_hooks {
328 compiler.compiler_configuration(i_slint_core::InternalToken).debug_hooks =
329 Some(std::hash::RandomState::new());
330 }
331 let r = spin_on::spin_on(compiler.build_from_source(code.to_string(), test_path()));
332 assert!(!r.has_errors(), "{:?}", r.diagnostics);
333 let instance = r.components().next().unwrap().create().unwrap();
334 [code.find("Rectangle").unwrap(), code.find("Text").unwrap()]
335 .into_iter()
336 .map(|off| {
337 let (elem, _) = instance
338 .element_node_at_source_code_position(&test_path(), off as u32)
339 .first()
340 .cloned()
341 .expect("element");
342 let g = instance.element_positions(&elem).first().expect("geometry").rect;
343 (g.origin.x, g.origin.y, g.size.width, g.size.height)
344 })
345 .collect()
346 };
347
348 let without = geometries(false);
349 let with = geometries(true);
350 for (a, b) in without.iter().zip(with.iter()) {
351 assert!(
352 (a.0 - b.0).abs() < 0.5
353 && (a.1 - b.1).abs() < 0.5
354 && (a.2 - b.2).abs() < 0.5
355 && (a.3 - b.3).abs() < 0.5,
356 "geometry differs with vs without debug_hooks: {a:?} vs {b:?}"
357 );
358 }
359 assert!((with[0].2 - 300.0).abs() < 0.5);
361 assert!((with[0].3 - 200.0).abs() < 0.5);
362 }
363}