diff --git a/emigui/src/containers/collapsing_header.rs b/emigui/src/containers/collapsing_header.rs index 0e33ac77..825744c3 100644 --- a/emigui/src/containers/collapsing_header.rs +++ b/emigui/src/containers/collapsing_header.rs @@ -91,10 +91,10 @@ impl CollapsingHeader { paint_icon(ui, &state, &interact); - ui.add_text( + ui.add_galley( text_pos, + galley, label.text_style, - galley.fragments, Some(ui.style().interact(&interact).stroke_color), ); diff --git a/emigui/src/context.rs b/emigui/src/context.rs index 914c2ba0..2dad6dda 100644 --- a/emigui/src/context.rs +++ b/emigui/src/context.rs @@ -370,13 +370,7 @@ impl Context { rect: rect.expand(2.0), }, ); - self.add_text( - layer, - rect.min, - text_style, - galley.fragments, - Some(color::RED), - ); + self.add_galley(layer, rect.min, galley, text_style, Some(color::RED)); } pub fn debug_text(&self, pos: Pos2, text: &str) { @@ -418,36 +412,33 @@ impl Context { text_style: TextStyle, align: (Align, Align), text_color: Option, - ) -> Vec2 { + ) -> Rect { let font = &self.fonts[text_style]; let galley = font.layout_multiline(text, f32::INFINITY); let rect = align_rect(Rect::from_min_size(pos, galley.size), align); - self.add_text(layer, rect.min, text_style, galley.fragments, text_color); - galley.size + self.add_galley(layer, rect.min, galley, text_style, text_color); + rect } /// Already layed out text. - pub fn add_text( + pub fn add_galley( &self, layer: Layer, pos: Pos2, + galley: font::Galley, text_style: TextStyle, - text: Vec, color: Option, ) { let color = color.unwrap_or_else(|| self.style().text_color()); - for fragment in text { - self.add_paint_cmd( - layer, - PaintCmd::Text { - color, - pos: pos + vec2(0.0, fragment.y_offset), - text: fragment.text, - text_style, - x_offsets: fragment.x_offsets, - }, - ); - } + self.add_paint_cmd( + layer, + PaintCmd::Text { + pos, + galley, + text_style, + color, + }, + ); } pub fn add_paint_cmd(&self, layer: Layer, paint_cmd: PaintCmd) { diff --git a/emigui/src/font.rs b/emigui/src/font.rs index 4700d71e..af0f8b32 100644 --- a/emigui/src/font.rs +++ b/emigui/src/font.rs @@ -9,7 +9,8 @@ use crate::{ }; /// A typeset piece of text on a single line. -pub struct Fragment { +#[derive(Clone, Debug)] +pub struct Line { /// The start of each character, probably starting at zero. /// The last element is the end of the last character. /// x_offsets.len() == text.chars().count() + 1 @@ -17,16 +18,21 @@ pub struct Fragment { /// Unit: points. pub x_offsets: Vec, - /// 0 for the first line, n * line_spacing for the rest + /// Top y offset of this line. 0.0 for the first line, n * line_spacing for the rest. /// Unit: points. pub y_offset: f32, - - // TODO: make this a str reference into a String owned by Galley - /// The actual characters. - pub text: String, } -impl Fragment { +impl Line { + pub fn sanity_check(&self) { + assert!(!self.x_offsets.is_empty()); + } + + pub fn char_count(&self) -> usize { + assert!(!self.x_offsets.is_empty()); + self.x_offsets.len() - 1 + } + pub fn min_x(&self) -> f32 { *self.x_offsets.first().unwrap() } @@ -36,22 +42,31 @@ impl Fragment { } } -// pub fn fn_text_width(fragmens: &[Fragment]) -> f32 { -// if fragmens.is_empty() { -// 0.0 -// } else { -// fragmens.last().unwrap().max_x() - fragmens.first().unwrap().min_x() -// } -// } - /// A collection of text locked into place. -#[derive(Default)] +#[derive(Clone, Debug, Default)] pub struct Galley { - // TODO: maybe rename/refactor this as `lines`? - pub fragments: Vec, + /// The full text + pub text: String, + + /// Lines of text, from top to bottom. + /// The number of chars in all lines sum up to text.chars().count() + pub lines: Vec, + + // TODO: remove? Can just calculate on the fly pub size: Vec2, } +impl Galley { + pub fn sanity_check(&self) { + let mut char_count = 0; + for line in &self.lines { + line.sanity_check(); + char_count += line.char_count(); + } + assert_eq!(char_count, self.text.chars().count()); + } +} + // ---------------------------------------------------------------------------- #[derive(Clone, Copy, Debug)] @@ -233,28 +248,24 @@ impl Font { /// Always returns exactly one frament. pub fn layout_single_line(&self, text: &str) -> Galley { let x_offsets = self.layout_single_line_fragment(text); - let fragment = Fragment { + let line = Line { x_offsets, y_offset: 0.0, - text: text.to_owned(), }; - assert_eq!(fragment.x_offsets.len(), fragment.text.chars().count() + 1); - let width = fragment.max_x(); + let width = line.max_x(); let size = vec2(width, self.height()); - Galley { - fragments: vec![fragment], + let galley = Galley { + text: text.to_owned(), + lines: vec![line], size, - } + }; + galley.sanity_check(); + galley } /// A paragraph is text with no line break character in it. /// The text will be linebreaked by the given `max_width_in_points`. - /// TODO: return Galley ? - pub fn layout_paragraph_max_width( - &self, - text: &str, - max_width_in_points: f32, - ) -> Vec { + pub fn layout_paragraph_max_width(&self, text: &str, max_width_in_points: f32) -> Vec { let full_x_offsets = self.layout_single_line_fragment(text); let mut line_start_x = full_x_offsets[0]; @@ -265,7 +276,7 @@ impl Font { // start index of the last space. A candidate for a new line. let mut last_space = None; - let mut out_fragments = vec![]; + let mut out_lines = vec![]; for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() { let line_width = x - line_start_x; @@ -273,35 +284,25 @@ impl Font { if line_width > max_width_in_points { if let Some(last_space_idx) = last_space { let include_trailing_space = true; - let fragment = if include_trailing_space { - Fragment { + let line = if include_trailing_space { + Line { x_offsets: full_x_offsets[line_start_idx..=last_space_idx + 1] .iter() .map(|x| x - line_start_x) .collect(), y_offset: cursor_y, - text: text - .chars() - .skip(line_start_idx) - .take(last_space_idx + 1 - line_start_idx) - .collect(), } } else { - Fragment { + Line { x_offsets: full_x_offsets[line_start_idx..=last_space_idx] .iter() .map(|x| x - line_start_x) .collect(), y_offset: cursor_y, - text: text - .chars() - .skip(line_start_idx) - .take(last_space_idx - line_start_idx) - .collect(), } }; - assert_eq!(fragment.x_offsets.len(), fragment.text.chars().count() + 1); - out_fragments.push(fragment); + line.sanity_check(); + out_lines.push(line); line_start_idx = last_space_idx + 1; line_start_x = full_x_offsets[line_start_idx]; @@ -318,49 +319,60 @@ impl Font { } if line_start_idx + 1 < full_x_offsets.len() { - let fragment = Fragment { + let line = Line { x_offsets: full_x_offsets[line_start_idx..] .iter() .map(|x| x - line_start_x) .collect(), y_offset: cursor_y, - text: text.chars().skip(line_start_idx).collect(), }; - assert_eq!(fragment.x_offsets.len(), fragment.text.chars().count() + 1); - out_fragments.push(fragment); + line.sanity_check(); + out_lines.push(line); } - out_fragments + out_lines } pub fn layout_multiline(&self, text: &str, max_width_in_points: f32) -> Galley { let line_spacing = self.line_spacing(); let mut cursor_y = 0.0; - let mut fragments = Vec::new(); - for line in text.split('\n') { - let mut paragraph_fragments = - self.layout_paragraph_max_width(line, max_width_in_points); - if let Some(last_fragment) = paragraph_fragments.last() { - let line_height = last_fragment.y_offset + line_spacing; - for fragment in &mut paragraph_fragments { - fragment.y_offset += cursor_y; - } - fragments.append(&mut paragraph_fragments); - cursor_y += line_height; // TODO: add extra spacing between paragraphs - } else { - cursor_y += line_spacing; + let mut lines = Vec::new(); + + let mut paragraph_start = 0; + + while paragraph_start < text.len() { + let next_newline = text[paragraph_start..].find('\n'); + let paragraph_end = next_newline + .map(|newline| paragraph_start + newline + 1) + .unwrap_or_else(|| text.len()); + + assert!(paragraph_start < paragraph_end); + let paragraph_text = &text[paragraph_start..paragraph_end]; + let mut paragraph_lines = + self.layout_paragraph_max_width(paragraph_text, max_width_in_points); + assert!(!paragraph_lines.is_empty()); + + let line_height = paragraph_lines.last().unwrap().y_offset + line_spacing; + for line in &mut paragraph_lines { + line.y_offset += cursor_y; } - cursor_y = self.round_to_pixel(cursor_y); + lines.append(&mut paragraph_lines); + cursor_y += line_height; // TODO: add extra spacing between paragraphs + + paragraph_start = paragraph_end; } let mut widest_line = 0.0; - for fragment in &fragments { - widest_line = fragment.max_x().max(widest_line); + for line in &lines { + widest_line = line.max_x().max(widest_line); } - Galley { - fragments, + let galley = Galley { + text: text.to_owned(), + lines, size: vec2(widest_line, cursor_y), - } + }; + galley.sanity_check(); + galley } } diff --git a/emigui/src/mesher.rs b/emigui/src/mesher.rs index 1984997d..c33f1667 100644 --- a/emigui/src/mesher.rs +++ b/emigui/src/mesher.rs @@ -564,30 +564,35 @@ pub fn mesh_command( } } PaintCmd::Text { - color, pos, - text, + galley, text_style, - x_offsets, + color, } => { + galley.sanity_check(); let font = &fonts[text_style]; - for (c, x_offset) in text.chars().zip(x_offsets.iter()) { - if let Some(glyph) = font.uv_rect(c) { - let mut top_left = Vertex { - pos: pos + glyph.offset + vec2(*x_offset, 0.0), - uv: glyph.min, - color, - }; - top_left.pos.x = font.round_to_pixel(top_left.pos.x); // Pixel-perfection. - top_left.pos.y = font.round_to_pixel(top_left.pos.y); // Pixel-perfection. - let bottom_right = Vertex { - pos: top_left.pos + glyph.size, - uv: glyph.max, - color, - }; - out_mesh.add_rect(top_left, bottom_right); + let mut chars = galley.text.chars(); + for line in &galley.lines { + for x_offset in line.x_offsets.iter().take(line.x_offsets.len() - 1) { + let c = chars.next().unwrap(); + if let Some(glyph) = font.uv_rect(c) { + let mut top_left = Vertex { + pos: pos + glyph.offset + vec2(*x_offset, line.y_offset), + uv: glyph.min, + color, + }; + top_left.pos.x = font.round_to_pixel(top_left.pos.x); // Pixel-perfection. + top_left.pos.y = font.round_to_pixel(top_left.pos.y); // Pixel-perfection. + let bottom_right = Vertex { + pos: top_left.pos + glyph.size, + uv: glyph.max, + color, + }; + out_mesh.add_rect(top_left, bottom_right); + } } } + assert_eq!(chars.next(), None); } } } diff --git a/emigui/src/types.rs b/emigui/src/types.rs index 43b869d9..136fa3de 100644 --- a/emigui/src/types.rs +++ b/emigui/src/types.rs @@ -4,6 +4,7 @@ use serde_derive::{Deserialize, Serialize}; use crate::{ color::Color, + font::Galley, fonts::TextStyle, math::{Pos2, Rect}, mesher::{Mesh, Path}, @@ -153,14 +154,12 @@ pub enum PaintCmd { }, /// Paint a single line of text Text { - color: Color, /// Top left corner of the first character. pos: Pos2, - text: String, - text_style: TextStyle, // TODO: Font - /// Start each character in the text, as offset from pos. - x_offsets: Vec, - // TODO: font info + /// The layed out text + galley: Galley, + text_style: TextStyle, // TODO: Font? + color: Color, }, /// Low-level triangle mesh Mesh(Mesh), diff --git a/emigui/src/ui.rs b/emigui/src/ui.rs index 2757075c..0b63d3f4 100644 --- a/emigui/src/ui.rs +++ b/emigui/src/ui.rs @@ -1,6 +1,6 @@ use std::{hash::Hash, sync::Arc}; -use crate::{color::*, containers::*, font::Fragment, layout::*, widgets::*, *}; +use crate::{color::*, containers::*, layout::*, widgets::*, *}; /// Represents a region of the screen /// with a type of layout (horizontal or vertical). @@ -457,32 +457,29 @@ impl Ui { text_style: TextStyle, align: (Align, Align), text_color: Option, - ) -> Vec2 { + ) -> Rect { let font = &self.fonts()[text_style]; let galley = font.layout_multiline(text, f32::INFINITY); let rect = align_rect(Rect::from_min_size(pos, galley.size), align); - self.add_text(rect.min, text_style, galley.fragments, text_color); - galley.size + self.add_galley(rect.min, galley, text_style, text_color); + rect } /// Already layed out text. - pub fn add_text( + pub fn add_galley( &mut self, pos: Pos2, + galley: font::Galley, text_style: TextStyle, - fragments: Vec, color: Option, ) { let color = color.unwrap_or_else(|| self.style().text_color()); - for fragment in fragments { - self.add_paint_cmd(PaintCmd::Text { - color, - pos: pos + vec2(0.0, fragment.y_offset), - text: fragment.text, - text_style, - x_offsets: fragment.x_offsets, - }); - } + self.add_paint_cmd(PaintCmd::Text { + pos, + galley, + text_style, + color, + }); } // ------------------------------------------------------------------------ diff --git a/emigui/src/widgets.rs b/emigui/src/widgets.rs index e0d357fa..4bd1f446 100644 --- a/emigui/src/widgets.rs +++ b/emigui/src/widgets.rs @@ -97,12 +97,7 @@ impl Widget for Label { }; let galley = self.layout(max_width, ui); let interact = ui.reserve_space(galley.size, None); - ui.add_text( - interact.rect.min, - self.text_style, - galley.fragments, - self.text_color, - ); + ui.add_galley(interact.rect.min, galley, self.text_style, self.text_color); ui.response(interact) } } @@ -160,12 +155,12 @@ impl Widget for Hyperlink { if interact.hovered { // Underline: - for fragment in &galley.fragments { + for line in &galley.lines { let pos = interact.rect.min; - let y = pos.y + fragment.y_offset + line_spacing; + let y = pos.y + line.y_offset + line_spacing; let y = ui.round_to_pixel(y); - let min_x = pos.x + fragment.min_x(); - let max_x = pos.x + fragment.max_x(); + let min_x = pos.x + line.min_x(); + let max_x = pos.x + line.max_x(); ui.add_paint_cmd(PaintCmd::line_segment( [pos2(min_x, y), pos2(max_x, y)], color, @@ -174,7 +169,7 @@ impl Widget for Hyperlink { } } - ui.add_text(interact.rect.min, text_style, galley.fragments, Some(color)); + ui.add_galley(interact.rect.min, galley, text_style, Some(color)); ui.response(interact) } @@ -243,7 +238,7 @@ impl Widget for Button { }); let stroke_color = ui.style().interact(&interact).stroke_color; let text_color = text_color.unwrap_or(stroke_color); - ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color)); + ui.add_galley(text_cursor, galley, text_style, Some(text_color)); ui.response(interact) } } @@ -313,7 +308,7 @@ impl<'a> Widget for Checkbox<'a> { } let text_color = self.text_color.unwrap_or(stroke_color); - ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color)); + ui.add_galley(text_cursor, galley, text_style, Some(text_color)); ui.response(interact) } } @@ -384,7 +379,7 @@ impl Widget for RadioButton { } let text_color = self.text_color.unwrap_or(stroke_color); - ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color)); + ui.add_galley(text_cursor, galley, text_style, Some(text_color)); ui.response(interact) } } diff --git a/emigui/src/widgets/slider.rs b/emigui/src/widgets/slider.rs index f81eef9a..0c606a50 100644 --- a/emigui/src/widgets/slider.rs +++ b/emigui/src/widgets/slider.rs @@ -119,7 +119,7 @@ impl<'a> Widget for Slider<'a> { // let galley = font.layout_multiline(&full_text, ui.available().width()); let galley = font.layout_single_line(&full_text); let pos = ui.reserve_space(galley.size, None).rect.min; - ui.add_text(pos, text_style, galley.fragments, text_color); + ui.add_galley(pos, galley, text_style, text_color); slider_sans_text.ui(ui) } else { ui.columns(2, |columns| { diff --git a/emigui/src/widgets/text_edit.rs b/emigui/src/widgets/text_edit.rs index 0c820c94..2ab6db03 100644 --- a/emigui/src/widgets/text_edit.rs +++ b/emigui/src/widgets/text_edit.rs @@ -90,7 +90,7 @@ impl<'t> Widget for TextEdit<'t> { let show_cursor = (ui.input().time * cursor_blink_hz as f64 * 3.0).floor() as i64 % 3 != 0; if show_cursor { - let cursor_pos = if let Some(last) = galley.fragments.last() { + let cursor_pos = if let Some(last) = galley.lines.last() { interact.rect.min + vec2(last.max_x(), last.y_offset) } else { interact.rect.min @@ -103,12 +103,7 @@ impl<'t> Widget for TextEdit<'t> { } } - ui.add_text( - interact.rect.min, - self.text_style, - galley.fragments, - self.text_color, - ); + ui.add_galley(interact.rect.min, galley, self.text_style, self.text_color); ui.response(interact) }