Skip to content
Snippets Groups Projects
Commit 2271e168 authored by Gino Valente's avatar Gino Valente
Browse files

Updated KayakFont::measure

Cleaned up API. Also added some new types to better encapsulate logic
and layout information.
parent 0e9008dd
No related branches found
No related tags found
No related merge requests found
......@@ -1883,6 +1883,7 @@ dependencies = [
"serde",
"serde_json",
"serde_path_to_error",
"unicode-segmentation",
]
[[package]]
......@@ -3143,6 +3144,12 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c74c96594835e10fa545e2a51e8709f30b173a092bfd6036ef2cec53376244f3"
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-width"
version = "0.1.9"
......
use indexmap::IndexSet;
use kayak_font::{CoordinateSystem, KayakFont};
use kayak_font::{Alignment, CoordinateSystem, KayakFont, TextProperties};
use morphorm::Units;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
......@@ -307,19 +307,17 @@ impl WidgetManager {
if let Some(font) = asset.get() {
if let Some(parent_id) = self.get_valid_parent(id) {
if let Some(parent_layout) = self.get_layout(&parent_id) {
if *parent_layout == Rect::default() {
// needs_layout = true;
}
*parent_size = (parent_layout.width, parent_layout.height);
// --- Calculate Text Layout --- //
let measurement = font.measure(
CoordinateSystem::PositiveYDown,
&content,
*size,
*line_height,
*parent_size,
);
let properties = TextProperties {
font_size: *size,
max_size: Some(*parent_size),
alignment: Alignment::Start,
line_height: *line_height,
};
let layout = font.measure(&content, properties);
let measurement = layout.size();
// --- Apply Layout --- //
if matches!(styles.width, StyleProp::Default) {
......
......@@ -16,3 +16,4 @@ bytemuck = "1.7.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_path_to_error = "0.1"
unicode-segmentation = "1.9"
......@@ -5,7 +5,8 @@ use bevy::{
window::WindowDescriptor,
DefaultPlugins,
};
use kayak_font::{bevy::KayakFontPlugin, Alignment, KayakFont};
use kayak_font::layout::Alignment;
use kayak_font::{bevy::KayakFontPlugin, KayakFont};
mod renderer;
use renderer::FontRenderPlugin;
......
use bevy::{math::Vec2, prelude::Component, render::color::Color};
use kayak_font::Alignment;
use kayak_font::layout::Alignment;
#[derive(Component)]
pub struct Text {
......
......@@ -2,8 +2,10 @@ use std::collections::HashMap;
#[cfg(feature = "bevy_renderer")]
use bevy::{prelude::Handle, reflect::TypeUuid, render::texture::Image};
use unicode_segmentation::UnicodeSegmentation;
use crate::Sdf;
use crate::layout::{Alignment, Line, TextLayout};
use crate::{utility, Sdf, TextProperties};
#[cfg(feature = "bevy_renderer")]
#[derive(Debug, Clone, TypeUuid, PartialEq)]
......@@ -34,13 +36,6 @@ pub enum CoordinateSystem {
PositiveYDown,
}
#[derive(Copy, Clone, PartialEq)]
pub enum Alignment {
Start,
Middle,
End,
}
impl KayakFont {
pub fn new(sdf: Sdf, #[cfg(feature = "bevy_renderer")] atlas_image: Handle<Image>) -> Self {
Self {
......@@ -85,63 +80,75 @@ impl KayakFont {
width
}
pub fn measure(
&self,
axis_alignment: CoordinateSystem,
content: &String,
font_size: f32,
line_height: f32,
max_size: (f32, f32),
) -> (f32, f32) {
/// Measures the given text content and calculates an appropriate layout
/// given a set of properties.
///
/// # Arguments
///
/// * `content`: The textual content to measure.
/// * `properties`: The text properties to use.
///
pub fn measure(&self, content: &str, properties: TextProperties) -> TextLayout {
let mut size: (f32, f32) = (0.0, 0.0);
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 x = 0.0;
let mut y = 0.0;
let mut i = 0;
for word in content.split(&split_chars[..]) {
let word_width = self.get_word_width(word, font_size);
if x + word_width > max_size.0 {
y -= shift_sign * line_height;
x = 0.0;
}
for c in word.chars() {
if c == '\n' {
y -= shift_sign * line_height;
x = 0.0;
}
if let Some(glyph) = self.sdf.glyphs.iter().find(|glyph| glyph.unicode == c) {
x += glyph.advance * font_size;
size.0 = size.0.max(x);
let mut lines = Vec::new();
let mut line = Line::default();
let mut grapheme_index = 0;
// We'll now split up the text content so that we can measure the layout.
// This is the "text pipeline" for this function:
// 1. Split the text by their UAX #29 word boundaries.
// 2. Split each word by its UAX #29 grapheme clusters.
// This step is important since "a̐" is technically two characters (codepoints),
// but rendered as a single glyph.
// 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()
};
}
}
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;
// === Iterate Grapheme Clusters === //
for grapheme in word.graphemes(true) {
// Updated first so that any new lines are using the correct index
grapheme_index += 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()
};
}
if let Some(glyph) = self.sdf.glyphs.iter().find(|glyph| glyph.unicode == c) {
line.width += glyph.advance * properties.font_size;
size.0 = size.0.max(line.width);
}
}
i += 1;
}
}
// One last shift..
y -= shift_sign * line_height;
size.1 = y.abs();
size
lines.push(line);
size.1 = properties.line_height * lines.len() as f32;
TextLayout::new(lines, size, properties)
}
pub fn get_layout(
......
use std::cmp::Ordering;
/// The text alignment.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Alignment {
Start,
Middle,
End,
}
/// Properties to control text layout.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct TextProperties {
/// The font size (in pixels).
pub font_size: f32,
/// 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)>,
/// The text alignment.
pub alignment: Alignment,
}
/// 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,
/// The total number of grapheme clusters in this line.
pub 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)
}
}
/// Calculated text layout.
///
/// This can be retrieved using [`measure`](crate::KayakFont::measure).
#[derive(Clone, Debug, PartialEq)]
pub struct TextLayout {
lines: Vec<Line>,
size: (f32, f32),
properties: TextProperties,
}
impl TextLayout {
/// Create a new [`TextLayout`].
pub fn new(lines: Vec<Line>, size: (f32, f32), properties: TextProperties) -> Self {
Self {
lines,
size,
properties,
}
}
/// Returns the calculated lines for the text content.
pub fn lines(&self) -> &Vec<Line> {
&self.lines
}
/// Returns the total width and height of the text content (in pixels).
pub fn size(&self) -> (f32, f32) {
self.size
}
/// Returns the properties used to calculate this layout.
pub fn properties(&self) -> TextProperties {
self.properties
}
}
mod atlas;
mod font;
mod glyph;
mod layout;
mod metrics;
mod sdf;
mod utility;
pub use atlas::*;
pub use font::*;
pub use glyph::*;
pub use layout::*;
pub use metrics::*;
pub use sdf::*;
......@@ -29,6 +32,7 @@ pub mod bevy {
},
utils::HashSet,
};
pub struct KayakFontPlugin;
impl Plugin for KayakFontPlugin {
......
/// Returns true if the given character is a newline.
pub fn is_newline(c: char) -> bool {
c == '\n'
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment