Source code
Revision control
Copy as Markdown
Other Tools
//! Functionality for wrapping text into columns.
use crate::core::display_width;
use crate::{wrap, Options};
/// Wrap text into columns with a given total width.
///
/// The `left_gap`, `middle_gap` and `right_gap` arguments specify the
/// strings to insert before, between, and after the columns. The
/// total width of all columns and all gaps is specified using the
/// `total_width_or_options` argument. This argument can simply be an
/// integer if you want to use default settings when wrapping, or it
/// can be a [`Options`] value if you want to customize the wrapping.
///
/// If the columns are narrow, it is recommended to set
/// [`Options::break_words`] to `true` to prevent words from
/// protruding into the margins.
///
/// The per-column width is computed like this:
///
/// ```
/// # let (left_gap, middle_gap, right_gap) = ("", "", "");
/// # let columns = 2;
/// # let options = textwrap::Options::new(80);
/// let inner_width = options.width
/// - textwrap::core::display_width(left_gap)
/// - textwrap::core::display_width(right_gap)
/// - textwrap::core::display_width(middle_gap) * (columns - 1);
/// let column_width = inner_width / columns;
/// ```
///
/// The `text` is wrapped using [`wrap()`] and the given `options`
/// argument, but the width is overwritten to the computed
/// `column_width`.
///
/// # Panics
///
/// Panics if `columns` is zero.
///
/// # Examples
///
/// ```
/// use textwrap::wrap_columns;
///
/// let text = "\
/// This is an example text, which is wrapped into three columns. \
/// Notice how the final column can be shorter than the others.";
///
/// #[cfg(feature = "smawk")]
/// assert_eq!(wrap_columns(text, 3, 50, "| ", " | ", " |"),
/// vec!["| This is | into three | column can be |",
/// "| an example | columns. | shorter than |",
/// "| text, which | Notice how | the others. |",
/// "| is wrapped | the final | |"]);
///
/// // Without the `smawk` feature, the middle column is a little more uneven:
/// #[cfg(not(feature = "smawk"))]
/// assert_eq!(wrap_columns(text, 3, 50, "| ", " | ", " |"),
/// vec!["| This is an | three | column can be |",
/// "| example text, | columns. | shorter than |",
/// "| which is | Notice how | the others. |",
/// "| wrapped into | the final | |"]);
pub fn wrap_columns<'a, Opt>(
text: &str,
columns: usize,
total_width_or_options: Opt,
left_gap: &str,
middle_gap: &str,
right_gap: &str,
) -> Vec<String>
where
Opt: Into<Options<'a>>,
{
assert!(columns > 0);
let mut options: Options = total_width_or_options.into();
let inner_width = options
.width
.saturating_sub(display_width(left_gap))
.saturating_sub(display_width(right_gap))
.saturating_sub(display_width(middle_gap) * (columns - 1));
let column_width = std::cmp::max(inner_width / columns, 1);
options.width = column_width;
let last_column_padding = " ".repeat(inner_width % column_width);
let wrapped_lines = wrap(text, options);
let lines_per_column =
wrapped_lines.len() / columns + usize::from(wrapped_lines.len() % columns > 0);
let mut lines = Vec::new();
for line_no in 0..lines_per_column {
let mut line = String::from(left_gap);
for column_no in 0..columns {
match wrapped_lines.get(line_no + column_no * lines_per_column) {
Some(column_line) => {
line.push_str(column_line);
line.push_str(&" ".repeat(column_width - display_width(column_line)));
}
None => {
line.push_str(&" ".repeat(column_width));
}
}
if column_no == columns - 1 {
line.push_str(&last_column_padding);
} else {
line.push_str(middle_gap);
}
}
line.push_str(right_gap);
lines.push(line);
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wrap_columns_empty_text() {
assert_eq!(wrap_columns("", 1, 10, "| ", "", " |"), vec!["| |"]);
}
#[test]
fn wrap_columns_single_column() {
assert_eq!(
wrap_columns("Foo", 3, 30, "| ", " | ", " |"),
vec!["| Foo | | |"]
);
}
#[test]
fn wrap_columns_uneven_columns() {
// The gaps take up a total of 5 columns, so the columns are
// (21 - 5)/4 = 4 columns wide:
assert_eq!(
wrap_columns("Foo Bar Baz Quux", 4, 21, "|", "|", "|"),
vec!["|Foo |Bar |Baz |Quux|"]
);
// As the total width increases, the last column absorbs the
// excess width:
assert_eq!(
wrap_columns("Foo Bar Baz Quux", 4, 24, "|", "|", "|"),
vec!["|Foo |Bar |Baz |Quux |"]
);
// Finally, when the width is 25, the columns can be resized
// to a width of (25 - 5)/4 = 5 columns:
assert_eq!(
wrap_columns("Foo Bar Baz Quux", 4, 25, "|", "|", "|"),
vec!["|Foo |Bar |Baz |Quux |"]
);
}
#[test]
#[cfg(feature = "unicode-width")]
fn wrap_columns_with_emojis() {
assert_eq!(
wrap_columns(
"Words and a few emojis 😍 wrapped in ⓶ columns",
2,
30,
"✨ ",
" ⚽ ",
" 👀"
),
vec![
"✨ Words ⚽ wrapped in 👀",
"✨ and a few ⚽ ⓶ columns 👀",
"✨ emojis 😍 ⚽ 👀"
]
);
}
#[test]
fn wrap_columns_big_gaps() {
// The column width shrinks to 1 because the gaps take up all
// the space.
assert_eq!(
wrap_columns("xyz", 2, 10, "----> ", " !!! ", " <----"),
vec![
"----> x !!! z <----", //
"----> y !!! <----"
]
);
}
#[test]
#[should_panic]
fn wrap_columns_panic_with_zero_columns() {
wrap_columns("", 0, 10, "", "", "");
}
}