From 9ba7354335a106607fe0670e1205a038ec54aa1b Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Wed, 31 Jan 2024 13:44:39 -0800 Subject: [PATCH] feat(text): implement iterators for Text (#900) This allows iterating over the `Lines`s of a text using `for` loops and other iterator methods. - add `iter` and `iter_mut` methods to `Text` - implement `IntoIterator` for `Text`, `&Text`, and `&mut Text` traits - update call sites to iterate over `Text` rather than `Text::lines` --- src/text/text.rs | 150 ++++++++++++++++++++++++++++++++++----- src/widgets/paragraph.rs | 14 ++-- src/widgets/reflow.rs | 2 +- 3 files changed, 139 insertions(+), 27 deletions(-) diff --git a/src/text/text.rs b/src/text/text.rs index d3f1105f..46feef22 100644 --- a/src/text/text.rs +++ b/src/text/text.rs @@ -79,7 +79,7 @@ pub struct Text<'a> { impl<'a> Text<'a> { /// Create some text (potentially multiple lines) with no style. /// - /// ## Examples + /// # Examples /// /// ```rust /// # use ratatui::prelude::*; @@ -125,7 +125,7 @@ impl<'a> Text<'a> { /// Returns the max width of all the lines. /// - /// ## Examples + /// # Examples /// /// ```rust /// # use ratatui::prelude::*; @@ -133,12 +133,12 @@ impl<'a> Text<'a> { /// assert_eq!(15, text.width()); /// ``` pub fn width(&self) -> usize { - self.lines.iter().map(Line::width).max().unwrap_or_default() + self.iter().map(Line::width).max().unwrap_or_default() } /// Returns the height. /// - /// ## Examples + /// # Examples /// /// ```rust /// # use ratatui::prelude::*; @@ -211,7 +211,7 @@ impl<'a> Text<'a> { /// /// This is a fluent setter method which must be chained or used as it consumes self /// - /// ## Examples + /// # Examples /// /// ```rust /// # use ratatui::prelude::*; @@ -335,6 +335,43 @@ impl<'a> Text<'a> { pub fn right_aligned(self) -> Self { self.alignment(Alignment::Right) } + + /// Returns an iterator over the lines of the text. + pub fn iter(&self) -> std::slice::Iter> { + self.lines.iter() + } + + /// Returns an iterator that allows modifying each line. + pub fn iter_mut(&mut self) -> std::slice::IterMut> { + self.lines.iter_mut() + } +} + +impl<'a> IntoIterator for Text<'a> { + type Item = Line<'a>; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.lines.into_iter() + } +} + +impl<'a> IntoIterator for &'a Text<'a> { + type Item = &'a Line<'a>; + type IntoIter = std::slice::Iter<'a, Line<'a>>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a> IntoIterator for &'a mut Text<'a> { + type Item = &'a mut Line<'a>; + type IntoIter = std::slice::IterMut<'a, Line<'a>>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } } impl<'a> From for Text<'a> { @@ -382,15 +419,6 @@ impl<'a> From>> for Text<'a> { } } -impl<'a> IntoIterator for Text<'a> { - type Item = Line<'a>; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.lines.into_iter() - } -} - impl<'a, T> Extend for Text<'a> where T: Into>, @@ -403,7 +431,7 @@ where impl std::fmt::Display for Text<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (position, line) in self.lines.iter().with_position() { + for (position, line) in self.iter().with_position() { if position == Position::Last { write!(f, "{line}")?; } else { @@ -433,7 +461,7 @@ impl Widget for &Option> { impl Widget for &Text<'_> { fn render(self, area: Rect, buf: &mut Buffer) { buf.set_style(area, self.style); - for (line, row) in self.lines.iter().zip(area.rows()) { + for (line, row) in self.iter().zip(area.rows()) { let line_width = line.width() as u16; let x_offset = match (self.alignment, line.alignment) { @@ -468,6 +496,8 @@ impl<'a> Styled for Text<'a> { #[cfg(test)] mod tests { + use rstest::{fixture, rstest}; + use super::*; use crate::style::Stylize; @@ -812,4 +842,92 @@ mod tests { let text = Text::from("Hello, world!").right_aligned(); assert_eq!(text.alignment, Some(Alignment::Right)); } + + mod iterators { + use super::*; + + /// a fixture used in the tests below to avoid repeating the same setup + #[fixture] + fn hello_world() -> Text<'static> { + Text::from(vec![ + Line::styled("Hello ", Color::Blue), + Line::styled("world!", Color::Green), + ]) + } + + #[rstest] + fn iter(hello_world: Text<'_>) { + let mut iter = hello_world.iter(); + assert_eq!(iter.next(), Some(&Line::styled("Hello ", Color::Blue))); + assert_eq!(iter.next(), Some(&Line::styled("world!", Color::Green))); + assert_eq!(iter.next(), None); + } + + #[rstest] + fn iter_mut(mut hello_world: Text<'_>) { + let mut iter = hello_world.iter_mut(); + assert_eq!(iter.next(), Some(&mut Line::styled("Hello ", Color::Blue))); + assert_eq!(iter.next(), Some(&mut Line::styled("world!", Color::Green))); + assert_eq!(iter.next(), None); + } + + #[rstest] + fn into_iter(hello_world: Text<'_>) { + let mut iter = hello_world.into_iter(); + assert_eq!(iter.next(), Some(Line::styled("Hello ", Color::Blue))); + assert_eq!(iter.next(), Some(Line::styled("world!", Color::Green))); + assert_eq!(iter.next(), None); + } + + #[rstest] + fn into_iter_ref(hello_world: Text<'_>) { + let mut iter = (&hello_world).into_iter(); + assert_eq!(iter.next(), Some(&Line::styled("Hello ", Color::Blue))); + assert_eq!(iter.next(), Some(&Line::styled("world!", Color::Green))); + assert_eq!(iter.next(), None); + } + + #[test] + fn into_iter_mut_ref() { + let mut hello_world = Text::from(vec![ + Line::styled("Hello ", Color::Blue), + Line::styled("world!", Color::Green), + ]); + let mut iter = (&mut hello_world).into_iter(); + assert_eq!(iter.next(), Some(&mut Line::styled("Hello ", Color::Blue))); + assert_eq!(iter.next(), Some(&mut Line::styled("world!", Color::Green))); + assert_eq!(iter.next(), None); + } + + #[rstest] + fn for_loop_ref(hello_world: Text<'_>) { + let mut result = String::new(); + for line in &hello_world { + result.push_str(line.to_string().as_ref()); + } + assert_eq!(result, "Hello world!"); + } + + #[rstest] + fn for_loop_mut_ref() { + let mut hello_world = Text::from(vec![ + Line::styled("Hello ", Color::Blue), + Line::styled("world!", Color::Green), + ]); + let mut result = String::new(); + for line in &mut hello_world { + result.push_str(line.to_string().as_ref()); + } + assert_eq!(result, "Hello world!"); + } + + #[rstest] + fn for_loop_into(hello_world: Text<'_>) { + let mut result = String::new(); + for line in hello_world { + result.push_str(line.to_string().as_ref()); + } + assert_eq!(result, "Hello world!"); + } + } } diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index 497b812f..20d0c5ad 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -277,7 +277,7 @@ impl<'a> Paragraph<'a> { } if let Some(Wrap { trim }) = self.wrap { - let styled = self.text.lines.iter().map(|line| { + let styled = self.text.iter().map(|line| { let graphemes = line .spans .iter() @@ -292,7 +292,7 @@ impl<'a> Paragraph<'a> { } count } else { - self.text.lines.len() + self.text.height() } } @@ -314,12 +314,7 @@ impl<'a> Paragraph<'a> { issue = "https://github.com/ratatui-org/ratatui/issues/293" )] pub fn line_width(&self) -> usize { - self.text - .lines - .iter() - .map(|l| l.width()) - .max() - .unwrap_or_default() + self.text.iter().map(Line::width).max().unwrap_or_default() } } @@ -344,9 +339,8 @@ impl Paragraph<'_> { return; } - let styled = self.text.lines.iter().map(|line| { + let styled = self.text.iter().map(|line| { let graphemes = line - .spans .iter() .flat_map(|span| span.styled_graphemes(self.style)); let alignment = line.alignment.unwrap_or(self.alignment); diff --git a/src/widgets/reflow.rs b/src/widgets/reflow.rs index 33bb81a9..65be9beb 100644 --- a/src/widgets/reflow.rs +++ b/src/widgets/reflow.rs @@ -355,7 +355,7 @@ mod test { text_area_width: u16, ) -> (Vec, Vec, Vec) { let text = text.into(); - let styled_lines = text.lines.iter().map(|line| { + let styled_lines = text.iter().map(|line| { ( line.iter() .flat_map(|span| span.styled_graphemes(Style::default())),