Skip to content

Commit

Permalink
Merge pull request #358 from mgeisler/line-widths-slice
Browse files Browse the repository at this point in the history
Switch wrapping functions to use a slice for `line_widths`
  • Loading branch information
mgeisler authored May 16, 2021
2 parents 7408d4e + 59bbeef commit f056656
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 27 deletions.
4 changes: 2 additions & 2 deletions examples/wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ pub fn draw_wrapped_text(
.map(|word| CanvasWord::from(ctx, word))
.collect::<Vec<_>>();

let line_lengths = |_| width * PRECISION;
let wrapped_words = core::wrap_first_fit(&canvas_words, line_lengths);
let line_lengths = [width * PRECISION];
let wrapped_words = core::wrap_first_fit(&canvas_words, &line_lengths);

for words_in_line in wrapped_words {
lineno += 1;
Expand Down
26 changes: 16 additions & 10 deletions src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,9 @@ pub enum WrapAlgorithm {

/// Wrap abstract fragments into lines with a first-fit algorithm.
///
/// The `line_widths` map line numbers (starting from 0) to a target
/// line width. This can be used to implement hanging indentation.
/// The `line_widths` slice give the target line width for each line
/// (the last slice element is repeated as necessary). This can be
/// used to implement hanging indentation.
///
/// The fragments must already have been split into the desired
/// widths, this function will not (and cannot) attempt to split them
Expand Down Expand Up @@ -477,7 +478,7 @@ pub enum WrapAlgorithm {
///
/// let text = "These few words will unfortunately not wrap nicely.";
/// let words = AsciiSpace.find_words(text).collect::<Vec<_>>();
/// assert_eq!(lines_to_strings(wrap_first_fit(&words, |_| 15)),
/// assert_eq!(lines_to_strings(wrap_first_fit(&words, &[15])),
/// vec!["These few words",
/// "will", // <-- short line
/// "unfortunately",
Expand All @@ -486,7 +487,7 @@ pub enum WrapAlgorithm {
///
/// // We can avoid the short line if we look ahead:
/// #[cfg(feature = "smawk")]
/// assert_eq!(lines_to_strings(textwrap::core::wrap_optimal_fit(&words, |_| 15)),
/// assert_eq!(lines_to_strings(textwrap::core::wrap_optimal_fit(&words, &[15])),
/// vec!["These few",
/// "words will",
/// "unfortunately",
Expand Down Expand Up @@ -551,7 +552,7 @@ pub enum WrapAlgorithm {
/// let mut days = Vec::new();
/// // Assign tasks to days. The assignment is a vector of slices,
/// // with a slice per day.
/// let assigned_days: Vec<&[Task<'a>]> = wrap_first_fit(&tasks, |i| day_length);
/// let assigned_days: Vec<&[Task<'a>]> = wrap_first_fit(&tasks, &[day_length]);
/// for day in assigned_days.iter() {
/// let last = day.last().unwrap();
/// let work_hours: usize = day.iter().map(|t| t.hours + t.sweep).sum();
Expand Down Expand Up @@ -587,16 +588,21 @@ pub enum WrapAlgorithm {
///
/// Apologies to anyone who actually knows how to build a house and
/// knows how long each step takes :-)
pub fn wrap_first_fit<T: Fragment, F: Fn(usize) -> usize>(
fragments: &[T],
line_widths: F,
) -> Vec<&[T]> {
pub fn wrap_first_fit<'a, 'b, T: Fragment>(
fragments: &'a [T],
line_widths: &'b [usize],
) -> Vec<&'a [T]> {
// The final line width is used for all remaining lines.
let default_line_width = line_widths.last().copied().unwrap_or(0);
let mut lines = Vec::new();
let mut start = 0;
let mut width = 0;

for (idx, fragment) in fragments.iter().enumerate() {
let line_width = line_widths(lines.len());
let line_width = line_widths
.get(lines.len())
.copied()
.unwrap_or(default_line_width);
if width + fragment.width() + fragment.penalty_width() > line_width && idx > start {
lines.push(&fragments[start..idx]);
start = idx;
Expand Down
27 changes: 17 additions & 10 deletions src/core/optimal_fit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,20 @@ const NLINE_PENALTY: i32 = 1000;
/// let fragments = vec![Word::from(short), Word::from(&long)];
///
/// // Perfect fit, both words are on a single line with no overflow.
/// let wrapped = wrap_optimal_fit(&fragments, |_| short.len() + long.len());
/// let wrapped = wrap_optimal_fit(&fragments, &[short.len() + long.len()]);
/// assert_eq!(wrapped, vec![&[Word::from(short), Word::from(&long)]]);
///
/// // The words no longer fit, yet we get a single line back. While
/// // the cost of overflow (`1 * 2500`) is the same as the cost of the
/// // gap (`50 * 50 = 2500`), the tie is broken by `NLINE_PENALTY`
/// // which makes it cheaper to overflow than to use two lines.
/// let wrapped = wrap_optimal_fit(&fragments, |_| short.len() + long.len() - 1);
/// let wrapped = wrap_optimal_fit(&fragments, &[short.len() + long.len() - 1]);
/// assert_eq!(wrapped, vec![&[Word::from(short), Word::from(&long)]]);
///
/// // The cost of overflow would be 2 * 2500, whereas the cost of the
/// // gap is only `49 * 49 + NLINE_PENALTY = 2401 + 1000 = 3401`. We
/// // therefore get two lines.
/// let wrapped = wrap_optimal_fit(&fragments, |_| short.len() + long.len() - 2);
/// let wrapped = wrap_optimal_fit(&fragments, &[short.len() + long.len() - 2]);
/// assert_eq!(wrapped, vec![&[Word::from(short)],
/// &[Word::from(&long)]]);
/// ```
Expand All @@ -81,8 +81,9 @@ const HYPHEN_PENALTY: i32 = 25;

/// Wrap abstract fragments into lines with an optimal-fit algorithm.
///
/// The `line_widths` map line numbers (starting from 0) to a target
/// line width. This can be used to implement hanging indentation.
/// The `line_widths` slice give the target line width for each line
/// (the last slice element is repeated as necessary). This can be
/// used to implement hanging indentation.
///
/// The fragments must already have been split into the desired
/// widths, this function will not (and cannot) attempt to split them
Expand Down Expand Up @@ -153,10 +154,12 @@ const HYPHEN_PENALTY: i32 = 25;
///
/// **Note:** Only available when the `smawk` Cargo feature is
/// enabled.
pub fn wrap_optimal_fit<T: Fragment, F: Fn(usize) -> usize>(
fragments: &[T],
line_widths: F,
) -> Vec<&[T]> {
pub fn wrap_optimal_fit<'a, 'b, T: Fragment>(
fragments: &'a [T],
line_widths: &'b [usize],
) -> Vec<&'a [T]> {
// The final line width is used for all remaining lines.
let default_line_width = line_widths.last().copied().unwrap_or(0);
let mut widths = Vec::with_capacity(fragments.len() + 1);
let mut width = 0;
widths.push(width);
Expand All @@ -170,7 +173,11 @@ pub fn wrap_optimal_fit<T: Fragment, F: Fn(usize) -> usize>(
let minima = smawk::online_column_minima(0, widths.len(), |minima, i, j| {
// Line number for fragment `i`.
let line_number = line_numbers.get(i, &minima);
let target_width = std::cmp::max(1, line_widths(line_number));
let line_width = line_widths
.get(line_number)
.copied()
.unwrap_or(default_line_width);
let target_width = std::cmp::max(1, line_width);

// Compute the width of a line spanning fragments[i..j] in
// constant time. We need to adjust widths[j] by subtracting
Expand Down
9 changes: 4 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1053,12 +1053,11 @@ where
split_words.collect::<Vec<_>>()
};

#[rustfmt::skip]
let line_lengths = |i| if i == 0 { initial_width } else { subsequent_width };
let line_widths = [initial_width, subsequent_width];
let wrapped_words = match options.wrap_algorithm {
#[cfg(feature = "smawk")]
core::WrapAlgorithm::OptimalFit => core::wrap_optimal_fit(&broken_words, line_lengths),
core::WrapAlgorithm::FirstFit => core::wrap_first_fit(&broken_words, line_lengths),
core::WrapAlgorithm::OptimalFit => core::wrap_optimal_fit(&broken_words, &line_widths),
core::WrapAlgorithm::FirstFit => core::wrap_first_fit(&broken_words, &line_widths),
};

let mut idx = 0;
Expand Down Expand Up @@ -1282,7 +1281,7 @@ pub fn fill_inplace(text: &mut String, width: usize) {
let mut offset = 0;
for line in text.split('\n') {
let words = AsciiSpace.find_words(line).collect::<Vec<_>>();
let wrapped_words = core::wrap_first_fit(&words, |_| width);
let wrapped_words = core::wrap_first_fit(&words, &[width]);

let mut line_offset = offset;
for words in &wrapped_words[..wrapped_words.len() - 1] {
Expand Down

0 comments on commit f056656

Please sign in to comment.