Skip to content

Commit

Permalink
Merge pull request #389 from mgeisler/optimal-fit-params
Browse files Browse the repository at this point in the history
Introduce parameters to `OptimalFit`
  • Loading branch information
mgeisler authored Jun 22, 2021
2 parents 050b749 + 01f51cd commit 607704a
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 99 deletions.
4 changes: 2 additions & 2 deletions benches/linear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub fn benchmark(c: &mut Criterion) {
#[cfg(feature = "unicode-linebreak")]
{
let options = textwrap::Options::new(LINE_LENGTH)
.wrap_algorithm(textwrap::wrap_algorithms::OptimalFit)
.wrap_algorithm(textwrap::wrap_algorithms::OptimalFit::new())
.word_separator(textwrap::word_separators::UnicodeBreakProperties);
group.bench_with_input(
BenchmarkId::new("fill_optimal_fit_unicode", length),
Expand All @@ -43,7 +43,7 @@ pub fn benchmark(c: &mut Criterion) {
}

let options = textwrap::Options::new(LINE_LENGTH)
.wrap_algorithm(textwrap::wrap_algorithms::OptimalFit)
.wrap_algorithm(textwrap::wrap_algorithms::OptimalFit::new())
.word_separator(textwrap::word_separators::AsciiSpace);
group.bench_with_input(
BenchmarkId::new("fill_optimal_fit_ascii", length),
Expand Down
16 changes: 12 additions & 4 deletions examples/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,20 @@ mod unix_only {

#[cfg(feature = "smawk")]
{
// The OptimalFit struct formats itself with a ton of
// parameters. This removes the parameters, leaving only
// the struct name behind.
let wrap_algorithm_label = format!("{:?}", options.wrap_algorithm)
.split(' ')
.next()
.unwrap()
.to_string();
write!(
stdout,
"{}- algorithm: {}{:?}{} (toggle with Ctrl-o)",
"{}- algorithm: {}{}{} (toggle with Ctrl-o)",
cursor::Goto(left_col, left_row),
style::Bold,
options.wrap_algorithm,
wrap_algorithm_label,
style::Reset,
)?;
left_row += 1;
Expand Down Expand Up @@ -233,9 +241,9 @@ 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)];
vec![Box::new(wrap_algorithms::FirstFit::new())];
#[cfg(feature = "smawk")]
wrap_algorithms.push(Box::new(wrap_algorithms::OptimalFit));
wrap_algorithms.push(Box::new(wrap_algorithms::OptimalFit::new()));

let mut word_splitters: Vec<Box<dyn word_splitters::WordSplitter>> = vec![
Box::new(word_splitters::HyphenSplitter),
Expand Down
4 changes: 3 additions & 1 deletion examples/wasm/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 50 additions & 2 deletions examples/wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use wasm_bindgen::JsCast;
use textwrap::core;
use textwrap::word_separators::{AsciiSpace, UnicodeBreakProperties, WordSeparator};
use textwrap::word_splitters::{split_words, HyphenSplitter, NoHyphenation, WordSplitter};
use textwrap::wrap_algorithms::{wrap_first_fit, wrap_optimal_fit};
use textwrap::wrap_algorithms::{wrap_first_fit, wrap_optimal_fit, OptimalFit};

#[wasm_bindgen]
extern "C" {
Expand Down Expand Up @@ -248,6 +248,48 @@ pub enum WasmWrapAlgorithm {
OptimalFit = "OptimalFit",
}

#[wasm_bindgen]
#[derive(Copy, Clone, Debug, Default)]
pub struct WasmOptimalFit {
pub nline_penalty: i32,
pub overflow_penalty: i32,
pub short_last_line_fraction: usize,
pub short_last_line_penalty: i32,
pub hyphen_penalty: i32,
}

#[wasm_bindgen]
impl WasmOptimalFit {
#[wasm_bindgen(constructor)]
pub fn new(
nline_penalty: i32,
overflow_penalty: i32,
short_last_line_fraction: usize,
short_last_line_penalty: i32,
hyphen_penalty: i32,
) -> WasmOptimalFit {
WasmOptimalFit {
nline_penalty,
overflow_penalty,
short_last_line_fraction,
short_last_line_penalty,
hyphen_penalty,
}
}
}

impl Into<OptimalFit> for WasmOptimalFit {
fn into(self) -> OptimalFit {
OptimalFit {
nline_penalty: self.nline_penalty,
overflow_penalty: self.overflow_penalty,
short_last_line_fraction: self.short_last_line_fraction,
short_last_line_penalty: self.short_last_line_penalty,
hyphen_penalty: self.hyphen_penalty,
}
}
}

#[wasm_bindgen]
#[derive(Copy, Clone, Debug)]
pub struct WasmOptions {
Expand All @@ -256,6 +298,7 @@ pub struct WasmOptions {
pub word_separator: WasmWordSeparator,
pub word_splitter: WasmWordSplitter,
pub wrap_algorithm: WasmWrapAlgorithm,
pub optimal_fit: WasmOptimalFit,
}

#[wasm_bindgen]
Expand All @@ -267,13 +310,15 @@ impl WasmOptions {
word_separator: WasmWordSeparator,
word_splitter: WasmWordSplitter,
wrap_algorithm: WasmWrapAlgorithm,
optimal_fit: WasmOptimalFit,
) -> WasmOptions {
WasmOptions {
width,
break_words,
word_separator,
word_splitter,
wrap_algorithm,
optimal_fit,
}
}
}
Expand Down Expand Up @@ -325,7 +370,10 @@ pub fn draw_wrapped_text(
let line_lengths = [options.width * PRECISION];
let wrapped_words = match options.wrap_algorithm {
WasmWrapAlgorithm::FirstFit => wrap_first_fit(&canvas_words, &line_lengths),
WasmWrapAlgorithm::OptimalFit => wrap_optimal_fit(&canvas_words, &line_lengths),
WasmWrapAlgorithm::OptimalFit => {
let penalties = options.optimal_fit.into();
wrap_optimal_fit(&canvas_words, &line_lengths, &penalties)
}
_ => Err("WasmOptions has an invalid wrap_algorithm field")?,
};

Expand Down
34 changes: 31 additions & 3 deletions examples/wasm/www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,43 @@ <h1>Textwrap WebAssembly Demo</h1>
</select>
</div>

<!-- See https://github.com/mgeisler/textwrap/issues/326
<div class="option">
<label for="wrap-algorithm">Wrap algorithm:</label>
<select id="wrap-algorithm">
<option value="OptimalFit">Optimal-fit</option>
<option value="FirstFit">First-fit</option>
</select>
</select>
</div>

<div class="option">
<label for="nline-penalty">Nth line penalty:</label>
<input type="number" id="nline-penalty-text">
<input type="range" id="nline-penalty" min="0" max="10000" value="1000">
</div>

<div class="option">
<label for="overflow-penalty">Overflow penalty:</label>
<input type="number" id="overflow-penalty-text">
<input type="range" id="overflow-penalty" min="-1000" max="10000" value="7500">
</div>

<div class="option">
<label for="short-line-fraction">Short line fraction:</label>
<input type="number" id="short-line-fraction-text">
<input type="range" id="short-line-fraction" min="1" max="10" value="4">
</div>

<div class="option">
<label for="short-last-line-penalty">Short line penalty:</label>
<input type="number" id="short-last-line-penalty-text">
<input type="range" id="short-last-line-penalty" min="-2000" max="2000" value="100">
</div>

<div class="option">
<label for="hyphen-penalty">Hyphen penalty:</label>
<input type="number" id="hyphen-penalty-text">
<input type="range" id="hyphen-penalty" min="-2000" max="2000" value="100">
</div>
-->
</div>

<div>
Expand Down
49 changes: 35 additions & 14 deletions examples/wasm/www/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { draw_wrapped_text, WasmOptions } from "textwrap-wasm-demo";
import { draw_wrapped_text, WasmOptions, WasmOptimalFit } from "textwrap-wasm-demo";

fetch("build-info.json").then(response => response.json()).then(buildInfo => {
if (buildInfo.date && buildInfo.commit) {
Expand All @@ -25,24 +25,45 @@ function redraw(event) {
let breakWords = document.getElementById("break-words").checked;
let wordSeparator = document.getElementById("word-separator").value;
let wordSplitter = document.getElementById("word-splitter").value;
// TODO: The optimal-fit algorithm does not work well for
// proportional fonts, so we always use FirstFit. See
// https://github.com/mgeisler/textwrap/issues/326.
let wrapAlgorithm = "FirstFit"; // document.getElementById("wrap-algorithm").value;
let options = new WasmOptions(lineWidth, breakWords, wordSeparator, wordSplitter, wrapAlgorithm);
draw_wrapped_text(ctx, options, text);
let wrapAlgorithm = document.getElementById("wrap-algorithm").value;
let optimalFit = new WasmOptimalFit(document.getElementById("nline-penalty").valueAsNumber,
document.getElementById("overflow-penalty").valueAsNumber,
document.getElementById("short-line-fraction").valueAsNumber,
document.getElementById("short-last-line-penalty").valueAsNumber,
document.getElementById("hyphen-penalty").valueAsNumber);
let options = new WasmOptions(lineWidth, breakWords, wordSeparator, wordSplitter, wrapAlgorithm, optimalFit);
draw_wrapped_text(ctx, options, text, optimalFit);
}

document.getElementById("line-width").addEventListener("input", (event) => {
let lineWidthText = document.getElementById("line-width-text");
lineWidthText.value = event.target.valueAsNumber;
document.getElementById("wrap-algorithm").addEventListener("input", (event) => {
let disableOptimalFitParams = (event.target.value == "FirstFit");
let rangeInputIds = ["nline-penalty",
"overflow-penalty",
"short-line-fraction",
"short-last-line-penalty",
"hyphen-penalty"];
rangeInputIds.forEach((rangeInputId) => {
let rangeInput = document.getElementById(rangeInputId);
let textInput = document.getElementById(`${rangeInputId}-text`);
rangeInput.disabled = disableOptimalFitParams;
textInput.disabled = disableOptimalFitParams;
});
});

document.getElementById("line-width-text").addEventListener("input", (event) => {
let lineWidth = document.getElementById("line-width");
lineWidth.value = event.target.valueAsNumber;
});

document.querySelectorAll("input[type=range]").forEach((rangeInput) => {
let textInput = document.getElementById(`${rangeInput.id}-text`);
textInput.min = rangeInput.min;
textInput.max = rangeInput.max;
textInput.value = rangeInput.value;

rangeInput.addEventListener("input", (event) => {
textInput.value = rangeInput.valueAsNumber;
});
textInput.addEventListener("input", (event) => {
rangeInput.value = textInput.valueAsNumber;
});
});

document.querySelectorAll("textarea, select, input").forEach((elem) => {
elem.addEventListener("input", redraw);
Expand Down
12 changes: 6 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,9 @@ impl<'a>
/// #[cfg(not(feature = "unicode-linebreak"))]
/// word_separator: textwrap::word_separators::AsciiSpace,
/// #[cfg(feature = "smawk")]
/// wrap_algorithm: textwrap::wrap_algorithms::OptimalFit,
/// wrap_algorithm: textwrap::wrap_algorithms::OptimalFit::new(),
/// #[cfg(not(feature = "smawk"))]
/// wrap_algorithm: textwrap::wrap_algorithms::FirstFit,
/// wrap_algorithm: textwrap::wrap_algorithms::FirstFit::new(),
/// word_splitter: textwrap::word_splitters::HyphenSplitter,
/// }
/// # ;
Expand Down Expand Up @@ -429,9 +429,9 @@ impl<'a, WordSplit> Options<'a, DefaultWrapAlgorithm!(), DefaultWordSeparator!()
/// #[cfg(not(feature = "unicode-linebreak"))]
/// word_separator: textwrap::word_separators::AsciiSpace,
/// #[cfg(feature = "smawk")]
/// wrap_algorithm: textwrap::wrap_algorithms::OptimalFit,
/// wrap_algorithm: textwrap::wrap_algorithms::OptimalFit::new(),
/// #[cfg(not(feature = "smawk"))]
/// wrap_algorithm: textwrap::wrap_algorithms::FirstFit,
/// wrap_algorithm: textwrap::wrap_algorithms::FirstFit::new(),
/// word_splitter: word_splitter,
/// }
/// # ;
Expand Down Expand Up @@ -492,7 +492,7 @@ impl<'a, WordSplit> Options<'a, DefaultWrapAlgorithm!(), DefaultWordSeparator!()
subsequent_indent: "",
break_words: true,
word_separator: DefaultWordSeparator!(),
wrap_algorithm: DefaultWrapAlgorithm!(),
wrap_algorithm: <DefaultWrapAlgorithm!()>::new(),
word_splitter: word_splitter,
}
}
Expand Down Expand Up @@ -993,7 +993,7 @@ where
/// # use textwrap::wrap_algorithms::OptimalFit;
/// #
/// # let lines = wrap("To be, or not to be: that is the question",
/// # Options::new(10).wrap_algorithm(OptimalFit));
/// # Options::new(10).wrap_algorithm(OptimalFit::new()));
/// # assert_eq!(lines.join("\n") + "\n", "\
/// To be,
/// or not to
Expand Down
23 changes: 19 additions & 4 deletions src/wrap_algorithms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,22 @@ impl WrapAlgorithm for Box<dyn WrapAlgorithm> {
/// 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)]
#[derive(Clone, Copy, Debug)]
pub struct FirstFit;

impl FirstFit {
/// Create a new empty struct.
pub const fn new() -> Self {
FirstFit
}
}

impl Default for FirstFit {
fn default() -> Self {
Self::new()
}
}

impl WrapAlgorithm for FirstFit {
#[inline]
fn wrap<'a, 'b>(&self, words: &'b [Word<'a>], line_widths: &'b [usize]) -> Vec<&'b [Word<'a>]> {
Expand Down Expand Up @@ -107,7 +120,7 @@ impl WrapAlgorithm for FirstFit {
///
/// ```
/// use textwrap::core::Word;
/// use textwrap::wrap_algorithms;
/// use textwrap::wrap_algorithms::wrap_first_fit;
/// use textwrap::word_separators::{AsciiSpace, WordSeparator};
///
/// // Helper to convert wrapped lines to a Vec<String>.
Expand All @@ -119,7 +132,7 @@ impl WrapAlgorithm for FirstFit {
///
/// 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])),
/// assert_eq!(lines_to_strings(wrap_first_fit(&words, &[15])),
/// vec!["These few words",
/// "will", // <-- short line
/// "unfortunately",
Expand All @@ -128,7 +141,9 @@ impl WrapAlgorithm for FirstFit {
///
/// // 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])),
/// use textwrap::wrap_algorithms::{wrap_optimal_fit, OptimalFit};
/// #[cfg(feature = "smawk")]
/// assert_eq!(lines_to_strings(wrap_optimal_fit(&words, &[15], &OptimalFit::new())),
/// vec!["These few",
/// "words will",
/// "unfortunately",
Expand Down
Loading

0 comments on commit 607704a

Please sign in to comment.