Skip to main content

dear_imgui_rs/widget/
text.rs

1//! Text helpers
2//!
3//! Convenience functions for colored text, wrapped text, disabled text and
4//! label helpers.
5//!
6//! Quick examples:
7//! ```no_run
8//! # use dear_imgui_rs::*;
9//! # let mut ctx = Context::create();
10//! # let ui = ctx.frame();
11//! ui.text("normal");
12//! ui.text_colored([1.0, 0.5, 0.0, 1.0], "warning");
13//! ui.text_disabled("disabled");
14//! ui.text_wrapped("very long text that will wrap when needed...");
15//! ```
16//!
17use crate::Ui;
18use crate::style::StyleColor;
19use crate::sys;
20
21impl Ui {
22    /// Calculates the size required to render text with the current font and font size.
23    ///
24    /// This is equivalent to [`Ui::calc_text_size_with_opts`] with
25    /// `hide_text_after_double_hash` set to `false` and wrapping disabled.
26    #[doc(alias = "CalcTextSize")]
27    pub fn calc_text_size(&self, text: impl AsRef<str>) -> [f32; 2] {
28        self.calc_text_size_with_opts(text, false, -1.0)
29    }
30
31    /// Calculates the size required to render text with explicit display options.
32    ///
33    /// When `hide_text_after_double_hash` is `true`, the `##` label suffix is
34    /// excluded from the measurement. A positive `wrap_width` enables wrapping;
35    /// values at or below zero disable it.
36    ///
37    /// # Panics
38    ///
39    /// Panics if `wrap_width` is not finite.
40    #[doc(alias = "CalcTextSize")]
41    pub fn calc_text_size_with_opts(
42        &self,
43        text: impl AsRef<str>,
44        hide_text_after_double_hash: bool,
45        wrap_width: f32,
46    ) -> [f32; 2] {
47        Self::assert_finite_f32("Ui::calc_text_size_with_opts()", "wrap_width", wrap_width);
48        let text = text.as_ref();
49
50        self.run_with_bound_context(|| unsafe {
51            self.calc_text_size_bound(text, hide_text_after_double_hash, wrap_width)
52        })
53    }
54
55    /// Measures text while the owning ImGui context is already current.
56    ///
57    /// # Safety
58    ///
59    /// The caller must bind this `Ui`'s live ImGui context for the duration of
60    /// the call.
61    pub(crate) unsafe fn calc_text_size_bound(
62        &self,
63        text: &str,
64        hide_text_after_double_hash: bool,
65        wrap_width: f32,
66    ) -> [f32; 2] {
67        let text_range = self.scratch_txt_range(text);
68        let size = unsafe {
69            sys::igCalcTextSize(
70                text_range.start,
71                text_range.end,
72                hide_text_after_double_hash,
73                wrap_width,
74            )
75        };
76        [size.x, size.y]
77    }
78
79    /// Display colored text
80    ///
81    /// This implementation uses zero-copy optimization with `igTextEx`,
82    /// avoiding string allocation and null-termination overhead.
83    ///
84    /// # Example
85    /// ```no_run
86    /// # use dear_imgui_rs::*;
87    /// # let mut ctx = Context::create();
88    /// # let ui = ctx.frame();
89    /// ui.text_colored([1.0, 0.0, 0.0, 1.0], "Red text");
90    /// ui.text_colored([0.0, 1.0, 0.0, 1.0], "Green text");
91    /// ```
92    #[doc(alias = "TextColored")]
93    pub fn text_colored(&self, color: [f32; 4], text: impl AsRef<str>) {
94        let s = text.as_ref();
95
96        // Temporarily set the text color
97        let _token = self.push_style_color(StyleColor::Text, color);
98
99        // Use igTextEx with zero-copy (begin/end pointers)
100        self.run_with_bound_context(|| unsafe {
101            let begin = s.as_ptr() as *const std::os::raw::c_char;
102            let end = begin.add(s.len());
103            sys::igTextEx(begin, end, 0); // ImGuiTextFlags_None = 0
104        })
105    }
106
107    /// Display disabled (grayed out) text
108    ///
109    /// This implementation uses zero-copy optimization with `igTextEx`,
110    /// avoiding string allocation and null-termination overhead.
111    ///
112    /// # Example
113    /// ```no_run
114    /// # use dear_imgui_rs::*;
115    /// # let mut ctx = Context::create();
116    /// # let ui = ctx.frame();
117    /// ui.text_disabled("This option is not available");
118    /// ```
119    #[doc(alias = "TextDisabled")]
120    pub fn text_disabled(&self, text: impl AsRef<str>) {
121        let s = text.as_ref();
122
123        // Get the disabled color from the current style
124        let disabled_color = self.style_color(StyleColor::TextDisabled);
125
126        // Temporarily set the text color to disabled color
127        let _token = self.push_style_color(StyleColor::Text, disabled_color);
128
129        // Use igTextEx with zero-copy (begin/end pointers)
130        self.run_with_bound_context(|| unsafe {
131            let begin = s.as_ptr() as *const std::os::raw::c_char;
132            let end = begin.add(s.len());
133            sys::igTextEx(begin, end, 0); // ImGuiTextFlags_None = 0
134        })
135    }
136
137    /// Display text wrapped to fit the current item width
138    ///
139    /// This uses `PushTextWrapPos + TextUnformatted + PopTextWrapPos` to avoid
140    /// calling C variadic APIs and to keep the input string unformatted.
141    #[doc(alias = "TextWrapped")]
142    pub fn text_wrapped(&self, text: impl AsRef<str>) {
143        let s = text.as_ref();
144        let _wrap = self.push_text_wrap_pos(0.0);
145        self.run_with_bound_context(|| unsafe {
146            let begin = s.as_ptr() as *const std::os::raw::c_char;
147            let end = begin.add(s.len());
148            sys::igTextUnformatted(begin, end);
149        })
150    }
151
152    /// Display a label and text on the same line
153    #[doc(alias = "LabelText")]
154    pub fn label_text(&self, label: impl AsRef<str>, text: impl AsRef<str>) {
155        let (label_ptr, text_ptr) = self.scratch_txt_two(label, text);
156        self.run_with_bound_context(|| unsafe {
157            // Always treat the value as unformatted user text.
158            const FMT: &[u8; 3] = b"%s\0";
159            sys::igLabelText(
160                label_ptr,
161                FMT.as_ptr() as *const std::os::raw::c_char,
162                text_ptr,
163            );
164        })
165    }
166
167    /// Render a hyperlink-style text button. Returns true when clicked.
168    #[doc(alias = "TextLink")]
169    pub fn text_link(&self, label: impl AsRef<str>) -> bool {
170        self.run_with_bound_context(|| unsafe { sys::igTextLink(self.scratch_txt(label)) })
171    }
172
173    /// Render a hyperlink-style text button, and open the given URL when clicked.
174    /// Returns true when clicked.
175    #[doc(alias = "TextLinkOpenURL")]
176    pub fn text_link_open_url(&self, label: impl AsRef<str>, url: impl AsRef<str>) -> bool {
177        let (label_ptr, url_ptr) = self.scratch_txt_two(label, url);
178        self.run_with_bound_context(|| unsafe { sys::igTextLinkOpenURL(label_ptr, url_ptr) })
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    fn setup_context() -> crate::Context {
185        let mut ctx = crate::Context::create();
186        ctx.io_mut().set_display_size([128.0, 128.0]);
187        ctx.io_mut().set_delta_time(1.0 / 60.0);
188        ctx.font_atlas()
189            .try_claim_legacy_renderer()
190            .expect("legacy renderer font atlas should be available")
191            .build();
192        ctx
193    }
194
195    #[test]
196    fn calc_text_size_supports_default_and_advanced_options() {
197        let mut ctx = setup_context();
198        let ui = ctx.frame();
199
200        let default_size = ui.calc_text_size("Column##sort_key");
201        let explicit_default = ui.calc_text_size_with_opts("Column##sort_key", false, -1.0);
202        let visible_label = ui.calc_text_size_with_opts("Column##sort_key", true, -1.0);
203        let trailing_hash = ui.calc_text_size_with_opts("Column#", true, -1.0);
204
205        assert_eq!(default_size, explicit_default);
206        assert!(visible_label[0] < default_size[0]);
207        assert_eq!(visible_label[1], default_size[1]);
208        assert_eq!(trailing_hash, ui.calc_text_size("Column#"));
209
210        let unwrapped = ui.calc_text_size("one two three four");
211        let wrapped = ui.calc_text_size_with_opts("one two three four", false, unwrapped[0] / 2.0);
212        assert!(wrapped[0] < unwrapped[0]);
213        assert!(wrapped[1] > unwrapped[1]);
214
215        for wrap_width in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
216            assert!(
217                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
218                    let _ = ui.calc_text_size_with_opts("text", false, wrap_width);
219                }))
220                .is_err()
221            );
222        }
223    }
224}