Repository: mgeisler/textwrap (language: rust)

Issue:
Introduce a trait for `WrapAlgorithm`
The `WrapAlgorithm` enum should be turned into a trait. This will allow us to

1. Avoid doing `.collect::<Vec<_>>()` in our procesing pipeline. We do this today since the different wrapping algorithms return different `Iterator` types.
2. Make the wrapping algorithm extensible by clients of the library.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d742a48c..8fef958b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,13 +5,26 @@ This file lists the most important changes made in each release of
 
 ## Unreleased
 
-This is a major feature release which adds a new generic type
-parameter to the `Options` struct. This new parameter lets you specify
-how words are found in the text.
+This is a major feature release which adds new generic type parameters
+to the `Options` struct. These parameters lets you statically
+configure the wrapping algorithm and the word separator:
+
+* `wrap_algorithms::WrapAlgorithm`: this trait replaces the old
+  `core::WrapAlgorithm` enum. The enum variants are now two structs:
+  `wrap_algorithms::FirstFit` and `wrap_algorithms::OptimalFit`.
+
+* `WordSeparator`: this new trait lets you specify how words are
+  separated in the text. Until now, Textwrap would simply split on
+  spaces. While this works okay for Western languages, it fails to
+  take emojis and East-Asian languages into account.
+
+  The new `AsciiSpace` and `UnicodeBreakProperties` structs implement
+  the trait. The latter is available if the new optional
+  `unicode-linebreak` Cargo feature is enabled.
 
 Common usages of textwrap stays unchanged, but if you previously
-spelled out the full type for `Options`, you now need to take th extra
-type parameter into account. This means that
+spelled out the full type for `Options`, you now need to take the
+extra type parameters into account. This means that
 
 ```rust
 let options: Options<HyphenSplitter> = Options::new(80);
@@ -20,7 +33,7 @@ let options: Options<HyphenSplitter> = Options::new(80);
 need to change to
 
 ```rust
-let options: Options<AsciiSpace, HyphenSplitter> = Options::new(80);
+let options: Options<wrap_algorithms::FirstFit, AsciiSpace, HyphenSplitter> = Options::new(80);
 ```
 
 You won’t see any chance if you call `wrap` directly with a width or
diff --git a/benches/linear.rs b/benches/linear.rs
index 067633bf..046a956e 100644
--- a/benches/linear.rs
+++ b/benches/linear.rs
@@ -31,7 +31,7 @@ pub fn benchmark(c: &mut Criterion) {
             #[cfg(feature = "unicode-linebreak")]
             {
                 let options = textwrap::Options::new(LINE_LENGTH)
-                    .wrap_algorithm(textwrap::core::WrapAlgorithm::OptimalFit)
+                    .wrap_algorithm(textwrap::wrap_algorithms::OptimalFit)
                     .word_separator(textwrap::UnicodeBreakProperties);
                 group.bench_with_input(
                     BenchmarkId::new("fill_optimal_fit_unicode", length),
@@ -43,7 +43,7 @@ pub fn benchmark(c: &mut Criterion) {
             }
 
             let options = textwrap::Options::new(LINE_LENGTH)
-                .wrap_algorithm(textwrap::core::WrapAlgorithm::OptimalFit)
+                .wrap_algorithm(textwrap::wrap_algorithms::OptimalFit)
                 .word_separator(textwrap::AsciiSpace);
             group.bench_with_input(
                 BenchmarkId::new("fill_optimal_fit_ascii", length),
@@ -55,7 +55,7 @@ pub fn benchmark(c: &mut Criterion) {
         }
 
         let options = textwrap::Options::new(LINE_LENGTH)
-            .wrap_algorithm(textwrap::core::WrapAlgorithm::FirstFit)
+            .wrap_algorithm(textwrap::wrap_algorithms::FirstFit)
             .word_separator(textwrap::AsciiSpace);
         group.bench_with_input(
             BenchmarkId::new("fill_first_fit", length),
diff --git a/examples/interactive.rs b/examples/interactive.rs
index 02c29b50..a75a7fc4 100644
--- a/examples/interactive.rs
+++ b/examples/interactive.rs
@@ -19,8 +19,7 @@ mod unix_only {
     use termion::raw::{IntoRawMode, RawTerminal};
     use termion::screen::AlternateScreen;
     use termion::{color, cursor, style};
-    #[cfg(feature = "smawk")]
-    use textwrap::core::WrapAlgorithm::{FirstFit, OptimalFit};
+    use textwrap::wrap_algorithms;
     use textwrap::{wrap, AsciiSpace, Options, WordSeparator};
     use textwrap::{HyphenSplitter, NoHyphenation, WordSplitter};
 
@@ -57,7 +56,12 @@ mod unix_only {
 
     fn draw_text<'a>(
         text: &str,
-        options: &Options<'a, Box<dyn WordSeparator>, Box<dyn WordSplitter>>,
+        options: &Options<
+            'a,
+            Box<dyn wrap_algorithms::WrapAlgorithm>,
+            Box<dyn WordSeparator>,
+            Box<dyn WordSplitter>,
+        >,
         splitter_label: &str,
         stdout: &mut RawTerminal<io::Stdout>,
     ) -> Result<(), io::Error> {
@@ -229,6 +233,11 @@ mod unix_only {
     }
 
     pub fn main() -> Result<(), io::Error> {
+        let mut wrap_algorithms: Vec<Box<dyn wrap_algorithms::WrapAlgorithm>> =
+            vec![Box::new(wrap_algorithms::FirstFit)];
+        #[cfg(feature = "smawk")]
+        wrap_algorithms.push(Box::new(wrap_algorithms::OptimalFit));
+
         let mut splitters: Vec<Box<dyn WordSplitter>> =
             vec![Box::new(HyphenSplitter), Box::new(NoHyphenation)];
         let mut splitter_labels: Vec<String> =
@@ -255,6 +264,7 @@ mod unix_only {
 
         let mut options = Options::new(35)
             .break_words(false)
+            .wrap_algorithm(wrap_algorithms.remove(0))
             .splitter(splitters.remove(0))
             .word_separator(Box::new(AsciiSpace) as Box<dyn WordSeparator>);
         let mut splitter_label = splitter_labels.remove(0);
@@ -291,10 +301,8 @@ mod unix_only {
                 Key::Ctrl('b') => options.break_words = !options.break_words,
                 #[cfg(feature = "smawk")]
                 Key::Ctrl('o') => {
-                    options.wrap_algorithm = match options.wrap_algorithm {
-                        OptimalFit => FirstFit,
-                        FirstFit => OptimalFit,
-                    }
+                    std::mem::swap(&mut options.wrap_algorithm, &mut wrap_algorithms[0]);
+                    wrap_algorithms.rotate_left(1);
                 }
                 Key::Ctrl('s') => {
                     // We always keep the next splitter at position 0.
diff --git a/examples/wasm/src/lib.rs b/examples/wasm/src/lib.rs
index 69d5f3d6..3f858563 100644
--- a/examples/wasm/src/lib.rs
+++ b/examples/wasm/src/lib.rs
@@ -1,7 +1,7 @@
 use wasm_bindgen::prelude::*;
 use wasm_bindgen::JsCast;
 
-use textwrap::{core, WordSeparator};
+use textwrap::{core, wrap_algorithms, WordSeparator};
 
 #[wasm_bindgen]
 extern "C" {
@@ -168,7 +168,7 @@ pub fn draw_wrapped_text(
             .collect::<Vec<_>>();
 
         let line_lengths = [width * PRECISION];
-        let wrapped_words = core::wrap_first_fit(&canvas_words, &line_lengths);
+        let wrapped_words = wrap_algorithms::wrap_first_fit(&canvas_words, &line_lengths);
 
         for words_in_line in wrapped_words {
             lineno += 1;
diff --git a/fuzz/fuzz_targets/fill_first_fit.rs b/fuzz/fuzz_targets/fill_first_fit.rs
index 3e1f0a7f..ca319b0a 100644
--- a/fuzz/fuzz_targets/fill_first_fit.rs
+++ b/fuzz/fuzz_targets/fill_first_fit.rs
@@ -1,9 +1,9 @@
 #![no_main]
 use libfuzzer_sys::fuzz_target;
-use textwrap::core::WrapAlgorithm::FirstFit;
+use textwrap::wrap_algorithms;
 use textwrap::Options;
 
 fuzz_target!(|input: (String, usize)| {
-    let options = Options::new(input.1).wrap_algorithm(FirstFit);
+    let options = Options::new(input.1).wrap_algorithm(wrap_algorithms::FirstFit);
     let _ = textwrap::fill(&input.0, &options);
 });
diff --git a/fuzz/fuzz_targets/fill_optimal_fit.rs b/fuzz/fuzz_targets/fill_optimal_fit.rs
index 13740cb7..6af652ba 100644
--- a/fuzz/fuzz_targets/fill_optimal_fit.rs
+++ b/fuzz/fuzz_targets/fill_optimal_fit.rs
@@ -1,9 +1,9 @@
 #![no_main]
 use libfuzzer_sys::fuzz_target;
-use textwrap::core::WrapAlgorithm::OptimalFit;
+use textwrap::wrap_algorithms;
 use textwrap::Options;
 
 fuzz_target!(|input: (String, usize)| {
-    let options = Options::new(input.1).wrap_algorithm(OptimalFit);
+    let options = Options::new(input.1).wrap_algorithm(wrap_algorithms::OptimalFit);
     let _ = textwrap::fill(&input.0, &options);
 });
diff --git a/src/core.rs b/src/core.rs
index 0240abef..ff419753 100644
--- a/src/core.rs
+++ b/src/core.rs
@@ -20,9 +20,11 @@
 //!    fit on a single line. This is implemented in [`break_words`].
 //!
 //! 4. Finally take your fragments and put them into lines. There are
-//!    two algorithms for this: [`wrap_optimal_fit`] and
-//!    [`wrap_first_fit`]. The former produces better line breaks, the
-//!    latter is faster.
+//!    two algorithms for this in the
+//!    [`wrap_algorithms`](crate::wrap_algorithms) module:
+//!    [`wrap_optimal_fit`](crate::wrap_algorithms::wrap_optimal_fit)
+//!    and [`wrap_first_fit`](crate::wrap_algorithms::wrap_first_fit).
+//!    The former produces better line breaks, the latter is faster.
 //!
 //! 5. Iterate through the slices returned by the wrapping functions
 //!    and construct your lines of output.
@@ -33,11 +35,6 @@
 
 use crate::{Options, WordSplitter};
 
-#[cfg(feature = "smawk")]
-mod optimal_fit;
-#[cfg(feature = "smawk")]
-pub use optimal_fit::wrap_optimal_fit;
-
 /// The CSI or “Control Sequence Introducer” introduces an ANSI escape
 /// sequence. This is typically used for colored text and will be
 /// ignored when computing the text width.
@@ -353,9 +350,9 @@ impl Fragment for Word<'_> {
 ///     vec![Word::from("foo-bar")]
 /// );
 /// ```
