diff --git a/src/widget/text/mod.rs b/src/widget/text/mod.rs index 48b1284..793c252 100644 --- a/src/widget/text/mod.rs +++ b/src/widget/text/mod.rs @@ -212,7 +212,7 @@ impl Text if self.wrap { - let lines = wrap_lines( &self.content, size, max_width, canvas, font.as_ref() ); + let lines = wrap_lines( &self.content, size, max_width - WRAP_PAD, canvas, font.as_ref() ); let h = line_h * lines.len().max( 1 ) as f32; ( max_width, h ) } @@ -236,12 +236,12 @@ impl Text if self.wrap { - let lines = wrap_lines( &self.content, size, rect.width, canvas, font.as_ref() ); + let lines = wrap_lines( &self.content, size, rect.width - WRAP_PAD, canvas, font.as_ref() ); for ( i, line ) in lines.iter().enumerate() { let line_w = self.measure( line, canvas, font.as_ref() ); let slack = ( rect.width - line_w ).max( 0.0 ); - let pad = 4.0_f32.min( slack ); + let pad = WRAP_PAD.min( slack ); let tx = match self.align { TextAlign::Left => rect.x + pad, @@ -297,10 +297,12 @@ impl Text } } +const WRAP_PAD: f32 = 4.0; + /// Greedy word-wrap: split `text` on whitespace and pack words into /// lines whose total width stays under `max_width`. A word longer -/// than `max_width` overflows on its own line rather than being -/// hyphenated. Routes through the font override when one is set so +/// than `max_width` is broken between characters and continues on +/// the next line. Routes through the font override when one is set so /// measurement and rendering agree on advance widths. fn wrap_lines( text: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc> ) -> Vec { @@ -323,23 +325,34 @@ fn wrap_lines( text: &str, size: f32, max_width: f32, canvas: &Canvas, font: Opt for word in text.split_whitespace() { let word_w = measure( word ); - if current.is_empty() - { - current.push_str( word ); - current_w = word_w; - } - else if current_w + space_w + word_w <= max_width + if !current.is_empty() && current_w + space_w + word_w <= max_width { current.push( ' ' ); current.push_str( word ); current_w += space_w + word_w; + continue; } - else + if !current.is_empty() { lines.push( std::mem::take( &mut current ) ); + } + if word_w <= max_width + { current.push_str( word ); current_w = word_w; + continue; } + let mut start = 0; + for ( i, ch ) in word.char_indices().skip( 1 ) + { + if measure( &word[ start..i + ch.len_utf8() ] ) > max_width + { + lines.push( word[ start..i ].to_string() ); + start = i; + } + } + current.push_str( &word[ start.. ] ); + current_w = measure( ¤t ); } if !current.is_empty() { lines.push( current ); } if lines.is_empty() { lines.push( String::new() ); } diff --git a/src/widget/text/tests.rs b/src/widget/text/tests.rs index d22ed45..ebb6ac7 100644 --- a/src/widget/text/tests.rs +++ b/src/widget/text/tests.rs @@ -117,6 +117,21 @@ fn line_height_multiplies_wrapped_text_height() assert!( ( h_doubled - 2.0 * h_base ).abs() < 1e-3, "h_base={h_base} h_doubled={h_doubled}" ); } +#[ test ] +fn wrap_breaks_words_wider_than_the_line() +{ + let canvas = make_canvas(); + let word = "supercalifragilisticexpialidocious"; + let lines = wrap_lines( &format!( "a {word} b" ), 24.0, 120.0, &canvas, None ); + assert!( lines.len() > 3, "lines={lines:?}" ); + assert_eq!( lines[ 0 ], "a" ); + assert_eq!( lines.concat().replace( ' ', "" ), format!( "a{word}b" ) ); + for line in &lines + { + assert!( canvas.measure_text( line, 24.0 ) <= 120.0, "line {line:?} overflows" ); + } +} + #[ test ] fn text_align_enum_implements_partial_eq() {