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 a text element with word-boundary wrapping.
85    ///
86    /// Long lines are broken at word boundaries to fit the container width.
87    /// Style chaining works the same as [`Context::text`].
88    ///
89    /// **Prefer** `ui.text("...").wrap()` — this method exists for convenience
90    /// but the chaining form is more consistent with the rest of the API.
91    #[deprecated(since = "0.15.4", note = "use ui.text(s).wrap() instead")]
92    pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
93        let content = s.into();
94        let default_fg = self
95            .rollback
96            .text_color_stack
97            .iter()
98            .rev()
99            .find_map(|c| *c)
100            .unwrap_or(self.theme.text);
101        self.commands.push(Command::Text {
102            content,
103            cursor_offset: None,
104            style: Style::new().fg(default_fg),
105            grow: 0,
106            align: Align::Start,
107            wrap: true,
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 an elapsed time display.
117    ///
118    /// Formats as `HH:MM:SS.CC` when hours are non-zero, otherwise `MM:SS.CC`.
119    pub fn timer_display(&mut self, elapsed: std::time::Duration) -> &mut Self {
120        let total_centis = elapsed.as_millis() / 10;
121        let centis = total_centis % 100;
122        let total_seconds = total_centis / 100;
123        let seconds = total_seconds % 60;
124        let minutes = (total_seconds / 60) % 60;
125        let hours = total_seconds / 3600;
126
127        let content = if hours > 0 {
128            format!("{hours:02}:{minutes:02}:{seconds:02}.{centis:02}")
129        } else {
130            format!("{minutes:02}:{seconds:02}.{centis:02}")
131        };
132
133        self.commands.push(Command::Text {
134            content,
135            cursor_offset: None,
136            style: Style::new().fg(self.theme.text),
137            grow: 0,
138            align: Align::Start,
139            wrap: false,
140            truncate: false,
141            margin: Margin::default(),
142            constraints: Constraints::default(),
143        });
144        self.rollback.last_text_idx = Some(self.commands.len() - 1);
145        self
146    }
147
148    /// Render help bar from a KeyMap. Shows visible bindings as key-description pairs.
149    pub fn help_from_keymap(&mut self, keymap: &KeyMap) -> Response {
150        let pairs: Vec<(&str, &str)> = keymap
151            .visible_bindings()
152            .map(|binding| (binding.display.as_str(), binding.description.as_str()))
153            .collect();
154        self.help(&pairs)
155    }
156
157    // ── style chain (applies to last text) ───────────────────────────
158
159    /// Apply bold to the last rendered text element.
160    pub fn bold(&mut self) -> &mut Self {
161        self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
162        self
163    }
164
165    /// Apply dim styling to the last rendered text element.
166    ///
167    /// Also sets the foreground color to the theme's `text_dim` color if no
168    /// explicit foreground has been set.
169    pub fn dim(&mut self) -> &mut Self {
170        let text_dim = self.theme.text_dim;
171        self.modify_last_style(|s| {
172            s.modifiers |= Modifiers::DIM;
173            if s.fg.is_none() {
174                s.fg = Some(text_dim);
175            }
176        });
177        self
178    }
179
180    /// Apply italic to the last rendered text element.
181    pub fn italic(&mut self) -> &mut Self {
182        self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
183        self
184    }
185
186    /// Apply underline to the last rendered text element.
187    pub fn underline(&mut self) -> &mut Self {
188        self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
189        self
190    }
191
192    /// Apply reverse-video to the last rendered text element.
193    pub fn reversed(&mut self) -> &mut Self {
194        self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
195        self
196    }
197
198    /// Apply strikethrough to the last rendered text element.
199    pub fn strikethrough(&mut self) -> &mut Self {
200        self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
201        self
202    }
203
204    /// Set the foreground color of the last rendered text element.
205    pub fn fg(&mut self, color: Color) -> &mut Self {
206        self.modify_last_style(|s| s.fg = Some(color));
207        self
208    }
209
210    /// Set the background color of the last rendered text element.
211    pub fn bg(&mut self, color: Color) -> &mut Self {
212        self.modify_last_style(|s| s.bg = Some(color));
213        self
214    }
215
216    /// Apply a per-character foreground gradient to the last rendered text.
217    pub fn gradient(&mut self, from: Color, to: Color) -> &mut Self {
218        if let Some(idx) = self.rollback.last_text_idx {
219            let replacement = match &self.commands[idx] {
220                Command::Text {
221                    content,
222                    style,
223                    wrap,
224                    align,
225                    margin,
226                    constraints,
227                    ..
228                } => {
229                    let chars: Vec<char> = content.chars().collect();
230                    let len = chars.len();
231                    let denom = len.saturating_sub(1).max(1) as f32;
232                    let segments = chars
233                        .into_iter()
234                        .enumerate()
235                        .map(|(i, ch)| {
236                            let mut seg_style = *style;
237                            seg_style.fg = Some(from.blend(to, i as f32 / denom));
238                            (ch.to_string(), seg_style)
239                        })
240                        .collect();
241
242                    Some(Command::RichText {
243                        segments,
244                        wrap: *wrap,
245                        align: *align,
246                        margin: *margin,
247                        constraints: *constraints,
248                    })
249                }
250                _ => None,
251            };
252
253            if let Some(command) = replacement {
254                self.commands[idx] = command;
255            }
256        }
257
258        self
259    }
260
261    /// Set foreground color when the current group is hovered or focused.
262    pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
263        let apply_group_style = self
264            .rollback
265            .group_stack
266            .last()
267            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
268            .unwrap_or(false);
269        if apply_group_style {
270            self.modify_last_style(|s| s.fg = Some(color));
271        }
272        self
273    }
274
275    /// Set background color when the current group is hovered or focused.
276    pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
277        let apply_group_style = self
278            .rollback
279            .group_stack
280            .last()
281            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
282            .unwrap_or(false);
283        if apply_group_style {
284            self.modify_last_style(|s| s.bg = Some(color));
285        }
286        self
287    }
288
289    /// Render a text element with an explicit [`Style`] applied immediately.
290    ///
291    /// Equivalent to calling `text(s)` followed by style-chain methods, but
292    /// more concise when you already have a `Style` value.
293    pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
294        self.styled_with_cursor(s, style, None)
295    }
296
297    pub(crate) fn styled_with_cursor(
298        &mut self,
299        s: impl Into<String>,
300        style: Style,
301        cursor_offset: Option<usize>,
302    ) -> &mut Self {
303        self.commands.push(Command::Text {
304            content: s.into(),
305            cursor_offset,
306            style,
307            grow: 0,
308            align: Align::Start,
309            wrap: false,
310            truncate: false,
311            margin: Margin::default(),
312            constraints: Constraints::default(),
313        });
314        self.rollback.last_text_idx = Some(self.commands.len() - 1);
315        self
316    }
317
318    /// Enable word-boundary wrapping on the last rendered text element.
319    pub fn wrap(&mut self) -> &mut Self {
320        if let Some(idx) = self.rollback.last_text_idx {
321            if let Command::Text { wrap, .. } = &mut self.commands[idx] {
322                *wrap = true;
323            }
324        }
325        self
326    }
327
328    /// Truncate the last rendered text with `…` when it exceeds its allocated width.
329    /// Use with `.w()` to set a fixed width, or let the parent container constrain it.
330    pub fn truncate(&mut self) -> &mut Self {
331        if let Some(idx) = self.rollback.last_text_idx {
332            if let Command::Text { truncate, .. } = &mut self.commands[idx] {
333                *truncate = true;
334            }
335        }
336        self
337    }
338
339    fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
340        if let Some(idx) = self.rollback.last_text_idx {
341            match &mut self.commands[idx] {
342                Command::Text { style, .. } | Command::Link { style, .. } => f(style),
343                _ => {}
344            }
345        }
346    }
347
348    fn modify_last_constraints(&mut self, f: impl FnOnce(&mut Constraints)) {
349        if let Some(idx) = self.rollback.last_text_idx {
350            match &mut self.commands[idx] {
351                Command::Text { constraints, .. } | Command::Link { constraints, .. } => {
352                    f(constraints)
353                }
354                _ => {}
355            }
356        }
357    }
358
359    fn modify_last_margin(&mut self, f: impl FnOnce(&mut Margin)) {
360        if let Some(idx) = self.rollback.last_text_idx {
361            match &mut self.commands[idx] {
362                Command::Text { margin, .. } | Command::Link { margin, .. } => f(margin),
363                _ => {}
364            }
365        }
366    }
367
368    // ── containers ───────────────────────────────────────────────────
369
370    /// Set the flex-grow factor of the last rendered text element.
371    ///
372    /// A value of `1` causes the element to expand and fill remaining space
373    /// along the main axis.
374    pub fn grow(&mut self, value: u16) -> &mut Self {
375        if let Some(idx) = self.rollback.last_text_idx {
376            if let Command::Text { grow, .. } = &mut self.commands[idx] {
377                *grow = value;
378            }
379        }
380        self
381    }
382
383    /// Set the text alignment of the last rendered text element.
384    pub fn align(&mut self, align: Align) -> &mut Self {
385        if let Some(idx) = self.rollback.last_text_idx {
386            if let Command::Text {
387                align: text_align, ..
388            } = &mut self.commands[idx]
389            {
390                *text_align = align;
391            }
392        }
393        self
394    }
395
396    /// Center-align the last rendered text element horizontally.
397    /// Shorthand for `.align(Align::Center)`. Requires the text to have
398    /// a width constraint (via `.w()` or parent container) to be visible.
399    pub fn text_center(&mut self) -> &mut Self {
400        self.align(Align::Center)
401    }
402
403    /// Right-align the last rendered text element horizontally.
404    /// Shorthand for `.align(Align::End)`.
405    pub fn text_right(&mut self) -> &mut Self {
406        self.align(Align::End)
407    }
408
409    // ── size constraints on last text/link ──────────────────────────
410
411    /// Set a fixed width on the last rendered text or link element.
412    ///
413    /// Sets both `min_width` and `max_width` to `value`, making the element
414    /// occupy exactly that many columns (padded with spaces or truncated).
415    pub fn w(&mut self, value: u32) -> &mut Self {
416        self.modify_last_constraints(|c| {
417            c.min_width = Some(value);
418            c.max_width = Some(value);
419        });
420        self
421    }
422
423    /// Set a fixed height on the last rendered text or link element.
424    ///
425    /// Sets both `min_height` and `max_height` to `value`.
426    pub fn h(&mut self, value: u32) -> &mut Self {
427        self.modify_last_constraints(|c| {
428            c.min_height = Some(value);
429            c.max_height = Some(value);
430        });
431        self
432    }
433
434    /// Set the minimum width on the last rendered text or link element.
435    pub fn min_w(&mut self, value: u32) -> &mut Self {
436        self.modify_last_constraints(|c| c.min_width = Some(value));
437        self
438    }
439
440    /// Set the maximum width on the last rendered text or link element.
441    pub fn max_w(&mut self, value: u32) -> &mut Self {
442        self.modify_last_constraints(|c| c.max_width = Some(value));
443        self
444    }
445
446    /// Set the minimum height on the last rendered text or link element.
447    pub fn min_h(&mut self, value: u32) -> &mut Self {
448        self.modify_last_constraints(|c| c.min_height = Some(value));
449        self
450    }
451
452    /// Set the maximum height on the last rendered text or link element.
453    pub fn max_h(&mut self, value: u32) -> &mut Self {
454        self.modify_last_constraints(|c| c.max_height = Some(value));
455        self
456    }
457
458    // ── margin on last text/link ────────────────────────────────────
459
460    /// Set uniform margin on all sides of the last rendered text or link element.
461    pub fn m(&mut self, value: u32) -> &mut Self {
462        self.modify_last_margin(|m| *m = Margin::all(value));
463        self
464    }
465
466    /// Set horizontal margin (left + right) on the last rendered text or link.
467    pub fn mx(&mut self, value: u32) -> &mut Self {
468        self.modify_last_margin(|m| {
469            m.left = value;
470            m.right = value;
471        });
472        self
473    }
474
475    /// Set vertical margin (top + bottom) on the last rendered text or link.
476    pub fn my(&mut self, value: u32) -> &mut Self {
477        self.modify_last_margin(|m| {
478            m.top = value;
479            m.bottom = value;
480        });
481        self
482    }
483
484    /// Set top margin on the last rendered text or link element.
485    pub fn mt(&mut self, value: u32) -> &mut Self {
486        self.modify_last_margin(|m| m.top = value);
487        self
488    }
489
490    /// Set right margin on the last rendered text or link element.
491    pub fn mr(&mut self, value: u32) -> &mut Self {
492        self.modify_last_margin(|m| m.right = value);
493        self
494    }
495
496    /// Set bottom margin on the last rendered text or link element.
497    pub fn mb(&mut self, value: u32) -> &mut Self {
498        self.modify_last_margin(|m| m.bottom = value);
499        self
500    }
501
502    /// Set left margin on the last rendered text or link element.
503    pub fn ml(&mut self, value: u32) -> &mut Self {
504        self.modify_last_margin(|m| m.left = value);
505        self
506    }
507
508    /// Render an invisible spacer that expands to fill available space.
509    ///
510    /// Useful for pushing siblings to opposite ends of a row or column.
511    pub fn spacer(&mut self) -> &mut Self {
512        self.commands.push(Command::Spacer { grow: 1 });
513        self.rollback.last_text_idx = None;
514        self
515    }
516}