Skip to main content

dear_imgui_winit/
platform.rs

1//! Main platform implementation for Dear ImGui winit backend
2//!
3//! This module contains the core `WinitPlatform` struct and its implementation
4//! for integrating Dear ImGui with winit windowing.
5
6use std::ffi::c_void;
7
8use dear_imgui_rs::{BackendFlags, ConfigFlags, Context};
9use winit::dpi::{LogicalPosition, LogicalSize};
10use winit::event::{Event, WindowEvent};
11use winit::window::{Window, WindowAttributes};
12
13#[cfg(not(target_arch = "wasm32"))]
14use std::time::Instant;
15#[cfg(target_arch = "wasm32")]
16use web_time::Instant;
17
18use crate::cursor::CursorSettings;
19use crate::events;
20
21type SetImeDataCallback = unsafe extern "C" fn(
22    *mut dear_imgui_rs::sys::ImGuiContext,
23    *mut dear_imgui_rs::sys::ImGuiViewport,
24    *mut dear_imgui_rs::sys::ImGuiPlatformImeData,
25);
26
27/// IME hook: Dear ImGui calls this when the text input caret moves. We forward
28/// the position to winit so platforms that support it can position the IME
29/// candidate/composition window near the caret.
30unsafe extern "C" fn imgui_winit_set_ime_data(
31    ctx: *mut dear_imgui_rs::sys::ImGuiContext,
32    viewport: *mut dear_imgui_rs::sys::ImGuiViewport,
33    data: *mut dear_imgui_rs::sys::ImGuiPlatformImeData,
34) {
35    use dear_imgui_rs::sys::{ImGuiPlatformImeData, ImGuiViewport};
36
37    let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe {
38        if viewport.is_null() || data.is_null() {
39            return;
40        }
41
42        // Retrieve the window pointer we stored in Platform_ImeUserData.
43        let pio = platform_io_for_ime_context(ctx);
44        if pio.is_null() {
45            return;
46        }
47
48        let user_data = (*pio).Platform_ImeUserData as *const Window;
49        if user_data.is_null() {
50            return;
51        }
52
53        // Safety: we rely on the application to keep the Window alive while the
54        // ImGui context is active. This matches typical winit usage.
55        let window: &Window = &*user_data;
56        let ime: &ImGuiPlatformImeData = &*data;
57        let vp: &ImGuiViewport = &*viewport;
58
59        // If IME is not visible and not expecting text input, there's nothing to do.
60        if !ime.WantVisible && !ime.WantTextInput {
61            return;
62        }
63
64        // Dear ImGui gives InputPos in the same coordinate space as the viewport's
65        // Pos. Convert to client-area coordinates by subtracting viewport origin.
66        let rel_x = (ime.InputPos.x - vp.Pos.x) as f64;
67        let rel_y = (ime.InputPos.y - vp.Pos.y) as f64;
68
69        let pos = LogicalPosition::new(rel_x, rel_y);
70
71        // Use the reported line height as a reasonable IME region height. Width is
72        // not very important for most IME implementations.
73        let line_h = if ime.InputLineHeight > 0.0 {
74            ime.InputLineHeight as f64
75        } else {
76            16.0
77        };
78        let size = LogicalSize::new(line_h, line_h);
79
80        window.set_ime_cursor_area(pos, size);
81    }));
82    if res.is_err() {
83        eprintln!("dear-imgui-winit: panic in Platform_SetImeDataFn");
84        std::process::abort();
85    }
86}
87
88fn is_winit_set_ime_data(callback: Option<SetImeDataCallback>) -> bool {
89    callback.is_some_and(|callback| {
90        std::ptr::fn_addr_eq(callback, imgui_winit_set_ime_data as SetImeDataCallback)
91    })
92}
93
94unsafe fn platform_io_for_ime_context(
95    ctx: *mut dear_imgui_rs::sys::ImGuiContext,
96) -> *mut dear_imgui_rs::sys::ImGuiPlatformIO {
97    if ctx.is_null() {
98        unsafe { dear_imgui_rs::sys::igGetPlatformIO_Nil() }
99    } else {
100        unsafe { dear_imgui_rs::sys::igGetPlatformIO_ContextPtr(ctx) }
101    }
102}
103
104/// DPI scaling mode for the platform
105#[derive(Copy, Clone, Debug, PartialEq, Default)]
106pub enum HiDpiMode {
107    /// Use the default DPI scaling
108    #[default]
109    Default,
110    /// Use a custom scale factor
111    Locked(f64),
112    /// Round the scale factor to the nearest integer
113    Rounded,
114}
115
116/// Main platform backend for Dear ImGui with winit integration
117pub struct WinitPlatform {
118    hidpi_mode: HiDpiMode,
119    hidpi_factor: f64,
120    cursor_cache: Option<CursorSettings>,
121    ime_enabled: bool,
122    ime_auto_manage: bool,
123    last_frame: Instant,
124}
125
126impl WinitPlatform {
127    /// Create a new winit platform backend
128    ///
129    /// # Example
130    ///
131    /// ```
132    /// use dear_imgui_rs::Context;
133    /// use dear_imgui_winit::WinitPlatform;
134    ///
135    /// let mut imgui_ctx = Context::create();
136    /// let mut platform = WinitPlatform::new(&mut imgui_ctx);
137    /// ```
138    pub fn new(imgui_ctx: &mut Context) -> Self {
139        // Set backend platform name for diagnostics before borrowing Io
140        let _ = imgui_ctx.set_platform_name(Some(format!(
141            "dear-imgui-winit {}",
142            env!("CARGO_PKG_VERSION")
143        )));
144
145        let io = imgui_ctx.io_mut();
146
147        // Set backend flags
148        let mut backend_flags = io.backend_flags();
149        backend_flags.insert(BackendFlags::HAS_MOUSE_CURSORS | BackendFlags::HAS_SET_MOUSE_POS);
150
151        #[cfg(feature = "multi-viewport")]
152        {
153            // Mark that this platform backend is capable of handling viewports.
154            // Note: we intentionally DO NOT enable `ConfigFlags::VIEWPORTS_ENABLE` here.
155            // Multi-viewport is an opt-in feature and should be enabled explicitly via:
156            //
157            //     imgui_ctx.enable_multi_viewport();
158            //
159            // This matches Dear ImGui's guidance and avoids partially-enabled viewport
160            // behavior when the renderer/platform callbacks are not fully wired.
161            backend_flags.insert(BackendFlags::PLATFORM_HAS_VIEWPORTS);
162            // Backend can also report hovered viewport ids via `Io::add_mouse_viewport_event`.
163            backend_flags.insert(BackendFlags::HAS_MOUSE_HOVERED_VIEWPORT);
164            // We keep `HAS_SET_MOUSE_POS` flag: `prepare_render()` will avoid using it
165            // whenever `ConfigFlags::VIEWPORTS_ENABLE` is actually set.
166        }
167
168        io.set_backend_flags(backend_flags);
169
170        Self {
171            hidpi_mode: HiDpiMode::default(),
172            hidpi_factor: 1.0,
173            cursor_cache: None,
174            ime_enabled: false,
175            ime_auto_manage: true,
176            last_frame: Instant::now(),
177        }
178    }
179
180    /// Set the DPI scaling mode
181    pub fn set_hidpi_mode(&mut self, hidpi_mode: HiDpiMode) {
182        self.hidpi_mode = hidpi_mode;
183    }
184
185    /// Enable or disable IME events for the attached window.
186    ///
187    /// Winit does not deliver `WindowEvent::Ime` events unless IME is explicitly
188    /// allowed on the window. When `ime_auto_manage` is enabled (default), the
189    /// backend will override this based on `io.want_text_input()` every frame.
190    /// Use this helper for immediate overrides (e.g. when auto-management is
191    /// disabled or you want to force a specific state for a while).
192    pub fn set_ime_allowed(&mut self, window: &Window, allowed: bool) {
193        window.set_ime_allowed(allowed);
194        self.ime_enabled = allowed;
195    }
196
197    /// Returns whether IME is currently allowed for the attached window.
198    ///
199    /// This reflects the last state set via `set_ime_allowed` or IME
200    /// `WindowEvent::Ime(Enabled/Disabled)` notifications.
201    pub fn ime_enabled(&self) -> bool {
202        self.ime_enabled
203    }
204
205    /// Enable or disable automatic IME management.
206    ///
207    /// When enabled (default), the backend will call `set_ime_allowed` based on
208    /// Dear ImGui's `io.want_text_input()` flag each frame, turning IME on
209    /// while text widgets are active and off otherwise. When disabled, IME
210    /// state is left entirely under application control.
211    pub fn set_ime_auto_management(&mut self, enabled: bool) {
212        self.ime_auto_manage = enabled;
213    }
214
215    /// Get the current DPI scaling factor
216    pub fn hidpi_factor(&self) -> f64 {
217        self.hidpi_factor
218    }
219
220    /// Attach the platform to a window
221    pub fn attach_window(
222        &mut self,
223        window: &Window,
224        hidpi_mode: HiDpiMode,
225        imgui_ctx: &mut Context,
226    ) {
227        self.hidpi_mode = hidpi_mode;
228        self.hidpi_factor = match hidpi_mode {
229            HiDpiMode::Default => window.scale_factor(),
230            HiDpiMode::Locked(factor) => factor,
231            HiDpiMode::Rounded => window.scale_factor().round(),
232        };
233
234        // Convert via winit scale then adapt to our active HiDPI mode
235        let logical_size = window.inner_size().to_logical(window.scale_factor());
236        let logical_size = self.scale_size_from_winit(window, logical_size);
237        let io = imgui_ctx.io_mut();
238
239        io.set_display_size([logical_size.width as f32, logical_size.height as f32]);
240        io.set_display_framebuffer_scale([self.hidpi_factor as f32, self.hidpi_factor as f32]);
241
242        // Enable IME by default so WindowEvent::Ime events and IME composition
243        // are available on desktop platforms. Auto-management (when enabled)
244        // will further refine this for text widgets.
245        self.set_ime_allowed(window, true);
246
247        // Register Dear ImGui -> winit IME bridge so text input widgets can
248        // move the platform IME candidate/composition window near the caret.
249        unsafe {
250            let pio = imgui_ctx.platform_io_mut().as_raw_mut();
251            if !pio.is_null() {
252                if (*pio).Platform_SetImeDataFn.is_none() {
253                    (*pio).Platform_SetImeDataFn = Some(imgui_winit_set_ime_data);
254                    (*pio).Platform_ImeUserData = window as *const Window as *mut c_void;
255                } else if is_winit_set_ime_data((*pio).Platform_SetImeDataFn) {
256                    (*pio).Platform_ImeUserData = window as *const Window as *mut c_void;
257                }
258            }
259        }
260    }
261
262    /// Detach the platform from a window and clear winit-owned IME hooks.
263    ///
264    /// Call this before destroying a window when the Dear ImGui context will outlive it. The
265    /// method only clears the IME callback/userdata pair if it is still owned by this backend and
266    /// still points at `window`.
267    pub fn detach_window(&mut self, window: &Window, imgui_ctx: &mut Context) {
268        window.set_ime_allowed(false);
269        self.ime_enabled = false;
270
271        unsafe {
272            let pio = imgui_ctx.platform_io_mut().as_raw_mut();
273            if pio.is_null() || !is_winit_set_ime_data((*pio).Platform_SetImeDataFn) {
274                return;
275            }
276
277            let window_ptr = window as *const Window as *mut c_void;
278            if (*pio).Platform_ImeUserData == window_ptr {
279                (*pio).Platform_ImeUserData = std::ptr::null_mut();
280                (*pio).Platform_SetImeDataFn = None;
281            }
282        }
283    }
284
285    /// Handle a winit event.
286    ///
287    /// This is the most general entry point: pass the full `Event<T>` from
288    /// your event loop and the backend will dispatch to the appropriate
289    /// handlers. For `ApplicationHandler::window_event`, where you already
290    /// receive a `WindowEvent` for a specific window, you can use
291    /// `handle_window_event` instead and avoid constructing a synthetic
292    /// `Event::WindowEvent`.
293    pub fn handle_event<T>(
294        &mut self,
295        imgui_ctx: &mut Context,
296        window: &Window,
297        event: &Event<T>,
298    ) -> bool {
299        match event {
300            Event::WindowEvent { event, .. } => {
301                self.handle_window_event_internal(imgui_ctx, window, event)
302            }
303            Event::DeviceEvent { event, .. } => {
304                events::handle_device_event(event);
305                false
306            }
307            _ => false,
308        }
309    }
310
311    /// Handle a single window event for a given window.
312    ///
313    /// This is a convenience wrapper for frameworks that already route
314    /// window-local events, such as winit's `ApplicationHandler::window_event`,
315    /// and don't need to build a full `Event::WindowEvent` value.
316    pub fn handle_window_event(
317        &mut self,
318        imgui_ctx: &mut Context,
319        window: &Window,
320        event: &WindowEvent,
321    ) -> bool {
322        self.handle_window_event_internal(imgui_ctx, window, event)
323    }
324
325    /// Internal implementation for window event handling.
326    fn handle_window_event_internal(
327        &mut self,
328        imgui_ctx: &mut Context,
329        window: &Window,
330        event: &WindowEvent,
331    ) -> bool {
332        match event {
333            WindowEvent::Resized(physical_size) => {
334                let logical_size = physical_size.to_logical(window.scale_factor());
335                let logical_size = self.scale_size_from_winit(window, logical_size);
336                imgui_ctx
337                    .io_mut()
338                    .set_display_size([logical_size.width as f32, logical_size.height as f32]);
339                false
340            }
341            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
342                let new_hidpi = match self.hidpi_mode {
343                    HiDpiMode::Default => *scale_factor,
344                    HiDpiMode::Locked(factor) => factor,
345                    HiDpiMode::Rounded => scale_factor.round(),
346                };
347                // Adjust mouse position proportionally when DPI factor changes
348                {
349                    let io = imgui_ctx.io_mut();
350                    let mouse = io.mouse_pos();
351                    if mouse[0].is_finite() && mouse[1].is_finite() && self.hidpi_factor > 0.0 {
352                        let scale = (new_hidpi / self.hidpi_factor) as f32;
353                        io.set_mouse_pos([mouse[0] * scale, mouse[1] * scale]);
354                    }
355                }
356                self.hidpi_factor = new_hidpi;
357
358                let logical_size = window.inner_size().to_logical(window.scale_factor());
359                let logical_size = self.scale_size_from_winit(window, logical_size);
360                let io = imgui_ctx.io_mut();
361                io.set_display_size([logical_size.width as f32, logical_size.height as f32]);
362                io.set_display_framebuffer_scale([
363                    self.hidpi_factor as f32,
364                    self.hidpi_factor as f32,
365                ]);
366                false
367            }
368            WindowEvent::KeyboardInput { event, .. } => {
369                events::handle_keyboard_input(event, imgui_ctx)
370            }
371            WindowEvent::CursorMoved { position, .. } => {
372                // With multi-viewports enabled, feed absolute/screen coordinates like upstream backends
373                #[cfg(feature = "multi-viewport")]
374                {
375                    if imgui_ctx
376                        .io()
377                        .config_flags()
378                        .contains(dear_imgui_rs::ConfigFlags::VIEWPORTS_ENABLE)
379                    {
380                        // Main window always maps to the main Dear ImGui viewport.
381                        let main_viewport_id = imgui_ctx.main_viewport().id();
382                        imgui_ctx
383                            .io_mut()
384                            .add_mouse_viewport_event(main_viewport_id.into());
385                        // Feed absolute/screen coordinates in logical pixels, matching io.DisplaySize.
386                        let scale = window.scale_factor();
387                        let pos_logical = position.to_logical::<f64>(scale);
388                        if let Ok(base_phys) = window.inner_position() {
389                            let base_logical = base_phys.to_logical::<f64>(scale);
390                            let sx = base_logical.x + pos_logical.x;
391                            let sy = base_logical.y + pos_logical.y;
392                            return events::handle_cursor_moved([sx, sy], imgui_ctx);
393                        } else if let Ok(base_phys) = window.outer_position() {
394                            let base_logical = base_phys.to_logical::<f64>(scale);
395                            let sx = base_logical.x + pos_logical.x;
396                            let sy = base_logical.y + pos_logical.y;
397                            return events::handle_cursor_moved([sx, sy], imgui_ctx);
398                        }
399                    }
400                }
401                // Fallback: local logical coordinates
402                let position = position.to_logical(window.scale_factor());
403                let position = self.scale_pos_from_winit(window, position);
404                events::handle_cursor_moved([position.x, position.y], imgui_ctx)
405            }
406            WindowEvent::MouseInput { button, state, .. } => {
407                events::handle_mouse_button(*button, *state, imgui_ctx)
408            }
409            WindowEvent::MouseWheel { delta, .. } => events::handle_mouse_wheel(*delta, imgui_ctx),
410            // When cursor leaves the window, tell ImGui the mouse is unavailable so
411            // software cursor (if enabled) won’t be drawn at the last position.
412            WindowEvent::CursorLeft { .. } => {
413                {
414                    let io = imgui_ctx.io_mut();
415                    io.add_mouse_pos_event([-f32::MAX, -f32::MAX]);
416                    // No Dear ImGui viewport is hovered anymore.
417                    io.add_mouse_viewport_event(dear_imgui_rs::Id::default());
418                }
419                false
420            }
421            WindowEvent::ModifiersChanged(modifiers) => {
422                events::handle_modifiers_changed(modifiers, imgui_ctx);
423                false
424            }
425            WindowEvent::Ime(ime) => {
426                events::handle_ime_event(ime, imgui_ctx);
427                // Track IME enabled/disabled state based on winit notifications.
428                self.ime_enabled = !matches!(ime, winit::event::Ime::Disabled);
429                imgui_ctx.io().want_capture_keyboard()
430            }
431            WindowEvent::Touch(touch) => {
432                events::handle_touch_event(touch, window, imgui_ctx);
433                imgui_ctx.io().want_capture_mouse()
434            }
435            WindowEvent::Focused(focused) => events::handle_focused(*focused, imgui_ctx),
436            _ => false,
437        }
438    }
439
440    /// Prepare for rendering - should be called before Dear ImGui rendering
441    pub fn prepare_render(&mut self, imgui_ctx: &mut Context, window: &Window) {
442        let now = Instant::now();
443        let delta = now - self.last_frame;
444        let delta_s = delta.as_secs() as f32 + delta.subsec_nanos() as f32 / 1_000_000_000.0;
445        self.last_frame = now;
446
447        imgui_ctx.io_mut().set_delta_time(delta_s);
448
449        // In multi-viewport mode, keep DisplaySize/FramebufferScale in sync every frame.
450        // This matches upstream backends (SDL/GLFW) and avoids stale or spurious DPI
451        // changes affecting the main viewport after platform windows are moved.
452        #[cfg(feature = "multi-viewport")]
453        {
454            if imgui_ctx
455                .io()
456                .config_flags()
457                .contains(ConfigFlags::VIEWPORTS_ENABLE)
458            {
459                let winit_scale = window.scale_factor();
460                let hidpi = match self.hidpi_mode {
461                    HiDpiMode::Default => winit_scale,
462                    HiDpiMode::Locked(factor) => factor,
463                    HiDpiMode::Rounded => winit_scale.round(),
464                };
465                self.hidpi_factor = hidpi;
466
467                let logical_size = window.inner_size().to_logical(winit_scale);
468                let logical_size = self.scale_size_from_winit(window, logical_size);
469                let io = imgui_ctx.io_mut();
470                io.set_display_size([logical_size.width as f32, logical_size.height as f32]);
471                io.set_display_framebuffer_scale([hidpi as f32, hidpi as f32]);
472            }
473        }
474
475        // If backend supports setting mouse pos and ImGui requests it, honor it
476        // Skip when multi-viewports are enabled (no global cursor set in winit)
477        if imgui_ctx.io().want_set_mouse_pos()
478            && !imgui_ctx
479                .io()
480                .config_flags()
481                .contains(ConfigFlags::VIEWPORTS_ENABLE)
482        {
483            let pos = imgui_ctx.io().mouse_pos();
484            let logical_pos = self
485                .scale_pos_for_winit(window, LogicalPosition::new(pos[0] as f64, pos[1] as f64));
486            let _ = window.set_cursor_position(logical_pos);
487        }
488        // Note: cursor shape update is exposed via prepare_render_with_ui()
489    }
490
491    /// Prepare frame - alias for prepare_render for compatibility
492    pub fn prepare_frame(&mut self, window: &Window, imgui_ctx: &mut Context) {
493        self.prepare_render(imgui_ctx, window);
494    }
495
496    /// Toggle Dear ImGui software-drawn cursor.
497    /// When enabled, the OS cursor is hidden and ImGui draws the cursor in draw data.
498    pub fn set_software_cursor_enabled(&mut self, imgui_ctx: &mut Context, enabled: bool) {
499        imgui_ctx.io_mut().set_mouse_draw_cursor(enabled);
500        // Invalidate cursor cache so next prepare_render_with_ui applies visibility change
501        self.cursor_cache = None;
502    }
503
504    /// Update cursor given a Ui reference (preferred, matches upstream)
505    pub fn prepare_render_with_ui(&mut self, ui: &dear_imgui_rs::Ui, window: &Window) {
506        // Auto-manage IME allowed state based on Dear ImGui's intent. This lets
507        // the platform show/hide IME (and soft keyboards on mobile) only when
508        // text input widgets are active.
509        if self.ime_auto_manage {
510            let want_text = ui.io().want_text_input();
511            if want_text && !self.ime_enabled {
512                window.set_ime_allowed(true);
513                self.ime_enabled = true;
514            } else if !want_text && self.ime_enabled {
515                window.set_ime_allowed(false);
516                self.ime_enabled = false;
517            }
518        }
519
520        // Only change OS cursor if not disabled by config flags
521        if !ui
522            .io()
523            .config_flags()
524            .contains(ConfigFlags::NO_MOUSE_CURSOR_CHANGE)
525        {
526            // Our Io wrapper does not currently expose MouseDrawCursor, assume false (OS cursor)
527            let cursor = CursorSettings {
528                cursor: ui.mouse_cursor(),
529                draw_cursor: ui.io().mouse_draw_cursor(),
530            };
531            if self.cursor_cache != Some(cursor) {
532                cursor.apply(window);
533                self.cursor_cache = Some(cursor);
534            }
535        }
536    }
537
538    /// Scale a logical size from winit to our active HiDPI mode
539    pub fn scale_size_from_winit(
540        &self,
541        window: &Window,
542        logical_size: LogicalSize<f64>,
543    ) -> LogicalSize<f64> {
544        match self.hidpi_mode {
545            HiDpiMode::Default => logical_size,
546            // Convert to physical using winit scale, then back to logical with our factor
547            _ => logical_size
548                .to_physical::<f64>(window.scale_factor())
549                .to_logical(self.hidpi_factor),
550        }
551    }
552
553    /// Scale a logical position from winit to our active HiDPI mode
554    pub fn scale_pos_from_winit(
555        &self,
556        window: &Window,
557        logical_pos: LogicalPosition<f64>,
558    ) -> LogicalPosition<f64> {
559        match self.hidpi_mode {
560            HiDpiMode::Default => logical_pos,
561            _ => logical_pos
562                .to_physical::<f64>(window.scale_factor())
563                .to_logical(self.hidpi_factor),
564        }
565    }
566
567    /// Scale a logical position for winit based on our active HiDPI mode
568    pub fn scale_pos_for_winit(
569        &self,
570        window: &Window,
571        logical_pos: LogicalPosition<f64>,
572    ) -> LogicalPosition<f64> {
573        match self.hidpi_mode {
574            HiDpiMode::Default => logical_pos,
575            _ => logical_pos
576                .to_physical::<f64>(self.hidpi_factor)
577                .to_logical(window.scale_factor()),
578        }
579    }
580
581    /// Create window attributes with Dear ImGui defaults
582    pub fn create_window_attributes() -> WindowAttributes {
583        WindowAttributes::default()
584            .with_title("Dear ImGui Window")
585            .with_inner_size(LogicalSize::new(1024.0, 768.0))
586    }
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592    use crate::test_util::test_sync::lock_context;
593
594    #[test]
595    fn test_hidpi_mode_default() {
596        assert_eq!(HiDpiMode::default(), HiDpiMode::Default);
597    }
598
599    #[test]
600    fn test_platform_creation() {
601        let _guard = lock_context();
602        let mut ctx = Context::create();
603        let platform = WinitPlatform::new(&mut ctx);
604
605        assert_eq!(platform.hidpi_mode, HiDpiMode::Default);
606        assert_eq!(platform.hidpi_factor, 1.0);
607        assert_eq!(platform.cursor_cache, None);
608        assert!(!platform.ime_enabled);
609    }
610
611    #[test]
612    fn test_hidpi_mode_setting() {
613        let _guard = lock_context();
614        let mut ctx = Context::create();
615        let mut platform = WinitPlatform::new(&mut ctx);
616
617        platform.set_hidpi_mode(HiDpiMode::Locked(2.0));
618        assert_eq!(platform.hidpi_mode, HiDpiMode::Locked(2.0));
619
620        platform.set_hidpi_mode(HiDpiMode::Rounded);
621        assert_eq!(platform.hidpi_mode, HiDpiMode::Rounded);
622    }
623
624    #[test]
625    fn test_ime_callback_ownership_detection() {
626        unsafe extern "C" fn other_ime_callback(
627            _ctx: *mut dear_imgui_rs::sys::ImGuiContext,
628            _viewport: *mut dear_imgui_rs::sys::ImGuiViewport,
629            _data: *mut dear_imgui_rs::sys::ImGuiPlatformImeData,
630        ) {
631        }
632
633        assert!(is_winit_set_ime_data(Some(imgui_winit_set_ime_data)));
634        assert!(!is_winit_set_ime_data(Some(other_ime_callback)));
635        assert!(!is_winit_set_ime_data(None));
636    }
637
638    #[test]
639    fn ime_platform_io_lookup_uses_passed_context() {
640        let _guard = lock_context();
641
642        let ctx_a = Context::create();
643        let raw_a = ctx_a.as_raw();
644        let marker_a = std::ptr::NonNull::<Window>::dangling().as_ptr();
645        unsafe {
646            let platform_io_a = dear_imgui_rs::sys::igGetPlatformIO_ContextPtr(raw_a);
647            (*platform_io_a).Platform_ImeUserData = marker_a.cast();
648            dear_imgui_rs::sys::igSetCurrentContext(std::ptr::null_mut());
649        }
650
651        let ctx_b = Context::create();
652        let raw_b = ctx_b.as_raw();
653        let marker_b = std::ptr::NonNull::<u8>::dangling().as_ptr();
654        unsafe {
655            let platform_io_b = dear_imgui_rs::sys::igGetPlatformIO_ContextPtr(raw_b);
656            (*platform_io_b).Platform_ImeUserData = marker_b.cast();
657
658            let selected = platform_io_for_ime_context(raw_a);
659            assert_eq!((*selected).Platform_ImeUserData, marker_a.cast());
660            assert_ne!((*selected).Platform_ImeUserData, marker_b.cast());
661
662            dear_imgui_rs::sys::igSetCurrentContext(raw_a);
663        }
664        drop(ctx_a);
665        unsafe {
666            dear_imgui_rs::sys::igSetCurrentContext(raw_b);
667        }
668        drop(ctx_b);
669    }
670
671    #[test]
672    fn test_window_attributes_creation() {
673        let attrs = WinitPlatform::create_window_attributes();
674        // Just test that it doesn't panic - actual values depend on winit defaults
675        let _ = attrs;
676    }
677}