-pub fn split_words<'a, I, R, S>(
+pub fn split_words<'a, I, A, R, S>(
     words: I,
-    options: &'a Options<'a, R, S>,
+    options: &'a Options<'a, A, R, S>,
 ) -> impl Iterator<Item = Word<'a>>
 where
     I: IntoIterator<Item = Word<'a>>,
@@ -413,207 +410,6 @@ where
     shortened_words
 }
 
-/// Wrapping algorithms.
-///
-/// After a text has been broken into [`Fragment`]s, the one now has
-/// to decide how to break the fragments into lines. The simplest
-/// algorithm for this is implemented by [`wrap_first_fit`]: it uses
-/// no look-ahead and simply adds fragments to the line as long as
-/// they fit. However, this can lead to poor line breaks if a large
-/// fragment almost-but-not-quite fits on a line. When that happens,
-/// the fragment is moved to the next line and it will leave behind a
-/// large gap. A more advanced algorithm, implemented by
-/// [`wrap_optimal_fit`], will take this into account. The optimal-fit
-/// algorithm considers all possible line breaks and will attempt to
-/// minimize the gaps left behind by overly short lines.
-///
-/// While both algorithms run in linear time, the first-fit algorithm
-/// is about 4 times faster than the optimal-fit algorithm.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub enum WrapAlgorithm {
-    /// Use an advanced algorithm which considers the entire paragraph
-    /// to find optimal line breaks. Implemented by
-    /// [`wrap_optimal_fit`].
-    ///
-    /// **Note:** Only available when the `smawk` Cargo feature is
-    /// enabled.
-    #[cfg(feature = "smawk")]
-    OptimalFit,
-    /// Use a fast and simple algorithm with no look-ahead to find
-    /// line breaks. Implemented by [`wrap_first_fit`].
-    FirstFit,
-}
-
-/// Wrap abstract fragments into lines with a first-fit algorithm.
-///
-/// 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
-/// further when arranging them into lines.
-///
-/// # First-Fit Algorithm
-///
-/// This implements a simple “greedy” algorithm: accumulate fragments
-/// one by one and when a fragment no longer fits, start a new line.
-/// There is no look-ahead, we simply take first fit of the fragments
-/// we find.
-///
-/// While fast and predictable, this algorithm can produce poor line
-/// breaks when a long fragment is moved to a new line, leaving behind
-/// a large gap:
-///
-/// ```
-/// use textwrap::core::{wrap_first_fit, Word};
-/// use textwrap::{AsciiSpace, WordSeparator};
-///
-/// // Helper to convert wrapped lines to a Vec<String>.
-/// fn lines_to_strings(lines: Vec<&[Word<'_>]>) -> Vec<String> {
-///     lines.iter().map(|line| {
-///         line.iter().map(|word| &**word).collect::<Vec<_>>().join(" ")
-///     }).collect::<Vec<_>>()
-/// }
-///
-/// 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])),
-///            vec!["These few words",
-///                 "will",  // <-- short line
-///                 "unfortunately",
-///                 "not wrap",
-///                 "nicely."]);
-///
-/// // 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])),
-///            vec!["These few",
-///                 "words will",
-///                 "unfortunately",
-///                 "not wrap",
-///                 "nicely."]);
-/// ```
-///
-/// The [`wrap_optimal_fit`] function was used above to get better
-/// line breaks. It uses an advanced algorithm which tries to avoid
-/// short lines. This function is about 4 times faster than
-/// [`wrap_optimal_fit`].
-///
-/// # Examples
-///
-/// Imagine you're building a house site and you have a number of
-/// tasks you need to execute. Things like pour foundation, complete
-/// framing, install plumbing, electric cabling, install insulation.
-///
-/// The construction workers can only work during daytime, so they
-/// need to pack up everything at night. Because they need to secure
-/// their tools and move machines back to the garage, this process
-/// takes much more time than the time it would take them to simply
-/// switch to another task.
-///
-/// You would like to make a list of tasks to execute every day based
-/// on your estimates. You can model this with a program like this:
-///
-/// ```
-/// use textwrap::core::{wrap_first_fit, Fragment};
-///
-/// #[derive(Debug)]
-/// struct Task<'a> {
-///     name: &'a str,
-///     hours: usize,   // Time needed to complete task.
-///     sweep: usize,   // Time needed for a quick sweep after task during the day.
-///     cleanup: usize, // Time needed for full cleanup if day ends with this task.
-/// }
-///
-/// impl Fragment for Task<'_> {
-///     fn width(&self) -> usize { self.hours }
-///     fn whitespace_width(&self) -> usize { self.sweep }
-///     fn penalty_width(&self) -> usize { self.cleanup }
-/// }
-///
-/// // The morning tasks
-/// let tasks = vec![
-///     Task { name: "Foundation",  hours: 4, sweep: 2, cleanup: 3 },
-///     Task { name: "Framing",     hours: 3, sweep: 1, cleanup: 2 },
-///     Task { name: "Plumbing",    hours: 2, sweep: 2, cleanup: 2 },
-///     Task { name: "Electrical",  hours: 2, sweep: 1, cleanup: 2 },
-///     Task { name: "Insulation",  hours: 2, sweep: 1, cleanup: 2 },
-///     Task { name: "Drywall",     hours: 3, sweep: 1, cleanup: 2 },
-///     Task { name: "Floors",      hours: 3, sweep: 1, cleanup: 2 },
-///     Task { name: "Countertops", hours: 1, sweep: 1, cleanup: 2 },
-///     Task { name: "Bathrooms",   hours: 2, sweep: 1, cleanup: 2 },
-/// ];
-///
-/// // Fill tasks into days, taking `day_length` into account. The
-/// // output shows the hours worked per day along with the names of
-/// // the tasks for that day.
-/// fn assign_days<'a>(tasks: &[Task<'a>], day_length: usize) -> Vec<(usize, Vec<&'a str>)> {
-///     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, &[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();
-///         let names = day.iter().map(|t| t.name).collect::<Vec<_>>();
-///         days.push((work_hours - last.sweep + last.cleanup, names));
-///     }
-///     days
-/// }
-///
-/// // With a single crew working 8 hours a day:
-/// assert_eq!(
-///     assign_days(&tasks, 8),
-///     [
-///         (7, vec!["Foundation"]),
-///         (8, vec!["Framing", "Plumbing"]),
-///         (7, vec!["Electrical", "Insulation"]),
-///         (5, vec!["Drywall"]),
-///         (7, vec!["Floors", "Countertops"]),
-///         (4, vec!["Bathrooms"]),
-///     ]
-/// );
-///
-/// // With two crews working in shifts, 16 hours a day:
-/// assert_eq!(
-///     assign_days(&tasks, 16),
-///     [
-///         (14, vec!["Foundation", "Framing", "Plumbing"]),
-///         (15, vec!["Electrical", "Insulation", "Drywall", "Floors"]),
-///         (6, vec!["Countertops", "Bathrooms"]),
-///     ]
-/// );
-/// ```
-///
-/// Apologies to anyone who actually knows how to build a house and
-/// knows how long each step takes :-)
-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
-            .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;
-            width = 0;
-        }
-        width += fragment.width() + fragment.whitespace_width();
-    }
-    lines.push(&fragments[start..]);
-    lines
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/lib.rs b/src/lib.rs
index ce751bf6..f112d1ce 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -153,11 +153,11 @@
 //!   other ways.
 //!
 //! * `smawk`: enables linear-time wrapping of the whole paragraph via
