1use crate::{Value, dynamic_item_tree};
5
6use smol_str::SmolStr;
7
8use std::pin::Pin;
9
10pub type DebugHookCallback = Box<dyn Fn(&str) -> Option<Value>>;
11
12pub(crate) fn set_debug_hook_callback(
13 component: Pin<&dynamic_item_tree::ItemTreeBox>,
14 func: Option<DebugHookCallback>,
15) {
16 let Some(global_storage) = component.description().compiled_globals() else {
17 return;
18 };
19 *(global_storage.debug_hook_callback.borrow_mut()) = func;
20}
21
22pub(crate) fn trigger_debug_hook(
23 component_instance: &dynamic_item_tree::InstanceRef,
24 id: SmolStr,
25) -> Option<Value> {
26 component_instance.description.compiled_globals().and_then(|global_storage| {
27 let callback = global_storage.debug_hook_callback.borrow();
28 callback.as_ref().and_then(|callback| callback(&id))
29 })
30}
31
32#[cfg(test)]
33mod tests {
34 use super::*;
35 use crate::{Compiler, ComponentInstance};
36 use i_slint_compiler::object_tree::Element;
37 use i_slint_core::{Property, graphics::ApproxEq};
38 use std::{cell::RefCell, collections::HashMap, path::PathBuf, rc::Rc};
39
40 fn compile_with_debug_hooks(code: &str) -> ComponentInstance {
41 i_slint_backend_testing::init_no_event_loop();
42
43 let mut compiler = Compiler::default();
44 compiler.compiler_configuration(i_slint_core::InternalToken).debug_hooks =
45 Some(std::hash::RandomState::new());
46 let compile_result =
47 spin_on::spin_on(compiler.build_from_source(code.to_string(), test_path()));
48 assert!(!compile_result.has_errors(), "{:?}", compile_result.diagnostics);
49 compile_result.components().next().unwrap().create().unwrap()
50 }
51
52 fn install_debug_hook_store(instance: &ComponentInstance) -> Store {
53 let store: Store = Default::default();
54 {
55 let store = Rc::clone(&store);
56 instance.set_debug_hook_callback(Some(Box::new(move |id: &str| -> Option<Value> {
57 let mut m = (*store).borrow_mut();
58 let p = m.entry(SmolStr::from(id)).or_insert_with(|| Box::pin(Property::new(None)));
59 p.as_ref().get()
60 })));
61 }
62 store
63 }
64
65 fn set_override(store: &Store, element_hash: u64, name: &str, value: Option<Value>) {
66 let id = i_slint_compiler::passes::property_id(element_hash, &SmolStr::from(name));
67 let mut store = (*store).borrow_mut();
68 let override_property = store.entry(id).or_insert_with(|| Box::pin(Property::new(None)));
69 (&**override_property).set(value);
70 }
71
72 fn test_path() -> PathBuf {
74 PathBuf::from("/tmp/test.slint")
75 }
76
77 fn find_element(
78 instance: &ComponentInstance,
79 code: &str,
80 search_term: &str,
81 ) -> (Rc<RefCell<Element>>, u64) {
82 let offset = code.find(search_term).unwrap() as u32;
83 let (element, debug_index) = instance
84 .element_node_at_source_code_position(&test_path(), offset)
85 .first()
86 .cloned()
87 .expect("element resolved");
88 let element_hash = element.borrow().debug[debug_index].element_hash;
89 assert_ne!(element_hash, 0, "debug_hooks should populate element_hash");
90 (element, element_hash)
91 }
92
93 type Store = Rc<RefCell<HashMap<SmolStr, Pin<Box<Property<Option<Value>>>>>>>;
96
97 #[test]
102 fn debug_hook_live_override() {
103 let code = r#"
104export component Win inherits Window {
105 width: 300px;
106 height: 300px;
107 rect := Rectangle {
108 x: 10px;
109 y: 20px;
110 width: 30px;
111 height: 40px;
112 }
113}"#;
114
115 let instance = compile_with_debug_hooks(code);
116
117 let (element, element_hash) = find_element(&instance, code, "Rectangle");
118
119 let store = install_debug_hook_store(&instance);
120
121 let base = instance.element_positions(&element).first().expect("geometry").rect;
122
123 set_override(&store, element_hash, "x", Some(Value::Number(100.0)));
124 set_override(&store, element_hash, "width", Some(Value::Number(70.0)));
125 let after = instance.element_positions(&element).first().expect("geometry").rect;
126 assert!(
127 after.origin.x.approx_eq(&(base.origin.x + 90.0)),
128 "x override should shift the element by 90px (base {}, after {})",
129 base.origin.x,
130 after.origin.x
131 );
132 assert!(
133 after.size.width.approx_eq(&(base.size.width + 40.0)),
134 "width override should grow the element by 40px (base {}, after {})",
135 base.size.width,
136 after.size.width
137 );
138
139 set_override(&store, element_hash, "x", None);
140 set_override(&store, element_hash, "width", None);
141 let reverted = instance.element_positions(&element).first().expect("geometry").rect;
142 assert!(reverted.origin.x.approx_eq(&base.origin.x), "x should revert");
143 assert!(reverted.size.width.approx_eq(&base.size.width), "width should revert");
144 }
145
146 #[test]
152 fn debug_hook_component_instance_override() {
153 let code = r#"
154component Sub inherits Rectangle {
155 in property <color> tint: blue;
156 background: tint;
157}
158export component Win inherits Window {
159 width: 300px;
160 height: 300px;
161 sub := Sub { x: 10px; y: 20px; width: 50px; height: 50px; }
162 for _idx in 2: Sub { width: 10px; height: 10px; }
163 out property <brush> sub-background: sub.background;
164}"#;
165 let instance = compile_with_debug_hooks(code);
166
167 let store = install_debug_hook_store(&instance);
168
169 let blue = Value::Brush(i_slint_core::Brush::SolidColor(i_slint_core::Color::from_rgb_u8(
170 0, 0, 255,
171 )));
172 assert_eq!(instance.get_property("sub-background").unwrap(), blue);
173
174 let (element, element_hash) = find_element(&instance, code, "Sub {");
175
176 let red = Value::Brush(i_slint_core::Brush::SolidColor(i_slint_core::Color::from_rgb_u8(
177 255, 0, 0,
178 )));
179 set_override(&store, element_hash, "background", Some(red.clone()));
180 assert_eq!(instance.get_property("sub-background").unwrap(), red);
181 set_override(&store, element_hash, "background", None);
182 assert_eq!(instance.get_property("sub-background").unwrap(), blue);
183
184 let base = instance.element_positions(&element).first().expect("geometry").rect;
185 set_override(&store, element_hash, "x", Some(Value::Number(110.0)));
186 let after = instance.element_positions(&element).first().expect("geometry").rect;
187 assert!(
188 (after.origin.x - base.origin.x - 100.0).abs() < 0.5,
189 "x override should shift the instance by 100px (base {}, after {})",
190 base.origin.x,
191 after.origin.x
192 );
193 set_override(&store, element_hash, "x", None);
194
195 set_override(&store, element_hash, "transform-rotation", Some(Value::Number(45.0)));
198 let rotated = instance.element_positions(&element).first().expect("geometry").rect;
199 assert!(
200 (rotated.origin.x - base.origin.x).abs() < 0.5,
201 "rotation must not move the origin"
202 );
203 set_override(&store, element_hash, "transform-rotation", None);
204 }
205
206 #[test]
214 fn debug_hooks_instantiate_special_elements() {
215 let code = r#"
216import { Button } from "std-widgets.slint";
217
218component MyPopup inherits PopupWindow {
219 Rectangle { background: yellow; }
220}
221
222export component Win inherits Window {
223 width: 300px;
224 height: 300px;
225
226 MenuBar {
227 Menu {
228 title: "File";
229 MenuItem { title: "Quit"; }
230 }
231 }
232
233 rect := Rectangle {
234 rotated := Rectangle { transform-rotation: 45deg; }
235 scaled := Rectangle { transform-scale: 150%; }
236 plain := Rectangle { }
237 }
238
239 covered := Rectangle {
240 Tooltip {
241 Rectangle { background: #222; }
242 }
243 }
244
245 Button { text: "a widget"; }
246
247 popup := PopupWindow {
248 Text { text: "popup content"; }
249 }
250 my-popup := MyPopup { }
251 callback show-the-popups();
252 show-the-popups() => { popup.show(); my-popup.show(); }
253
254 Timer { interval: 1s; running: false; }
255
256 for _ in 3: Rectangle { width: 10px; }
257 if true: Rectangle { height: 5px; }
258
259 VerticalLayout {
260 Rectangle { }
261 }
262}"#;
263
264 let instance = compile_with_debug_hooks(code);
265
266 instance.invoke("show-the-popups", &[]).unwrap();
268 }
269
270 #[test]
275 fn debug_hooks_preserve_geometry() {
276 i_slint_backend_testing::init_no_event_loop();
277
278 let code = r#"
279export component Win inherits Window {
280 width: 300px;
281 height: 200px;
282 rect := Rectangle { } // no explicit geometry -> fills the parent
283 txt := Text { text: "Hello"; } // implicit (font-dependent) size, inherited font
284}"#;
285 let geometries = |debug_hooks: bool| -> Vec<(f32, f32, f32, f32)> {
286 let mut compiler = Compiler::default();
287 if debug_hooks {
288 compiler.compiler_configuration(i_slint_core::InternalToken).debug_hooks =
289 Some(std::hash::RandomState::new());
290 }
291 let r = spin_on::spin_on(compiler.build_from_source(code.to_string(), test_path()));
292 assert!(!r.has_errors(), "{:?}", r.diagnostics);
293 let instance = r.components().next().unwrap().create().unwrap();
294 [code.find("Rectangle").unwrap(), code.find("Text").unwrap()]
295 .into_iter()
296 .map(|off| {
297 let (elem, _) = instance
298 .element_node_at_source_code_position(&test_path(), off as u32)
299 .first()
300 .cloned()
301 .expect("element");
302 let g = instance.element_positions(&elem).first().expect("geometry").rect;
303 (g.origin.x, g.origin.y, g.size.width, g.size.height)
304 })
305 .collect()
306 };
307
308 let without = geometries(false);
309 let with = geometries(true);
310 for (a, b) in without.iter().zip(with.iter()) {
311 assert!(
312 (a.0 - b.0).abs() < 0.5
313 && (a.1 - b.1).abs() < 0.5
314 && (a.2 - b.2).abs() < 0.5
315 && (a.3 - b.3).abs() < 0.5,
316 "geometry differs with vs without debug_hooks: {a:?} vs {b:?}"
317 );
318 }
319 assert!((with[0].2 - 300.0).abs() < 0.5);
321 assert!((with[0].3 - 200.0).abs() < 0.5);
322 }
323}