ratatui_core/text/
span.rs

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