Skip to content

Commit

Permalink
wallet/text: wrap very long strings that cross line_width by forceful…
Browse files Browse the repository at this point in the history
…ly splitting them up into multiple words each less than line_width long.
  • Loading branch information
darkfi committed Jan 4, 2025
1 parent 0f1eac3 commit 6034184
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 13 deletions.
1 change: 1 addition & 0 deletions bin/darkwallet/src/text/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ pub const EMOJI_PROP_ABOVE_BASELINE: f32 = 0.75;
// * Glyph cache. Key is (glyph_id, font_size)
// * Glyph texture cache: (glyph_id, font_size, color)

#[derive(Clone)]
pub struct GlyphPositionIter<'a> {
font_size: f32,
window_scale: f32,
Expand Down
123 changes: 114 additions & 9 deletions bin/darkwallet/src/text/wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,21 +99,17 @@ fn tokenize(font_size: f32, window_scale: f32, glyphs: &Vec<Glyph>) -> Vec<Token

/// Given a series of words, apply wrapping.
/// Whitespace is completely perserved.
fn apply_wrap(line_width: f32, tokens: Vec<Token>) -> Vec<Vec<Glyph>> {
fn apply_wrap(line_width: f32, mut tokens: Vec<Token>) -> Vec<Vec<Glyph>> {
//debug!(target: "text::wrap", "apply_wrap({line_width}, {tokens:?})");

let mut lines = vec![];
let mut line = vec![];
let mut start = 0.;

for mut token in tokens {
let mut tokens_iter = tokens.iter_mut().peekable();
while let Some(token) = tokens_iter.next() {
assert!(token.token_type != TokenType::Null);

// Triggered by if below
if start < 0. {
start = token.lhs;
}

// Does this token cross over the end of the line?
if token.rhs > start + line_width {
// Whitespace tokens that cause wrapping are prepended to the current line before
Expand All @@ -125,12 +121,17 @@ fn apply_wrap(line_width: f32, tokens: Vec<Token>) -> Vec<Vec<Glyph>> {
// Start a new line
let line = std::mem::take(&mut line);
//debug!(target: "text::apply_wrap", "adding line: {}", glyph_str(&line));
lines.push(line);
// This can happen if this token is very long and crosses the line boundary
if !line.is_empty() {
lines.push(line);
}

// Move to the next token if this is whitespace
if token.token_type == TokenType::Whitespace {
// Load LHS from next token in loop
start = -1.;
if let Some(next_token) = tokens_iter.peek() {
start = next_token.lhs;
}
} else {
start = token.lhs;
}
Expand All @@ -149,6 +150,80 @@ fn apply_wrap(line_width: f32, tokens: Vec<Token>) -> Vec<Vec<Glyph>> {
lines
}

/// Splits any Word token that exceeds the line width.
/// So Word("aaaaaaaaaaaaaaa") => [Word("aaaaaaaa"), Word("aaaaaaa")].
pub fn restrict_word_len(
font_size: f32,
window_scale: f32,
raw_tokens: Vec<Token>,
line_width: f32,
) -> Vec<Token> {
let mut tokens = vec![];
for token in raw_tokens {
match token.token_type {
TokenType::Word => {
assert!(!token.glyphs.is_empty());
let token_width = token.rhs - token.lhs;
// No change required. This is the usual code path
if token_width < line_width {
tokens.push(token);
continue
}
}
_ => {
tokens.push(token);
continue
}
}

// OK we have encountered a Word that is very long. Lets split it up
// into multiple Words each under line_width.

let glyphs2 = token.glyphs.clone();
let glyph_pos_iter = GlyphPositionIter::new(font_size, window_scale, &glyphs2, 0.);
let mut token_glyphs = vec![];
let mut lhs = -1.;
let mut rhs = 0.;

// Just loop through each glyph. When the running total exceeds line_width
// then push the buffer, and start again.
// Very basic stuff.
for (pos, glyph) in glyph_pos_iter.zip(token.glyphs.into_iter()) {
if lhs < 0. {
lhs = pos.x;
}
rhs = pos.x + pos.w;

token_glyphs.push(glyph);

let curr_width = rhs - lhs;
// Line width exceeded. Do our thing.
if curr_width > line_width {
let token = Token {
token_type: TokenType::Word,
lhs,
rhs,
glyphs: std::mem::take(&mut token_glyphs),
};
tokens.push(token);
lhs = -1.;
}
}

// Take care of any remainders left over.
if !token_glyphs.is_empty() {
let token = Token {
token_type: TokenType::Word,
lhs,
rhs,
glyphs: std::mem::take(&mut token_glyphs),
};
tokens.push(token);
}
}
tokens
}

pub fn wrap(
line_width: f32,
font_size: f32,
Expand All @@ -160,6 +235,8 @@ pub fn wrap(
//debug!(target: "text::wrap", "tokenized words {:?}",
// words.iter().map(|w| w.as_str()).collect::<Vec<_>>());

let tokens = restrict_word_len(font_size, window_scale, tokens, line_width);

let lines = apply_wrap(line_width, tokens);

//if lines.len() > 1 {
Expand All @@ -171,3 +248,31 @@ pub fn wrap(

lines
}

#[cfg(test)]
mod tests {
use super::{super::*, *};

#[test]
fn wrap_simple() {
let shaper = TextShaper::new();
let glyphs = shaper.shape("hello world 123".to_string(), 32., 1.);

let wrapped = wrap(200., 32., 1., &glyphs);
assert_eq!(wrapped.len(), 3);
assert_eq!(glyph_str(&wrapped[0]), "hello ");
assert_eq!(glyph_str(&wrapped[1]), "world ");
assert_eq!(glyph_str(&wrapped[2]), "123");
}

#[test]
fn wrap_long() {
let shaper = TextShaper::new();
let glyphs = shaper.shape("aaaaaaaaaaaaaaa".to_string(), 32., 1.);

let wrapped = wrap(200., 32., 1., &glyphs);
assert_eq!(wrapped.len(), 2);
assert_eq!(glyph_str(&wrapped[0]), "aaaaaaaa");
assert_eq!(glyph_str(&wrapped[1]), "aaaaaaa");
}
}
28 changes: 24 additions & 4 deletions bin/darkwallet/src/ui/chatedit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ impl TextWrap {
after_text += &glyph.substr;
}
}
self.editable.end_compose();
self.editable.set_text(before_text, after_text);
self.clear_cache();
}
Expand Down Expand Up @@ -1024,6 +1025,7 @@ impl ChatEdit {
}

async fn insert_char(&self, key: char) {
debug!(target: "ui::chatedit", "insert_char({key})");
{
let mut text_wrap = &mut self.text_wrap.lock();
text_wrap.clear_cache();
Expand All @@ -1039,24 +1041,43 @@ impl ChatEdit {
self.redraw().await;
}

async fn handle_shortcut(&self, key: char, mods: &KeyMods) {
async fn handle_shortcut(&self, key: char, mods: &KeyMods) -> bool {
debug!(target: "ui::chatedit", "handle_shortcut({:?}, {:?})", key, mods);

match key {
'a' => {
if mods.ctrl {
{
let mut text_wrap = self.text_wrap.lock();
let rendered = text_wrap.get_render();
let end_pos = rendered.glyphs.len();

let select = &mut text_wrap.select;
select.clear();
select.push(Selection::new(0, end_pos));
}

self.redraw().await;
return true
}
}
'c' => {
if mods.ctrl {
self.copy_highlighted().unwrap();
return true
}
}
'v' => {
if mods.ctrl {
if let Some(text) = window::clipboard_get() {
self.paste_text(text).await;
}
return true
}
}
_ => {}
}
false
}

async fn handle_key(&self, key: &KeyCode, mods: &KeyMods) {
Expand Down Expand Up @@ -1731,8 +1752,7 @@ impl UIObject for ChatEdit {
if repeat {
return false
}
self.handle_shortcut(key, &mods).await;
return true
return self.handle_shortcut(key, &mods).await
}

let actions = {
Expand Down Expand Up @@ -1788,7 +1808,7 @@ impl UIObject for ChatEdit {
if !rect.contains(mouse_pos) {
if self.is_focused.get() {
debug!(target: "ui::chatedit", "EditBox unfocused");
self.is_focused.set(false);
//self.is_focused.set(false);
self.text_wrap.lock().select.clear();

self.redraw().await;
Expand Down

0 comments on commit 6034184

Please sign in to comment.