diff --git a/emgui/src/font.rs b/emgui/src/font.rs index d0003586..eae8ccca 100644 --- a/emgui/src/font.rs +++ b/emgui/src/font.rs @@ -1,5 +1,27 @@ use rusttype::{point, Scale}; +use crate::math::{vec2, Vec2}; + +pub struct TextFragment { + /// The start of each character, starting at zero. + pub x_offsets: Vec, + /// 0 for the first line, n * line_spacing for the rest + pub y_offset: f32, + pub text: String, +} + +impl TextFragment { + pub fn min_x(&self) -> f32 { + *self.x_offsets.first().unwrap() + } + + pub fn max_x(&self) -> f32 { + *self.x_offsets.last().unwrap() + } +} + +// ---------------------------------------------------------------------------- + #[derive(Clone, Copy, Debug, PartialEq)] pub struct UvRect { /// X/Y offset for nice rendering @@ -190,27 +212,109 @@ impl Font { } } - /// Returns the start (X) of each character, starting at zero, plus the total width. - /// i.e. returns text.chars().count() + 1 numbers. - pub fn layout_single_line(&self, text: &str) -> Vec { + /// Returns the a single line of characters separated into words + pub fn layout_single_line(&self, text: &str) -> Vec { let scale = Scale::uniform(self.scale as f32); - let mut x_offsets = Vec::new(); + let mut current_fragment = TextFragment { + x_offsets: vec![0.0], + y_offset: 0.0, + text: String::new(), + }; + let mut all_fragments = vec![]; let mut cursor_x = 0.0f32; let mut last_glyph_id = None; + for c in text.chars() { - cursor_x = cursor_x.round(); - x_offsets.push(cursor_x); if let Some(glyph) = self.glyph_info(c) { if let Some(last_glyph_id) = last_glyph_id { cursor_x += self.font.pair_kerning(scale, last_glyph_id, glyph.id) } cursor_x += glyph.advance_width; + cursor_x = cursor_x.round(); last_glyph_id = Some(glyph.id); + + let is_space = glyph.uv.is_none(); + if is_space { + // TODO: also break after hyphens etc + if !current_fragment.text.is_empty() { + all_fragments.push(current_fragment); + current_fragment = TextFragment { + x_offsets: vec![cursor_x], + y_offset: 0.0, + text: String::new(), + } + } + } else { + current_fragment.text.push(c); + current_fragment.x_offsets.push(cursor_x); + } + } else { + // Ignore unknown glyph } } - x_offsets.push(cursor_x); - x_offsets + + if !current_fragment.text.is_empty() { + all_fragments.push(current_fragment) + } + all_fragments + } + + pub fn layout_single_line_max_width(&self, text: &str, max_width: f32) -> Vec { + let mut words = self.layout_single_line(text); + if words.is_empty() || words.last().unwrap().max_x() <= max_width { + return words; // Early-out + } + + let line_spacing = self.line_spacing(); + + // Break up lines: + let mut line_start_x = 0.0; + let mut cursor_y = 0.0; + + for word in words.iter_mut().skip(1) { + if word.max_x() - line_start_x >= max_width { + // Time for a new line: + cursor_y += line_spacing; + line_start_x = word.min_x(); + } + + word.y_offset += cursor_y; + for x in &mut word.x_offsets { + *x -= line_start_x; + } + } + + words + } + + /// Returns each line + total bounding box size. + pub fn layout_multiline(&self, text: &str, max_width: f32) -> (Vec, Vec2) { + let line_spacing = self.line_spacing(); + let mut cursor_y = 0.0; + let mut text_fragments = Vec::new(); + for line in text.split('\n') { + let mut line_fragments = self.layout_single_line_max_width(&line, max_width); + if let Some(last_word) = line_fragments.last() { + let line_height = last_word.y_offset + line_spacing; + for fragment in &mut line_fragments { + fragment.y_offset += cursor_y; + } + text_fragments.append(&mut line_fragments); + cursor_y += line_height; // TODO: add extra spacing between paragraphs + } else { + cursor_y += line_spacing; + } + cursor_y = cursor_y.round(); + } + + let mut widest_line = 0.0; + for fragment in &text_fragments { + widest_line = fragment.max_x().max(widest_line); + } + + let bounding_size = vec2(widest_line, cursor_y); + (text_fragments, bounding_size) } pub fn debug_print_atlas_ascii_art(&self) { diff --git a/emgui/src/layout.rs b/emgui/src/layout.rs index 608579db..04d02150 100644 --- a/emgui/src/layout.rs +++ b/emgui/src/layout.rs @@ -5,7 +5,7 @@ use std::{ }; use crate::{ - font::Font, + font::{Font, TextFragment}, math::*, types::*, widgets::{label, Widget}, @@ -95,18 +95,6 @@ pub struct Memory { // ---------------------------------------------------------------------------- -pub struct TextFragment { - /// The start of each character, starting at zero. - x_offsets: Vec, - /// 0 for the first line, n * line_spacing for the rest - y_offset: f32, - text: String, -} - -pub type TextFragments = Vec; - -// ---------------------------------------------------------------------------- - #[derive(Clone, Copy, Debug, PartialEq)] pub enum Direction { Horizontal, @@ -220,7 +208,7 @@ where dir: Direction::Vertical, cursor: window_pos + window_padding, bounding_size: vec2(0.0, 0.0), - available_space: vec2(400.0, std::f32::INFINITY), // TODO + available_space: vec2(400.0, std::f32::INFINITY), // TODO: popup/tooltip width }; add_contents(&mut popup_region); @@ -283,6 +271,14 @@ impl Region { self.cursor } + pub fn font(&self) -> &Font { + &*self.data.font + } + + pub fn width(&self) -> f32 { + self.available_space.x + } + // ------------------------------------------------------------------------ // Sub-regions: @@ -297,7 +293,7 @@ impl Region { ); let text: String = text.into(); let id = self.make_child_id(&text); - let (text, text_size) = self.layout_text(&text); + let (text, text_size) = self.font().layout_multiline(&text, self.width()); let text_cursor = self.cursor + self.options().button_padding; let (rect, interact) = self.reserve_space( vec2( @@ -434,7 +430,6 @@ impl Region { // ------------------------------------------------------------------------ - // TODO: Return a Rect pub fn reserve_space( &mut self, size: Vec2, @@ -499,28 +494,6 @@ impl Region { }) } - // TODO: move this function to Font - pub fn layout_text(&self, text: &str) -> (TextFragments, Vec2) { - let line_spacing = self.data.font.line_spacing(); - let mut cursor_y = 0.0; - let mut max_width = 0.0; - let mut text_fragments = Vec::new(); - for line in text.split('\n') { - let x_offsets = self.data.font.layout_single_line(&line); - let line_width = *x_offsets.last().unwrap(); - text_fragments.push(TextFragment { - x_offsets, - y_offset: cursor_y, - text: line.into(), - }); - - cursor_y += line_spacing; - max_width = line_width.max(max_width); - } - let bounding_size = vec2(max_width, cursor_y); - (text_fragments, bounding_size) - } - pub fn add_text(&mut self, pos: Vec2, text: Vec) { for fragment in text { self.add_graphic(GuiCmd::Text { diff --git a/emgui/src/widgets.rs b/emgui/src/widgets.rs index 2d6e5ff8..60416a2c 100644 --- a/emgui/src/widgets.rs +++ b/emgui/src/widgets.rs @@ -29,7 +29,7 @@ pub fn label>(text: S) -> Label { impl Widget for Label { fn add_to(self, region: &mut Region) -> GuiResponse { - let (text, text_size) = region.layout_text(&self.text); + let (text, text_size) = region.font().layout_multiline(&self.text, region.width()); region.add_text(region.cursor(), text); let (_, interact) = region.reserve_space(text_size, None); region.response(interact) @@ -51,7 +51,7 @@ impl Button { impl Widget for Button { fn add_to(self, region: &mut Region) -> GuiResponse { let id = region.make_child_id(&self.text); - let (text, text_size) = region.layout_text(&self.text); + let (text, text_size) = region.font().layout_multiline(&self.text, region.width()); let text_cursor = region.cursor() + region.options().button_padding; let (rect, interact) = region.reserve_space(text_size + 2.0 * region.options().button_padding, Some(id)); @@ -81,7 +81,7 @@ impl<'a> Checkbox<'a> { impl<'a> Widget for Checkbox<'a> { fn add_to(self, region: &mut Region) -> GuiResponse { let id = region.make_child_id(&self.text); - let (text, text_size) = region.layout_text(&self.text); + let (text, text_size) = region.font().layout_multiline(&self.text, region.width()); let text_cursor = region.cursor() + region.options().button_padding + vec2(region.options().start_icon_width, 0.0); @@ -129,7 +129,7 @@ pub fn radio>(checked: bool, text: S) -> RadioButton { impl Widget for RadioButton { fn add_to(self, region: &mut Region) -> GuiResponse { let id = region.make_child_id(&self.text); - let (text, text_size) = region.layout_text(&self.text); + let (text, text_size) = region.font().layout_multiline(&self.text, region.width()); let text_cursor = region.cursor() + region.options().button_padding + vec2(region.options().start_icon_width, 0.0); @@ -196,7 +196,7 @@ impl<'a> Widget for Slider<'a> { naked.text = None; if text_on_top { - let (text, text_size) = region.layout_text(&full_text); + let (text, text_size) = region.font().layout_multiline(&full_text, region.width()); region.add_text(region.cursor(), text); region.reserve_space_inner(text_size); naked.add_to(region)