diff --git a/Cargo.lock b/Cargo.lock index 882d7185363492a4d3a9a82c533ef95da131da8a..07e9a91959a4ec5aeb54a9e061252368272a05ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1884,6 +1884,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "unicode-segmentation", + "xi-unicode", ] [[package]] diff --git a/bevy_kayak_ui/src/render/font/extract.rs b/bevy_kayak_ui/src/render/font/extract.rs index 23d37fac30c190edc20b912435f91de4f419ffb4..54c6f2ab51c09509d3a7c0d04505acfc7bc9feeb 100644 --- a/bevy_kayak_ui/src/render/font/extract.rs +++ b/bevy_kayak_ui/src/render/font/extract.rs @@ -4,7 +4,7 @@ use bevy::{ sprite::Rect, }; use kayak_core::render_primitive::RenderPrimitive; -use kayak_font::{Alignment, CoordinateSystem, KayakFont}; +use kayak_font::{Alignment, KayakFont, TextProperties}; use crate::to_bevy_color; use bevy_kayak_renderer::{ @@ -52,19 +52,27 @@ pub fn extract_texts( let font = font.unwrap(); - let chars_layouts = font.get_layout( - CoordinateSystem::PositiveYDown, - Alignment::Start, - (layout.posx, layout.posy + font_size), - (parent_size.0, parent_size.1), - content, - *line_height, + let properties = TextProperties { + alignment: Alignment::Start, font_size, + line_height: *line_height, + max_size: (parent_size.0, parent_size.1), + ..Default::default() + }; + + let text_layout = font.measure( + content, + properties, ); - for char_layout in chars_layouts { - let position = char_layout.position.into(); - let size: Vec2 = char_layout.size.into(); + let base_position = Vec2::new(layout.posx, layout.posy + font_size); + + for glyph_rect in text_layout.glyphs() { + let mut position = Vec2::from(glyph_rect.position); + position += base_position; + + let size = Vec2::from(glyph_rect.size); + extracted_texts.push(ExtractQuadBundle { extracted_quad: ExtractedQuad { font_handle: Some(font_handle.clone()), @@ -74,7 +82,7 @@ pub fn extract_texts( }, color: to_bevy_color(background_color), vertex_index: 0, - char_id: font.get_char_id(char_layout.content).unwrap(), + char_id: font.get_char_id(glyph_rect.content).unwrap(), z_index: layout.z_index, quad_type: UIQuadType::Text, type_index: 0, diff --git a/kayak_font/Cargo.toml b/kayak_font/Cargo.toml index 04190d61d9fc1eac031b89b02403b6c82c0da8c3..f080a4381ac77bcdb9fff3d26277386ad17ff1af 100644 --- a/kayak_font/Cargo.toml +++ b/kayak_font/Cargo.toml @@ -17,3 +17,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_path_to_error = "0.1" unicode-segmentation = "1.9" + +# Provides UAX #14 line break segmentation +xi-unicode = "0.3" diff --git a/kayak_font/examples/renderer/extract.rs b/kayak_font/examples/renderer/extract.rs index ae3ff2429d93882818c208b86cf4d6bb4adf23ff..5afe1c494450877bf0856e1c54cfb49941c0e43b 100644 --- a/kayak_font/examples/renderer/extract.rs +++ b/kayak_font/examples/renderer/extract.rs @@ -3,7 +3,7 @@ use bevy::{ prelude::{Assets, Commands, Handle, Query, Res}, sprite::Rect, }; -use kayak_font::{CoordinateSystem, KayakFont}; +use kayak_font::{Alignment, KayakFont, TextProperties}; use super::{ pipeline::{ExtractCharBundle, ExtractedChar}, @@ -19,19 +19,26 @@ pub fn extract( for (text, font_handle) in texts.iter() { if let Some(font) = fonts.get(font_handle) { - let layouts = font.get_layout( - CoordinateSystem::PositiveYUp, - text.horz_alignment, - (text.position.x, text.position.y), - (text.size.x, text.size.y), + + let properties = TextProperties { + font_size: text.font_size, + line_height: text.line_height, + max_size: (text.size.x, text.size.y), + alignment: text.horz_alignment, + ..Default::default() + }; + + let text_layout = font.measure( &text.content, - text.line_height, - text.font_size, + properties ); - for layout in layouts { - let position = layout.position.into(); - let size: Vec2 = layout.size.into(); + for glyph_rect in text_layout.glyphs() { + let mut position = Vec2::from(glyph_rect.position); + position.y *= -1.0; + position += text.position; + + let size = Vec2::from(glyph_rect.size); extracted_texts.push(ExtractCharBundle { extracted_quad: ExtractedChar { @@ -42,7 +49,7 @@ pub fn extract( }, color: text.color, vertex_index: 0, - char_id: font.get_char_id(layout.content).unwrap(), + char_id: font.get_char_id(glyph_rect.content).unwrap(), z_index: 0.0, }, }); diff --git a/kayak_font/examples/renderer/text.rs b/kayak_font/examples/renderer/text.rs index 32f92a28f43c15bfa570d0968192aad7b2c3b1de..21832d9bb7a4bfe4c2be2f4f9f85acd969a77674 100644 --- a/kayak_font/examples/renderer/text.rs +++ b/kayak_font/examples/renderer/text.rs @@ -1,5 +1,5 @@ use bevy::{math::Vec2, prelude::Component, render::color::Color}; -use kayak_font::layout::Alignment; +use kayak_font::Alignment; #[derive(Component)] pub struct Text { diff --git a/kayak_font/src/atlas.rs b/kayak_font/src/atlas.rs index cb90b9512ef44dee491d09fe3ec7bd3cf1ff23a6..97e6f5421b7f42a9879a8d1c77502d8d6be67be2 100644 --- a/kayak_font/src/atlas.rs +++ b/kayak_font/src/atlas.rs @@ -24,7 +24,8 @@ pub struct Atlas { pub sdf_type: SDFType, #[serde(alias = "distanceRange")] pub distance_range: f32, - pub size: f32, + #[serde(alias = "size")] + pub font_size: f32, pub width: u32, pub height: u32, #[serde(alias = "yOrigin")] diff --git a/kayak_font/src/font.rs b/kayak_font/src/font.rs index 6aeabdf0ea27c5914a5887b0835e9c8c687debbf..0ffda87ec2eaedbb84826d96530e821b7e275d38 100644 --- a/kayak_font/src/font.rs +++ b/kayak_font/src/font.rs @@ -1,11 +1,15 @@ use std::collections::HashMap; +use std::iter::Peekable; #[cfg(feature = "bevy_renderer")] use bevy::{prelude::Handle, reflect::TypeUuid, render::texture::Image}; +use serde_json::de::Read; use unicode_segmentation::UnicodeSegmentation; +use xi_unicode::LineBreakIterator; use crate::layout::{Alignment, Line, TextLayout}; -use crate::{utility, Sdf, TextProperties}; +use crate::{utility, Sdf, TextProperties, Glyph, GlyphRect}; +use crate::utility::{BreakableWord, BreakableWordIter, SPACE}; #[cfg(feature = "bevy_renderer")] #[derive(Debug, Clone, TypeUuid, PartialEq)] @@ -14,6 +18,7 @@ pub struct KayakFont { pub sdf: Sdf, pub atlas_image: Handle<Image>, char_ids: HashMap<char, u32>, + max_glyph_size: (f32, f32), } #[cfg(not(feature = "bevy_renderer"))] @@ -21,28 +26,36 @@ pub struct KayakFont { pub struct KayakFont { pub sdf: Sdf, char_ids: HashMap<char, u32>, + max_glyph_size: (f32, f32), } -#[derive(Default, Debug, Clone, Copy)] -pub struct LayoutRect { - pub position: (f32, f32), - pub size: (f32, f32), - pub content: char, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum CoordinateSystem { - PositiveYUp, - PositiveYDown, -} +// TODO: Remove me +// #[derive(Default, Debug, Clone, Copy)] +// pub struct LayoutRect { +// pub position: (f32, f32), +// pub size: (f32, f32), +// pub content: char, +// } + +// TODO: Remove me (?) +// #[derive(Debug, Clone, Copy, PartialEq)] +// pub enum CoordinateSystem { +// PositiveYUp, +// PositiveYDown, +// } impl KayakFont { pub fn new(sdf: Sdf, #[cfg(feature = "bevy_renderer")] atlas_image: Handle<Image>) -> Self { + let max_glyph_size = sdf.max_glyph_size(); + assert!(sdf.glyphs.len() < u32::MAX as usize, "SDF contains too many glyphs"); + let char_ids = sdf.glyphs.iter().enumerate().map(|(idx, glyph)| (glyph.unicode, idx as u32)).collect(); + Self { sdf, #[cfg(feature = "bevy_renderer")] atlas_image, - char_ids: HashMap::default(), + char_ids, + max_glyph_size, } } @@ -58,22 +71,18 @@ impl KayakFont { self.char_ids.get(&c).and_then(|id| Some(*id)) } - pub fn get_word_width(&self, word: &str, font_size: f32) -> f32 { + pub fn get_word_width(&self, word: &str, properties: TextProperties) -> f32 { + let space_width = self.get_space_width(properties); + let tab_width = self.get_tab_width(properties); + let mut width = 0.0; for c in word.chars() { - if let Some(glyph) = self.sdf.glyphs.iter().find(|glyph| glyph.unicode == c) { - let plane_bounds = glyph.plane_bounds.as_ref(); - let (_, _, char_width, _) = match plane_bounds { - Some(val) => ( - val.left, - val.top, - val.size().0 * font_size, - val.size().1 * font_size, - ), - None => (0.0, 0.0, 0.0, 0.0), - }; - - width += char_width; + if utility::is_space(c) { + width += space_width; + } else if utility::is_tab(c) { + width += tab_width; + } else if let Some(glyph) = self.get_glyph(c) { + width += glyph.advance * properties.font_size; } } @@ -89,11 +98,48 @@ impl KayakFont { /// * `properties`: The text properties to use. /// pub fn measure(&self, content: &str, properties: TextProperties) -> TextLayout { + let space_width = self.get_space_width(properties); + let tab_width = self.get_tab_width(properties); + let mut size: (f32, f32) = (0.0, 0.0); + let mut glyph_rects = Vec::new(); let mut lines = Vec::new(); + // The current line being calculated let mut line = Line::default(); + // The current grapheme cluster index let mut grapheme_index = 0; + // The current character index + let mut char_index = 0; + + // This is the normalized glyph bounds for all glyphs in the atlas. + // It's needed to ensure all glyphs render proportional to each other. + let norm_glyph_bounds = self.calc_glyph_size(properties.font_size); + + // The word index to break a line before + let mut break_index = None; + // The word index until attempting to find another line break + let mut skip_until_index = None; + + /// Local function to apply the line break, if any + fn try_break_line(index: usize, char_index: usize, grapheme_index: usize, line: &mut Line, lines: &mut Vec<Line>, break_index: &mut Option<usize>) { + if let Some(idx) = break_index { + if *idx == index { + add_line(char_index, grapheme_index, line, lines); + *break_index = None; + } + } + } + + /// Local function to finalize the current line and start a new one + fn add_line(char_index: usize, grapheme_index: usize, line: &mut Line, lines: &mut Vec<Line>) { + lines.push(*line); + *line = Line { + grapheme_index, + char_index, + ..Default::default() + }; + } // We'll now split up the text content so that we can measure the layout. // This is the "text pipeline" for this function: @@ -104,163 +150,341 @@ impl KayakFont { // 3. Process each character within the grapheme cluster. // // FIXME: I think #3 is wrong— we probably need to process the full grapheme cluster - // rather than each character individually,— however, this can probably be - // addressed later. Once resolved, this comment should be updated accordingly. - - for word in content.split_word_bounds() { - let word_width = self.get_word_width(word, properties.font_size); - - // === Confine to Bounds === // - if let Some((max_width, _)) = properties.max_size { - if line.width + word_width > max_width { - // Word exceeds bounds -> New line - lines.push(line); - line = Line { - index: grapheme_index, - ..Default::default() - }; + // rather than each character individually,— however, this might take some + // careful thought and consideration, so it should probably be addressed later. + // Once resolved, this comment should be updated accordingly. + + let mut words = utility::split_breakable_words(content).collect::<Vec<_>>(); + for (index, word) in words.iter().enumerate() { + + // === Line Break === // + // If the `break_index` is set, apply it. + try_break_line(index, char_index, grapheme_index, &mut line, &mut lines, &mut break_index); + if break_index.is_none() { + match skip_until_index { + Some(idx) if index < idx => { + // Skip finding a line break since we're guaranteed not to find one until `idx` + } + _ => { + let (next_break, next_skip) = self.find_next_break(index, &words, line.width, properties); + break_index = next_break; + skip_until_index = next_skip; + } } } + // If the `break_index` is set, apply it + try_break_line(index, char_index, grapheme_index, &mut line, &mut lines, &mut break_index); // === Iterate Grapheme Clusters === // - for grapheme in word.graphemes(true) { + for grapheme in word.content.graphemes(true) { // Updated first so that any new lines are using the correct index grapheme_index += 1; + line.grapheme_len += 1; for c in grapheme.chars() { if utility::is_newline(c) { // Character is new line -> New line - lines.push(line); - line = Line { - index: grapheme_index, - ..Default::default() - }; + add_line(char_index, grapheme_index, &mut line, &mut lines); + continue; } - if let Some(glyph) = self.sdf.glyphs.iter().find(|glyph| glyph.unicode == c) { + if utility::is_space(c) { + line.width += space_width; + } else if utility::is_tab(c) { + line.width += tab_width; + } else if let Some(glyph) = self.get_glyph(c) { + // Character is valid glyph -> calculate its size and position + let plane_bounds = glyph.plane_bounds.as_ref(); + let (left, top, _width, _height) = match plane_bounds { + Some(rect) => ( + rect.left, + rect.top, + rect.width() * properties.font_size, + rect.height() * properties.font_size, + ), + None => (0.0, 0.0, 0.0, 0.0), + }; + + // Calculate position relative to line and normalized glyph bounds + let pos_x = line.width + left * properties.font_size; + let mut pos_y = properties.line_height * lines.len() as f32; + pos_y -= top * properties.font_size; + + glyph_rects.push(GlyphRect { + position: (pos_x, pos_y), + size: norm_glyph_bounds, + content: c, + }); + + char_index += 1; + line.char_len += 1; line.width += glyph.advance * properties.font_size; - size.0 = size.0.max(line.width); } + + size.0 = size.0.max(line.width); } } } + // Push the final line lines.push(line); size.1 = properties.line_height * lines.len() as f32; - TextLayout::new(lines, size, properties) + // === Shift Lines & Glyphs === // + for line in lines.iter() { + let shift_x = match properties.alignment { + Alignment::Start => 0.0, + Alignment::Middle => (properties.max_size.0 - line.width) / 2.0, + Alignment::End => properties.max_size.0 - line.width, + }; + + let start = line.char_index; + let end = start + line.char_len; + + for index in start..end { + let rect = &mut glyph_rects[index]; + rect.position.0 += shift_x; + } + } + + TextLayout::new(glyph_rects, lines, size, properties) } - pub fn get_layout( - &self, - axis_alignment: CoordinateSystem, - alignment: Alignment, - position: (f32, f32), - max_size: (f32, f32), - content: &String, - line_height: f32, - font_size: f32, - ) -> Vec<LayoutRect> { - let mut positions_and_size = Vec::new(); - let max_glyph_size = self.sdf.max_glyph_size(); - let font_ratio = font_size / self.sdf.atlas.size; - let resized_max_glyph_size = (max_glyph_size.0 * font_ratio, max_glyph_size.1 * font_ratio); - - // TODO: Make this configurable? - let split_chars = vec![' ', '\t']; - let missing_chars: Vec<char> = content - .chars() - .filter(|c| split_chars.iter().any(|c2| c == c2)) - .collect(); - - let shift_sign = match axis_alignment { - CoordinateSystem::PositiveYDown => -1.0, - CoordinateSystem::PositiveYUp => 1.0, + /// Attempts to find the next line break for a given set of [breakable words](BreakableWord). + /// + /// # Returns + /// + /// A tuple. The first field of the tuple indicates which word index to break _before_, if any. + /// The second field indicates which word index to wait _until_ before calling this method again + /// (exclusive), if any. The reason for the second field is that there are cases where the line + /// break behavior can be accounted for ahead of time. + /// + /// # Arguments + /// + /// * `index`: The current word index + /// * `words`: The list of breakable words + /// * `line_width`: The current line's current width + /// * `properties`: The associated text properties + /// + fn find_next_break(&self, index: usize, words: &[BreakableWord], line_width: f32, properties: TextProperties) -> (Option<usize>, Option<usize>) { + let curr_index = index; + let mut next_index = index + 1; + + let curr = if let Some(curr) = words.get(curr_index) { + curr + } else { + return (None, None); }; - let mut line_widths = Vec::new(); - - let mut x = 0.0; - let mut y = 0.0; - let mut i = 0; - let mut line_starting_index = 0; - let mut last_width = 0.0; - for word in content.split(&split_chars[..]) { - let word_width = self.get_word_width(word, font_size); - if x + word_width + (font_size / 2.0) > max_size.0 { - y -= shift_sign * line_height; - line_widths.push((x, line_starting_index, positions_and_size.len())); - line_starting_index = positions_and_size.len(); - x = 0.0; - } - for c in word.chars() { - if c == '\n' { - y -= shift_sign * line_height; - line_widths.push((x, line_starting_index, positions_and_size.len())); - line_starting_index = positions_and_size.len(); - x = 0.0; - } + if curr.hard_break { + // Hard break -> break before next word + return (Some(next_index), None) + } - if let Some(glyph) = self.sdf.glyphs.iter().find(|glyph| glyph.unicode == c) { - let plane_bounds = glyph.plane_bounds.as_ref(); - let (left, top, width, _height) = match plane_bounds { - Some(val) => ( - val.left, - val.top, - val.size().0 * font_size, - val.size().1 * font_size, - ), - None => (0.0, 0.0, 0.0, 0.0), - }; - - last_width = width; - - let position_x = x + left * font_size; - let position_y = - y + (shift_sign * top * font_size) + ((line_height - font_size) / 2.0); - - positions_and_size.push(LayoutRect { - position: (position_x, position_y), - size: (resized_max_glyph_size.0, resized_max_glyph_size.1), - content: c, - }); - - x += glyph.advance * font_size; - } + let mut total_width = self.get_word_width(curr.content, properties); + + if curr.content.ends_with(char::is_whitespace) { + // End in whitespace -> allow line break if needed + + let next = if let Some(next) = words.get(next_index) { + next + } else { + return (None, None); + }; + total_width += self.get_word_width(next.content.trim_end(), properties); + + // Current word will not be joining the next word + return if total_width + line_width > properties.max_size.0 { + // Break before the next word + (Some(next_index), None) + } else { + // No break needed + (None, None) + }; + } + + let mut best_break_point = if total_width + line_width <= properties.max_size.0 { + // Joined word could fit on current line + Some(next_index) + } else { + // Joined word should start on new line + Some(index) + }; + + while let Some(word) = words.get(next_index) { + total_width += self.get_word_width(word.content, properties); + + if total_width + line_width <= properties.max_size.0 { + // Still within confines of LINE -> break line here if needed + best_break_point = Some(next_index + 1); } - if let Some(next_missing) = missing_chars.get(i) { - if let Some(glyph) = self - .sdf - .glyphs - .iter() - .find(|glyph| glyph.unicode == *next_missing) - { - x += glyph.advance * font_size; - } - i += 1; + + if word.content.ends_with(char::is_whitespace) { + // End of joining words + break; } + + next_index += 1; } - line_widths.push(( - x + last_width, - line_starting_index, - positions_and_size.len(), - )); + // The index to skip until (i.e. the last joined word). + let skip_until_index = next_index - 1; - for (line_width, starting_index, end_index) in line_widths { - let shift_x = match alignment { - Alignment::Start => 0.0, - Alignment::Middle => (max_size.0 - line_width) / 2.0, - Alignment::End => max_size.0 - line_width, - }; - for i in starting_index..end_index { - let layout_rect = &mut positions_and_size[i]; + if total_width + line_width <= properties.max_size.0 { + // Still within confines of LINE -> no need to break + return (None, Some(skip_until_index)); + } - layout_rect.position.0 += position.0 + shift_x; - layout_rect.position.1 += position.1; - } + if total_width <= properties.max_size.0 { + // Still within confines of MAX (can fit within a single line) + return (Some(index), Some(skip_until_index)); + } + + // Attempt to break at the best possible point + (best_break_point, Some(skip_until_index)) + } + + /// Returns the pixel width of a space. + fn get_space_width(&self, properties: TextProperties) -> f32 { + if let Some(glyph) = self.get_glyph(SPACE) { + glyph.advance * properties.font_size + } else { + 0.0 } + } + + /// Returns the pixel width of a tab. + fn get_tab_width(&self, properties: TextProperties) -> f32 { + self.get_space_width(properties) * properties.tab_size as f32 + } + + /// Attempts to find the glyph corresponding to the given character. + /// + /// Returns `None` if no glyph was found. + pub fn get_glyph(&self, c: char) -> Option<&Glyph> { + self.char_ids.get(&c).and_then(|index| self.sdf.glyphs.get(*index as usize)) + } - positions_and_size + /// Calculates the appropriate glyph size for a desired font size. + /// + /// This glyph size can then be used to provide a normalized size across all glyphs + /// in the atlas. + fn calc_glyph_size(&self, font_size: f32) -> (f32, f32) { + let font_scale = font_size / self.sdf.atlas.font_size; + (self.max_glyph_size.0 * font_scale, self.max_glyph_size.1 * font_scale) } + + // TODO: Remove + // pub fn get_layout( + // &self, + // axis_alignment: CoordinateSystem, + // alignment: Alignment, + // position: (f32, f32), + // max_size: (f32, f32), + // content: &String, + // line_height: f32, + // font_size: f32, + // ) -> Vec<LayoutRect> { + // let mut positions_and_size = Vec::new(); + // let max_glyph_size = self.sdf.max_glyph_size(); + // let font_ratio = font_size / self.sdf.atlas.font_size; + // let resized_max_glyph_size = (max_glyph_size.0 * font_ratio, max_glyph_size.1 * font_ratio); + // + // // TODO: Make this configurable? + // let split_chars = vec![' ', '\t']; + // let missing_chars: Vec<char> = content + // .chars() + // .filter(|c| split_chars.iter().any(|c2| c == c2)) + // .collect(); + // + // let shift_sign = match axis_alignment { + // CoordinateSystem::PositiveYDown => -1.0, + // CoordinateSystem::PositiveYUp => 1.0, + // }; + // + // let mut line_widths = Vec::new(); + // + // let mut x = 0.0; + // let mut y = 0.0; + // let mut i = 0; + // let mut line_starting_index = 0; + // let mut last_width = 0.0; + // for word in content.split(&split_chars[..]) { + // let word_width = self.get_word_width(word, TextProperties::default()); + // if x + word_width + (font_size / 2.0) > max_size.0 { + // y -= shift_sign * line_height; + // line_widths.push((x, line_starting_index, positions_and_size.len())); + // line_starting_index = positions_and_size.len(); + // x = 0.0; + // } + // for c in word.chars() { + // if c == '\n' { + // y -= shift_sign * line_height; + // line_widths.push((x, line_starting_index, positions_and_size.len())); + // line_starting_index = positions_and_size.len(); + // x = 0.0; + // } + // + // if let Some(glyph) = self.get_glyph(c) { + // let plane_bounds = glyph.plane_bounds.as_ref(); + // let (left, top, width, _height) = match plane_bounds { + // Some(val) => ( + // val.left, + // val.top, + // val.size().0 * font_size, + // val.size().1 * font_size, + // ), + // None => (0.0, 0.0, 0.0, 0.0), + // }; + // + // last_width = width; + // + // let position_x = x + left * font_size; + // let position_y = + // y + (shift_sign * top * font_size) + ((line_height - font_size) / 2.0); + // + // positions_and_size.push(LayoutRect { + // position: (position_x, position_y), + // size: (resized_max_glyph_size.0, resized_max_glyph_size.1), + // content: c, + // }); + // + // x += glyph.advance * font_size; + // } + // } + // if let Some(next_missing) = missing_chars.get(i) { + // if let Some(glyph) = self + // .sdf + // .glyphs + // .iter() + // .find(|glyph| glyph.unicode == *next_missing) + // { + // x += glyph.advance * font_size; + // } + // i += 1; + // } + // } + // + // line_widths.push(( + // x + last_width, + // line_starting_index, + // positions_and_size.len(), + // )); + // + // for (line_width, starting_index, end_index) in line_widths { + // let shift_x = match alignment { + // Alignment::Start => 0.0, + // Alignment::Middle => (max_size.0 - line_width) / 2.0, + // Alignment::End => max_size.0 - line_width, + // }; + // for i in starting_index..end_index { + // let layout_rect = &mut positions_and_size[i]; + // + // layout_rect.position.0 += position.0 + shift_x; + // layout_rect.position.1 += position.1; + // } + // } + // + // positions_and_size + // } } diff --git a/kayak_font/src/layout.rs b/kayak_font/src/layout.rs index 0f58b57bbe2960bfa59a9138767891519db97050..26e55cd7ea52414bf6008258ebe1ffefce99a883 100644 --- a/kayak_font/src/layout.rs +++ b/kayak_font/src/layout.rs @@ -1,5 +1,12 @@ use std::cmp::Ordering; +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct GlyphRect { + pub position: (f32, f32), + pub size: (f32, f32), + pub content: char, +} + /// The text alignment. #[derive(Copy, Clone, Debug, PartialEq)] pub enum Alignment { @@ -16,25 +23,43 @@ pub struct TextProperties { /// The line height (in pixels). pub line_height: f32, /// The maximum width and height a block of text can take up (in pixels). - pub max_size: Option<(f32, f32)>, + pub max_size: (f32, f32), /// The text alignment. pub alignment: Alignment, + /// The size of a tab (`'\t'`) character in equivalent spaces. + pub tab_size: u8, +} + +impl Default for TextProperties { + fn default() -> Self { + Self { + font_size: 14.0, + line_height: 14.0 * 1.2, + max_size: (f32::MAX, f32::MAX), + tab_size: 4, + alignment: Alignment::Start, + } + } } /// Contains details for a calculated line of text. #[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct Line { /// The index of the starting grapheme cluster within the text content. - pub index: usize, + pub grapheme_index: usize, + /// The index of the starting char within the text content. + pub char_index: usize, /// The total number of grapheme clusters in this line. - pub len: usize, + pub grapheme_len: usize, + /// The total number of chars in this line. + pub char_len: usize, /// The total width of this line (in pixels). pub width: f32, } impl PartialOrd for Line { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - self.index.partial_cmp(&other.index) + self.grapheme_index.partial_cmp(&other.grapheme_index) } } @@ -43,6 +68,7 @@ impl PartialOrd for Line { /// This can be retrieved using [`measure`](crate::KayakFont::measure). #[derive(Clone, Debug, PartialEq)] pub struct TextLayout { + glyphs: Vec<GlyphRect>, lines: Vec<Line>, size: (f32, f32), properties: TextProperties, @@ -50,12 +76,8 @@ pub struct TextLayout { impl TextLayout { /// Create a new [`TextLayout`]. - pub fn new(lines: Vec<Line>, size: (f32, f32), properties: TextProperties) -> Self { - Self { - lines, - size, - properties, - } + pub fn new(glyphs: Vec<GlyphRect>, lines: Vec<Line>, size: (f32, f32), properties: TextProperties) -> Self { + Self { glyphs, lines, size, properties } } /// Returns the calculated lines for the text content. @@ -63,6 +85,11 @@ impl TextLayout { &self.lines } + /// Returns the calculated glyph rects for the text content. + pub fn glyphs(&self) -> &Vec<GlyphRect> { + &self.glyphs + } + /// Returns the total width and height of the text content (in pixels). pub fn size(&self) -> (f32, f32) { self.size diff --git a/kayak_font/src/lib.rs b/kayak_font/src/lib.rs index 6c57032ae28835c45b97fd94737d77eb3c0bfe3f..64e1b04ba138861f898141a5043d91b5ddc3b7e7 100644 --- a/kayak_font/src/lib.rs +++ b/kayak_font/src/lib.rs @@ -171,10 +171,8 @@ pub mod bevy { load_context.get_handle(atlas_image_path.clone()), ); - font.generate_char_ids(); - - load_context - .set_default_asset(LoadedAsset::new(font).with_dependency(atlas_image_path)); + let asset = LoadedAsset::new(font).with_dependency(atlas_image_path); + load_context.set_default_asset(asset); Ok(()) }) diff --git a/kayak_font/src/renderer/font_texture_cache.rs b/kayak_font/src/renderer/font_texture_cache.rs index 890b8e73975c3f21132b3c35290bc82a32927a29..5b18fbcee6181a72ddf3a791e85f4462b52af7f7 100644 --- a/kayak_font/src/renderer/font_texture_cache.rs +++ b/kayak_font/src/renderer/font_texture_cache.rs @@ -20,7 +20,7 @@ pub trait FontRenderingPipeline { fn get_font_image_layout(&self) -> &BindGroupLayout; } -pub const MAX_CHARACTERS: u32 = 100; +pub const MAX_CHARACTERS: u32 = 500; pub struct FontTextureCache { images: HashMap<Handle<KayakFont>, GpuImage>, diff --git a/kayak_font/src/utility.rs b/kayak_font/src/utility.rs index b320a5bd7e92cbe81a85de273879c82ce7b1d814..66884a66c2ae89f1430a7bc6a277c9446f881cc9 100644 --- a/kayak_font/src/utility.rs +++ b/kayak_font/src/utility.rs @@ -1,4 +1,81 @@ +use std::str::{CharIndices, Split}; +use unicode_segmentation::UnicodeSegmentation; +use xi_unicode::LineBreakIterator; + +pub const NEWLINE: char = '\n'; +pub const SPACE: char = ' '; +pub const NBSP: char = '\u{a0}'; +pub const TAB: char = '\t'; + /// Returns true if the given character is a newline. pub fn is_newline(c: char) -> bool { - c == '\n' + c == NEWLINE } + +/// Returns true if the given character is a space. +/// +/// Includes the non-breaking space ([`NBSP`]). +pub fn is_space(c: char) -> bool { + c == SPACE || c == NBSP +} + +/// Returns true if the given character is a tab. +pub fn is_tab(c: char) -> bool { + c == TAB +} + +/// Split a string into a collection of "words" that may be followed by a line break, +/// according to [UAX #14](https://www.unicode.org/reports/tr14/). +/// +/// For example, `"Hello, world!"` would be broken into `["Hello, ", "world!"]`. And +/// `"A-rather-long-word"` would be broken into `["A-", "rather-", "long-", "word"]`. +pub fn split_breakable_words(text: &str) -> BreakableWordIter { + BreakableWordIter::new(text) +} + +/// A "word" (or, rather substring) that may be followed by a line break, +/// according to [UAX #14](https://www.unicode.org/reports/tr14/). +#[derive(Copy, Clone, Debug)] +pub struct BreakableWord<'a> { + /// The index of the last character in this word. + pub char_index: usize, + /// The content of this word. + pub content: &'a str, + /// If true, this word __must__ be followed by a line break. + pub hard_break: bool, +} + +/// An iterator over [`BreakableWord`]. +#[derive(Copy, Clone)] +pub struct BreakableWordIter<'a> { + text: &'a str, + iter: LineBreakIterator<'a>, + index: usize, +} + +impl<'a> BreakableWordIter<'a> { + pub fn new(text: &'a str) -> Self { + Self { text, iter: LineBreakIterator::new(text), index: 0 } + } +} + +impl<'a> Iterator for BreakableWordIter<'a> { + type Item = BreakableWord<'a>; + + fn next(&mut self) -> Option<Self::Item> { + let (next_idx, is_hard) = self.iter.next()?; + let word = self.text.get(self.index..next_idx)?; + self.index = next_idx; + + Some(BreakableWord { + char_index: next_idx, + content: word, + hard_break: is_hard, + }) + } + + fn size_hint(&self) -> (usize, Option<usize>) { + self.iter.size_hint() + } +} +