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()
+    }
+}
+