diff --git a/examples/text_wrap.rs b/examples/text_wrap.rs new file mode 100644 index 00000000..ebff5332 --- /dev/null +++ b/examples/text_wrap.rs @@ -0,0 +1,38 @@ +use macroquad::prelude::*; + +static LOREM: &str = "Lorem ipsum odor amet, consectetuer adipiscing elit. Ultrices nostra volutpat facilisis magna mus. Rhoncus tempor feugiat netus maecenas pretium leo vitae. Eros aliquet maecenas eu diam aliquet varius hac elementum. Sociosqu platea per ultricies vitae praesent mauris nostra ridiculus. Est cursus pulvinar efficitur mus vel leo. Integer et nec eleifend non leo. Lorem rutrum ultrices potenti facilisis hendrerit facilisi metus sit. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + +Intentional newlines +are preserved."; + +#[macroquad::main("Text Wrap")] +async fn main() { + let font_size = 24; + loop { + clear_background(BLACK); + + let maximum_line_length = f32::max(20.0, mouse_position().0 - 20.0); + let text = wrap_text(LOREM, None, font_size, 1.0, maximum_line_length); + let dimensions = measure_multiline_text(&text, None, font_size, 1.0, Some(1.0)); + + draw_multiline_text( + &text, + 20.0, + 20.0 + dimensions.offset_y, + font_size as f32, + Some(1.0), + WHITE, + ); + draw_rectangle_lines(20.0, 20.0, dimensions.width, dimensions.height, 2.0, BLUE); + draw_line( + 20.0 + maximum_line_length, + 0.0, + 20.0 + maximum_line_length, + screen_height(), + 1.0, + RED, + ); + + next_frame().await + } +} diff --git a/src/text.rs b/src/text.rs index 0107c468..4ce79616 100644 --- a/src/text.rs +++ b/src/text.rs @@ -158,6 +158,7 @@ impl Font { font_size: u16, font_scale_x: f32, font_scale_y: f32, + mut glyph_callback: impl FnMut(f32), ) -> TextDimensions { let dpi_scaling = miniquad::window::dpi_scale(); let font_size = (font_size as f32 * dpi_scaling).ceil() as u16; @@ -176,7 +177,9 @@ impl Font { let atlas = self.atlas.lock().unwrap(); let glyph = atlas.get(font_data.sprite).unwrap().rect; - width += font_data.advance * font_scale_x; + let advance = font_data.advance * font_scale_x; + glyph_callback(advance); + width += advance; min_y = min_y.min(offset_y); max_y = max_y.max(glyph.h * font_scale_y + offset_y); } @@ -395,7 +398,7 @@ pub fn draw_multiline_text( font_size: f32, line_distance_factor: Option, color: Color, -) { +) -> TextDimensions { draw_multiline_text_ex( text, x, @@ -407,7 +410,7 @@ pub fn draw_multiline_text( color, ..Default::default() }, - ); + ) } /// Draw multiline text with the given line distance and custom params such as font, font size and font scale. @@ -418,7 +421,7 @@ pub fn draw_multiline_text_ex( mut y: f32, line_distance_factor: Option, params: TextParams, -) { +) -> TextDimensions { let line_distance = match line_distance_factor { Some(distance) => distance, None => { @@ -432,10 +435,25 @@ pub fn draw_multiline_text_ex( } }; + let mut dimensions = TextDimensions::default(); + let y_step = line_distance * params.font_size as f32 * params.font_scale; + for line in text.lines() { - draw_text_ex(line, x, y, params.clone()); - y += line_distance * params.font_size as f32 * params.font_scale; + // Trailing whitespace has a size, but isn't shown in any way. + let line = line.trim_end(); + + let line_dimensions = draw_text_ex(line, x, y, params.clone()); + + y += y_step; + + dimensions.width = f32::max(dimensions.width, line_dimensions.width); + dimensions.height += y_step; + if dimensions.offset_y == 0.0 { + dimensions.offset_y = line_dimensions.offset_y; + } } + + dimensions } /// Get the text center. @@ -462,7 +480,112 @@ pub fn measure_text( ) -> TextDimensions { let font = font.unwrap_or_else(|| &get_context().fonts_storage.default_font); - font.measure_text(text, font_size, font_scale, font_scale) + font.measure_text(text, font_size, font_scale, font_scale, |_| {}) +} + +pub fn measure_multiline_text( + text: &str, + font: Option<&Font>, + font_size: u16, + font_scale: f32, + line_distance_factor: Option, +) -> TextDimensions { + let font = font.unwrap_or_else(|| &get_context().fonts_storage.default_font); + let line_distance = match line_distance_factor { + Some(distance) => distance, + None => match font.font.horizontal_line_metrics(1.0) { + Some(metrics) => metrics.new_line_size, + None => 1.0, + }, + }; + + let mut dimensions = TextDimensions::default(); + let y_step = line_distance * font_size as f32 * font_scale; + + for line in text.lines() { + // Trailing whitespace has a size, but isn't shown in any way. + let line = line.trim_end(); + + let line_dimensions = font.measure_text(line, font_size, font_scale, font_scale, |_| {}); + + dimensions.width = f32::max(dimensions.width, line_dimensions.width); + dimensions.height += y_step; + if dimensions.offset_y == 0.0 { + dimensions.offset_y = line_dimensions.offset_y; + } + } + + dimensions +} + +/// Converts word breaks to newlines wherever the text would otherwise exceed the given length. +pub fn wrap_text( + text: &str, + font: Option<&Font>, + font_size: u16, + font_scale: f32, + maximum_line_length: f32, +) -> String { + let font = font.unwrap_or_else(|| &get_context().fonts_storage.default_font); + + // This is always a bit too much memory, but it saves a lot of reallocations. + let mut new_text = + String::with_capacity(text.len() + text.chars().filter(|c| c.is_whitespace()).count()); + + let mut current_word_start = 0usize; + let mut current_word_end = 0usize; + let mut characters = text.char_indices(); + let mut total_width = 0.0; + let mut word_width = 0.0; + + font.measure_text(text, font_size, font_scale, font_scale, |mut width| { + // It's impossible this is called more often than the text has characters. + let (idx, c) = characters.next().unwrap(); + let mut keep_char = true; + + if c.is_whitespace() { + new_text.push_str(&text[current_word_start..idx + c.len_utf8()]); + current_word_start = idx + c.len_utf8(); + word_width = 0.0; + keep_char = false; + + // If we would wrap, ignore the whitespace. + if total_width + width > maximum_line_length { + width = 0.0; + } + } + + // If a single word expands past the length limit, just break it up. + if word_width + width > maximum_line_length { + new_text.push_str(&text[current_word_start..current_word_end]); + new_text.push('\n'); + current_word_start = current_word_end; + total_width = 0.0; + word_width = 0.0; + } + + current_word_end = idx + c.len_utf8(); + if keep_char { + word_width += width; + } + + if c == '\n' { + total_width = 0.0; + word_width = 0.0; + return; + } + + total_width += width; + + if total_width > maximum_line_length { + new_text.push('\n'); + total_width = word_width; + } + }); + + new_text.push_str(&text[current_word_start..current_word_end]); + + new_text } pub(crate) struct FontsStorage { diff --git a/src/ui/render/painter.rs b/src/ui/render/painter.rs index 8037dff9..8bd2255a 100644 --- a/src/ui/render/painter.rs +++ b/src/ui/render/painter.rs @@ -311,7 +311,7 @@ impl Painter { font: &mut Font, font_size: u16, ) -> TextDimensions { - font.measure_text(label, font_size, 1.0, 1.0) + font.measure_text(label, font_size, 1.0, 1.0, |_| {}) } /// If character is in font atlas - will return x advance from position to potential next character position