dear-imgui-winit 0.18.0

Winit platform backend for dear-imgui-rs
Documentation
//! Event handling for Dear ImGui winit backend
//!
//! This module contains event processing logic for various winit events
//! including keyboard, mouse, touch, and IME events.

use dear_imgui_rs::Context;
use dear_imgui_rs::input::MouseSource;
use winit::event::{DeviceEvent, ElementState, Ime, KeyEvent, MouseScrollDelta, TouchPhase};

use winit::window::Window;

use crate::input::{to_imgui_mouse_button, winit_key_to_imgui_key};
use crate::sanitize;

/// Handle keyboard input events
pub fn handle_keyboard_input(event: &KeyEvent, imgui_ctx: &mut Context) -> bool {
    let io = imgui_ctx.io_mut();

    // Inject text for character input on key press (matches upstream imgui-winit behavior)
    if event.state.is_pressed()
        && let Some(txt) = &event.text
    {
        for ch in txt.chars() {
            // Filter out DEL control code as upstream does
            if ch != '\u{7f}' {
                io.add_input_character(ch);
            }
        }
    }

    if let Some(imgui_key) = winit_key_to_imgui_key(&event.logical_key, event.physical_key) {
        let pressed = event.state == ElementState::Pressed;
        io.add_key_event(imgui_key, pressed);
        return io.want_capture_keyboard();
    }

    false
}

/// Handle mouse wheel scrolling
pub fn handle_mouse_wheel(
    delta: MouseScrollDelta,
    phase: TouchPhase,
    scale_factor: f64,
    imgui_ctx: &mut Context,
) -> bool {
    if phase == TouchPhase::Cancelled {
        return imgui_ctx.io().want_capture_mouse();
    }

    let io = imgui_ctx.io_mut();

    // Desktop winit mouse wheel events always come from a physical mouse.
    io.add_mouse_source_event(MouseSource::Mouse);

    io.add_mouse_wheel_event(normalize_mouse_wheel_delta(delta, scale_factor));

    io.want_capture_mouse()
}

/// Handle mouse button events
pub fn handle_mouse_button(
    button: winit::event::MouseButton,
    state: ElementState,
    imgui_ctx: &mut Context,
) -> bool {
    if let Some(imgui_button) = to_imgui_mouse_button(button) {
        let pressed = state == ElementState::Pressed;
        {
            let io = imgui_ctx.io_mut();
            // Mouse button events are generated by a physical mouse here.
            io.add_mouse_source_event(MouseSource::Mouse);
            io.add_mouse_button_event(imgui_button, pressed);
        }
        return imgui_ctx.io().want_capture_mouse();
    }
    false
}

/// Handle cursor movement events
pub fn handle_cursor_moved(position: [f64; 2], imgui_ctx: &mut Context) -> bool {
    let Some(position) = sanitize::finite_vec2_f64_to_f32(position) else {
        return imgui_ctx.io().want_capture_mouse();
    };

    {
        let io = imgui_ctx.io_mut();
        // Cursor move events from winit are produced by a physical mouse.
        io.add_mouse_source_event(MouseSource::Mouse);
        io.add_mouse_pos_event(position);
    }
    imgui_ctx.io().want_capture_mouse()
}

/// Handle modifier key state changes
pub(crate) fn modifier_key_events(
    modifiers: &winit::event::Modifiers,
) -> [(dear_imgui_rs::Key, bool); 4] {
    let state = modifiers.state();
    [
        (dear_imgui_rs::Key::ModShift, state.shift_key()),
        (dear_imgui_rs::Key::ModCtrl, state.control_key()),
        (dear_imgui_rs::Key::ModAlt, state.alt_key()),
        (dear_imgui_rs::Key::ModSuper, state.super_key()),
    ]
}

