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        self.content.width()
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<'a, T> From<T> for Span<'a>
380where
381    T: Into<Cow<'a, str>>,
382{
383    fn from(s: T) -> Self {
384        Span::raw(s.into())
385    }
386}
387
388impl<'a> core::ops::Add<Self> for Span<'a> {
389    type Output = Line<'a>;
390
391    fn add(self, rhs: Self) -> Self::Output {
392        Line::from_iter([self, rhs])
393    }
394}
395
396impl Styled for Span<'_> {
397    type Item = Self;
398
399    fn style(&self) -> Style {
400        self.style
401    }
402
403    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
404        self.style(style)
405    }
406}
407
408impl Widget for Span<'_> {
409    fn render(self, area: Rect, buf: &mut Buffer) {
410        Widget::render(&self, area, buf);
411    }
412}
413
414impl Widget for &Span<'_> {
415    fn render(self, area: Rect, buf: &mut Buffer) {
416        let area = area.intersection(buf.area);
417        if area.is_empty() {
418            return;
419        }
420        let Rect { mut x, y, .. } = area;
421        for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
422            let symbol_width = grapheme.symbol.width();
423            let next_x = x.saturating_add(symbol_width as u16);
424            if next_x > area.right() {
425                break;
426            }
427
428            if i == 0 {
429                // the first grapheme is always set on the cell
430                buf[(x, y)]
431                    .set_symbol(grapheme.symbol)
432                    .set_style(grapheme.style);
433            } else if x == area.x {
434                // there is one or more zero-width graphemes in the first cell, so the first cell
435                // must be appended to.
436                buf[(x, y)]
437                    .append_symbol(grapheme.symbol)
438                    .set_style(grapheme.style);
439            } else if symbol_width == 0 {
440                // append zero-width graphemes to the previous cell
441                buf[(x - 1, y)]
442                    .append_symbol(grapheme.symbol)
443                    .set_style(grapheme.style);
444            } else {
445                // just a normal grapheme (not first, not zero-width, not overflowing the area)
446                buf[(x, y)]
447                    .set_symbol(grapheme.symbol)
448                    .set_style(grapheme.style);
449            }
450
451            // multi-width graphemes must clear the cells of characters that are hidden by the
452            // grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
453            // overwritten.
454            for x_hidden in (x + 1)..next_x {
455                // it may seem odd that the style of the hidden cells are not set to the style of
456                // the grapheme, but this is how the existing buffer.set_span() method works.
457                buf[(x_hidden, y)].reset();
458            }
459            x = next_x;
460        }
461    }
462}
463
464/// A trait for converting a value to a [`Span`].
465///
466/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
467/// such, `ToSpan` shouln't be implemented directly: [`Display`] should be implemented instead, and
468/// you get the `ToSpan` implementation for free.
469///
470/// [`Display`]: std::fmt::Display
471pub trait ToSpan {
472    /// Converts the value to a [`Span`].
473    fn to_span(&self) -> Span<'_>;
474}
475
476/// # Panics
477///
478/// In this implementation, the `to_span` method panics if the `Display` implementation returns an
479/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
480/// returns an error itself.
481impl<T: fmt::Display> ToSpan for T {
482    fn to_span(&self) -> Span<'_> {
483        Span::raw(self.to_string())
484    }
485}
486
487impl fmt::Display for Span<'_> {
488    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
489        for line in self.content.lines() {
490            fmt::Display::fmt(line, f)?;
491        }
492        Ok(())
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use alloc::string::String;
499    use alloc::{format, vec};
500
501    use rstest::{fixture, rstest};
502
503    use super::*;
504    use crate::buffer::Cell;
505    use crate::layout::Alignment;
506    use crate::style::Stylize;
507
508    #[fixture]
509    fn small_buf() -> Buffer {
510        Buffer::empty(Rect::new(0, 0, 10, 1))
511    }
512
513    #[test]
514    fn default() {
515        let span = Span::default();
516        assert_eq!(span.content, Cow::Borrowed(""));
517        assert_eq!(span.style, Style::default());
518    }
519
520    #[test]
521    fn raw_str() {
522        let span = Span::raw("test content");
523        assert_eq!(span.content, Cow::Borrowed("test content"));
524        assert_eq!(span.style, Style::default());
525    }
526
527    #[test]
528    fn raw_string() {
529        let content = String::from("test content");
530        let span = Span::raw(content.clone());
531        assert_eq!(span.content, Cow::Owned::<str>(content));
532        assert_eq!(span.style, Style::default());
533    }
534
535    #[test]
536    fn styled_str() {
537        let style = Style::new().red();
538        let span = Span::styled("test content", style);
539        assert_eq!(span.content, Cow::Borrowed("test content"));
540        assert_eq!(span.style, Style::new().red());
541    }
542
543    #[test]
544    fn styled_string() {
545        let content = String::from("test content");
546        let style = Style::new().green();
547        let span = Span::styled(content.clone(), style);
548        assert_eq!(span.content, Cow::Owned::<str>(content));
549        assert_eq!(span.style, style);
550    }
551
552    #[test]
553    fn set_content() {
554        let span = Span::default().content("test content");
555        assert_eq!(span.content, Cow::Borrowed("test content"));
556    }
557
558    #[test]
559    fn set_style() {
560        let span = Span::default().style(Style::new().green());
561        assert_eq!(span.style, Style::new().green());
562    }
563
564    #[test]
565    fn from_ref_str_borrowed_cow() {
566        let content = "test content";
567        let span = Span::from(content);
568        assert_eq!(span.content, Cow::Borrowed(content));
569        assert_eq!(span.style, Style::default());
570    }
571
572    #[test]
573    fn from_string_ref_str_borrowed_cow() {
574        let content = String::from("test content");
575        let span = Span::from(content.as_str());
576        assert_eq!(span.content, Cow::Borrowed(content.as_str()));
577        assert_eq!(span.style, Style::default());
578    }
579
580    #[test]
581    fn from_string_owned_cow() {
582        let content = String::from("test content");
583        let span = Span::from(content.clone());
584        assert_eq!(span.content, Cow::Owned::<str>(content));
585        assert_eq!(span.style, Style::default());
586    }
587
588    #[test]
589    fn from_ref_string_borrowed_cow() {
590        let content = String::from("test content");
591        let span = Span::from(&content);
592        assert_eq!(span.content, Cow::Borrowed(content.as_str()));
593        assert_eq!(span.style, Style::default());
594    }
595
596    #[test]
597    fn to_span() {
598        assert_eq!(42.to_span(), Span::raw("42"));
599        assert_eq!("test".to_span(), Span::raw("test"));
600    }
601
602    #[test]
603    fn reset_style() {
604        let span = Span::styled("test content", Style::new().green()).reset_style();
605        assert_eq!(span.style, Style::reset());
606    }
607
608    #[test]
609    fn patch_style() {
610        let span = Span::styled("test content", Style::new().green().on_yellow())
611            .patch_style(Style::new().red().bold());
612        assert_eq!(span.style, Style::new().red().on_yellow().bold());
613    }
614
615    #[test]
616    fn width() {
617        assert_eq!(Span::raw("").width(), 0);
618        assert_eq!(Span::raw("test").width(), 4);
619        assert_eq!(Span::raw("test content").width(), 12);
620        // Needs reconsideration: https://github.com/ratatui/ratatui/issues/1271
621        assert_eq!(Span::raw("test\ncontent").width(), 12);
622    }
623
624    #[test]
625    fn stylize() {
626        let span = Span::raw("test content").green();
627        assert_eq!(span.content, Cow::Borrowed("test content"));
628        assert_eq!(span.style, Style::new().green());
629
630        let span = Span::styled("test content", Style::new().green());
631        let stylized = span.on_yellow().bold();
632        assert_eq!(stylized.content, Cow::Borrowed("test content"));
633        assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
634    }
635
636    #[test]
637    fn display_span() {
638        let span = Span::raw("test content");
639        assert_eq!(format!("{span}"), "test content");
640        assert_eq!(format!("{span:.4}"), "test");
641    }
642
643    #[test]
644    fn display_newline_span() {
645        let span = Span::raw("test\ncontent");
646        assert_eq!(format!("{span}"), "testcontent");
647    }
648
649    #[test]
650    fn display_styled_span() {
651        let stylized_span = Span::styled("stylized test content", Style::new().green());
652        assert_eq!(format!("{stylized_span}"), "stylized test content");
653        assert_eq!(format!("{stylized_span:.8}"), "stylized");
654    }
655
656    #[test]
657    fn left_aligned() {
658        let span = Span::styled("Test Content", Style::new().green().italic());
659        let line = span.into_left_aligned_line();
660        assert_eq!(line.alignment, Some(Alignment::Left));
661    }
662
663    #[test]
664    fn centered() {
665        let span = Span::styled("Test Content", Style::new().green().italic());
666        let line = span.into_centered_line();
667        assert_eq!(line.alignment, Some(Alignment::Center));
668    }
669
670    #[test]
671    fn right_aligned() {
672        let span = Span::styled("Test Content", Style::new().green().italic());
673        let line = span.into_right_aligned_line();
674        assert_eq!(line.alignment, Some(Alignment::Right));
675    }
676
677    mod widget {
678        use rstest::rstest;
679
680        use super::*;
681
682        #[test]
683        fn render() {
684            let style = Style::new().green().on_yellow();
685            let span = Span::styled("test content", style);
686            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
687            span.render(buf.area, &mut buf);
688            let expected = Buffer::with_lines([Line::from(vec![
689                "test content".green().on_yellow(),
690                "   ".into(),
691            ])]);
692            assert_eq!(buf, expected);
693        }
694
695        #[rstest]
696        #[case::x(20, 0)]
697        #[case::y(0, 20)]
698        #[case::both(20, 20)]
699        fn render_out_of_bounds(mut small_buf: Buffer, #[case] x: u16, #[case] y: u16) {
700            let out_of_bounds = Rect::new(x, y, 10, 1);
701            Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
702            assert_eq!(small_buf, Buffer::empty(small_buf.area));
703        }
704
705        /// When the content of the span is longer than the area passed to render, the content
706        /// should be truncated
707        #[test]
708        fn render_truncates_too_long_content() {
709            let style = Style::new().green().on_yellow();
710            let span = Span::styled("test content", style);
711
712            let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
713            span.render(Rect::new(0, 0, 5, 1), &mut buf);
714
715            let expected = Buffer::with_lines([Line::from(vec![
716                "test ".green().on_yellow(),
717                "     ".into(),
718            ])]);
719            assert_eq!(buf, expected);
720        }
721
722        /// When there is already a style set on the buffer, the style of the span should be
723        /// patched with the existing style
724        #[test]
725        fn render_patches_existing_style() {
726            let style = Style::new().green().on_yellow();
727            let span = Span::styled("test content", style);
728            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
729            buf.set_style(buf.area, Style::new().italic());
730            span.render(buf.area, &mut buf);
731            let expected = Buffer::with_lines([Line::from(vec![
732                "test content".green().on_yellow().italic(),
733                "   ".italic(),
734            ])]);
735            assert_eq!(buf, expected);
736        }
737
738        /// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
739        /// of the hidden characters are cleared.
740        #[test]
741        fn render_multi_width_symbol() {
742            let style = Style::new().green().on_yellow();
743            let span = Span::styled("test 😃 content", style);
744            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
745            span.render(buf.area, &mut buf);
746            // The existing code in buffer.set_line() handles multi-width graphemes by clearing the
747            // cells of the hidden characters. This test ensures that the existing behavior is
748            // preserved.
749            let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
750            assert_eq!(buf, expected);
751        }
752
753        /// When the span contains a multi-width grapheme that does not fit in the area passed to
754        /// render, the entire grapheme will be truncated.
755        #[test]
756        fn render_multi_width_symbol_truncates_entire_symbol() {
757            // the 😃 emoji is 2 columns wide so it will be truncated
758            let style = Style::new().green().on_yellow();
759            let span = Span::styled("test 😃 content", style);
760            let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
761            span.render(buf.area, &mut buf);
762
763            let expected =
764                Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
765            assert_eq!(buf, expected);
766        }
767
768        /// When the area passed to render overflows the buffer, the content should be truncated
769        /// to fit the buffer.
770        #[test]
771        fn render_overflowing_area_truncates() {
772            let style = Style::new().green().on_yellow();
773            let span = Span::styled("test content", style);
774            let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
775            span.render(Rect::new(10, 0, 20, 1), &mut buf);
776
777            let expected = Buffer::with_lines([Line::from(vec![
778                "          ".into(),
779                "test ".green().on_yellow(),
780            ])]);
781            assert_eq!(buf, expected);
782        }
783
784        #[test]
785        fn render_first_zero_width() {
786            let span = Span::raw("\u{200B}abc");
787            let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
788            span.render(buf.area, &mut buf);
789            assert_eq!(
790                buf.content(),
791                [Cell::new("\u{200B}a"), Cell::new("b"), Cell::new("c"),]
792            );
793        }
794
795        #[test]
796        fn render_second_zero_width() {
797            let span = Span::raw("a\u{200B}bc");
798            let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
799            span.render(buf.area, &mut buf);
800            assert_eq!(
801                buf.content(),
802                [Cell::new("a\u{200B}"), Cell::new("b"), Cell::new("c")]
803            );
804        }
805
806        #[test]
807        fn render_middle_zero_width() {
808            let span = Span::raw("ab\u{200B}c");
809            let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
810            span.render(buf.area, &mut buf);
811            assert_eq!(
812                buf.content(),
813                [Cell::new("a"), Cell::new("b\u{200B}"), Cell::new("c")]
814            );
815        }
816
817        #[test]
818        fn render_last_zero_width() {
819            let span = Span::raw("abc\u{200B}");
820            let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
821            span.render(buf.area, &mut buf);
822            assert_eq!(
823                buf.content(),
824                [Cell::new("a"), Cell::new("b"), Cell::new("c\u{200B}")]
825            );
826        }
827
828        #[test]
829        fn render_with_newlines() {
830            let span = Span::raw("a\nb");
831            let mut buf = Buffer::empty(Rect::new(0, 0, 2, 1));
832            span.render(buf.area, &mut buf);
833            assert_eq!(buf.content(), [Cell::new("a"), Cell::new("b")]);
834        }
835    }
836
837    /// Regression test for <https://github.com/ratatui/ratatui/issues/1160> One line contains
838    /// some Unicode Left-Right-Marks (U+200E)
839    ///
840    /// The issue was that a zero-width character at the end of the buffer causes the buffer bounds
841    /// to be exceeded (due to a position + 1 calculation that fails to account for the possibility
842    /// that the next position might not be available).
843    #[test]
844    fn issue_1160() {
845        let span = Span::raw("Hello\u{200E}");
846        let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
847        span.render(buf.area, &mut buf);
848        assert_eq!(
849            buf.content(),
850            [
851                Cell::new("H"),
852                Cell::new("e"),
853                Cell::new("l"),
854                Cell::new("l"),
855                Cell::new("o\u{200E}"),
856            ]
857        );
858    }
859
860    #[test]
861    fn add() {
862        assert_eq!(
863            Span::default() + Span::default(),
864            Line::from(vec![Span::default(), Span::default()])
865        );
866
867        assert_eq!(
868            Span::default() + Span::raw("test"),
869            Line::from(vec![Span::default(), Span::raw("test")])
870        );
871
872        assert_eq!(
873            Span::raw("test") + Span::default(),
874            Line::from(vec![Span::raw("test"), Span::default()])
875        );
876
877        assert_eq!(
878            Span::raw("test") + Span::raw("content"),
879            Line::from(vec![Span::raw("test"), Span::raw("content")])
880        );
881    }
882
883    #[rstest]
884    #[case::default(Span::default(), "Span::default()")]
885    #[case::raw(Span::raw("test"), r#"Span::from("test")"#)]
886    #[case::styled(Span::styled("test", Style::new().green()), r#"Span::from("test").green()"#)]
887    #[case::styled_italic(
888        Span::styled("test", Style::new().green().italic()),
889        r#"Span::from("test").green().italic()"#
890    )]
891    fn debug(#[case] span: Span, #[case] expected: &str) {
892        assert_eq!(format!("{span:?}"), expected);
893    }
894}