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