diff --git a/emigui/src/containers/collapsing_header.rs b/emigui/src/containers/collapsing_header.rs index 9a2ad29d..0e33ac77 100644 --- a/emigui/src/containers/collapsing_header.rs +++ b/emigui/src/containers/collapsing_header.rs @@ -61,18 +61,18 @@ impl CollapsingHeader { let available = ui.available_finite(); let text_pos = available.min + vec2(ui.style().indent, 0.0); - let (text, text_size) = label.layout(available.width() - ui.style().indent, ui); - let text_max_x = text_pos.x + text_size.x; + let galley = label.layout(available.width() - ui.style().indent, ui); + let text_max_x = text_pos.x + galley.size.x; let desired_width = available.width().max(text_max_x - available.left()); let interact = ui.reserve_space( vec2( desired_width, - text_size.y + 2.0 * ui.style().button_padding.y, + galley.size.y + 2.0 * ui.style().button_padding.y, ), Some(id), ); - let text_pos = pos2(text_pos.x, interact.rect.center().y - text_size.y / 2.0); + let text_pos = pos2(text_pos.x, interact.rect.center().y - galley.size.y / 2.0); let mut state = { let mut memory = ui.memory(); @@ -94,7 +94,7 @@ impl CollapsingHeader { ui.add_text( text_pos, label.text_style, - text, + galley.fragments, Some(ui.style().interact(&interact).stroke_color), ); diff --git a/emigui/src/context.rs b/emigui/src/context.rs index 68723de2..914c2ba0 100644 --- a/emigui/src/context.rs +++ b/emigui/src/context.rs @@ -359,8 +359,8 @@ impl Context { let layer = Layer::debug(); let text_style = TextStyle::Monospace; let font = &self.fonts[text_style]; - let (text, size) = font.layout_multiline(text, f32::INFINITY); - let rect = align_rect(Rect::from_min_size(pos, size), align); + let galley = font.layout_multiline(text, f32::INFINITY); + let rect = align_rect(Rect::from_min_size(pos, galley.size), align); self.add_paint_cmd( layer, PaintCmd::Rect { @@ -370,7 +370,13 @@ impl Context { rect: rect.expand(2.0), }, ); - self.add_text(layer, rect.min, text_style, text, Some(color::RED)); + self.add_text( + layer, + rect.min, + text_style, + galley.fragments, + Some(color::RED), + ); } pub fn debug_text(&self, pos: Pos2, text: &str) { @@ -414,10 +420,10 @@ impl Context { text_color: Option, ) -> Vec2 { let font = &self.fonts[text_style]; - let (text, size) = font.layout_multiline(text, f32::INFINITY); - let rect = align_rect(Rect::from_min_size(pos, size), align); - self.add_text(layer, rect.min, text_style, text, text_color); - size + 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 } /// Already layed out text. @@ -426,7 +432,7 @@ impl Context { layer: Layer, pos: Pos2, text_style: TextStyle, - text: Vec, + text: Vec, color: Option, ) { let color = color.unwrap_or_else(|| self.style().text_color()); diff --git a/emigui/src/font.rs b/emigui/src/font.rs index ac5ed690..7fca9b3d 100644 --- a/emigui/src/font.rs +++ b/emigui/src/font.rs @@ -8,7 +8,8 @@ use crate::{ texture_atlas::TextureAtlas, }; -pub struct TextFragment { +/// A typeset piece of text on a single line. Could be a whole line, or just a word. +pub struct Fragment { /// The start of each character, starting at zero. /// Unit: points. pub x_offsets: Vec, @@ -16,10 +17,12 @@ pub struct TextFragment { /// 0 for the first line, n * line_spacing for the rest /// Unit: points. pub y_offset: f32, + + /// The actual characters pub text: String, } -impl TextFragment { +impl Fragment { pub fn min_x(&self) -> f32 { *self.x_offsets.first().unwrap() } @@ -29,7 +32,7 @@ impl TextFragment { } } -// pub fn fn_text_width(fragmens: &[TextFragment]) -> f32 { +// pub fn fn_text_width(fragmens: &[Fragment]) -> f32 { // if fragmens.is_empty() { // 0.0 // } else { @@ -37,6 +40,12 @@ impl TextFragment { // } // } +/// A collection of text locked into place. +pub struct Galley { + pub fragments: Vec, + pub size: Vec2, +} + // ---------------------------------------------------------------------------- #[derive(Clone, Copy, Debug)] @@ -184,17 +193,16 @@ impl Font { } /// Returns the a single line of characters separated into words - /// Always returns at least one frament. TODO: Vec1 - /// Returns total size. - pub fn layout_single_line(&self, text: &str) -> (Vec, Vec2) { + /// Always returns at least one frament. + fn layout_words(&self, text: &str) -> Galley { let scale_in_pixels = Scale::uniform(self.scale_in_pixels); - let mut current_fragment = TextFragment { + let mut current_fragment = Fragment { x_offsets: vec![0.0], y_offset: 0.0, text: String::new(), }; - let mut all_fragments = vec![]; + let mut fragments = vec![]; let mut cursor_x_in_points = 0.0f32; let mut last_glyph_id = None; @@ -214,13 +222,14 @@ impl Font { if is_space { // TODO: also break after hyphens etc if !current_fragment.text.is_empty() { - all_fragments.push(current_fragment); - current_fragment = TextFragment { + fragments.push(current_fragment); + current_fragment = Fragment { x_offsets: vec![cursor_x_in_points], y_offset: 0.0, text: String::new(), } } + // TODO: add a fragment for the space aswell } else { current_fragment.text.push(c); current_fragment.x_offsets.push(cursor_x_in_points); @@ -231,29 +240,39 @@ impl Font { } if !current_fragment.text.is_empty() { - all_fragments.push(current_fragment) + fragments.push(current_fragment) } - let width = if all_fragments.is_empty() { + let width = if fragments.is_empty() { 0.0 } else { - all_fragments.last().unwrap().max_x() + fragments.last().unwrap().max_x() }; let size = vec2(width, self.height()); - (all_fragments, size) + Galley { fragments, size } + } + + /// Typeset the given text onto one line. + /// Always returns at least one frament. + pub fn layout_single_line(&self, text: &str) -> Galley { + // TODO: return a single Fragment instead of calling layout_words + // saves a lot of allocations + self.layout_words(text) } /// 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 { - let (mut words, size) = self.layout_single_line(text); - if words.is_empty() || size.x <= max_width_in_points { - return words; // Early-out + ) -> Vec { + let mut galley = self.layout_words(text); + if galley.fragments.is_empty() || galley.size.x <= max_width_in_points { + return galley.fragments; // Early-out } let line_spacing = self.line_spacing(); @@ -262,7 +281,7 @@ impl Font { let mut line_start_x = 0.0; let mut cursor_y = 0.0; - for word in words.iter_mut().skip(1) { + for word in galley.fragments.iter_mut().skip(1) { if word.max_x() - line_start_x >= max_width_in_points { // Time for a new line: cursor_y += line_spacing; @@ -275,18 +294,13 @@ impl Font { } } - words + galley.fragments } - /// Returns each line + total bounding box size. - pub fn layout_multiline( - &self, - text: &str, - max_width_in_points: f32, - ) -> (Vec, Vec2) { + 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 text_fragments = Vec::new(); + let mut fragments = Vec::new(); for line in text.split('\n') { let mut line_fragments = self.layout_paragraph_max_width(line, max_width_in_points); if let Some(last_word) = line_fragments.last() { @@ -294,7 +308,7 @@ impl Font { for fragment in &mut line_fragments { fragment.y_offset += cursor_y; } - text_fragments.append(&mut line_fragments); + fragments.append(&mut line_fragments); cursor_y += line_height; // TODO: add extra spacing between paragraphs } else { cursor_y += line_spacing; @@ -303,11 +317,13 @@ impl Font { } let mut widest_line = 0.0; - for fragment in &text_fragments { + for fragment in &fragments { widest_line = fragment.max_x().max(widest_line); } - let bounding_size = vec2(widest_line, cursor_y); - (text_fragments, bounding_size) + Galley { + fragments, + size: vec2(widest_line, cursor_y), + } } } diff --git a/emigui/src/ui.rs b/emigui/src/ui.rs index 2a984680..2757075c 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::TextFragment, layout::*, widgets::*, *}; +use crate::{color::*, containers::*, font::Fragment, layout::*, widgets::*, *}; /// Represents a region of the screen /// with a type of layout (horizontal or vertical). @@ -459,10 +459,10 @@ impl Ui { text_color: Option, ) -> Vec2 { let font = &self.fonts()[text_style]; - let (text, size) = font.layout_multiline(text, f32::INFINITY); - let rect = align_rect(Rect::from_min_size(pos, size), align); - self.add_text(rect.min, text_style, text, text_color); - size + 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 } /// Already layed out text. @@ -470,11 +470,11 @@ impl Ui { &mut self, pos: Pos2, text_style: TextStyle, - text: Vec, + fragments: Vec, color: Option, ) { let color = color.unwrap_or_else(|| self.style().text_color()); - for fragment in text { + for fragment in fragments { self.add_paint_cmd(PaintCmd::Text { color, pos: pos + vec2(0.0, fragment.y_offset), diff --git a/emigui/src/widgets.rs b/emigui/src/widgets.rs index 42e087f4..d84e54e4 100644 --- a/emigui/src/widgets.rs +++ b/emigui/src/widgets.rs @@ -63,7 +63,7 @@ impl Label { self } - pub fn layout(&self, max_width: f32, ui: &Ui) -> (Vec, Vec2) { + pub fn layout(&self, max_width: f32, ui: &Ui) -> font::Galley { let font = &ui.fonts()[self.text_style]; if self.multiline { font.layout_multiline(&self.text, max_width) @@ -95,9 +95,14 @@ impl Widget for Label { } else { ui.available().width() }; - let (text, text_size) = self.layout(max_width, ui); - let interact = ui.reserve_space(text_size, None); - ui.add_text(interact.rect.min, self.text_style, text, self.text_color); + 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.response(interact) } } @@ -145,8 +150,8 @@ impl Widget for Hyperlink { let font = &ui.fonts()[text_style]; let line_spacing = font.line_spacing(); // TODO: underline - let (text, text_size) = font.layout_multiline(&self.text, ui.available().width()); - let interact = ui.reserve_space(text_size, Some(id)); + let galley = font.layout_multiline(&self.text, ui.available().width()); + let interact = ui.reserve_space(galley.size, Some(id)); if interact.hovered { ui.ctx().output().cursor_icon = CursorIcon::PointingHand; } @@ -157,7 +162,7 @@ impl Widget for Hyperlink { if interact.hovered { // Underline: // TODO: underline spaces between words too. - for fragment in &text { + for fragment in &galley.fragments { let pos = interact.rect.min; let y = pos.y + fragment.y_offset + line_spacing; let y = ui.round_to_pixel(y); @@ -171,7 +176,7 @@ impl Widget for Hyperlink { } } - ui.add_text(interact.rect.min, text_style, text, Some(color)); + ui.add_text(interact.rect.min, text_style, galley.fragments, Some(color)); ui.response(interact) } @@ -224,12 +229,12 @@ impl Widget for Button { let id = ui.make_position_id(); let font = &ui.fonts()[text_style]; - let (text, text_size) = font.layout_multiline(&text, ui.available().width()); + let galley = font.layout_multiline(&text, ui.available().width()); let padding = ui.style().button_padding; - let mut size = text_size + 2.0 * padding; + let mut size = galley.size + 2.0 * padding; size.y = size.y.max(ui.style().clickable_diameter); let interact = ui.reserve_space(size, Some(id)); - let mut text_cursor = interact.rect.left_center() + vec2(padding.x, -0.5 * text_size.y); + let mut text_cursor = interact.rect.left_center() + vec2(padding.x, -0.5 * galley.size.y); text_cursor.y += 2.0; // TODO: why is this needed? let fill_color = fill_color.or(ui.style().interact(&interact).fill_color); ui.add_paint_cmd(PaintCmd::Rect { @@ -240,7 +245,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, text, Some(text_color)); + ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color)); ui.response(interact) } } @@ -274,11 +279,11 @@ impl<'a> Widget for Checkbox<'a> { let id = ui.make_position_id(); let text_style = TextStyle::Button; let font = &ui.fonts()[text_style]; - let (text, text_size) = font.layout_single_line(&self.text); + let galley = font.layout_single_line(&self.text); let interact = ui.reserve_space( ui.style().button_padding + vec2(ui.style().start_icon_width, 0.0) - + text_size + + galley.size + ui.style().button_padding, Some(id), ); @@ -310,7 +315,7 @@ impl<'a> Widget for Checkbox<'a> { } let text_color = self.text_color.unwrap_or(stroke_color); - ui.add_text(text_cursor, text_style, text, Some(text_color)); + ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color)); ui.response(interact) } } @@ -348,11 +353,11 @@ impl Widget for RadioButton { let id = ui.make_position_id(); let text_style = TextStyle::Button; let font = &ui.fonts()[text_style]; - let (text, text_size) = font.layout_multiline(&self.text, ui.available().width()); + let galley = font.layout_multiline(&self.text, ui.available().width()); let interact = ui.reserve_space( ui.style().button_padding + vec2(ui.style().start_icon_width, 0.0) - + text_size + + galley.size + ui.style().button_padding, Some(id), ); @@ -381,7 +386,7 @@ impl Widget for RadioButton { } let text_color = self.text_color.unwrap_or(stroke_color); - ui.add_text(text_cursor, text_style, text, Some(text_color)); + ui.add_text(text_cursor, text_style, galley.fragments, Some(text_color)); ui.response(interact) } } diff --git a/emigui/src/widgets/slider.rs b/emigui/src/widgets/slider.rs index 657bdf08..f81eef9a 100644 --- a/emigui/src/widgets/slider.rs +++ b/emigui/src/widgets/slider.rs @@ -116,10 +116,10 @@ impl<'a> Widget for Slider<'a> { let slider_sans_text = Slider { text: None, ..self }; if text_on_top { - // let (text, text_size) = font.layout_multiline(&full_text, ui.available().width()); - let (text, text_size) = font.layout_single_line(&full_text); - let pos = ui.reserve_space(text_size, None).rect.min; - ui.add_text(pos, text_style, text, text_color); + // 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); 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 4950c6b9..0c820c94 100644 --- a/emigui/src/widgets/text_edit.rs +++ b/emigui/src/widgets/text_edit.rs @@ -40,8 +40,8 @@ impl<'t> Widget for TextEdit<'t> { let font = &ui.fonts()[self.text_style]; let line_spacing = font.line_spacing(); - let (text, text_size) = font.layout_multiline(self.text.as_str(), ui.available().width()); - let desired_size = text_size.max(vec2(ui.available().width(), line_spacing)); + let galley = font.layout_multiline(self.text.as_str(), ui.available().width()); + let desired_size = galley.size.max(vec2(ui.available().width(), line_spacing)); let interact = ui.reserve_space(desired_size, Some(id)); if interact.clicked { @@ -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) = text.last() { + let cursor_pos = if let Some(last) = galley.fragments.last() { interact.rect.min + vec2(last.max_x(), last.y_offset) } else { interact.rect.min @@ -103,7 +103,12 @@ impl<'t> Widget for TextEdit<'t> { } } - ui.add_text(interact.rect.min, self.text_style, text, self.text_color); + ui.add_text( + interact.rect.min, + self.text_style, + galley.fragments, + self.text_color, + ); ui.response(interact) }