-//!   the [smawk] crate. See the [`core::wrap_optimal_fit`] function
-//!   for details on the optimal-fit algorithm.
+//!   the [smawk] crate. See the [`wrap_algorithms::wrap_optimal_fit`]
+//!   function for details on the optimal-fit algorithm.
 //!
 //!   This feature can be disabled if you only ever intend to use
-//!   [`core::wrap_first_fit`].
+//!   [`wrap_algorithms::wrap_first_fit`].
 //!
 //! ## Optional Features
 //!
@@ -192,6 +192,8 @@ pub use crate::indentation::indent;
 mod splitting;
 pub use crate::splitting::{HyphenSplitter, NoHyphenation, WordSplitter};
 
+pub mod wrap_algorithms;
+
 mod word_separator;
 #[cfg(feature = "unicode-linebreak")]
 pub use word_separator::UnicodeBreakProperties;
@@ -199,8 +201,22 @@ pub use word_separator::{AsciiSpace, WordSeparator};
 
 pub mod core;
 
-// This private macro lets us hide the actual WordSeperator used in
-// function signatures below.
+// These private macros lets us hide the actual WrapAlgorithm and
+// WordSeperator used in the function signatures below.
+#[cfg(feature = "smawk")]
+macro_rules! DefaultWrapAlgorithm {
+    () => {
+        wrap_algorithms::OptimalFit
+    };
+}
+
+#[cfg(not(feature = "smawk"))]
+macro_rules! DefaultWrapAlgorithm {
+    () => {
+        wrap_algorithms::FirstFit
+    };
+}
+
 #[cfg(feature = "unicode-linebreak")]
 macro_rules! DefaultWordSeparator {
     () => {
@@ -217,7 +233,12 @@ macro_rules! DefaultWordSeparator {
 
 /// Holds settings for wrapping and filling text.
 #[derive(Debug, Clone)]
-pub struct Options<'a, R = Box<dyn WordSeparator>, S = Box<dyn WordSplitter>> {
+pub struct Options<
+    'a,
+    A = Box<dyn wrap_algorithms::WrapAlgorithm>,
+    R = Box<dyn WordSeparator>,
+    S = Box<dyn WordSplitter>,
+> {
     /// The width in columns at which the text will be wrapped.
     pub width: usize,
     /// Indentation used for the first line of output. See the
@@ -230,9 +251,9 @@ pub struct Options<'a, R = Box<dyn WordSeparator>, S = Box<dyn WordSplitter>> {
     /// When set to `false`, some lines may be longer than
     /// `self.width`. See the [`Options::break_words`] method.
     pub break_words: bool,
-    /// Wraping algorithm to use, see [`core::WrapAlgorithm`] for
-    /// details.
-    pub wrap_algorithm: core::WrapAlgorithm,
+    /// Wrapping algorithm to use, see the implementations of the
+    /// [`wrap_algorithms::WrapAlgorithm`] trait for details.
+    pub wrap_algorithm: A,
     /// The line breaking algorithm to use, see [`WordSeparator`]
     /// trait for an overview and possible implementations.
     pub word_separator: R,
@@ -243,28 +264,30 @@ pub struct Options<'a, R = Box<dyn WordSeparator>, S = Box<dyn WordSplitter>> {
     pub splitter: S,
 }
 
-impl<'a, R: Clone, S: Clone> From<&'a Options<'a, R, S>> for Options<'a, R, S> {
-    fn from(options: &'a Options<'a, R, S>) -> Self {
+impl<'a, A: Clone, R: Clone, S: Clone> From<&'a Options<'a, A, R, S>> for Options<'a, A, R, S> {
+    fn from(options: &'a Options<'a, A, R, S>) -> Self {
         Self {
             width: options.width,
             initial_indent: options.initial_indent,
             subsequent_indent: options.subsequent_indent,
             break_words: options.break_words,
             word_separator: options.word_separator.clone(),
-            wrap_algorithm: options.wrap_algorithm,
+            wrap_algorithm: options.wrap_algorithm.clone(),
             splitter: options.splitter.clone(),
         }
     }
 }
 
-impl<'a> From<usize> for Options<'a, DefaultWordSeparator!(), HyphenSplitter> {
+impl<'a> From<usize>
+    for Options<'a, DefaultWrapAlgorithm!(), DefaultWordSeparator!(), HyphenSplitter>
+{
     fn from(width: usize) -> Self {
         Options::new(width)
     }
 }
 
 /// Constructors for boxed Options, specifically.
-impl<'a> Options<'a, DefaultWordSeparator!(), HyphenSplitter> {
+impl<'a> Options<'a, DefaultWrapAlgorithm!(), DefaultWordSeparator!(), HyphenSplitter> {
     /// Creates a new [`Options`] with the specified width and static
     /// dispatch using the [`HyphenSplitter`]. Equivalent to
     ///
@@ -285,9 +308,9 @@ impl<'a> Options<'a, DefaultWordSeparator!(), HyphenSplitter> {
     ///     #[cfg(not(feature = "unicode-linebreak"))]
     ///     word_separator: AsciiSpace,
     ///     #[cfg(feature = "smawk")]
-    ///     wrap_algorithm: textwrap::core::WrapAlgorithm::OptimalFit,
+    ///     wrap_algorithm: textwrap::wrap_algorithms::OptimalFit,
     ///     #[cfg(not(feature = "smawk"))]
-    ///     wrap_algorithm: textwrap::core::WrapAlgorithm::FirstFit,
+    ///     wrap_algorithm: textwrap::wrap_algorithms::FirstFit,
     ///     splitter: HyphenSplitter,
     /// }
     /// # ;
@@ -295,7 +318,6 @@ impl<'a> Options<'a, DefaultWordSeparator!(), HyphenSplitter> {
     /// # assert_eq!(actual.initial_indent, expected.initial_indent);
     /// # assert_eq!(actual.subsequent_indent, expected.subsequent_indent);
     /// # assert_eq!(actual.break_words, expected.break_words);
-    /// # assert_eq!(actual.wrap_algorithm, expected.wrap_algorithm);
     /// ```
     ///
     /// Note that the default word separator and wrap algorithms
@@ -335,11 +357,11 @@ impl<'a> Options<'a, DefaultWordSeparator!(), HyphenSplitter> {
     ///
     /// // uses HyphenSplitter with dynamic dispatch
     /// // the actual type: Options<AsciiSpace, Box<dyn WordSplitter>>
-    /// let opt: Options<_> = Options::new(width).splitter(Box::new(HyphenSplitter));
+    /// let opt: Options<_, _, _> = Options::new(width).splitter(Box::new(HyphenSplitter));
     ///
     /// // uses NoHyphenation with dynamic dispatch
     /// // the actual type: Options<AsciiSpace, Box<dyn WordSplitter>>
-    /// let opt: Options<_> = Options::new(width).splitter(Box::new(NoHyphenation));
+    /// let opt: Options<_, _, _> = Options::new(width).splitter(Box::new(NoHyphenation));
     /// ```
     ///
     /// Notice that the last two variables have the same type, despite
@@ -373,7 +395,7 @@ impl<'a> Options<'a, DefaultWordSeparator!(), HyphenSplitter> {
     }
 }
 
-impl<'a, S> Options<'a, DefaultWordSeparator!(), S> {
+impl<'a, S> Options<'a, DefaultWrapAlgorithm!(), DefaultWordSeparator!(), S> {
     /// Creates a new [`Options`] with the specified width and
     /// splitter. Equivalent to
     ///
@@ -395,9 +417,9 @@ impl<'a, S> Options<'a, DefaultWordSeparator!(), S> {
     ///     #[cfg(not(feature = "unicode-linebreak"))]
     ///     word_separator: textwrap::AsciiSpace,
     ///     #[cfg(feature = "smawk")]
-    ///     wrap_algorithm: textwrap::core::WrapAlgorithm::OptimalFit,
+    ///     wrap_algorithm: textwrap::wrap_algorithms::OptimalFit,
     ///     #[cfg(not(feature = "smawk"))]
-    ///     wrap_algorithm: textwrap::core::WrapAlgorithm::FirstFit,
+    ///     wrap_algorithm: textwrap::wrap_algorithms::FirstFit,
     ///     splitter: splitter,
     /// }
     /// # ;
@@ -405,7 +427,6 @@ impl<'a, S> Options<'a, DefaultWordSeparator!(), S> {
     /// # assert_eq!(actual.initial_indent, expected.initial_indent);
     /// # assert_eq!(actual.subsequent_indent, expected.subsequent_indent);
     /// # assert_eq!(actual.break_words, expected.break_words);
-    /// # assert_eq!(actual.wrap_algorithm, expected.wrap_algorithm);
     /// ```
     ///
     /// This constructor allows to specify the splitter to be used. It
@@ -421,9 +442,9 @@ impl<'a, S> Options<'a, DefaultWordSeparator!(), S> {
     ///
     /// // This opt contains a boxed trait object as splitter.
     /// // The type annotation is important, otherwise it will be not a trait object
-    /// let mut opt: Options<_, Box<dyn WordSplitter>> = Options::with_splitter(width, Box::new(NoHyphenation));
+    /// let mut opt: Options<_, _, Box<dyn WordSplitter>> = Options::with_splitter(width, Box::new(NoHyphenation));
     /// // Its type is actually: `Options<AsciiSpace, Box<dyn WordSplitter>>`:
-    /// let opt_coerced: Options<_, Box<dyn WordSplitter>> = opt;
+    /// let opt_coerced: Options<_, _, Box<dyn WordSplitter>> = opt;
     ///
     /// // Thus, it can be overridden with a different splitter.
     /// opt = Options::with_splitter(width, Box::new(HyphenSplitter));
@@ -441,12 +462,14 @@ impl<'a, S> Options<'a, DefaultWordSeparator!(), S> {
     ///
     /// ```
     /// use textwrap::{HyphenSplitter, Options, AsciiSpace};
+    /// use textwrap::wrap_algorithms::FirstFit;
     /// # const width: usize = 80;
     ///
-    /// # #[cfg(not(feature = "unicode-linebreak"))]
-    /// const FOO: Options<AsciiSpace, HyphenSplitter> = Options::with_splitter(width, HyphenSplitter);
-    /// # #[cfg(not(feature = "unicode-linebreak"))]
-    /// static BAR: Options<AsciiSpace, HyphenSplitter> = FOO;
+    /// # #[cfg(all(not(feature = "smawk"), not(feature = "unicode-linebreak")))] {
+    /// const FOO: Options<FirstFit, AsciiSpace, HyphenSplitter> =
+    ///     Options::with_splitter(width, HyphenSplitter);
+    /// static BAR: Options<FirstFit, AsciiSpace, HyphenSplitter> = FOO;
+    /// # }
     /// ```
     pub const fn with_splitter(width: usize, splitter: S) -> Self {
         Options {
@@ -455,16 +478,13 @@ impl<'a, S> Options<'a, DefaultWordSeparator!(), S> {
             subsequent_indent: "",
             break_words: true,
             word_separator: DefaultWordSeparator!(),
-            #[cfg(feature = "smawk")]
-            wrap_algorithm: core::WrapAlgorithm::OptimalFit,
-            #[cfg(not(feature = "smawk"))]
-            wrap_algorithm: core::WrapAlgorithm::FirstFit,
+            wrap_algorithm: DefaultWrapAlgorithm!(),
             splitter: splitter,
         }
     }
 }
 
-impl<'a, R, S> Options<'a, R, S> {
+impl<'a, A, R, S> Options<'a, A, R, S> {
     /// Change [`self.initial_indent`]. The initial indentation is
     /// used on the very first line of output.
     ///
@@ -558,7 +578,7 @@ impl<'a, R, S> Options<'a, R, S> {
     /// See [`WordSeparator`] for details on the choices.
     ///
     /// [`self.word_separator`]: #structfield.word_separator
-    pub fn word_separator<T>(self, word_separator: T) -> Options<'a, T, S> {
+    pub fn word_separator<T>(self, word_separator: T) -> Options<'a, A, T, S> {
         Options {
             width: self.width,
             initial_indent: self.initial_indent,
@@ -572,13 +592,19 @@ impl<'a, R, S> Options<'a, R, S> {
 
     /// Change [`self.wrap_algorithm`].
     ///
-    /// See [`core::WrapAlgorithm`] for details on the choices.
+    /// See the [`wrap_algorithms::WrapAlgorithm`] trait for details on
+    /// the choices.
     ///
     /// [`self.wrap_algorithm`]: #structfield.wrap_algorithm
-    pub fn wrap_algorithm(self, wrap_algorithm: core::WrapAlgorithm) -> Self {
+    pub fn wrap_algorithm<B>(self, wrap_algorithm: B) -> Options<'a, B, R, S> {
         Options {
-            wrap_algorithm,
-            ..self
+            width: self.width,
+            initial_indent: self.initial_indent,
+            subsequent_indent: self.subsequent_indent,
+            break_words: self.break_words,
+            word_separator: self.word_separator,
+            wrap_algorithm: wrap_algorithm,
+            splitter: self.splitter,
         }
     }
 
@@ -593,13 +619,13 @@ impl<'a, R, S> Options<'a, R, S> {
     /// ```
     /// use textwrap::{HyphenSplitter, NoHyphenation, Options};
     /// // The default type returned by `new`:
-    /// let opt: Options<_, HyphenSplitter> = Options::new(80);
+    /// let opt: Options<_, _, HyphenSplitter> = Options::new(80);
     /// // Setting a different splitter changes the type
-    /// let opt: Options<_, NoHyphenation> = opt.splitter(NoHyphenation);
+    /// let opt: Options<_, _, NoHyphenation> = opt.splitter(NoHyphenation);
     /// ```
     ///
     /// [`self.splitter`]: #structfield.splitter
-    pub fn splitter<T>(self, splitter: T) -> Options<'a, R, T> {
+    pub fn splitter<T>(self, splitter: T) -> Options<'a, A, R, T> {
         Options {
             width: self.width,
             initial_indent: self.initial_indent,
@@ -672,11 +698,12 @@ pub fn termwidth() -> usize {
 ///     "- Memory safety\n  without\n  garbage\n  collection."
 /// );
 /// ```
-pub fn fill<'a, R, S, Opt>(text: &str, width_or_options: Opt) -> String
+pub fn fill<'a, A, R, S, Opt>(text: &str, width_or_options: Opt) -> String
 where
+    A: wrap_algorithms::WrapAlgorithm,
     R: WordSeparator,
     S: WordSplitter,
-    Opt: Into<Options<'a, R, S>>,
+    Opt: Into<Options<'a, A, R, S>>,
 {
     // This will avoid reallocation in simple cases (no
     // indentation, no hyphenation).
@@ -738,7 +765,12 @@ where
 /// assert_eq!(options.initial_indent, "* ");
 /// assert_eq!(options.subsequent_indent, "  ");
 /// ```
-pub fn unfill(text: &str) -> (String, Options<'_, DefaultWordSeparator!(), HyphenSplitter>) {
+pub fn unfill(
+    text: &str,
+) -> (
+    String,
+    Options<'_, DefaultWrapAlgorithm!(), DefaultWordSeparator!(), HyphenSplitter>,
+) {
     let trimmed = text.trim_end_matches('\n');
     let prefix_chars: &[_] = &[' ', '-', '+', '*', '>', '#', '/'];
 
@@ -833,11 +865,12 @@ pub fn unfill(text: &str) -> (String, Options<'_, DefaultWordSeparator!(), Hyphe
 ///   item.
 /// ");
 /// ```
-pub fn refill<'a, R, S, Opt>(filled_text: &str, new_width_or_options: Opt) -> String
+pub fn refill<'a, A, R, S, Opt>(filled_text: &str, new_width_or_options: Opt) -> String
 where
+    A: wrap_algorithms::WrapAlgorithm,
     R: WordSeparator,
     S: WordSplitter,
-    Opt: Into<Options<'a, R, S>>,
+    Opt: Into<Options<'a, A, R, S>>,
 {
     let trimmed = filled_text.trim_end_matches('\n');
     let (text, options) = unfill(trimmed);
@@ -903,8 +936,7 @@ where
 /// narrow column with room for only 10 characters looks like this:
 ///
 /// ```
-/// # use textwrap::{Options, wrap};
-/// # use textwrap::core::WrapAlgorithm::FirstFit;
+/// # use textwrap::{wrap_algorithms::FirstFit, Options, wrap};
 /// #
 /// # let lines = wrap("To be, or not to be: that is the question",
 /// #                  Options::new(10).wrap_algorithm(FirstFit));
@@ -929,7 +961,7 @@ where
 /// ```
 /// # #[cfg(feature = "smawk")] {
 /// # use textwrap::{Options, wrap};
-/// # use textwrap::core::WrapAlgorithm::OptimalFit;
+/// # use textwrap::wrap_algorithms::OptimalFit;
 /// #
 /// # let lines = wrap("To be, or not to be: that is the question",
 /// #                  Options::new(10).wrap_algorithm(OptimalFit));
@@ -942,7 +974,7 @@ where
 /// # "); }
 /// ```
 ///
-/// Please see [`core::WrapAlgorithm`] for details.
+/// Please see the [`wrap_algorithms::WrapAlgorithm`] trait for details.
 ///
 /// # Examples
 ///
@@ -1019,11 +1051,12 @@ where
 /// assert_eq!(wrap("  foo bar", 8), vec!["  foo", "bar"]);
 /// assert_eq!(wrap("  foo bar", 4), vec!["", "foo", "bar"]);
 /// ```
-pub fn wrap<'a, R, S, Opt>(text: &str, width_or_options: Opt) -> Vec<Cow<'_, str>>
+pub fn wrap<'a, A, R, S, Opt>(text: &str, width_or_options: Opt) -> Vec<Cow<'_, str>>
 where
+    A: wrap_algorithms::WrapAlgorithm,
     R: WordSeparator,
     S: WordSplitter,
-    Opt: Into<Options<'a, R, S>>,
+    Opt: Into<Options<'a, A, R, S>>,
 {
     let options = width_or_options.into();
 
@@ -1054,11 +1087,7 @@ where
         };
 
         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_widths),
-            core::WrapAlgorithm::FirstFit => core::wrap_first_fit(&broken_words, &line_widths),
-        };
+        let wrapped_words = options.wrap_algorithm.wrap(&broken_words, &line_widths);
 
         let mut idx = 0;
         for words in wrapped_words {
@@ -1167,7 +1196,7 @@ where
 ///                 "| example text, | columns.      | shorter than   |",
 ///                 "| which is      | Notice how    | the others.    |",
 ///                 "| wrapped into  | the final     |                |"]);
-pub fn wrap_columns<'a, R, S, Opt>(
+pub fn wrap_columns<'a, A, R, S, Opt>(
     text: &str,
     columns: usize,
     total_width_or_options: Opt,
@@ -1176,9 +1205,10 @@ pub fn wrap_columns<'a, R, S, Opt>(
     right_gap: &str,
 ) -> Vec<String>
 where
+    A: wrap_algorithms::WrapAlgorithm,
     R: WordSeparator,
     S: WordSplitter,
-    Opt: Into<Options<'a, R, S>>,
+    Opt: Into<Options<'a, A, R, S>>,
 {
     assert!(columns > 0);
 
@@ -1238,6 +1268,7 @@ where
 ///
 /// ```
 /// # use textwrap::{core, AsciiSpace, Options, NoHyphenation};
+/// # use textwrap::wrap_algorithms;
 /// # let width = 80;
 /// Options {
 ///     width: width,
@@ -1245,12 +1276,12 @@ where
 ///     subsequent_indent: "",
 ///     break_words: false,
 ///     word_separator: AsciiSpace,
-///     wrap_algorithm: textwrap::core::WrapAlgorithm::FirstFit,
+///     wrap_algorithm: wrap_algorithms::FirstFit,
 ///     splitter: NoHyphenation,
 /// };
 /// ```
 ///
-/// The wrap algorithm is [`core::WrapAlgorithm::FirstFit`] since this
+/// The wrap algorithm is [`wrap_algorithms::FirstFit`] since this
 /// is the fastest algorithm — and the main reason to use
 /// `fill_inplace` is to get the string broken into newlines as fast
 /// as possible.
@@ -1281,7 +1312,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 = wrap_algorithms::wrap_first_fit(&words, &[width]);
 
         let mut line_offset = offset;
         for words in &wrapped_words[..wrapped_words.len() - 1] {
@@ -1311,6 +1342,8 @@ pub fn fill_inplace(text: &mut String, width: usize) {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::wrap_algorithms;
+
     #[cfg(feature = "hyphenation")]
     use hyphenation::{Language, Load, Standard};
 
@@ -1344,7 +1377,7 @@ mod tests {
         assert_eq!(
             wrap(
                 "To be, or not to be, that is the question.",
-                Options::new(10).wrap_algorithm(core::WrapAlgorithm::FirstFit)
+                Options::new(10).wrap_algorithm(wrap_algorithms::FirstFit)
             ),
             vec!["To be, or", "not to be,", "that is", "the", "question."]
         );
@@ -1566,7 +1599,7 @@ mod tests {
 
     #[test]
     fn simple_hyphens_dynamic() {
-        let options: Options<_> = Options::new(8).splitter(Box::new(HyphenSplitter));
+        let options: Options<_, _> = Options::new(8).splitter(Box::new(HyphenSplitter));
         assert_eq!(wrap("foo bar-baz", &options), vec!["foo bar-", "baz"]);
     }
 
@@ -1578,7 +1611,7 @@ mod tests {
 
     #[test]
     fn no_hyphenation_dynamic() {
-        let options: Options<_> = Options::new(8).splitter(Box::new(NoHyphenation));
+        let options: Options<_, _> = Options::new(8).splitter(Box::new(NoHyphenation));
         assert_eq!(wrap("foo bar-baz", &options), vec!["foo", "bar-baz"]);
     }
 
@@ -1603,7 +1636,7 @@ mod tests {
     #[cfg(feature = "hyphenation")]
     fn auto_hyphenation_double_hyphenation_dynamic() {
         let dictionary = Standard::from_embedded(Language::EnglishUS).unwrap();
-        let mut options: Options<'_, _, Box<dyn WordSplitter>> =
+        let mut options: Options<_, _, Box<dyn WordSplitter>> =
             Options::new(10).splitter(Box::new(HyphenSplitter));
         assert_eq!(
             wrap("Internationalization", &options),
@@ -1731,14 +1764,14 @@ mod tests {
         assert_eq!(
             fill(
                 "1 3 5 7\n1 3 5 7",
-                Options::new(7).wrap_algorithm(core::WrapAlgorithm::FirstFit)
+                Options::new(7).wrap_algorithm(wrap_algorithms::FirstFit)
             ),
             "1 3 5 7\n1 3 5 7"
         );
         assert_eq!(
             fill(
                 "1 3 5 7\n1 3 5 7",
-                Options::new(5).wrap_algorithm(core::WrapAlgorithm::FirstFit)
+                Options::new(5).wrap_algorithm(wrap_algorithms::FirstFit)
             ),
             "1 3 5\n7\n1 3 5\n7"
         );
@@ -1781,9 +1814,10 @@ mod tests {
     }
 
     #[test]
+    #[cfg(not(feature = "smawk"))]
     #[cfg(not(feature = "unicode-linebreak"))]
     fn cloning_works() {
-        static OPT: Options<AsciiSpace, HyphenSplitter> =
+        static OPT: Options<wrap_algorithms::FirstFit, AsciiSpace, HyphenSplitter> =
             Options::with_splitter(80, HyphenSplitter);
         #[allow(clippy::clone_on_copy)]
         let opt = OPT.clone();
@@ -1928,11 +1962,11 @@ mod tests {
     #[test]
     fn trait_object_vec() {
         // Create a vector of Options containing trait-objects.
-        let mut vector: Vec<Options<Box<dyn WordSeparator>, Box<dyn WordSplitter>>> = Vec::new();
+        let mut vector: Vec<Options<_, Box<dyn WordSeparator>, Box<dyn WordSplitter>>> = Vec::new();
         // Expected result from each options
         let mut results = Vec::new();
 
-        let opt_full_type: Options<Box<dyn WordSeparator>, Box<dyn WordSplitter>> =
+        let opt_full_type: Options<_, Box<dyn WordSeparator>, Box<dyn WordSplitter>> =
             Options::new(10)
                 .splitter(Box::new(HyphenSplitter) as Box<dyn WordSplitter>)
                 .word_separator(Box::new(AsciiSpace) as Box<dyn WordSeparator>);
diff --git a/src/splitting.rs b/src/splitting.rs
index 4f7cd065..c8ac14b6 100644
--- a/src/splitting.rs
+++ b/src/splitting.rs
@@ -55,7 +55,8 @@ pub trait WordSplitter: WordSplitterClone + std::fmt::Debug {
 
 // The internal `WordSplitterClone` trait is allows us to implement
 // `Clone` for `Box<dyn WordSplitter>`. This in used in the
-// `From<&Options<'_, R, S>> for Options<'a, R, S>` implementation.
+// `From<&Options<'_, A, R, S>> for Options<'a, A, R, S>`
+// implementation.
 pub trait WordSplitterClone {
     fn clone_box(&self) -> Box<dyn WordSplitter>;
 }
diff --git a/src/word_separator.rs b/src/word_separator.rs
index 469c9da4..ddbcaa3c 100644
--- a/src/word_separator.rs
+++ b/src/word_separator.rs
@@ -33,7 +33,8 @@ pub trait WordSeparator: WordSeparatorClone + std::fmt::Debug {
 
 // The internal `WordSeparatorClone` trait is allows us to implement
 // `Clone` for `Box<dyn WordSeparator>`. This in used in the
-// `From<&Options<'_, R, S>> for Options<'a, R, S>` implementation.
+// `From<&Options<'_, A, R, S>> for Options<'a, A, R, S>`
+// implementation.
 pub trait WordSeparatorClone {
     fn clone_box(&self) -> Box<dyn WordSeparator>;
 }
diff --git a/src/wrap_algorithms.rs b/src/wrap_algorithms.rs
new file mode 100644
index 00000000..6b0845d9
--- /dev/null
+++ b/src/wrap_algorithms.rs
@@ -0,0 +1,257 @@
+//! Word wrapping algorithms.
+//!
+//! After a text has been broken into words (or [`Fragment`]s), one
+//! now has to decide how to break the fragments into lines. The
+//! simplest algorithm for this is implemented by [`wrap_first_fit`]:
+//! it uses no look-ahead and simply adds fragments to the line as
+//! long as they fit. However, this can lead to poor line breaks if a
+//! large fragment almost-but-not-quite fits on a line. When that
+//! happens, the fragment is moved to the next line and it will leave
+//! behind a large gap. A more advanced algorithm, implemented by
+//! [`wrap_optimal_fit`], will take this into account. The optimal-fit
+//! algorithm considers all possible line breaks and will attempt to
+//! minimize the gaps left behind by overly short lines.
+//!
+//! While both algorithms run in linear time, the first-fit algorithm
+//! is about 4 times faster than the optimal-fit algorithm.
+
+#[cfg(feature = "smawk")]
+mod optimal_fit;
+#[cfg(feature = "smawk")]
+pub use optimal_fit::{wrap_optimal_fit, OptimalFit};
+
+use crate::core::{Fragment, Word};
+
+/// Describes how to wrap words into lines.
+///
+/// The simplest approach is to wrap words one word at a time. This is
+/// implemented by [`FirstFit`]. If the `smawk` Cargo feature is
+/// enabled, a more complex algorithm is available, implemented by
+/// [`OptimalFit`], which will look at an entire paragraph at a time
+/// in order to find optimal line breaks.
+pub trait WrapAlgorithm: WrapAlgorithmClone + std::fmt::Debug {
+    /// Wrap words according to line widths.
+    ///
+    /// The `line_widths` slice gives the target line width for each
+    /// line (the last slice element is repeated as necessary). This
+    /// can be used to implement hanging indentation.
+    ///
+    /// Please see the implementors of the trait for examples.
+    fn wrap<'a, 'b>(&self, words: &'b [Word<'a>], line_widths: &'b [usize]) -> Vec<&'b [Word<'a>]>;
+}
+
+// The internal `WrapAlgorithmClone` trait is allows us to implement
+// `Clone` for `Box<dyn WrapAlgorithm>`. This in used in the
+// `From<&Options<'_, A, R, S>> for Options<'a, A, R, S>`
+// implementation.
+#[doc(hidden)]
+pub trait WrapAlgorithmClone {
+    fn clone_box(&self) -> Box<dyn WrapAlgorithm>;
+}
+
+impl<T: WrapAlgorithm + Clone + 'static> WrapAlgorithmClone for T {
+    fn clone_box(&self) -> Box<dyn WrapAlgorithm> {
+        Box::new(self.clone())
+    }
+}
+
+impl Clone for Box<dyn WrapAlgorithm> {
+    fn clone(&self) -> Box<dyn WrapAlgorithm> {
+        use std::ops::Deref;
+        self.deref().clone_box()
+    }
+}
+
+impl WrapAlgorithm for Box<dyn WrapAlgorithm> {
+    fn wrap<'a, 'b>(&self, words: &'b [Word<'a>], line_widths: &'b [usize]) -> Vec<&'b [Word<'a>]> {
+        use std::ops::Deref;
+        self.deref().wrap(words, line_widths)
+    }
+}
+
+/// Wrap words using a fast and simple algorithm.
+///
+/// This algorithm uses no look-ahead when finding line breaks.
+/// Implemented by [`wrap_first_fit`], please see that function for
+/// details and examples.
+#[derive(Clone, Copy, Debug, Default)]
+pub struct FirstFit;
+
+impl WrapAlgorithm for FirstFit {
+    #[inline]
+    fn wrap<'a, 'b>(&self, words: &'b [Word<'a>], line_widths: &'b [usize]) -> Vec<&'b [Word<'a>]> {
+        wrap_first_fit(words, line_widths)
+    }
+}
+
+/// Wrap abstract fragments into lines with a first-fit algorithm.
+///
+/// The `line_widths` slice gives 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
+/// further when arranging them into lines.
+///
+/// # First-Fit Algorithm
+///
+/// This implements a simple “greedy” algorithm: accumulate fragments
+/// one by one and when a fragment no longer fits, start a new line.
+/// There is no look-ahead, we simply take first fit of the fragments
+/// we find.
+///
+/// While fast and predictable, this algorithm can produce poor line
+/// breaks when a long fragment is moved to a new line, leaving behind
+/// a large gap:
+///
+/// ```
+/// use textwrap::core::Word;
+/// use textwrap::wrap_algorithms;
+/// use textwrap::{AsciiSpace, WordSeparator};
+///
+/// // Helper to convert wrapped lines to a Vec<String>.
+/// fn lines_to_strings(lines: Vec<&[Word<'_>]>) -> Vec<String> {
+///     lines.iter().map(|line| {
+///         line.iter().map(|word| &**word).collect::<Vec<_>>().join(" ")
+///     }).collect::<Vec<_>>()
+/// }
+///
+/// let text = "These few words will unfortunately not wrap nicely.";
+/// let words = AsciiSpace.find_words(text).collect::<Vec<_>>();
+/// assert_eq!(lines_to_strings(wrap_algorithms::wrap_first_fit(&words, &[15])),
+///            vec!["These few words",
+///                 "will",  // <-- short line
+///                 "unfortunately",
+///                 "not wrap",
+///                 "nicely."]);
+///
+/// // We can avoid the short line if we look ahead:
+/// #[cfg(feature = "smawk")]
+/// assert_eq!(lines_to_strings(wrap_algorithms::wrap_optimal_fit(&words, &[15])),
+///            vec!["These few",
+///                 "words will",
+///                 "unfortunately",
+///                 "not wrap",
+///                 "nicely."]);
+/// ```
+///
+/// The [`wrap_optimal_fit`] function was used above to get better
+/// line breaks. It uses an advanced algorithm which tries to avoid
+/// short lines. This function is about 4 times faster than
+/// [`wrap_optimal_fit`].
+///
+/// # Examples
+///
+/// Imagine you're building a house site and you have a number of
+/// tasks you need to execute. Things like pour foundation, complete
+/// framing, install plumbing, electric cabling, install insulation.
+///
+/// The construction workers can only work during daytime, so they
+/// need to pack up everything at night. Because they need to secure
+/// their tools and move machines back to the garage, this process
+/// takes much more time than the time it would take them to simply
+/// switch to another task.
+///
+/// You would like to make a list of tasks to execute every day based
+/// on your estimates. You can model this with a program like this:
+///
+/// ```
+/// use textwrap::wrap_algorithms::wrap_first_fit;
+/// use textwrap::core::{Fragment, Word};
+///
+/// #[derive(Debug)]
+/// struct Task<'a> {
+///     name: &'a str,
+///     hours: usize,   // Time needed to complete task.
+///     sweep: usize,   // Time needed for a quick sweep after task during the day.
+///     cleanup: usize, // Time needed for full cleanup if day ends with this task.
+/// }
+///
+/// impl Fragment for Task<'_> {
+///     fn width(&self) -> usize { self.hours }
+///     fn whitespace_width(&self) -> usize { self.sweep }
+///     fn penalty_width(&self) -> usize { self.cleanup }
+/// }
+///
+/// // The morning tasks
+/// let tasks = vec![
+///     Task { name: "Foundation",  hours: 4, sweep: 2, cleanup: 3 },
+///     Task { name: "Framing",     hours: 3, sweep: 1, cleanup: 2 },
+///     Task { name: "Plumbing",    hours: 2, sweep: 2, cleanup: 2 },
+///     Task { name: "Electrical",  hours: 2, sweep: 1, cleanup: 2 },
+///     Task { name: "Insulation",  hours: 2, sweep: 1, cleanup: 2 },
+///     Task { name: "Drywall",     hours: 3, sweep: 1, cleanup: 2 },
+///     Task { name: "Floors",      hours: 3, sweep: 1, cleanup: 2 },
+///     Task { name: "Countertops", hours: 1, sweep: 1, cleanup: 2 },
+///     Task { name: "Bathrooms",   hours: 2, sweep: 1, cleanup: 2 },
+/// ];
+///
+/// // Fill tasks into days, taking `day_length` into account. The
+/// // output shows the hours worked per day along with the names of
+/// // the tasks for that day.
+/// fn assign_days<'a>(tasks: &[Task<'a>], day_length: usize) -> Vec<(usize, Vec<&'a str>)> {
+///     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, &[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();
+///         let names = day.iter().map(|t| t.name).collect::<Vec<_>>();
+///         days.push((work_hours - last.sweep + last.cleanup, names));
+///     }
+///     days
+/// }
+///
+/// // With a single crew working 8 hours a day:
+/// assert_eq!(
+///     assign_days(&tasks, 8),
+///     [
+///         (7, vec!["Foundation"]),
+///         (8, vec!["Framing", "Plumbing"]),
+///         (7, vec!["Electrical", "Insulation"]),
+///         (5, vec!["Drywall"]),
+///         (7, vec!["Floors", "Countertops"]),
+///         (4, vec!["Bathrooms"]),
+///     ]
+/// );
+///
+/// // With two crews working in shifts, 16 hours a day:
+/// assert_eq!(
+///     assign_days(&tasks, 16),
+///     [
+///         (14, vec!["Foundation", "Framing", "Plumbing"]),
+///         (15, vec!["Electrical", "Insulation", "Drywall", "Floors"]),
+///         (6, vec!["Countertops", "Bathrooms"]),
+///     ]
+/// );
+/// ```
+///
+/// Apologies to anyone who actually knows how to build a house and
+/// knows how long each step takes :-)
+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
+            .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;
+            width = 0;
+        }
+        width += fragment.width() + fragment.whitespace_width();
+    }
+    lines.push(&fragments[start..]);
+    lines
+}
diff --git a/src/core/optimal_fit.rs b/src/wrap_algorithms/optimal_fit.rs
similarity index 91%
rename from src/core/optimal_fit.rs
rename to src/wrap_algorithms/optimal_fit.rs
index b97afb87..95ecf1f7 100644
--- a/src/core/optimal_fit.rs
+++ b/src/wrap_algorithms/optimal_fit.rs
@@ -1,6 +1,26 @@
-use crate::core::Fragment;
 use std::cell::RefCell;
 
+use crate::core::{Fragment, Word};
+use crate::wrap_algorithms::WrapAlgorithm;
+
+/// Wrap words using an advanced algorithm with look-ahead.
+///
+/// This wrapping algorithm considers the entire paragraph to find
+/// optimal line breaks. Implemented by [`wrap_optimal_fit`], please
+/// see that function for details and examples.
+///
+/// **Note:** Only available when the `smawk` Cargo feature is
+/// enabled.
+#[derive(Clone, Copy, Debug, Default)]
+pub struct OptimalFit;
+
+impl WrapAlgorithm for OptimalFit {
+    #[inline]
+    fn wrap<'a, 'b>(&self, words: &'b [Word<'a>], line_widths: &'b [usize]) -> Vec<&'b [Word<'a>]> {
+        wrap_optimal_fit(words, line_widths)
+    }
+}
+
 /// Cache for line numbers. This is necessary to avoid a O(n**2)
 /// behavior when computing line numbers in [`wrap_optimal_fit`].
 struct LineNumbers {
@@ -39,7 +59,8 @@ const NLINE_PENALTY: i32 = 1000;
 /// overflow the line by 1 character in extreme cases:
 ///
 /// ```
-/// use textwrap::core::{wrap_optimal_fit, Word};
+/// use textwrap::wrap_algorithms::wrap_optimal_fit;
+/// use textwrap::core::Word;
 ///
 /// let short = "foo ";
 /// let long = "x".repeat(50);
@@ -81,7 +102,7 @@ const HYPHEN_PENALTY: i32 = 25;
 
 /// Wrap abstract fragments into lines with an optimal-fit algorithm.
 ///
-/// The `line_widths` slice give the target line width for each line
+/// The `line_widths` slice gives the target line width for each line
 /// (the last slice element is repeated as necessary). This can be
 /// used to implement hanging indentation.
 ///


Test patch:
diff --git a/tests/traits.rs b/tests/traits.rs
index c4bda24e..dfc52b9e 100644
--- a/tests/traits.rs
+++ b/tests/traits.rs
@@ -1,4 +1,7 @@
-use textwrap::{AsciiSpace, NoHyphenation, Options, WordSeparator, WordSplitter};
+use textwrap::wrap_algorithms::{FirstFit, WrapAlgorithm};
+use textwrap::Options;
+use textwrap::{AsciiSpace, WordSeparator};
+use textwrap::{NoHyphenation, WordSplitter};
 
 /// Cleaned up type name.
 fn type_name<T: ?Sized>(_val: &T) -> String {
@@ -9,27 +12,43 @@ fn type_name<T: ?Sized>(_val: &T) -> String {
 }
 
 #[test]
+#[cfg(not(feature = "smawk"))]
 #[cfg(not(feature = "unicode-linebreak"))]
 fn static_hyphensplitter() {
     // Inferring the full type.
     let options = Options::new(10);
     assert_eq!(
         type_name(&options),
-        "textwrap::Options<textwrap::AsciiSpace, textwrap::HyphenSplitter>"
+        format!(
+            "textwrap::Options<{}, {}, {}>",
+            "textwrap::wrap_algorithms::FirstFit",
+            "textwrap::AsciiSpace",
+            "textwrap::HyphenSplitter"
+        )
     );
 
     // Inferring part of the type.
-    let options: Options<_, textwrap::HyphenSplitter> = Options::new(10);
+    let options: Options<_, _, textwrap::HyphenSplitter> = Options::new(10);
     assert_eq!(
         type_name(&options),
-        "textwrap::Options<textwrap::AsciiSpace, textwrap::HyphenSplitter>"
+        format!(
+            "textwrap::Options<{}, {}, {}>",
+            "textwrap::wrap_algorithms::FirstFit",
+            "textwrap::AsciiSpace",
+            "textwrap::HyphenSplitter"
+        )
     );
 
     // Explicitly making all parameters inferred.
-    let options: Options<'_, _, _> = Options::new(10);
+    let options: Options<_, _, _> = Options::new(10);
     assert_eq!(
         type_name(&options),
-        "textwrap::Options<textwrap::AsciiSpace, textwrap::HyphenSplitter>"
+        format!(
+            "textwrap::Options<{}, {}, {}>",
+            "textwrap::wrap_algorithms::FirstFit",
+            "textwrap::AsciiSpace",
+            "textwrap::HyphenSplitter"
+        )
     );
 }
 
@@ -37,11 +56,17 @@ fn static_hyphensplitter() {
 fn box_static_nohyphenation() {
     // Inferred static type.
     let options = Options::new(10)
+        .wrap_algorithm(Box::new(FirstFit))
         .splitter(Box::new(NoHyphenation))
         .word_separator(Box::new(AsciiSpace));
     assert_eq!(
         type_name(&options),
-        "textwrap::Options<Box<textwrap::AsciiSpace>, Box<textwrap::NoHyphenation>>"
+        format!(
+            "textwrap::Options<{}, {}, {}>",
+            "Box<textwrap::wrap_algorithms::FirstFit>",
+            "Box<textwrap::AsciiSpace>",
+            "Box<textwrap::NoHyphenation>"
+        )
     );
 }
 
@@ -49,10 +74,16 @@ fn box_static_nohyphenation() {
 fn box_dyn_wordsplitter() {
     // Inferred dynamic type due to default type parameter.
     let options = Options::new(10)
+        .wrap_algorithm(Box::new(FirstFit) as Box<dyn WrapAlgorithm>)
         .splitter(Box::new(NoHyphenation) as Box<dyn WordSplitter>)
         .word_separator(Box::new(AsciiSpace) as Box<dyn WordSeparator>);
     assert_eq!(
         type_name(&options),
-        "textwrap::Options<Box<dyn textwrap::WordSeparator>, Box<dyn textwrap::WordSplitter>>"
+        format!(
+            "textwrap::Options<{}, {}, {}>",
+            "Box<dyn textwrap::wrap_algorithms::WrapAlgorithm>",
+            "Box<dyn textwrap::WordSeparator>",
+            "Box<dyn textwrap::WordSplitter>"
+        )
     );
 }