dear_imgui_rs/
string.rs

1//! String helpers (ImString and scratch buffers)
2//!
3//! Utilities for working with strings across the Rust <-> Dear ImGui FFI
4//! boundary.
5//!
6//! - `ImString`: an owned, growable UTF-8 string that maintains a trailing
7//!   NUL byte as required by C APIs. Useful for zero-copy text editing via
8//!   ImGui callbacks.
9//! - `UiBuffer`: an internal scratch buffer used by [`Ui`] methods to stage
10//!   temporary C strings for widget labels and hints.
11//!
12//! Example (zero-copy text input with `ImString`):
13//! ```no_run
14//! # use dear_imgui_rs::*;
15//! # let mut ctx = Context::create();
16//! # let ui = ctx.frame();
17//! let mut s = ImString::with_capacity(256);
18//! if ui.input_text_imstr("Edit", &mut s).build() {
19//!     // edited in-place, no extra copies
20//! }
21//! ```
22//!
23use std::borrow::Cow;
24use std::cell::RefCell;
25use std::fmt;
26use std::ops::{Deref, Index, RangeFull};
27use std::os::raw::c_char;
28use std::str;
29
30/// Internal buffer for UI string operations
31#[derive(Debug)]
32pub struct UiBuffer {
33    pub buffer: Vec<u8>,
34    pub max_len: usize,
35}
36
37impl UiBuffer {
38    /// Creates a new buffer with the specified capacity
39    pub const fn new(max_len: usize) -> Self {
40        Self {
41            buffer: Vec::new(),
42            max_len,
43        }
44    }
45
46    /// Internal method to push a single text to our scratch buffer.
47    ///
48    /// Note: any interior NUL bytes (`'\0'`) will be replaced with `?` to preserve C string semantics.
49    pub fn scratch_txt(&mut self, txt: impl AsRef<str>) -> *const std::os::raw::c_char {
50        self.refresh_buffer();
51
52        let start_of_substr = self.push(txt);
53        unsafe { self.offset(start_of_substr) }
54    }
55
56    /// Internal method to push an option text to our scratch buffer.
57    pub fn scratch_txt_opt(&mut self, txt: Option<impl AsRef<str>>) -> *const std::os::raw::c_char {
58        match txt {
59            Some(v) => self.scratch_txt(v),
60            None => std::ptr::null(),
61        }
62    }
63
64    /// Helper method, same as [`Self::scratch_txt`] but for two strings
65    pub fn scratch_txt_two(
66        &mut self,
67        txt_0: impl AsRef<str>,
68        txt_1: impl AsRef<str>,
69    ) -> (*const std::os::raw::c_char, *const std::os::raw::c_char) {
70        self.refresh_buffer();
71
72        let first_offset = self.push(txt_0);
73        let second_offset = self.push(txt_1);
74
75        unsafe { (self.offset(first_offset), self.offset(second_offset)) }
76    }
77
78    /// Helper method, same as [`Self::scratch_txt`] but with one optional value
79    pub fn scratch_txt_with_opt(
80        &mut self,
81        txt_0: impl AsRef<str>,
82        txt_1: Option<impl AsRef<str>>,
83    ) -> (*const std::os::raw::c_char, *const std::os::raw::c_char) {
84        match txt_1 {
85            Some(value) => self.scratch_txt_two(txt_0, value),
86            None => (self.scratch_txt(txt_0), std::ptr::null()),
87        }
88    }
89
90    /// Attempts to clear the buffer if it's over the maximum length allowed.
91    /// This is to prevent us from making a giant vec over time.
92    pub fn refresh_buffer(&mut self) {
93        if self.buffer.len() > self.max_len {
94            self.buffer.clear();
95        }
96    }
97
98    /// Given a position, gives an offset from the start of the scratch buffer.
99    ///
100    /// # Safety
101    /// This can return a pointer to undefined data if given a `pos >= self.buffer.len()`.
102    /// This is marked as unsafe to reflect that.
103    pub unsafe fn offset(&self, pos: usize) -> *const std::os::raw::c_char {
104        unsafe { self.buffer.as_ptr().add(pos) as *const _ }
105    }
106
107    /// Pushes a new scratch sheet text and return the byte index where the sub-string
108    /// starts.
109    pub fn push(&mut self, txt: impl AsRef<str>) -> usize {
110        let txt = txt.as_ref();
111        let len = self.buffer.len();
112        let bytes = txt.as_bytes();
113        if bytes.contains(&0) {
114            self.buffer
115                .extend(bytes.iter().map(|&b| if b == 0 { b'?' } else { b }));
116        } else {
117            self.buffer.extend(bytes);
118        }
119        self.buffer.push(b'\0');
120
121        len
122    }
123}
124
125thread_local! {
126    static TLS_SCRATCH: RefCell<UiBuffer> = RefCell::new(UiBuffer::new(1024));
127}
128
129/// Creates a temporary, NUL-terminated C string pointer backed by a thread-local scratch buffer.
130///
131/// The returned pointer is only valid until the next scratch-string call on the same thread.
132///
133/// This API is **not re-entrant**: any nested call to `tls_scratch_txt`/`with_scratch_txt` (or `Ui::scratch_txt`)
134/// on the same thread will overwrite the backing buffer and invalidate previously returned pointers.
135pub(crate) fn tls_scratch_txt(txt: impl AsRef<str>) -> *const c_char {
136    TLS_SCRATCH.with(|buf| buf.borrow_mut().scratch_txt(txt))
137}
138
139/// Calls `f` with a temporary, NUL-terminated C string pointer backed by a thread-local scratch buffer.
140///
141/// The pointer is only valid for the duration of the call (and will be overwritten by subsequent
142/// scratch-string operations on the same thread). Like [`tls_scratch_txt`], this is not re-entrant.
143pub fn with_scratch_txt<R>(txt: impl AsRef<str>, f: impl FnOnce(*const c_char) -> R) -> R {
144    TLS_SCRATCH.with(|buf| {
145        let mut buf = buf.borrow_mut();
146        let ptr = buf.scratch_txt(txt);
147        f(ptr)
148    })
149}
150
151/// Same as [`tls_scratch_txt`] but returns two pointers that stay valid together.
152pub(crate) fn tls_scratch_txt_two(
153    txt_0: impl AsRef<str>,
154    txt_1: impl AsRef<str>,
155) -> (*const c_char, *const c_char) {
156    TLS_SCRATCH.with(|buf| buf.borrow_mut().scratch_txt_two(txt_0, txt_1))
157}
158
159/// Calls `f` with two temporary, NUL-terminated C string pointers backed by a thread-local scratch buffer.
160///
161/// Both pointers are valid together for the duration of the call (and will be overwritten by
162/// subsequent scratch-string operations on the same thread).
163pub fn with_scratch_txt_two<R>(
164    txt_0: impl AsRef<str>,
165    txt_1: impl AsRef<str>,
166    f: impl FnOnce(*const c_char, *const c_char) -> R,
167) -> R {
168    TLS_SCRATCH.with(|buf| {
169        let mut buf = buf.borrow_mut();
170        let (a, b) = buf.scratch_txt_two(txt_0, txt_1);
171        f(a, b)
172    })
173}
174
175/// Calls `f` with three temporary, NUL-terminated C string pointers backed by a thread-local scratch buffer.
176///
177/// All pointers are valid together for the duration of the call (and will be overwritten by
178/// subsequent scratch-string operations on the same thread).
179pub fn with_scratch_txt_three<R>(
180    txt_0: impl AsRef<str>,
181    txt_1: impl AsRef<str>,
182    txt_2: impl AsRef<str>,
183    f: impl FnOnce(*const c_char, *const c_char, *const c_char) -> R,
184) -> R {
185    TLS_SCRATCH.with(|buf| {
186        let mut buf = buf.borrow_mut();
187        buf.refresh_buffer();
188        let o0 = buf.push(txt_0);
189        let o1 = buf.push(txt_1);
190        let o2 = buf.push(txt_2);
191        unsafe { f(buf.offset(o0), buf.offset(o1), buf.offset(o2)) }
192    })
193}
194
195/// Calls `f` with a list of temporary, NUL-terminated C string pointers backed by a thread-local scratch buffer.
196///
197/// The pointers are only valid for the duration of the call (and will be overwritten by subsequent
198/// scratch-string operations on the same thread).
199pub fn with_scratch_txt_slice<R>(txts: &[&str], f: impl FnOnce(&[*const c_char]) -> R) -> R {
200    TLS_SCRATCH.with(|buf| {
201        let mut buf = buf.borrow_mut();
202        buf.refresh_buffer();
203
204        let total_bytes: usize = txts.iter().map(|s| s.len() + 1).sum();
205        buf.buffer.reserve(total_bytes);
206
207        let mut offsets: Vec<usize> = Vec::with_capacity(txts.len());
208        for &s in txts {
209            offsets.push(buf.push(s));
210        }
211
212        let mut ptrs: Vec<*const c_char> = Vec::with_capacity(txts.len());
213        for off in offsets {
214            ptrs.push(unsafe { buf.offset(off) });
215        }
216
217        f(&ptrs)
218    })
219}
220
221/// Calls `f` with a list of temporary, NUL-terminated C string pointers and one optional pointer backed by
222/// a thread-local scratch buffer.
223///
224/// The returned pointers are only valid for the duration of the call (and will be overwritten by subsequent
225/// scratch-string operations on the same thread).
226pub fn with_scratch_txt_slice_with_opt<R>(
227    txts: &[&str],
228    txt_opt: Option<&str>,
229    f: impl FnOnce(&[*const c_char], *const c_char) -> R,
230) -> R {
231    TLS_SCRATCH.with(|buf| {
232        let mut buf = buf.borrow_mut();
233        buf.refresh_buffer();
234
235        let total_bytes: usize = txts.iter().map(|s| s.len() + 1).sum::<usize>()
236            + txt_opt.map(|s| s.len() + 1).unwrap_or(0);
237        buf.buffer.reserve(total_bytes);
238
239        let mut offsets: Vec<usize> = Vec::with_capacity(txts.len());
240        for &s in txts {
241            offsets.push(buf.push(s));
242        }
243
244        let opt_off = txt_opt.map(|s| buf.push(s));
245
246        let mut ptrs: Vec<*const c_char> = Vec::with_capacity(txts.len());
247        for off in offsets {
248            ptrs.push(unsafe { buf.offset(off) });
249        }
250
251        let opt_ptr = match opt_off {
252            Some(off) => unsafe { buf.offset(off) },
253            None => std::ptr::null(),
254        };
255
256        f(&ptrs, opt_ptr)
257    })
258}
259
260/// A UTF-8 encoded, growable, implicitly nul-terminated string.
261#[derive(Clone, Hash, Ord, Eq, PartialOrd, PartialEq)]
262pub struct ImString(pub(crate) Vec<u8>);
263
264impl ImString {
265    /// Creates a new `ImString` from an existing string.
266    pub fn new<T: Into<String>>(value: T) -> ImString {
267        let value = value.into();
268        assert!(!value.contains('\0'), "ImString contained null byte");
269        unsafe {
270            let mut s = ImString::from_utf8_unchecked(value.into_bytes());
271            s.refresh_len();
272            s
273        }
274    }
275
276    /// Creates a new empty `ImString` with a particular capacity
277    #[inline]
278    pub fn with_capacity(capacity: usize) -> ImString {
279        let mut v = Vec::with_capacity(capacity + 1);
280        v.push(b'\0');
281        ImString(v)
282    }
283
284    /// Converts a vector of bytes to a `ImString` without checking that the string contains valid
285    /// UTF-8
286    ///
287    /// # Safety
288    ///
289    /// It is up to the caller to guarantee the vector contains valid UTF-8 and no null terminator.
290    #[inline]
291    pub unsafe fn from_utf8_unchecked(mut v: Vec<u8>) -> ImString {
292        v.push(b'\0');
293        ImString(v)
294    }
295
296    /// Converts a vector of bytes to a `ImString` without checking that the string contains valid
297    /// UTF-8
298    ///
299    /// # Safety
300    ///
301    /// It is up to the caller to guarantee the vector contains valid UTF-8 and a null terminator.
302    #[inline]
303    pub unsafe fn from_utf8_with_nul_unchecked(v: Vec<u8>) -> ImString {
304        ImString(v)
305    }
306
307    /// Truncates this `ImString`, removing all contents
308    #[inline]
309    pub fn clear(&mut self) {
310        self.0.clear();
311        self.0.push(b'\0');
312    }
313
314    /// Appends the given character to the end of this `ImString`
315    #[inline]
316    pub fn push(&mut self, ch: char) {
317        let mut buf = [0; 4];
318        self.push_str(ch.encode_utf8(&mut buf));
319    }
320
321    /// Appends a given string slice to the end of this `ImString`
322    #[inline]
323    pub fn push_str(&mut self, string: &str) {
324        assert!(!string.contains('\0'), "ImString contained null byte");
325        self.0.pop();
326        self.0.extend(string.bytes());
327        self.0.push(b'\0');
328        unsafe {
329            self.refresh_len();
330        }
331    }
332
333    /// Returns the capacity of this `ImString` in bytes
334    #[inline]
335    pub fn capacity(&self) -> usize {
336        self.0.capacity() - 1
337    }
338
339    /// Returns the capacity of this `ImString` in bytes, including the implicit null byte
340    #[inline]
341    pub fn capacity_with_nul(&self) -> usize {
342        self.0.capacity()
343    }
344
345    /// Ensures that the capacity of this `ImString` is at least `additional` bytes larger than the
346    /// current length.
347    ///
348    /// The capacity may be increased by more than `additional` bytes.
349    pub fn reserve(&mut self, additional: usize) {
350        self.0.reserve(additional);
351    }
352
353    /// Ensures that the capacity of this `ImString` is at least `additional` bytes larger than the
354    /// current length
355    pub fn reserve_exact(&mut self, additional: usize) {
356        self.0.reserve_exact(additional);
357    }
358
359    /// Returns a raw pointer to the underlying buffer
360    #[inline]
361    pub fn as_ptr(&self) -> *const c_char {
362        self.0.as_ptr() as *const c_char
363    }
364
365    /// Returns a raw mutable pointer to the underlying buffer.
366    ///
367    /// If the underlying data is modified, `refresh_len` *must* be called afterwards.
368    #[inline]
369    pub fn as_mut_ptr(&mut self) -> *mut c_char {
370        self.0.as_mut_ptr() as *mut c_char
371    }
372
373    /// Ensures the internal buffer length matches the requested size (including the trailing NUL).
374    ///
375    /// This is primarily used to prepare the backing storage for C APIs that write into the buffer
376    /// using an explicit `BufSize` parameter (e.g. `InputText`).
377    pub(crate) fn ensure_buf_size(&mut self, buf_size: usize) {
378        if self.0.len() < buf_size {
379            self.0.resize(buf_size, 0);
380        } else if self.0.len() > buf_size {
381            self.0.truncate(buf_size);
382            if let Some(last) = self.0.last_mut() {
383                *last = 0;
384            } else {
385                self.0.push(0);
386            }
387        } else if let Some(last) = self.0.last_mut() {
388            *last = 0;
389        }
390    }
391
392    /// Refreshes the length of the string by searching for the null terminator
393    ///
394    /// # Safety
395    ///
396    /// This function is unsafe because it assumes the buffer contains valid UTF-8
397    /// and has a null terminator somewhere within the allocated capacity.
398    ///
399    /// If the terminator is not within the current Vec length, this will scan up to the full
400    /// allocation capacity. In that case, the caller must ensure that bytes up to the first
401    /// terminator (or the full capacity, if no terminator exists) are initialized.
402    pub unsafe fn refresh_len(&mut self) {
403        if let Some(pos) = self.0.iter().position(|&b| b == 0) {
404            self.0.truncate(pos + 1);
405            return;
406        }
407
408        let cap = self.0.capacity();
409        if cap == 0 {
410            self.0.push(0);
411            return;
412        }
413
414        let ptr = self.0.as_ptr();
415        let bytes = unsafe { std::slice::from_raw_parts(ptr, cap) };
416        let pos = bytes.iter().position(|&b| b == 0).unwrap_or(cap - 1);
417        if pos == cap - 1 && bytes[pos] != 0 {
418            unsafe {
419                *self.0.as_mut_ptr().add(cap - 1) = 0;
420            }
421        }
422        unsafe {
423            self.0.set_len(pos + 1);
424        }
425    }
426
427    /// Returns the length of this `ImString` in bytes, excluding the null terminator
428    pub fn len(&self) -> usize {
429        self.0.len().saturating_sub(1)
430    }
431
432    /// Returns true if this `ImString` is empty
433    pub fn is_empty(&self) -> bool {
434        self.len() == 0
435    }
436
437    /// Converts to a string slice
438    pub fn to_str(&self) -> &str {
439        unsafe { str::from_utf8_unchecked(&self.0[..self.len()]) }
440    }
441}
442
443impl Default for ImString {
444    fn default() -> Self {
445        ImString::with_capacity(0)
446    }
447}
448
449impl fmt::Display for ImString {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        fmt::Display::fmt(self.to_str(), f)
452    }
453}
454
455impl fmt::Debug for ImString {
456    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457        fmt::Debug::fmt(self.to_str(), f)
458    }
459}
460
461impl Deref for ImString {
462    type Target = str;
463    fn deref(&self) -> &str {
464        self.to_str()
465    }
466}
467
468impl AsRef<str> for ImString {
469    fn as_ref(&self) -> &str {
470        self.to_str()
471    }
472}
473
474impl From<String> for ImString {
475    fn from(s: String) -> ImString {
476        ImString::new(s)
477    }
478}
479
480impl From<&str> for ImString {
481    fn from(s: &str) -> ImString {
482        ImString::new(s)
483    }
484}
485
486impl Index<RangeFull> for ImString {
487    type Output = str;
488    fn index(&self, _index: RangeFull) -> &str {
489        self.to_str()
490    }
491}
492
493/// Represents a borrowed string that can be either a Rust string slice or an ImString
494pub type ImStr<'a> = Cow<'a, str>;
495
496/// Creates an ImString from a string literal at compile time
497#[macro_export]
498macro_rules! im_str {
499    ($e:expr) => {{ $crate::ImString::new($e) }};
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use std::ffi::CStr;
506
507    #[test]
508    fn im_string_ensure_buf_size_resizes_and_nul_terminates() {
509        let mut s = ImString::new("abc");
510        s.ensure_buf_size(16);
511        assert_eq!(s.0.len(), 16);
512        assert_eq!(&s.0[..3], b"abc");
513        assert_eq!(s.0[3], 0);
514        assert!(s.0[4..].iter().all(|&b| b == 0));
515    }
516
517    #[test]
518    fn im_string_refresh_len_scans_capacity_when_len_has_no_nul() {
519        let mut v = vec![b'x'; 16];
520        v[..4].copy_from_slice(b"abcd");
521        v[10] = 0;
522        v.truncate(4);
523
524        let mut s = ImString(v);
525        unsafe { s.refresh_len() };
526        assert_eq!(s.to_str(), "abcdxxxxxx");
527        assert_eq!(s.0.last().copied(), Some(0));
528    }
529
530    #[test]
531    fn ui_buffer_push_appends_nul() {
532        let mut buf = UiBuffer::new(1024);
533        let start = buf.push("abc");
534        assert_eq!(start, 0);
535        assert_eq!(&buf.buffer, b"abc\0");
536    }
537
538    #[test]
539    fn ui_buffer_sanitizes_interior_nul() {
540        let mut buf = UiBuffer::new(1024);
541        let ptr = buf.scratch_txt("a\0b");
542        let s = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap();
543        assert_eq!(s, "a?b");
544    }
545
546    #[test]
547    fn tls_scratch_txt_is_nul_terminated() {
548        let ptr = tls_scratch_txt("hello");
549        let s = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap();
550        assert_eq!(s, "hello");
551    }
552
553    #[test]
554    fn tls_scratch_txt_two_returns_two_valid_strings() {
555        let (a_ptr, b_ptr) = tls_scratch_txt_two("a", "bcd");
556        let a = unsafe { CStr::from_ptr(a_ptr) }.to_str().unwrap();
557        let b = unsafe { CStr::from_ptr(b_ptr) }.to_str().unwrap();
558        assert_eq!(a, "a");
559        assert_eq!(b, "bcd");
560    }
561
562    #[test]
563    fn with_scratch_txt_slice_returns_sequential_pointers() {
564        with_scratch_txt_slice(&["a", "bc", "def"], |ptrs| {
565            assert_eq!(ptrs.len(), 3);
566
567            let a = unsafe { CStr::from_ptr(ptrs[0]) }.to_str().unwrap();
568            let b = unsafe { CStr::from_ptr(ptrs[1]) }.to_str().unwrap();
569            let c = unsafe { CStr::from_ptr(ptrs[2]) }.to_str().unwrap();
570            assert_eq!(a, "a");
571            assert_eq!(b, "bc");
572            assert_eq!(c, "def");
573
574            let ab = (ptrs[1] as usize) - (ptrs[0] as usize);
575            let bc = (ptrs[2] as usize) - (ptrs[1] as usize);
576            assert_eq!(ab, "a".len() + 1);
577            assert_eq!(bc, "bc".len() + 1);
578        });
579    }
580
581    #[test]
582    fn with_scratch_txt_slice_with_opt_returns_null_for_none() {
583        with_scratch_txt_slice_with_opt(&["a", "bc"], None, |ptrs, opt_ptr| {
584            assert_eq!(ptrs.len(), 2);
585            assert!(opt_ptr.is_null());
586
587            let a = unsafe { CStr::from_ptr(ptrs[0]) }.to_str().unwrap();
588            let b = unsafe { CStr::from_ptr(ptrs[1]) }.to_str().unwrap();
589            assert_eq!(a, "a");
590            assert_eq!(b, "bc");
591        });
592    }
593
594    #[test]
595    fn with_scratch_txt_slice_with_opt_appends_opt_string() {
596        with_scratch_txt_slice_with_opt(&["a", "bc"], Some("fmt"), |ptrs, opt_ptr| {
597            assert_eq!(ptrs.len(), 2);
598            assert!(!opt_ptr.is_null());
599
600            let a = unsafe { CStr::from_ptr(ptrs[0]) }.to_str().unwrap();
601            let b = unsafe { CStr::from_ptr(ptrs[1]) }.to_str().unwrap();
602            let fmt = unsafe { CStr::from_ptr(opt_ptr) }.to_str().unwrap();
603            assert_eq!(a, "a");
604            assert_eq!(b, "bc");
605            assert_eq!(fmt, "fmt");
606
607            let ab = (ptrs[1] as usize) - (ptrs[0] as usize);
608            let bf = (opt_ptr as usize) - (ptrs[1] as usize);
609            assert_eq!(ab, "a".len() + 1);
610            assert_eq!(bf, "bc".len() + 1);
611        });
612    }
613
614    #[test]
615    #[should_panic(expected = "null byte")]
616    fn imstring_new_rejects_interior_nul() {
617        let _ = ImString::new("a\0b");
618    }
619
620    #[test]
621    #[should_panic(expected = "null byte")]
622    fn imstring_push_str_rejects_interior_nul() {
623        let mut s = ImString::new("a");
624        s.push_str("b\0c");
625    }
626}