Skip to content

Commit

Permalink
Add option for setting break_words in Wasm demo
Browse files Browse the repository at this point in the history
This adds a checkbox to control breaking long words that don’t fit on
a single line. The words are broken grapheme cluster boundaries, which
ensures that combining diacritical marks will be kept together with
their base character.
  • Loading branch information
mgeisler committed May 29, 2021
1 parent 8cceba7 commit 5310e86
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 2 deletions.
7 changes: 7 additions & 0 deletions examples/wasm/Cargo.lock

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

1 change: 1 addition & 0 deletions examples/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
95 changes: 94 additions & 1 deletion examples/wasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use unicode_segmentation::UnicodeSegmentation;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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];
Expand Down
5 changes: 5 additions & 0 deletions examples/wasm/www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
4 changes: 3 additions & 1 deletion examples/wasm/www/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 5310e86

Please sign in to comment.