ratatui_core/text/
span.rs

1use std::{borrow::Cow, fmt};
2
3use unicode_segmentation::UnicodeSegmentation;
4use unicode_width::UnicodeWidthStr;
5
6use crate::{
7    buffer::Buffer,
8    layout::Rect,
9    style::{Style, Styled},
10    text::{Line, StyledGrapheme},
11    widgets::Widget,
12};
13
14/// Represents a part of a line that is contiguous and where all characters share the same style.
15///
16/// A `Span` is the smallest unit of text that can be styled. It is usually combined in the [`Line`]
17/// type to represent a line of text where each `Span` may have a different style.
18///
19/// # Constructor Methods
20///
21/// - [`Span::default`] creates an span with empty content and the default style.
22/// - [`Span::raw`] creates an span with the specified content and the default style.
23/// - [`Span::styled`] creates an span with the specified content and style.
24///
25/// # Setter Methods
26///
27/// These methods are fluent setters. They return a new `Span` with the specified property set.
28///
29/// - [`Span::content`] sets the content of the span.
30/// - [`Span::style`] sets the style of the span.
31///
32/// # Other Methods
33///
34/// - [`Span::patch_style`] patches the style of the span, adding modifiers from the given style.
35/// - [`Span::reset_style`] resets the style of the span.
36/// - [`Span::width`] returns the unicode width of the content held by this span.
37/// - [`Span::styled_graphemes`] returns an iterator over the graphemes held by this span.
38///
39/// # Examples
40///
41/// A `Span` with `style` set to [`Style::default()`] can be created from a `&str`, a `String`, or
42/// any type convertible to [`Cow<str>`].
43///
44/// ```rust
45/// use ratatui_core::text::Span;
46///
47/// let span = Span::raw("test content");
48/// let span = Span::raw(String::from("test content"));
49/// let span = Span::from("test content");
50/// let span = Span::from(String::from("test content"));
51/// let span: Span = "test content".into();
52/// let span: Span = String::from("test content").into();
53/// ```
54///
55/// Styled spans can be created using [`Span::styled`] or by converting strings using methods from
56/// the [`Stylize`] trait.
57///
58/// ```rust
59/// use ratatui_core::{
60///     style::{Style, Stylize},
61///     text::Span,
62/// };
63///
64/// let span = Span::styled("test content", Style::new().green());
65/// let span = Span::styled(String::from("test content"), Style::new().green());
66///
67/// // using Stylize trait shortcuts
68/// let span = "test content".green();
69/// let span = String::from("test content").green();
70/// ```
71///
72/// `Span` implements the [`Styled`] trait, which allows it to be styled using the shortcut methods
73/// defined in the [`Stylize`] trait.
74///
75/// ```rust
76/// use ratatui_core::{style::Stylize, text::Span};
77///
78/// let span = Span::raw("test content").green().on_yellow().italic();
79/// let span = Span::raw(String::from("test content"))
80///     .green()
81///     .on_yellow()
82///     .italic();
83/// ```
84///
85/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Often
86/// apps will use the `Paragraph` widget instead of rendering `Span` directly, as it handles text
87/// wrapping and alignment for you.
88///
89/// ```rust,ignore
90/// use ratatui::{style::Stylize, Frame};
91///
92/// # fn render_frame(frame: &mut Frame) {
93/// frame.render_widget("test content".green().on_yellow().italic(), frame.area());
94/// # }
95/// ```
96/// [`Line`]: crate::text::Line
97/// [`Stylize`]: crate::style::Stylize
98/// [`Cow<str>`]: std::borrow::Cow
99#[derive(Default, Clone, Eq, PartialEq, Hash)]
100pub struct Span<'a> {
101    /// The style of the span.
102    pub style: Style,
103    /// The content of the span as a Clone-on-write string.
104    pub content: Cow<'a, str>,
105}
106
107impl fmt::Debug for Span<'_> {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        if self.content.is_empty() {
110            write!(f, "Span::default()")?;
111        } else {
112            write!(f, "Span::from({:?})", self.content)?;
113        }
114        if self.style != Style::default() {
115            self.style.fmt_stylize(f)?;
116        }
117        Ok(())
118    }
119}
120
121impl<'a> Span<'a> {
122    /// Create a span with the default style.
123    ///
124    /// # Examples
125    ///
126    /// ```rust
127    /// use ratatui_core::text::Span;
128    ///
129    /// Span::raw("test content");
130    /// Span::raw(String::from("test content"));
131    /// ```
132    pub fn raw<T>(content: T) -> Self
133    where
134        T: Into<Cow<'a, str>>,
135    {
136        Self {
137            content: content.into(),
138            style: Style::default(),
139        }
140    }
141
142    /// Create a span with the specified style.
143    ///
144    /// `content` accepts any type that is convertible to [`Cow<str>`] (e.g. `&str`, `String`,
145    /// `&String`, etc.).
146    ///
147    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
148    /// your own type that implements [`Into<Style>`]).
149    ///
150    /// # Examples
151    ///
152    /// ```rust
153    /// use ratatui_core::{
154    ///     style::{Style, Stylize},
155    ///     text::Span,
156    /// };
157    ///
158    /// let style = Style::new().yellow().on_green().italic();
159    /// Span::styled("test content", style);
160    /// Span::styled(String::from("test content"), style);
161    /// ```
162    ///
163    /// [`Color`]: crate::style::Color
164    pub fn styled<T, S>(content: T, style: S) -> Self
165    where
166        T: Into<Cow<'a, str>>,
167        S: Into<Style>,
168    {
169        Self {
170            content: content.into(),
171            style: style.into(),
172        }
173    }
174
175    /// Sets the content of the span.
176    ///
177    /// This is a fluent setter method which must be chained or used as it consumes self
178    ///
179    /// Accepts any type that can be converted to [`Cow<str>`] (e.g. `&str`, `String`, `&String`,
180    /// etc.).
181    ///
182    /// # Examples
183    ///
184    /// ```rust
185    /// use ratatui_core::text::Span;
186    ///
187    /// let mut span = Span::default().content("content");
188    /// ```
189    #[must_use = "method moves the value of self and returns the modified value"]
190    pub fn content<T>(mut self, content: T) -> Self
191    where
192        T: Into<Cow<'a, str>>,
193    {
194        self.content = content.into();
195        self
196    }
197
198    /// Sets the style of the span.
199    ///
200    /// This is a fluent setter method which must be chained or used as it consumes self
201    ///
202    /// In contrast to [`Span::patch_style`], this method replaces the style of the span instead of
203    /// patching it.
204    ///
205    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
206    /// your own type that implements [`Into<Style>`]).
207    ///
208    /// # Examples
209    ///
210    /// ```rust
211    /// use ratatui_core::{
212    ///     style::{Style, Stylize},
213    ///     text::Span,
214    /// };
215    ///
216    /// let mut span = Span::default().style(Style::new().green());
217    /// ```
218    ///
219    /// [`Color`]: crate::style::Color
220    #[must_use = "method moves the value of self and returns the modified value"]
221    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
222        self.style = style.into();
223        self
224    }
225
226    /// Patches the style of the Span, adding modifiers from the given style.
227    ///
228    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
229    /// your own type that implements [`Into<Style>`]).
230    ///
231    /// This is a fluent setter method which must be chained or used as it consumes self
232    ///
233    /// # Example
234    ///
235    /// ```rust
236    /// use ratatui_core::{
237    ///     style::{Style, Stylize},
238    ///     text::Span,
239    /// };
240    ///
241    /// let span = Span::styled("test content", Style::new().green().italic())
242    ///     .patch_style(Style::new().red().on_yellow().bold());
243    /// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
244    /// ```
245    ///
246    /// [`Color`]: crate::style::Color
247    #[must_use = "method moves the value of self and returns the modified value"]
248    pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
249        self.style = self.style.patch(style);
250        self
251    }
252
253    /// Resets the style of the Span.
254    ///
255    /// This is Equivalent to calling `patch_style(Style::reset())`.
256    ///
257    /// This is a fluent setter method which must be chained or used as it consumes self
258    ///
259    /// # Example
260    ///
261    /// ```rust
262    /// use ratatui_core::{
263    ///     style::{Style, Stylize},
264    ///     text::Span,
265    /// };
266    ///
267    /// let span = Span::styled(
268    ///     "Test Content",
269    ///     Style::new().dark_gray().on_yellow().italic(),
270    /// )
271    /// .reset_style();
272    /// assert_eq!(span.style, Style::reset());
273    /// ```
274    #[must_use = "method moves the value of self and returns the modified value"]
275    pub fn reset_style(self) -> Self {
276        self.patch_style(Style::reset())
277    }
278
279    /// Returns the unicode width of the content held by this span.
280    pub fn width(&self) -> usize {
281        self.content.width()
282    }
283
284    /// Returns an iterator over the graphemes held by this span.
285    ///
286    /// `base_style` is the [`Style`] that will be patched with the `Span`'s `style` to get the
287    /// resulting [`Style`].
288    ///
289    /// `base_style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`],
290    /// or your own type that implements [`Into<Style>`]).
291    ///
292    /// # Example
293    ///
294    /// ```rust
295    /// use std::iter::Iterator;
296    ///
297    /// use ratatui_core::{
298    ///     style::{Style, Stylize},
299    ///     text::{Span, StyledGrapheme},
300    /// };
301    ///
302    /// let span = Span::styled("Test", Style::new().green().italic());
303    /// let style = Style::new().red().on_yellow();
304    /// assert_eq!(
305    ///     span.styled_graphemes(style)
306    ///         .collect::<Vec<StyledGrapheme>>(),
307    ///     vec![
308    ///         StyledGrapheme::new("T", Style::new().green().on_yellow().italic()),
309    ///         StyledGrapheme::new("e", Style::new().green().on_yellow().italic()),
310    ///         StyledGrapheme::new("s", Style::new().green().on_yellow().italic()),
311    ///         StyledGrapheme::new("t", Style::new().green().on_yellow().italic()),
312    ///     ],
313    /// );
314    /// ```
315    ///
316    /// [`Color`]: crate::style::Color
317    pub fn styled_graphemes<S: Into<Style>>(
318        &'a self,
319        base_style: S,
320    ) -> impl Iterator<Item = StyledGrapheme<'a>> {
321        let style = base_style.into().patch(self.style);
322        self.content
323            .as_ref()
324            .graphemes(true)
325            .filter(|g| !g.contains(char::is_control))
326            .map(move |g| StyledGrapheme { symbol: g, style })
327    }
328
329    /// Converts this Span into a left-aligned [`Line`]
330    ///
331    /// # Example
332    ///
333    /// ```rust
334    /// use ratatui_core::style::Stylize;
335    ///
336    /// let line = "Test Content".green().italic().into_left_aligned_line();
337    /// ```
338    #[must_use = "method moves the value of self and returns the modified value"]
339    pub fn into_left_aligned_line(self) -> Line<'a> {
340        Line::from(self).left_aligned()
341    }
342
343    #[allow(clippy::wrong_self_convention)]
344    #[deprecated = "use into_left_aligned_line"]
345    pub fn to_left_aligned_line(self) -> Line<'a> {
346        self.into_left_aligned_line()
347    }
348
349    /// Converts this Span into a center-aligned [`Line`]
350    ///
351    /// # Example
352    ///
353    /// ```rust
354    /// use ratatui_core::style::Stylize;
355    ///
356    /// let line = "Test Content".green().italic().into_centered_line();
357    /// ```
358    #[must_use = "method moves the value of self and returns the modified value"]
359    pub fn into_centered_line(self) -> Line<'a> {
360        Line::from(self).centered()
361    }
362
363    #[allow(clippy::wrong_self_convention)]
364    #[deprecated = "use into_centered_line"]
365    pub fn to_centered_line(self) -> Line<'a> {
366        self.into_centered_line()
367    }
368
369    /// Converts this Span into a right-aligned [`Line`]
370    ///
371    /// # Example
372    ///
373    /// ```rust
374    /// use ratatui_core::style::Stylize;
375    ///
376    /// let line = "Test Content".green().italic().into_right_aligned_line();
377    /// ```
378    #[must_use = "method moves the value of self and returns the modified value"]
379    pub fn into_right_aligned_line(self) -> Line<'a> {
380        Line::from(self).right_aligned()
381    }
382
383    #[allow(clippy::wrong_self_convention)]
384    #[deprecated = "use into_right_aligned_line"]
385    pub fn to_right_aligned_line(self) -> Line<'a> {
386        self.into_right_aligned_line()
387    }
388}
389
390impl<'a, T> From<T> for Span<'a>
391where
392    T: Into<Cow<'a, str>>,
393{
394    fn from(s: T) -> Self {
395        Span::raw(s.into())
396    }
397}
398
399impl<'a> std::ops::Add<Self> for Span<'a> {
400    type Output = Line<'a>;
401
402    fn add(self, rhs: Self) -> Self::Output {
403        Line::from_iter([self, rhs])
404    }
405}
406
407impl Styled for Span<'_> {
408    type Item = Self;
409
410    fn style(&self) -> Style {
411        self.style
412    }
413
414    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
415        self.style(style)
416    }
417}
418
419impl Widget for Span<'_> {
420    fn render(self, area: Rect, buf: &mut Buffer) {
421        Widget::render(&self, area, buf);
422    }
423}
424
425impl Widget for &Span<'_> {
426    fn render(self, area: Rect, buf: &mut Buffer) {
427        let area = area.intersection(buf.area);
428        if area.is_empty() {
429            return;
430        }
431        let Rect { mut x, y, .. } = area;
432        for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
433            let symbol_width = grapheme.symbol.width();
434            let next_x = x.saturating_add(symbol_width as u16);
435            if next_x > area.right() {
436                break;
437            }
438
439            if i == 0 {
440                // the first grapheme is always set on the cell
441                buf[(x, y)]
442                    .set_symbol(grapheme.symbol)
443                    .set_style(grapheme.style);
444            } else if x == area.x {
445                // there is one or more zero-width graphemes in the first cell, so the first cell
446                // must be appended to.
447                buf[(x, y)]
448                    .append_symbol(grapheme.symbol)
449                    .set_style(grapheme.style);
450            } else if symbol_width == 0 {
451                // append zero-width graphemes to the previous cell
452                buf[(x - 1, y)]
453                    .append_symbol(grapheme.symbol)
454                    .set_style(grapheme.style);
455            } else {
456                // just a normal grapheme (not first, not zero-width, not overflowing the area)
457                buf[(x, y)]
458                    .set_symbol(grapheme.symbol)
459                    .set_style(grapheme.style);
460            }
461
462            // multi-width graphemes must clear the cells of characters that are hidden by the
463            // grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
464            // overwritten.
465            for x_hidden in (x + 1)..next_x {
466                // it may seem odd that the style of the hidden cells are not set to the style of
467                // the grapheme, but this is how the existing buffer.set_span() method works.
468                buf[(x_hidden, y)].reset();
469            }
470            x = next_x;
471        }
472    }
473}
474
475/// A trait for converting a value to a [`Span`].
476///
477/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
478/// such, `ToSpan` shouln't be implemented directly: [`Display`] should be implemented instead, and
479/// you get the `ToSpan` implementation for free.
480///
481/// [`Display`]: std::fmt::Display
482pub trait ToSpan {
483    /// Converts the value to a [`Span`].
484    fn to_span(&self) -> Span<'_>;
485}
486
487/// # Panics
488///
489/// In this implementation, the `to_span` method panics if the `Display` implementation returns an
490/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
491/// returns an error itself.
492impl<T: fmt::Display> ToSpan for T {
493    fn to_span(&self) -> Span<'_> {
494        Span::raw(self.to_string())
495    }
496}
497
498impl fmt::Display for Span<'_> {
499    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500        for line in self.content.lines() {
501            fmt::Display::fmt(line, f)?;
502        }
503        Ok(())
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use rstest::{fixture, rstest};
510
511    use super::*;
512    use crate::{buffer::Cell, layout::Alignment, style::Stylize};
513
514    #[fixture]
515    fn small_buf() -> Buffer {
516        Buffer::empty(Rect::new(0, 0, 10, 1))
517    }
518
519    #[test]
520    fn default() {
521        let span = Span::default();
522        assert_eq!(span.content, Cow::Borrowed(""));
523        assert_eq!(span.style, Style::default());
524    }
525
526    #[test]
527    fn raw_str() {
528        let span = Span::raw("test content");
529        assert_eq!(span.content, Cow::Borrowed("test content"));
530        assert_eq!(span.style, Style::default());
531    }
532
533    #[test]
534    fn raw_string() {
535        let content = String::from("test content");
536        let span = Span::raw(content.clone());
537        assert_eq!(span.content, Cow::Owned::<str>(content));
538        assert_eq!(span.style, Style::default());
539    }
540
541    #[test]
542    fn styled_str() {
543        let style = Style::new().red();
544        let span = Span::styled("test content", style);
545        assert_eq!(span.content, Cow::Borrowed("test content"));
546        assert_eq!(span.style, Style::new().red());
547    }
548
549    #[test]
550    fn styled_string() {
551        let content = String::from("test content");
552        let style = Style::new().green();
553        let span = Span::styled(content.clone(), style);
554        assert_eq!(span.content, Cow::Owned::<str>(content));
555        assert_eq!(span.style, style);
556    }
557
558    #[test]
559    fn set_content() {
560        let span = Span::default().content("test content");
561        assert_eq!(span.content, Cow::Borrowed("test content"));
562    }
563
564    #[test]
565    fn set_style() {
566        let span = Span::default().style(Style::new().green());
567        assert_eq!(span.style, Style::new().green());
568    }
569
570    #[test]
571    fn from_ref_str_borrowed_cow() {
572        let content = "test content";
573        let span = Span::from(content);
574        assert_eq!(span.content, Cow::Borrowed(content));
575        assert_eq!(span.style, Style::default());
576    }
577
578    #[test]
579    fn from_string_ref_str_borrowed_cow() {
580        let content = String::from("test content");
581        let span = Span::from(content.as_str());
582        assert_eq!(span.content, Cow::Borrowed(content.as_str()));
583        assert_eq!(span.style, Style::default());
584    }
585
586    #[test]
587    fn from_string_owned_cow() {
588        let content = String::from("test content");
589        let span = Span::from(content.clone());
590        assert_eq!(span.content, Cow::Owned::<str>(content));
591        assert_eq!(span.style, Style::default());
592    }
593
594    #[test]
595    fn from_ref_string_borrowed_cow() {
596        let content = String::from("test content");
597        let span = Span::from(&content);
598        assert_eq!(span.content, Cow::Borrowed(content.as_str()));
599        assert_eq!(span.style, Style::default());
600    }
601
602    #[test]
603    fn to_span() {
604        assert_eq!(42.to_span(), Span::raw("42"));
605        assert_eq!("test".to_span(), Span::raw("test"));
606    }
607
608    #[test]
609    fn reset_style() {
610        let span = Span::styled("test content", Style::new().green()).reset_style();
611        assert_eq!(span.style, Style::reset());
612    }
613
614    #[test]
615    fn patch_style() {
616        let span = Span::styled("test content", Style::new().green().on_yellow())
617            .patch_style(Style::new().red().bold());
618        assert_eq!(span.style, Style::new().red().on_yellow().bold());
619    }
620
621    #[test]
622    fn width() {
623        assert_eq!(Span::raw("").width(), 0);
624        assert_eq!(Span::raw("test").width(), 4);
625        assert_eq!(Span::raw("test content").width(), 12);
626        // Needs reconsideration: https://github.com/ratatui/ratatui/issues/1271
627        assert_eq!(Span::raw("test\ncontent").width(), 12);
628    }
629
630    #[test]
631    fn stylize() {
632        let span = Span::raw("test content").green();
633        assert_eq!(span.content, Cow::Borrowed("test content"));
634        assert_eq!(span.style, Style::new().green());
635
636        let span = Span::styled("test content", Style::new().green());
637        let stylized = span.on_yellow().bold();
638        assert_eq!(stylized.content, Cow::Borrowed("test content"));
639        assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
640    }
641
642    #[test]
643    fn display_span() {
644        let span = Span::raw("test content");
645        assert_eq!(format!("{span}"), "test content");
646        assert_eq!(format!("{span:.4}"), "test");
647    }
648
649    #[test]
650    fn display_newline_span() {
651        let span = Span::raw("test\ncontent");
652        assert_eq!(format!("{span}"), "testcontent");
653    }
654
655    #[test]
656    fn display_styled_span() {
657        let stylized_span = Span::styled("stylized test content", Style::new().green());
658        assert_eq!(format!("{stylized_span}"), "stylized test content");
659        assert_eq!(format!("{stylized_span:.8}"), "stylized");
660    }
661
662    #[test]
663    fn left_aligned() {
664        let span = Span::styled("Test Content", Style::new().green().italic());
665        let line = span.into_left_aligned_line();
666        assert_eq!(line.alignment, Some(Alignment::Left));
667    }
668
669    #[test]
670    fn centered() {
671        let span = Span::styled("Test Content", Style::new().green().italic());
672        let line = span.into_centered_line();
673        assert_eq!(line.alignment, Some(Alignment::Center));
674    }
675
676    #[test]
677    fn right_aligned() {
678        let span = Span::styled("Test Content", Style::new().green().italic());
679        let line = span.into_right_aligned_line();
680        assert_eq!(line.alignment, Some(Alignment::Right));
681    }
682
683    mod widget {
684        use rstest::rstest;
685
686        use super::*;
687
688        #[test]
689        fn render() {
690            let style = Style::new().green().on_yellow();
691            let span = Span::styled("test content", style);
692            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
693            span.render(buf.area, &mut buf);
694            let expected = Buffer::with_lines([Line::from(vec![
695                "test content".green().on_yellow(),
696                "   ".into(),
697            ])]);
698            assert_eq!(buf, expected);
699        }
700
701        #[rstest]
702        #[case::x(20, 0)]
703        #[case::y(0, 20)]
704        #[case::both(20, 20)]
705        fn render_out_of_bounds(mut small_buf: Buffer, #[case] x: u16, #[case] y: u16) {
706            let out_of_bounds = Rect::new(x, y, 10, 1);
707            Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
708            assert_eq!(small_buf, Buffer::empty(small_buf.area));
709        }
710
711        /// When the content of the span is longer than the area passed to render, the content
712        /// should be truncated
713        #[test]
714        fn render_truncates_too_long_content() {
715            let style = Style::new().green().on_yellow();
716            let span = Span::styled("test content", style);
717
718            let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
719            span.render(Rect::new(0, 0, 5, 1), &mut buf);
720
721            let expected = Buffer::with_lines([Line::from(vec![
722                "test ".green().on_yellow(),
723                "     ".into(),
724            ])]);
725            assert_eq!(buf, expected);
726        }
727
728        /// When there is already a style set on the buffer, the style of the span should be
729        /// patched with the existing style
730        #[test]
731        fn render_patches_existing_style() {
732            let style = Style::new().green().on_yellow();
733            let span = Span::styled("test content", style);
734            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
735            buf.set_style(buf.area, Style::new().italic());
736            span.render(buf.area, &mut buf);
737            let expected = Buffer::with_lines([Line::from(vec![
738                "test content".green().on_yellow().italic(),
739                "   ".italic(),
740            ])]);
741            assert_eq!(buf, expected);
742        }
743
744        /// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
745        /// of the hidden characters are cleared.
746        #[test]
747        fn render_multi_width_symbol() {
748            let style = Style::new().green().on_yellow();
749            let span = Span::styled("test 😃 content", style);
750            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
751            span.render(buf.area, &mut buf);
752            // The existing code in buffer.set_line() handles multi-width graphemes by clearing the
753            // cells of the hidden characters. This test ensures that the existing behavior is
754            // preserved.
755            let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
756            assert_eq!(buf, expected);
757        }
758
759        /// When the span contains a multi-width grapheme that does not fit in the area passed to
760        /// render, the entire grapheme will be truncated.
761        #[test]
762        fn render_multi_width_symbol_truncates_entire_symbol() {
763            // the 😃 emoji is 2 columns wide so it will be truncated
764            let style = Style::new().green().on_yellow();
765            let span = Span::styled("test 😃 content", style);
766            let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
767            span.render(buf.area, &mut buf);
768
769            let expected =
770                Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
771            assert_eq!(buf, expected);
772        }
773
774        /// When the area passed to render overflows the buffer, the content should be truncated
775        /// to fit the buffer.
776        #[test]
777        fn render_overflowing_area_truncates() {
778            let style = Style::new().green().on_yellow();
779            let span = Span::styled("test content", style);
780            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
781            span.render(Rect::new(10, 0, 20, 1), &mut buf);
782
783            let expected = Buffer::with_lines([Line::from(vec![
784                "          ".into(),
785                "test ".green().on_yellow(),
786            ])]);
787            assert_eq!(buf, expected);
788        }
789
790        #[test]
791        fn render_first_zero_width() {
792            let span = Span::raw("\u{200B}abc");
793            let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
794            span.render(buf.area, &mut buf);
795            assert_eq!(
796                buf.content(),
797                [Cell::new("\u{200B}a"), Cell::new("b"), Cell::new("c"),]
798            );
799        }
800
801        #[test]
802        fn render_second_zero_width() {
803            let span = Span::raw("a\u{200B}bc");
804            let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
805            span.render(buf.area, &mut buf);
806            assert_eq!(
807                buf.content(),
808                [Cell::new("a\u{200B}"), Cell::new("b"), Cell::new("c")]
809            );
810        }
811
812        #[test]
813        fn render_middle_zero_width() {
814            let span = Span::raw("ab\u{200B}c");
815            let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
816            span.render(buf.area, &mut buf);
817            assert_eq!(
818                buf.content(),
819                [Cell::new("a"), Cell::new("b\u{200B}"), Cell::new("c")]
820            );
821        }
822
823        #[test]
824        fn render_last_zero_width() {
825            let span = Span::raw("abc\u{200B}");
826            let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
827            span.render(buf.area, &mut buf);
828            assert_eq!(
829                buf.content(),
830                [Cell::new("a"), Cell::new("b"), Cell::new("c\u{200B}")]
831            );
832        }
833
834        #[test]
835        fn render_with_newlines() {
836            let span = Span::raw("a\nb");
837            let mut buf = Buffer::empty(Rect::new(0, 0, 2, 1));
838            span.render(buf.area, &mut buf);
839            assert_eq!(buf.content(), [Cell::new("a"), Cell::new("b")]);
840        }
841    }
842
843    /// Regression test for <https://github.com/ratatui/ratatui/issues/1160> One line contains
844    /// some Unicode Left-Right-Marks (U+200E)
845    ///
846    /// The issue was that a zero-width character at the end of the buffer causes the buffer bounds
847    /// to be exceeded (due to a position + 1 calculation that fails to account for the possibility
848    /// that the next position might not be available).
849    #[test]
850    fn issue_1160() {
851        let span = Span::raw("Hello\u{200E}");
852        let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
853        span.render(buf.area, &mut buf);
854        assert_eq!(
855            buf.content(),
856            [
857                Cell::new("H"),
858                Cell::new("e"),
859                Cell::new("l"),
860                Cell::new("l"),
861                Cell::new("o\u{200E}"),
862            ]
863        );
864    }
865
866    #[test]
867    fn add() {
868        assert_eq!(
869            Span::default() + Span::default(),
870            Line::from(vec![Span::default(), Span::default()])
871        );
872
873        assert_eq!(
874            Span::default() + Span::raw("test"),
875            Line::from(vec![Span::default(), Span::raw("test")])
876        );
877
878        assert_eq!(
879            Span::raw("test") + Span::default(),
880            Line::from(vec![Span::raw("test"), Span::default()])
881        );
882
883        assert_eq!(
884            Span::raw("test") + Span::raw("content"),
885            Line::from(vec![Span::raw("test"), Span::raw("content")])
886        );
887    }
888
889    #[rstest]
890    #[case::default(Span::default(), "Span::default()")]
891    #[case::raw(Span::raw("test"), r#"Span::from("test")"#)]
892    #[case::styled(Span::styled("test", Style::new().green()), r#"Span::from("test").green()"#)]
893    #[case::styled_italic(
894        Span::styled("test", Style::new().green().italic()),
895        r#"Span::from("test").green().italic()"#
896    )]
897    fn debug(#[case] span: Span, #[case] expected: &str) {
898        assert_eq!(format!("{span:?}"), expected);
899    }
900}