dear-imgui-rs 0.14.0

High-level Rust bindings to Dear ImGui v1.92.7 with docking, WGPU/GL backends, and extensions (ImPlot/ImPlot3D, ImNodes, ImGuizmo, file browser, reflection-based UI)
Documentation
use super::buffers::resize_string_input_buffer;
use super::callbacks::{HistoryDirection, InputTextCallbackHandler, TextCallbackData};
use crate::string::ImString;
use crate::sys;
use std::ffi::{c_int, c_void};
use std::panic::{AssertUnwindSafe, catch_unwind};

pub(in crate::widget::input) struct StringResizeCallbackState {
    buffer: *mut Vec<u8>,
}

impl StringResizeCallbackState {
    pub(in crate::widget::input) fn new(buffer: &mut Vec<u8>) -> Self {
        Self {
            buffer: buffer as *mut Vec<u8>,
        }
    }

    pub(in crate::widget::input) fn user_ptr(&mut self) -> *mut c_void {
        self as *mut Self as *mut c_void
    }
}

pub(in crate::widget::input) struct StringCallbackState<T> {
    buffer: *mut Vec<u8>,
    handler: T,
}

impl<T> StringCallbackState<T> {
    pub(in crate::widget::input) fn new(buffer: &mut Vec<u8>, handler: T) -> Self {
        Self {
            buffer: buffer as *mut Vec<u8>,
            handler,
        }
    }

    pub(in crate::widget::input) fn user_ptr(&mut self) -> *mut c_void {
        self as *mut Self as *mut c_void
    }
}

pub(in crate::widget::input) extern "C" fn im_string_resize_callback(
    data: *mut sys::ImGuiInputTextCallbackData,
) -> c_int {
    resize_im_string_with_panic_message(data, "dear-imgui-rs: panic in ImString resize callback")
}

pub(in crate::widget::input) extern "C" fn im_string_multiline_resize_callback(
    data: *mut sys::ImGuiInputTextCallbackData,
) -> c_int {
    resize_im_string_with_panic_message(
        data,
        "dear-imgui-rs: panic in ImString multiline resize callback",
    )
}

pub(in crate::widget::input) extern "C" fn string_multiline_resize_callback(
    data: *mut sys::ImGuiInputTextCallbackData,
) -> c_int {
    if data.is_null() {
        return 0;
    }

    abort_on_panic(
        "dear-imgui-rs: panic in multiline InputText resize callback",
        || unsafe {
            let state = (*data).UserData as *mut StringResizeCallbackState;
            route_string_resize_callback(state, data)
        },
    )
}

pub(in crate::widget::input) extern "C" fn string_callback_router<T: InputTextCallbackHandler>(
    data: *mut sys::ImGuiInputTextCallbackData,
) -> c_int {
    route_string_callback_with_panic_message::<T>(
        data,
        "dear-imgui-rs: panic in InputText callback",
    )
}

pub(in crate::widget::input) extern "C" fn string_multiline_callback_router<
    T: InputTextCallbackHandler,
>(
    data: *mut sys::ImGuiInputTextCallbackData,
) -> c_int {
    route_string_callback_with_panic_message::<T>(
        data,
        "dear-imgui-rs: panic in InputText multiline callback",
    )
}

fn resize_im_string_with_panic_message(
    data: *mut sys::ImGuiInputTextCallbackData,
    panic_message: &str,
) -> c_int {
    if data.is_null() {
        return 0;
    }

    abort_on_panic(panic_message, || unsafe { resize_im_string(data) })
}

fn route_string_callback_with_panic_message<T: InputTextCallbackHandler>(
    data: *mut sys::ImGuiInputTextCallbackData,
    panic_message: &str,
) -> c_int {
    if data.is_null() {
        return 0;
    }

    abort_on_panic(panic_message, || unsafe {
        let state = (*data).UserData as *mut StringCallbackState<T>;
        route_string_callback(state, data)
    })
}

fn abort_on_panic(message: &str, f: impl FnOnce() -> c_int) -> c_int {
    match catch_unwind(AssertUnwindSafe(f)) {
        Ok(value) => value,
        Err(_) => {
            eprintln!("{message}");
            std::process::abort();
        }
    }
}

unsafe fn resize_im_string(data: *mut sys::ImGuiInputTextCallbackData) -> c_int {
    let event_flag = unsafe { (*data).EventFlag as i32 };
    if event_flag != sys::ImGuiInputTextFlags_CallbackResize as i32 {
        return 0;
    }

    let user_data = unsafe { (*data).UserData as *mut ImString };
    let Some(im_string) = (unsafe { user_data.as_mut() }) else {
        return 0;
    };

    let requested_i32 = unsafe { (*data).BufSize };
    if requested_i32 < 0 {
        return 0;
    }

    let requested = requested_i32 as usize;
    im_string.ensure_buf_size(requested);
    unsafe {
        (*data).Buf = im_string.as_mut_ptr();
        (*data).BufDirty = true;
    }
    0
}

unsafe fn route_string_resize_callback(
    state: *mut StringResizeCallbackState,
    data: *mut sys::ImGuiInputTextCallbackData,
) -> c_int {
    let event_flag = unsafe { (*data).EventFlag as i32 };
    if event_flag != sys::ImGuiInputTextFlags_CallbackResize as i32 {
        return 0;
    }

    let Some(state) = (unsafe { state.as_mut() }) else {
        return 0;
    };
    let Some(buffer) = (unsafe { state.buffer.as_mut() }) else {
        return 0;
    };

    unsafe {
        debug_assert_eq!(buffer.as_ptr() as *const _, (*data).Buf);
        resize_string_input_buffer(buffer, (*data).BufSize, data)
    }
}

