Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch wrapping functions to use a slice for line_widths #358

Merged
merged 1 commit into from
May 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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