fn normalize_mouse_wheel_delta(delta: MouseScrollDelta, scale_factor: f64) -> [f32; 2] {
    match delta {
        MouseScrollDelta::LineDelta(horizontal, vertical) => [
            sanitize::finite_or_zero(horizontal),
            sanitize::finite_or_zero(vertical),
        ],
        MouseScrollDelta::PixelDelta(position) => {
            let scale_factor = sanitize::positive_finite_or(scale_factor, 1.0);
            // Winit reports physical pixels. Dear ImGui expects small, continuous wheel units.
            // Keeping the magnitude preserves trackpad acceleration and fractional scrolling.
            [
                sanitize::finite_f64_to_f32(position.x / scale_factor * 0.01).unwrap_or(0.0),
                sanitize::finite_f64_to_f32(position.y / scale_factor * 0.01).unwrap_or(0.0),
            ]
        }
    }
}

/// Handle modifier key state changes
pub fn handle_modifiers_changed(modifiers: &winit::event::Modifiers, imgui_ctx: &mut Context) {
    let io = imgui_ctx.io_mut();

    // Update modifier key states.
    //
    // Dear ImGui derives `io.KeyMods`/`io.KeyCtrl`/`io.KeyShift`/`io.KeyAlt`/`io.KeySuper`
    // from the `ImGuiMod_*` keys, so we must submit those in addition to the left/right keys.
    //
    // Left/right modifier identity comes from `KeyEvent::physical_key`. `ModifiersChanged`
    // provides only aggregate Mod* state and must not synthesize both physical keys.
    for (key, pressed) in modifier_key_events(modifiers) {
        io.add_key_event(key, pressed);
    }
}

