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(all(test, feature = "internal", feature = "internal-highlight"))]
31pub(crate) mod tests {
32 use super::*;
33 use crate::{Compiler, ComponentInstance};
34 use i_slint_compiler::source_path::SourcePath;
35 use i_slint_core::{Property, graphics::ApproxEq};
36 use std::{cell::RefCell, collections::HashMap, 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 = spin_on::spin_on(compiler.build_from_source_path(
45 code.to_string(),
46 test_path(),
47 i_slint_core::InternalToken,
48 ));
49 assert!(!compile_result.has_errors(), "{:?}", compile_result.diagnostics);
50 compile_result.components().next().unwrap().create().unwrap()
51 }
52
53 fn install_debug_hook_store(instance: &ComponentInstance) -> Store {
54 let store: Store = Default::default();
55 {
56 let store = Rc::clone(&store);
57 instance.set_debug_hook_callback(Some(Box::new(move |id: &str| -> Option<Value> {
58 let mut m = (*store).borrow_mut();
59 let p = m.entry(SmolStr::from(id)).or_insert_with(|| Box::pin(Property::new(None)));
60 p.as_ref().get()
61 })));
62 }
63 store
64 }
65
66 fn set_override(store: &Store, element_hash: u64, name: &str, value: Option<Value>) {
67 let id = i_slint_compiler::passes::property_id(element_hash, &SmolStr::from(name));
68 let mut store = (*store).borrow_mut();
69 let override_property = store.entry(id).or_insert_with(|| Box::pin(Property::new(None)));
70 (&**override_property).set(value);
71 }
72
73 pub fn test_path() -> SourcePath {
75 SourcePath::new("/tmp/test.slint")
76 }
77
78 fn find_element_hash(instance: &ComponentInstance, code: &str, search_term: &str) -> u64 {
79 let offset = code.find(search_term).unwrap() as u32;
80 let (element, debug_index) = instance
81 .element_node_at_source_code_position(&test_path(), offset)
82 .first()
83 .cloned()
84 .expect("element resolved");
85 let element_hash = element.borrow().debug[debug_index].element_hash;
86 assert_ne!(element_hash, 0, "debug_hooks should populate element_hash");
87 element_hash
88 }
89
90 fn geometry_at(
91 instance: &ComponentInstance,
92 code: &str,
93 search_term: &str,
94 ) -> i_slint_core::lengths::LogicalRect {
95 let offset = code.find(search_term).unwrap() as u32;
96 instance.component_positions(&test_path(), offset).first().expect("geometry").rect
97 }
98
99 type Store = Rc<RefCell<HashMap<SmolStr, Pin<Box<Property<Option<Value>>>>>>>;
102
103 #[test]
108 fn debug_hook_live_override() {
109 let code = r#"
110export component Win inherits Window {
111 width: 300px;
112 height: 300px;
113 rect := Rectangle {
114 x: 10px;
115 y: 20px;
116 width: 30px;
117 height: 40px;
118 }
119}"#;
120
121 let instance = compile_with_debug_hooks(code);
122
123 let element_hash = find_element_hash(&instance, code, "Rectangle");
124
125 let store = install_debug_hook_store(&instance);
126
127 let base = geometry_at(&instance, code, "Rectangle");
128
129 set_override(&store, element_hash, "x", Some(Value::Number(100.0)));
130 set_override(&store, element_hash, "width", Some(Value::Number(70.0)));
131 let after = geometry_at(&instance, code, "Rectangle");
132 assert!(
133 after.origin.x.approx_eq(&(base.origin.x + 90.0)),
134 "x override should shift the element by 90px (base {}, after {})",
135 base.origin.x,
136 after.origin.x
137 );
138 assert!(
139 after.size.width.approx_eq(&(base.size.width + 40.0)),
140 "width override should grow the element by 40px (base {}, after {})",
141 base.size.width,
142 after.size.width
143 );
144
145 set_override(&store, element_hash, "x", None);
146 set_override(&store, element_hash, "width", None);
147 let reverted = geometry_at(&instance, code, "Rectangle");
148 assert!(reverted.origin.x.approx_eq(&base.origin.x), "x should revert");
149 assert!(reverted.size.width.approx_eq(&base.size.width), "width should revert");
150 }
151
152 #[test]
153 fn debug_hook_component_instance_override() {
154 let code = r#"
155component Sub inherits Rectangle {
156 in property <color> tint: blue;
157 background: tint;
158}
159export component Win inherits Window {
160 width: 300px;
161 height: 300px;
162 sub := Sub { x: 10px; y: 20px; width: 50px; height: 50px; }
163 for _idx in 2: Sub { width: 10px; height: 10px; }
164 out property <brush> sub-background: sub.background;
165}"#;
166 let instance = compile_with_debug_hooks(code);
167
168 let store = install_debug_hook_store(&instance);
169
170 let blue = Value::Brush(i_slint_core::Brush::SolidColor(i_slint_core::Color::from_rgb_u8(
171 0, 0, 255,
172 )));
173 assert_eq!(instance.get_property("sub-background").unwrap(), blue);
174
175 let element_hash = find_element_hash(&instance, code, "Sub {");
176
177 let red = Value::Brush(i_slint_core::Brush::SolidColor(i_slint_core::Color::from_rgb_u8(
178 255, 0, 0,
179 )));
180 set_override(&store, element_hash, "background", Some(red.clone()));
181 assert_eq!(instance.get_property("sub-background").unwrap(), red);
182 set_override(&store, element_hash, "background", None);
183 assert_eq!(instance.get_property("sub-background").unwrap(), blue);
184
185 let base = geometry_at(&instance, code, "Sub {");
186 set_override(&store, element_hash, "x", Some(Value::Number(110.0)));
187 let after = geometry_at(&instance, code, "Sub {");
188 assert!(
189 (after.origin.x - base.origin.x - 100.0).abs() < 0.5,
190 "x override should shift the instance by 100px (base {}, after {})",
191 base.origin.x,
192 after.origin.x
193 );
194 set_override(&store, element_hash, "x", None);
195
196 set_override(&store, element_hash, "transform-rotation", Some(Value::Number(45.0)));
199 let rotated = geometry_at(&instance, code, "Sub {");
200 assert!(
201 (rotated.origin.x - base.origin.x).abs() < 0.5,
202 "rotation must not move the origin"
203 );
204 set_override(&store, element_hash, "transform-rotation", None);
205 }
206
207 #[test]
208 fn debug_hook_forwards_live_private_state_per_instance() {
209 let code = r#"
210component Sub inherits Rectangle {
211 in property <length> seed;
212 in property <bool> active;
213 in-out property <length> inherited-value: private-child.x;
214 private-child := Rectangle { x: root.seed; }
215 states [
216 active when root.active: {
217 private-child.x: 50px;
218 }
219 ]
220}
221export component Win inherits Window {
222 in-out property <length> first-seed: 10px;
223 in-out property <bool> first-active;
224 first := Sub { seed: root.first-seed; active: root.first-active; }
225 second := Sub { seed: 20px; }
226 out property <length> first-value: first.inherited-value;
227 out property <length> second-value: second.inherited-value;
228}"#;
229 let instance = compile_with_debug_hooks(code);
230 let store = install_debug_hook_store(&instance);
231 let first_hash = find_element_hash(&instance, code, "Sub { seed: root.first-seed");
232
233 assert_eq!(instance.get_property("first-value").unwrap(), Value::Number(10.0));
234 assert_eq!(instance.get_property("second-value").unwrap(), Value::Number(20.0));
235
236 instance.set_property("first-seed", Value::Number(15.0)).unwrap();
237 assert_eq!(instance.get_property("first-value").unwrap(), Value::Number(15.0));
238 assert_eq!(instance.get_property("second-value").unwrap(), Value::Number(20.0));
239
240 instance.set_property("first-active", Value::Bool(true)).unwrap();
241 assert_eq!(instance.get_property("first-value").unwrap(), Value::Number(50.0));
242
243 set_override(&store, first_hash, "inherited-value", Some(Value::Number(90.0)));
244 instance.set_property("first-active", Value::Bool(false)).unwrap();
245 instance.set_property("first-seed", Value::Number(25.0)).unwrap();
246 assert_eq!(instance.get_property("first-value").unwrap(), Value::Number(90.0));
247 assert_eq!(instance.get_property("second-value").unwrap(), Value::Number(20.0));
248
249 set_override(&store, first_hash, "inherited-value", None);
250 assert_eq!(instance.get_property("first-value").unwrap(), Value::Number(25.0));
251 assert_eq!(instance.get_property("second-value").unwrap(), Value::Number(20.0));
252 }
253
254 #[test]
262 fn debug_hooks_instantiate_special_elements() {
263 let code = r#"
264import { Button } from "std-widgets.slint";
265
266component MyPopup inherits PopupWindow {
267 Rectangle { background: yellow; }
268}
269
270export component Win inherits Window {
271 width: 300px;
272 height: 300px;
273
274 MenuBar {
275 Menu {
276 title: "File";
277 MenuItem { title: "Quit"; }
278 }
279 }
280
281 rect := Rectangle {
282 rotated := Rectangle { transform-rotation: 45deg; }
283 scaled := Rectangle { transform-scale: 150%; }
284 plain := Rectangle { }
285 }
286
287 covered := Rectangle {
288 Tooltip {
289 Rectangle { background: #222; }
290 }
291 }
292
293 Button { text: "a widget"; }
294
295 popup := PopupWindow {
296 Text { text: "popup content"; }
297 }
298 my-popup := MyPopup { }
299 callback show-the-popups();
300 show-the-popups() => { popup.show(); my-popup.show(); }
301
302 Timer { interval: 1s; running: false; }
303
304 for _ in 3: Rectangle { width: 10px; }
305 if true: Rectangle { height: 5px; }
306
307 VerticalLayout {
308 Rectangle { }
309 }
310}"#;
311
312 let instance = compile_with_debug_hooks(code);
313
314 instance.invoke("show-the-popups", &[]).unwrap();
316 }
317
318 #[test]
323 fn debug_hooks_preserve_geometry() {
324 i_slint_backend_testing::init_no_event_loop();
325
326 let code = r#"
327export component Win inherits Window {
328 width: 300px;
329 height: 200px;
330 rect := Rectangle { } // no explicit geometry -> fills the parent
331 txt := Text { text: "Hello"; } // implicit (font-dependent) size, inherited font
332}"#;
333 let geometries = |debug_hooks: bool| -> Vec<(f32, f32, f32, f32)> {
334 let mut compiler = Compiler::default();
335 if debug_hooks {
336 compiler.compiler_configuration(i_slint_core::InternalToken).debug_hooks =
337 Some(std::hash::RandomState::new());
338 }
339 let r = spin_on::spin_on(compiler.build_from_source_path(
340 code.to_string(),
341 test_path(),
342 i_slint_core::InternalToken,
343 ));
344 assert!(!r.has_errors(), "{:?}", r.diagnostics);
345 let instance = r.components().next().unwrap().create().unwrap();
346 [code.find("Rectangle").unwrap(), code.find("Text").unwrap()]
347 .into_iter()
348 .map(|offset| {
349 let geometry = instance
350 .component_positions(&test_path(), offset as u32)
351 .first()
352 .expect("geometry")
353 .rect;
354 (
355 geometry.origin.x,
356 geometry.origin.y,
357 geometry.size.width,
358 geometry.size.height,
359 )
360 })
361 .collect()
362 };
363
364 let without = geometries(false);
365 let with = geometries(true);
366 for (a, b) in without.iter().zip(with.iter()) {
367 assert!(
368 (a.0 - b.0).abs() < 0.5
369 && (a.1 - b.1).abs() < 0.5
370 && (a.2 - b.2).abs() < 0.5
371 && (a.3 - b.3).abs() < 0.5,
372 "geometry differs with vs without debug_hooks: {a:?} vs {b:?}"
373 );
374 }
375 assert!((with[0].2 - 300.0).abs() < 0.5);
377 assert!((with[0].3 - 200.0).abs() < 0.5);
378 }
379}