use crate::sys;
bitflags::bitflags! {
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct InputTextCallback: u32 {
const COMPLETION = sys::ImGuiInputTextFlags_CallbackCompletion as u32;
const HISTORY = sys::ImGuiInputTextFlags_CallbackHistory as u32;
const ALWAYS = sys::ImGuiInputTextFlags_CallbackAlways as u32;
const CHAR_FILTER = sys::ImGuiInputTextFlags_CallbackCharFilter as u32;
const EDIT = sys::ImGuiInputTextFlags_CallbackEdit as u32;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HistoryDirection {
Up,
Down,
}
pub trait InputTextCallbackHandler {
fn char_filter(&mut self, c: char) -> Option<char> {
Some(c)
}
fn on_completion(&mut self, _data: TextCallbackData<'_>) {}
fn on_history(&mut self, _direction: HistoryDirection, _data: TextCallbackData<'_>) {}
fn on_always(&mut self, _data: TextCallbackData<'_>) {}
fn on_edit(&mut self, _data: TextCallbackData<'_>) {}
}
pub struct TextCallbackData<'cb>(
*mut sys::ImGuiInputTextCallbackData,
std::marker::PhantomData<&'cb mut sys::ImGuiInputTextCallbackData>,
);
impl<'cb> TextCallbackData<'cb> {
pub(super) unsafe fn new(data: *mut sys::ImGuiInputTextCallbackData) -> Self {
Self(data, std::marker::PhantomData)
}
fn data(&self) -> &sys::ImGuiInputTextCallbackData {
unsafe {
self.0
.as_ref()
.expect("internal imgui error: InputText callback data was null")
}
}
fn data_mut(&mut self) -> &mut sys::ImGuiInputTextCallbackData {
unsafe {
self.0
.as_mut()
.expect("internal imgui error: InputText callback data was null")
}
}
fn valid_text_len(&self) -> usize {
let data = self.data();
assert!(!data.Buf.is_null(), "internal imgui error: Buf was null");
assert!(
data.BufTextLen >= 0,
"internal imgui error: BufTextLen was negative"
);
assert!(
data.BufSize >= 0,
"internal imgui error: BufSize was negative"
);
assert!(
data.BufTextLen <= data.BufSize,
"internal imgui error: BufTextLen exceeded BufSize"
);
data.BufTextLen as usize
}
fn valid_text_len_i32(&self) -> i32 {
self.valid_text_len() as i32
}
fn position(name: &str, pos: i32, len: usize) -> usize {
let pos = usize::try_from(pos).unwrap_or_else(|_| {
panic!("internal imgui error: {name} was negative");
});
assert!(
pos <= len,
"internal imgui error: {name} exceeded BufTextLen"
);
pos
}
fn position_to_i32(name: &str, pos: usize) -> i32 {
i32::try_from(pos).unwrap_or_else(|_| {
panic!("{name} exceeded ImGui's i32 position range");
})
}
fn assert_byte_boundary(text: &str, name: &str, pos: usize) {
assert!(
text.is_char_boundary(pos),
"{name} must lie on a UTF-8 character boundary"
);
}
pub fn str(&self) -> &str {
let len = self.valid_text_len();
unsafe {
std::str::from_utf8(std::slice::from_raw_parts(self.data().Buf as *const _, len))
.expect("internal imgui error -- it boofed a utf8")
}
}
pub fn cursor_pos(&self) -> usize {
let len = self.valid_text_len();
Self::position("CursorPos", self.data().CursorPos, len)
}
pub fn set_cursor_pos(&mut self, pos: usize) {
let text = self.str();
assert!(pos <= text.len(), "cursor position out of bounds");
Self::assert_byte_boundary(text, "cursor position", pos);
self.data_mut().CursorPos = Self::position_to_i32("cursor position", pos);
}
pub fn selection_start(&self) -> usize {
let len = self.valid_text_len();
Self::position("SelectionStart", self.data().SelectionStart, len)
}
pub fn set_selection_start(&mut self, pos: usize) {
let text = self.str();
assert!(pos <= text.len(), "selection start out of bounds");
Self::assert_byte_boundary(text, "selection start", pos);
self.data_mut().SelectionStart = Self::position_to_i32("selection start", pos);
}
pub fn selection_end(&self) -> usize {
let len = self.valid_text_len();
Self::position("SelectionEnd", self.data().SelectionEnd, len)
}
pub fn set_selection_end(&mut self, pos: usize) {
let text = self.str();
assert!(pos <= text.len(), "selection end out of bounds");
Self::assert_byte_boundary(text, "selection end", pos);
self.data_mut().SelectionEnd = Self::position_to_i32("selection end", pos);
}
pub fn select_all(&mut self) {
let len = self.valid_text_len_i32();
let data = self.data_mut();
data.SelectionStart = 0;
data.SelectionEnd = len;
}
pub fn clear_selection(&mut self) {
let cursor_pos = Self::position_to_i32("cursor position", self.cursor_pos());
let data = self.data_mut();
data.SelectionStart = cursor_pos;
data.SelectionEnd = cursor_pos;
}
pub fn has_selection(&self) -> bool {
self.data().SelectionStart != self.data().SelectionEnd
}
pub fn remove_chars(&mut self, pos: usize, bytes_count: usize) {
let text = self.str();
let end = pos
.checked_add(bytes_count)
.expect("delete range overflowed usize");
assert!(end <= text.len(), "delete range out of bounds");
Self::assert_byte_boundary(text, "delete start", pos);
Self::assert_byte_boundary(text, "delete end", end);
let pos = Self::position_to_i32("delete start", pos);
let bytes_count = Self::position_to_i32("delete byte count", bytes_count);
unsafe {
sys::ImGuiInputTextCallbackData_DeleteChars(self.0, pos, bytes_count);
}
}
pub fn insert_chars(&mut self, pos: usize, text: &str) {
let current = self.str();
assert!(pos <= current.len(), "insert position out of bounds");
Self::assert_byte_boundary(current, "insert position", pos);
let pos = Self::position_to_i32("insert position", pos);
let text_ptr = text.as_ptr() as *const std::os::raw::c_char;
unsafe {
sys::ImGuiInputTextCallbackData_InsertChars(
self.0,
pos,
text_ptr,
text_ptr.add(text.len()),
);
}
}
pub unsafe fn str_as_bytes_mut(&mut self) -> &mut [u8] {
let len = self.valid_text_len();
unsafe {
let str = std::str::from_utf8_mut(std::slice::from_raw_parts_mut(
self.data_mut().Buf as *mut u8,
len,
))
.expect("internal imgui error -- it boofed a utf8");
str.as_bytes_mut()
}
}
pub fn set_dirty(&mut self) {
self.data_mut().BufDirty = true;
}
pub fn selected(&self) -> &str {
let text = self.str();
let start = self.selection_start().min(self.selection_end());
let end = self.selection_start().max(self.selection_end());
assert!(end <= text.len(), "selection range out of bounds");
Self::assert_byte_boundary(text, "selection start", start);
Self::assert_byte_boundary(text, "selection end", end);
&text[start..end]
}
pub fn push_str(&mut self, text: &str) {
let current_len = self.valid_text_len();
self.insert_chars(current_len, text);
}
}
pub struct PassthroughCallback;
impl InputTextCallbackHandler for PassthroughCallback {}
#[cfg(test)]
mod tests {
use super::*;
struct DefaultHandler;
impl InputTextCallbackHandler for DefaultHandler {}
fn callback_data_for_text(text: &mut [u8]) -> sys::ImGuiInputTextCallbackData {
let len = text.len().saturating_sub(1);
let mut data = sys::ImGuiInputTextCallbackData::default();
data.Buf = text.as_mut_ptr().cast();
data.BufTextLen = len as i32;
data.BufSize = text.len() as i32;
data.CursorPos = len as i32;
data.SelectionStart = 0;
data.SelectionEnd = len as i32;
data
}
#[test]
fn default_char_filter_keeps_character() {
let mut handler = DefaultHandler;
assert_eq!(handler.char_filter('x'), Some('x'));
}
#[test]
fn passthrough_char_filter_keeps_character() {
let mut handler = PassthroughCallback;
assert_eq!(handler.char_filter('x'), Some('x'));
}
#[test]
fn text_callback_positions_accept_bounds() {
let mut text = *b"abcd\0";
let mut data = callback_data_for_text(&mut text);
data.CursorPos = 4;
data.SelectionStart = 1;
data.SelectionEnd = 3;
let info = unsafe { TextCallbackData::new(&mut data) };
assert_eq!(info.cursor_pos(), 4);
assert_eq!(info.selection_start(), 1);
assert_eq!(info.selection_end(), 3);
assert_eq!(info.selected(), "bc");
}
#[test]
#[should_panic(expected = "CursorPos exceeded BufTextLen")]
fn text_callback_cursor_pos_rejects_out_of_bounds() {
let mut text = *b"abcd\0";
let mut data = callback_data_for_text(&mut text);
data.CursorPos = 5;
let info = unsafe { TextCallbackData::new(&mut data) };
let _ = info.cursor_pos();
}
#[test]
#[should_panic(expected = "CursorPos exceeded BufTextLen")]
fn text_callback_clear_selection_rejects_out_of_bounds_cursor() {
let mut text = *b"abcd\0";
let mut data = callback_data_for_text(&mut text);
data.CursorPos = 5;
let mut info = unsafe { TextCallbackData::new(&mut data) };
info.clear_selection();
}
}