Skip to main content

termint/widgets/
span.rs

1use std::fmt;
2
3use crate::{
4    buffer::Buffer,
5    enums::{Color, Modifier, Wrap},
6    geometry::{Rect, TextAlign, Vec2},
7    style::Style,
8    text::{Text, TextParser},
9    widgets::cache::Cache,
10};
11
12use super::{widget::Widget, Element};
13
14/// A widget for styling text where all characters share the same style.
15///
16/// # Supported styles
17/// - `style`: style of text, set using [`Style`]
18/// - `align`: text alignment, set using [`TextAlign`]
19/// - `wrap`: text wrapping type, set using [`Wrap`]
20/// - `ellipsis`: string shown when text overflows (default: '...')
21///
22/// # Examples
23/// There are multiple ways to create a [`Span`].
24/// ```rust
25/// # use termint::{
26/// #     enums::{Color, Modifier, Wrap},
27/// #     geometry::{TextAlign},
28/// #     modifiers,
29/// #     widgets::{Span, ToSpan},
30/// # };
31/// // Using `new` with red foreground:
32/// let span = Span::new("Red text").fg(Color::Red);
33///
34/// // Cyan bold and italic text on yellow background (using `modifiers` macro)
35/// let span = "Cyan bold and italic on yellow"
36///     .fg(Color::Cyan)
37///     .bg(Color::Yellow)
38///     .modifier(modifiers!(BOLD, ITALIC))
39///     .align(TextAlign::Center)
40///     .wrap(Wrap::Letter)
41///     .ellipsis("...");
42/// ```
43///
44/// Printing a [`Span`] applies styling but ignores wrapping and ellipsis:
45/// ```rust
46/// # use termint::{
47/// #     widgets::{ToSpan},
48/// # };
49/// # let span = "test".to_span();
50/// println!("{span}");
51/// ```
52///
53/// To apply wrapping and ellipsis, render the span with a [`Term`] (or
54/// manually with [`Buffer`]):
55/// ```rust
56/// # use termint::{
57/// #     buffer::Buffer,
58/// #     geometry::Rect,
59/// #     widgets::{ToSpan, Widget},
60/// #     term::Term,
61/// # };
62/// # fn example() -> Result<(), termint::Error> {
63/// # let span = "test".to_span();
64///
65/// let mut term = Term::default();
66/// term.render(span)?;
67/// # Ok(())
68/// # }
69/// ```
70#[derive(Debug)]
71pub struct Span {
72    text: String,
73    style: Style,
74    align: TextAlign,
75    wrap: Wrap,
76    ellipsis: String,
77}
78
79impl Span {
80    /// Creates a new [`Span`] from any type convertible to string slice.
81    ///
82    /// # Example
83    /// ```rust
84    /// # use termint::widgets::Span;
85    /// let span = Span::new("Hello, World!");
86    /// let span = Span::new(String::from("Hello, Termint!"));
87    /// let span = Span::new(&String::from("Hello, All!"));
88    /// ```
89    #[must_use]
90    pub fn new<T>(text: T) -> Self
91    where
92        T: AsRef<str>,
93    {
94        Self {
95            text: text.as_ref().to_string(),
96            ..Default::default()
97        }
98    }
99
100    /// Sets the base style of the [`Span`].
101    ///
102    /// You can provide any type convertible to [`Style`].
103    ///
104    /// # Example
105    /// ```rust
106    /// # use termint::{widgets::Span, style::Style, enums::{Color, Modifier}};
107    /// let span = Span::new("style").style(Style::new().bg(Color::Red));
108    /// let span = Span::new("style").style(Color::Blue);
109    /// ```
110    #[must_use]
111    pub fn style<T>(mut self, style: T) -> Self
112    where
113        T: Into<Style>,
114    {
115        self.style = style.into();
116        self
117    }
118
119    /// Sets the foreground color of the [`Span`].
120    ///
121    /// You can provide any type convertible to [`Color`].
122    #[must_use]
123    pub fn fg<T>(mut self, fg: T) -> Self
124    where
125        T: Into<Option<Color>>,
126    {
127        self.style = self.style.fg(fg);
128        self
129    }
130
131    /// Sets the background color of the [`Span`].
132    ///
133    /// You can provide any type convertible to [`Color`].
134    #[must_use]
135    pub fn bg<T>(mut self, bg: T) -> Self
136    where
137        T: Into<Option<Color>>,
138    {
139        self.style = self.style.bg(bg);
140        self
141    }
142
143    /// Sets the modifier flags of the [`Span`].
144    ///
145    /// # Example
146    /// ```rust
147    /// # use termint::{widgets::Span, enums::Modifier, modifiers};
148    /// // Single modifier
149    /// let span = Span::new("modifier").modifier(Modifier::ITALIC);
150    /// // Multiple modifiers
151    /// let span = Span::new("modifier")
152    ///     .modifier(Modifier::ITALIC | Modifier::BOLD);
153    /// let span = Span::new("modifier").modifier(modifiers!(BOLD, ITALIC));
154    /// ```
155    #[must_use]
156    pub fn modifier(mut self, modifier: Modifier) -> Self {
157        self.style = self.style.modifier(modifier);
158        self
159    }
160
161    /// Adds a modifier flag to the existing modifiers.
162    ///
163    /// # Example
164    /// ```rust
165    /// # use termint::{widgets::Span, enums::Modifier};
166    /// let span = Span::new("add_modifier").add_modifier(Modifier::ITALIC);
167    /// ```
168    #[must_use]
169    pub fn add_modifier(mut self, flag: Modifier) -> Self {
170        self.style = self.style.add_modifier(flag);
171        self
172    }
173
174    /// Removes a modifier flag from the existing modifiers.
175    ///
176    /// # Example
177    /// ```rust
178    /// # use termint::{widgets::Span, enums::Modifier};
179    /// let span = Span::new("remove_modifier")
180    ///     .remove_modifier(Modifier::ITALIC);
181    /// ```
182    #[must_use]
183    pub fn remove_modifier(mut self, flag: Modifier) -> Self {
184        self.style = self.style.remove_modifier(flag);
185        self
186    }
187
188    /// Sets text alignment of the [`Span`].
189    ///
190    /// Default value is [`TextAlign::Left`].
191    #[must_use]
192    pub fn align(mut self, align: TextAlign) -> Self {
193        self.align = align;
194        self
195    }
196
197    /// Sets text wrapping style of the [`Span`].
198    ///
199    /// Default value is [`Wrap::Word`].
200    #[must_use]
201    pub fn wrap(mut self, wrap: Wrap) -> Self {
202        self.wrap = wrap;
203        self
204    }
205
206    /// Sets the ellipsis string to use when text overflows.
207    ///
208    /// The default is `"..."``. Any custom string may be used.
209    #[must_use]
210    pub fn ellipsis<T>(mut self, ellipsis: T) -> Self
211    where
212        T: AsRef<str>,
213    {
214        self.ellipsis = ellipsis.as_ref().to_string();
215        self
216    }
217}
218
219impl Widget for Span {
220    fn render(&self, buffer: &mut Buffer, rect: Rect, _cache: &mut Cache) {
221        _ = self.render_offset(buffer, rect, 0, None);
222    }
223
224    fn height(&self, size: &Vec2) -> usize {
225        match self.wrap {
226            Wrap::Letter => self.height_letter_wrap(size),
227            Wrap::Word => self.height_word_wrap(size),
228        }
229    }
230
231    fn width(&self, size: &Vec2) -> usize {
232        match self.wrap {
233            Wrap::Letter => self.width_letter_wrap(size),
234            Wrap::Word => self.width_word_wrap(size),
235        }
236    }
237}
238
239impl Text for Span {
240    fn render_offset(
241        &self,
242        buffer: &mut Buffer,
243        rect: Rect,
244        offset: usize,
245        wrap: Option<Wrap>,
246    ) -> Vec2 {
247        if rect.is_empty() {
248            return Vec2::new(0, rect.y());
249        }
250
251        let wrap = wrap.unwrap_or(self.wrap);
252        let mut chars = self.text.chars();
253        let mut parser = TextParser::new(&mut chars).wrap(wrap);
254
255        let mut pos = Vec2::new(rect.x() + offset, rect.y());
256        let mut fin_pos = pos;
257
258        let right_end = rect.x() + rect.width();
259        while pos.y <= rect.bottom() {
260            let line_len = right_end.saturating_sub(pos.x);
261            let Some((text, len)) = parser.next_line(line_len) else {
262                break;
263            };
264
265            fin_pos.x =
266                self.render_line(buffer, &rect, &parser, text, len, &pos);
267            fin_pos.y = pos.y;
268            pos.x = rect.x();
269            pos.y += 1;
270        }
271        fin_pos
272    }
273
274    fn get(&self) -> String {
275        format!("{}{}\x1b[0m", self.get_mods(), self.text)
276    }
277
278    fn get_text(&self) -> &str {
279        &self.text
280    }
281
282    fn get_mods(&self) -> String {
283        self.style.to_string()
284    }
285}
286
287impl Default for Span {
288    fn default() -> Self {
289        Self {
290            text: Default::default(),
291            style: Default::default(),
292            align: Default::default(),
293            wrap: Default::default(),
294            ellipsis: "...".to_string(),
295        }
296    }
297}
298
299impl fmt::Display for Span {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        write!(f, "{}", self.get())
302    }
303}
304
305impl Span {
306    /// Renders one line of text and aligns it based on set alignment
307    fn render_line(
308        &self,
309        buffer: &mut Buffer,
310        rect: &Rect,
311        parser: &TextParser,
312        mut line: String,
313        mut len: usize,
314        pos: &Vec2,
315    ) -> usize {
316        if pos.y >= rect.bottom() && !parser.is_end() {
317            len += self.ellipsis.len();
318            if len > rect.width() {
319                len = rect.width();
320                let end = rect.width().saturating_sub(self.ellipsis.len());
321                line = line[..end].to_string();
322            }
323            line.push_str(&self.ellipsis);
324        }
325
326        let x = match self.align {
327            TextAlign::Left => 0,
328            TextAlign::Center => rect.width().saturating_sub(len) >> 1,
329            TextAlign::Right => rect.width().saturating_sub(len),
330        };
331        buffer.set_str_styled(line, &Vec2::new(pos.x + x, pos.y), self.style);
332        pos.x + x + len - 1
333    }
334
335    /// Gets height of the [`Span`] when using word wrap
336    fn height_word_wrap(&self, size: &Vec2) -> usize {
337        let mut chars = self.text.chars();
338        let mut parser = TextParser::new(&mut chars);
339
340        let mut pos = Vec2::new(0, 0);
341        loop {
342            if parser.next_line(size.x).is_none() {
343                break;
344            }
345            pos.y += 1;
346        }
347        pos.y
348    }
349
350    /// Gets width of the [`Span`] when using word wrap
351    fn width_word_wrap(&self, size: &Vec2) -> usize {
352        let mut guess =
353            Vec2::new(self.size_letter_wrap(size.y).saturating_sub(1), 0);
354
355        while self.height_word_wrap(&guess) > size.y {
356            let Some(val) = guess.x.checked_add(1) else {
357                break;
358            };
359            guess.x = val;
360        }
361        guess.x
362    }
363
364    /// Gets height of the [`Span`] when using letter wrap
365    fn height_letter_wrap(&self, size: &Vec2) -> usize {
366        self.text
367            .lines()
368            .map(|l| {
369                (l.chars().count() as f32 / size.x as f32).ceil() as usize
370            })
371            .sum()
372    }
373
374    /// Gets width of the [`Span`] when using letter wrap
375    fn width_letter_wrap(&self, size: &Vec2) -> usize {
376        let mut guess = Vec2::new(self.size_letter_wrap(size.y), 0);
377        while self.height_letter_wrap(&guess) > size.y {
378            guess.x += 1;
379        }
380        guess.x
381    }
382
383    /// Gets size of the [`Span`] when using letter wrap
384    fn size_letter_wrap(&self, size: usize) -> usize {
385        (self.text.chars().count() as f32 / size as f32).ceil() as usize
386    }
387}
388
389/// Enables creating [`Span`] by calling one of the functions on type
390/// implementing this trait.
391///
392/// It's recommended to use `std::fmt::Display` trait. Types implementing this
393/// trait will contain `ToSpan` as well and can be converted to `Span`.
394pub trait ToSpan {
395    /// Creates [`Span`] from string and sets its style to given value
396    fn style<T>(self, style: T) -> Span
397    where
398        T: Into<Style>;
399
400    /// Creates [`Span`] from string and sets its fg to given color
401    fn fg<T>(self, fg: T) -> Span
402    where
403        T: Into<Option<Color>>;
404
405    /// Creates [`Span`] from string and sets its bg to given color
406    fn bg<T>(self, bg: T) -> Span
407    where
408        T: Into<Option<Color>>;
409
410    /// Creates [`Span`] from string and sets its modifier to given value
411    fn modifier(self, modifier: Modifier) -> Span;
412
413    /// Creates [`Span`] from string and add given modifier to it
414    fn add_modifier(self, flag: Modifier) -> Span;
415
416    /// Creates [`Span`] from string and sets its alignment to given value
417    fn align(self, align: TextAlign) -> Span;
418
419    /// Creates [`Span`] from string and sets its wrapping to given value
420    fn wrap(self, wrap: Wrap) -> Span;
421
422    /// Creates [`Span`] from string and sets its ellipsis to given value
423    fn ellipsis<T>(self, ellipsis: T) -> Span
424    where
425        T: AsRef<str>;
426
427    /// Converts type to [`Span`]
428    fn to_span(self) -> Span;
429}
430
431impl<T> ToSpan for &T
432where
433    T: std::fmt::Display,
434{
435    fn style<S>(self, style: S) -> Span
436    where
437        S: Into<Style>,
438    {
439        Span::new(self.to_string()).style(style)
440    }
441
442    fn fg<C>(self, fg: C) -> Span
443    where
444        C: Into<Option<Color>>,
445    {
446        Span::new(self.to_string()).fg(fg)
447    }
448
449    fn bg<C>(self, bg: C) -> Span
450    where
451        C: Into<Option<Color>>,
452    {
453        Span::new(self.to_string()).bg(bg)
454    }
455
456    fn modifier(self, modifier: Modifier) -> Span {
457        Span::new(self.to_string()).modifier(modifier)
458    }
459
460    fn add_modifier(self, flag: Modifier) -> Span {
461        Span::new(self.to_string()).add_modifier(flag)
462    }
463
464    fn align(self, align: TextAlign) -> Span {
465        Span::new(self.to_string()).align(align)
466    }
467
468    fn wrap(self, wrap: Wrap) -> Span {
469        Span::new(self.to_string()).wrap(wrap)
470    }
471
472    fn ellipsis<R>(self, ellipsis: R) -> Span
473    where
474        R: AsRef<str>,
475    {
476        Span::new(self.to_string()).ellipsis(ellipsis.as_ref())
477    }
478
479    fn to_span(self) -> Span {
480        Span::new(self.to_string())
481    }
482}
483
484// From implementations
485impl<T> From<T> for Span
486where
487    T: AsRef<str>,
488{
489    fn from(value: T) -> Self {
490        Span::new(value)
491    }
492}
493
494impl<T> From<T> for Box<dyn Widget>
495where
496    T: AsRef<str>,
497{
498    fn from(value: T) -> Self {
499        Box::new(Span::new(value.as_ref()))
500    }
501}
502
503impl<T> From<T> for Box<dyn Text>
504where
505    T: AsRef<str>,
506{
507    fn from(value: T) -> Self {
508        Box::new(Span::new(value))
509    }
510}
511
512impl<T> From<T> for Element
513where
514    T: AsRef<str>,
515{
516    fn from(value: T) -> Self {
517        Element::new(Span::new(value))
518    }
519}
520
521impl From<Span> for Box<dyn Widget> {
522    fn from(value: Span) -> Self {
523        Box::new(value)
524    }
525}
526
527impl From<Span> for Box<dyn Text> {
528    fn from(value: Span) -> Self {
529        Box::new(value)
530    }
531}
532
533impl From<Span> for Element {
534    fn from(value: Span) -> Self {
535        Element::new(value)
536    }
537}