Skip to main content

dear_imgui_rs/widget/input/
callbacks.rs

1use crate::sys;
2
3// InputText Callback System
4// =========================
5
6bitflags::bitflags! {
7    /// Callback flags for InputText widgets
8    #[repr(transparent)]
9    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10    pub struct InputTextCallback: u32 {
11        /// Call user function on pressing TAB (for completion handling)
12        const COMPLETION = sys::ImGuiInputTextFlags_CallbackCompletion as u32;
13        /// Call user function on pressing Up/Down arrows (for history handling)
14        const HISTORY = sys::ImGuiInputTextFlags_CallbackHistory as u32;
15        /// Call user function every time. User code may query cursor position, modify text buffer.
16        const ALWAYS = sys::ImGuiInputTextFlags_CallbackAlways as u32;
17        /// Call user function to filter character.
18        const CHAR_FILTER = sys::ImGuiInputTextFlags_CallbackCharFilter as u32;
19        /// Callback on buffer edit (note that InputText already returns true on edit, the
20        /// callback is useful mainly to manipulate the underlying buffer while focus is active)
21        const EDIT = sys::ImGuiInputTextFlags_CallbackEdit as u32;
22    }
23}
24
25/// Direction for history navigation
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum HistoryDirection {
28    /// Up arrow key pressed
29    Up,
30    /// Down arrow key pressed
31    Down,
32}
33
34/// This trait provides an interface which ImGui will call on InputText callbacks.
35///
36/// Each method is called *if and only if* the corresponding flag for each
37/// method is passed to ImGui in the `callback` builder.
38pub trait InputTextCallbackHandler {
39    /// Filters a char -- returning a `None` means that the char is removed,
40    /// and returning another char substitutes it out.
41    ///
42    /// To make ImGui run this callback, use [InputTextCallback::CHAR_FILTER].
43    fn char_filter(&mut self, c: char) -> Option<char> {
44        Some(c)
45    }
46
47    /// Called when the user presses the completion key (TAB by default).
48    ///
49    /// To make ImGui run this callback, use [InputTextCallback::COMPLETION].
50    fn on_completion(&mut self, _data: TextCallbackData<'_>) {}
51
52    /// Called when the user presses Up/Down arrow keys for history navigation.
53    ///
54    /// To make ImGui run this callback, use [InputTextCallback::HISTORY].
55    fn on_history(&mut self, _direction: HistoryDirection, _data: TextCallbackData<'_>) {}
56
57    /// Called every frame when the input text is active.
58    ///
59    /// To make ImGui run this callback, use [InputTextCallback::ALWAYS].
60    fn on_always(&mut self, _data: TextCallbackData<'_>) {}
61
62    /// Called when the text buffer is edited.
63    ///
64    /// To make ImGui run this callback, use [InputTextCallback::EDIT].
65    fn on_edit(&mut self, _data: TextCallbackData<'_>) {}
66}
67
68/// This struct provides methods to edit the underlying text buffer that
69/// Dear ImGui manipulates. Primarily, it gives [remove_chars](Self::remove_chars),
70/// [insert_chars](Self::insert_chars), and mutable access to what text is selected.
71pub struct TextCallbackData<'cb>(
72    *mut sys::ImGuiInputTextCallbackData,
73    std::marker::PhantomData<&'cb mut sys::ImGuiInputTextCallbackData>,
74);
75
76impl<'cb> TextCallbackData<'cb> {
77    /// Creates the buffer.
78    pub(super) unsafe fn new(data: *mut sys::ImGuiInputTextCallbackData) -> Self {
79        Self(data, std::marker::PhantomData)
80    }
81
82    fn data(&self) -> &sys::ImGuiInputTextCallbackData {
83        unsafe {
84            self.0
85                .as_ref()
86                .expect("internal imgui error: InputText callback data was null")
87        }
88    }
89
90    fn data_mut(&mut self) -> &mut sys::ImGuiInputTextCallbackData {
91        unsafe {
92            self.0
93                .as_mut()
94                .expect("internal imgui error: InputText callback data was null")
95        }
96    }
97
98    fn valid_text_len(&self) -> usize {
99        let data = self.data();
100        assert!(!data.Buf.is_null(), "internal imgui error: Buf was null");
101        assert!(
102            data.BufTextLen >= 0,
103            "internal imgui error: BufTextLen was negative"
104        );
105        assert!(
106            data.BufSize >= 0,
107            "internal imgui error: BufSize was negative"
108        );
109        assert!(
110            data.BufTextLen <= data.BufSize,
111            "internal imgui error: BufTextLen exceeded BufSize"
112        );
113        data.BufTextLen as usize
114    }
115
116    fn valid_text_len_i32(&self) -> i32 {
117        self.valid_text_len() as i32
118    }
119
120    fn position(name: &str, pos: i32, len: usize) -> usize {
121        let pos = usize::try_from(pos).unwrap_or_else(|_| {
122            panic!("internal imgui error: {name} was negative");
123        });
124        assert!(
125            pos <= len,
126            "internal imgui error: {name} exceeded BufTextLen"
127        );
128        pos
129    }
130
131    fn position_to_i32(name: &str, pos: usize) -> i32 {
132        i32::try_from(pos).unwrap_or_else(|_| {
133            panic!("{name} exceeded ImGui's i32 position range");
134        })
135    }
136
137    fn assert_byte_boundary(text: &str, name: &str, pos: usize) {
138        assert!(
139            text.is_char_boundary(pos),
140            "{name} must lie on a UTF-8 character boundary"
141        );
142    }
143
144    /// Get a reference to the text callback buffer's str.
145    pub fn str(&self) -> &str {
146        let len = self.valid_text_len();
147        unsafe {
148            std::str::from_utf8(std::slice::from_raw_parts(self.data().Buf as *const _, len))
149                .expect("internal imgui error -- it boofed a utf8")
150        }
151    }
152
153    /// Get the current cursor position
154    pub fn cursor_pos(&self) -> usize {
155        let len = self.valid_text_len();
156        Self::position("CursorPos", self.data().CursorPos, len)
157    }
158
159    /// Set the cursor position
160    pub fn set_cursor_pos(&mut self, pos: usize) {
161        let text = self.str();
162        assert!(pos <= text.len(), "cursor position out of bounds");
163        Self::assert_byte_boundary(text, "cursor position", pos);
164        self.data_mut().CursorPos = Self::position_to_i32("cursor position", pos);
165    }
166
167    /// Get the selection start position
168    pub fn selection_start(&self) -> usize {
169        let len = self.valid_text_len();
170        Self::position("SelectionStart", self.data().SelectionStart, len)
171    }
172
173    /// Set the selection start position
174    pub fn set_selection_start(&mut self, pos: usize) {
175        let text = self.str();
176        assert!(pos <= text.len(), "selection start out of bounds");
177        Self::assert_byte_boundary(text, "selection start", pos);
178        self.data_mut().SelectionStart = Self::position_to_i32("selection start", pos);
179    }
180
181    /// Get the selection end position
182    pub fn selection_end(&self) -> usize {
183        let len = self.valid_text_len();
184        Self::position("SelectionEnd", self.data().SelectionEnd, len)
185    }
186
187    /// Set the selection end position
188    pub fn set_selection_end(&mut self, pos: usize) {
189        let text = self.str();
190        assert!(pos <= text.len(), "selection end out of bounds");
191        Self::assert_byte_boundary(text, "selection end", pos);
192        self.data_mut().SelectionEnd = Self::position_to_i32("selection end", pos);
193    }
194
195    /// Select all text
196    pub fn select_all(&mut self) {
197        let len = self.valid_text_len_i32();
198        let data = self.data_mut();
199        data.SelectionStart = 0;
200        data.SelectionEnd = len;
201    }
202
203    /// Clear selection
204    pub fn clear_selection(&mut self) {
205        let cursor_pos = Self::position_to_i32("cursor position", self.cursor_pos());
206        let data = self.data_mut();
207        data.SelectionStart = cursor_pos;
208        data.SelectionEnd = cursor_pos;
209    }
210
211    /// Returns true if there is a selection
212    pub fn has_selection(&self) -> bool {
213        self.data().SelectionStart != self.data().SelectionEnd
214    }
215
216    /// Delete characters in the range [pos, pos+bytes_count)
217    pub fn remove_chars(&mut self, pos: usize, bytes_count: usize) {
218        let text = self.str();
219        let end = pos
220            .checked_add(bytes_count)
221            .expect("delete range overflowed usize");
222        assert!(end <= text.len(), "delete range out of bounds");
223        Self::assert_byte_boundary(text, "delete start", pos);
224        Self::assert_byte_boundary(text, "delete end", end);
225        let pos = Self::position_to_i32("delete start", pos);
226        let bytes_count = Self::position_to_i32("delete byte count", bytes_count);
227        unsafe {
228            sys::ImGuiInputTextCallbackData_DeleteChars(self.0, pos, bytes_count);
229        }
230    }
231
232    /// Insert text at the given position
233    pub fn insert_chars(&mut self, pos: usize, text: &str) {
234        let current = self.str();
235        assert!(pos <= current.len(), "insert position out of bounds");
236        Self::assert_byte_boundary(current, "insert position", pos);
237        let pos = Self::position_to_i32("insert position", pos);
238        let text_ptr = text.as_ptr() as *const std::os::raw::c_char;
239        unsafe {
240            sys::ImGuiInputTextCallbackData_InsertChars(
241                self.0,
242                pos,
243                text_ptr,
244                text_ptr.add(text.len()),
245            );
246        }
247    }
248
249    /// Gives access to the underlying byte array MUTABLY.
250    ///
251    /// ## Safety
252    ///
253    /// This is very unsafe, and the following invariants must be
254    /// upheld:
255    /// 1. Keep the data utf8 valid.
256    /// 2. After editing the string, call [set_dirty].
257    ///
258    /// To truncate the string, please use [remove_chars]. To extend
259    /// the string, please use [insert_chars] and [push_str].
260    ///
261    /// This function should have highly limited usage, but could be for
262    /// editing certain characters in the buffer based on some external condition.
263    ///
264    /// [remove_chars]: Self::remove_chars
265    /// [set_dirty]: Self::set_dirty
266    /// [insert_chars]: Self::insert_chars
267    /// [push_str]: Self::push_str
268    pub unsafe fn str_as_bytes_mut(&mut self) -> &mut [u8] {
269        let len = self.valid_text_len();
270        unsafe {
271            let str = std::str::from_utf8_mut(std::slice::from_raw_parts_mut(
272                self.data_mut().Buf as *mut u8,
273                len,
274            ))
275            .expect("internal imgui error -- it boofed a utf8");
276
277            str.as_bytes_mut()
278        }
279    }
280
281    /// Sets the dirty flag on the text to imgui, indicating that
282    /// it should reapply this string to its internal state.
283    ///
284    /// **NB:** You only need to use this method if you're using `[str_as_bytes_mut]`.
285    /// If you use the helper methods [remove_chars] and [insert_chars],
286    /// this will be set for you. However, this is no downside to setting
287    /// the dirty flag spuriously except the minor CPU time imgui will spend.
288    ///
289    /// [str_as_bytes_mut]: Self::str_as_bytes_mut
290    /// [remove_chars]: Self::remove_chars
291    /// [insert_chars]: Self::insert_chars
292    pub fn set_dirty(&mut self) {
293        self.data_mut().BufDirty = true;
294    }
295
296    /// Returns the selected text directly. Note that if no text is selected,
297    /// an empty str slice will be returned.
298    pub fn selected(&self) -> &str {
299        let text = self.str();
300        let start = self.selection_start().min(self.selection_end());
301        let end = self.selection_start().max(self.selection_end());
302        assert!(end <= text.len(), "selection range out of bounds");
303        Self::assert_byte_boundary(text, "selection start", start);
304        Self::assert_byte_boundary(text, "selection end", end);
305        &text[start..end]
306    }
307
308    /// Pushes the given str to the end of this buffer. If this
309    /// would require the String to resize, it will be resized.
310    /// This is automatically handled.
311    pub fn push_str(&mut self, text: &str) {
312        let current_len = self.valid_text_len();
313        self.insert_chars(current_len, text);
314    }
315}
316
317/// This is a ZST which implements InputTextCallbackHandler as a passthrough.
318///
319/// If you do not set a callback handler, this will be used (but will never
320/// actually run, since you will not have passed imgui any flags).
321pub struct PassthroughCallback;
322impl InputTextCallbackHandler for PassthroughCallback {}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    struct DefaultHandler;
329    impl InputTextCallbackHandler for DefaultHandler {}
330
331    fn callback_data_for_text(text: &mut [u8]) -> sys::ImGuiInputTextCallbackData {
332        let len = text.len().saturating_sub(1);
333        let mut data = sys::ImGuiInputTextCallbackData::default();
334        data.Buf = text.as_mut_ptr().cast();
335        data.BufTextLen = len as i32;
336        data.BufSize = text.len() as i32;
337        data.CursorPos = len as i32;
338        data.SelectionStart = 0;
339        data.SelectionEnd = len as i32;
340        data
341    }
342
343    #[test]
344    fn default_char_filter_keeps_character() {
345        let mut handler = DefaultHandler;
346        assert_eq!(handler.char_filter('x'), Some('x'));
347    }
348
349    #[test]
350    fn passthrough_char_filter_keeps_character() {
351        let mut handler = PassthroughCallback;
352        assert_eq!(handler.char_filter('x'), Some('x'));
353    }
354
355    #[test]
356    fn text_callback_positions_accept_bounds() {
357        let mut text = *b"abcd\0";
358        let mut data = callback_data_for_text(&mut text);
359        data.CursorPos = 4;
360        data.SelectionStart = 1;
361        data.SelectionEnd = 3;
362
363        let info = unsafe { TextCallbackData::new(&mut data) };
364
365        assert_eq!(info.cursor_pos(), 4);
366        assert_eq!(info.selection_start(), 1);
367        assert_eq!(info.selection_end(), 3);
368        assert_eq!(info.selected(), "bc");
369    }
370
371    #[test]
372    #[should_panic(expected = "CursorPos exceeded BufTextLen")]
373    fn text_callback_cursor_pos_rejects_out_of_bounds() {
374        let mut text = *b"abcd\0";
375        let mut data = callback_data_for_text(&mut text);
376        data.CursorPos = 5;
377
378        let info = unsafe { TextCallbackData::new(&mut data) };
379        let _ = info.cursor_pos();
380    }
381
382    #[test]
383    #[should_panic(expected = "CursorPos exceeded BufTextLen")]
384    fn text_callback_clear_selection_rejects_out_of_bounds_cursor() {
385        let mut text = *b"abcd\0";
386        let mut data = callback_data_for_text(&mut text);
387        data.CursorPos = 5;
388
389        let mut info = unsafe { TextCallbackData::new(&mut data) };
390        info.clear_selection();
391    }
392}