Repository: mgeisler/textwrap (language: rust)

Issue:
Add drop-down for selecting `splitter`



Discussion / hints:

Fix patch:
diff --git a/examples/wasm/src/lib.rs b/examples/wasm/src/lib.rs
index af8d6a07..2b8f6740 100644
--- a/examples/wasm/src/lib.rs
+++ b/examples/wasm/src/lib.rs
@@ -1,7 +1,8 @@
 use wasm_bindgen::prelude::*;
 use wasm_bindgen::JsCast;
 
-use textwrap::{core, wrap_algorithms, WordSeparator};
+use textwrap::core;
+use textwrap::wrap_algorithms::{wrap_first_fit, wrap_optimal_fit};
 
 #[wasm_bindgen]
 extern "C" {
@@ -141,11 +142,59 @@ fn draw_word(
     Ok(())
 }
 
+#[wasm_bindgen]
+#[derive(Copy, Clone, Debug)]
+pub enum WasmWordSeparator {
+    AsciiSpace = "AsciiSpace",
+    UnicodeBreakProperties = "UnicodeBreakProperties",
+}
+
+#[wasm_bindgen]
+#[derive(Copy, Clone, Debug)]
+pub enum WasmWordSplitter {
+    NoHyphenation = "NoHyphenation",
+    HyphenSplitter = "HyphenSplitter",
+}
+
+#[wasm_bindgen]
+#[derive(Copy, Clone, Debug)]
+pub enum WasmWrapAlgorithm {
+    FirstFit = "FirstFit",
+    OptimalFit = "OptimalFit",
+}
+
+#[wasm_bindgen]
+#[derive(Copy, Clone, Debug)]
+pub struct WasmOptions {
+    pub width: usize,
+    pub word_separator: WasmWordSeparator,
+    pub word_splitter: WasmWordSplitter,
+    pub wrap_algorithm: WasmWrapAlgorithm,
+}
+
+#[wasm_bindgen]
+impl WasmOptions {
+    #[wasm_bindgen(constructor)]
+    pub fn new(
+        width: usize,
+        word_separator: WasmWordSeparator,
+        word_splitter: WasmWordSplitter,
+        wrap_algorithm: WasmWrapAlgorithm,
+    ) -> WasmOptions {
+        WasmOptions {
+            width,
+            word_separator,
+            word_splitter,
+            wrap_algorithm,
+        }
+    }
+}
+
 #[wasm_bindgen]
 pub fn draw_wrapped_text(
     ctx: &web_sys::CanvasRenderingContext2d,
+    options: &WasmOptions,
     text: &str,
-    width: usize,
 ) -> Result<(), JsValue> {
     console_error_panic_hook::set_once();
 
@@ -156,19 +205,34 @@ pub fn draw_wrapped_text(
     // tall character when supported by Firefox.
     let line_height = metrics.actual_bounding_box_ascent() + metrics.actual_bounding_box_descent();
     let baseline_distance = 1.5 * line_height;
-    let options = textwrap::Options::from(width);
+
+    let word_separator: Box<dyn textwrap::WordSeparator> = match options.word_separator {
+        WasmWordSeparator::AsciiSpace => Box::new(textwrap::AsciiSpace),
+        WasmWordSeparator::UnicodeBreakProperties => Box::new(textwrap::UnicodeBreakProperties),
+        _ => Err("WasmOptions has an invalid word_separator field")?,
+    };
+
+    let word_splitter: Box<dyn textwrap::WordSplitter> = match options.word_splitter {
+        WasmWordSplitter::NoHyphenation => Box::new(textwrap::NoHyphenation),
+        WasmWordSplitter::HyphenSplitter => Box::new(textwrap::HyphenSplitter),
+        _ => Err("WasmOptions has an invalid word_splitter field")?,
+    };
 
     let mut lineno = 0;
     for line in text.split('\n') {
-        let words = options.word_separator.find_words(line);
-        let split_words = core::split_words(words, &options.splitter);
+        let words = word_separator.find_words(line);
+        let split_words = core::split_words(words, &word_splitter);
 
         let canvas_words = split_words
             .map(|word| CanvasWord::from(ctx, word))
             .collect::<Vec<_>>();
 
-        let line_lengths = [width * PRECISION];
-        let wrapped_words = wrap_algorithms::wrap_first_fit(&canvas_words, &line_lengths);
+        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),
+            _ => Err("WasmOptions has an invalid wrap_algorithm field")?,
+        };
 
         for words_in_line in wrapped_words {
             lineno += 1;
@@ -190,7 +254,7 @@ pub fn draw_wrapped_text(
             ctx.set_font("10px sans-serif");
             ctx.fill_text(
                 &format!("{:.1}px", x - X_OFFSET),
-                1.5 * X_OFFSET + width as f64,
+                1.5 * X_OFFSET + options.width as f64,
                 y,
             )?;
             ctx.restore();
@@ -201,7 +265,7 @@ pub fn draw_wrapped_text(
         ctx,
         "blue",
         (
-            X_OFFSET + width as f64,
+            X_OFFSET + options.width as f64,
             metrics.actual_bounding_box_ascent(),
         ),
         &[(0.0, baseline_distance * lineno as f64)],
diff --git a/examples/wasm/www/index.html b/examples/wasm/www/index.html
index 2ee3f152..0e652652 100644
--- a/examples/wasm/www/index.html
+++ b/examples/wasm/www/index.html
@@ -30,6 +30,11 @@
        bottom: 0;
        border-top: thin solid grey;
      }
+
+     div.option {
+       display: inline-block;
+       padding-right: 1em;
+     }
     </style>
   </head>
   <body>
@@ -39,14 +44,49 @@ <h1>Textwrap WebAssembly Demo</h1>
 
 You can test the word-wrapping behavior by editing the text 😍✨</textarea>
 
-    <p>Font family:
-      <select id="font-family">
-        <option value="monospace">Monospace</option>
-        <option value="serif">Serif</option>
-        <option value="sans-serif">Sans-serif</option>
-      </select>.
-      Line width: <input type="number" id="line-width-text" min="0"> px.
-    </p>
+    <div>
+      <div class="option">
+        <label for="font-family">Font family:</label>
+        <select id="font-family">
+          <option value="monospace">Monospace</option>
+          <option value="serif">Serif</option>
+          <option value="sans-serif">Sans-serif</option>
+        </select>
+      </div>
+
+      <div class="option">
+        <label for="line-width-text">Line width:</label>
+        <input type="number" id="line-width-text" min="0"> px.
+      </div>
+
+      <div class="option">
+        <label for="word-separator">Word separator:</label>
+        <select id="word-separator">
+          <option value="UnicodeBreakProperties">Unicode breaks</option>
+          <option value="AsciiSpace">ASCII space</option>
+        </select>
+      </div>
+
+      <div class="option">
+        <label for="word-splitter">Word splitter:</label>
+        <select id="word-splitter">
+          <option value="HyphenSplitter">Hyphens</option>
+          <option value="NoHyphenation">No hyphenation</option>
+        </select>
+      </div>
+
+      <div class="option">
+        <label for="wrap-algorithm">Wrap algorithm:</label>
+        <select id="wrap-algorithm">
+          <!--
+               The optimal-fit algorithm does not work well for proportional
+               fonts, see https://github.com/mgeisler/textwrap/issues/326.
+               <option value="OptimalFit">Optimal-fit</option>
+          -->
+          <option value="FirstFit">First-fit</option>
+        </select>
+      </div>
+    </div>
 
     <div>
       <input type="range" id="line-width" min="0" max="300" value="250">
diff --git a/examples/wasm/www/index.js b/examples/wasm/www/index.js
index a01e9783..5a9e7657 100644
--- a/examples/wasm/www/index.js
+++ b/examples/wasm/www/index.js
@@ -1,4 +1,4 @@
-import { draw_wrapped_text } from "textwrap-wasm-demo";
+import { draw_wrapped_text, WasmOptions } from "textwrap-wasm-demo";
 
 fetch("build-info.json").then(response => response.json()).then(buildInfo => {
     if (buildInfo.date && buildInfo.commit) {
@@ -20,12 +20,19 @@ function redraw(event) {
     ctx.font = `20px ${fontFamily}`;
 
     let text = document.getElementById("text").value;
-    let lineWidth = document.getElementById("line-width");
-    draw_wrapped_text(ctx, text, lineWidth.valueAsNumber);
+    let lineWidth = document.getElementById("line-width").valueAsNumber;
+    let wordSeparator = document.getElementById("word-separator").value;
+    let wordSplitter = document.getElementById("word-splitter").value;
+    let wrapAlgorithm = document.getElementById("wrap-algorithm").value;
+    let options = new WasmOptions(lineWidth, wordSeparator, wordSplitter, wrapAlgorithm);
+    draw_wrapped_text(ctx, options, text);
 }
 
 document.getElementById("text").addEventListener("input", redraw);
 document.getElementById("font-family").addEventListener("input", redraw);
+document.getElementById("word-separator").addEventListener("input", redraw);
+document.getElementById("word-splitter").addEventListener("input", redraw);
+document.getElementById("wrap-algorithm").addEventListener("input", redraw);
 
 document.getElementById("line-width").addEventListener("input", (event) => {
     let lineWidthText = document.getElementById("line-width-text");


Test patch: