From 622b848c008f292ebea2411e870072106c7b72c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 28 Nov 2024 16:47:05 +0100 Subject: [PATCH 01/22] Fix typo --- crates/epaint/src/text/fonts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 4be2fdfe8d0..6e19a7f34fb 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -733,7 +733,7 @@ impl GalleyCache { // Say the user asks to wrap at width 200.0. // The text layout wraps, and reports that the final width was 196.0 points. - // This than trickles up the `Ui` chain and gets stored as the width for a tooltip (say). + // This then trickles up the `Ui` chain and gets stored as the width for a tooltip (say). // On the next frame, this is then set as the max width for the tooltip, // and we end up calling the text layout code again, this time with a wrap width of 196.0. // Except, somewhere in the `Ui` chain with added margins etc, a rounding error was introduced, From 150c0f662b36cab7bd5d5a294911e6233150b45d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 28 Nov 2024 16:47:40 +0100 Subject: [PATCH 02/22] Cache individual lines of text in GalleyCache --- crates/epaint/src/text/fonts.rs | 134 ++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 7 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 6e19a7f34fb..c981279dd93 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -762,13 +762,133 @@ impl GalleyCache { cached.galley.clone() } std::collections::hash_map::Entry::Vacant(entry) => { - let galley = super::layout(fonts, job.into()); - let galley = Arc::new(galley); - entry.insert(CachedGalley { - last_used: self.generation, - galley: galley.clone(), - }); - galley + if job.break_on_newline { + let mut current_section = 0; + let mut current = 0; + let mut left_max_rows = job.wrap.max_rows; + let mut galleys = Vec::new(); + let mut text_left = job.text.as_str(); + loop { + let end = text_left + .find('\n') + .map(|i| i + current) + .unwrap_or(job.text.len()); + + let mut line_job = LayoutJob::default(); + line_job.text = job.text[current..end].to_string(); + line_job.wrap = crate::text::TextWrapping { + max_rows: left_max_rows, + ..job.wrap + }; + line_job.halign = job.halign; + line_job.justify = job.justify; + + let line_start = current; + while current < end { + let mut s = &job.sections[current_section]; + while s.byte_range.end <= current { + current_section += 1; + s = &job.sections[current_section]; + } + + assert!(s.byte_range.contains(¤t)); + let section_end = s.byte_range.end.min(end); + line_job.sections.push(crate::text::LayoutSection { + leading_space: s.leading_space, + byte_range: current - line_start..section_end - line_start, + format: s.format.clone(), + }); + current = section_end; + } + + // Prevent an infinite recursion + line_job.break_on_newline = false; + + let galley = self.layout(fonts, line_job); + // This will prevent us from invalidating cache entries unnecessarily + if left_max_rows != usize::MAX { + left_max_rows -= galley.rows.len(); + } + galleys.push(galley); + + current = end + 1; + if current >= job.text.len() { + break; + } else { + text_left = &job.text[current..]; + } + } + + let mut merged_galley = Galley { + job: Arc::new(job), + rows: Vec::new(), + elided: false, + rect: emath::Rect::ZERO, + mesh_bounds: emath::Rect::ZERO, + num_vertices: 0, + num_indices: 0, + pixels_per_point: fonts.pixels_per_point, + }; + + for galley in galleys { + let current_offset = emath::vec2(0.0, merged_galley.rect.height()); + merged_galley.rows.extend(galley.rows.iter().map(|row| { + super::Row { + // FIXME: what is this??? + section_index_at_start: row.section_index_at_start, + glyphs: row + .glyphs + .iter() + .cloned() + .map(|mut p| { + p.pos.y += current_offset.y; + p + }) + .collect(), + rect: row.rect.translate(current_offset), + visuals: { + let mut visuals = row.visuals.clone(); + for vertex in visuals.mesh.vertices.iter_mut() { + vertex.pos.y += current_offset.y; + } + visuals.mesh_bounds = + visuals.mesh_bounds.translate(current_offset); + merged_galley.mesh_bounds = + merged_galley.mesh_bounds.union(visuals.mesh_bounds); + visuals + }, + ends_with_newline: row.ends_with_newline, + } + })); + merged_galley.rect = merged_galley + .rect + .union(galley.rect.translate(current_offset)); + merged_galley.num_vertices += galley.num_vertices; + merged_galley.num_indices += galley.num_indices; + if galley.elided { + merged_galley.elided = true; + break; + } + } + + let galley = Arc::new(merged_galley); + self.cache.insert( + hash, + CachedGalley { + last_used: self.generation, + galley: galley.clone(), + }, + ); + galley + } else { + let galley = super::layout(fonts, job.into()); + let galley = Arc::new(galley); + entry.insert(CachedGalley { + last_used: self.generation, + galley: galley.clone(), + }); + galley + } } } } From db32a1ed4474d4bf36d06878cca771ab08b385e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 28 Nov 2024 17:40:59 +0100 Subject: [PATCH 03/22] Make Galleys share Rows and store their offsets --- .../egui/src/text_selection/accesskit_text.rs | 4 +- .../egui/src/text_selection/cursor_range.rs | 2 +- .../text_selection/label_text_selection.rs | 7 +- crates/egui/src/text_selection/visuals.rs | 4 +- crates/egui/src/widget_text.rs | 2 +- crates/egui/src/widgets/label.rs | 13 +- crates/epaint/src/shape.rs | 4 +- crates/epaint/src/shape_transform.rs | 3 +- crates/epaint/src/stats.rs | 10 +- crates/epaint/src/tessellator.rs | 8 +- crates/epaint/src/text/fonts.rs | 36 +---- crates/epaint/src/text/text_layout.rs | 142 +++++++++++------- crates/epaint/src/text/text_layout_types.rs | 66 ++++---- 13 files changed, 174 insertions(+), 127 deletions(-) diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index d0c3869038d..ad11c16a4d9 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -39,11 +39,11 @@ pub fn update_accesskit_for_text_widget( }; ctx.with_accessibility_parent(parent_id, || { - for (row_index, row) in galley.rows.iter().enumerate() { + for (row_index, (row, offset)) in galley.rows.iter().enumerate() { let row_id = parent_id.with(row_index); ctx.accesskit_node_builder(row_id, |builder| { builder.set_role(accesskit::Role::TextRun); - let rect = row.rect.translate(galley_pos.to_vec2()); + let rect = row.rect.translate(offset.to_vec2() + galley_pos.to_vec2()); builder.set_bounds(accesskit::Rect { x0: rect.min.x.into(), y0: rect.min.y.into(), diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs index bd3f496fd8e..dbdba5ba8eb 100644 --- a/crates/egui/src/text_selection/cursor_range.rs +++ b/crates/egui/src/text_selection/cursor_range.rs @@ -284,7 +284,7 @@ fn ccursor_from_accesskit_text_position( position: &accesskit::TextPosition, ) -> Option { let mut total_length = 0usize; - for (i, row) in galley.rows.iter().enumerate() { + for (i, (row, _)) in galley.rows.iter().enumerate() { let row_id = id.with(i); if row_id.accesskit_id() == position.node { return Some(CCursor { diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index fe5eac00e78..321d8d94609 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -179,7 +179,10 @@ impl LabelSelectionState { if let epaint::Shape::Text(text_shape) = &mut shape.shape { let galley = Arc::make_mut(&mut text_shape.galley); for row_selection in row_selections { - if let Some(row) = galley.rows.get_mut(row_selection.row) { + if let Some((row, _)) = + galley.rows.get_mut(row_selection.row) + { + let row = Arc::make_mut(row); for vertex_index in row_selection.vertex_indices { if let Some(vertex) = row .visuals @@ -659,7 +662,7 @@ fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String { } fn estimate_row_height(galley: &Galley) -> f32 { - if let Some(row) = galley.rows.first() { + if let Some((row, _)) = galley.rows.first() { row.rect.height() } else { galley.size().y diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index d86f9dc56c2..0000632e77c 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -31,7 +31,9 @@ pub fn paint_text_selection( let max = max.rcursor; for ri in min.row..=max.row { - let row = &mut galley.rows[ri]; + let (row, _) = &mut galley.rows[ri]; + let row = Arc::make_mut(row); + let left = if ri == min.row { row.x_offset(min.column) } else { diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 011a4adcbb0..bde229828be 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -640,7 +640,7 @@ impl WidgetText { Self::RichText(text) => text.font_height(fonts, style), Self::LayoutJob(job) => job.font_height(fonts), Self::Galley(galley) => { - if let Some(row) = galley.rows.first() { + if let Some((row, _)) = galley.rows.first() { row.height() } else { galley.size().y diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index b6ade45ae30..276c9576b61 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::{ - epaint, pos2, text_selection, vec2, Align, Direction, FontSelection, Galley, Pos2, Response, - Sense, Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType, + epaint, pos2, text_selection, Align, Direction, FontSelection, Galley, Pos2, Response, Sense, + Stroke, TextWrapMode, Ui, Widget, WidgetInfo, WidgetText, WidgetType, }; use self::text_selection::LabelSelectionState; @@ -194,10 +194,13 @@ impl Label { let pos = pos2(ui.max_rect().left(), ui.cursor().top()); assert!(!galley.rows.is_empty(), "Galleys are never empty"); // collect a response from many rows: - let rect = galley.rows[0].rect.translate(vec2(pos.x, pos.y)); + let rect = galley.rows[0] + .0 + .rect + .translate(galley.rows[0].1.to_vec2() + pos.to_vec2()); let mut response = ui.allocate_rect(rect, sense); - for row in galley.rows.iter().skip(1) { - let rect = row.rect.translate(vec2(pos.x, pos.y)); + for (row, offset) in galley.rows.iter().skip(1) { + let rect = row.rect.translate(offset.to_vec2() + pos.to_vec2()); response |= ui.allocate_rect(rect, sense); } (pos, galley, response) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 6f67a2bc6a6..0d2d77d47ad 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -433,7 +433,9 @@ impl Shape { // Scale text: let galley = Arc::make_mut(&mut text_shape.galley); - for row in &mut galley.rows { + for (row, offset) in &mut galley.rows { + let row = Arc::make_mut(row); + *offset = *offset * transform.scaling; row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds; for v in &mut row.visuals.mesh.vertices { v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index f072393f557..4d74c938513 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -88,7 +88,8 @@ pub fn adjust_colors( if !galley.is_empty() { let galley = std::sync::Arc::make_mut(galley); - for row in &mut galley.rows { + for (row, _) in &mut galley.rows { + let row = Arc::make_mut(row); for vertex in &mut row.visuals.mesh.vertices { adjust_color(&mut vertex.color); } diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index 68bba622ed2..ad3705bea4b 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -88,7 +88,11 @@ impl AllocInfo { pub fn from_galley(galley: &Galley) -> Self { Self::from_slice(galley.text().as_bytes()) + Self::from_slice(&galley.rows) - + galley.rows.iter().map(Self::from_galley_row).sum() + + galley + .rows + .iter() + .map(|(row, _)| Self::from_galley_row(row)) + .sum() } fn from_galley_row(row: &crate::text::Row) -> Self { @@ -213,8 +217,8 @@ impl PaintStats { self.shape_text += AllocInfo::from_galley(&text_shape.galley); for row in &text_shape.galley.rows { - self.text_shape_indices += AllocInfo::from_slice(&row.visuals.mesh.indices); - self.text_shape_vertices += AllocInfo::from_slice(&row.visuals.mesh.vertices); + self.text_shape_indices += AllocInfo::from_slice(&row.0.visuals.mesh.indices); + self.text_shape_vertices += AllocInfo::from_slice(&row.0.visuals.mesh.vertices); } } Shape::Mesh(mesh) => { diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index fdbe270914e..393edfbbf67 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1778,16 +1778,18 @@ impl Tessellator { let rotator = Rot2::from_angle(*angle); - for row in &galley.rows { + for (row, row_pos) in &galley.rows { if row.visuals.mesh.is_empty() { continue; } + let final_pos = galley_pos + row_pos.to_vec2(); + let mut row_rect = row.visuals.mesh_bounds; if *angle != 0.0 { row_rect = row_rect.rotate_bb(rotator); } - row_rect = row_rect.translate(galley_pos.to_vec2()); + row_rect = row_rect.translate(final_pos.to_vec2()); if self.options.coarse_tessellation_culling && !self.clip_rect.intersects(row_rect) { // culling individual lines of text is important, since a single `Shape::Text` @@ -1836,7 +1838,7 @@ impl Tessellator { }; Vertex { - pos: galley_pos + offset, + pos: final_pos + offset, uv: (uv.to_vec2() * uv_normalizer).to_pos2(), color, } diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index c981279dd93..eccd2fd06b8 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -832,34 +832,14 @@ impl GalleyCache { for galley in galleys { let current_offset = emath::vec2(0.0, merged_galley.rect.height()); - merged_galley.rows.extend(galley.rows.iter().map(|row| { - super::Row { - // FIXME: what is this??? - section_index_at_start: row.section_index_at_start, - glyphs: row - .glyphs - .iter() - .cloned() - .map(|mut p| { - p.pos.y += current_offset.y; - p - }) - .collect(), - rect: row.rect.translate(current_offset), - visuals: { - let mut visuals = row.visuals.clone(); - for vertex in visuals.mesh.vertices.iter_mut() { - vertex.pos.y += current_offset.y; - } - visuals.mesh_bounds = - visuals.mesh_bounds.translate(current_offset); - merged_galley.mesh_bounds = - merged_galley.mesh_bounds.union(visuals.mesh_bounds); - visuals - }, - ends_with_newline: row.ends_with_newline, - } - })); + merged_galley + .rows + .extend(galley.rows.iter().map(|(row, prev_offset)| { + merged_galley.mesh_bounds = + merged_galley.mesh_bounds.union(row.visuals.mesh_bounds); + + (row.clone(), *prev_offset + current_offset) + })); merged_galley.rect = merged_galley .rect .union(galley.rect.translate(current_offset)); diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 9db77f888af..ed7347418e0 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -96,7 +96,8 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let mut elided = false; let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided); if elided { - if let Some(last_row) = rows.last_mut() { + if let Some((last_row, _)) = rows.last_mut() { + let last_row = Arc::get_mut(last_row).unwrap(); replace_last_glyph_with_overflow_character(fonts, &job, last_row); if let Some(last) = last_row.glyphs.last() { last_row.rect.max.x = last.max_x(); @@ -108,12 +109,12 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { if justify || job.halign != Align::LEFT { let num_rows = rows.len(); - for (i, row) in rows.iter_mut().enumerate() { + for (i, (row, _)) in rows.iter_mut().enumerate() { let is_last_row = i + 1 == num_rows; let justify_row = justify && !row.ends_with_newline && !is_last_row; halign_and_justify_row( point_scale, - row, + Arc::get_mut(row).unwrap(), job.halign, job.wrap.max_width, justify_row, @@ -198,7 +199,7 @@ fn rows_from_paragraphs( paragraphs: Vec, job: &LayoutJob, elided: &mut bool, -) -> Vec { +) -> Vec<(Arc, Pos2)> { let num_paragraphs = paragraphs.len(); let mut rows = vec![]; @@ -212,31 +213,38 @@ fn rows_from_paragraphs( let is_last_paragraph = (i + 1) == num_paragraphs; if paragraph.glyphs.is_empty() { - rows.push(Row { - section_index_at_start: paragraph.section_index_at_start, - glyphs: vec![], - visuals: Default::default(), - rect: Rect::from_min_size( - pos2(paragraph.cursor_x, 0.0), - vec2(0.0, paragraph.empty_paragraph_height), - ), - ends_with_newline: !is_last_paragraph, - }); + rows.push(( + Arc::new(Row { + section_index_at_start: paragraph.section_index_at_start, + glyphs: vec![], + visuals: Default::default(), + rect: Rect::from_min_size( + pos2(paragraph.cursor_x, 0.0), + vec2(0.0, paragraph.empty_paragraph_height), + ), + ends_with_newline: !is_last_paragraph, + }), + Pos2::ZERO, + )); } else { let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x(); if paragraph_max_x <= job.effective_wrap_width() { // Early-out optimization: the whole paragraph fits on one row. let paragraph_min_x = paragraph.glyphs[0].pos.x; - rows.push(Row { - section_index_at_start: paragraph.section_index_at_start, - glyphs: paragraph.glyphs, - visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: !is_last_paragraph, - }); + rows.push(( + Arc::new(Row { + section_index_at_start: paragraph.section_index_at_start, + glyphs: paragraph.glyphs, + visuals: Default::default(), + rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), + ends_with_newline: !is_last_paragraph, + }), + Pos2::ZERO, + )); } else { line_break(¶graph, job, &mut rows, elided); - rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph; + let last_row = Arc::get_mut(&mut rows.last_mut().unwrap().0).unwrap(); + last_row.ends_with_newline = !is_last_paragraph; } } } @@ -244,7 +252,12 @@ fn rows_from_paragraphs( rows } -fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, elided: &mut bool) { +fn line_break( + paragraph: &Paragraph, + job: &LayoutJob, + out_rows: &mut Vec<(Arc, Pos2)>, + elided: &mut bool, +) { let wrap_width = job.effective_wrap_width(); // Keeps track of good places to insert row break if we exceed `wrap_width`. @@ -270,13 +283,16 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, e { // Allow the first row to be completely empty, because we know there will be more space on the next row: // TODO(emilk): this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height. - out_rows.push(Row { - section_index_at_start: paragraph.section_index_at_start, - glyphs: vec![], - visuals: Default::default(), - rect: rect_from_x_range(first_row_indentation..=first_row_indentation), - ends_with_newline: false, - }); + out_rows.push(( + Arc::new(Row { + section_index_at_start: paragraph.section_index_at_start, + glyphs: vec![], + visuals: Default::default(), + rect: rect_from_x_range(first_row_indentation..=first_row_indentation), + ends_with_newline: false, + }), + Pos2::ZERO, + )); row_start_x += first_row_indentation; first_row_indentation = 0.0; } else if let Some(last_kept_index) = row_break_candidates.get(job.wrap.break_anywhere) @@ -294,13 +310,16 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, e let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - out_rows.push(Row { - section_index_at_start, - glyphs, - visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: false, - }); + out_rows.push(( + Arc::new(Row { + section_index_at_start, + glyphs, + visuals: Default::default(), + rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), + ends_with_newline: false, + }), + Pos2::ZERO, + )); // Start a new row: row_start_idx = last_kept_index + 1; @@ -333,13 +352,16 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, e let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - out_rows.push(Row { - section_index_at_start, - glyphs, - visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: false, - }); + out_rows.push(( + Arc::new(Row { + section_index_at_start, + glyphs, + visuals: Default::default(), + rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), + ends_with_newline: false, + }), + Pos2::ZERO, + )); } } } @@ -592,14 +614,15 @@ fn halign_and_justify_row( fn galley_from_rows( point_scale: PointScale, job: Arc, - mut rows: Vec, + mut rows: Vec<(Arc, Pos2)>, elided: bool, ) -> Galley { let mut first_row_min_height = job.first_row_min_height; let mut cursor_y = 0.0; let mut min_x: f32 = 0.0; let mut max_x: f32 = 0.0; - for row in &mut rows { + for (row, _) in &mut rows { + let row = Arc::get_mut(row).unwrap(); let mut max_row_height = first_row_min_height.max(row.rect.height()); first_row_min_height = 0.0; for glyph in &row.glyphs { @@ -639,7 +662,8 @@ fn galley_from_rows( let mut num_vertices = 0; let mut num_indices = 0; - for row in &mut rows { + for (row, _) in &mut rows { + let row = Arc::get_mut(row).unwrap(); row.visuals = tessellate_row(point_scale, &job, &format_summary, row); mesh_bounds = mesh_bounds.union(row.visuals.mesh_bounds); num_vertices += row.visuals.mesh.vertices.len(); @@ -1072,7 +1096,7 @@ mod tests { assert!(galley.elided); assert_eq!(galley.rows.len(), 1); - let row_text = galley.rows[0].text(); + let row_text = galley.rows[0].0.text(); assert!( row_text.ends_with('…'), "Expected row to end with `…`, got {row_text:?} when line-breaking the text {text:?} with max_width {max_width} and break_anywhere {break_anywhere}.", @@ -1091,7 +1115,7 @@ mod tests { assert!(galley.elided); assert_eq!(galley.rows.len(), 1); - let row_text = galley.rows[0].text(); + let row_text = galley.rows[0].0.text(); assert_eq!(row_text, "Hello…"); } } @@ -1106,7 +1130,11 @@ mod tests { layout_job.wrap.max_width = 90.0; let galley = layout(&mut fonts, layout_job.into()); assert_eq!( - galley.rows.iter().map(|row| row.text()).collect::>(), + galley + .rows + .iter() + .map(|row| row.0.text()) + .collect::>(), vec!["日本語と", "Englishの混在", "した文章"] ); } @@ -1121,7 +1149,11 @@ mod tests { layout_job.wrap.max_width = 110.0; let galley = layout(&mut fonts, layout_job.into()); assert_eq!( - galley.rows.iter().map(|row| row.text()).collect::>(), + galley + .rows + .iter() + .map(|row| row.0.text()) + .collect::>(), vec!["日本語とEnglish", "の混在した文章"] ); } @@ -1136,10 +1168,14 @@ mod tests { let galley = layout(&mut fonts, layout_job.into()); assert!(galley.elided); assert_eq!( - galley.rows.iter().map(|row| row.text()).collect::>(), + galley + .rows + .iter() + .map(|row| row.0.text()) + .collect::>(), vec!["# DNA…"] ); let row = &galley.rows[0]; - assert_eq!(row.rect.max.x, row.glyphs.last().unwrap().max_x()); + assert_eq!(row.0.rect.max.x, row.0.glyphs.last().unwrap().max_x()); } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 17826e6afb1..31a44c5dd3a 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -499,14 +499,14 @@ pub struct Galley { /// Contains the original string and style sections. pub job: Arc, - /// Rows of text, from top to bottom. + /// Rows of text, from top to bottom, and their offsets. /// /// The number of characters in all rows sum up to `job.text.chars().count()` /// unless [`Self::elided`] is `true`. /// /// Note that a paragraph (a piece of text separated with `\n`) /// can be split up into multiple rows. - pub rows: Vec, + pub rows: Vec<(Arc, Pos2)>, /// Set to true the text was truncated due to [`TextWrapping::max_rows`]. pub elided: bool, @@ -755,7 +755,7 @@ impl std::ops::Deref for Galley { impl Galley { /// Zero-width rect past the last character. fn end_pos(&self) -> Rect { - if let Some(row) = self.rows.last() { + if let Some((row, _)) = self.rows.last() { let x = row.rect.right(); Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } else { @@ -773,7 +773,7 @@ impl Galley { pub fn pos_from_pcursor(&self, pcursor: PCursor) -> Rect { let mut it = PCursor::default(); - for row in &self.rows { + for (row, offset) in &self.rows { if it.paragraph == pcursor.paragraph { // Right paragraph, but is it the right row in the paragraph? @@ -787,8 +787,11 @@ impl Galley { && !row.ends_with_newline && column >= row.char_count_excluding_newline(); if !select_next_row_instead { - let x = row.x_offset(column); - return Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())); + let x = row.x_offset(column) + offset.x; + return Rect::from_min_max( + pos2(x, row.min_y() + offset.y), + pos2(x, row.max_y() + offset.y), + ); } } } @@ -822,13 +825,13 @@ impl Galley { /// same as a cursor at the end. /// This allows implementing text-selection by dragging above/below the galley. pub fn cursor_from_pos(&self, pos: Vec2) -> Cursor { - if let Some(first_row) = self.rows.first() { - if pos.y < first_row.min_y() { + if let Some((first_row, offset)) = self.rows.first() { + if pos.y < first_row.min_y() + offset.y { return self.begin(); } } - if let Some(last_row) = self.rows.last() { - if last_row.max_y() < pos.y { + if let Some((last_row, offset)) = self.rows.last() { + if last_row.max_y() + offset.y < pos.y { return self.end(); } } @@ -839,9 +842,12 @@ impl Galley { let mut ccursor_index = 0; let mut pcursor_it = PCursor::default(); - for (row_nr, row) in self.rows.iter().enumerate() { - let is_pos_within_row = row.min_y() <= pos.y && pos.y <= row.max_y(); - let y_dist = (row.min_y() - pos.y).abs().min((row.max_y() - pos.y).abs()); + for (row_nr, (row, offset)) in self.rows.iter().enumerate() { + let min_y = row.min_y() + offset.y; + let max_y = row.max_y() + offset.y; + + let is_pos_within_row = min_y <= pos.y && pos.y <= max_y; + let y_dist = (min_y - pos.y).abs().min((max_y - pos.y).abs()); if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; let column = row.char_at(pos.x); @@ -904,7 +910,7 @@ impl Galley { offset: 0, prefer_next_row: true, }; - for row in &self.rows { + for (row, _) in &self.rows { let row_char_count = row.char_count_including_newline(); ccursor.index += row_char_count; if row.ends_with_newline { @@ -922,7 +928,7 @@ impl Galley { } pub fn end_rcursor(&self) -> RCursor { - if let Some(last_row) = self.rows.last() { + if let Some((last_row, _)) = self.rows.last() { RCursor { row: self.rows.len() - 1, column: last_row.char_count_including_newline(), @@ -948,7 +954,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, row) in self.rows.iter().enumerate() { + for (row_nr, (row, _)) in self.rows.iter().enumerate() { let row_char_count = row.char_count_excluding_newline(); if ccursor_it.index <= ccursor.index @@ -993,7 +999,7 @@ impl Galley { } let prefer_next_row = - rcursor.column < self.rows[rcursor.row].char_count_excluding_newline(); + rcursor.column < self.rows[rcursor.row].0.char_count_excluding_newline(); let mut ccursor_it = CCursor { index: 0, prefer_next_row, @@ -1004,7 +1010,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, row) in self.rows.iter().enumerate() { + for (row_nr, (row, _)) in self.rows.iter().enumerate() { if row_nr == rcursor.row { ccursor_it.index += rcursor.column.at_most(row.char_count_excluding_newline()); @@ -1048,7 +1054,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, row) in self.rows.iter().enumerate() { + for (row_nr, (row, _)) in self.rows.iter().enumerate() { if pcursor_it.paragraph == pcursor.paragraph { // Right paragraph, but is it the right row in the paragraph? @@ -1122,7 +1128,9 @@ impl Galley { let new_row = cursor.rcursor.row - 1; let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); + >= self.rows[cursor.rcursor.row] + .0 + .char_count_excluding_newline(); let new_rcursor = if cursor_is_beyond_end_of_current_row { // keep same column @@ -1133,11 +1141,12 @@ impl Galley { } else { // keep same X coord let x = self.pos_from_cursor(cursor).center().x; - let column = if x > self.rows[new_row].rect.right() { + let (row, offset) = &self.rows[new_row]; + let column = if x > row.rect.right() + offset.x { // beyond the end of this row - keep same column cursor.rcursor.column } else { - self.rows[new_row].char_at(x) + row.char_at(x) }; RCursor { row: new_row, @@ -1153,7 +1162,9 @@ impl Galley { let new_row = cursor.rcursor.row + 1; let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); + >= self.rows[cursor.rcursor.row] + .0 + .char_count_excluding_newline(); let new_rcursor = if cursor_is_beyond_end_of_current_row { // keep same column @@ -1164,11 +1175,12 @@ impl Galley { } else { // keep same X coord let x = self.pos_from_cursor(cursor).center().x; - let column = if x > self.rows[new_row].rect.right() { + let (row, offset) = &self.rows[new_row]; + let column = if x > row.rect.right() + offset.x { // beyond the end of the next row - keep same column cursor.rcursor.column } else { - self.rows[new_row].char_at(x) + row.char_at(x) }; RCursor { row: new_row, @@ -1192,7 +1204,9 @@ impl Galley { pub fn cursor_end_of_row(&self, cursor: &Cursor) -> Cursor { self.from_rcursor(RCursor { row: cursor.rcursor.row, - column: self.rows[cursor.rcursor.row].char_count_excluding_newline(), + column: self.rows[cursor.rcursor.row] + .0 + .char_count_excluding_newline(), }) } } From 4e3f1628016a4ca9a84454643206e1137c810fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Fri, 29 Nov 2024 11:50:24 +0100 Subject: [PATCH 04/22] Fix lints --- crates/epaint/src/text/fonts.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index eccd2fd06b8..2d434ed707a 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -769,21 +769,20 @@ impl GalleyCache { let mut galleys = Vec::new(); let mut text_left = job.text.as_str(); loop { - let end = text_left - .find('\n') - .map(|i| i + current) - .unwrap_or(job.text.len()); - - let mut line_job = LayoutJob::default(); - line_job.text = job.text[current..end].to_string(); - line_job.wrap = crate::text::TextWrapping { - max_rows: left_max_rows, - ..job.wrap + let end = text_left.find('\n').map_or(job.text.len(), |i| i + current); + let start = current; + + let mut line_job = LayoutJob { + text: job.text[current..end].to_string(), + wrap: crate::text::TextWrapping { + max_rows: left_max_rows, + ..job.wrap + }, + halign: job.halign, + justify: job.justify, + ..Default::default() }; - line_job.halign = job.halign; - line_job.justify = job.justify; - let line_start = current; while current < end { let mut s = &job.sections[current_section]; while s.byte_range.end <= current { @@ -795,7 +794,7 @@ impl GalleyCache { let section_end = s.byte_range.end.min(end); line_job.sections.push(crate::text::LayoutSection { leading_space: s.leading_space, - byte_range: current - line_start..section_end - line_start, + byte_range: current - start..section_end - start, format: s.format.clone(), }); current = section_end; From 3de1723659b9433fd6937ca8a4308b13015e74ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Fri, 29 Nov 2024 11:55:15 +0100 Subject: [PATCH 05/22] Don't add leading space to more than one split layout section --- crates/epaint/src/text/fonts.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 2d434ed707a..b4566d14da8 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -793,7 +793,14 @@ impl GalleyCache { assert!(s.byte_range.contains(¤t)); let section_end = s.byte_range.end.min(end); line_job.sections.push(crate::text::LayoutSection { - leading_space: s.leading_space, + // Leading space should only be added to the first section + // if the there are multiple sections that will be created + // from splitting the current section. + leading_space: if current == s.byte_range.start { + s.leading_space + } else { + 0.0 + }, byte_range: current - start..section_end - start, format: s.format.clone(), }); From f028154da81d1080744b2c6c6b45aff990878538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Fri, 29 Nov 2024 13:01:15 +0100 Subject: [PATCH 06/22] Move cached-multiline-layout code into a helper function --- crates/epaint/src/text/fonts.rs | 195 ++++++++++++++++---------------- 1 file changed, 100 insertions(+), 95 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index b4566d14da8..4933d3d4a4e 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -727,6 +727,104 @@ struct GalleyCache { } impl GalleyCache { + fn layout_multiline(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Galley { + let mut current_section = 0; + let mut current = 0; + let mut left_max_rows = job.wrap.max_rows; + let mut galleys = Vec::new(); + let mut text_left = job.text.as_str(); + loop { + let end = text_left.find('\n').map_or(job.text.len(), |i| i + current); + let start = current; + + let mut line_job = LayoutJob { + text: job.text[current..end].to_string(), + wrap: crate::text::TextWrapping { + max_rows: left_max_rows, + ..job.wrap + }, + halign: job.halign, + justify: job.justify, + ..Default::default() + }; + + while current < end { + let mut s = &job.sections[current_section]; + while s.byte_range.end <= current { + current_section += 1; + s = &job.sections[current_section]; + } + + assert!(s.byte_range.contains(¤t)); + let section_end = s.byte_range.end.min(end); + line_job.sections.push(crate::text::LayoutSection { + // Leading space should only be added to the first section + // if the there are multiple sections that will be created + // from splitting the current section. + leading_space: if current == s.byte_range.start { + s.leading_space + } else { + 0.0 + }, + byte_range: current - start..section_end - start, + format: s.format.clone(), + }); + current = section_end; + } + + // Prevent an infinite recursion + line_job.break_on_newline = false; + + let galley = self.layout(fonts, line_job); + // This will prevent us from invalidating cache entries unnecessarily + if left_max_rows != usize::MAX { + left_max_rows -= galley.rows.len(); + } + galleys.push(galley); + + current = end + 1; + if current >= job.text.len() { + break; + } else { + text_left = &job.text[current..]; + } + } + + let mut merged_galley = Galley { + job: Arc::new(job), + rows: Vec::new(), + elided: false, + rect: emath::Rect::ZERO, + mesh_bounds: emath::Rect::ZERO, + num_vertices: 0, + num_indices: 0, + pixels_per_point: fonts.pixels_per_point, + }; + + for galley in galleys { + let current_offset = emath::vec2(0.0, merged_galley.rect.height()); + merged_galley + .rows + .extend(galley.rows.iter().map(|(row, prev_offset)| { + merged_galley.mesh_bounds = + merged_galley.mesh_bounds.union(row.visuals.mesh_bounds); + + (row.clone(), *prev_offset + current_offset) + })); + merged_galley.rect = merged_galley + .rect + .union(galley.rect.translate(current_offset)); + merged_galley.num_vertices += galley.num_vertices; + merged_galley.num_indices += galley.num_indices; + if galley.elided { + merged_galley.elided = true; + break; + } + } + + merged_galley + } + fn layout(&mut self, fonts: &mut FontsImpl, mut job: LayoutJob) -> Arc { if job.wrap.max_width.is_finite() { // Protect against rounding errors in egui layout code. @@ -763,101 +861,8 @@ impl GalleyCache { } std::collections::hash_map::Entry::Vacant(entry) => { if job.break_on_newline { - let mut current_section = 0; - let mut current = 0; - let mut left_max_rows = job.wrap.max_rows; - let mut galleys = Vec::new(); - let mut text_left = job.text.as_str(); - loop { - let end = text_left.find('\n').map_or(job.text.len(), |i| i + current); - let start = current; - - let mut line_job = LayoutJob { - text: job.text[current..end].to_string(), - wrap: crate::text::TextWrapping { - max_rows: left_max_rows, - ..job.wrap - }, - halign: job.halign, - justify: job.justify, - ..Default::default() - }; - - while current < end { - let mut s = &job.sections[current_section]; - while s.byte_range.end <= current { - current_section += 1; - s = &job.sections[current_section]; - } - - assert!(s.byte_range.contains(¤t)); - let section_end = s.byte_range.end.min(end); - line_job.sections.push(crate::text::LayoutSection { - // Leading space should only be added to the first section - // if the there are multiple sections that will be created - // from splitting the current section. - leading_space: if current == s.byte_range.start { - s.leading_space - } else { - 0.0 - }, - byte_range: current - start..section_end - start, - format: s.format.clone(), - }); - current = section_end; - } - - // Prevent an infinite recursion - line_job.break_on_newline = false; - - let galley = self.layout(fonts, line_job); - // This will prevent us from invalidating cache entries unnecessarily - if left_max_rows != usize::MAX { - left_max_rows -= galley.rows.len(); - } - galleys.push(galley); - - current = end + 1; - if current >= job.text.len() { - break; - } else { - text_left = &job.text[current..]; - } - } - - let mut merged_galley = Galley { - job: Arc::new(job), - rows: Vec::new(), - elided: false, - rect: emath::Rect::ZERO, - mesh_bounds: emath::Rect::ZERO, - num_vertices: 0, - num_indices: 0, - pixels_per_point: fonts.pixels_per_point, - }; - - for galley in galleys { - let current_offset = emath::vec2(0.0, merged_galley.rect.height()); - merged_galley - .rows - .extend(galley.rows.iter().map(|(row, prev_offset)| { - merged_galley.mesh_bounds = - merged_galley.mesh_bounds.union(row.visuals.mesh_bounds); - - (row.clone(), *prev_offset + current_offset) - })); - merged_galley.rect = merged_galley - .rect - .union(galley.rect.translate(current_offset)); - merged_galley.num_vertices += galley.num_vertices; - merged_galley.num_indices += galley.num_indices; - if galley.elided { - merged_galley.elided = true; - break; - } - } - - let galley = Arc::new(merged_galley); + let galley = self.layout_multiline(fonts, job); + let galley = Arc::new(galley); self.cache.insert( hash, CachedGalley { From bc86bec1cb69a99e0b2d49272608f298021824d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Fri, 29 Nov 2024 15:53:50 +0100 Subject: [PATCH 07/22] Properly handle row repositioning --- .../egui/src/text_selection/accesskit_text.rs | 8 +- .../egui/src/text_selection/cursor_range.rs | 2 +- .../text_selection/label_text_selection.rs | 8 +- crates/egui/src/text_selection/visuals.rs | 19 ++- crates/egui/src/widget_text.rs | 4 +- crates/egui/src/widgets/label.rs | 9 +- crates/epaint/src/shape.rs | 5 +- crates/epaint/src/shape_transform.rs | 4 +- crates/epaint/src/stats.rs | 12 +- crates/epaint/src/tessellator.rs | 4 +- crates/epaint/src/text/fonts.rs | 23 ++- crates/epaint/src/text/text_layout.rs | 147 ++++++++--------- crates/epaint/src/text/text_layout_types.rs | 155 ++++++++++-------- 13 files changed, 209 insertions(+), 191 deletions(-) diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index ad11c16a4d9..2197bb6b178 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -39,11 +39,11 @@ pub fn update_accesskit_for_text_widget( }; ctx.with_accessibility_parent(parent_id, || { - for (row_index, (row, offset)) in galley.rows.iter().enumerate() { + for (row_index, row) in galley.rows.iter().enumerate() { let row_id = parent_id.with(row_index); ctx.accesskit_node_builder(row_id, |builder| { builder.set_role(accesskit::Role::TextRun); - let rect = row.rect.translate(offset.to_vec2() + galley_pos.to_vec2()); + let rect = row.rect().translate(galley_pos.to_vec2()); builder.set_bounds(accesskit::Rect { x0: rect.min.x.into(), y0: rect.min.y.into(), @@ -74,14 +74,14 @@ pub fn update_accesskit_for_text_widget( let old_len = value.len(); value.push(glyph.chr); character_lengths.push((value.len() - old_len) as _); - character_positions.push(glyph.pos.x - row.rect.min.x); + character_positions.push(glyph.pos.x - row.pos.x); character_widths.push(glyph.advance_width); } if row.ends_with_newline { value.push('\n'); character_lengths.push(1); - character_positions.push(row.rect.max.x - row.rect.min.x); + character_positions.push(row.size.x); character_widths.push(0.0); } word_lengths.push((character_lengths.len() - last_word_start) as _); diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs index dbdba5ba8eb..bd3f496fd8e 100644 --- a/crates/egui/src/text_selection/cursor_range.rs +++ b/crates/egui/src/text_selection/cursor_range.rs @@ -284,7 +284,7 @@ fn ccursor_from_accesskit_text_position( position: &accesskit::TextPosition, ) -> Option { let mut total_length = 0usize; - for (i, (row, _)) in galley.rows.iter().enumerate() { + for (i, row) in galley.rows.iter().enumerate() { let row_id = id.with(i); if row_id.accesskit_id() == position.node { return Some(CCursor { diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index 321d8d94609..41210b3718c 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -179,10 +179,10 @@ impl LabelSelectionState { if let epaint::Shape::Text(text_shape) = &mut shape.shape { let galley = Arc::make_mut(&mut text_shape.galley); for row_selection in row_selections { - if let Some((row, _)) = + if let Some(placed_row) = galley.rows.get_mut(row_selection.row) { - let row = Arc::make_mut(row); + let row = Arc::make_mut(&mut placed_row.row); for vertex_index in row_selection.vertex_indices { if let Some(vertex) = row .visuals @@ -662,8 +662,8 @@ fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String { } fn estimate_row_height(galley: &Galley) -> f32 { - if let Some((row, _)) = galley.rows.first() { - row.rect.height() + if let Some(placed_row) = galley.rows.first() { + placed_row.height() } else { galley.size().y } diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index 0000632e77c..3eecfaf4bf3 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -31,26 +31,27 @@ pub fn paint_text_selection( let max = max.rcursor; for ri in min.row..=max.row { - let (row, _) = &mut galley.rows[ri]; - let row = Arc::make_mut(row); + let placed_row = &mut galley.rows[ri]; let left = if ri == min.row { - row.x_offset(min.column) + placed_row.x_offset(min.column) } else { - row.rect.left() + 0.0 }; let right = if ri == max.row { - row.x_offset(max.column) + placed_row.x_offset(max.column) } else { - let newline_size = if row.ends_with_newline { - row.height() / 2.0 // visualize that we select the newline + let newline_size = if placed_row.ends_with_newline { + placed_row.height() / 2.0 // visualize that we select the newline } else { 0.0 }; - row.rect.right() + newline_size + placed_row.size.x + newline_size }; - let rect = Rect::from_min_max(pos2(left, row.min_y()), pos2(right, row.max_y())); + let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, placed_row.size.y)); + + let row = Arc::make_mut(&mut placed_row.row); let mesh = &mut row.visuals.mesh; // Time to insert the selection rectangle into the row mesh. diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index bde229828be..2aae0320d96 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -640,8 +640,8 @@ impl WidgetText { Self::RichText(text) => text.font_height(fonts, style), Self::LayoutJob(job) => job.font_height(fonts), Self::Galley(galley) => { - if let Some((row, _)) = galley.rows.first() { - row.height() + if let Some(placed_row) = galley.rows.first() { + placed_row.height() } else { galley.size().y } diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 276c9576b61..d542940c631 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -194,13 +194,10 @@ impl Label { let pos = pos2(ui.max_rect().left(), ui.cursor().top()); assert!(!galley.rows.is_empty(), "Galleys are never empty"); // collect a response from many rows: - let rect = galley.rows[0] - .0 - .rect - .translate(galley.rows[0].1.to_vec2() + pos.to_vec2()); + let rect = galley.rows[0].rect().translate(pos.to_vec2()); let mut response = ui.allocate_rect(rect, sense); - for (row, offset) in galley.rows.iter().skip(1) { - let rect = row.rect.translate(offset.to_vec2() + pos.to_vec2()); + for placed_row in galley.rows.iter().skip(1) { + let rect = placed_row.rect().translate(pos.to_vec2()); response |= ui.allocate_rect(rect, sense); } (pos, galley, response) diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 0d2d77d47ad..4d2c48b2c0a 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -433,9 +433,8 @@ impl Shape { // Scale text: let galley = Arc::make_mut(&mut text_shape.galley); - for (row, offset) in &mut galley.rows { - let row = Arc::make_mut(row); - *offset = *offset * transform.scaling; + for placed_row in &mut galley.rows { + let row = Arc::make_mut(&mut placed_row.row); row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds; for v in &mut row.visuals.mesh.vertices { v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y); diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 4d74c938513..58281d09b8d 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -88,8 +88,8 @@ pub fn adjust_colors( if !galley.is_empty() { let galley = std::sync::Arc::make_mut(galley); - for (row, _) in &mut galley.rows { - let row = Arc::make_mut(row); + for placed_row in &mut galley.rows { + let row = Arc::make_mut(&mut placed_row.row); for vertex in &mut row.visuals.mesh.vertices { adjust_color(&mut vertex.color); } diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index ad3705bea4b..456dea85fcf 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -88,14 +88,10 @@ impl AllocInfo { pub fn from_galley(galley: &Galley) -> Self { Self::from_slice(galley.text().as_bytes()) + Self::from_slice(&galley.rows) - + galley - .rows - .iter() - .map(|(row, _)| Self::from_galley_row(row)) - .sum() + + galley.rows.iter().map(Self::from_galley_row).sum() } - fn from_galley_row(row: &crate::text::Row) -> Self { + fn from_galley_row(row: &crate::text::PlacedRow) -> Self { Self::from_mesh(&row.visuals.mesh) + Self::from_slice(&row.glyphs) } @@ -217,8 +213,8 @@ impl PaintStats { self.shape_text += AllocInfo::from_galley(&text_shape.galley); for row in &text_shape.galley.rows { - self.text_shape_indices += AllocInfo::from_slice(&row.0.visuals.mesh.indices); - self.text_shape_vertices += AllocInfo::from_slice(&row.0.visuals.mesh.vertices); + self.text_shape_indices += AllocInfo::from_slice(&row.visuals.mesh.indices); + self.text_shape_vertices += AllocInfo::from_slice(&row.visuals.mesh.vertices); } } Shape::Mesh(mesh) => { diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 393edfbbf67..a6290e9c636 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1778,12 +1778,12 @@ impl Tessellator { let rotator = Rot2::from_angle(*angle); - for (row, row_pos) in &galley.rows { + for row in &galley.rows { if row.visuals.mesh.is_empty() { continue; } - let final_pos = galley_pos + row_pos.to_vec2(); + let final_pos = galley_pos + row.pos.to_vec2(); let mut row_rect = row.visuals.mesh_bounds; if *angle != 0.0 { diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 4933d3d4a4e..234752e81f3 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -805,12 +805,21 @@ impl GalleyCache { let current_offset = emath::vec2(0.0, merged_galley.rect.height()); merged_galley .rows - .extend(galley.rows.iter().map(|(row, prev_offset)| { - merged_galley.mesh_bounds = - merged_galley.mesh_bounds.union(row.visuals.mesh_bounds); - - (row.clone(), *prev_offset + current_offset) + .extend(galley.rows.iter().map(|placed_row| { + let new_pos = placed_row.pos + current_offset; + merged_galley.mesh_bounds = merged_galley + .mesh_bounds + .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); + + super::PlacedRow { + row: placed_row.row.clone(), + pos: new_pos, + ends_with_newline: placed_row.ends_with_newline, + } })); + if let Some(last) = merged_galley.rows.last_mut() { + last.ends_with_newline = true; + } merged_galley.rect = merged_galley .rect .union(galley.rect.translate(current_offset)); @@ -822,6 +831,10 @@ impl GalleyCache { } } + if let Some(last) = merged_galley.rows.last_mut() { + last.ends_with_newline = false; + } + merged_galley } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index ed7347418e0..9203dfd0559 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -5,7 +5,7 @@ use emath::{pos2, vec2, Align, NumExt, Pos2, Rect, Vec2}; use crate::{stroke::PathStroke, text::font::Font, Color32, Mesh, Stroke, Vertex}; -use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals}; +use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, PlacedRow, Row, RowVisuals}; // ---------------------------------------------------------------------------- @@ -96,11 +96,11 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let mut elided = false; let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided); if elided { - if let Some((last_row, _)) = rows.last_mut() { - let last_row = Arc::get_mut(last_row).unwrap(); + if let Some(last_placed) = rows.last_mut() { + let last_row = Arc::get_mut(&mut last_placed.row).unwrap(); replace_last_glyph_with_overflow_character(fonts, &job, last_row); if let Some(last) = last_row.glyphs.last() { - last_row.rect.max.x = last.max_x(); + last_row.size.x = last.max_x(); } } } @@ -109,12 +109,13 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { if justify || job.halign != Align::LEFT { let num_rows = rows.len(); - for (i, (row, _)) in rows.iter_mut().enumerate() { + for (i, placed_row) in rows.iter_mut().enumerate() { let is_last_row = i + 1 == num_rows; - let justify_row = justify && !row.ends_with_newline && !is_last_row; + let justify_row = justify && !placed_row.ends_with_newline && !is_last_row; halign_and_justify_row( point_scale, - Arc::get_mut(row).unwrap(), + Arc::get_mut(&mut placed_row.row).unwrap(), + &mut placed_row.pos, job.halign, job.wrap.max_width, justify_row, @@ -199,7 +200,7 @@ fn rows_from_paragraphs( paragraphs: Vec, job: &LayoutJob, elided: &mut bool, -) -> Vec<(Arc, Pos2)> { +) -> Vec { let num_paragraphs = paragraphs.len(); let mut rows = vec![]; @@ -213,38 +214,35 @@ fn rows_from_paragraphs( let is_last_paragraph = (i + 1) == num_paragraphs; if paragraph.glyphs.is_empty() { - rows.push(( - Arc::new(Row { + rows.push(PlacedRow { + row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: vec![], visuals: Default::default(), - rect: Rect::from_min_size( - pos2(paragraph.cursor_x, 0.0), - vec2(0.0, paragraph.empty_paragraph_height), - ), - ends_with_newline: !is_last_paragraph, + size: vec2(0.0, paragraph.empty_paragraph_height), }), - Pos2::ZERO, - )); + pos: pos2(paragraph.cursor_x, 0.0), + ends_with_newline: !is_last_paragraph, + }); } else { let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x(); if paragraph_max_x <= job.effective_wrap_width() { // Early-out optimization: the whole paragraph fits on one row. let paragraph_min_x = paragraph.glyphs[0].pos.x; - rows.push(( - Arc::new(Row { + let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); + rows.push(PlacedRow { + row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: paragraph.glyphs, visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: !is_last_paragraph, + size: rect.size(), }), - Pos2::ZERO, - )); + pos: rect.min, + ends_with_newline: !is_last_paragraph, + }); } else { line_break(¶graph, job, &mut rows, elided); - let last_row = Arc::get_mut(&mut rows.last_mut().unwrap().0).unwrap(); - last_row.ends_with_newline = !is_last_paragraph; + rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph; } } } @@ -255,7 +253,7 @@ fn rows_from_paragraphs( fn line_break( paragraph: &Paragraph, job: &LayoutJob, - out_rows: &mut Vec<(Arc, Pos2)>, + out_rows: &mut Vec, elided: &mut bool, ) { let wrap_width = job.effective_wrap_width(); @@ -283,16 +281,17 @@ fn line_break( { // Allow the first row to be completely empty, because we know there will be more space on the next row: // TODO(emilk): this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height. - out_rows.push(( - Arc::new(Row { + let rect = rect_from_x_range(first_row_indentation..=first_row_indentation); + out_rows.push(PlacedRow { + row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: vec![], visuals: Default::default(), - rect: rect_from_x_range(first_row_indentation..=first_row_indentation), - ends_with_newline: false, + size: rect.size(), }), - Pos2::ZERO, - )); + pos: rect.min, + ends_with_newline: false, + }); row_start_x += first_row_indentation; first_row_indentation = 0.0; } else if let Some(last_kept_index) = row_break_candidates.get(job.wrap.break_anywhere) @@ -310,16 +309,17 @@ fn line_break( let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - out_rows.push(( - Arc::new(Row { + let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); + out_rows.push(PlacedRow { + row: Arc::new(Row { section_index_at_start, glyphs, visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: false, + size: rect.size(), }), - Pos2::ZERO, - )); + pos: rect.min, + ends_with_newline: false, + }); // Start a new row: row_start_idx = last_kept_index + 1; @@ -352,16 +352,17 @@ fn line_break( let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - out_rows.push(( - Arc::new(Row { + let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); + out_rows.push(PlacedRow { + row: Arc::new(Row { section_index_at_start, glyphs, visuals: Default::default(), - rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), - ends_with_newline: false, + size: rect.size(), }), - Pos2::ZERO, - )); + pos: rect.min, + ends_with_newline: false, + }); } } } @@ -523,6 +524,7 @@ fn replace_last_glyph_with_overflow_character( fn halign_and_justify_row( point_scale: PointScale, row: &mut Row, + pos: &mut Pos2, halign: Align, wrap_width: f32, justify: bool, @@ -606,24 +608,25 @@ fn halign_and_justify_row( } // Note we ignore the leading/trailing whitespace here! - row.rect.min.x = target_min_x; - row.rect.max.x = target_max_x; + pos.x = target_min_x; + row.size.x = target_max_x - target_min_x; } /// Calculate the Y positions and tessellate the text. fn galley_from_rows( point_scale: PointScale, job: Arc, - mut rows: Vec<(Arc, Pos2)>, + mut rows: Vec, elided: bool, ) -> Galley { let mut first_row_min_height = job.first_row_min_height; let mut cursor_y = 0.0; let mut min_x: f32 = 0.0; let mut max_x: f32 = 0.0; - for (row, _) in &mut rows { - let row = Arc::get_mut(row).unwrap(); - let mut max_row_height = first_row_min_height.max(row.rect.height()); + for placed_row in &mut rows { + let mut max_row_height = first_row_min_height.max(placed_row.rect().height()); + let row = Arc::get_mut(&mut placed_row.row).unwrap(); + first_row_min_height = 0.0; for glyph in &row.glyphs { max_row_height = max_row_height.max(glyph.line_height); @@ -634,8 +637,7 @@ fn galley_from_rows( for glyph in &mut row.glyphs { let format = &job.sections[glyph.section_index as usize].format; - glyph.pos.y = cursor_y - + glyph.font_impl_ascent + glyph.pos.y = glyph.font_impl_ascent // Apply valign to the different in height of the entire row, and the height of this `Font`: + format.valign.to_factor() * (max_row_height - glyph.line_height) @@ -644,14 +646,16 @@ fn galley_from_rows( // we always center the difference: + 0.5 * (glyph.font_height - glyph.font_impl_height); - glyph.pos.y = point_scale.round_to_pixel(glyph.pos.y); + // FIXME(afishhh): HACK! change the proper code above instead!! + // this should probably not be merged like this! + glyph.pos.x -= placed_row.pos.x; } - row.rect.min.y = cursor_y; - row.rect.max.y = cursor_y + max_row_height; + placed_row.pos.y = cursor_y; + row.size.y = max_row_height; - min_x = min_x.min(row.rect.min.x); - max_x = max_x.max(row.rect.max.x); + min_x = min_x.min(placed_row.rect().min.x); + max_x = max_x.max(placed_row.rect().max.x); cursor_y += max_row_height; cursor_y = point_scale.round_to_pixel(cursor_y); } @@ -662,8 +666,8 @@ fn galley_from_rows( let mut num_vertices = 0; let mut num_indices = 0; - for (row, _) in &mut rows { - let row = Arc::get_mut(row).unwrap(); + for placed_row in &mut rows { + let row = Arc::get_mut(&mut placed_row.row).unwrap(); row.visuals = tessellate_row(point_scale, &job, &format_summary, row); mesh_bounds = mesh_bounds.union(row.visuals.mesh_bounds); num_vertices += row.visuals.mesh.vertices.len(); @@ -1096,7 +1100,7 @@ mod tests { assert!(galley.elided); assert_eq!(galley.rows.len(), 1); - let row_text = galley.rows[0].0.text(); + let row_text = galley.rows[0].text(); assert!( row_text.ends_with('…'), "Expected row to end with `…`, got {row_text:?} when line-breaking the text {text:?} with max_width {max_width} and break_anywhere {break_anywhere}.", @@ -1115,7 +1119,7 @@ mod tests { assert!(galley.elided); assert_eq!(galley.rows.len(), 1); - let row_text = galley.rows[0].0.text(); + let row_text = galley.rows[0].text(); assert_eq!(row_text, "Hello…"); } } @@ -1130,11 +1134,7 @@ mod tests { layout_job.wrap.max_width = 90.0; let galley = layout(&mut fonts, layout_job.into()); assert_eq!( - galley - .rows - .iter() - .map(|row| row.0.text()) - .collect::>(), + galley.rows.iter().map(|row| row.text()).collect::>(), vec!["日本語と", "Englishの混在", "した文章"] ); } @@ -1149,11 +1149,7 @@ mod tests { layout_job.wrap.max_width = 110.0; let galley = layout(&mut fonts, layout_job.into()); assert_eq!( - galley - .rows - .iter() - .map(|row| row.0.text()) - .collect::>(), + galley.rows.iter().map(|row| row.text()).collect::>(), vec!["日本語とEnglish", "の混在した文章"] ); } @@ -1168,14 +1164,11 @@ mod tests { let galley = layout(&mut fonts, layout_job.into()); assert!(galley.elided); assert_eq!( - galley - .rows - .iter() - .map(|row| row.0.text()) - .collect::>(), + galley.rows.iter().map(|row| row.text()).collect::>(), vec!["# DNA…"] ); let row = &galley.rows[0]; - assert_eq!(row.0.rect.max.x, row.0.glyphs.last().unwrap().max_x()); + assert_eq!(row.pos, Pos2::ZERO); + assert_eq!(row.rect().max.x, row.glyphs.last().unwrap().max_x()); } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 31a44c5dd3a..0229665cbec 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -506,7 +506,7 @@ pub struct Galley { /// /// Note that a paragraph (a piece of text separated with `\n`) /// can be split up into multiple rows. - pub rows: Vec<(Arc, Pos2)>, + pub rows: Vec, /// Set to true the text was truncated due to [`TextWrapping::max_rows`]. pub elided: bool, @@ -538,6 +538,39 @@ pub struct Galley { pub pixels_per_point: f32, } +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct PlacedRow { + /// The underlying row unpositioned [`Row`]. + pub row: Arc, + + /// The position of this [`Row`] relative to the galley. + pub pos: Pos2, + + /// If true, this [`PlacedRow`] came from a paragraph ending with a `\n`. + /// The `\n` itself is omitted from [`Row::glyphs`]. + /// A `\n` in the input text always creates a new [`PlacedRow`] below it, + /// so that text that ends with `\n` has an empty [`PlacedRow`] last. + /// This also implies that the last [`PlacedRow`] in a [`Galley`] always has `ends_with_newline == false`. + pub ends_with_newline: bool, +} + +impl PlacedRow { + /// Logical bounding rectangle on font heights etc. + /// Use this when drawing a selection or similar! + pub fn rect(&self) -> Rect { + Rect::from_min_size(self.pos, self.row.size) + } +} + +impl std::ops::Deref for PlacedRow { + type Target = Row; + + fn deref(&self) -> &Self::Target { + &self.row + } +} + #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Row { @@ -547,20 +580,12 @@ pub struct Row { /// One for each `char`. pub glyphs: Vec, - /// Logical bounding rectangle based on font heights etc. - /// Use this when drawing a selection or similar! + /// Logical size based on font heights etc. /// Includes leading and trailing whitespace. - pub rect: Rect, + pub size: Vec2, /// The mesh, ready to be rendered. pub visuals: RowVisuals, - - /// If true, this [`Row`] came from a paragraph ending with a `\n`. - /// The `\n` itself is omitted from [`Self::glyphs`]. - /// A `\n` in the input text always creates a new [`Row`] below it, - /// so that text that ends with `\n` has an empty [`Row`] last. - /// This also implies that the last [`Row`] in a [`Galley`] always has `ends_with_newline == false`. - pub ends_with_newline: bool, } /// The tessellated output of a row. @@ -668,6 +693,27 @@ impl Row { self.glyphs.len() } + /// Closest char at the desired x coordinate in row-relative coordinates. + /// Returns something in the range `[0, char_count_excluding_newline()]`. + pub fn char_at(&self, desired_x: f32) -> usize { + for (i, glyph) in self.glyphs.iter().enumerate() { + if desired_x < glyph.logical_rect().center().x { + return i; + } + } + self.char_count_excluding_newline() + } + + pub fn x_offset(&self, column: usize) -> f32 { + if let Some(glyph) = self.glyphs.get(column) { + glyph.pos.x + } else { + self.size.x + } + } +} + +impl PlacedRow { /// Includes the implicit `\n` after the [`Row`], if any. #[inline] pub fn char_count_including_newline(&self) -> usize { @@ -676,36 +722,17 @@ impl Row { #[inline] pub fn min_y(&self) -> f32 { - self.rect.top() + self.rect().top() } #[inline] pub fn max_y(&self) -> f32 { - self.rect.bottom() + self.rect().bottom() } #[inline] pub fn height(&self) -> f32 { - self.rect.height() - } - - /// Closest char at the desired x coordinate. - /// Returns something in the range `[0, char_count_excluding_newline()]`. - pub fn char_at(&self, desired_x: f32) -> usize { - for (i, glyph) in self.glyphs.iter().enumerate() { - if desired_x < glyph.logical_rect().center().x { - return i; - } - } - self.char_count_excluding_newline() - } - - pub fn x_offset(&self, column: usize) -> f32 { - if let Some(glyph) = self.glyphs.get(column) { - glyph.pos.x - } else { - self.rect.right() - } + self.row.size.y } } @@ -755,8 +782,8 @@ impl std::ops::Deref for Galley { impl Galley { /// Zero-width rect past the last character. fn end_pos(&self) -> Rect { - if let Some((row, _)) = self.rows.last() { - let x = row.rect.right(); + if let Some(row) = self.rows.last() { + let x = row.rect().right(); Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } else { // Empty galley @@ -773,7 +800,7 @@ impl Galley { pub fn pos_from_pcursor(&self, pcursor: PCursor) -> Rect { let mut it = PCursor::default(); - for (row, offset) in &self.rows { + for row in &self.rows { if it.paragraph == pcursor.paragraph { // Right paragraph, but is it the right row in the paragraph? @@ -787,11 +814,8 @@ impl Galley { && !row.ends_with_newline && column >= row.char_count_excluding_newline(); if !select_next_row_instead { - let x = row.x_offset(column) + offset.x; - return Rect::from_min_max( - pos2(x, row.min_y() + offset.y), - pos2(x, row.max_y() + offset.y), - ); + let x = row.x_offset(column); + return Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())); } } } @@ -825,13 +849,13 @@ impl Galley { /// same as a cursor at the end. /// This allows implementing text-selection by dragging above/below the galley. pub fn cursor_from_pos(&self, pos: Vec2) -> Cursor { - if let Some((first_row, offset)) = self.rows.first() { - if pos.y < first_row.min_y() + offset.y { + if let Some(first_row) = self.rows.first() { + if pos.y < first_row.min_y() { return self.begin(); } } - if let Some((last_row, offset)) = self.rows.last() { - if last_row.max_y() + offset.y < pos.y { + if let Some(last_row) = self.rows.last() { + if last_row.max_y() < pos.y { return self.end(); } } @@ -842,15 +866,16 @@ impl Galley { let mut ccursor_index = 0; let mut pcursor_it = PCursor::default(); - for (row_nr, (row, offset)) in self.rows.iter().enumerate() { - let min_y = row.min_y() + offset.y; - let max_y = row.max_y() + offset.y; + for (row_nr, row) in self.rows.iter().enumerate() { + let min_y = row.min_y(); + let max_y = row.max_y(); let is_pos_within_row = min_y <= pos.y && pos.y <= max_y; let y_dist = (min_y - pos.y).abs().min((max_y - pos.y).abs()); if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; - let column = row.char_at(pos.x); + // char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos. + let column = row.char_at(pos.x - row.pos.x); let prefer_next_row = column < row.char_count_excluding_newline(); cursor = Cursor { ccursor: CCursor { @@ -910,7 +935,7 @@ impl Galley { offset: 0, prefer_next_row: true, }; - for (row, _) in &self.rows { + for row in &self.rows { let row_char_count = row.char_count_including_newline(); ccursor.index += row_char_count; if row.ends_with_newline { @@ -928,7 +953,7 @@ impl Galley { } pub fn end_rcursor(&self) -> RCursor { - if let Some((last_row, _)) = self.rows.last() { + if let Some(last_row) = self.rows.last() { RCursor { row: self.rows.len() - 1, column: last_row.char_count_including_newline(), @@ -954,7 +979,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, (row, _)) in self.rows.iter().enumerate() { + for (row_nr, row) in self.rows.iter().enumerate() { let row_char_count = row.char_count_excluding_newline(); if ccursor_it.index <= ccursor.index @@ -999,7 +1024,7 @@ impl Galley { } let prefer_next_row = - rcursor.column < self.rows[rcursor.row].0.char_count_excluding_newline(); + rcursor.column < self.rows[rcursor.row].char_count_excluding_newline(); let mut ccursor_it = CCursor { index: 0, prefer_next_row, @@ -1010,7 +1035,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, (row, _)) in self.rows.iter().enumerate() { + for (row_nr, row) in self.rows.iter().enumerate() { if row_nr == rcursor.row { ccursor_it.index += rcursor.column.at_most(row.char_count_excluding_newline()); @@ -1054,7 +1079,7 @@ impl Galley { prefer_next_row, }; - for (row_nr, (row, _)) in self.rows.iter().enumerate() { + for (row_nr, row) in self.rows.iter().enumerate() { if pcursor_it.paragraph == pcursor.paragraph { // Right paragraph, but is it the right row in the paragraph? @@ -1128,9 +1153,7 @@ impl Galley { let new_row = cursor.rcursor.row - 1; let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row] - .0 - .char_count_excluding_newline(); + >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); let new_rcursor = if cursor_is_beyond_end_of_current_row { // keep same column @@ -1141,8 +1164,8 @@ impl Galley { } else { // keep same X coord let x = self.pos_from_cursor(cursor).center().x; - let (row, offset) = &self.rows[new_row]; - let column = if x > row.rect.right() + offset.x { + let row = &self.rows[new_row]; + let column = if x > row.rect().right() { // beyond the end of this row - keep same column cursor.rcursor.column } else { @@ -1162,9 +1185,7 @@ impl Galley { let new_row = cursor.rcursor.row + 1; let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row] - .0 - .char_count_excluding_newline(); + >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); let new_rcursor = if cursor_is_beyond_end_of_current_row { // keep same column @@ -1175,8 +1196,8 @@ impl Galley { } else { // keep same X coord let x = self.pos_from_cursor(cursor).center().x; - let (row, offset) = &self.rows[new_row]; - let column = if x > row.rect.right() + offset.x { + let row = &self.rows[new_row]; + let column = if x > row.rect().right() { // beyond the end of the next row - keep same column cursor.rcursor.column } else { @@ -1204,9 +1225,7 @@ impl Galley { pub fn cursor_end_of_row(&self, cursor: &Cursor) -> Cursor { self.from_rcursor(RCursor { row: cursor.rcursor.row, - column: self.rows[cursor.rcursor.row] - .0 - .char_count_excluding_newline(), + column: self.rows[cursor.rcursor.row].char_count_excluding_newline(), }) } } From abbc561ae15760460c69101f40047f66b6dd4e8c Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 15:59:50 +0100 Subject: [PATCH 08/22] Correctly handle empty lines --- crates/epaint/src/text/fonts.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 234752e81f3..c323fb0d6db 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -772,6 +772,20 @@ impl GalleyCache { current = section_end; } + // If the current line is empty, add an extra offset to make sure it's not omitted + // because the resulting galley will have a height of zero. + let extra_y_offset = if start == end && end != job.text.len() { + while job.sections[current_section].byte_range.end == end { + current_section += 1; + } + let format = &job.sections[current_section].format; + format + .line_height + .unwrap_or(fonts.row_height(&format.font_id)) + } else { + 0.0 + }; + // Prevent an infinite recursion line_job.break_on_newline = false; @@ -780,7 +794,7 @@ impl GalleyCache { if left_max_rows != usize::MAX { left_max_rows -= galley.rows.len(); } - galleys.push(galley); + galleys.push((galley, extra_y_offset)); current = end + 1; if current >= job.text.len() { @@ -801,7 +815,7 @@ impl GalleyCache { pixels_per_point: fonts.pixels_per_point, }; - for galley in galleys { + for (galley, extra_y_offset) in galleys { let current_offset = emath::vec2(0.0, merged_galley.rect.height()); merged_galley .rows @@ -823,6 +837,7 @@ impl GalleyCache { merged_galley.rect = merged_galley .rect .union(galley.rect.translate(current_offset)); + merged_galley.rect.max.y += extra_y_offset; merged_galley.num_vertices += galley.num_vertices; merged_galley.num_indices += galley.num_indices; if galley.elided { From 6d6bc3befbeedb26b342900f9801d551d8ea2f0d Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 16:46:49 +0100 Subject: [PATCH 09/22] Respect first_row_min_height during multiline Galley layout --- crates/epaint/src/text/fonts.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index c323fb0d6db..b2fa099e0f6 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -733,6 +733,7 @@ impl GalleyCache { let mut left_max_rows = job.wrap.max_rows; let mut galleys = Vec::new(); let mut text_left = job.text.as_str(); + let mut first_row_min_height = job.first_row_min_height; loop { let end = text_left.find('\n').map_or(job.text.len(), |i| i + current); let start = current; @@ -743,10 +744,15 @@ impl GalleyCache { max_rows: left_max_rows, ..job.wrap }, + sections: Vec::new(), + // Prevent an infinite recursion + break_on_newline: false, halign: job.halign, justify: job.justify, - ..Default::default() + first_row_min_height, + round_output_size_to_nearest_ui_point: job.round_output_size_to_nearest_ui_point, }; + first_row_min_height = 0.0; while current < end { let mut s = &job.sections[current_section]; @@ -786,9 +792,6 @@ impl GalleyCache { 0.0 }; - // Prevent an infinite recursion - line_job.break_on_newline = false; - let galley = self.layout(fonts, line_job); // This will prevent us from invalidating cache entries unnecessarily if left_max_rows != usize::MAX { From 6147ff3668ab680a4ec57f73552cab95c050420a Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 16:59:38 +0100 Subject: [PATCH 10/22] Round `PlacedRow` positions to pixels during multiline layout --- crates/epaint/src/text/fonts.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index b2fa099e0f6..a2644299120 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -728,6 +728,10 @@ struct GalleyCache { impl GalleyCache { fn layout_multiline(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Galley { + let pixels_per_point = fonts.pixels_per_point; + let round_to_pixel = + move |point: emath::Pos2| (point * pixels_per_point).round() / pixels_per_point; + let mut current_section = 0; let mut current = 0; let mut left_max_rows = job.wrap.max_rows; @@ -830,7 +834,7 @@ impl GalleyCache { super::PlacedRow { row: placed_row.row.clone(), - pos: new_pos, + pos: round_to_pixel(new_pos), ends_with_newline: placed_row.ends_with_newline, } })); From 66c83c31ce59e85969b74c9f65c842a25268aea3 Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 15:38:58 +0100 Subject: [PATCH 11/22] Move `ends_with_newline` back into `Row` --- crates/epaint/src/text/fonts.rs | 95 +++++++++++---------- crates/epaint/src/text/text_layout.rs | 14 +-- crates/epaint/src/text/text_layout_types.rs | 14 +-- 3 files changed, 65 insertions(+), 58 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index a2644299120..dbd2fa3c045 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -739,7 +739,9 @@ impl GalleyCache { let mut text_left = job.text.as_str(); let mut first_row_min_height = job.first_row_min_height; loop { - let end = text_left.find('\n').map_or(job.text.len(), |i| i + current); + let end = text_left + .find('\n') + .map_or(job.text.len(), |i| i + current + 1); let start = current; let mut line_job = LayoutJob { @@ -749,8 +751,7 @@ impl GalleyCache { ..job.wrap }, sections: Vec::new(), - // Prevent an infinite recursion - break_on_newline: false, + break_on_newline: true, halign: job.halign, justify: job.justify, first_row_min_height, @@ -782,28 +783,14 @@ impl GalleyCache { current = section_end; } - // If the current line is empty, add an extra offset to make sure it's not omitted - // because the resulting galley will have a height of zero. - let extra_y_offset = if start == end && end != job.text.len() { - while job.sections[current_section].byte_range.end == end { - current_section += 1; - } - let format = &job.sections[current_section].format; - format - .line_height - .unwrap_or(fonts.row_height(&format.font_id)) - } else { - 0.0 - }; - - let galley = self.layout(fonts, line_job); + let galley = self.layout_component_line(fonts, line_job); // This will prevent us from invalidating cache entries unnecessarily if left_max_rows != usize::MAX { left_max_rows -= galley.rows.len(); } - galleys.push((galley, extra_y_offset)); + galleys.push(galley); - current = end + 1; + current = end; if current >= job.text.len() { break; } else { @@ -822,29 +809,30 @@ impl GalleyCache { pixels_per_point: fonts.pixels_per_point, }; - for (galley, extra_y_offset) in galleys { + for (i, galley) in galleys.iter().enumerate() { let current_offset = emath::vec2(0.0, merged_galley.rect.height()); - merged_galley - .rows - .extend(galley.rows.iter().map(|placed_row| { - let new_pos = placed_row.pos + current_offset; - merged_galley.mesh_bounds = merged_galley - .mesh_bounds - .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); - - super::PlacedRow { - row: placed_row.row.clone(), - pos: round_to_pixel(new_pos), - ends_with_newline: placed_row.ends_with_newline, - } - })); - if let Some(last) = merged_galley.rows.last_mut() { - last.ends_with_newline = true; + + let mut rows = galley.rows.iter(); + if i != galleys.len() - 1 && !galley.elided { + let popped = rows.next_back(); + debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0); } - merged_galley.rect = merged_galley - .rect - .union(galley.rect.translate(current_offset)); - merged_galley.rect.max.y += extra_y_offset; + + merged_galley.rows.extend(rows.map(|placed_row| { + let new_pos = round_to_pixel(placed_row.pos + current_offset); + merged_galley.mesh_bounds = merged_galley + .mesh_bounds + .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); + merged_galley.rect = merged_galley + .rect + .union(emath::Rect::from_min_size(new_pos, placed_row.size)); + + super::PlacedRow { + row: placed_row.row.clone(), + pos: new_pos, + } + })); + merged_galley.num_vertices += galley.num_vertices; merged_galley.num_indices += galley.num_indices; if galley.elided { @@ -853,13 +841,30 @@ impl GalleyCache { } } - if let Some(last) = merged_galley.rows.last_mut() { - last.ends_with_newline = false; - } - merged_galley } + fn layout_component_line(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Arc { + let hash = crate::util::hash(&job); + + match self.cache.entry(hash) { + std::collections::hash_map::Entry::Occupied(entry) => { + let cached = entry.into_mut(); + cached.last_used = self.generation; + cached.galley.clone() + } + std::collections::hash_map::Entry::Vacant(entry) => { + let galley = super::layout(fonts, job.into()); + let galley = Arc::new(galley); + entry.insert(CachedGalley { + last_used: self.generation, + galley: galley.clone(), + }); + galley + } + } + } + fn layout(&mut self, fonts: &mut FontsImpl, mut job: LayoutJob) -> Arc { if job.wrap.max_width.is_finite() { // Protect against rounding errors in egui layout code. diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 9203dfd0559..798fa1d3bac 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -220,9 +220,9 @@ fn rows_from_paragraphs( glyphs: vec![], visuals: Default::default(), size: vec2(0.0, paragraph.empty_paragraph_height), + ends_with_newline: !is_last_paragraph, }), pos: pos2(paragraph.cursor_x, 0.0), - ends_with_newline: !is_last_paragraph, }); } else { let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x(); @@ -236,13 +236,15 @@ fn rows_from_paragraphs( glyphs: paragraph.glyphs, visuals: Default::default(), size: rect.size(), + ends_with_newline: !is_last_paragraph, }), pos: rect.min, - ends_with_newline: !is_last_paragraph, }); } else { line_break(¶graph, job, &mut rows, elided); - rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph; + let placed_row = rows.last_mut().unwrap(); + let row = Arc::get_mut(&mut placed_row.row).unwrap(); + row.ends_with_newline = !is_last_paragraph; } } } @@ -288,9 +290,9 @@ fn line_break( glyphs: vec![], visuals: Default::default(), size: rect.size(), + ends_with_newline: false, }), pos: rect.min, - ends_with_newline: false, }); row_start_x += first_row_indentation; first_row_indentation = 0.0; @@ -316,9 +318,9 @@ fn line_break( glyphs, visuals: Default::default(), size: rect.size(), + ends_with_newline: false, }), pos: rect.min, - ends_with_newline: false, }); // Start a new row: @@ -359,9 +361,9 @@ fn line_break( glyphs, visuals: Default::default(), size: rect.size(), + ends_with_newline: false, }), pos: rect.min, - ends_with_newline: false, }); } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 0229665cbec..b3d26764e18 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -546,13 +546,6 @@ pub struct PlacedRow { /// The position of this [`Row`] relative to the galley. pub pos: Pos2, - - /// If true, this [`PlacedRow`] came from a paragraph ending with a `\n`. - /// The `\n` itself is omitted from [`Row::glyphs`]. - /// A `\n` in the input text always creates a new [`PlacedRow`] below it, - /// so that text that ends with `\n` has an empty [`PlacedRow`] last. - /// This also implies that the last [`PlacedRow`] in a [`Galley`] always has `ends_with_newline == false`. - pub ends_with_newline: bool, } impl PlacedRow { @@ -586,6 +579,13 @@ pub struct Row { /// The mesh, ready to be rendered. pub visuals: RowVisuals, + + /// If true, this [`Row`] came from a paragraph ending with a `\n`. + /// The `\n` itself is omitted from [`glyphs`]. + /// A `\n` in the input text always creates a new [`Row`] below it, + /// so that text that ends with `\n` has an empty [`Row`] last. + /// This also implies that the last [`Row`] in a [`Galley`] always has `ends_with_newline == false`. + pub ends_with_newline: bool, } /// The tessellated output of a row. From 1be24ba470429960e751edd62dcf3e59ec48255f Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 17:49:59 +0100 Subject: [PATCH 12/22] Respect `LayoutJob::round_output_size_to_nearest_ui_point` --- crates/epaint/src/text/fonts.rs | 7 ++++++ crates/epaint/src/text/mod.rs | 2 +- crates/epaint/src/text/text_layout.rs | 32 +++++++++++++++------------ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index dbd2fa3c045..df6547428e5 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -841,6 +841,13 @@ impl GalleyCache { } } + if merged_galley.job.round_output_size_to_nearest_ui_point { + super::round_output_size_to_nearest_ui_point( + &mut merged_galley.rect, + &merged_galley.job, + ); + } + merged_galley } diff --git a/crates/epaint/src/text/mod.rs b/crates/epaint/src/text/mod.rs index 3cb0e98cbc5..cf5c8ebfc99 100644 --- a/crates/epaint/src/text/mod.rs +++ b/crates/epaint/src/text/mod.rs @@ -14,7 +14,7 @@ pub use { FontData, FontDefinitions, FontFamily, FontId, FontInsert, FontPriority, FontTweak, Fonts, FontsImpl, InsertFontFamily, }, - text_layout::layout, + text_layout::*, text_layout_types::*, }; diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 798fa1d3bac..5f52235c566 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -679,20 +679,7 @@ fn galley_from_rows( let mut rect = Rect::from_min_max(pos2(min_x, 0.0), pos2(max_x, cursor_y)); if job.round_output_size_to_nearest_ui_point { - let did_exceed_wrap_width_by_a_lot = rect.width() > job.wrap.max_width + 1.0; - - // We round the size to whole ui points here (not pixels!) so that the egui layout code - // can have the advantage of working in integer units, avoiding rounding errors. - rect.min = rect.min.round(); - rect.max = rect.max.round(); - - if did_exceed_wrap_width_by_a_lot { - // If the user picked a too aggressive wrap width (e.g. more narrow than any individual glyph), - // we should let the user know by reporting that our width is wider than the wrap width. - } else { - // Make sure we don't report being wider than the wrap width the user picked: - rect.max.x = rect.max.x.at_most(rect.min.x + job.wrap.max_width).floor(); - } + round_output_size_to_nearest_ui_point(&mut rect, &job); } Galley { @@ -707,6 +694,23 @@ fn galley_from_rows( } } +pub(crate) fn round_output_size_to_nearest_ui_point(rect: &mut Rect, job: &LayoutJob) { + let did_exceed_wrap_width_by_a_lot = rect.width() > job.wrap.max_width + 1.0; + + // We round the size to whole ui points here (not pixels!) so that the egui layout code + // can have the advantage of working in integer units, avoiding rounding errors. + rect.min = rect.min.round(); + rect.max = rect.max.round(); + + if did_exceed_wrap_width_by_a_lot { + // If the user picked a too aggressive wrap width (e.g. more narrow than any individual glyph), + // we should let the user know by reporting that our width is wider than the wrap width. + } else { + // Make sure we don't report being wider than the wrap width the user picked: + rect.max.x = rect.max.x.at_most(rect.min.x + job.wrap.max_width).floor(); + } +} + #[derive(Default)] struct FormatSummary { any_background: bool, From fd8413c62a2e038d9b1bdc4235f402168dcca863 Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 17:54:04 +0100 Subject: [PATCH 13/22] Simplify `layout_multiline` `loop` loop into a `while` loop --- crates/epaint/src/text/fonts.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index df6547428e5..871cecabdb1 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -736,10 +736,9 @@ impl GalleyCache { let mut current = 0; let mut left_max_rows = job.wrap.max_rows; let mut galleys = Vec::new(); - let mut text_left = job.text.as_str(); let mut first_row_min_height = job.first_row_min_height; - loop { - let end = text_left + while current != job.text.len() { + let end = job.text[current..] .find('\n') .map_or(job.text.len(), |i| i + current + 1); let start = current; @@ -791,11 +790,6 @@ impl GalleyCache { galleys.push(galley); current = end; - if current >= job.text.len() { - break; - } else { - text_left = &job.text[current..]; - } } let mut merged_galley = Galley { From bbe566256ef5271b33a80801720910636eccd2f8 Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 18:17:38 +0100 Subject: [PATCH 14/22] Fix `Row::ends_with_newline` docs, explain skipping of last galley row --- crates/epaint/src/text/fonts.rs | 3 +++ crates/epaint/src/text/text_layout_types.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 871cecabdb1..2e0343d6d4d 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -807,6 +807,9 @@ impl GalleyCache { let current_offset = emath::vec2(0.0, merged_galley.rect.height()); let mut rows = galley.rows.iter(); + // As documented in `Row::ends_with_newline`, a '\n' will always create a + // new `Row` immediately below the current one. Here it doesn't make sense + // for us to append this new row so we just ignore it. if i != galleys.len() - 1 && !galley.elided { let popped = rows.next_back(); debug_assert_eq!(popped.unwrap().row.glyphs.len(), 0); diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index b3d26764e18..3ebf07b8f5e 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -581,7 +581,7 @@ pub struct Row { pub visuals: RowVisuals, /// If true, this [`Row`] came from a paragraph ending with a `\n`. - /// The `\n` itself is omitted from [`glyphs`]. + /// The `\n` itself is omitted from [`Self::glyphs`]. /// A `\n` in the input text always creates a new [`Row`] below it, /// so that text that ends with `\n` has an empty [`Row`] last. /// This also implies that the last [`Row`] in a [`Galley`] always has `ends_with_newline == false`. From 110a9c39e3c0a5b9bf7f7838c6288a0c05a9c0ae Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 18:25:02 +0100 Subject: [PATCH 15/22] Move some `PlacedRow` methods back to `Row` --- crates/epaint/src/text/text_layout_types.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 3ebf07b8f5e..f90210ca136 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -693,6 +693,12 @@ impl Row { self.glyphs.len() } + /// Includes the implicit `\n` after the [`Row`], if any. + #[inline] + pub fn char_count_including_newline(&self) -> usize { + self.glyphs.len() + (self.ends_with_newline as usize) + } + /// Closest char at the desired x coordinate in row-relative coordinates. /// Returns something in the range `[0, char_count_excluding_newline()]`. pub fn char_at(&self, desired_x: f32) -> usize { @@ -711,15 +717,14 @@ impl Row { self.size.x } } -} -impl PlacedRow { - /// Includes the implicit `\n` after the [`Row`], if any. #[inline] - pub fn char_count_including_newline(&self) -> usize { - self.glyphs.len() + (self.ends_with_newline as usize) + pub fn height(&self) -> f32 { + self.size.y } +} +impl PlacedRow { #[inline] pub fn min_y(&self) -> f32 { self.rect().top() @@ -729,11 +734,6 @@ impl PlacedRow { pub fn max_y(&self) -> f32 { self.rect().bottom() } - - #[inline] - pub fn height(&self) -> f32 { - self.row.size.y - } } impl Galley { From 139f28640d8349cea751644caeaf9455492b316d Mon Sep 17 00:00:00 2001 From: Fishhh Date: Sat, 30 Nov 2024 19:23:32 +0100 Subject: [PATCH 16/22] Replace a hack with a proper implementation --- crates/epaint/src/text/fonts.rs | 5 ++-- crates/epaint/src/text/text_layout.rs | 39 +++++++-------------------- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 2e0343d6d4d..36f2aa5d195 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -730,7 +730,7 @@ impl GalleyCache { fn layout_multiline(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Galley { let pixels_per_point = fonts.pixels_per_point; let round_to_pixel = - move |point: emath::Pos2| (point * pixels_per_point).round() / pixels_per_point; + move |point: f32| (point * pixels_per_point).round() / pixels_per_point; let mut current_section = 0; let mut current = 0; @@ -816,7 +816,8 @@ impl GalleyCache { } merged_galley.rows.extend(rows.map(|placed_row| { - let new_pos = round_to_pixel(placed_row.pos + current_offset); + let mut new_pos = placed_row.pos + current_offset; + new_pos.y = round_to_pixel(new_pos.y); merged_galley.mesh_bounds = merged_galley .mesh_bounds .union(placed_row.visuals.mesh_bounds.translate(new_pos.to_vec2())); diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 5f52235c566..c5105966647 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -1,4 +1,3 @@ -use std::ops::RangeInclusive; use std::sync::Arc; use emath::{pos2, vec2, Align, NumExt, Pos2, Rect, Vec2}; @@ -115,7 +114,6 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { halign_and_justify_row( point_scale, Arc::get_mut(&mut placed_row.row).unwrap(), - &mut placed_row.pos, job.halign, job.wrap.max_width, justify_row, @@ -190,11 +188,6 @@ fn layout_section( } } -/// We ignore y at this stage -fn rect_from_x_range(x_range: RangeInclusive) -> Rect { - Rect::from_x_y_ranges(x_range, 0.0..=0.0) -} - // Ignores the Y coordinate. fn rows_from_paragraphs( paragraphs: Vec, @@ -222,23 +215,21 @@ fn rows_from_paragraphs( size: vec2(0.0, paragraph.empty_paragraph_height), ends_with_newline: !is_last_paragraph, }), - pos: pos2(paragraph.cursor_x, 0.0), + pos: pos2(0.0, 0.0), }); } else { let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x(); if paragraph_max_x <= job.effective_wrap_width() { // Early-out optimization: the whole paragraph fits on one row. - let paragraph_min_x = paragraph.glyphs[0].pos.x; - let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); rows.push(PlacedRow { row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: paragraph.glyphs, visuals: Default::default(), - size: rect.size(), + size: vec2(paragraph_max_x, 0.0), ends_with_newline: !is_last_paragraph, }), - pos: rect.min, + pos: pos2(0.0, f32::NAN), }); } else { line_break(¶graph, job, &mut rows, elided); @@ -283,16 +274,15 @@ fn line_break( { // Allow the first row to be completely empty, because we know there will be more space on the next row: // TODO(emilk): this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height. - let rect = rect_from_x_range(first_row_indentation..=first_row_indentation); out_rows.push(PlacedRow { row: Arc::new(Row { section_index_at_start: paragraph.section_index_at_start, glyphs: vec![], visuals: Default::default(), - size: rect.size(), + size: vec2(0.0, 0.0), ends_with_newline: false, }), - pos: rect.min, + pos: pos2(0.0, f32::NAN), }); row_start_x += first_row_indentation; first_row_indentation = 0.0; @@ -308,19 +298,17 @@ fn line_break( .collect(); let section_index_at_start = glyphs[0].section_index; - let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); out_rows.push(PlacedRow { row: Arc::new(Row { section_index_at_start, glyphs, visuals: Default::default(), - size: rect.size(), + size: vec2(paragraph_max_x, 0.0), ends_with_newline: false, }), - pos: rect.min, + pos: pos2(0.0, f32::NAN), }); // Start a new row: @@ -354,16 +342,15 @@ fn line_break( let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); - let rect = rect_from_x_range(paragraph_min_x..=paragraph_max_x); out_rows.push(PlacedRow { row: Arc::new(Row { section_index_at_start, glyphs, visuals: Default::default(), - size: rect.size(), + size: vec2(paragraph_max_x - paragraph_min_x, 0.0), ends_with_newline: false, }), - pos: rect.min, + pos: pos2(paragraph_min_x, 0.0), }); } } @@ -526,7 +513,6 @@ fn replace_last_glyph_with_overflow_character( fn halign_and_justify_row( point_scale: PointScale, row: &mut Row, - pos: &mut Pos2, halign: Align, wrap_width: f32, justify: bool, @@ -609,8 +595,7 @@ fn halign_and_justify_row( } } - // Note we ignore the leading/trailing whitespace here! - pos.x = target_min_x; + // Note we **don't** ignore the leading/trailing whitespace here! row.size.x = target_max_x - target_min_x; } @@ -647,10 +632,6 @@ fn galley_from_rows( // When mixing different `FontImpl` (e.g. latin and emojis), // we always center the difference: + 0.5 * (glyph.font_height - glyph.font_impl_height); - - // FIXME(afishhh): HACK! change the proper code above instead!! - // this should probably not be merged like this! - glyph.pos.x -= placed_row.pos.x; } placed_row.pos.y = cursor_y; From e15b34b9849239f76b1028c1488b9c5ef4e0036d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Mon, 2 Dec 2024 00:52:12 +0100 Subject: [PATCH 17/22] Slightly simplify `paint_text_selection` code --- crates/egui/src/text_selection/visuals.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index 3eecfaf4bf3..b2c2e8d9ea7 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -31,27 +31,25 @@ pub fn paint_text_selection( let max = max.rcursor; for ri in min.row..=max.row { - let placed_row = &mut galley.rows[ri]; + let row = Arc::make_mut(&mut galley.rows[ri].row); let left = if ri == min.row { - placed_row.x_offset(min.column) + row.x_offset(min.column) } else { 0.0 }; let right = if ri == max.row { - placed_row.x_offset(max.column) + row.x_offset(max.column) } else { - let newline_size = if placed_row.ends_with_newline { - placed_row.height() / 2.0 // visualize that we select the newline + let newline_size = if row.ends_with_newline { + row.height() / 2.0 // visualize that we select the newline } else { 0.0 }; - placed_row.size.x + newline_size + row.size.x + newline_size }; - let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, placed_row.size.y)); - - let row = Arc::make_mut(&mut placed_row.row); + let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, row.size.y)); let mesh = &mut row.visuals.mesh; // Time to insert the selection rectangle into the row mesh. From c6592ec8980a6872ebef31c03e338d17b31918e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Wed, 4 Dec 2024 19:30:13 +0100 Subject: [PATCH 18/22] Fix nits --- crates/epaint/src/tessellator.rs | 6 +- crates/epaint/src/text/fonts.rs | 118 +++++++++++++------------- crates/epaint/src/text/text_layout.rs | 2 +- 3 files changed, 63 insertions(+), 63 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index a6290e9c636..05da7cc04cc 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1783,13 +1783,13 @@ impl Tessellator { continue; } - let final_pos = galley_pos + row.pos.to_vec2(); + let final_row_pos = galley_pos + row.pos.to_vec2(); let mut row_rect = row.visuals.mesh_bounds; if *angle != 0.0 { row_rect = row_rect.rotate_bb(rotator); } - row_rect = row_rect.translate(final_pos.to_vec2()); + row_rect = row_rect.translate(final_row_pos.to_vec2()); if self.options.coarse_tessellation_culling && !self.clip_rect.intersects(row_rect) { // culling individual lines of text is important, since a single `Shape::Text` @@ -1838,7 +1838,7 @@ impl Tessellator { }; Vertex { - pos: final_pos + offset, + pos: final_row_pos + offset, uv: (uv.to_vec2() * uv_normalizer).to_pos2(), color, } diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 36f2aa5d195..75dcf1958cc 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -727,6 +727,65 @@ struct GalleyCache { } impl GalleyCache { + fn layout(&mut self, fonts: &mut FontsImpl, mut job: LayoutJob) -> Arc { + if job.wrap.max_width.is_finite() { + // Protect against rounding errors in egui layout code. + + // Say the user asks to wrap at width 200.0. + // The text layout wraps, and reports that the final width was 196.0 points. + // This then trickles up the `Ui` chain and gets stored as the width for a tooltip (say). + // On the next frame, this is then set as the max width for the tooltip, + // and we end up calling the text layout code again, this time with a wrap width of 196.0. + // Except, somewhere in the `Ui` chain with added margins etc, a rounding error was introduced, + // so that we actually set a wrap-width of 195.9997 instead. + // Now the text that fit perfrectly at 196.0 needs to wrap one word earlier, + // and so the text re-wraps and reports a new width of 185.0 points. + // And then the cycle continues. + + // So we limit max_width to integers. + + // Related issues: + // * https://github.com/emilk/egui/issues/4927 + // * https://github.com/emilk/egui/issues/4928 + // * https://github.com/emilk/egui/issues/5084 + // * https://github.com/emilk/egui/issues/5163 + + job.wrap.max_width = job.wrap.max_width.round(); + } + + let hash = crate::util::hash(&job); // TODO(emilk): even faster hasher? + + match self.cache.entry(hash) { + std::collections::hash_map::Entry::Occupied(entry) => { + let cached = entry.into_mut(); + cached.last_used = self.generation; + cached.galley.clone() + } + std::collections::hash_map::Entry::Vacant(entry) => { + if job.break_on_newline { + let galley = self.layout_multiline(fonts, job); + let galley = Arc::new(galley); + self.cache.insert( + hash, + CachedGalley { + last_used: self.generation, + galley: galley.clone(), + }, + ); + galley + } else { + let galley = super::layout(fonts, job.into()); + let galley = Arc::new(galley); + entry.insert(CachedGalley { + last_used: self.generation, + galley: galley.clone(), + }); + galley + } + } + } + } + fn layout_multiline(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Galley { let pixels_per_point = fonts.pixels_per_point; let round_to_pixel = @@ -870,65 +929,6 @@ impl GalleyCache { } } - fn layout(&mut self, fonts: &mut FontsImpl, mut job: LayoutJob) -> Arc { - if job.wrap.max_width.is_finite() { - // Protect against rounding errors in egui layout code. - - // Say the user asks to wrap at width 200.0. - // The text layout wraps, and reports that the final width was 196.0 points. - // This then trickles up the `Ui` chain and gets stored as the width for a tooltip (say). - // On the next frame, this is then set as the max width for the tooltip, - // and we end up calling the text layout code again, this time with a wrap width of 196.0. - // Except, somewhere in the `Ui` chain with added margins etc, a rounding error was introduced, - // so that we actually set a wrap-width of 195.9997 instead. - // Now the text that fit perfrectly at 196.0 needs to wrap one word earlier, - // and so the text re-wraps and reports a new width of 185.0 points. - // And then the cycle continues. - - // So we limit max_width to integers. - - // Related issues: - // * https://github.com/emilk/egui/issues/4927 - // * https://github.com/emilk/egui/issues/4928 - // * https://github.com/emilk/egui/issues/5084 - // * https://github.com/emilk/egui/issues/5163 - - job.wrap.max_width = job.wrap.max_width.round(); - } - - let hash = crate::util::hash(&job); // TODO(emilk): even faster hasher? - - match self.cache.entry(hash) { - std::collections::hash_map::Entry::Occupied(entry) => { - let cached = entry.into_mut(); - cached.last_used = self.generation; - cached.galley.clone() - } - std::collections::hash_map::Entry::Vacant(entry) => { - if job.break_on_newline { - let galley = self.layout_multiline(fonts, job); - let galley = Arc::new(galley); - self.cache.insert( - hash, - CachedGalley { - last_used: self.generation, - galley: galley.clone(), - }, - ); - galley - } else { - let galley = super::layout(fonts, job.into()); - let galley = Arc::new(galley); - entry.insert(CachedGalley { - last_used: self.generation, - galley: galley.clone(), - }); - galley - } - } - } - } - pub fn num_galleys_in_cache(&self) -> usize { self.cache.len() } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index c5105966647..4217bffb064 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -595,7 +595,7 @@ fn halign_and_justify_row( } } - // Note we **don't** ignore the leading/trailing whitespace here! + // Note we ignore the leading/trailing whitespace here! row.size.x = target_max_x - target_min_x; } From 25da82279f02f4a344c806879b0cea765025c3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 5 Dec 2024 18:27:38 +0100 Subject: [PATCH 19/22] Fix text horizontal alignment --- crates/epaint/src/text/text_layout.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 4217bffb064..2d4c30a92b2 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -113,7 +113,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let justify_row = justify && !placed_row.ends_with_newline && !is_last_row; halign_and_justify_row( point_scale, - Arc::get_mut(&mut placed_row.row).unwrap(), + placed_row, job.halign, job.wrap.max_width, justify_row, @@ -512,11 +512,13 @@ fn replace_last_glyph_with_overflow_character( /// Ignores the Y coordinate. fn halign_and_justify_row( point_scale: PointScale, - row: &mut Row, + placed_row: &mut PlacedRow, halign: Align, wrap_width: f32, justify: bool, ) { + let row = Arc::get_mut(&mut placed_row.row).unwrap(); + if row.glyphs.is_empty() { return; } @@ -584,7 +586,8 @@ fn halign_and_justify_row( / (num_spaces_in_range as f32); } - let mut translate_x = target_min_x - original_min_x - extra_x_per_glyph * glyph_range.0 as f32; + placed_row.pos.x = point_scale.round_to_pixel(target_min_x); + let mut translate_x = -original_min_x - extra_x_per_glyph * glyph_range.0 as f32; for glyph in &mut row.glyphs { glyph.pos.x += translate_x; From 40f237d43c8fc913a2e30f09fb932edde6155adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 5 Dec 2024 18:43:22 +0100 Subject: [PATCH 20/22] Add comment and check for newline before multiline layout --- crates/epaint/src/text/fonts.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 75dcf1958cc..1db2050caeb 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -762,7 +762,11 @@ impl GalleyCache { cached.galley.clone() } std::collections::hash_map::Entry::Vacant(entry) => { - if job.break_on_newline { + // If the text contains newlines that will always break into a new row then + // we can easily lay out all the lines individually and then merge the `Galley`s. + // This allows individual lines to be cached separately which means small + // modifications to the source text will only cause impacted lines to be laid out again. + if job.break_on_newline && job.text.contains('\n') { let galley = self.layout_multiline(fonts, job); let galley = Arc::new(galley); self.cache.insert( From 17a5f1f75e320fabb71e02581b12d2fd77c38c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Thu, 5 Dec 2024 19:15:23 +0100 Subject: [PATCH 21/22] Fix incorrect behavior with `LayoutJob::max_rows` --- crates/epaint/src/text/fonts.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 1db2050caeb..a681c3f0575 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -849,8 +849,17 @@ impl GalleyCache { // This will prevent us from invalidating cache entries unnecessarily if left_max_rows != usize::MAX { left_max_rows -= galley.rows.len(); + // Ignore extra trailing row, see merging counterpart below for more details. + if end < job.text.len() && !galley.elided { + left_max_rows += 1; + } } + + let elided = galley.elided; galleys.push(galley); + if elided { + break; + } current = end; } @@ -896,10 +905,9 @@ impl GalleyCache { merged_galley.num_vertices += galley.num_vertices; merged_galley.num_indices += galley.num_indices; - if galley.elided { - merged_galley.elided = true; - break; - } + // Note that if `galley.elided` is true this will be the last `Galley` in + // the vector and the loop will end. + merged_galley.elided |= galley.elided; } if merged_galley.job.round_output_size_to_nearest_ui_point { From 3e1ed1829991cca5f6d32a7b6b175fe0a0940c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20G=C5=82uchowski?= Date: Wed, 11 Dec 2024 18:42:40 +0100 Subject: [PATCH 22/22] Add benchmark --- Cargo.lock | 1 + crates/egui_demo_lib/Cargo.toml | 1 + crates/egui_demo_lib/benches/benchmark.rs | 31 +++++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a2e858d4b41..aa57f075462 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,6 +1313,7 @@ dependencies = [ "egui_demo_lib", "egui_extras", "egui_kittest", + "rand", "serde", "unicode_names2", "wgpu", diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index f8f1f47f686..fc59280f232 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -58,6 +58,7 @@ serde = { workspace = true, optional = true } # when running tests we always want to use the `chrono` feature egui_demo_lib = { workspace = true, features = ["chrono"] } +rand = "0.8" criterion.workspace = true egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } wgpu = { workspace = true, features = ["metal"] } diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index d3820603d5c..8be00f72d2b 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -1,7 +1,10 @@ +use std::fmt::Write as _; + use criterion::{criterion_group, criterion_main, Criterion}; use egui::epaint::TextShape; use egui_demo_lib::LOREM_IPSUM_LONG; +use rand::Rng as _; pub fn criterion_benchmark(c: &mut Criterion) { use egui::RawInput; @@ -122,6 +125,34 @@ pub fn criterion_benchmark(c: &mut Criterion) { }); }); + c.bench_function("text_layout_cached_with_modify", |b| { + const MAX_REMOVED_BYTES: usize = 5000; + + let mut string = String::new(); + // 2000 lines * 200 bytes * ~3 characters = 1.2MB + string.reserve(2000 * 200 * 3 + 2000); + for _ in 0..2000 { + for i in 0..200u8 { + write!(string, "{i:02X} ").unwrap(); + } + string.push('\n'); + } + + let mut rng = rand::thread_rng(); + b.iter(|| { + fonts.begin_pass(pixels_per_point, max_texture_side); + let mut temp_string = String::with_capacity(string.len()); + let modified_start = rng.gen_range(0..string.len()); + let max_end = (modified_start + MAX_REMOVED_BYTES).min(string.len()); + let modified_end = rng.gen_range(modified_start..max_end); + + temp_string.push_str(&string[..modified_start]); + temp_string.push_str(&string[modified_end..]); + + fonts.layout(temp_string, font_id.clone(), text_color, wrap_width); + }); + }); + let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, text_color, wrap_width); let font_image_size = fonts.font_image_size(); let prepared_discs = fonts.texture_atlas().lock().prepared_discs();