/// Handle IME (Input Method Editor) events for international text input
pub fn handle_ime_event(ime: &Ime, imgui_ctx: &mut Context) {
    match ime {
        Ime::Preedit(_text, _cursor_range) => {
            // Do not inject preedit text into Dear ImGui; composition should be handled by the OS/IME.
        }
        Ime::Commit(text) => {
            // Handle committed text (final text input)
            for ch in text.chars() {
                if !ch.is_control() {
                    imgui_ctx.io_mut().add_input_character(ch);
                }
            }
        }
        Ime::Enabled => {
            // IME was enabled - we could set a flag here if needed
        }
        Ime::Disabled => {
            // IME was disabled - we could clear a flag here if needed
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum TouchAction {
    Press,
    Move,
    Release,
}

pub(crate) fn touch_transition(
    active_id: Option<u64>,
    event_id: u64,
    phase: TouchPhase,
) -> (Option<u64>, Option<TouchAction>) {
    match phase {
        TouchPhase::Started if active_id.is_none() => (Some(event_id), Some(TouchAction::Press)),
        TouchPhase::Moved if active_id == Some(event_id) => (active_id, Some(TouchAction::Move)),
        TouchPhase::Ended | TouchPhase::Cancelled if active_id == Some(event_id) => {
            (None, Some(TouchAction::Release))
        }
        _ => (active_id, None),
    }
}

pub(crate) fn touch_logical_position(
    touch: &winit::event::Touch,
    window: &Window,
) -> Option<[f32; 2]> {
    let position = touch
        .location
        .to_logical::<f64>(sanitize::positive_finite_or(window.scale_factor(), 1.0));
    sanitize::finite_position(position)
}

pub(crate) fn handle_touch_event_at(
    action: TouchAction,
    position: Option<[f32; 2]>,
    viewport: Option<dear_imgui_rs::Id>,
    imgui_ctx: &mut Context,
) -> bool {
    let io = imgui_ctx.io_mut();
    io.add_mouse_source_event(MouseSource::TouchScreen);
    if let Some(viewport) = viewport {
        io.add_mouse_viewport_event(viewport);
    }
    if let Some(position) = position {
        io.add_mouse_pos_event(position);
    }
    match action {
        TouchAction::Press => {
            io.add_mouse_button_event(dear_imgui_rs::input::MouseButton::Left, true);
        }
        TouchAction::Move => {}
        TouchAction::Release => {
            io.add_mouse_button_event(dear_imgui_rs::input::MouseButton::Left, false);
        }
    }
    true
}

/// Handle device events (raw input events)
pub fn handle_device_event(_event: &DeviceEvent) {
    // Handle device-specific events if needed
    // Currently no specific handling required
}

/// Handle window focus events
pub fn handle_focused(focused: bool, imgui_ctx: &mut Context) -> bool {
    // Tell Dear ImGui about host window focus change
    imgui_ctx.io_mut().add_focus_event(focused);
    false
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_util::test_sync::lock_context;
    use dear_imgui_rs::Context;
    use winit::event::{ElementState, MouseButton, TouchPhase};
    use winit::keyboard::ModifiersState;

    #[test]
    fn test_keyboard_input_handling() {
        // We can't construct KeyEvent directly due to private fields
        // So we'll just test that the function exists and can be called
        // In a real scenario, KeyEvent would be provided by winit

        // Test that the function exists and can be called
        // We'll use a dummy test that doesn't require constructing KeyEvent
        // This is a placeholder test - in a real implementation we would test actual key handling
    }

    #[test]
    fn test_mouse_button_handling() {
        let _guard = lock_context();
        let mut ctx = Context::create();

        let handled = handle_mouse_button(MouseButton::Left, ElementState::Pressed, &mut ctx);
        // The result depends on whether imgui wants to capture mouse
        // We just test that it doesn't panic
        // Test that the function returns a boolean value (always true)
        let _ = handled; // Just verify it's a boolean
    }

    #[test]
    fn test_cursor_moved() {
        let _guard = lock_context();
        let mut ctx = Context::create();

        let handled = handle_cursor_moved([100.0, 200.0], &mut ctx);
        // The result depends on whether imgui wants to capture mouse
        // We just test that it doesn't panic
        // Test that the function returns a boolean value (always true)
        let _ = handled; // Just verify it's a boolean
    }

    #[test]
    fn test_cursor_moved_ignores_non_finite_position() {
        let _guard = lock_context();
        let mut ctx = Context::create();

        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            handle_cursor_moved([f64::NAN, f64::INFINITY], &mut ctx);
        }));

        assert!(result.is_ok());
    }

    #[test]
    fn test_mouse_wheel_ignores_non_finite_line_delta() {
        let _guard = lock_context();
        let mut ctx = Context::create();

        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            handle_mouse_wheel(
                MouseScrollDelta::LineDelta(f32::NAN, f32::INFINITY),
                TouchPhase::Moved,
                1.0,
                &mut ctx,
            );
        }));

        assert!(result.is_ok());
    }

    #[test]
    fn pixel_wheel_preserves_fractional_logical_motion() {
        assert_eq!(
            normalize_mouse_wheel_delta(
                MouseScrollDelta::PixelDelta(winit::dpi::PhysicalPosition::new(25.0, -10.0)),
                2.0,
            ),
            [0.125, -0.05]
        );
    }

    #[test]
    fn test_modifiers_changed_updates_key_mods() {
        let _guard = lock_context();
        let mut ctx = Context::create();
        let io = ctx.io_mut();
        io.set_display_size([1.0, 1.0]);
        ctx.font_atlas()
            .try_claim_legacy_renderer()
            .expect("the input-event test uses headless legacy rendering")
            .build();

        let macos_behaviors = ctx.io().config_macosx_behaviors();
        let modifiers: winit::event::Modifiers = ModifiersState::CONTROL.into();
        handle_modifiers_changed(&modifiers, &mut ctx);

        let ui = ctx.frame();
        if macos_behaviors {
            assert!(!ui.io().key_ctrl());
            assert!(ui.io().key_super());
        } else {
            assert!(ui.io().key_ctrl());
            assert!(!ui.io().key_super());
        }
        assert!(!ui.io().key_shift());
        assert!(!ui.io().key_alt());
    }

    #[test]
    fn touch_transition_keeps_the_first_finger_until_release() {
        assert_eq!(
            touch_transition(None, 7, TouchPhase::Started),
            (Some(7), Some(TouchAction::Press))
        );
        assert_eq!(
            touch_transition(Some(7), 8, TouchPhase::Started),
            (Some(7), None)
        );
        assert_eq!(
            touch_transition(Some(7), 7, TouchPhase::Moved),
            (Some(7), Some(TouchAction::Move))
        );
        assert_eq!(
            touch_transition(Some(7), 7, TouchPhase::Ended),
            (None, Some(TouchAction::Release))
        );
    }
}