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::fmt;
25use std::ops::{Deref, Index, RangeFull};
26use std::os::raw::c_char;
27use std::str;
28
29/// Internal buffer for UI string operations
30#[derive(Debug)]
31pub struct UiBuffer {
32    pub buffer: Vec<u8>,
33    pub max_len: usize,
34}
35
36impl UiBuffer {
37    /// Creates a new buffer with the specified capacity
38    pub const fn new(max_len: usize) -> Self {
39        Self {
40            buffer: Vec::new(),
41            max_len,
42        }
43    }
44
45    /// Internal method to push a single text to our scratch buffer.
46    pub fn scratch_txt(&mut self, txt: impl AsRef<str>) -> *const std::os::raw::c_char {
47        self.refresh_buffer();
48
49        let start_of_substr = self.push(txt);
50        unsafe { self.offset(start_of_substr) }
51    }
52
53    /// Internal method to push an option text to our scratch buffer.
54    pub fn scratch_txt_opt(&mut self, txt: Option<impl AsRef<str>>) -> *const std::os::raw::c_char {
55        match txt {
56            Some(v) => self.scratch_txt(v),
57            None => std::ptr::null(),
58        }
59    }
60
61    /// Helper method, same as [`Self::scratch_txt`] but for two strings
62    pub fn scratch_txt_two(
63        &mut self,
64        txt_0: impl AsRef<str>,
65        txt_1: impl AsRef<str>,
66    ) -> (*const std::os::raw::c_char, *const std::os::raw::c_char) {
67        self.refresh_buffer();
68
69        let first_offset = self.push(txt_0);
70        let second_offset = self.push(txt_1);
71
72        unsafe { (self.offset(first_offset), self.offset(second_offset)) }
73    }
74
75    /// Helper method, same as [`Self::scratch_txt`] but with one optional value
76    pub fn scratch_txt_with_opt(
77        &mut self,
78        txt_0: impl AsRef<str>,
79        txt_1: Option<impl AsRef<str>>,
80    ) -> (*const std::os::raw::c_char, *const std::os::raw::c_char) {
81        match txt_1 {
82            Some(value) => self.scratch_txt_two(txt_0, value),
83            None => (self.scratch_txt(txt_0), std::ptr::null()),
84        }
85    }
86
87    /// Attempts to clear the buffer if it's over the maximum length allowed.
88    /// This is to prevent us from making a giant vec over time.
89    pub fn refresh_buffer(&mut self) {
90        if self.buffer.len() > self.max_len {
91            self.buffer.clear();
92        }
93    }
94
95    /// Given a position, gives an offset from the start of the scratch buffer.
96    ///
97    /// # Safety
98    /// This can return a pointer to undefined data if given a `pos >= self.buffer.len()`.
99    /// This is marked as unsafe to reflect that.
100    pub unsafe fn offset(&self, pos: usize) -> *const std::os::raw::c_char {
101        unsafe { self.buffer.as_ptr().add(pos) as *const _ }
102    }
103
104    /// Pushes a new scratch sheet text and return the byte index where the sub-string
105    /// starts.
106    pub fn push(&mut self, txt: impl AsRef<str>) -> usize {
107        let len = self.buffer.len();
108        self.buffer.extend(txt.as_ref().as_bytes());
109        self.buffer.push(b'\0');
110
111        len
112    }
113}
114
115/// A UTF-8 encoded, growable, implicitly nul-terminated string.
116#[derive(Clone, Hash, Ord, Eq, PartialOrd, PartialEq)]
117pub struct ImString(pub(crate) Vec<u8>);
118
119impl ImString {
120    /// Creates a new `ImString` from an existing string.
121    pub fn new<T: Into<String>>(value: T) -> ImString {
122        unsafe {
123            let mut s = ImString::from_utf8_unchecked(value.into().into_bytes());
124            s.refresh_len();
125            s
126        }
127    }
128
129    /// Creates a new empty `ImString` with a particular capacity
130    #[inline]
131    pub fn with_capacity(capacity: usize) -> ImString {
132        let mut v = Vec::with_capacity(capacity + 1);
133        v.push(b'\0');
134        ImString(v)
135    }
136
137    /// Converts a vector of bytes to a `ImString` without checking that the string contains valid
138    /// UTF-8
139    ///
140    /// # Safety
141    ///
142    /// It is up to the caller to guarantee the vector contains valid UTF-8 and no null terminator.
143    #[inline]
144    pub unsafe fn from_utf8_unchecked(mut v: Vec<u8>) -> ImString {
145        v.push(b'\0');
146        ImString(v)
147    }
148
149    /// Converts a vector of bytes to a `ImString` without checking that the string contains valid
150    /// UTF-8
151    ///
152    /// # Safety
153    ///
154    /// It is up to the caller to guarantee the vector contains valid UTF-8 and a null terminator.
155    #[inline]
156    pub unsafe fn from_utf8_with_nul_unchecked(v: Vec<u8>) -> ImString {
157        ImString(v)
158    }
159
160    /// Truncates this `ImString`, removing all contents
161    #[inline]
162    pub fn clear(&mut self) {
163        self.0.clear();
164        self.0.push(b'\0');
165    }
166
167    /// Appends the given character to the end of this `ImString`
168    #[inline]
169    pub fn push(&mut self, ch: char) {
170        let mut buf = [0; 4];
171        self.push_str(ch.encode_utf8(&mut buf));
172    }
173
174    /// Appends a given string slice to the end of this `ImString`
175    #[inline]
176    pub fn push_str(&mut self, string: &str) {
177        self.0.pop();
178        self.0.extend(string.bytes());
179        self.0.push(b'\0');
180        unsafe {
181            self.refresh_len();
182        }
183    }
184
185    /// Returns the capacity of this `ImString` in bytes
186    #[inline]
187    pub fn capacity(&self) -> usize {
188        self.0.capacity() - 1
189    }
190
191    /// Returns the capacity of this `ImString` in bytes, including the implicit null byte
192    #[inline]
193    pub fn capacity_with_nul(&self) -> usize {
194        self.0.capacity()
195    }
196
197    /// Ensures that the capacity of this `ImString` is at least `additional` bytes larger than the
198    /// current length.
199    ///
200    /// The capacity may be increased by more than `additional` bytes.
201    pub fn reserve(&mut self, additional: usize) {
202        self.0.reserve(additional);
203    }
204
205    /// Ensures that the capacity of this `ImString` is at least `additional` bytes larger than the
206    /// current length
207    pub fn reserve_exact(&mut self, additional: usize) {
208        self.0.reserve_exact(additional);
209    }
210
211    /// Returns a raw pointer to the underlying buffer
212    #[inline]
213    pub fn as_ptr(&self) -> *const c_char {
214        self.0.as_ptr() as *const c_char
215    }
216
217    /// Returns a raw mutable pointer to the underlying buffer.
218    ///
219    /// If the underlying data is modified, `refresh_len` *must* be called afterwards.
220    #[inline]
221    pub fn as_mut_ptr(&mut self) -> *mut c_char {
222        self.0.as_mut_ptr() as *mut c_char
223    }
224
225    /// Refreshes the length of the string by searching for the null terminator
226    ///
227    /// # Safety
228    ///
229    /// This function is unsafe because it assumes the buffer contains valid UTF-8
230    /// and has a null terminator somewhere within the allocated capacity.
231    pub unsafe fn refresh_len(&mut self) {
232        unsafe {
233            // For now, we'll use a simple implementation without libc
234            // In a real implementation, you'd want to use libc::strlen or similar
235            let mut len = 0;
236            let ptr = self.as_ptr() as *const u8;
237            while *ptr.add(len) != 0 {
238                len += 1;
239            }
240            self.0.set_len(len + 1);
241        }
242    }
243
244    /// Returns the length of this `ImString` in bytes, excluding the null terminator
245    pub fn len(&self) -> usize {
246        self.0.len().saturating_sub(1)
247    }
248
249    /// Returns true if this `ImString` is empty
250    pub fn is_empty(&self) -> bool {
251        self.len() == 0
252    }
253
254    /// Converts to a string slice
255    pub fn to_str(&self) -> &str {
256        unsafe { str::from_utf8_unchecked(&self.0[..self.len()]) }
257    }
258}
259
260impl Default for ImString {
261    fn default() -> Self {
262        ImString::with_capacity(0)
263    }
264}
265
266impl fmt::Display for ImString {
267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        fmt::Display::fmt(self.to_str(), f)
269    }
270}
271
272impl fmt::Debug for ImString {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        fmt::Debug::fmt(self.to_str(), f)
275    }
276}
277
278impl Deref for ImString {
279    type Target = str;
280    fn deref(&self) -> &str {
281        self.to_str()
282    }
283}
284
285impl AsRef<str> for ImString {
286    fn as_ref(&self) -> &str {
287        self.to_str()
288    }
289}
290
291impl From<String> for ImString {
292    fn from(s: String) -> ImString {
293        ImString::new(s)
294    }
295}
296
297impl From<&str> for ImString {
298    fn from(s: &str) -> ImString {
299        ImString::new(s)
300    }
301}
302
303impl Index<RangeFull> for ImString {
304    type Output = str;
305    fn index(&self, _index: RangeFull) -> &str {
306        self.to_str()
307    }
308}
309
310/// Represents a borrowed string that can be either a Rust string slice or an ImString
311pub type ImStr<'a> = Cow<'a, str>;
312
313/// Creates an ImString from a string literal at compile time
314#[macro_export]
315macro_rules! im_str {
316    ($e:expr) => {{ $crate::ImString::new($e) }};
317}