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}