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`
This commit is contained in:
Josh McKinney 2024-01-31 13:44:39 -08:00 committed by GitHub
parent dab08b99b6
commit 9ba7354335
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 139 additions and 27 deletions

View file

@ -79,7 +79,7 @@ pub struct Text<'a> {
impl<'a> Text<'a> { impl<'a> Text<'a> {
/// Create some text (potentially multiple lines) with no style. /// Create some text (potentially multiple lines) with no style.
/// ///
/// ## Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ratatui::prelude::*; /// # use ratatui::prelude::*;
@ -125,7 +125,7 @@ impl<'a> Text<'a> {
/// Returns the max width of all the lines. /// Returns the max width of all the lines.
/// ///
/// ## Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ratatui::prelude::*; /// # use ratatui::prelude::*;
@ -133,12 +133,12 @@ impl<'a> Text<'a> {
/// assert_eq!(15, text.width()); /// assert_eq!(15, text.width());
/// ``` /// ```
pub fn width(&self) -> usize { 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. /// Returns the height.
/// ///
/// ## Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ratatui::prelude::*; /// # 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 /// This is a fluent setter method which must be chained or used as it consumes self
/// ///
/// ## Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use ratatui::prelude::*; /// # use ratatui::prelude::*;
@ -335,6 +335,43 @@ impl<'a> Text<'a> {
pub fn right_aligned(self) -> Self { pub fn right_aligned(self) -> Self {
self.alignment(Alignment::Right) self.alignment(Alignment::Right)
} }
/// Returns an iterator over the lines of the text.
pub fn iter(&self) -> std::slice::Iter<Line<'a>> {
self.lines.iter()
}
/// Returns an iterator that allows modifying each line.
pub fn iter_mut(&mut self) -> std::slice::IterMut<Line<'a>> {
self.lines.iter_mut()
}
}
impl<'a> IntoIterator for Text<'a> {
type Item = Line<'a>;
type IntoIter = std::vec::IntoIter<Self::Item>;
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<String> for Text<'a> { impl<'a> From<String> for Text<'a> {
@ -382,15 +419,6 @@ impl<'a> From<Vec<Line<'a>>> for Text<'a> {
} }
} }
impl<'a> IntoIterator for Text<'a> {
type Item = Line<'a>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.lines.into_iter()
}
}
impl<'a, T> Extend<T> for Text<'a> impl<'a, T> Extend<T> for Text<'a>
where where
T: Into<Line<'a>>, T: Into<Line<'a>>,
@ -403,7 +431,7 @@ where
impl std::fmt::Display for Text<'_> { impl std::fmt::Display for Text<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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 { if position == Position::Last {
write!(f, "{line}")?; write!(f, "{line}")?;
} else { } else {
@ -433,7 +461,7 @@ impl Widget for &Option<Text<'_>> {
impl Widget for &Text<'_> { impl Widget for &Text<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); 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 line_width = line.width() as u16;
let x_offset = match (self.alignment, line.alignment) { let x_offset = match (self.alignment, line.alignment) {
@ -468,6 +496,8 @@ impl<'a> Styled for Text<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use rstest::{fixture, rstest};
use super::*; use super::*;
use crate::style::Stylize; use crate::style::Stylize;
@ -812,4 +842,92 @@ mod tests {
let text = Text::from("Hello, world!").right_aligned(); let text = Text::from("Hello, world!").right_aligned();
assert_eq!(text.alignment, Some(Alignment::Right)); 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!");
}
}
} }

View file

@ -277,7 +277,7 @@ impl<'a> Paragraph<'a> {
} }
if let Some(Wrap { trim }) = self.wrap { 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 let graphemes = line
.spans .spans
.iter() .iter()
@ -292,7 +292,7 @@ impl<'a> Paragraph<'a> {
} }
count count
} else { } 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" issue = "https://github.com/ratatui-org/ratatui/issues/293"
)] )]
pub fn line_width(&self) -> usize { pub fn line_width(&self) -> usize {
self.text self.text.iter().map(Line::width).max().unwrap_or_default()
.lines
.iter()
.map(|l| l.width())
.max()
.unwrap_or_default()
} }
} }
@ -344,9 +339,8 @@ impl Paragraph<'_> {
return; return;
} }
let styled = self.text.lines.iter().map(|line| { let styled = self.text.iter().map(|line| {
let graphemes = line let graphemes = line
.spans
.iter() .iter()
.flat_map(|span| span.styled_graphemes(self.style)); .flat_map(|span| span.styled_graphemes(self.style));
let alignment = line.alignment.unwrap_or(self.alignment); let alignment = line.alignment.unwrap_or(self.alignment);

View file

@ -355,7 +355,7 @@ mod test {
text_area_width: u16, text_area_width: u16,
) -> (Vec<String>, Vec<u16>, Vec<Alignment>) { ) -> (Vec<String>, Vec<u16>, Vec<Alignment>) {
let text = text.into(); let text = text.into();
let styled_lines = text.lines.iter().map(|line| { let styled_lines = text.iter().map(|line| {
( (
line.iter() line.iter()
.flat_map(|span| span.styled_graphemes(Style::default())), .flat_map(|span| span.styled_graphemes(Style::default())),