Skip to main content

slt/context/widgets_display/
text.rs

1use super::*;
2use crate::KeyMap;
3
4impl Context {
5    /// Render a text element. Returns `&mut Self` for style chaining.
6    ///
7    /// # Example
8    ///
9    /// ```no_run
10    /// # slt::run(|ui: &mut slt::Context| {
11    /// use slt::Color;
12    /// ui.text("hello").bold().fg(Color::Cyan);
13    /// # });
14    /// ```
15    pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
16        let content = s.into();
17        let default_fg = self
18            .rollback
19            .text_color_stack
20            .iter()
21            .rev()
22            .find_map(|c| *c)
23            .unwrap_or(self.theme.text);
24        self.commands.push(Command::Text {
25            content,
26            cursor_offset: None,
27            style: Style::new().fg(default_fg),
28            grow: 0,
29            align: Align::Start,
30            wrap: false,
31            truncate: false,
32            margin: Margin::default(),
33            constraints: Constraints::default(),
34        });
35        self.rollback.last_text_idx = Some(self.commands.len() - 1);
36        self
37    }
38
39    /// Render a clickable hyperlink.
40    ///
41    /// The link is interactive: clicking it (or pressing Enter/Space when
42    /// focused) opens the URL in the system browser. OSC 8 is also emitted
43    /// for terminals that support native hyperlinks.
44    #[allow(clippy::print_stderr)]
45    pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
46        let url_str = url.into();
47        let focused = self.register_focusable();
48        let (_interaction_id, response) = self.begin_widget_interaction(focused);
49
50        let activated = response.clicked || self.consume_activation_keys(focused);
51
52        if activated {
53            if let Err(e) = open_url(&url_str) {
54                eprintln!("[slt] failed to open URL: {e}");
55            }
56        }
57
58        let style = if focused {
59            Style::new()
60                .fg(self.theme.primary)
61                .bg(self.theme.surface_hover)
62                .underline()
63                .bold()
64        } else if response.hovered {
65            Style::new()
66                .fg(self.theme.accent)
67                .bg(self.theme.surface_hover)
68                .underline()
69        } else {
70            Style::new().fg(self.theme.primary).underline()
71        };
72
73        self.commands.push(Command::Link {
74            text: text.into(),
75            url: url_str,
76            style,
77            margin: Margin::default(),
78            constraints: Constraints::default(),
79        });
80        self.rollback.last_text_idx = Some(self.commands.len() - 1);
81        self
82    }
83
84    /// Render an elapsed time display.
85    ///
86    /// Formats as `HH:MM:SS.CC` when hours are non-zero, otherwise `MM:SS.CC`.
87    pub fn timer_display(&mut self, elapsed: std::time::Duration) -> &mut Self {
88        let total_centis = elapsed.as_millis() / 10;
89        let centis = total_centis % 100;
90        let total_seconds = total_centis / 100;
91        let seconds = total_seconds % 60;
92        let minutes = (total_seconds / 60) % 60;
93        let hours = total_seconds / 3600;
94
95        let content = if hours > 0 {
96            format!("{hours:02}:{minutes:02}:{seconds:02}.{centis:02}")
97        } else {
98            format!("{minutes:02}:{seconds:02}.{centis:02}")
99        };
100
101        self.commands.push(Command::Text {
102            content,
103            cursor_offset: None,
104            style: Style::new().fg(self.theme.text),
105            grow: 0,
106            align: Align::Start,
107            wrap: false,
108            truncate: false,
109            margin: Margin::default(),
110            constraints: Constraints::default(),
111        });
112        self.rollback.last_text_idx = Some(self.commands.len() - 1);
113        self
114    }
115
116    /// Render help bar from a KeyMap. Shows visible bindings as key-description pairs.
117    pub fn help_from_keymap(&mut self, keymap: &KeyMap) -> Response {
118        let pairs: Vec<(&str, &str)> = keymap
119            .visible_bindings()
120            .map(|binding| (binding.display.as_str(), binding.description.as_str()))
121            .collect();
122        self.help(&pairs)
123    }
124
125    // ── style chain (applies to last text) ───────────────────────────
126
127    /// Apply bold to the last rendered text element.
128    pub fn bold(&mut self) -> &mut Self {
129        self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
130        self
131    }
132
133    /// Apply dim styling to the last rendered text element.
134    ///
135    /// Also sets the foreground color to the theme's `text_dim` color if no
136    /// explicit foreground has been set.
137    pub fn dim(&mut self) -> &mut Self {
138        let text_dim = self.theme.text_dim;
139        self.modify_last_style(|s| {
140            s.modifiers |= Modifiers::DIM;
141            if s.fg.is_none() {
142                s.fg = Some(text_dim);
143            }
144        });
145        self
146    }
147
148    /// Apply italic to the last rendered text element.
149    pub fn italic(&mut self) -> &mut Self {
150        self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
151        self
152    }
153
154    /// Apply underline to the last rendered text element.
155    pub fn underline(&mut self) -> &mut Self {
156        self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
157        self
158    }
159
160    /// Apply reverse-video to the last rendered text element.
161    pub fn reversed(&mut self) -> &mut Self {
162        self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
163        self
164    }
165
166    /// Apply strikethrough to the last rendered text element.
167    pub fn strikethrough(&mut self) -> &mut Self {
168        self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
169        self
170    }
171
172    /// Set the foreground color of the last rendered text element.
173    pub fn fg(&mut self, color: Color) -> &mut Self {
174        self.modify_last_style(|s| s.fg = Some(color));
175        self
176    }
177
178    /// Set the background color of the last rendered text element.
179    pub fn bg(&mut self, color: Color) -> &mut Self {
180        self.modify_last_style(|s| s.bg = Some(color));
181        self
182    }
183
184    /// Apply a per-character foreground gradient to the last rendered text.
185    pub fn gradient(&mut self, from: Color, to: Color) -> &mut Self {
186        if let Some(idx) = self.rollback.last_text_idx {
187            let replacement = match &self.commands[idx] {
188                Command::Text {
189                    content,
190                    style,
191                    wrap,
192                    align,
193                    margin,
194                    constraints,
195                    ..
196                } => {
197                    let chars: Vec<char> = content.chars().collect();
198                    let len = chars.len();
199                    let denom = len.saturating_sub(1).max(1) as f32;
200                    let segments = chars
201                        .into_iter()
202                        .enumerate()
203                        .map(|(i, ch)| {
204                            let mut seg_style = *style;
205                            seg_style.fg = Some(from.blend(to, i as f32 / denom));
206                            (ch.to_string(), seg_style)
207                        })
208                        .collect();
209
210                    Some(Command::RichText {
211                        segments,
212                        wrap: *wrap,
213                        align: *align,
214                        margin: *margin,
215                        constraints: *constraints,
216                    })
217                }
218                _ => None,
219            };
220
221            if let Some(command) = replacement {
222                self.commands[idx] = command;
223            }
224        }
225
226        self
227    }
228
229    /// Set foreground color when the current group is hovered or focused.
230    pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
231        let apply_group_style = self
232            .rollback
233            .group_stack
234            .last()
235            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
236            .unwrap_or(false);
237        if apply_group_style {
238            self.modify_last_style(|s| s.fg = Some(color));
239        }
240        self
241    }
242
243    /// Set background color when the current group is hovered or focused.
244    pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
245        let apply_group_style = self
246            .rollback
247            .group_stack
248            .last()
249            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
250            .unwrap_or(false);
251        if apply_group_style {
252            self.modify_last_style(|s| s.bg = Some(color));
253        }
254        self
255    }
256
257    /// Render a text element with an explicit [`Style`] applied immediately.
258    ///
259    /// Equivalent to calling `text(s)` followed by style-chain methods, but
260    /// more concise when you already have a `Style` value.
261    pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
262        self.styled_with_cursor(s, style, None)
263    }
264
265    pub(crate) fn styled_with_cursor(
266        &mut self,
267        s: impl Into<String>,
268        style: Style,
269        cursor_offset: Option<usize>,
270    ) -> &mut Self {
271        self.commands.push(Command::Text {
272            content: s.into(),
273            cursor_offset,
274            style,
275            grow: 0,
276            align: Align::Start,
277            wrap: false,
278            truncate: false,
279            margin: Margin::default(),
280            constraints: Constraints::default(),
281        });
282        self.rollback.last_text_idx = Some(self.commands.len() - 1);
283        self
284    }
285
286    /// Enable word-boundary wrapping on the last rendered text element.
287    pub fn wrap(&mut self) -> &mut Self {
288        if let Some(idx) = self.rollback.last_text_idx {
289            if let Command::Text { wrap, .. } = &mut self.commands[idx] {
290                *wrap = true;
291            }
292        }
293        self
294    }
295
296    /// Truncate the last rendered text with `…` when it exceeds its allocated width.
297    /// Use with `.w()` to set a fixed width, or let the parent container constrain it.
298    pub fn truncate(&mut self) -> &mut Self {
299        if let Some(idx) = self.rollback.last_text_idx {
300            if let Command::Text { truncate, .. } = &mut self.commands[idx] {
301                *truncate = true;
302            }
303        }
304        self
305    }
306
307    fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
308        if let Some(idx) = self.rollback.last_text_idx {
309            match &mut self.commands[idx] {
310                Command::Text { style, .. } | Command::Link { style, .. } => f(style),
311                _ => {}
312            }
313        }
314    }
315
316    fn modify_last_constraints(&mut self, f: impl FnOnce(&mut Constraints)) {
317        if let Some(idx) = self.rollback.last_text_idx {
318            match &mut self.commands[idx] {
319                Command::Text { constraints, .. } | Command::Link { constraints, .. } => {
320                    f(constraints)
321                }
322                _ => {}
323            }
324        }
325    }
326
327    fn modify_last_margin(&mut self, f: impl FnOnce(&mut Margin)) {
328        if let Some(idx) = self.rollback.last_text_idx {
329            match &mut self.commands[idx] {
330                Command::Text { margin, .. } | Command::Link { margin, .. } => f(margin),
331                _ => {}
332            }
333        }
334    }
335
336    // ── containers ───────────────────────────────────────────────────
337
338    /// Set the flex-grow factor of the last rendered text element.
339    ///
340    /// A value of `1` causes the element to expand and fill remaining space
341    /// along the main axis.
342    pub fn grow(&mut self, value: u16) -> &mut Self {
343        if let Some(idx) = self.rollback.last_text_idx {
344            if let Command::Text { grow, .. } = &mut self.commands[idx] {
345                *grow = value;
346            }
347        }
348        self
349    }
350
351    /// Set the text alignment of the last rendered text element.
352    pub fn align(&mut self, align: Align) -> &mut Self {
353        if let Some(idx) = self.rollback.last_text_idx {
354            if let Command::Text {
355                align: text_align, ..
356            } = &mut self.commands[idx]
357            {
358                *text_align = align;
359            }
360        }
361        self
362    }
363
364    /// Center-align the last rendered text element horizontally.
365    /// Shorthand for `.align(Align::Center)`. Requires the text to have
366    /// a width constraint (via `.w()` or parent container) to be visible.
367    pub fn text_center(&mut self) -> &mut Self {
368        self.align(Align::Center)
369    }
370
371    /// Right-align the last rendered text element horizontally.
372    /// Shorthand for `.align(Align::End)`.
373    pub fn text_right(&mut self) -> &mut Self {
374        self.align(Align::End)
375    }
376
377    // ── size constraints on last text/link ──────────────────────────
378
379    /// Set a fixed width on the last rendered text or link element.
380    ///
381    /// Sets the [`WidthSpec`](crate::WidthSpec) to `Fixed(value)`, making the
382    /// element occupy exactly that many columns (padded with spaces or
383    /// truncated).
384    pub fn w(&mut self, value: u32) -> &mut Self {
385        self.modify_last_constraints(|c| {
386            *c = c.w(value);
387        });
388        self
389    }
390
391    /// Set a fixed height on the last rendered text or link element.
392    ///
393    /// Sets the [`HeightSpec`](crate::HeightSpec) to `Fixed(value)`.
394    pub fn h(&mut self, value: u32) -> &mut Self {
395        self.modify_last_constraints(|c| {
396            *c = c.h(value);
397        });
398        self
399    }
400
401    /// Set the minimum width on the last rendered text or link element.
402    pub fn min_w(&mut self, value: u32) -> &mut Self {
403        self.modify_last_constraints(|c| c.set_min_width(Some(value)));
404        self
405    }
406
407    /// Set the maximum width on the last rendered text or link element.
408    pub fn max_w(&mut self, value: u32) -> &mut Self {
409        self.modify_last_constraints(|c| c.set_max_width(Some(value)));
410        self
411    }
412
413    /// Set the minimum height on the last rendered text or link element.
414    pub fn min_h(&mut self, value: u32) -> &mut Self {
415        self.modify_last_constraints(|c| c.set_min_height(Some(value)));
416        self
417    }
418
419    /// Set the maximum height on the last rendered text or link element.
420    pub fn max_h(&mut self, value: u32) -> &mut Self {
421        self.modify_last_constraints(|c| c.set_max_height(Some(value)));
422        self
423    }
424
425    // ── margin on last text/link ────────────────────────────────────
426
427    /// Set uniform margin on all sides of the last rendered text or link element.
428    pub fn m(&mut self, value: u32) -> &mut Self {
429        self.modify_last_margin(|m| *m = Margin::all(value));
430        self
431    }
432
433    /// Set horizontal margin (left + right) on the last rendered text or link.
434    pub fn mx(&mut self, value: u32) -> &mut Self {
435        self.modify_last_margin(|m| {
436            m.left = value;
437            m.right = value;
438        });
439        self
440    }
441
442    /// Set vertical margin (top + bottom) on the last rendered text or link.
443    pub fn my(&mut self, value: u32) -> &mut Self {
444        self.modify_last_margin(|m| {
445            m.top = value;
446            m.bottom = value;
447        });
448        self
449    }
450
451    /// Set top margin on the last rendered text or link element.
452    pub fn mt(&mut self, value: u32) -> &mut Self {
453        self.modify_last_margin(|m| m.top = value);
454        self
455    }
456
457    /// Set right margin on the last rendered text or link element.
458    pub fn mr(&mut self, value: u32) -> &mut Self {
459        self.modify_last_margin(|m| m.right = value);
460        self
461    }
462
463    /// Set bottom margin on the last rendered text or link element.
464    pub fn mb(&mut self, value: u32) -> &mut Self {
465        self.modify_last_margin(|m| m.bottom = value);
466        self
467    }
468
469    /// Set left margin on the last rendered text or link element.
470    pub fn ml(&mut self, value: u32) -> &mut Self {
471        self.modify_last_margin(|m| m.left = value);
472        self
473    }
474
475    /// Render an invisible spacer that expands to fill available space.
476    ///
477    /// Useful for pushing siblings to opposite ends of a row or column.
478    pub fn spacer(&mut self) -> &mut Self {
479        self.commands.push(Command::Spacer { grow: 1 });
480        self.rollback.last_text_idx = None;
481        self
482    }
483
484    // ── conditional / grouped style helpers ─────────────────────────
485
486    /// Apply `f` only if `cond` is true. Returns `self` so chaining continues.
487    ///
488    /// Use this to attach a block of style modifiers to the last rendered text
489    /// without breaking the fluent chain. The closure receives the same
490    /// `&mut Context`, so any style-chain method (`.bold()`, `.fg()`, etc.)
491    /// applies to the most recent text element.
492    ///
493    /// Zero allocation: the closure is inlined and skipped entirely when
494    /// `cond` is `false`.
495    ///
496    /// # Example
497    ///
498    /// ```no_run
499    /// # slt::run(|ui: &mut slt::Context| {
500    /// use slt::Color;
501    /// let is_error = true;
502    /// let is_selected = false;
503    /// ui.text("Status")
504    ///     .with_if(is_error, |t| {
505    ///         t.bold().fg(Color::Red);
506    ///     })
507    ///     .with_if(is_selected, |t| {
508    ///         t.bg(Color::DarkGray);
509    ///     });
510    /// # });
511    /// ```
512    pub fn with_if(&mut self, cond: bool, f: impl FnOnce(&mut Self)) -> &mut Self {
513        if cond {
514            f(self);
515        }
516        self
517    }
518
519    /// Apply `f` unconditionally. Useful for factoring out a block of modifier
520    /// calls that should always run, while keeping the fluent chain intact.
521    ///
522    /// # Example
523    ///
524    /// ```no_run
525    /// # slt::run(|ui: &mut slt::Context| {
526    /// use slt::Color;
527    /// ui.text("hi").with(|t| {
528    ///     t.bold().fg(Color::Cyan);
529    /// });
530    /// # });
531    /// ```
532    pub fn with(&mut self, f: impl FnOnce(&mut Self)) -> &mut Self {
533        f(self);
534        self
535    }
536}