diff --git a/bevy_kayak_ui/src/render/unified/font/extract.rs b/bevy_kayak_ui/src/render/unified/font/extract.rs index 3e609e616e0513e85ce25475b5f37d2424ab23ec..71903034e3e0bc35c0779102439f348a62c7dcc7 100644 --- a/bevy_kayak_ui/src/render/unified/font/extract.rs +++ b/bevy_kayak_ui/src/render/unified/font/extract.rs @@ -4,7 +4,7 @@ use bevy::{ sprite2::Rect, }; use kayak_core::render_primitive::RenderPrimitive; -use kayak_font::{CoordinateSystem, KayakFont}; +use kayak_font::{Alignment, CoordinateSystem, KayakFont}; use crate::{ render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType}, @@ -54,8 +54,11 @@ pub fn extract_texts( let chars_layouts = font.get_layout( CoordinateSystem::PositiveYDown, + Alignment::Start, Vec2::new(layout.posx, layout.posy), + Vec2::new(layout.width, layout.height), content, + font_size * 1.2, font_size, ); diff --git a/kayak_font/examples/bevy.rs b/kayak_font/examples/bevy.rs index 1fa0e5a6b101202f235dcda432d6b1a28fcb78ff..622d39f4afda084db92de1bc4a638b306fba6d7f 100644 --- a/kayak_font/examples/bevy.rs +++ b/kayak_font/examples/bevy.rs @@ -5,7 +5,7 @@ use bevy::{ window::WindowDescriptor, PipelinedDefaultPlugins, }; -use kayak_font::{KayakFont, KayakFontPlugin}; +use kayak_font::{Alignment, KayakFont, KayakFontPlugin}; mod renderer; use renderer::FontRenderPlugin; @@ -19,11 +19,39 @@ fn startup(mut commands: Commands, asset_server: Res<AssetServer>) { commands .spawn() .insert(Text { + horz_alignment: Alignment::Start, color: Color::WHITE, - content: "Hello World!".into(), + content: "Hello World! This text should wrap because its super long!".into(), font_size: 32.0, + line_height: 32.0 * 1.2, // Firefox method of calculating default line heights see: https://developer.mozilla.org/en-US/docs/Web/CSS/line-height position: Vec2::new(5.0, 5.0), - size: Vec2::new(100.0, 100.0), + size: Vec2::new(250.0, 100.0), + }) + .insert(font_handle.clone()); + + commands + .spawn() + .insert(Text { + horz_alignment: Alignment::End, + color: Color::WHITE, + content: "This is some text that will wrap and also be aligned to the right.".into(), + font_size: 32.0, + line_height: 32.0 * 1.2, // Firefox method of calculating default line heights see: https://developer.mozilla.org/en-US/docs/Web/CSS/line-height + position: Vec2::new(-255.0, 5.0), + size: Vec2::new(250.0, 100.0), + }) + .insert(font_handle.clone()); + + commands + .spawn() + .insert(Text { + horz_alignment: Alignment::Middle, + color: Color::WHITE, + content: "This is some text that will wrap and also be aligned in the middle.".into(), + font_size: 32.0, + line_height: 32.0 * 1.2, // Firefox method of calculating default line heights see: https://developer.mozilla.org/en-US/docs/Web/CSS/line-height + position: Vec2::new(-125.0, -155.0), + size: Vec2::new(250.0, 100.0), }) .insert(font_handle); } diff --git a/kayak_font/examples/renderer/extract.rs b/kayak_font/examples/renderer/extract.rs index 2900f192f62848298ebb1c930c4f133928fa84fe..26f0437c960c8f62df18cb84dc6972e2735c56b3 100644 --- a/kayak_font/examples/renderer/extract.rs +++ b/kayak_font/examples/renderer/extract.rs @@ -20,8 +20,11 @@ pub fn extract( if let Some(font) = fonts.get(font_handle) { let layouts = font.get_layout( CoordinateSystem::PositiveYUp, + text.horz_alignment, text.position, + text.size, &text.content, + text.line_height, text.font_size, ); diff --git a/kayak_font/examples/renderer/text.rs b/kayak_font/examples/renderer/text.rs index 845e2e47696dd0b1b2d9f7d2969ca0b4fd5efad1..631611f3c6a9274ee8b18e9452621d33e2339574 100644 --- a/kayak_font/examples/renderer/text.rs +++ b/kayak_font/examples/renderer/text.rs @@ -1,10 +1,13 @@ use bevy::{math::Vec2, prelude::Component, render2::color::Color}; +use kayak_font::Alignment; #[derive(Component)] pub struct Text { + pub horz_alignment: Alignment, pub content: String, pub position: Vec2, pub size: Vec2, pub font_size: f32, + pub line_height: f32, pub color: Color, } diff --git a/kayak_font/src/font.rs b/kayak_font/src/font.rs index 000aeb6c1ef35d279553fc95f37e47f852dff3a4..09e9acaf3da3882a84a79b062857be9b529bc64e 100644 --- a/kayak_font/src/font.rs +++ b/kayak_font/src/font.rs @@ -25,12 +25,19 @@ pub struct LayoutRect { pub content: char, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum CoordinateSystem { PositiveYUp, PositiveYDown, } +#[derive(Copy, Clone, PartialEq)] +pub enum Alignment { + Start, + Middle, + End, +} + impl KayakFont { pub fn new(sdf: Sdf, atlas_image: Handle<Image>) -> Self { Self { @@ -52,11 +59,36 @@ impl KayakFont { self.char_ids.get(&c).and_then(|id| Some(*id)) } + pub fn get_word_width(&self, word: &str, font_size: f32) -> f32 { + 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().x * font_size, + val.size().y * font_size, + ), + None => (0.0, 0.0, 0.0, 0.0), + }; + + width += char_width; + } + } + + width + } + pub fn get_layout( &self, axis_alignment: CoordinateSystem, + alignment: Alignment, position: Vec2, + max_size: Vec2, content: &String, + line_height: f32, font_size: f32, ) -> Vec<LayoutRect> { let mut positions_and_size = Vec::new(); @@ -64,35 +96,90 @@ impl KayakFont { let font_ratio = font_size / self.sdf.atlas.size; let resized_max_glyph_size = (max_glyph_size.x * font_ratio, max_glyph_size.y * font_ratio); - let mut x = 0.0; - for c in content.chars() { - 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().x * font_size, - val.size().y * font_size, - ), - None => (0.0, 0.0, 0.0, 0.0), - }; + // TODO: Make this configurable? + let split_chars = vec![' ', '\t', '-', '\n']; + 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 shift_sign = match axis_alignment { + CoordinateSystem::PositiveYDown => -1.0, + CoordinateSystem::PositiveYUp => 1.0, + }; - let position_x = position.x + x + left * font_size; - let position_y = (position.y + (shift_sign * top * font_size)) + font_size; + let mut line_widths = Vec::new(); - positions_and_size.push(LayoutRect { - position: Vec2::new(position_x, position_y), - size: Vec2::new(resized_max_glyph_size.0, resized_max_glyph_size.1), - content: c, - }); + 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 > max_size.x { + 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 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().x * font_size, + val.size().y * 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); + + positions_and_size.push(LayoutRect { + position: Vec2::new(position_x, position_y), + size: Vec2::new(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; + } + } - x += glyph.advance * font_size; + 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.x - line_width) / 2.0, + Alignment::End => max_size.x - line_width, + }; + for i in starting_index..end_index { + let layout_rect = &mut positions_and_size[i]; + + layout_rect.position.x += position.x + shift_x; + layout_rect.position.y += position.y; } }