unsafe fn route_string_callback<T: InputTextCallbackHandler>(
    state: *mut StringCallbackState<T>,
    data: *mut sys::ImGuiInputTextCallbackData,
) -> c_int {
    let Some(state) = (unsafe { state.as_mut() }) else {
        return 0;
    };
    let Some(buffer) = (unsafe { state.buffer.as_mut() }) else {
        return 0;
    };

    let event_flag = unsafe { (*data).EventFlag as i32 };
    match event_flag {
        value if value == sys::ImGuiInputTextFlags_CallbackResize as i32 => unsafe {
            debug_assert_eq!(buffer.as_ptr() as *const _, (*data).Buf);
            resize_string_input_buffer(buffer, (*data).BufSize, data)
        },
        value if value == sys::ImGuiInputTextFlags_CallbackCompletion as i32 => {
            let info = unsafe { TextCallbackData::new(data) };
            state.handler.on_completion(info);
            0
        }
        value if value == sys::ImGuiInputTextFlags_CallbackHistory as i32 => {
            let dir = unsafe { history_direction(data) };
            let info = unsafe { TextCallbackData::new(data) };
            state.handler.on_history(dir, info);
            0
        }
        value if value == sys::ImGuiInputTextFlags_CallbackAlways as i32 => {
            let info = unsafe { TextCallbackData::new(data) };
            state.handler.on_always(info);
            0
        }
        value if value == sys::ImGuiInputTextFlags_CallbackEdit as i32 => {
            let info = unsafe { TextCallbackData::new(data) };
            state.handler.on_edit(info);
            0
        }
        value if value == sys::ImGuiInputTextFlags_CallbackCharFilter as i32 => {
            dispatch_char_filter(&mut state.handler, data);
            0
        }
        _ => 0,
    }
}

unsafe fn history_direction(data: *mut sys::ImGuiInputTextCallbackData) -> HistoryDirection {
    let key = unsafe { (*data).EventKey };
    if key == sys::ImGuiKey_UpArrow {
        HistoryDirection::Up
    } else {
        HistoryDirection::Down
    }
}

fn dispatch_char_filter<T: InputTextCallbackHandler>(
    handler: &mut T,
    data: *mut sys::ImGuiInputTextCallbackData,
) {
    let ch = unsafe { std::char::from_u32((*data).EventChar as u32).unwrap_or('\0') };
    let new_ch = handler.char_filter(ch).map(|c| c as u32).unwrap_or(0);
    unsafe {
        (*data).EventChar = sys::ImWchar::try_from(new_ch).unwrap_or(0 as sys::ImWchar);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Default)]
    struct RecordingHandler {
        char_filter_seen: Option<char>,
        history_seen: Option<HistoryDirection>,
    }

    impl InputTextCallbackHandler for RecordingHandler {
        fn char_filter(&mut self, c: char) -> Option<char> {
            self.char_filter_seen = Some(c);
            Some('z')
        }

        fn on_history(&mut self, direction: HistoryDirection, _data: TextCallbackData<'_>) {
            self.history_seen = Some(direction);
        }
    }

    fn callback_data_for_buffer(buffer: &mut [u8]) -> sys::ImGuiInputTextCallbackData {
        let mut data = sys::ImGuiInputTextCallbackData::default();
        data.Buf = buffer.as_mut_ptr().cast();
        data.BufTextLen = buffer.len().saturating_sub(1) as i32;
        data.BufSize = buffer.len() as i32;
        data
    }

    #[test]
    fn string_callback_router_dispatches_char_filter() {
        let mut buffer = b"abc\0".to_vec();
        let mut callback_state = StringCallbackState::new(&mut buffer, RecordingHandler::default());
        let mut data = callback_data_for_buffer(&mut buffer);
        data.UserData = callback_state.user_ptr();
        data.EventFlag = sys::ImGuiInputTextFlags_CallbackCharFilter as i32;
        data.EventChar = 'a' as sys::ImWchar;

        assert_eq!(string_callback_router::<RecordingHandler>(&mut data), 0);

        assert_eq!(callback_state.handler.char_filter_seen, Some('a'));
        assert_eq!(data.EventChar, 'z' as sys::ImWchar);
    }

    #[test]
    fn string_callback_router_dispatches_history_direction() {
        let mut buffer = b"abc\0".to_vec();
        let mut callback_state = StringCallbackState::new(&mut buffer, RecordingHandler::default());
        let mut data = callback_data_for_buffer(&mut buffer);
        data.UserData = callback_state.user_ptr();
        data.EventFlag = sys::ImGuiInputTextFlags_CallbackHistory as i32;
        data.EventKey = sys::ImGuiKey_UpArrow;

        assert_eq!(string_callback_router::<RecordingHandler>(&mut data), 0);

        assert_eq!(
            callback_state.handler.history_seen,
            Some(HistoryDirection::Up)
        );
    }

    #[test]
    fn string_resize_callback_updates_buffer_pointer() {
        let mut buffer = b"abc\0".to_vec();
        let mut callback_state = StringResizeCallbackState::new(&mut buffer);
        let mut data = callback_data_for_buffer(&mut buffer);
        data.UserData = callback_state.user_ptr();
        data.EventFlag = sys::ImGuiInputTextFlags_CallbackResize as i32;
        data.BufSize = 32;

        assert_eq!(string_multiline_resize_callback(&mut data), 0);

        assert_eq!(buffer.len(), 32);
        assert_eq!(data.Buf, buffer.as_mut_ptr().cast());
        assert!(data.BufDirty);
    }
}