Skip to main content

dear_imgui_test_engine/
lib.rs

1//! Dear ImGui Test Engine bindings for `dear-imgui-rs`.
2//!
3//! This crate wraps `dear-imgui-test-engine-sys` with a small safe API for
4//! engine lifetime management and per-frame UI integration.
5
6use bitflags::bitflags;
7use dear_imgui_rs::{
8    Context, ContextAliveToken, ImGuiError, ImGuiResult, KeyChord, KeyMods, MouseButton, Ui,
9    with_scratch_txt, with_scratch_txt_two,
10};
11use dear_imgui_test_engine_sys as sys;
12use std::{marker::PhantomData, rc::Rc};
13
14pub use dear_imgui_test_engine_sys as raw;
15
16#[repr(i32)]
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum RunSpeed {
19    Fast = sys::ImGuiTestEngineRunSpeed_Fast as i32,
20    Normal = sys::ImGuiTestEngineRunSpeed_Normal as i32,
21    Cinematic = sys::ImGuiTestEngineRunSpeed_Cinematic as i32,
22}
23
24#[repr(i32)]
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum VerboseLevel {
27    Silent = sys::ImGuiTestEngineVerboseLevel_Silent as i32,
28    Error = sys::ImGuiTestEngineVerboseLevel_Error as i32,
29    Warning = sys::ImGuiTestEngineVerboseLevel_Warning as i32,
30    Info = sys::ImGuiTestEngineVerboseLevel_Info as i32,
31    Debug = sys::ImGuiTestEngineVerboseLevel_Debug as i32,
32    Trace = sys::ImGuiTestEngineVerboseLevel_Trace as i32,
33}
34
35#[repr(i32)]
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum InputMode {
38    Mouse = dear_imgui_rs::sys::ImGuiInputSource_Mouse as i32,
39    Keyboard = dear_imgui_rs::sys::ImGuiInputSource_Keyboard as i32,
40    Gamepad = dear_imgui_rs::sys::ImGuiInputSource_Gamepad as i32,
41}
42
43#[repr(i32)]
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum TestGroup {
46    Unknown = sys::ImGuiTestEngineGroup_Unknown,
47    Tests = sys::ImGuiTestEngineGroup_Tests,
48    Perfs = sys::ImGuiTestEngineGroup_Perfs,
49}
50
51bitflags! {
52    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
53    pub struct RunFlags: u32 {
54        const NONE = sys::ImGuiTestEngineRunFlags_None as u32;
55        const GUI_FUNC_DISABLE = sys::ImGuiTestEngineRunFlags_GuiFuncDisable as u32;
56        const GUI_FUNC_ONLY = sys::ImGuiTestEngineRunFlags_GuiFuncOnly as u32;
57        const NO_SUCCESS_MSG = sys::ImGuiTestEngineRunFlags_NoSuccessMsg as u32;
58        const ENABLE_RAW_INPUTS = sys::ImGuiTestEngineRunFlags_EnableRawInputs as u32;
59        const RUN_FROM_GUI = sys::ImGuiTestEngineRunFlags_RunFromGui as u32;
60        const RUN_FROM_COMMAND_LINE = sys::ImGuiTestEngineRunFlags_RunFromCommandLine as u32;
61        const NO_ERROR = sys::ImGuiTestEngineRunFlags_NoError as u32;
62        const SHARE_VARS = sys::ImGuiTestEngineRunFlags_ShareVars as u32;
63        const SHARE_TEST_CONTEXT = sys::ImGuiTestEngineRunFlags_ShareTestContext as u32;
64    }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub struct ResultSummary {
69    pub count_tested: i32,
70    pub count_success: i32,
71    pub count_in_queue: i32,
72}
73
74/// Dear ImGui Test Engine context.
75///
76/// The upstream engine is not thread-safe; create and use it on the same thread as the target ImGui context.
77pub struct TestEngine {
78    raw: *mut sys::ImGuiTestEngine,
79    bound_imgui_ctx_raw: Option<*mut dear_imgui_rs::sys::ImGuiContext>,
80    bound_imgui_alive: Option<ContextAliveToken>,
81    _not_send_sync: PhantomData<Rc<()>>,
82}
83
84struct Script {
85    raw: *mut sys::ImGuiTestEngineScript,
86}
87
88impl Script {
89    fn create() -> ImGuiResult<Self> {
90        let raw = unsafe { sys::imgui_test_engine_script_create() };
91        if raw.is_null() {
92            return Err(ImGuiError::invalid_operation(
93                "imgui_test_engine_script_create returned null",
94            ));
95        }
96        Ok(Self { raw })
97    }
98
99    fn into_raw(mut self) -> *mut sys::ImGuiTestEngineScript {
100        let raw = self.raw;
101        self.raw = std::ptr::null_mut();
102        raw
103    }
104}
105
106impl Drop for Script {
107    fn drop(&mut self) {
108        if !self.raw.is_null() {
109            unsafe { sys::imgui_test_engine_script_destroy(self.raw) };
110            self.raw = std::ptr::null_mut();
111        }
112    }
113}
114
115pub struct ScriptTest<'a> {
116    script: &'a mut Script,
117}
118
119impl ScriptTest<'_> {
120    pub fn set_ref(&mut self, r#ref: &str) -> ImGuiResult<()> {
121        if r#ref.contains('\0') {
122            return Err(ImGuiError::invalid_operation(
123                "set_ref contained interior NUL",
124            ));
125        }
126        with_scratch_txt(r#ref, |ptr| unsafe {
127            sys::imgui_test_engine_script_set_ref(self.script.raw, ptr)
128        });
129        Ok(())
130    }
131
132    pub fn item_click(&mut self, r#ref: &str) -> ImGuiResult<()> {
133        if r#ref.contains('\0') {
134            return Err(ImGuiError::invalid_operation(
135                "item_click contained interior NUL",
136            ));
137        }
138        with_scratch_txt(r#ref, |ptr| unsafe {
139            sys::imgui_test_engine_script_item_click(self.script.raw, ptr)
140        });
141        Ok(())
142    }
143
144    pub fn item_click_with_button(&mut self, r#ref: &str, button: MouseButton) -> ImGuiResult<()> {
145        if r#ref.contains('\0') {
146            return Err(ImGuiError::invalid_operation(
147                "item_click_with_button contained interior NUL",
148            ));
149        }
150        let button = button as i32;
151        with_scratch_txt(r#ref, |ptr| unsafe {
152            sys::imgui_test_engine_script_item_click_with_button(self.script.raw, ptr, button)
153        });
154        Ok(())
155    }
156
157    pub fn item_double_click(&mut self, r#ref: &str) -> ImGuiResult<()> {
158        if r#ref.contains('\0') {
159            return Err(ImGuiError::invalid_operation(
160                "item_double_click contained interior NUL",
161            ));
162        }
163        with_scratch_txt(r#ref, |ptr| unsafe {
164            sys::imgui_test_engine_script_item_double_click(self.script.raw, ptr)
165        });
166        Ok(())
167    }
168
169    pub fn item_open(&mut self, r#ref: &str) -> ImGuiResult<()> {
170        if r#ref.contains('\0') {
171            return Err(ImGuiError::invalid_operation(
172                "item_open contained interior NUL",
173            ));
174        }
175        with_scratch_txt(r#ref, |ptr| unsafe {
176            sys::imgui_test_engine_script_item_open(self.script.raw, ptr)
177        });
178        Ok(())
179    }
180
181    pub fn item_close(&mut self, r#ref: &str) -> ImGuiResult<()> {
182        if r#ref.contains('\0') {
183            return Err(ImGuiError::invalid_operation(
184                "item_close contained interior NUL",
185            ));
186        }
187        with_scratch_txt(r#ref, |ptr| unsafe {
188            sys::imgui_test_engine_script_item_close(self.script.raw, ptr)
189        });
190        Ok(())
191    }
192
193    pub fn item_set_opened(&mut self, r#ref: &str, opened: bool) -> ImGuiResult<()> {
194        if opened {
195            self.item_open(r#ref)
196        } else {
197            self.item_close(r#ref)
198        }
199    }
200
201    pub fn item_check(&mut self, r#ref: &str) -> ImGuiResult<()> {
202        if r#ref.contains('\0') {
203            return Err(ImGuiError::invalid_operation(
204                "item_check contained interior NUL",
205            ));
206        }
207        with_scratch_txt(r#ref, |ptr| unsafe {
208            sys::imgui_test_engine_script_item_check(self.script.raw, ptr)
209        });
210        Ok(())
211    }
212
213    pub fn item_set_checked(&mut self, r#ref: &str, checked: bool) -> ImGuiResult<()> {
214        if checked {
215            self.item_check(r#ref)
216        } else {
217            self.item_uncheck(r#ref)
218        }
219    }
220
221    pub fn item_uncheck(&mut self, r#ref: &str) -> ImGuiResult<()> {
222        if r#ref.contains('\0') {
223            return Err(ImGuiError::invalid_operation(
224                "item_uncheck contained interior NUL",
225            ));
226        }
227        with_scratch_txt(r#ref, |ptr| unsafe {
228            sys::imgui_test_engine_script_item_uncheck(self.script.raw, ptr)
229        });
230        Ok(())
231    }
232
233    pub fn item_input_int(&mut self, r#ref: &str, v: i32) -> ImGuiResult<()> {
234        if r#ref.contains('\0') {
235            return Err(ImGuiError::invalid_operation(
236                "item_input_int contained interior NUL",
237            ));
238        }
239        with_scratch_txt(r#ref, |ptr| unsafe {
240            sys::imgui_test_engine_script_item_input_int(self.script.raw, ptr, v)
241        });
242        Ok(())
243    }
244
245    pub fn item_input_str(&mut self, r#ref: &str, v: &str) -> ImGuiResult<()> {
246        if r#ref.contains('\0') || v.contains('\0') {
247            return Err(ImGuiError::invalid_operation(
248                "item_input_str contained interior NUL",
249            ));
250        }
251        with_scratch_txt_two(r#ref, v, |ref_ptr, v_ptr| unsafe {
252            sys::imgui_test_engine_script_item_input_str(self.script.raw, ref_ptr, v_ptr)
253        });
254        Ok(())
255    }
256
257    pub fn mouse_move(&mut self, r#ref: &str) -> ImGuiResult<()> {
258        if r#ref.contains('\0') {
259            return Err(ImGuiError::invalid_operation(
260                "mouse_move contained interior NUL",
261            ));
262        }
263        with_scratch_txt(r#ref, |ptr| unsafe {
264            sys::imgui_test_engine_script_mouse_move(self.script.raw, ptr)
265        });
266        Ok(())
267    }
268
269    pub fn mouse_move_to_pos(&mut self, x: f32, y: f32) -> ImGuiResult<()> {
270        if !x.is_finite() || !y.is_finite() {
271            return Err(ImGuiError::invalid_operation(
272                "mouse_move_to_pos requires finite values",
273            ));
274        }
275        unsafe { sys::imgui_test_engine_script_mouse_move_to_pos(self.script.raw, x, y) };
276        Ok(())
277    }
278
279    pub fn mouse_teleport_to_pos(&mut self, x: f32, y: f32) -> ImGuiResult<()> {
280        if !x.is_finite() || !y.is_finite() {
281            return Err(ImGuiError::invalid_operation(
282                "mouse_teleport_to_pos requires finite values",
283            ));
284        }
285        unsafe { sys::imgui_test_engine_script_mouse_teleport_to_pos(self.script.raw, x, y) };
286        Ok(())
287    }
288
289    pub fn mouse_move_to_void(&mut self) {
290        unsafe { sys::imgui_test_engine_script_mouse_move_to_void(self.script.raw) };
291    }
292
293    pub fn mouse_click(&mut self, button: MouseButton) {
294        let button = button as i32;
295        unsafe { sys::imgui_test_engine_script_mouse_click(self.script.raw, button) };
296    }
297
298    pub fn mouse_click_multi(&mut self, button: MouseButton, count: i32) -> ImGuiResult<()> {
299        if count < 1 {
300            return Err(ImGuiError::invalid_operation(
301                "mouse_click_multi count must be >= 1",
302            ));
303        }
304        let button = button as i32;
305        unsafe { sys::imgui_test_engine_script_mouse_click_multi(self.script.raw, button, count) };
306        Ok(())
307    }
308
309    pub fn mouse_double_click(&mut self, button: MouseButton) {
310        let button = button as i32;
311        unsafe { sys::imgui_test_engine_script_mouse_double_click(self.script.raw, button) };
312    }
313
314    pub fn mouse_down(&mut self, button: MouseButton) {
315        let button = button as i32;
316        unsafe { sys::imgui_test_engine_script_mouse_down(self.script.raw, button) };
317    }
318
319    pub fn mouse_up(&mut self, button: MouseButton) {
320        let button = button as i32;
321        unsafe { sys::imgui_test_engine_script_mouse_up(self.script.raw, button) };
322    }
323
324    pub fn mouse_lift_drag_threshold(&mut self, button: MouseButton) {
325        let button = button as i32;
326        unsafe { sys::imgui_test_engine_script_mouse_lift_drag_threshold(self.script.raw, button) };
327    }
328
329    pub fn mouse_drag_with_delta(
330        &mut self,
331        dx: f32,
332        dy: f32,
333        button: MouseButton,
334    ) -> ImGuiResult<()> {
335        if !dx.is_finite() || !dy.is_finite() {
336            return Err(ImGuiError::invalid_operation(
337                "mouse_drag_with_delta requires finite values",
338            ));
339        }
340        let button = button as i32;
341        unsafe {
342            sys::imgui_test_engine_script_mouse_drag_with_delta(self.script.raw, dx, dy, button)
343        };
344        Ok(())
345    }
346
347    pub fn mouse_click_on_void(&mut self, button: i32, count: i32) -> ImGuiResult<()> {
348        if button < 0 {
349            return Err(ImGuiError::invalid_operation(
350                "mouse_click_on_void button must be >= 0",
351            ));
352        }
353        if count < 1 {
354            return Err(ImGuiError::invalid_operation(
355                "mouse_click_on_void count must be >= 1",
356            ));
357        }
358        unsafe {
359            sys::imgui_test_engine_script_mouse_click_on_void(self.script.raw, button, count)
360        };
361        Ok(())
362    }
363
364    pub fn mouse_wheel(&mut self, dx: f32, dy: f32) -> ImGuiResult<()> {
365        if !dx.is_finite() || !dy.is_finite() {
366            return Err(ImGuiError::invalid_operation(
367                "mouse_wheel requires finite values",
368            ));
369        }
370        unsafe { sys::imgui_test_engine_script_mouse_wheel(self.script.raw, dx, dy) };
371        Ok(())
372    }
373
374    pub fn key_down(&mut self, key_chord: KeyChord) {
375        unsafe { sys::imgui_test_engine_script_key_down(self.script.raw, key_chord.raw()) };
376    }
377
378    pub fn key_up(&mut self, key_chord: KeyChord) {
379        unsafe { sys::imgui_test_engine_script_key_up(self.script.raw, key_chord.raw()) };
380    }
381
382    pub fn key_press(&mut self, key_chord: KeyChord, count: i32) -> ImGuiResult<()> {
383        if count < 1 {
384            return Err(ImGuiError::invalid_operation(
385                "key_press count must be >= 1",
386            ));
387        }
388        unsafe { sys::imgui_test_engine_script_key_press(self.script.raw, key_chord.raw(), count) };
389        Ok(())
390    }
391
392    pub fn key_hold(&mut self, key_chord: KeyChord, seconds: f32) -> ImGuiResult<()> {
393        if !seconds.is_finite() || seconds < 0.0 {
394            return Err(ImGuiError::invalid_operation(
395                "key_hold requires a finite non-negative value",
396            ));
397        }
398        unsafe {
399            sys::imgui_test_engine_script_key_hold(self.script.raw, key_chord.raw(), seconds)
400        };
401        Ok(())
402    }
403
404    pub fn key_chars(&mut self, chars: &str) -> ImGuiResult<()> {
405        if chars.contains('\0') {
406            return Err(ImGuiError::invalid_operation(
407                "key_chars contained interior NUL",
408            ));
409        }
410        with_scratch_txt(chars, |ptr| unsafe {
411            sys::imgui_test_engine_script_key_chars(self.script.raw, ptr)
412        });
413        Ok(())
414    }
415
416    pub fn key_chars_append(&mut self, chars: &str) -> ImGuiResult<()> {
417        if chars.contains('\0') {
418            return Err(ImGuiError::invalid_operation(
419                "key_chars_append contained interior NUL",
420            ));
421        }
422        with_scratch_txt(chars, |ptr| unsafe {
423            sys::imgui_test_engine_script_key_chars_append(self.script.raw, ptr)
424        });
425        Ok(())
426    }
427
428    pub fn key_chars_append_enter(&mut self, chars: &str) -> ImGuiResult<()> {
429        if chars.contains('\0') {
430            return Err(ImGuiError::invalid_operation(
431                "key_chars_append_enter contained interior NUL",
432            ));
433        }
434        with_scratch_txt(chars, |ptr| unsafe {
435            sys::imgui_test_engine_script_key_chars_append_enter(self.script.raw, ptr)
436        });
437        Ok(())
438    }
439
440    pub fn key_chars_replace(&mut self, chars: &str) -> ImGuiResult<()> {
441        if chars.contains('\0') {
442            return Err(ImGuiError::invalid_operation(
443                "key_chars_replace contained interior NUL",
444            ));
445        }
446        with_scratch_txt(chars, |ptr| unsafe {
447            sys::imgui_test_engine_script_key_chars_replace(self.script.raw, ptr)
448        });
449        Ok(())
450    }
451
452    pub fn key_chars_replace_enter(&mut self, chars: &str) -> ImGuiResult<()> {
453        if chars.contains('\0') {
454            return Err(ImGuiError::invalid_operation(
455                "key_chars_replace_enter contained interior NUL",
456            ));
457        }
458        with_scratch_txt(chars, |ptr| unsafe {
459            sys::imgui_test_engine_script_key_chars_replace_enter(self.script.raw, ptr)
460        });
461        Ok(())
462    }
463
464    pub fn item_hold(&mut self, r#ref: &str, seconds: f32) -> ImGuiResult<()> {
465        if r#ref.contains('\0') {
466            return Err(ImGuiError::invalid_operation(
467                "item_hold contained interior NUL",
468            ));
469        }
470        if !seconds.is_finite() || seconds < 0.0 {
471            return Err(ImGuiError::invalid_operation(
472                "item_hold requires a finite non-negative value",
473            ));
474        }
475        with_scratch_txt(r#ref, |ptr| unsafe {
476            sys::imgui_test_engine_script_item_hold(self.script.raw, ptr, seconds)
477        });
478        Ok(())
479    }
480
481    pub fn item_hold_for_frames(&mut self, r#ref: &str, frames: i32) -> ImGuiResult<()> {
482        if r#ref.contains('\0') {
483            return Err(ImGuiError::invalid_operation(
484                "item_hold_for_frames contained interior NUL",
485            ));
486        }
487        if frames < 1 {
488            return Err(ImGuiError::invalid_operation(
489                "item_hold_for_frames frames must be >= 1",
490            ));
491        }
492        with_scratch_txt(r#ref, |ptr| unsafe {
493            sys::imgui_test_engine_script_item_hold_for_frames(self.script.raw, ptr, frames)
494        });
495        Ok(())
496    }
497
498    pub fn item_drag_over_and_hold(&mut self, src_ref: &str, dst_ref: &str) -> ImGuiResult<()> {
499        if src_ref.contains('\0') {
500            return Err(ImGuiError::invalid_operation(
501                "item_drag_over_and_hold src_ref contained interior NUL",
502            ));
503        }
504        if dst_ref.contains('\0') {
505            return Err(ImGuiError::invalid_operation(
506                "item_drag_over_and_hold dst_ref contained interior NUL",
507            ));
508        }
509        with_scratch_txt_two(src_ref, dst_ref, |src_ptr, dst_ptr| unsafe {
510            sys::imgui_test_engine_script_item_drag_over_and_hold(self.script.raw, src_ptr, dst_ptr)
511        });
512        Ok(())
513    }
514
515    pub fn item_drag_and_drop(
516        &mut self,
517        src_ref: &str,
518        dst_ref: &str,
519        button: MouseButton,
520    ) -> ImGuiResult<()> {
521        if src_ref.contains('\0') {
522            return Err(ImGuiError::invalid_operation(
523                "item_drag_and_drop src_ref contained interior NUL",
524            ));
525        }
526        if dst_ref.contains('\0') {
527            return Err(ImGuiError::invalid_operation(
528                "item_drag_and_drop dst_ref contained interior NUL",
529            ));
530        }
531        let button = button as i32;
532        with_scratch_txt_two(src_ref, dst_ref, |src_ptr, dst_ptr| unsafe {
533            sys::imgui_test_engine_script_item_drag_and_drop(
534                self.script.raw,
535                src_ptr,
536                dst_ptr,
537                button,
538            )
539        });
540        Ok(())
541    }
542
543    pub fn item_drag_with_delta(&mut self, r#ref: &str, dx: f32, dy: f32) -> ImGuiResult<()> {
544        if r#ref.contains('\0') {
545            return Err(ImGuiError::invalid_operation(
546                "item_drag_with_delta contained interior NUL",
547            ));
548        }
549        if !dx.is_finite() || !dy.is_finite() {
550            return Err(ImGuiError::invalid_operation(
551                "item_drag_with_delta requires finite values",
552            ));
553        }
554        with_scratch_txt(r#ref, |ptr| unsafe {
555            sys::imgui_test_engine_script_item_drag_with_delta(self.script.raw, ptr, dx, dy)
556        });
557        Ok(())
558    }
559
560    pub fn scroll_to_x(&mut self, r#ref: &str, scroll_x: f32) -> ImGuiResult<()> {
561        if r#ref.contains('\0') {
562            return Err(ImGuiError::invalid_operation(
563                "scroll_to_x contained interior NUL",
564            ));
565        }
566        if !scroll_x.is_finite() {
567            return Err(ImGuiError::invalid_operation(
568                "scroll_to_x requires a finite value",
569            ));
570        }
571        with_scratch_txt(r#ref, |ptr| unsafe {
572            sys::imgui_test_engine_script_scroll_to_x(self.script.raw, ptr, scroll_x)
573        });
574        Ok(())
575    }
576
577    pub fn scroll_to_y(&mut self, r#ref: &str, scroll_y: f32) -> ImGuiResult<()> {
578        if r#ref.contains('\0') {
579            return Err(ImGuiError::invalid_operation(
580                "scroll_to_y contained interior NUL",
581            ));
582        }
583        if !scroll_y.is_finite() {
584            return Err(ImGuiError::invalid_operation(
585                "scroll_to_y requires a finite value",
586            ));
587        }
588        with_scratch_txt(r#ref, |ptr| unsafe {
589            sys::imgui_test_engine_script_scroll_to_y(self.script.raw, ptr, scroll_y)
590        });
591        Ok(())
592    }
593
594    pub fn scroll_to_pos_x(&mut self, window_ref: &str, pos_x: f32) -> ImGuiResult<()> {
595        if window_ref.contains('\0') {
596            return Err(ImGuiError::invalid_operation(
597                "scroll_to_pos_x window_ref contained interior NUL",
598            ));
599        }
600        if !pos_x.is_finite() {
601            return Err(ImGuiError::invalid_operation(
602                "scroll_to_pos_x requires a finite value",
603            ));
604        }
605        with_scratch_txt(window_ref, |ptr| unsafe {
606            sys::imgui_test_engine_script_scroll_to_pos_x(self.script.raw, ptr, pos_x)
607        });
608        Ok(())
609    }
610
611    pub fn scroll_to_pos_y(&mut self, window_ref: &str, pos_y: f32) -> ImGuiResult<()> {
612        if window_ref.contains('\0') {
613            return Err(ImGuiError::invalid_operation(
614                "scroll_to_pos_y window_ref contained interior NUL",
615            ));
616        }
617        if !pos_y.is_finite() {
618            return Err(ImGuiError::invalid_operation(
619                "scroll_to_pos_y requires a finite value",
620            ));
621        }
622        with_scratch_txt(window_ref, |ptr| unsafe {
623            sys::imgui_test_engine_script_scroll_to_pos_y(self.script.raw, ptr, pos_y)
624        });
625        Ok(())
626    }
627
628    pub fn scroll_to_item_x(&mut self, r#ref: &str) -> ImGuiResult<()> {
629        if r#ref.contains('\0') {
630            return Err(ImGuiError::invalid_operation(
631                "scroll_to_item_x contained interior NUL",
632            ));
633        }
634        with_scratch_txt(r#ref, |ptr| unsafe {
635            sys::imgui_test_engine_script_scroll_to_item_x(self.script.raw, ptr)
636        });
637        Ok(())
638    }
639
640    pub fn scroll_to_item_y(&mut self, r#ref: &str) -> ImGuiResult<()> {
641        if r#ref.contains('\0') {
642            return Err(ImGuiError::invalid_operation(
643                "scroll_to_item_y contained interior NUL",
644            ));
645        }
646        with_scratch_txt(r#ref, |ptr| unsafe {
647            sys::imgui_test_engine_script_scroll_to_item_y(self.script.raw, ptr)
648        });
649        Ok(())
650    }
651
652    pub fn scroll_to_top(&mut self, r#ref: &str) -> ImGuiResult<()> {
653        if r#ref.contains('\0') {
654            return Err(ImGuiError::invalid_operation(
655                "scroll_to_top contained interior NUL",
656            ));
657        }
658        with_scratch_txt(r#ref, |ptr| unsafe {
659            sys::imgui_test_engine_script_scroll_to_top(self.script.raw, ptr)
660        });
661        Ok(())
662    }
663
664    pub fn scroll_to_bottom(&mut self, r#ref: &str) -> ImGuiResult<()> {
665        if r#ref.contains('\0') {
666            return Err(ImGuiError::invalid_operation(
667                "scroll_to_bottom contained interior NUL",
668            ));
669        }
670        with_scratch_txt(r#ref, |ptr| unsafe {
671            sys::imgui_test_engine_script_scroll_to_bottom(self.script.raw, ptr)
672        });
673        Ok(())
674    }
675
676    pub fn tab_close(&mut self, r#ref: &str) -> ImGuiResult<()> {
677        if r#ref.contains('\0') {
678            return Err(ImGuiError::invalid_operation(
679                "tab_close contained interior NUL",
680            ));
681        }
682        with_scratch_txt(r#ref, |ptr| unsafe {
683            sys::imgui_test_engine_script_tab_close(self.script.raw, ptr)
684        });
685        Ok(())
686    }
687
688    pub fn combo_click(&mut self, r#ref: &str) -> ImGuiResult<()> {
689        if r#ref.contains('\0') {
690            return Err(ImGuiError::invalid_operation(
691                "combo_click contained interior NUL",
692            ));
693        }
694        with_scratch_txt(r#ref, |ptr| unsafe {
695            sys::imgui_test_engine_script_combo_click(self.script.raw, ptr)
696        });
697        Ok(())
698    }
699
700    pub fn combo_click_all(&mut self, r#ref: &str) -> ImGuiResult<()> {
701        if r#ref.contains('\0') {
702            return Err(ImGuiError::invalid_operation(
703                "combo_click_all contained interior NUL",
704            ));
705        }
706        with_scratch_txt(r#ref, |ptr| unsafe {
707            sys::imgui_test_engine_script_combo_click_all(self.script.raw, ptr)
708        });
709        Ok(())
710    }
711
712    pub fn item_open_all(&mut self, parent_ref: &str, depth: i32, passes: i32) -> ImGuiResult<()> {
713        if parent_ref.contains('\0') {
714            return Err(ImGuiError::invalid_operation(
715                "item_open_all parent_ref contained interior NUL",
716            ));
717        }
718        with_scratch_txt(parent_ref, |ptr| unsafe {
719            sys::imgui_test_engine_script_item_open_all(self.script.raw, ptr, depth, passes)
720        });
721        Ok(())
722    }
723
724    pub fn item_close_all(&mut self, parent_ref: &str, depth: i32, passes: i32) -> ImGuiResult<()> {
725        if parent_ref.contains('\0') {
726            return Err(ImGuiError::invalid_operation(
727                "item_close_all parent_ref contained interior NUL",
728            ));
729        }
730        with_scratch_txt(parent_ref, |ptr| unsafe {
731            sys::imgui_test_engine_script_item_close_all(self.script.raw, ptr, depth, passes)
732        });
733        Ok(())
734    }
735
736    pub fn table_click_header(
737        &mut self,
738        table_ref: &str,
739        label: &str,
740        key_mods: KeyMods,
741    ) -> ImGuiResult<()> {
742        if table_ref.contains('\0') {
743            return Err(ImGuiError::invalid_operation(
744                "table_click_header table_ref contained interior NUL",
745            ));
746        }
747        if label.contains('\0') {
748            return Err(ImGuiError::invalid_operation(
749                "table_click_header label contained interior NUL",
750            ));
751        }
752        let key_mods = key_mods.bits();
753        with_scratch_txt_two(table_ref, label, |table_ptr, label_ptr| unsafe {
754            sys::imgui_test_engine_script_table_click_header(
755                self.script.raw,
756                table_ptr,
757                label_ptr,
758                key_mods,
759            )
760        });
761        Ok(())
762    }
763
764    pub fn table_open_context_menu(&mut self, table_ref: &str, column_n: i32) -> ImGuiResult<()> {
765        if table_ref.contains('\0') {
766            return Err(ImGuiError::invalid_operation(
767                "table_open_context_menu table_ref contained interior NUL",
768            ));
769        }
770        with_scratch_txt(table_ref, |ptr| unsafe {
771            sys::imgui_test_engine_script_table_open_context_menu(self.script.raw, ptr, column_n)
772        });
773        Ok(())
774    }
775
776    pub fn table_set_column_enabled(
777        &mut self,
778        table_ref: &str,
779        column_n: i32,
780        enabled: bool,
781    ) -> ImGuiResult<()> {
782        if table_ref.contains('\0') {
783            return Err(ImGuiError::invalid_operation(
784                "table_set_column_enabled table_ref contained interior NUL",
785            ));
786        }
787        with_scratch_txt(table_ref, |ptr| unsafe {
788            sys::imgui_test_engine_script_table_set_column_enabled(
789                self.script.raw,
790                ptr,
791                column_n,
792                enabled,
793            )
794        });
795        Ok(())
796    }
797
798    pub fn table_set_column_enabled_by_label(
799        &mut self,
800        table_ref: &str,
801        label: &str,
802        enabled: bool,
803    ) -> ImGuiResult<()> {
804        if table_ref.contains('\0') {
805            return Err(ImGuiError::invalid_operation(
806                "table_set_column_enabled_by_label table_ref contained interior NUL",
807            ));
808        }
809        if label.contains('\0') {
810            return Err(ImGuiError::invalid_operation(
811                "table_set_column_enabled_by_label label contained interior NUL",
812            ));
813        }
814        with_scratch_txt_two(table_ref, label, |table_ptr, label_ptr| unsafe {
815            sys::imgui_test_engine_script_table_set_column_enabled_by_label(
816                self.script.raw,
817                table_ptr,
818                label_ptr,
819                enabled,
820            )
821        });
822        Ok(())
823    }
824
825    pub fn table_resize_column(
826        &mut self,
827        table_ref: &str,
828        column_n: i32,
829        width: f32,
830    ) -> ImGuiResult<()> {
831        if table_ref.contains('\0') {
832            return Err(ImGuiError::invalid_operation(
833                "table_resize_column table_ref contained interior NUL",
834            ));
835        }
836        if !width.is_finite() || width < 0.0 {
837            return Err(ImGuiError::invalid_operation(
838                "table_resize_column requires a finite non-negative value",
839            ));
840        }
841        with_scratch_txt(table_ref, |ptr| unsafe {
842            sys::imgui_test_engine_script_table_resize_column(self.script.raw, ptr, column_n, width)
843        });
844        Ok(())
845    }
846
847    pub fn menu_click(&mut self, r#ref: &str) -> ImGuiResult<()> {
848        if r#ref.contains('\0') {
849            return Err(ImGuiError::invalid_operation(
850                "menu_click contained interior NUL",
851            ));
852        }
853        with_scratch_txt(r#ref, |ptr| unsafe {
854            sys::imgui_test_engine_script_menu_click(self.script.raw, ptr)
855        });
856        Ok(())
857    }
858
859    pub fn menu_check(&mut self, r#ref: &str) -> ImGuiResult<()> {
860        if r#ref.contains('\0') {
861            return Err(ImGuiError::invalid_operation(
862                "menu_check contained interior NUL",
863            ));
864        }
865        with_scratch_txt(r#ref, |ptr| unsafe {
866            sys::imgui_test_engine_script_menu_check(self.script.raw, ptr)
867        });
868        Ok(())
869    }
870
871    pub fn menu_uncheck(&mut self, r#ref: &str) -> ImGuiResult<()> {
872        if r#ref.contains('\0') {
873            return Err(ImGuiError::invalid_operation(
874                "menu_uncheck contained interior NUL",
875            ));
876        }
877        with_scratch_txt(r#ref, |ptr| unsafe {
878            sys::imgui_test_engine_script_menu_uncheck(self.script.raw, ptr)
879        });
880        Ok(())
881    }
882
883    pub fn menu_check_all(&mut self, parent_ref: &str) -> ImGuiResult<()> {
884        if parent_ref.contains('\0') {
885            return Err(ImGuiError::invalid_operation(
886                "menu_check_all parent_ref contained interior NUL",
887            ));
888        }
889        with_scratch_txt(parent_ref, |ptr| unsafe {
890            sys::imgui_test_engine_script_menu_check_all(self.script.raw, ptr)
891        });
892        Ok(())
893    }
894
895    pub fn menu_uncheck_all(&mut self, parent_ref: &str) -> ImGuiResult<()> {
896        if parent_ref.contains('\0') {
897            return Err(ImGuiError::invalid_operation(
898                "menu_uncheck_all parent_ref contained interior NUL",
899            ));
900        }
901        with_scratch_txt(parent_ref, |ptr| unsafe {
902            sys::imgui_test_engine_script_menu_uncheck_all(self.script.raw, ptr)
903        });
904        Ok(())
905    }
906
907    pub fn set_input_mode(&mut self, mode: InputMode) {
908        unsafe { sys::imgui_test_engine_script_set_input_mode(self.script.raw, mode as i32) };
909    }
910
911    pub fn nav_move_to(&mut self, r#ref: &str) -> ImGuiResult<()> {
912        if r#ref.contains('\0') {
913            return Err(ImGuiError::invalid_operation(
914                "nav_move_to contained interior NUL",
915            ));
916        }
917        with_scratch_txt(r#ref, |ptr| unsafe {
918            sys::imgui_test_engine_script_nav_move_to(self.script.raw, ptr)
919        });
920        Ok(())
921    }
922
923    pub fn nav_activate(&mut self) {
924        unsafe { sys::imgui_test_engine_script_nav_activate(self.script.raw) };
925    }
926
927    pub fn nav_input(&mut self) {
928        unsafe { sys::imgui_test_engine_script_nav_input(self.script.raw) };
929    }
930
931    pub fn window_close(&mut self, window_ref: &str) -> ImGuiResult<()> {
932        if window_ref.contains('\0') {
933            return Err(ImGuiError::invalid_operation(
934                "window_close window_ref contained interior NUL",
935            ));
936        }
937        with_scratch_txt(window_ref, |ptr| unsafe {
938            sys::imgui_test_engine_script_window_close(self.script.raw, ptr)
939        });
940        Ok(())
941    }
942
943    pub fn window_collapse(&mut self, window_ref: &str, collapsed: bool) -> ImGuiResult<()> {
944        if window_ref.contains('\0') {
945            return Err(ImGuiError::invalid_operation(
946                "window_collapse window_ref contained interior NUL",
947            ));
948        }
949        with_scratch_txt(window_ref, |ptr| unsafe {
950            sys::imgui_test_engine_script_window_collapse(self.script.raw, ptr, collapsed)
951        });
952        Ok(())
953    }
954
955    pub fn window_focus(&mut self, window_ref: &str) -> ImGuiResult<()> {
956        if window_ref.contains('\0') {
957            return Err(ImGuiError::invalid_operation(
958                "window_focus window_ref contained interior NUL",
959            ));
960        }
961        with_scratch_txt(window_ref, |ptr| unsafe {
962            sys::imgui_test_engine_script_window_focus(self.script.raw, ptr)
963        });
964        Ok(())
965    }
966
967    pub fn window_bring_to_front(&mut self, window_ref: &str) -> ImGuiResult<()> {
968        if window_ref.contains('\0') {
969            return Err(ImGuiError::invalid_operation(
970                "window_bring_to_front window_ref contained interior NUL",
971            ));
972        }
973        with_scratch_txt(window_ref, |ptr| unsafe {
974            sys::imgui_test_engine_script_window_bring_to_front(self.script.raw, ptr)
975        });
976        Ok(())
977    }
978
979    pub fn window_move(&mut self, window_ref: &str, x: f32, y: f32) -> ImGuiResult<()> {
980        if window_ref.contains('\0') {
981            return Err(ImGuiError::invalid_operation(
982                "window_move window_ref contained interior NUL",
983            ));
984        }
985        if !x.is_finite() || !y.is_finite() {
986            return Err(ImGuiError::invalid_operation(
987                "window_move requires finite values",
988            ));
989        }
990        with_scratch_txt(window_ref, |ptr| unsafe {
991            sys::imgui_test_engine_script_window_move(self.script.raw, ptr, x, y)
992        });
993        Ok(())
994    }
995
996    pub fn window_resize(&mut self, window_ref: &str, w: f32, h: f32) -> ImGuiResult<()> {
997        if window_ref.contains('\0') {
998            return Err(ImGuiError::invalid_operation(
999                "window_resize window_ref contained interior NUL",
1000            ));
1001        }
1002        if !w.is_finite() || !h.is_finite() || w < 0.0 || h < 0.0 {
1003            return Err(ImGuiError::invalid_operation(
1004                "window_resize requires finite non-negative values",
1005            ));
1006        }
1007        with_scratch_txt(window_ref, |ptr| unsafe {
1008            sys::imgui_test_engine_script_window_resize(self.script.raw, ptr, w, h)
1009        });
1010        Ok(())
1011    }
1012
1013    pub fn sleep_seconds(&mut self, seconds: f32) -> ImGuiResult<()> {
1014        if !seconds.is_finite() || seconds < 0.0 {
1015            return Err(ImGuiError::invalid_operation(
1016                "sleep_seconds requires a finite non-negative value",
1017            ));
1018        }
1019        unsafe { sys::imgui_test_engine_script_sleep(self.script.raw, seconds) };
1020        Ok(())
1021    }
1022
1023    pub fn assert_item_exists(&mut self, r#ref: &str) -> ImGuiResult<()> {
1024        if r#ref.contains('\0') {
1025            return Err(ImGuiError::invalid_operation(
1026                "assert_item_exists contained interior NUL",
1027            ));
1028        }
1029        with_scratch_txt(r#ref, |ptr| unsafe {
1030            sys::imgui_test_engine_script_assert_item_exists(self.script.raw, ptr)
1031        });
1032        Ok(())
1033    }
1034
1035    pub fn assert_item_visible(&mut self, r#ref: &str) -> ImGuiResult<()> {
1036        if r#ref.contains('\0') {
1037            return Err(ImGuiError::invalid_operation(
1038                "assert_item_visible contained interior NUL",
1039            ));
1040        }
1041        with_scratch_txt(r#ref, |ptr| unsafe {
1042            sys::imgui_test_engine_script_assert_item_visible(self.script.raw, ptr)
1043        });
1044        Ok(())
1045    }
1046
1047    pub fn assert_item_read_int_eq(&mut self, r#ref: &str, expected: i32) -> ImGuiResult<()> {
1048        if r#ref.contains('\0') {
1049            return Err(ImGuiError::invalid_operation(
1050                "assert_item_read_int_eq contained interior NUL",
1051            ));
1052        }
1053        with_scratch_txt(r#ref, |ptr| unsafe {
1054            sys::imgui_test_engine_script_assert_item_read_int_eq(self.script.raw, ptr, expected)
1055        });
1056        Ok(())
1057    }
1058
1059    pub fn assert_item_read_str_eq(&mut self, r#ref: &str, expected: &str) -> ImGuiResult<()> {
1060        if r#ref.contains('\0') || expected.contains('\0') {
1061            return Err(ImGuiError::invalid_operation(
1062                "assert_item_read_str_eq contained interior NUL",
1063            ));
1064        }
1065        with_scratch_txt_two(r#ref, expected, |ref_ptr, expected_ptr| unsafe {
1066            sys::imgui_test_engine_script_assert_item_read_str_eq(
1067                self.script.raw,
1068                ref_ptr,
1069                expected_ptr,
1070            )
1071        });
1072        Ok(())
1073    }
1074
1075    pub fn assert_item_read_float_eq(
1076        &mut self,
1077        r#ref: &str,
1078        expected: f32,
1079        epsilon: f32,
1080    ) -> ImGuiResult<()> {
1081        if r#ref.contains('\0') {
1082            return Err(ImGuiError::invalid_operation(
1083                "assert_item_read_float_eq contained interior NUL",
1084            ));
1085        }
1086        if !expected.is_finite() || !epsilon.is_finite() || epsilon < 0.0 {
1087            return Err(ImGuiError::invalid_operation(
1088                "assert_item_read_float_eq requires finite expected and finite non-negative epsilon",
1089            ));
1090        }
1091        with_scratch_txt(r#ref, |ptr| unsafe {
1092            sys::imgui_test_engine_script_assert_item_read_float_eq(
1093                self.script.raw,
1094                ptr,
1095                expected,
1096                epsilon,
1097            )
1098        });
1099        Ok(())
1100    }
1101
1102    pub fn assert_item_checked(&mut self, r#ref: &str) -> ImGuiResult<()> {
1103        if r#ref.contains('\0') {
1104            return Err(ImGuiError::invalid_operation(
1105                "assert_item_checked contained interior NUL",
1106            ));
1107        }
1108        with_scratch_txt(r#ref, |ptr| unsafe {
1109            sys::imgui_test_engine_script_assert_item_checked(self.script.raw, ptr)
1110        });
1111        Ok(())
1112    }
1113
1114    pub fn assert_item_opened(&mut self, r#ref: &str) -> ImGuiResult<()> {
1115        if r#ref.contains('\0') {
1116            return Err(ImGuiError::invalid_operation(
1117                "assert_item_opened contained interior NUL",
1118            ));
1119        }
1120        with_scratch_txt(r#ref, |ptr| unsafe {
1121            sys::imgui_test_engine_script_assert_item_opened(self.script.raw, ptr)
1122        });
1123        Ok(())
1124    }
1125
1126    pub fn wait_for_item(&mut self, r#ref: &str, max_frames: i32) -> ImGuiResult<()> {
1127        if r#ref.contains('\0') {
1128            return Err(ImGuiError::invalid_operation(
1129                "wait_for_item contained interior NUL",
1130            ));
1131        }
1132        if max_frames < 1 {
1133            return Err(ImGuiError::invalid_operation(
1134                "wait_for_item max_frames must be >= 1",
1135            ));
1136        }
1137        with_scratch_txt(r#ref, |ptr| unsafe {
1138            sys::imgui_test_engine_script_wait_for_item(self.script.raw, ptr, max_frames)
1139        });
1140        Ok(())
1141    }
1142
1143    pub fn wait_for_item_visible(&mut self, r#ref: &str, max_frames: i32) -> ImGuiResult<()> {
1144        if r#ref.contains('\0') {
1145            return Err(ImGuiError::invalid_operation(
1146                "wait_for_item_visible contained interior NUL",
1147            ));
1148        }
1149        if max_frames < 1 {
1150            return Err(ImGuiError::invalid_operation(
1151                "wait_for_item_visible max_frames must be >= 1",
1152            ));
1153        }
1154        with_scratch_txt(r#ref, |ptr| unsafe {
1155            sys::imgui_test_engine_script_wait_for_item_visible(self.script.raw, ptr, max_frames)
1156        });
1157        Ok(())
1158    }
1159
1160    pub fn wait_for_item_checked(&mut self, r#ref: &str, max_frames: i32) -> ImGuiResult<()> {
1161        if r#ref.contains('\0') {
1162            return Err(ImGuiError::invalid_operation(
1163                "wait_for_item_checked contained interior NUL",
1164            ));
1165        }
1166        if max_frames < 1 {
1167            return Err(ImGuiError::invalid_operation(
1168                "wait_for_item_checked max_frames must be >= 1",
1169            ));
1170        }
1171        with_scratch_txt(r#ref, |ptr| unsafe {
1172            sys::imgui_test_engine_script_wait_for_item_checked(self.script.raw, ptr, max_frames)
1173        });
1174        Ok(())
1175    }
1176
1177    pub fn wait_for_item_opened(&mut self, r#ref: &str, max_frames: i32) -> ImGuiResult<()> {
1178        if r#ref.contains('\0') {
1179            return Err(ImGuiError::invalid_operation(
1180                "wait_for_item_opened contained interior NUL",
1181            ));
1182        }
1183        if max_frames < 1 {
1184            return Err(ImGuiError::invalid_operation(
1185                "wait_for_item_opened max_frames must be >= 1",
1186            ));
1187        }
1188        with_scratch_txt(r#ref, |ptr| unsafe {
1189            sys::imgui_test_engine_script_wait_for_item_opened(self.script.raw, ptr, max_frames)
1190        });
1191        Ok(())
1192    }
1193
1194    pub fn input_text_replace(
1195        &mut self,
1196        r#ref: &str,
1197        text: &str,
1198        submit_enter: bool,
1199    ) -> ImGuiResult<()> {
1200        self.item_click(r#ref)?;
1201        if submit_enter {
1202            self.key_chars_replace_enter(text)?;
1203        } else {
1204            self.key_chars_replace(text)?;
1205        }
1206        Ok(())
1207    }
1208
1209    pub fn yield_frames(&mut self, frames: i32) {
1210        unsafe { sys::imgui_test_engine_script_yield(self.script.raw, frames) };
1211    }
1212}
1213
1214impl TestEngine {
1215    /// Creates a new test engine context.
1216    pub fn try_create() -> ImGuiResult<Self> {
1217        let raw = unsafe { sys::imgui_test_engine_create_context() };
1218        if raw.is_null() {
1219            return Err(ImGuiError::context_creation(
1220                "imgui_test_engine_create_context returned null",
1221            ));
1222        }
1223        Ok(Self {
1224            raw,
1225            bound_imgui_ctx_raw: None,
1226            bound_imgui_alive: None,
1227            _not_send_sync: PhantomData,
1228        })
1229    }
1230
1231    /// Creates a new test engine context.
1232    ///
1233    /// # Panics
1234    /// Panics if the underlying context creation fails.
1235    pub fn create() -> Self {
1236        Self::try_create().expect("Failed to create Dear ImGui Test Engine context")
1237    }
1238
1239    pub fn as_raw(&self) -> *mut sys::ImGuiTestEngine {
1240        self.raw
1241    }
1242
1243    pub fn is_bound(&self) -> bool {
1244        unsafe { sys::imgui_test_engine_is_bound(self.raw) }
1245    }
1246
1247    pub fn is_started(&self) -> bool {
1248        unsafe { sys::imgui_test_engine_is_started(self.raw) }
1249    }
1250
1251    fn ui_context_target(&self) -> *mut dear_imgui_rs::sys::ImGuiContext {
1252        unsafe { sys::imgui_test_engine_get_ui_context_target(self.raw) }
1253    }
1254
1255    /// Tries to start (bind) the test engine to an ImGui context.
1256    ///
1257    /// Calling this multiple times with the same context is a no-op.
1258    ///
1259    /// Returns an error if the engine is already bound to a different context, or if the engine
1260    /// was previously stopped but is still bound (call `shutdown()` to detach first).
1261    pub fn try_start(&mut self, imgui_ctx: &Context) -> ImGuiResult<()> {
1262        let ctx = imgui_ctx.as_raw();
1263        if ctx.is_null() {
1264            return Err(ImGuiError::invalid_operation(
1265                "TestEngine::try_start() called with a null ImGui context",
1266            ));
1267        }
1268
1269        let bound = self.ui_context_target();
1270        if !bound.is_null() {
1271            if bound == ctx {
1272                if self.is_started() {
1273                    return Ok(());
1274                }
1275                return Err(ImGuiError::invalid_operation(
1276                    "TestEngine::try_start() called but the engine is already bound (and not started). \
1277                     Call TestEngine::shutdown() to detach, then start again.",
1278                ));
1279            }
1280            return Err(ImGuiError::invalid_operation(
1281                "TestEngine::try_start() called while already bound to a different ImGui context",
1282            ));
1283        }
1284
1285        unsafe { sys::imgui_test_engine_start(self.raw, ctx) };
1286        self.bound_imgui_ctx_raw = Some(ctx);
1287        self.bound_imgui_alive = Some(imgui_ctx.alive_token());
1288        Ok(())
1289    }
1290
1291    /// Starts (binds) the test engine to an ImGui context.
1292    ///
1293    /// Calling this multiple times with the same context is a no-op.
1294    ///
1295    /// # Panics
1296    /// Panics if called while already started with a different ImGui context.
1297    pub fn start(&mut self, imgui_ctx: &Context) {
1298        self.try_start(imgui_ctx)
1299            .expect("Failed to start Dear ImGui Test Engine context");
1300    }
1301
1302    /// Stops the test coroutine and exports results, but keeps the engine bound to the ImGui context.
1303    pub fn stop(&mut self) {
1304        unsafe { sys::imgui_test_engine_stop(self.raw) };
1305    }
1306
1307    /// Stops (if needed) and detaches the engine from the bound ImGui context.
1308    ///
1309    /// This is the most ergonomic shutdown path for Rust applications: it avoids relying on drop order
1310    /// between `Context` and `TestEngine`.
1311    pub fn shutdown(&mut self) {
1312        if let Some(alive) = &self.bound_imgui_alive {
1313            assert!(
1314                alive.is_alive(),
1315                "dear-imgui-test-engine: ImGui context has been dropped"
1316            );
1317        }
1318        unsafe { sys::imgui_test_engine_unbind(self.raw) };
1319        self.bound_imgui_ctx_raw = None;
1320        self.bound_imgui_alive = None;
1321    }
1322
1323    pub fn post_swap(&mut self) {
1324        unsafe { sys::imgui_test_engine_post_swap(self.raw) };
1325    }
1326
1327    pub fn show_windows(&mut self, _ui: &Ui, opened: Option<&mut bool>) {
1328        let ptr = opened.map_or(std::ptr::null_mut(), |b| b as *mut bool);
1329        unsafe { sys::imgui_test_engine_show_windows(self.raw, ptr) };
1330    }
1331
1332    /// Registers a small set of built-in demo tests (useful to validate integration).
1333    pub fn register_default_tests(&mut self) {
1334        unsafe { sys::imgui_test_engine_register_default_tests(self.raw) };
1335    }
1336
1337    /// Registers a script-driven test.
1338    ///
1339    /// Script tests do not provide a GUI function: they are meant to drive your application's existing UI.
1340    pub fn add_script_test<F>(&mut self, category: &str, name: &str, build: F) -> ImGuiResult<()>
1341    where
1342        F: FnOnce(&mut ScriptTest<'_>) -> ImGuiResult<()>,
1343    {
1344        if category.contains('\0') {
1345            return Err(ImGuiError::invalid_operation(
1346                "add_script_test category contained interior NUL",
1347            ));
1348        }
1349        if name.contains('\0') {
1350            return Err(ImGuiError::invalid_operation(
1351                "add_script_test name contained interior NUL",
1352            ));
1353        }
1354
1355        let mut script = Script::create()?;
1356        build(&mut ScriptTest {
1357            script: &mut script,
1358        })?;
1359        let script_raw = script.into_raw();
1360
1361        with_scratch_txt_two(category, name, |cat_ptr, name_ptr| unsafe {
1362            sys::imgui_test_engine_register_script_test(self.raw, cat_ptr, name_ptr, script_raw)
1363        });
1364
1365        Ok(())
1366    }
1367
1368    pub fn queue_tests(
1369        &mut self,
1370        group: TestGroup,
1371        filter: Option<&str>,
1372        run_flags: RunFlags,
1373    ) -> ImGuiResult<()> {
1374        if let Some(filter) = filter {
1375            if filter.contains('\0') {
1376                return Err(ImGuiError::invalid_operation(
1377                    "queue_tests filter contained interior NUL",
1378                ));
1379            }
1380            with_scratch_txt(filter, |ptr| unsafe {
1381                sys::imgui_test_engine_queue_tests(
1382                    self.raw,
1383                    group as sys::ImGuiTestEngineGroup,
1384                    ptr,
1385                    run_flags.bits() as i32,
1386                )
1387            });
1388            return Ok(());
1389        }
1390
1391        unsafe {
1392            sys::imgui_test_engine_queue_tests(
1393                self.raw,
1394                group as sys::ImGuiTestEngineGroup,
1395                std::ptr::null(),
1396                run_flags.bits() as i32,
1397            )
1398        };
1399        Ok(())
1400    }
1401
1402    pub fn queue_all_tests(&mut self) {
1403        let _ = self.queue_tests(TestGroup::Tests, None, RunFlags::NONE);
1404    }
1405
1406    /// Returns a best-effort snapshot of test results.
1407    ///
1408    /// Note: upstream asserts if queried while a test is running; our sys shim
1409    /// intentionally avoids aborting and will count `Running` tests as "remaining".
1410    pub fn result_summary(&self) -> ResultSummary {
1411        let mut raw = sys::ImGuiTestEngineResultSummary_c {
1412            CountTested: 0,
1413            CountSuccess: 0,
1414            CountInQueue: 0,
1415        };
1416        unsafe { sys::imgui_test_engine_get_result_summary(self.raw, &mut raw) };
1417        ResultSummary {
1418            count_tested: raw.CountTested,
1419            count_success: raw.CountSuccess,
1420            count_in_queue: raw.CountInQueue,
1421        }
1422    }
1423
1424    pub fn is_test_queue_empty(&self) -> bool {
1425        unsafe { sys::imgui_test_engine_is_test_queue_empty(self.raw) }
1426    }
1427
1428    pub fn is_running_tests(&self) -> bool {
1429        unsafe { sys::imgui_test_engine_is_running_tests(self.raw) }
1430    }
1431
1432    pub fn is_requesting_max_app_speed(&self) -> bool {
1433        unsafe { sys::imgui_test_engine_is_requesting_max_app_speed(self.raw) }
1434    }
1435
1436    pub fn try_abort_engine(&mut self) -> bool {
1437        unsafe { sys::imgui_test_engine_try_abort_engine(self.raw) }
1438    }
1439
1440    pub fn abort_current_test(&mut self) {
1441        unsafe { sys::imgui_test_engine_abort_current_test(self.raw) };
1442    }
1443
1444    pub fn set_run_speed(&mut self, speed: RunSpeed) {
1445        unsafe {
1446            sys::imgui_test_engine_set_run_speed(self.raw, speed as sys::ImGuiTestEngineRunSpeed)
1447        };
1448    }
1449
1450    pub fn set_verbose_level(&mut self, level: VerboseLevel) {
1451        unsafe {
1452            sys::imgui_test_engine_set_verbose_level(
1453                self.raw,
1454                level as sys::ImGuiTestEngineVerboseLevel,
1455            )
1456        };
1457    }
1458
1459    pub fn set_capture_enabled(&mut self, enabled: bool) {
1460        unsafe { sys::imgui_test_engine_set_capture_enabled(self.raw, enabled) };
1461    }
1462
1463    pub fn install_default_crash_handler() {
1464        unsafe { sys::imgui_test_engine_install_default_crash_handler() };
1465    }
1466}
1467
1468impl Drop for TestEngine {
1469    fn drop(&mut self) {
1470        if !self.raw.is_null() {
1471            if let Some(alive) = &self.bound_imgui_alive {
1472                if !alive.is_alive() {
1473                    // Avoid calling into ImGui allocators after the context has been dropped.
1474                    // Best-effort: leak the test engine context instead of risking UB.
1475                    return;
1476                }
1477            }
1478            if self.ui_context_target().is_null() {
1479                self.bound_imgui_ctx_raw = None;
1480                self.bound_imgui_alive = None;
1481            }
1482            if !self.ui_context_target().is_null() {
1483                if let Some(bound) = self.bound_imgui_ctx_raw {
1484                    unsafe {
1485                        let prev = dear_imgui_rs::sys::igGetCurrentContext();
1486                        dear_imgui_rs::sys::igSetCurrentContext(bound);
1487                        sys::imgui_test_engine_unbind(self.raw);
1488                        dear_imgui_rs::sys::igSetCurrentContext(prev);
1489                    }
1490                } else {
1491                    unsafe { sys::imgui_test_engine_unbind(self.raw) };
1492                }
1493            }
1494            unsafe { sys::imgui_test_engine_destroy_context(self.raw) };
1495            self.raw = std::ptr::null_mut();
1496        }
1497    }
1498}