diff --git a/Cargo.toml b/Cargo.toml index 4d452c4d..67752c4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,12 +93,18 @@ underline-color = ["dep:crossterm"] #! The following features are unstable and may change in the future: ## Enable all unstable features. -unstable = ["unstable-segment-size"] +unstable = ["unstable-segment-size", "unstable-rendered-line-info"] ## Enables the [`Layout::segment_size`](crate::layout::Layout::segment_size) method which is experimental and may change in the ## future. See [Issue #536](https://github.com/ratatui-org/ratatui/issues/536) for more details. unstable-segment-size = [] +## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count) +## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods +## which are experimental and may change in the future. +## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details. +unstable-rendered-line-info = [] + [package.metadata.docs.rs] all-features = true # see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index bf7c2449..6d351341 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -213,6 +213,77 @@ impl<'a> Paragraph<'a> { self.alignment = alignment; self } + + /// Calculates the number of lines needed to fully render. + /// + /// Given a max line width, this method calculates the number of lines that a paragraph will + /// need in order to be fully rendered. For paragraphs that do not use wrapping, this count is + /// simply the number of lines present in the paragraph. + /// + /// # Example + /// + /// ```ignore + /// # use ratatui::{prelude::*, widgets::*}; + /// let paragraph = Paragraph::new("Hello World") + /// .wrap(Wrap { trim: false }); + /// assert_eq!(paragraph.line_count(20), 1); + /// assert_eq!(paragraph.line_count(10), 2); + /// ``` + #[stability::unstable( + feature = "rendered-line-info", + reason = "The design for text wrapping is not stable and might affect this API.", + issue = "https://github.com/ratatui-org/ratatui/issues/293" + )] + pub fn line_count(&self, width: u16) -> usize { + if width < 1 { + return 0; + } + + if let Some(Wrap { trim }) = self.wrap { + let styled = self.text.lines.iter().map(|line| { + let graphemes = line + .spans + .iter() + .flat_map(|span| span.styled_graphemes(self.style)); + let alignment = line.alignment.unwrap_or(self.alignment); + (graphemes, alignment) + }); + let mut line_composer = WordWrapper::new(styled, width, trim); + let mut count = 0; + while line_composer.next_line().is_some() { + count += 1; + } + count + } else { + self.text.lines.len() + } + } + + /// Calculates the shortest line width needed to avoid any word being wrapped or truncated. + /// + /// # Example + /// + /// ```ignore + /// # use ratatui::{prelude::*, widgets::*}; + /// let paragraph = Paragraph::new("Hello World"); + /// assert_eq!(paragraph.line_width(), 11); + /// + /// let paragraph = Paragraph::new("Hello World\nhi\nHello World!!!"); + /// assert_eq!(paragraph.line_width(), 14); + /// ``` + #[stability::unstable( + feature = "rendered-line-info", + reason = "The design for text wrapping is not stable and might affect this API.", + 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() + } } impl<'a> Widget for Paragraph<'a> { @@ -815,4 +886,46 @@ mod test { .remove_modifier(Modifier::DIM) ) } + + #[test] + fn widgets_paragraph_count_rendered_lines() { + let paragraph = Paragraph::new("Hello World"); + assert_eq!(paragraph.line_count(20), 1); + assert_eq!(paragraph.line_count(10), 1); + let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: false }); + assert_eq!(paragraph.line_count(20), 1); + assert_eq!(paragraph.line_count(10), 2); + let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: true }); + assert_eq!(paragraph.line_count(20), 1); + assert_eq!(paragraph.line_count(10), 2); + + let text = "Hello World ".repeat(100); + let paragraph = Paragraph::new(text.trim()); + assert_eq!(paragraph.line_count(11), 1); + assert_eq!(paragraph.line_count(6), 1); + let paragraph = paragraph.wrap(Wrap { trim: false }); + assert_eq!(paragraph.line_count(11), 100); + assert_eq!(paragraph.line_count(6), 200); + let paragraph = paragraph.wrap(Wrap { trim: true }); + assert_eq!(paragraph.line_count(11), 100); + assert_eq!(paragraph.line_count(6), 200); + } + + #[test] + fn widgets_paragraph_line_width() { + let paragraph = Paragraph::new("Hello World"); + assert_eq!(paragraph.line_width(), 11); + let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: false }); + assert_eq!(paragraph.line_width(), 11); + let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: true }); + assert_eq!(paragraph.line_width(), 11); + + let text = "Hello World ".repeat(100); + let paragraph = Paragraph::new(text); + assert_eq!(paragraph.line_width(), 1200); + let paragraph = paragraph.wrap(Wrap { trim: false }); + assert_eq!(paragraph.line_width(), 1200); + let paragraph = paragraph.wrap(Wrap { trim: true }); + assert_eq!(paragraph.line_width(), 1200); + } }