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(), }) } }