Skip to main content

dear_imgui_rs/
text_filter.rs

1//! Text filtering functionality for Dear ImGui
2//!
3//! This module provides a text filter system that allows users to filter content
4//! based on text patterns. The filter supports include/exclude syntax similar to
5//! many search interfaces.
6//!
7//! # Basic Usage
8//!
9//! ```no_run
10//! # use dear_imgui_rs::*;
11//! # let mut ctx = Context::create();
12//! # let ui = ctx.frame();
13//! let mut filter = TextFilter::new("Search");
14//!
15//! // Draw the filter input
16//! filter.draw(&ui);
17//!
18//! // Test if text passes the filter
19//! if filter.pass_filter("some text") {
20//!     // Display matching content
21//! }
22//! ```
23//!
24//! # Filter Syntax
25//!
26//! The filter supports the following syntax:
27//! - `word` - Include items containing "word"
28//! - `-word` - Exclude items containing "word"
29//! - `word1,word2` - Include items containing "word1" OR "word2"
30//! - `word1,-word2` - Include items containing "word1" but NOT "word2"
31
32use crate::{Ui, sys};
33use std::ops::Range;
34use std::os::raw::c_char;
35use std::ptr;
36
37/// Helper to parse and apply text filters
38///
39/// This struct provides text filtering functionality similar to many search interfaces.
40/// It supports include/exclude patterns and can be used to filter lists of items.
41///
42/// # Examples
43///
44/// ```no_run
45/// # use dear_imgui_rs::*;
46/// # let mut ctx = Context::create();
47/// # let ui = ctx.frame();
48/// // Create a filter with default empty pattern
49/// let mut filter = TextFilter::new("Search".to_string());
50///
51/// // Create a filter with initial pattern
52/// let mut filter_with_pattern = TextFilter::new_with_filter(
53///     "Advanced Search".to_string(),
54///     "include,-exclude".to_string()
55/// );
56/// ```
57pub struct TextFilter {
58    label: String,
59    raw: *mut sys::ImGuiTextFilter,
60}
61
62impl TextFilter {
63    /// Creates a new TextFilter with an empty filter.
64    ///
65    /// This is equivalent to [`new_with_filter`](Self::new_with_filter) with `filter` set to `""`.
66    ///
67    /// # Arguments
68    /// * `label` - The label to display for the filter input
69    ///
70    /// # Examples
71    ///
72    /// ```no_run
73    /// # use dear_imgui_rs::*;
74    /// let filter = TextFilter::new("Search");
75    /// ```
76    pub fn new(label: impl Into<String>) -> Self {
77        Self::new_with_filter(label, "")
78    }
79
80    /// Creates a new TextFilter with a custom filter pattern.
81    ///
82    /// # Arguments
83    /// * `label` - The label to display for the filter input
84    /// * `filter` - The initial filter pattern
85    ///
86    /// # Examples
87    ///
88    /// ```no_run
89    /// # use dear_imgui_rs::*;
90    /// let filter = TextFilter::new_with_filter(
91    ///     "Search",
92    ///     "include,-exclude"
93    /// );
94    /// ```
95    pub fn new_with_filter(label: impl Into<String>, filter: impl AsRef<str>) -> Self {
96        let label = label.into();
97        let filter_ptr = crate::string::tls_scratch_txt(filter);
98        unsafe {
99            let raw = sys::ImGuiTextFilter_ImGuiTextFilter(filter_ptr);
100            if raw.is_null() {
101                panic!("ImGuiTextFilter_ImGuiTextFilter() returned null");
102            }
103            Self { label, raw }
104        }
105    }
106
107    /// Builds the TextFilter with its current filter pattern.
108    ///
109    /// You can use [`pass_filter`](Self::pass_filter) after calling this method.
110    /// If you want to control the filter with an InputText, use [`draw`](Self::draw) instead.
111    ///
112    /// # Examples
113    ///
114    /// ```no_run
115    /// # use dear_imgui_rs::*;
116    /// let mut filter = TextFilter::new_with_filter(
117    ///     "Search".to_string(),
118    ///     "test".to_string()
119    /// );
120    /// filter.build();
121    ///
122    /// if filter.pass_filter("test string") {
123    ///     println!("Text matches filter!");
124    /// }
125    /// ```
126    pub fn build(&mut self) {
127        unsafe {
128            sys::ImGuiTextFilter_Build(self.raw);
129        }
130    }
131
132    /// Draws an InputText widget to control the filter.
133    ///
134    /// This is equivalent to [`draw_with_size`](Self::draw_with_size) with `size` set to `0.0`.
135    /// Returns `true` if the filter was modified.
136    ///
137    /// # Examples
138    ///
139    /// ```no_run
140    /// # use dear_imgui_rs::*;
141    /// # let mut ctx = Context::create();
142    /// # let ui = ctx.frame();
143    /// let mut filter = TextFilter::new("Search");
144    ///
145    /// if filter.draw(&ui) {
146    ///     println!("Filter was modified!");
147    /// }
148    /// ```
149    pub fn draw(&mut self, ui: &Ui) -> bool {
150        self.draw_with_size(ui, 0.0)
151    }
152
153    /// Draws an InputText widget to control the filter with a specific width.
154    ///
155    /// # Arguments
156    /// * `width` - The width of the input text widget (0.0 for default width)
157    ///
158    /// Returns `true` if the filter was modified.
159    ///
160    /// # Examples
161    ///
162    /// ```no_run
163    /// # use dear_imgui_rs::*;
164    /// # let mut ctx = Context::create();
165    /// # let ui = ctx.frame();
166    /// let mut filter = TextFilter::new("Search");
167    ///
168    /// if filter.draw_with_size(&ui, 200.0) {
169    ///     println!("Filter was modified!");
170    /// }
171    /// ```
172    pub fn draw_with_size(&mut self, ui: &Ui, width: f32) -> bool {
173        assert!(
174            width.is_finite(),
175            "TextFilter::draw_with_size() width must be finite"
176        );
177        let label_ptr = ui.scratch_txt(&self.label);
178        ui.run_with_bound_context(|| unsafe {
179            sys::ImGuiTextFilter_Draw(self.raw, label_ptr, width)
180        })
181    }
182
183    /// Returns true if the filter is not empty.
184    ///
185    /// An empty filter (no pattern specified) will match all text.
186    ///
187    /// # Examples
188    ///
189    /// ```no_run
190    /// # use dear_imgui_rs::*;
191    /// let empty_filter = TextFilter::new("Search");
192    /// assert!(!empty_filter.is_active());
193    ///
194    /// let active_filter = TextFilter::new_with_filter(
195    ///     "Search",
196    ///     "test"
197    /// );
198    /// assert!(active_filter.is_active());
199    /// ```
200    pub fn is_active(&self) -> bool {
201        // IsActive() is an inline method: return !Filters.empty();
202        // We need to check if the Filters vector is empty
203        unsafe { (*self.raw).Filters.Size > 0 }
204    }
205
206    /// Returns true if the text matches the filter.
207    ///
208    /// [`draw`](Self::draw) or [`build`](Self::build) must be called **before** this function.
209    ///
210    /// # Arguments
211    /// * `text` - The text to test against the filter
212    ///
213    /// # Examples
214    ///
215    /// ```no_run
216    /// # use dear_imgui_rs::*;
217    /// let mut filter = TextFilter::new_with_filter(
218    ///     "Search",
219    ///     "test"
220    /// );
221    /// filter.build();
222    ///
223    /// assert!(filter.pass_filter("test string"));
224    /// assert!(!filter.pass_filter("example string"));
225    /// ```
226    pub fn pass_filter(&self, text: &str) -> bool {
227        let text_ptr = crate::string::tls_scratch_txt(text);
228        unsafe { sys::ImGuiTextFilter_PassFilter(self.raw, text_ptr, ptr::null()) }
229    }
230
231    /// Returns true if a substring range matches the filter.
232    ///
233    /// This is the safe Rust equivalent of `PassFilter(text, text_end)` in Dear ImGui,
234    /// where `text_end` points somewhere inside the same buffer as `text`.
235    ///
236    /// `range` is in bytes and must lie on UTF-8 char boundaries.
237    pub fn pass_filter_range(&self, text: &str, range: Range<usize>) -> bool {
238        if range.start > range.end || range.end > text.len() {
239            return false;
240        }
241        if !text.is_char_boundary(range.start) || !text.is_char_boundary(range.end) {
242            return false;
243        }
244
245        let start_ptr = unsafe { text.as_ptr().add(range.start) as *const c_char };
246        let end_ptr = unsafe { text.as_ptr().add(range.end) as *const c_char };
247        unsafe { sys::ImGuiTextFilter_PassFilter(self.raw, start_ptr, end_ptr) }
248    }
249
250    /// Clears the filter pattern.
251    ///
252    /// This sets the filter to an empty state, which will match all text.
253    ///
254    /// # Examples
255    ///
256    /// ```no_run
257    /// # use dear_imgui_rs::*;
258    /// let mut filter = TextFilter::new_with_filter(
259    ///     "Search",
260    ///     "test"
261    /// );
262    ///
263    /// assert!(filter.is_active());
264    /// filter.clear();
265    /// assert!(!filter.is_active());
266    /// ```
267    pub fn clear(&mut self) {
268        // Clear() is an inline method: InputBuf[0] = 0; Build();
269        unsafe {
270            (*self.raw).InputBuf[0] = 0;
271            sys::ImGuiTextFilter_Build(self.raw);
272        }
273    }
274}
275
276impl Drop for TextFilter {
277    fn drop(&mut self) {
278        unsafe { sys::ImGuiTextFilter_destroy(self.raw) }
279    }
280}
281
282impl Ui {
283    /// Creates a new TextFilter with an empty pattern.
284    ///
285    /// This is a convenience method equivalent to [`TextFilter::new`].
286    ///
287    /// # Arguments
288    /// * `label` - The label to display for the filter input
289    ///
290    /// # Examples
291    ///
292    /// ```no_run
293    /// # use dear_imgui_rs::*;
294    /// # let mut ctx = Context::create();
295    /// # let ui = ctx.frame();
296    /// let filter = ui.text_filter("Search");
297    /// ```
298    pub fn text_filter(&self, label: impl Into<String>) -> TextFilter {
299        TextFilter::new(label)
300    }
301
302    /// Creates a new TextFilter with a custom filter pattern.
303    ///
304    /// This is a convenience method equivalent to [`TextFilter::new_with_filter`].
305    ///
306    /// # Arguments
307    /// * `label` - The label to display for the filter input
308    /// * `filter` - The initial filter pattern
309    ///
310    /// # Examples
311    ///
312    /// ```no_run
313    /// # use dear_imgui_rs::*;
314    /// # let mut ctx = Context::create();
315    /// # let ui = ctx.frame();
316    /// let filter = ui.text_filter_with_filter(
317    ///     "Search",
318    ///     "include,-exclude"
319    /// );
320    /// ```
321    pub fn text_filter_with_filter(
322        &self,
323        label: impl Into<String>,
324        filter: impl AsRef<str>,
325    ) -> TextFilter {
326        TextFilter::new_with_filter(label, filter)
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use std::sync::Mutex;
334
335    // Dear ImGui maintains a single global "current context". Tests that create
336    // a context must be serialized to avoid `ContextAlreadyActive`.
337    static TEST_CTX_LOCK: Mutex<()> = Mutex::new(());
338
339    #[test]
340    fn text_filter_build_and_pass_filter_work() {
341        let _lock = TEST_CTX_LOCK.lock().unwrap();
342        let _ctx = crate::Context::create();
343
344        let mut filter = TextFilter::new("Search");
345        filter.build();
346        assert!(filter.pass_filter("anything"));
347
348        let mut filter = TextFilter::new_with_filter("Search", "abc");
349        filter.build();
350        assert!(filter.pass_filter("xxabcxx"));
351        assert!(!filter.pass_filter("xxdefxx"));
352    }
353
354    #[test]
355    fn pass_filter_range_validates_bounds_and_char_boundaries() {
356        let _lock = TEST_CTX_LOCK.lock().unwrap();
357        let _ctx = crate::Context::create();
358
359        let mut filter = TextFilter::new_with_filter("Search", "test");
360        filter.build();
361
362        let start = 2usize;
363        let end = 1usize;
364        assert!(!filter.pass_filter_range("abc", start..end));
365        assert!(!filter.pass_filter_range("abc", 0..4));
366        assert!(!filter.pass_filter_range("é", 1..2));
367    }
368
369    #[test]
370    fn pass_filter_range_matches_full_string() {
371        let _lock = TEST_CTX_LOCK.lock().unwrap();
372        let _ctx = crate::Context::create();
373
374        let mut filter = TextFilter::new_with_filter("Search", "test");
375        filter.build();
376
377        let text = "hello test world";
378        assert_eq!(
379            filter.pass_filter(text),
380            filter.pass_filter_range(text, 0..text.len())
381        );
382    }
383
384    #[test]
385    fn draw_uses_owner_ui_context_and_restores_previous_current_context() {
386        let _lock = TEST_CTX_LOCK.lock().unwrap();
387        let mut ctx_a = crate::Context::create();
388        let raw_a = unsafe { crate::sys::igGetCurrentContext() };
389        let raw_b = unsafe { crate::sys::igCreateContext(std::ptr::null_mut()) };
390        assert!(!raw_b.is_null());
391
392        unsafe { crate::sys::igSetCurrentContext(raw_a) };
393        let _ = ctx_a.font_atlas_mut().build();
394        ctx_a.io_mut().set_display_size([128.0, 128.0]);
395        ctx_a.io_mut().set_delta_time(1.0 / 60.0);
396
397        {
398            let ui_a = ctx_a.frame();
399            let _ = ui_a.window("TextFilter owner context").build(|| {
400                let mut filter = TextFilter::new("Search");
401
402                unsafe { crate::sys::igSetCurrentContext(raw_b) };
403                assert_eq!(unsafe { crate::sys::igGetCurrentContext() }, raw_b);
404
405                let _ = filter.draw(&ui_a);
406
407                assert_eq!(unsafe { crate::sys::igGetCurrentContext() }, raw_b);
408            });
409        }
410
411        unsafe { crate::sys::igSetCurrentContext(raw_a) };
412        let _ = ctx_a.render();
413        unsafe { crate::sys::igDestroyContext(raw_b) };
414
415        drop(ctx_a);
416    }
417}