Repository: mgeisler/textwrap (language: rust)

Issue:
Add checkbox for toggling `break_words`.



Discussion / hints:

Fix patch:
diff --git a/examples/wasm/Cargo.lock b/examples/wasm/Cargo.lock
index 852a1c80..d5761ae6 100644
--- a/examples/wasm/Cargo.lock
+++ b/examples/wasm/Cargo.lock
@@ -143,6 +143,7 @@ dependencies = [
  "console_error_panic_hook",
  "js-sys",
  "textwrap",
+ "unicode-segmentation",
  "wasm-bindgen",
  "wasm-bindgen-test",
  "web-sys",
@@ -157,6 +158,12 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "unicode-segmentation"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
+
 [[package]]
 name = "unicode-width"
 version = "0.1.8"
diff --git a/examples/wasm/Cargo.toml b/examples/wasm/Cargo.toml
index 8bf3da6c..c8116804 100644
--- a/examples/wasm/Cargo.toml
+++ b/examples/wasm/Cargo.toml
@@ -18,6 +18,7 @@ console_error_panic_hook = "0.1"
 js-sys = "0.3"
 wasm-bindgen = "0.2"
 web-sys = { version = "0.3", features = ["CanvasRenderingContext2d", "TextMetrics"] }
+unicode-segmentation = "1.7"
 
 [dev-dependencies]
 wasm-bindgen-test = "0.3"
diff --git a/examples/wasm/src/lib.rs b/examples/wasm/src/lib.rs
index 2b8f6740..92c94ba8 100644
--- a/examples/wasm/src/lib.rs
+++ b/examples/wasm/src/lib.rs
@@ -1,3 +1,4 @@
+use unicode_segmentation::UnicodeSegmentation;
 use wasm_bindgen::prelude::*;
 use wasm_bindgen::JsCast;
 
@@ -59,6 +60,88 @@ impl<'a> CanvasWord<'a> {
             penalty_width: canvas_width(ctx, word.penalty),
         }
     }
+
+    fn break_apart(
+        self,
+        ctx: &'_ web_sys::CanvasRenderingContext2d,
+        max_width: f64,
+    ) -> Vec<CanvasWord<'a>> {
+        if self.width <= max_width {
+            return vec![self];
+        }
+
+        let mut start = 0;
+        let mut words = Vec::new();
+        for (idx, grapheme) in self.word.grapheme_indices(true) {
+            let with_grapheme = &self.word[start..idx + grapheme.len()];
+            let without_grapheme = &self.word[start..idx];
+            if idx > 0 && canvas_width(&ctx, with_grapheme) > max_width {
+                // The part without the grapheme fits on the line. We
+                // give it a width of max_width instead of its natural
+                // width to ensure that it takes up the full line.
+                //
+                // Otherwise, we can end up with a situation where a
+                // text fits in _fewer_ lines when the line width is
+                // _smaller_. This happens with proportional fonts,
+                // such as the sans-serif or serif fonts. An example
+                // text which illustrates the problem is:
+                //
+                //   i XYZ
+                //
+                // Line width: 42px. Normal break, XYZ doesn't fit on
+                // first line:
+                //
+                //   i
+                //   XYZ
+                //
+                // Line width: 41px. XYZ takes up 41.1px, so it is
+                // broken apart. The first part now fits on the first
+                // line:
+                //
+                //   i XY
+                //   Z
+                //
+                // Line width: 39px. There is no longer room for XY on
+                // the first line:
+                //
+                //   i
+                //   XY
+                //   Z
+                //
+                // Line width: 28px. XY takes up 28.9px, so it is
+                // broken apart. YZ takes up 26.7px, so everything
+                // suddenly fits on two lines again:
+                //
+                //   i X
+                //   YZ
+                //
+                // We can be a more "natural" or "monotone" behavior
+                // by making the parts take up at least the full line
+                // width.
+                let natural_width = canvas_width(ctx, &without_grapheme);
+                words.push(CanvasWord {
+                    word: &without_grapheme,
+                    width: max_width.max(natural_width),
+                    whitespace: "",
+                    whitespace_width: 0.0,
+                    penalty: "",
+                    penalty_width: 0.0,
+                });
+                start = idx;
+            }
+        }
+
+        words.push(CanvasWord {
+            word: &self.word[start..],
+            width: canvas_width(ctx, &self.word[start..]),
+            whitespace: self.whitespace,
+            whitespace_width: self.whitespace_width,
+            penalty: self.penalty,
+            penalty_width: self.penalty_width,
+        });
+
+        words
+    }
 }
 
 const PRECISION: usize = 10;
@@ -167,6 +250,7 @@ pub enum WasmWrapAlgorithm {
 #[derive(Copy, Clone, Debug)]
 pub struct WasmOptions {
     pub width: usize,
+    pub break_words: bool,
     pub word_separator: WasmWordSeparator,
     pub word_splitter: WasmWordSplitter,
     pub wrap_algorithm: WasmWrapAlgorithm,
@@ -177,12 +261,14 @@ impl WasmOptions {
     #[wasm_bindgen(constructor)]
     pub fn new(
         width: usize,
+        break_words: bool,
         word_separator: WasmWordSeparator,
         word_splitter: WasmWordSplitter,
         wrap_algorithm: WasmWrapAlgorithm,
     ) -> WasmOptions {
         WasmOptions {
             width,
+            break_words,
             word_separator,
             word_splitter,
             wrap_algorithm,
@@ -224,7 +310,14 @@ pub fn draw_wrapped_text(
         let split_words = core::split_words(words, &word_splitter);
 
         let canvas_words = split_words
-            .map(|word| CanvasWord::from(ctx, word))
+            .flat_map(|word| {
+                let canvas_word = CanvasWord::from(ctx, word);
+                if options.break_words {
+                    canvas_word.break_apart(ctx, options.width as f64)
+                } else {
+                    vec![canvas_word]
+                }
+            })
             .collect::<Vec<_>>();
 
         let line_lengths = [options.width * PRECISION];
diff --git a/examples/wasm/www/index.html b/examples/wasm/www/index.html
index 0e652652..214d997a 100644
--- a/examples/wasm/www/index.html
+++ b/examples/wasm/www/index.html
@@ -59,6 +59,11 @@ <h1>Textwrap WebAssembly Demo</h1>
         <input type="number" id="line-width-text" min="0"> px.
       </div>
 
+      <div class="option">
+        <label for="break-words">Break long words:</label>
+        <input id="break-words" type="checkbox" checked>
+      </div>
+
       <div class="option">
         <label for="word-separator">Word separator:</label>
         <select id="word-separator">
diff --git a/examples/wasm/www/index.js b/examples/wasm/www/index.js
index 5a9e7657..fb5983c1 100644
--- a/examples/wasm/www/index.js
+++ b/examples/wasm/www/index.js
@@ -21,15 +21,17 @@ function redraw(event) {
 
     let text = document.getElementById("text").value;
     let lineWidth = document.getElementById("line-width").valueAsNumber;
+    let breakWords = document.getElementById("break-words").checked;
     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);
+    let options = new WasmOptions(lineWidth, breakWords, wordSeparator, wordSplitter, wrapAlgorithm);
     draw_wrapped_text(ctx, options, text);
 }
 
 document.getElementById("text").addEventListener("input", redraw);
 document.getElementById("font-family").addEventListener("input", redraw);
+document.getElementById("break-words").addEventListener("input", redraw);
 document.getElementById("word-separator").addEventListener("input", redraw);
 document.getElementById("word-splitter").addEventListener("input", redraw);
 document.getElementById("wrap-algorithm").addEventListener("input", redraw);


Test patch: