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