diff --git a/assets/lato-light - Copy.png b/assets/lato-light - Copy.png
deleted file mode 100644
index 103c6324efec5ccfef51aab02cde1afb0d7895ae..0000000000000000000000000000000000000000
Binary files a/assets/lato-light - Copy.png and /dev/null differ
diff --git a/assets/lato-light.kttf b/assets/lato-light.kttf
new file mode 100644
index 0000000000000000000000000000000000000000..15e547e1171fc26bd38cfd25279144307481d46c
--- /dev/null
+++ b/assets/lato-light.kttf
@@ -0,0 +1,5 @@
+{
+    "file": "lato-light.ttf",
+    "char_range_start": "0x20",
+    "char_range_end": "0x7f"
+}
\ No newline at end of file
diff --git a/assets/lato-light.kttf-cached.png b/assets/lato-light.kttf-cached.png
new file mode 100644
index 0000000000000000000000000000000000000000..9cec7f72d026cb867b02cba99b0ffa9129c52187
Binary files /dev/null and b/assets/lato-light.kttf-cached.png differ
diff --git a/assets/lato-light.ttf b/assets/lato-light.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..dfa72ce808fbb783042da88ce1bae5d9adb54fb6
Binary files /dev/null and b/assets/lato-light.ttf differ
diff --git a/assets/main_menu/kayak.png b/assets/main_menu/kayak.png
new file mode 100644
index 0000000000000000000000000000000000000000..ba89446f92474ae91e932892edb9e623a0974dbb
Binary files /dev/null and b/assets/main_menu/kayak.png differ
diff --git a/assets/roboto.kttf b/assets/roboto.kttf
new file mode 100644
index 0000000000000000000000000000000000000000..9c7d59c6ed7f1c15a4d89b38b98ae946f470213b
--- /dev/null
+++ b/assets/roboto.kttf
@@ -0,0 +1,5 @@
+{
+    "file": "roboto.ttf",
+    "char_range_start": "0x20",
+    "char_range_end": "0x7f"
+}
\ No newline at end of file
diff --git a/assets/roboto.kttf-cached.png b/assets/roboto.kttf-cached.png
new file mode 100644
index 0000000000000000000000000000000000000000..07d69a7ff42902c316d4d34295c32460a815e84e
Binary files /dev/null and b/assets/roboto.kttf-cached.png differ
diff --git a/assets/roboto.ttf b/assets/roboto.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..2b6392ffe8712b9c5450733320cd220d6c0f4bce
Binary files /dev/null and b/assets/roboto.ttf differ
diff --git a/book/src/chapter_1.md b/book/src/chapter_1.md
index 1f820f085398a6084c627b6091a804b909e21ff0..e33eb818736f2293784dcf6e1167ab9cb081338d 100644
--- a/book/src/chapter_1.md
+++ b/book/src/chapter_1.md
@@ -1,12 +1,11 @@
 # Chapter 1 - Installing and hello world!
 Kayak UI is quite easy to setup! First make sure you add it to your cargo.toml file in your project. 
 
-Because a crate has yet to be released this currently this looks like:
 ```toml
-kayak_ui = { git = "https://github.com/StarArawn/kayak_ui/", rev = "9b212e230a5325a3ac6897390ded0bc358eebc80"}
+kayak_ui = "0.1"
 ```
 
-Once you've added Kayak UI to your bevy project you can now start to use it! In order for you to copy and run this in your own project don't forget to move the `roboto.kayak_font` and the `roboto.png` files to your asset folder. Optionally you can also generate your own font! See: [Chapter 5 - Fonts](./chapter_5.md)
+Once you've added Kayak UI to your bevy project you can now start to use it! In order for you to copy and run this in your own project don't forget to move the `roboto.kayak_font` and the `roboto.png` files to your asset folder. Optionally you can also generate your own font! See: [Chapter 5 - Fonts](./chapter_6.md)
 
 Hello World Example:
 ```rust
@@ -20,8 +19,6 @@ fn startup(
 ) {
     font_mapping.set_default(asset_server.load("roboto.kayak_font"));
 
-    commands.spawn(UICameraBundle::new());
-
     let mut widget_context = KayakRootContext::new();
     let parent_id = None;
 
diff --git a/book/src/chapter_2.md b/book/src/chapter_2.md
index 8782e06df3518eee37931e0320fe9e849f563d83..3abec0b72c2e25894df3cbf52ce4b4f8b0cfb0d1 100644
--- a/book/src/chapter_2.md
+++ b/book/src/chapter_2.md
@@ -8,7 +8,8 @@ Kayak UI builds out UI using a tree structure. A widget can be defined as any ob
 ### Widgets are entities
 Kayak UI uses Bevy ECS. Each widget is considered an entity with a collection of data. Typically an widget and it's entity can contain whatever data desired, but some common components are:
 - Mount - A component tag used to know that a widget was spawned and added to the tree.
-- KStyle - Used to describe how a widget looks. Kayak uses this component to dictate UI rendering.
+- KStyle - Used to pass in styles from outside of a widget.
+- ComputedStyles - The actual styles of a widget. Styles define the look and layout of the widget. Kayak uses this component to dictate UI rendering. 
 - KChildren - A collection of entities that are added to the tree in a deferred way. These entities are coming from higher up the hierarchy. 
 - OnEvent - A mini/micro bevy system that lets you respond to UI input events.
 - OnChange - A mini/micro system which allows changes to state based on value changes to state. 
@@ -23,6 +24,7 @@ It's advised to have bundles that correspond to a group of components on a widge
 pub struct BackgroundBundle {
     pub background: Background,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub children: KChildren,
     pub on_event: OnEvent,
     pub widget_name: WidgetName,
diff --git a/book/src/chapter_3.md b/book/src/chapter_3.md
index 256b25e6f0143cbd5a2727e284674e05054ed77c..4ee11e239e98d0a3e2347ce3c6c2335624eaf51c 100644
--- a/book/src/chapter_3.md
+++ b/book/src/chapter_3.md
@@ -27,6 +27,7 @@ impl Widget for MyButtonProps { }
 pub struct MyButtonBundle {
     pub props: MyButtonProps,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub children: KChildren,
     // This allows us to hook into on click events!
     pub on_event: OnEvent,
@@ -39,6 +40,7 @@ impl Default for MyButtonBundle {
         Self {
             props: MyButtonProps::default(),
             styles: KStyle::default(),
+            computed_styles: ComputedStyles::default(),
             children: KChildren::default(),
             on_event: OnEvent::default(),
             // Kayak uses this component to find out more information about your widget.
@@ -76,6 +78,7 @@ pub fn my_button_render(
 
         rsx! {
             <BackgroundBundle
+                styles={background_styles}
                 // We pass the children to the background bundle!
                 children={children.clone()}
             />
diff --git a/book/src/chapter_6.md b/book/src/chapter_6.md
index 7581bf697f5edd856ac7505462b387f6e460cf04..446bad97fb064e784468f2cb9f249e832ebb8a6d 100644
--- a/book/src/chapter_6.md
+++ b/book/src/chapter_6.md
@@ -4,7 +4,18 @@ Kayak UI uses SDF(signed distance fields) for rendering fonts. More specifically
 - Fast rendering!
 - No need for a new asset for each font size. MSDF's can size to any font size!
 
-Font's are stored as an atlased image and a json file which tells Kayak about the font glyphs. Check out `roboto.kayak_font` and `roboto.png` in the `assets` folder.
+Fonts are stored in two different ways. First a font can be defined as a Kayak TTF(kttf) file. 
+These font files are relatively simple and simply link to a ttf font:
+```json
+{
+    "file": "roboto.ttf",
+    "char_range_start": "0x20",
+    "char_range_end": "0x7f"
+}
+``` 
+The char range is a defined as u32 char values. 0x20 through 0x7f represents most of the standard English language characters. Font's using this method are processed in native rust into MSDF's. The output is cached as the generation can take a while. 
+
+Fonts are also stored as an atlased image and a json file which tells Kayak about the font glyphs. These fonts are generated using `msdf-atlas-gen`. Check out `roboto.kayak_font` and `roboto.png` in the `assets` folder. The cached file name will be located next to the kttf file and have the file format of: `{font_name}.kttf-cached.png`.
 
 ## Generating new fonts.
 In order to create a new font you need to use the `msdf-atlas-gen` tool. This can be found at:
diff --git a/examples/bevy_scene.rs b/examples/bevy_scene.rs
index 096ee9f9f901cbb67fa6ec05682308352a500a5c..7e77f0c4c351792f1a5ad48c5f45d79733faf50b 100644
--- a/examples/bevy_scene.rs
+++ b/examples/bevy_scene.rs
@@ -225,10 +225,10 @@ fn startup(
                 }}
             >
                 <TextWidgetBundle
+                    styles={text_styles.clone()}
                     text={TextProps {
                         size: 13.0,
                         content: "You can check if the cursor is over the UI or on a focusable widget using the BevyContext resource.".to_string(),
-                        user_styles: text_styles.clone(),
                         ..Default::default()
                     }}
                 />
@@ -241,13 +241,13 @@ fn startup(
                     styles={button_styles}
                 />
                 <TextWidgetBundle
+                    styles={KStyle {
+                        top: Units::Pixels(10.0).into(),
+                        ..text_styles
+                    }}
                     text={TextProps {
                         size: 11.0,
                         content: "Go ahead and click the button! The tile won't move.".to_string(),
-                        user_styles: KStyle {
-                            top: Units::Pixels(10.0).into(),
-                            ..text_styles
-                        },
                         ..Default::default()
                     }}
                 />
diff --git a/examples/clipping.rs b/examples/clipping.rs
index ee5a3032f5133edca1ad952381d1f22f4c866f88..322a0ed2c81f869f3a28dbd20973c23d17364c09 100644
--- a/examples/clipping.rs
+++ b/examples/clipping.rs
@@ -6,7 +6,7 @@ fn startup(
     mut font_mapping: ResMut<FontMapping>,
     asset_server: Res<AssetServer>,
 ) {
-    font_mapping.set_default(asset_server.load("lato-light.kayak_font"));
+    font_mapping.set_default(asset_server.load("lato-light.kttf"));
 
     let image = asset_server.load("panel.png");
 
diff --git a/examples/conditional_widget.rs b/examples/conditional_widget.rs
index b668f17ce4655b6706002df9a4dcf201fb0b84fb..42476e4df8a16797810c83e7f029a3857c493473 100644
--- a/examples/conditional_widget.rs
+++ b/examples/conditional_widget.rs
@@ -39,13 +39,13 @@ fn my_widget_render(
         rsx! {
             <ElementBundle>
                 <KButtonBundle
+                    styles={KStyle {
+                        left: Units::Stretch(1.0).into(),
+                        right: Units::Stretch(1.0).into(),
+                        ..Default::default()
+                    }}
                     button={KButton {
                         text: "Show Window".into(),
-                        user_styles: KStyle {
-                            left: Units::Stretch(1.0).into(),
-                            right: Units::Stretch(1.0).into(),
-                            ..Default::default()
-                        }
                     }}
                     on_event={OnEvent::new(
                         move |In((event_dispatcher_context, _, mut event, _entity)): In<(EventDispatcherContext, WidgetState, Event, Entity)>,
@@ -109,7 +109,7 @@ fn startup(
     mut font_mapping: ResMut<FontMapping>,
     asset_server: Res<AssetServer>,
 ) {
-    font_mapping.set_default(asset_server.load("lato-light.kayak_font"));
+    font_mapping.set_default(asset_server.load("lato-light.kttf"));
 
     // Camera 2D forces a clear pass in bevy.
     // We do this because our scene is not rendering anything else.
diff --git a/examples/context.rs b/examples/context.rs
index aa3696f59c8a94ccbcd584620bfba509ce0e9688..9523f64fdebaa7fed2e38f6e4388c16bb05c9de3 100644
--- a/examples/context.rs
+++ b/examples/context.rs
@@ -256,14 +256,14 @@ fn update_theme_demo(
                 rsx! {
                     <ElementBundle>
                         <TextWidgetBundle
+                            styles={KStyle {
+                                height: StyleProp::Value(Units::Pixels(28.0)),
+                                ..Default::default()
+                            }}
                             text={TextProps {
                                 content: select_lbl,
                                 size: 14.0,
                                 line_height: Some(28.0),
-                                user_styles: KStyle {
-                                    height: StyleProp::Value(Units::Pixels(28.0)),
-                                    ..Default::default()
-                                },
                                 ..Default::default()
                             }}
                         />
@@ -278,10 +278,10 @@ fn update_theme_demo(
                             }}
                         >
                             <TextWidgetBundle
+                                styles={text_styles}
                                 text={TextProps {
                                     content: "Lorem ipsum dolor...".into(),
                                     size: 12.0,
-                                    user_styles: text_styles,
                                     ..Default::default()
                                 }}
                             />
diff --git a/examples/main_menu.rs b/examples/main_menu.rs
index a1ec90cd1e51d239f9b45a14e9249b9db4031f24..7310a647c0343bceecd7b0c16fd0c1c31ca55295 100644
--- a/examples/main_menu.rs
+++ b/examples/main_menu.rs
@@ -86,20 +86,23 @@ fn menu_button_render(
                 styles={KStyle {
                     width: Units::Stretch(1.0).into(),
                     height: Units::Pixels(40.0).into(),
+                    bottom: Units::Pixels(30.0).into(),
+                    left: Units::Pixels(50.0).into(),
+                    right: Units::Pixels(50.0).into(),
                     ..KStyle::default()
                 }}
                 on_event={on_event}
             >
                 <TextWidgetBundle
+                    styles={KStyle {
+                        top: Units::Stretch(1.0).into(),
+                        bottom: Units::Stretch(1.0).into(),
+                        ..Default::default()
+                    }}
                     text={TextProps {
                         alignment: Alignment::Middle,
                         content: button_text,
                         size: 28.0,
-                        user_styles: KStyle {
-                            top: Units::Stretch(1.0).into(),
-                            bottom: Units::Stretch(1.0).into(),
-                            ..Default::default()
-                        },
                         ..Default::default()
                     }}
                 />
@@ -120,7 +123,7 @@ fn startup(
     asset_server: Res<AssetServer>,
     mut preload_resource: ResMut<PreloadResource>,
 ) {
-    font_mapping.set_default(asset_server.load("lato-light.kayak_font"));
+    font_mapping.set_default(asset_server.load("lato-light.kttf"));
 
     let mut widget_context = KayakRootContext::new();
     widget_context.add_plugin(KayakWidgetsContextPlugin);
@@ -133,6 +136,7 @@ fn startup(
 
     let panel1_image = asset_server.load("main_menu/panel1.png");
     let logo_image = asset_server.load("main_menu/logo.png");
+    let kayak_image = asset_server.load("main_menu/kayak.png");
     let button_image = asset_server.load("main_menu/button.png");
     let button_image_hover = asset_server.load("main_menu/button-hover.png");
 
@@ -185,6 +189,16 @@ fn startup(
                     ..KStyle::default()
                 }}
             >
+                <KImageBundle
+                    image={KImage(kayak_image)}
+                    styles={KStyle {
+                        width: Units::Pixels(310.0).into(),
+                        height: Units::Pixels(104.0).into(),
+                        top: Units::Pixels(25.0).into(),
+                        bottom: Units::Pixels(25.0).into(),
+                        ..KStyle::default()
+                    }}
+                />
                 <KImageBundle
                     image={KImage(logo_image)}
                     styles={KStyle {
@@ -194,21 +208,12 @@ fn startup(
                         ..KStyle::default()
                     }}
                 />
-                <ElementBundle
-                    id={"button_area"}
-                    styles={KStyle {
-                        left: Units::Pixels(50.0).into(),
-                        right: Units::Pixels(50.0).into(),
-                        ..Default::default()
-                    }}
-                >
-                    <MenuButtonBundle button={MenuButton { text: "Play".into() }} />
-                    <MenuButtonBundle button={MenuButton { text: "Options".into() }} />
-                    <MenuButtonBundle
-                        button={MenuButton { text: "Quit".into() }}
-                        on_event={handle_click_close}
-                    />
-                </ElementBundle>
+                <MenuButtonBundle button={MenuButton { text: "Play".into() }} />
+                <MenuButtonBundle button={MenuButton { text: "Options".into() }} />
+                <MenuButtonBundle
+                    button={MenuButton { text: "Quit".into() }}
+                    on_event={handle_click_close}
+                />
             </NinePatchBundle>
         </KayakAppBundle>
     }
diff --git a/examples/quads.rs b/examples/quads.rs
index e0afabedbe25817f52a5b68a6e396174380d72f9..9bd772229ff1035c9851805798c2cee402a306c9 100644
--- a/examples/quads.rs
+++ b/examples/quads.rs
@@ -14,19 +14,26 @@ pub struct MyQuad {
 
 fn my_quad_update(
     In((_widget_context, entity)): In<(KayakWidgetContext, Entity)>,
-    mut query: Query<(&MyQuad, &mut KStyle, &mut OnEvent)>,
+    mut query: Query<(&MyQuad, &KStyle, &mut ComputedStyles, &mut OnEvent)>,
 ) -> bool {
-    if let Ok((quad, mut style, mut on_event)) = query.get_mut(entity) {
-        if style.render_command.resolve() != RenderCommand::Quad {
-            style.render_command = StyleProp::Value(RenderCommand::Quad);
-            style.position_type = StyleProp::Value(KPositionType::SelfDirected);
-            style.left = StyleProp::Value(Units::Pixels(quad.pos.x));
-            style.top = StyleProp::Value(Units::Pixels(quad.pos.y));
-            style.width = StyleProp::Value(Units::Pixels(quad.size.x));
-            style.height = StyleProp::Value(Units::Pixels(quad.size.y));
-            style.background_color = StyleProp::Value(quad.color);
-            style.z_index = StyleProp::Value(quad.z_index);
-        }
+    if let Ok((quad, style, mut computed_styles, mut on_event)) = query.get_mut(entity) {
+        *computed_styles = KStyle::default()
+            .with_style(KStyle {
+                render_command: StyleProp::Value(RenderCommand::Quad),
+                position_type: StyleProp::Value(KPositionType::SelfDirected),
+                left: StyleProp::Value(Units::Pixels(quad.pos.x)),
+                top: StyleProp::Value(Units::Pixels(quad.pos.y)),
+                width: StyleProp::Value(Units::Pixels(quad.size.x)),
+                height: StyleProp::Value(Units::Pixels(quad.size.y)),
+                z_index: StyleProp::Value(quad.z_index),
+                ..Default::default()
+            })
+            .with_style(style)
+            .with_style(KStyle {
+                background_color: StyleProp::Value(quad.color),
+                ..Default::default()
+            })
+            .into();
 
         *on_event = OnEvent::new(
             move |In((event_dispatcher_context, _, mut event, entity)): In<(
@@ -65,6 +72,7 @@ impl Widget for MyQuad {}
 pub struct MyQuadBundle {
     my_quad: MyQuad,
     styles: KStyle,
+    computed_styles: ComputedStyles,
     on_event: OnEvent,
     widget_name: WidgetName,
 }
@@ -75,6 +83,7 @@ impl Default for MyQuadBundle {
             my_quad: Default::default(),
             styles: KStyle::default(),
             on_event: OnEvent::default(),
+            computed_styles: ComputedStyles::default(),
             widget_name: MyQuad::default().get_name(),
         }
     }
diff --git a/examples/scrolling.rs b/examples/scrolling.rs
index c4c23871bbcb5d7153edafebf6531b9250585c11..078f3ce7737677dc674241e7cb787984a24edde8 100644
--- a/examples/scrolling.rs
+++ b/examples/scrolling.rs
@@ -6,7 +6,13 @@ fn startup(
     mut font_mapping: ResMut<FontMapping>,
     asset_server: Res<AssetServer>,
 ) {
-    font_mapping.set_default(asset_server.load("roboto.kayak_font"));
+    let font_asset = asset_server.load("roboto.kayak_font");
+    font_mapping.set_default(font_asset.clone());
+
+    // You can force the entire font to use subpixel rendering.
+    // Note: The subpixel settings on the text widget or render command
+    // will be ignored if this setting is used.
+    font_mapping.force_subpixel(&font_asset);
 
     // Camera 2D forces a clear pass in bevy.
     // We do this because our scene is not rendering anything else.
diff --git a/examples/simple_state.rs b/examples/simple_state.rs
index 0e2e8e2d0768e06dae6320fd45dca1b90bb6e389..a427a40c0b11f4ab892ac408aefe3c91ee9097c7 100644
--- a/examples/simple_state.rs
+++ b/examples/simple_state.rs
@@ -15,6 +15,7 @@ struct CurrentCountState {
 struct CurrentCountBundle {
     count: CurrentCount,
     styles: KStyle,
+    computed_styles: ComputedStyles,
     widget_name: WidgetName,
 }
 
@@ -23,6 +24,7 @@ impl Default for CurrentCountBundle {
         Self {
             count: CurrentCount::default(),
             styles: KStyle::default(),
+            computed_styles: ComputedStyles::default(),
             widget_name: CurrentCount::default().get_name(),
         }
     }
@@ -83,7 +85,7 @@ fn startup(
     mut font_mapping: ResMut<FontMapping>,
     asset_server: Res<AssetServer>,
 ) {
-    font_mapping.set_default(asset_server.load("lato-light.kayak_font"));
+    font_mapping.set_default(asset_server.load("lato-light.kttf"));
 
     // Camera 2D forces a clear pass in bevy.
     // We do this because our scene is not rendering anything else.
diff --git a/examples/tabs/tab_button.rs b/examples/tabs/tab_button.rs
index d9ca5dccc2c7cb5996648725ab516499a5760a1a..440afd8918e7784bbe508100be4ba69c62e27c24 100644
--- a/examples/tabs/tab_button.rs
+++ b/examples/tabs/tab_button.rs
@@ -75,14 +75,14 @@ pub fn tab_button_render(
 
             rsx! {
                 <KButtonBundle
+                    styles={KStyle {
+                        background_color: StyleProp::Value(background_color),
+                        border_radius: Corner::all(0.0).into(),
+                        height: StyleProp::Value(Units::Pixels(25.0)),
+                        ..Default::default()
+                    }}
                     button={KButton {
                         text: tab_button.title.clone(),
-                        user_styles: KStyle {
-                            background_color: StyleProp::Value(background_color),
-                            border_radius: Corner::all(0.0).into(),
-                            height: StyleProp::Value(Units::Pixels(25.0)),
-                            ..Default::default()
-                        },
                     }}
                     on_event={on_event}
                 />
diff --git a/examples/text.rs b/examples/text.rs
index b2f82d38f691c2012a5a13f6e94013457b9da7e9..325c756327ed5362c0f225e7ab12c4b1c2232622 100644
--- a/examples/text.rs
+++ b/examples/text.rs
@@ -9,18 +9,24 @@ pub struct MyWidgetProps {
 fn my_widget_1_render(
     In((_widget_context, entity)): In<(KayakWidgetContext, Entity)>,
     my_resource: Res<MyResource>,
-    mut query: Query<(&mut MyWidgetProps, &mut KStyle)>,
+    mut query: Query<(&mut MyWidgetProps, &KStyle, &mut ComputedStyles)>,
 ) -> bool {
-    if let Ok((mut my_widget, mut style)) = query.get_mut(entity) {
+    if let Ok((mut my_widget, style, mut computed_styles)) = query.get_mut(entity) {
         my_widget.foo = my_resource.0;
         dbg!(my_widget.foo);
         // Note: We will see two updates because of the mutable change to styles.
         // Which means when foo changes MyWidget will render twice!
-        style.render_command = StyleProp::Value(RenderCommand::Text {
-            content: format!("My number is: {}", my_widget.foo),
-            alignment: Alignment::Start,
-            word_wrap: false,
-        });
+        *computed_styles = KStyle {
+            render_command: StyleProp::Value(RenderCommand::Text {
+                content: format!("My number is: {}", my_widget.foo),
+                alignment: Alignment::Start,
+                word_wrap: false,
+                subpixel: false,
+            }),
+            ..Default::default()
+        }
+        .with_style(style)
+        .into();
     }
 
     true
@@ -44,6 +50,7 @@ impl Widget for MyWidgetProps {}
 pub struct MyWidgetBundle {
     props: MyWidgetProps,
     styles: KStyle,
+    computed_styles: ComputedStyles,
     widget_name: WidgetName,
 }
 
@@ -52,6 +59,7 @@ impl Default for MyWidgetBundle {
         Self {
             props: Default::default(),
             styles: Default::default(),
+            computed_styles: Default::default(),
             widget_name: MyWidgetProps::default().get_name(),
         }
     }
diff --git a/examples/todo/input.rs b/examples/todo/input.rs
index 12120651cea94a8aa4643d6b3f55fc19f28b0bbc..8080596ab9ce442bd617dd3eab1b4d77f734ba02 100644
--- a/examples/todo/input.rs
+++ b/examples/todo/input.rs
@@ -13,6 +13,7 @@ pub struct TodoInputBundle {
     pub widget: TodoInputProps,
     pub focusable: Focusable,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub widget_name: WidgetName,
 }
 
@@ -21,14 +22,15 @@ impl Default for TodoInputBundle {
         Self {
             widget: TodoInputProps::default(),
             focusable: Default::default(),
-            styles: KStyle {
+            styles: KStyle::default(),
+            computed_styles: ComputedStyles(KStyle {
                 render_command: StyleProp::Value(RenderCommand::Layout),
                 // height: StyleProp::Value(Units::Stretch(1.0)),
                 height: StyleProp::Value(Units::Auto),
                 width: StyleProp::Value(Units::Stretch(1.0)),
                 bottom: StyleProp::Value(Units::Pixels(20.0)),
                 ..KStyle::default()
-            },
+            }),
             widget_name: TodoInputProps::default().get_name(),
         }
     }
@@ -85,8 +87,8 @@ pub fn render_todo_input(
             }
             <TextBoxBundle
                 styles={KStyle {
-                    bottom: StyleProp::Value(Units::Stretch(1.0)),
-                    top: StyleProp::Value(Units::Stretch(1.0)),
+                    // bottom: StyleProp::Value(Units::Stretch(1.0)),
+                    // top: StyleProp::Value(Units::Stretch(1.0)),
                     ..Default::default()
                 }}
                 text_box={TextBoxProps {
@@ -97,14 +99,14 @@ pub fn render_todo_input(
                 on_change={on_change}
             />
             <KButtonBundle
+                styles={KStyle {
+                    width: StyleProp::Value(Units::Pixels(32.0)),
+                    height: StyleProp::Value(Units::Pixels(32.0)),
+                    left: StyleProp::Value(Units::Pixels(5.0)),
+                    ..Default::default()
+                }}
                 button={KButton {
                     text: "+".into(),
-                    user_styles: KStyle {
-                        width: StyleProp::Value(Units::Pixels(32.0)),
-                        height: StyleProp::Value(Units::Pixels(32.0)),
-                        left: StyleProp::Value(Units::Pixels(5.0)),
-                        ..Default::default()
-                    }
                 }}
                 on_event={handle_click}
             />
diff --git a/examples/todo/items.rs b/examples/todo/items.rs
index 99c802c71bd50e4ebab2613c1007323748dfcfca..4af3366942d762db2b1f751258723e75f25e0afd 100644
--- a/examples/todo/items.rs
+++ b/examples/todo/items.rs
@@ -12,6 +12,7 @@ impl Widget for TodoItemsProps {}
 pub struct TodoItemsBundle {
     pub widget: TodoItemsProps,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub widget_name: WidgetName,
 }
 
@@ -19,12 +20,13 @@ impl Default for TodoItemsBundle {
     fn default() -> Self {
         Self {
             widget: TodoItemsProps::default(),
-            styles: KStyle {
+            styles: KStyle::default(),
+            computed_styles: ComputedStyles(KStyle {
                 render_command: StyleProp::Value(RenderCommand::Layout),
                 height: StyleProp::Value(Units::Auto),
                 width: StyleProp::Value(Units::Stretch(1.0)),
                 ..KStyle::default()
-            },
+            }),
             widget_name: TodoItemsProps::default().get_name(),
         }
     }
@@ -75,26 +77,26 @@ pub fn render_todo_items(
                         }}
                     >
                         <TextWidgetBundle
+                            styles={KStyle {
+                                right: StyleProp::Value(Units::Stretch(1.0)),
+                                top: StyleProp::Value(Units::Stretch(1.0)),
+                                bottom: StyleProp::Value(Units::Stretch(1.0)),
+                                ..Default::default()
+                            }}
                             text={TextProps {
                                 content: content.clone(),
-                                user_styles: KStyle {
-                                    right: StyleProp::Value(Units::Stretch(1.0)),
-                                    top: StyleProp::Value(Units::Stretch(1.0)),
-                                    bottom: StyleProp::Value(Units::Stretch(1.0)),
-                                    ..Default::default()
-                                },
                                 ..Default::default()
                             }}
                         />
                         <KButtonBundle
+                            styles={KStyle {
+                                width: StyleProp::Value(Units::Pixels(32.0)),
+                                height: StyleProp::Value(Units::Pixels(32.0)),
+                                left: StyleProp::Value(Units::Pixels(15.0)),
+                                ..Default::default()
+                            }}
                             button={KButton {
                                 text: "X".into(),
-                                user_styles: KStyle {
-                                    width: StyleProp::Value(Units::Pixels(32.0)),
-                                    height: StyleProp::Value(Units::Pixels(32.0)),
-                                    left: StyleProp::Value(Units::Pixels(15.0)),
-                                    ..Default::default()
-                                }
                             }}
                             on_event={handle_click}
                         />
diff --git a/examples/vec.rs b/examples/vec.rs
index 4d9daf3316a2afd0a860358793400a86bde43b02..98c61427e034c1502b1cd5627c560046efb03dc3 100644
--- a/examples/vec.rs
+++ b/examples/vec.rs
@@ -1,5 +1,5 @@
 use bevy::prelude::*;
-use kayak_ui::prelude::{widgets::*, KStyle, *};
+use kayak_ui::prelude::{widgets::*, *};
 
 #[derive(Component, Default, PartialEq, Eq, Clone)]
 pub struct MyWidgetProps {}
@@ -40,7 +40,6 @@ impl Widget for MyWidgetProps {}
 #[derive(Bundle)]
 pub struct MyWidgetBundle {
     props: MyWidgetProps,
-    styles: KStyle,
     widget_name: WidgetName,
 }
 
@@ -48,7 +47,6 @@ impl Default for MyWidgetBundle {
     fn default() -> Self {
         Self {
             props: Default::default(),
-            styles: Default::default(),
             widget_name: MyWidgetProps::default().get_name(),
         }
     }
diff --git a/images/screen1.png b/images/screen1.png
index 6d6ef7380ad22715586e1d7d30953506271de2b0..9b5c473f90a3ab1f1368fb14e0cfb2c9851fede4 100644
Binary files a/images/screen1.png and b/images/screen1.png differ
diff --git a/kayak_font/Cargo.toml b/kayak_font/Cargo.toml
index 1d24693eb10705332968dd199b9abcd55e0e61ff..f326aa45e4afb0c5a59d84f3c7f65fefaf7920d7 100644
--- a/kayak_font/Cargo.toml
+++ b/kayak_font/Cargo.toml
@@ -18,6 +18,11 @@ bevy_renderer = ["bevy"]
 anyhow = { version = "1.0" }
 nanoserde = "0.1.30"
 unicode-segmentation = "1.10.0"
+num = "0.4"
+num-derive = "0.3"
+num-traits = "0.2"
+ttf-parser = "0.17"
+image = "0.24"
 
 # Provides UAX #14 line break segmentation
 xi-unicode = "0.3"
diff --git a/kayak_font/assets/roboto.kttf b/kayak_font/assets/roboto.kttf
new file mode 100644
index 0000000000000000000000000000000000000000..9c7d59c6ed7f1c15a4d89b38b98ae946f470213b
--- /dev/null
+++ b/kayak_font/assets/roboto.kttf
@@ -0,0 +1,5 @@
+{
+    "file": "roboto.ttf",
+    "char_range_start": "0x20",
+    "char_range_end": "0x7f"
+}
\ No newline at end of file
diff --git a/kayak_font/assets/roboto.kttf-cached.png b/kayak_font/assets/roboto.kttf-cached.png
new file mode 100644
index 0000000000000000000000000000000000000000..07d69a7ff42902c316d4d34295c32460a815e84e
Binary files /dev/null and b/kayak_font/assets/roboto.kttf-cached.png differ
diff --git a/kayak_font/assets/roboto.ttf b/kayak_font/assets/roboto.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..2b6392ffe8712b9c5450733320cd220d6c0f4bce
Binary files /dev/null and b/kayak_font/assets/roboto.ttf differ
diff --git a/kayak_font/examples/bevy.rs b/kayak_font/examples/bevy.rs
index 5eb6a1a737feddb92d39cc455164e673ee81c1d3..ac6413002da01dd77fef9096436c433bd93b8026 100644
--- a/kayak_font/examples/bevy.rs
+++ b/kayak_font/examples/bevy.rs
@@ -18,7 +18,7 @@ const FONT_SIZE: f32 = 24.0;
 const INITIAL_SIZE: Vec2 = Vec2::from_array([400.0, 300.0]);
 const INITIAL_POS: Vec2 = Vec2::from_array([-200.0, 0.0]);
 const INSTRUCTIONS: &str =
-    "Press 'A' and 'D' to shrink and grow the text box.\nPress 'Space' to cycle text alignment.";
+    "Press 'A' and 'D' to shrink and grow the text box.\nPress \"Space\" to cycle text alignment.";
 
 #[derive(Component)]
 struct Instructions;
@@ -26,7 +26,8 @@ struct Instructions;
 fn startup(mut commands: Commands, asset_server: Res<AssetServer>) {
     commands.spawn(Camera2dBundle::default());
 
-    let font_handle: Handle<KayakFont> = asset_server.load("roboto.kayak_font");
+    let font_handle: Handle<KayakFont> = asset_server.load("roboto.kttf");
+    // let font_handle: Handle<KayakFont> = asset_server.load("roboto.kayak_font");
 
     commands
         .spawn(Text {
diff --git a/kayak_font/roboto.kttf-cached.png b/kayak_font/roboto.kttf-cached.png
new file mode 100644
index 0000000000000000000000000000000000000000..07d69a7ff42902c316d4d34295c32460a815e84e
Binary files /dev/null and b/kayak_font/roboto.kttf-cached.png differ
diff --git a/kayak_font/src/atlas.rs b/kayak_font/src/atlas.rs
index 917163e47fe064462e52fc21a91f125ca6d4b0ed..3f094cdc5f069c2d8cdabf0ace6024f17dd959b7 100644
--- a/kayak_font/src/atlas.rs
+++ b/kayak_font/src/atlas.rs
@@ -6,6 +6,12 @@ pub enum SDFType {
     Msdf,
 }
 
+impl Default for SDFType {
+    fn default() -> Self {
+        Self::Msdf
+    }
+}
+
 #[derive(DeJson, Debug, Copy, Clone, PartialEq, Eq)]
 pub enum Origin {
     #[nserde(rename = "bottom")]
@@ -18,7 +24,13 @@ pub enum Origin {
     Top,
 }
 
-#[derive(DeJson, Debug, Copy, Clone, PartialEq)]
+impl Default for Origin {
+    fn default() -> Self {
+        Self::Bottom
+    }
+}
+
+#[derive(DeJson, Default, Debug, Copy, Clone, PartialEq)]
 pub struct Atlas {
     #[nserde(rename = "type")]
     pub sdf_type: SDFType,
diff --git a/kayak_font/src/bevy/font_texture.rs b/kayak_font/src/bevy/font_texture.rs
index 26110bf83642556c3e0064a0e1210f014c9dc5fc..1b4b43a2f379355eff394aca8b1bd66367066c6c 100644
--- a/kayak_font/src/bevy/font_texture.rs
+++ b/kayak_font/src/bevy/font_texture.rs
@@ -22,7 +22,7 @@ pub fn init_font_texture(
     let not_processed_fonts = not_processed.drain(..).collect::<Vec<_>>();
     for font_handle in not_processed_fonts {
         if let Some(font) = fonts.get(&font_handle) {
-            if let Some(mut texture) = images.get_mut(&font.atlas_image) {
+            if let Some(mut texture) = images.get_mut(font.image.get()) {
                 texture.texture_descriptor.format = TextureFormat::Rgba8Unorm;
                 texture.sampler_descriptor = ImageSampler::Descriptor(SamplerDescriptor {
                     label: Some("Present Sampler"),
diff --git a/kayak_font/src/bevy/loader.rs b/kayak_font/src/bevy/loader.rs
index 4bc0748f1fea1d51973d32c0c73efd4f103f5639..6e9c236e4ee50566280f52b5006ec1db3e3ec161 100644
--- a/kayak_font/src/bevy/loader.rs
+++ b/kayak_font/src/bevy/loader.rs
@@ -1,4 +1,4 @@
-use crate::{KayakFont, Sdf};
+use crate::{ImageType, KayakFont, Sdf};
 use bevy::asset::{AssetLoader, AssetPath, BoxedFuture, LoadContext, LoadedAsset};
 
 #[derive(Default)]
@@ -16,7 +16,7 @@ impl AssetLoader for KayakFontLoader {
             let atlas_image_path = AssetPath::new(path, None);
             let font = KayakFont::new(
                 Sdf::from_bytes(bytes),
-                load_context.get_handle(atlas_image_path.clone()),
+                ImageType::Atlas(load_context.get_handle(atlas_image_path.clone())),
             );
 
             let asset = LoadedAsset::new(font).with_dependency(atlas_image_path);
diff --git a/kayak_font/src/bevy/mod.rs b/kayak_font/src/bevy/mod.rs
index 660e2edf0912d16e91d5a4e4ae6d2fdf3465b151..d2140100b608b146a22422944f864ef9ec263c01 100644
--- a/kayak_font/src/bevy/mod.rs
+++ b/kayak_font/src/bevy/mod.rs
@@ -24,6 +24,7 @@ mod plugin {
     impl Plugin for KayakFontPlugin {
         fn build(&self, app: &mut bevy::prelude::App) {
             app.add_asset::<KayakFont>()
+                .add_asset_loader(crate::ttf::loader::TTFLoader)
                 .add_asset_loader(KayakFontLoader)
                 .add_system(init_font_texture);
 
diff --git a/kayak_font/src/bevy/renderer/extract.rs b/kayak_font/src/bevy/renderer/extract.rs
index 94bc95cc0eaa91735abed21fb07532e1e9005669..d770ee67e59e65c1ce5d4b0ebc8766e4f50d6c2b 100644
--- a/kayak_font/src/bevy/renderer/extract.rs
+++ b/kayak_font/src/bevy/renderer/extract.rs
@@ -46,7 +46,7 @@ pub(crate) fn extract_fonts(
 
     for handle in changed_assets {
         let font_asset = font_assets.get(&handle).unwrap();
-        if let Some(image) = textures.get(&font_asset.atlas_image) {
+        if let Some(image) = textures.get(font_asset.image.get()) {
             if !image
                 .texture_descriptor
                 .usage
diff --git a/kayak_font/src/bevy/renderer/font_texture_cache.rs b/kayak_font/src/bevy/renderer/font_texture_cache.rs
index 5bd2981f22157999520cd5484bedfccff7741b0b..ec1baecfc02a7489c1f2b4f9e5e67b783ea1f21c 100644
--- a/kayak_font/src/bevy/renderer/font_texture_cache.rs
+++ b/kayak_font/src/bevy/renderer/font_texture_cache.rs
@@ -1,4 +1,4 @@
-use crate::{KayakFont, Sdf};
+use crate::{ImageType, KayakFont, Sdf};
 use bevy::{
     math::Vec2,
     prelude::{Handle, Res, Resource},
@@ -73,20 +73,34 @@ impl FontTextureCache {
         for kayak_font_handle in new_fonts {
             let mut was_processed = true;
             if let Some(font) = self.fonts.get(&kayak_font_handle) {
-                if let Some(atlas_texture) = render_images.get(&font.atlas_image) {
-                    Self::create_from_atlas(
-                        &mut self.images,
-                        &mut self.bind_groups,
-                        &font.sdf,
-                        kayak_font_handle.clone_weak(),
-                        device,
-                        queue,
-                        pipeline,
-                        atlas_texture,
-                        font.sdf.max_glyph_size().into(),
-                    );
+                if matches!(font.image, ImageType::Array(..)) {
+                    if let Some(array_texture) = render_images.get(font.image.get()) {
+                        Self::create_from_array(
+                            &mut self.bind_groups,
+                            kayak_font_handle.clone_weak(),
+                            device,
+                            pipeline,
+                            array_texture,
+                        );
+                    } else {
+                        was_processed = false;
+                    }
                 } else {
-                    was_processed = false;
+                    if let Some(atlas_texture) = render_images.get(font.image.get()) {
+                        Self::create_from_atlas(
+                            &mut self.images,
+                            &mut self.bind_groups,
+                            &font.sdf,
+                            kayak_font_handle.clone_weak(),
+                            device,
+                            queue,
+                            pipeline,
+                            atlas_texture,
+                            font.sdf.max_glyph_size().into(),
+                        );
+                    } else {
+                        was_processed = false;
+                    }
                 }
             }
             if !was_processed {
@@ -300,4 +314,30 @@ impl FontTextureCache {
         let command_buffer = command_encoder.finish();
         queue.submit(vec![command_buffer]);
     }
+
+    pub fn create_from_array<T: FontRenderingPipeline>(
+        bind_groups: &mut HashMap<Handle<KayakFont>, BindGroup>,
+        font_handle: Handle<KayakFont>,
+        device: &RenderDevice,
+        pipeline: &T,
+        array_texture: &GpuImage,
+    ) {
+        // create bind group
+        let binding = device.create_bind_group(&BindGroupDescriptor {
+            label: Some("text_image_bind_group"),
+            entries: &[
+                BindGroupEntry {
+                    binding: 0,
+                    resource: BindingResource::TextureView(&array_texture.texture_view),
+                },
+                BindGroupEntry {
+                    binding: 1,
+                    resource: BindingResource::Sampler(&array_texture.sampler),
+                },
+            ],
+            layout: pipeline.get_font_image_layout(),
+        });
+
+        bind_groups.insert(font_handle.clone_weak(), binding);
+    }
 }
diff --git a/kayak_font/src/font.rs b/kayak_font/src/font.rs
index 38d4c020972d8f46a4c8e1655b2e73105f43a92a..49b170fe97058c2c63df23f335bfd3abf2e18b85 100644
--- a/kayak_font/src/font.rs
+++ b/kayak_font/src/font.rs
@@ -14,12 +14,28 @@ use crate::{
 #[uuid = "4fe4732c-6731-49bb-bafc-4690d636b848"]
 pub struct KayakFont {
     pub sdf: Sdf,
-    pub atlas_image: Handle<Image>,
+    pub image: ImageType,
     pub missing_glyph: Option<char>,
     char_ids: HashMap<char, u32>,
     max_glyph_size: (f32, f32),
 }
 
+#[cfg(feature = "bevy_renderer")]
+#[derive(Debug, Clone, PartialEq)]
+pub enum ImageType {
+    Atlas(Handle<Image>),
+    Array(Handle<Image>),
+}
+
+impl ImageType {
+    pub fn get(&self) -> &Handle<Image> {
+        match self {
+            Self::Atlas(handle) => handle,
+            Self::Array(handle) => handle,
+        }
+    }
+}
+
 #[cfg(not(feature = "bevy_renderer"))]
 #[derive(Debug, Clone)]
 pub struct KayakFont {
@@ -30,7 +46,7 @@ pub struct KayakFont {
 }
 
 impl KayakFont {
-    pub fn new(sdf: Sdf, #[cfg(feature = "bevy_renderer")] atlas_image: Handle<Image>) -> Self {
+    pub fn new(sdf: Sdf, #[cfg(feature = "bevy_renderer")] image_type: ImageType) -> Self {
         let max_glyph_size = sdf.max_glyph_size();
         assert!(
             sdf.glyphs.len() < u32::MAX as usize,
@@ -55,7 +71,7 @@ impl KayakFont {
         Self {
             sdf,
             #[cfg(feature = "bevy_renderer")]
-            atlas_image,
+            image: image_type,
             missing_glyph,
             char_ids,
             max_glyph_size,
@@ -260,7 +276,6 @@ impl KayakFont {
                 rect.position.0 += shift_x;
             }
         }
-
         TextLayout::new(glyph_rects, lines, size, properties)
     }
 
diff --git a/kayak_font/src/lib.rs b/kayak_font/src/lib.rs
index 48e8cdddc414c84ebead34c3c3bac47959ceafd2..30e246008d37a7c67aee7fc29d05a37d0c6867ca 100644
--- a/kayak_font/src/lib.rs
+++ b/kayak_font/src/lib.rs
@@ -3,7 +3,9 @@ mod font;
 mod glyph;
 mod layout;
 mod metrics;
+mod msdf;
 mod sdf;
+mod ttf;
 mod utility;
 
 pub use atlas::*;
@@ -18,14 +20,17 @@ pub mod bevy;
 
 #[cfg(test)]
 mod tests {
-    use crate::{Alignment, KayakFont, Sdf, TextProperties};
+    use crate::{Alignment, ImageType, KayakFont, Sdf, TextProperties};
 
     fn make_font() -> KayakFont {
         let bytes = std::fs::read("assets/roboto.kayak_font")
             .expect("a `roboto.kayak_font` file in the `assets/` directory of this crate");
 
         #[cfg(feature = "bevy_renderer")]
-        return KayakFont::new(Sdf::from_bytes(&bytes), bevy::asset::Handle::default());
+        return KayakFont::new(
+            Sdf::from_bytes(&bytes),
+            ImageType::Atlas(bevy::asset::Handle::default()),
+        );
 
         #[cfg(not(feature = "bevy_renderer"))]
         return KayakFont::new(Sdf::from_bytes(&bytes));
diff --git a/kayak_font/src/metrics.rs b/kayak_font/src/metrics.rs
index 83d307f6206bb9231492057ea2d960fda28306b6..d610e29a49ca51f0194bad2e4e2e20dadb80e1c4 100644
--- a/kayak_font/src/metrics.rs
+++ b/kayak_font/src/metrics.rs
@@ -1,6 +1,6 @@
 use nanoserde::DeJson;
 
-#[derive(DeJson, Debug, Copy, Clone, PartialEq)]
+#[derive(DeJson, Default, Debug, Copy, Clone, PartialEq)]
 pub struct Metrics {
     #[nserde(rename = "emSize")]
     em_size: f32,
diff --git a/kayak_font/src/msdf/bitmap.rs b/kayak_font/src/msdf/bitmap.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4bbe4737d11265fcfc71bbe176c1d3c2524d2ddf
--- /dev/null
+++ b/kayak_font/src/msdf/bitmap.rs
@@ -0,0 +1,80 @@
+#![allow(dead_code)]
+
+#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
+pub struct FloatRGB {
+    pub r: f32,
+    pub g: f32,
+    pub b: f32,
+}
+
+impl FloatRGB {
+    pub fn new(r: f32, g: f32, b: f32) -> Self {
+        Self { r, g, b }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct FloatBmp {
+    buffer: Vec<f32>,
+    w: usize,
+    h: usize,
+}
+
+impl FloatBmp {
+    pub fn new(w: usize, h: usize) -> Self {
+        Self {
+            buffer: Vec::with_capacity(w * h),
+            w,
+            h,
+        }
+    }
+
+    pub fn width(&self) -> usize {
+        self.w
+    }
+
+    pub fn height(&self) -> usize {
+        self.h
+    }
+
+    pub fn set_pixel(&mut self, x: usize, y: usize, value: f32) {
+        self.buffer[x + (y * self.w)] = value;
+    }
+
+    pub fn get_pixel(&self, x: usize, y: usize) -> f32 {
+        self.buffer[x + (y * self.w)]
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct FloatRGBBmp {
+    pub buffer: Vec<FloatRGB>,
+    w: usize,
+    h: usize,
+}
+
+impl FloatRGBBmp {
+    pub fn new(w: usize, h: usize) -> Self {
+        Self {
+            buffer: vec![FloatRGB::new(0.0, 0.0, 0.0); w * h],
+            w,
+            h,
+        }
+    }
+
+    pub fn width(&self) -> usize {
+        self.w
+    }
+
+    pub fn height(&self) -> usize {
+        self.h
+    }
+
+    pub fn set_pixel(&mut self, x: usize, y: usize, value: FloatRGB) {
+        self.buffer[x + (y * self.w)] = value;
+    }
+
+    pub fn get_pixel(&self, x: usize, y: usize) -> FloatRGB {
+        self.buffer[x + (y * self.w)]
+    }
+}
diff --git a/kayak_font/src/msdf/contour.rs b/kayak_font/src/msdf/contour.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dc3f55adbf13e115851d8abbfa8609b29028db95
--- /dev/null
+++ b/kayak_font/src/msdf/contour.rs
@@ -0,0 +1,199 @@
+#![allow(dead_code)]
+
+use crate::msdf::{edge_segment::EdgeSegment, vector::Vector2, EdgeColor};
+
+#[derive(Debug, Default, Clone)]
+pub struct Contour {
+    pub edges: Vec<EdgeSegment>,
+    has_calculated_bounds: bool,
+    bounds_left: f64,
+    bounds_right: f64,
+    bounds_top: f64,
+    bounds_bottom: f64,
+}
+
+impl Contour {
+    pub fn new() -> Self {
+        Self {
+            edges: Vec::new(),
+            has_calculated_bounds: false,
+            bounds_left: 0.0,
+            bounds_right: 0.0,
+            bounds_top: 0.0,
+            bounds_bottom: 0.0,
+        }
+    }
+
+    pub fn add_edge(&mut self, edge: EdgeSegment) -> &EdgeSegment {
+        self.edges.push(edge);
+        self.edges.last().unwrap()
+    }
+
+    pub fn add_line(&mut self, x0: f64, y0: f64, x1: f64, y1: f64) -> &EdgeSegment {
+        self.add_edge(EdgeSegment::new_linear(
+            Vector2::new(x0, y0),
+            Vector2::new(x1, y1),
+            EdgeColor::WHITE,
+        ))
+    }
+
+    pub fn add_quadratic_segment(
+        &mut self,
+        x0: f64,
+        y0: f64,
+        x1: f64,
+        y1: f64,
+        x2: f64,
+        y2: f64,
+    ) -> &EdgeSegment {
+        self.add_edge(EdgeSegment::new_quadratic(
+            Vector2::new(x0, y0),
+            Vector2::new(x1, y1),
+            Vector2::new(x2, y2),
+            EdgeColor::WHITE,
+        ))
+    }
+
+    pub fn add_cubic_segment(
+        &mut self,
+        x0: f64,
+        y0: f64,
+        x1: f64,
+        y1: f64,
+        x2: f64,
+        y2: f64,
+        x3: f64,
+        y3: f64,
+    ) -> &EdgeSegment {
+        self.add_edge(EdgeSegment::new_cubic(
+            Vector2::new(x0, y0),
+            Vector2::new(x1, y1),
+            Vector2::new(x2, y2),
+            Vector2::new(x3, y3),
+            EdgeColor::WHITE,
+        ))
+    }
+
+    pub fn find_bounds(
+        &mut self,
+        left: &mut f64,
+        bottom: &mut f64,
+        right: &mut f64,
+        top: &mut f64,
+    ) {
+        if !self.has_calculated_bounds {
+            self.bounds_left = std::f64::MAX;
+            self.bounds_right = std::f64::MIN;
+            self.bounds_top = std::f64::MIN;
+            self.bounds_bottom = std::f64::MAX;
+
+            for edge in self.edges.iter() {
+                edge.find_bounds(
+                    &mut self.bounds_left,
+                    &mut self.bounds_bottom,
+                    &mut self.bounds_right,
+                    &mut self.bounds_top,
+                );
+            }
+            self.has_calculated_bounds = true;
+        }
+        if self.bounds_left > *left {
+            *left = self.bounds_left;
+        }
+        if self.bounds_right < *right {
+            *right = self.bounds_right;
+        }
+        if self.bounds_bottom < *bottom {
+            *bottom = self.bounds_bottom;
+        }
+        if self.bounds_top > *top {
+            *top = self.bounds_top;
+        }
+    }
+
+    pub fn winding(&self) -> i32 {
+        let mut total: f64 = 0.0;
+        match self.edges.len() {
+            0 => {
+                return 0;
+            }
+            1 => {
+                let a = self.edges[0].point(0.0);
+                let b = self.edges[0].point(1.0 / 3.0);
+                let c = self.edges[0].point(2.0 / 3.0);
+                total += Vector2::shoelace(a, b);
+                total += Vector2::shoelace(b, c);
+                total += Vector2::shoelace(c, a);
+            }
+            2 => {
+                let a = self.edges[0].point(0.0);
+                let b = self.edges[0].point(0.5);
+                let c = self.edges[1].point(0.0);
+                let d = self.edges[1].point(0.5);
+
+                total += Vector2::shoelace(a, b);
+                total += Vector2::shoelace(b, c);
+                total += Vector2::shoelace(c, d);
+                total += Vector2::shoelace(d, a);
+            }
+            _ => {
+                let mut prev = self.edges.last().unwrap().point(0.0);
+                for edge in self.edges.iter() {
+                    let cur = edge.point(0.0);
+                    total += Vector2::shoelace(prev, cur);
+                    prev = cur;
+                }
+            }
+        }
+        return Vector2::sign(total) as i32;
+    }
+
+    pub fn bound_miters(
+        &self,
+        l: &mut f64,
+        b: &mut f64,
+        r: &mut f64,
+        t: &mut f64,
+        border: f64,
+        miter_limit: f64,
+        polarity: i32,
+    ) {
+        if self.edges.is_empty() {
+            return;
+        }
+
+        let mut prev_dir = self.edges.last().unwrap().direction(1.0).normalize(true);
+
+        for edge in self.edges.iter() {
+            let mut dir = edge.direction(0.0).normalize(true);
+            dir = Vector2::new(-dir.x, -dir.y);
+
+            if polarity as f64 * Vector2::cross_product(prev_dir, dir) >= 0.0 {
+                let miter_length;
+                let q = 0.5 * (1.0 - Vector2::dot_product(prev_dir, dir));
+                if q > 0.0 {
+                    miter_length = (1.0 / q.sqrt()).min(miter_limit);
+                    let miter =
+                        edge.point(0.0) + border * miter_length * (prev_dir + dir).normalize(true);
+                    bound_point(l, b, r, t, miter);
+                }
+            }
+            prev_dir = edge.direction(1.0).normalize(true);
+        }
+    }
+}
+
+fn bound_point(l: &mut f64, b: &mut f64, r: &mut f64, t: &mut f64, p: Vector2) {
+    if p.x < *l {
+        *l = p.x;
+    }
+    if p.y < *b {
+        *b = p.y;
+    }
+    if p.x > *r {
+        *r = p.x;
+    }
+    if p.y > *t {
+        *t = p.y;
+    }
+}
diff --git a/kayak_font/src/msdf/edge_coloring.rs b/kayak_font/src/msdf/edge_coloring.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b74f326fc602802d9baa702e7965034010fdf7c4
--- /dev/null
+++ b/kayak_font/src/msdf/edge_coloring.rs
@@ -0,0 +1,307 @@
+#![allow(dead_code)]
+
+use crate::msdf::{edge_segment::EdgeSegment, shape::Shape, vector::Vector2, EdgeColor};
+
+fn is_corner(a_dir: Vector2, b_dir: Vector2, cross_threshold: f64) -> bool {
+    Vector2::dot_product(a_dir, b_dir) <= 0.0
+        || Vector2::cross_product(a_dir, b_dir).abs() > cross_threshold
+}
+
+const MSDFGEN_EDGE_LENGTH_PRECISION: usize = 4;
+
+fn estimate_edge_length(edge: &EdgeSegment) -> f64 {
+    let mut len = 0.0;
+    let mut prev = edge.point(0.0);
+    for i in 1..MSDFGEN_EDGE_LENGTH_PRECISION {
+        let cur = edge.point(1.0 / MSDFGEN_EDGE_LENGTH_PRECISION as f64 * i as f64);
+        len += (cur - prev).length();
+        prev = cur;
+    }
+    return len;
+}
+
+fn switch_color(color: &mut EdgeColor, seed: &mut usize, banned: EdgeColor) {
+    let combined: EdgeColor =
+        num::cast::FromPrimitive::from_usize(*color as usize & banned as usize).unwrap();
+
+    if combined == EdgeColor::RED || combined == EdgeColor::GREEN || combined == EdgeColor::BLUE {
+        *color =
+            num::cast::FromPrimitive::from_usize(combined as usize ^ EdgeColor::WHITE as usize)
+                .unwrap();
+        return;
+    }
+    if *color == EdgeColor::BLACK || *color == EdgeColor::WHITE {
+        match *seed % 3 {
+            0 => {
+                *color = EdgeColor::CYAN;
+            }
+            1 => {
+                *color = EdgeColor::MAGENTA;
+            }
+            2 => {
+                *color = EdgeColor::YELLOW;
+            }
+            _ => panic!("Not supported!"),
+        }
+
+        *seed /= 3;
+        return;
+    }
+
+    let shifted = (*color as usize) << (1 + (*seed & 1));
+    *color = num::cast::FromPrimitive::from_usize(
+        (shifted | shifted >> 3) & (EdgeColor::WHITE as usize),
+    )
+    .unwrap();
+    *seed >>= 1;
+}
+
+pub fn simple(shape: &mut Shape, angle_threshold: f64, mut seed: usize) {
+    let cross_threshold = angle_threshold.sin();
+    let mut corners = Vec::new();
+
+    for contour in shape.contours.iter_mut() {
+        corners.clear();
+
+        let edges = &mut contour.edges;
+
+        let edge_count = edges.len();
+        if edge_count != 0 {
+            let mut prev_dir = edges.last().unwrap().direction(1.0);
+            for i in 0..edge_count {
+                let edge = &edges[i];
+                if is_corner(
+                    prev_dir.normalize(false),
+                    edge.direction(0.0).normalize(false),
+                    cross_threshold,
+                ) {
+                    corners.push(i);
+                }
+                prev_dir = edge.direction(1.0);
+            }
+        }
+
+        if corners.len() == 0 {
+            for i in 0..edge_count {
+                edges[i].set_color(EdgeColor::WHITE);
+            }
+        } else if corners.len() == 1 {
+            let mut colors = vec![EdgeColor::WHITE, EdgeColor::WHITE, EdgeColor::BLACK];
+            switch_color(&mut colors[0], &mut seed, EdgeColor::BLACK);
+            colors[2] = colors[0];
+            switch_color(&mut colors[2], &mut seed, EdgeColor::BLACK);
+
+            let corner = corners[0];
+            if edge_count >= 3 {
+                let m = edge_count;
+                for i in 0..m {
+                    let lookup =
+                        ((3.0 + 2.875 * i as f64 / (m as f64 - 1.0) - 1.4375 + 0.5) as i32 - 3) + 1;
+                    contour.edges[(corner + i) % m].set_color(colors[lookup as usize]);
+                }
+            } else if edge_count >= 1 {
+                let mut parts = [EdgeSegment::default(); 7];
+
+                let (o1, o2, o3) = edges[0].split_in_thirds();
+                parts[0 + 3 * corner] = o1;
+                parts[1 + 3 * corner] = o2;
+                parts[2 + 3 * corner] = o3;
+
+                if edge_count >= 2 {
+                    let (o1, o2, o3) = edges[1].split_in_thirds();
+                    parts[3 - 3 * corner] = o1;
+                    parts[4 - 3 * corner] = o2;
+                    parts[5 - 3 * corner] = o3;
+                    parts[1].set_color(colors[0]);
+                    parts[0].set_color(parts[1].get_color());
+                    parts[3].set_color(colors[1]);
+                    parts[2].set_color(parts[3].get_color());
+                    parts[5].set_color(colors[2]);
+                    parts[4].set_color(parts[5].get_color());
+                } else {
+                    parts[0].set_color(colors[0]);
+                    parts[1].set_color(colors[1]);
+                    parts[2].set_color(colors[2]);
+                }
+                edges.clear();
+                for i in 0..7 {
+                    edges.push(parts[i]);
+                }
+            }
+        } else {
+            let corner_count = corners.len();
+            let mut spline = 0;
+            let start = corners[0];
+
+            let mut color = EdgeColor::WHITE;
+            switch_color(&mut color, &mut seed, EdgeColor::BLACK);
+            let initial_color = color;
+            for i in 0..edge_count {
+                let index = (start + i) % edge_count;
+                if spline + 1 < corner_count && corners[spline + 1] == index {
+                    spline += 1;
+                    let banned_color =
+                        (if spline == corner_count - 1 { 1 } else { 0 }) * initial_color as usize;
+                    switch_color(
+                        &mut color,
+                        &mut seed,
+                        num::cast::FromPrimitive::from_usize(banned_color).unwrap(),
+                    );
+                }
+                edges[index].set_color(color);
+            }
+        }
+    }
+}
+
+struct EdgeColoringInkTrapCorner {
+    pub index: i32,
+    pub prev_edge_length_estimate: f64,
+    pub minor: bool,
+    pub color: EdgeColor,
+    pub spline_length: f64,
+}
+
+pub fn ink_trap(shape: &mut Shape, angle_threshold: f64, mut seed: usize) {
+    let cross_threshold = angle_threshold.sin();
+    let mut corners = Vec::new();
+    for contour in shape.contours.iter_mut() {
+        let mut spline_length = 0.0;
+        corners.clear();
+        if !contour.edges.is_empty() {
+            let mut prev_direction = contour.edges.last().unwrap().direction(1.0);
+            let mut index = 0;
+            for edge in contour.edges.iter() {
+                if is_corner(
+                    prev_direction.normalize(false),
+                    edge.direction(0.0).normalize(false),
+                    cross_threshold,
+                ) {
+                    let corner = EdgeColoringInkTrapCorner {
+                        index,
+                        spline_length,
+                        color: EdgeColor::BLACK,
+                        prev_edge_length_estimate: 0.0,
+                        minor: false,
+                    };
+                    corners.push(corner);
+                    spline_length = 0.0;
+                }
+                spline_length += estimate_edge_length(edge);
+                prev_direction = edge.direction(1.0);
+                index += 1;
+            }
+        }
+
+        if corners.is_empty() {
+            for edge in contour.edges.iter_mut() {
+                edge.set_color(EdgeColor::WHITE);
+            }
+        } else if corners.len() == 1 {
+            let mut colors = vec![EdgeColor::WHITE, EdgeColor::WHITE, EdgeColor::BLACK];
+            switch_color(&mut colors[0], &mut seed, EdgeColor::BLACK);
+            colors[2] = colors[0];
+            switch_color(&mut colors[2], &mut seed, EdgeColor::BLACK);
+            let corner = corners[0].index as usize;
+            if contour.edges.len() >= 3 {
+                let m = contour.edges.len();
+                for i in 0..m {
+                    let lookup =
+                        ((3.0 + 2.875 * i as f64 / (m as f64 - 1.0) - 1.4375 + 0.5) as i32 - 3) + 1;
+                    contour.edges[(corner + i) % m].set_color(colors[lookup as usize]);
+                }
+            } else if contour.edges.len() >= 1 {
+                let mut parts = vec![EdgeSegment::default(); 7];
+                let (o1, o2, o3) = contour.edges[0].split_in_thirds();
+                parts[0 + 3 * corner] = o1;
+                parts[1 + 3 * corner] = o2;
+                parts[2 + 3 * corner] = o3;
+                if contour.edges.len() >= 2 {
+                    let (o1, o2, o3) = contour.edges[1].split_in_thirds();
+                    parts[3 - 3 * corner] = o1;
+                    parts[4 - 3 * corner] = o2;
+                    parts[5 - 3 * corner] = o3;
+                    parts[1].set_color(colors[0]);
+                    let part1_color = parts[1].get_color();
+                    parts[0].set_color(part1_color);
+                    parts[3].set_color(colors[1]);
+                    let part3_color = parts[3].get_color();
+                    parts[2].set_color(part3_color);
+                    parts[5].set_color(colors[2]);
+                    let part5_color = parts[5].get_color();
+                    parts[4].set_color(part5_color);
+                } else {
+                    parts[0].set_color(colors[0]);
+                    parts[1].set_color(colors[1]);
+                    parts[2].set_color(colors[2]);
+                }
+                contour.edges.clear();
+                for part in parts.into_iter() {
+                    contour.edges.push(part);
+                }
+            } else {
+                let corner_count = corners.len();
+                let mut major_corner_count = corner_count;
+
+                if corner_count > 3 {
+                    corners.first_mut().unwrap().prev_edge_length_estimate += spline_length;
+                    for i in 0..corner_count {
+                        if corners[i].prev_edge_length_estimate
+                            > corners[(i + 1) % corner_count].prev_edge_length_estimate
+                            && corners[(i + 1) % corner_count].prev_edge_length_estimate
+                                < corners[(i + 2) % corner_count].prev_edge_length_estimate
+                        {
+                            corners[i].minor = true;
+                            major_corner_count -= 1;
+                        }
+                    }
+
+                    let mut color = EdgeColor::WHITE;
+                    let mut initial_color = EdgeColor::BLACK;
+                    for i in 0..corner_count {
+                        if !corners[i].minor {
+                            major_corner_count -= 1;
+                            switch_color(
+                                &mut color,
+                                &mut seed,
+                                num::cast::FromPrimitive::from_usize(
+                                    !major_corner_count * initial_color as usize,
+                                )
+                                .unwrap(),
+                            );
+                            corners[i].color = color;
+                            if initial_color != EdgeColor::BLACK {
+                                initial_color = color;
+                            }
+                        }
+                    }
+                    for i in 0..corner_count {
+                        if corners[i].minor {
+                            let next_color = corners[(i + 1) % corner_count].color;
+                            corners[i].color = num::cast::FromPrimitive::from_usize(
+                                (color as usize & next_color as usize) ^ EdgeColor::WHITE as usize,
+                            )
+                            .unwrap();
+                        } else {
+                            color = corners[i].color;
+                        }
+                    }
+
+                    let mut spline = 0;
+                    let start = corners[0].index as usize;
+                    let mut color = corners[0].color;
+                    let m = contour.edges.len();
+                    for i in 0..m {
+                        let index = (start + i) % m;
+                        if spline + 1 < corner_count && corners[spline + 1].index as usize == index
+                        {
+                            spline += 1;
+                            color = corners[spline].color;
+                        }
+                        contour.edges[index].set_color(color);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/kayak_font/src/msdf/edge_point.rs b/kayak_font/src/msdf/edge_point.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0fe5d674d5a6a2cc170d180a45816e38d6c4792d
--- /dev/null
+++ b/kayak_font/src/msdf/edge_point.rs
@@ -0,0 +1,17 @@
+use crate::msdf::{edge_segment::EdgeSegment, signed_distance::SignedDistance};
+
+#[derive(Debug, Clone, Copy)]
+pub struct EdgePoint {
+    pub min_distance: SignedDistance,
+    pub near_edge: Option<EdgeSegment>,
+    pub near_param: f64,
+}
+
+impl EdgePoint {
+    // pub fn calculate_contour_color(&mut self, p: Vector2) -> f64 {
+    //     if let Some(near_edge) = self.near_edge {
+    //         near_edge.distance_to_pseudo_distance(&mut self.min_distance, p, self.near_param);
+    //     }
+    //     return self.min_distance.distance;
+    // }
+}
diff --git a/kayak_font/src/msdf/edge_segment/cubic.rs b/kayak_font/src/msdf/edge_segment/cubic.rs
new file mode 100644
index 0000000000000000000000000000000000000000..21444bfa057dc957a9b6cd2680826390bb9dd1d2
--- /dev/null
+++ b/kayak_font/src/msdf/edge_segment/cubic.rs
@@ -0,0 +1,205 @@
+use crate::msdf::{signed_distance::SignedDistance, vector::Vector2, EdgeColor};
+
+use super::{
+    equation_solver::{self, fabs},
+    mix, non_zero_sign, EdgeSegment,
+};
+
+pub const MSDFGEN_CUBIC_SEARCH_STARTS: usize = 4;
+pub const MSDFGEN_CUBIC_SEARCH_STEPS: usize = 4;
+
+pub fn direction(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, param: f64) -> Vector2 {
+    let tangent = mix(
+        mix(p1 - p0, p2 - p1, param),
+        mix(p2 - p1, p3 - p2, param),
+        param,
+    );
+    if !tangent.is_zero() {
+        if param == 0.0 {
+            return p2 - p0;
+        }
+        if param == 1.0 {
+            return p3 - p1;
+        }
+    }
+    tangent
+}
+
+pub fn point(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, param: f64) -> Vector2 {
+    let p12 = mix(p1, p2, param);
+    mix(
+        mix(mix(p0, p1, param), p12, param),
+        mix(p12, mix(p2, p3, param), param),
+        param,
+    )
+}
+
+pub fn find_bounds(
+    p0: Vector2,
+    p1: Vector2,
+    p2: Vector2,
+    p3: Vector2,
+    l: &mut f64,
+    b: &mut f64,
+    r: &mut f64,
+    t: &mut f64,
+) {
+    Vector2::point_bounds(p0, l, b, r, t);
+    Vector2::point_bounds(p3, l, b, r, t);
+
+    let a0 = p1 - p0;
+    let a1 = 2.0 * (p2 - p1 - a0);
+    let a2 = p3 - 3.0 * p2 + 3.0 * p1 - p0;
+
+    let (solutions, result) = equation_solver::solve_quadratic(a2.x, a1.x, a0.x);
+    for i in 0..solutions {
+        let par = result[i as usize];
+        if par > 0.0 && par < 1.0 {
+            Vector2::point_bounds(point(p0, p1, p2, p3, par), l, b, r, t);
+        }
+    }
+
+    let (solutions, result) = equation_solver::solve_quadratic(a2.y, a1.y, a0.y);
+
+    for i in 0..solutions {
+        let par = result[i as usize];
+        if par > 0.0 && par < 1.0 {
+            Vector2::point_bounds(point(p0, p1, p2, p3, par), l, b, r, t);
+        }
+    }
+}
+
+pub fn split_in_thirds(
+    p0: Vector2,
+    p1: Vector2,
+    p2: Vector2,
+    p3: Vector2,
+    color: EdgeColor,
+) -> (EdgeSegment, EdgeSegment, EdgeSegment) {
+    (
+        EdgeSegment::new_cubic(
+            p0,
+            if p0 == p1 { p0 } else { mix(p0, p1, 1.0 / 3.0) },
+            mix(mix(p0, p1, 1.0 / 3.0), mix(p1, p2, 1.0 / 3.0), 1.0 / 3.0),
+            point(p0, p1, p2, p3, 1.0 / 3.0),
+            color,
+        ),
+        EdgeSegment::new_cubic(
+            point(p0, p1, p2, p3, 1.0 / 3.0),
+            mix(
+                mix(mix(p0, p1, 1.0 / 3.0), mix(p1, p2, 1.0 / 3.0), 1.0 / 3.0),
+                mix(mix(p1, p2, 1.0 / 3.0), mix(p2, p3, 1.0 / 3.0), 1.0 / 3.0),
+                2.0 / 3.0,
+            ),
+            mix(
+                mix(mix(p0, p1, 2.0 / 3.0), mix(p1, p2, 2.0 / 3.0), 2.0 / 3.0),
+                mix(mix(p1, p2, 2.0 / 3.0), mix(p2, p3, 2.0 / 3.0), 2.0 / 3.0),
+                1.0 / 3.0,
+            ),
+            point(p0, p1, p2, p3, 2.0 / 3.0),
+            color,
+        ),
+        EdgeSegment::new_cubic(
+            point(p0, p1, p2, p3, 2.0 / 3.0),
+            mix(mix(p1, p2, 2.0 / 3.0), mix(p2, p3, 2.0 / 3.0), 2.0 / 3.0),
+            if p2 == p3 { p3 } else { mix(p2, p3, 2.0 / 3.0) },
+            p3,
+            color,
+        ),
+    )
+}
+
+pub fn signed_distance(
+    p0: Vector2,
+    p1: Vector2,
+    p2: Vector2,
+    p3: Vector2,
+    origin: Vector2,
+) -> (SignedDistance, f64) {
+    let qa = p0 - origin;
+    let ab = p1 - p0;
+    let br = p2 - p1 - ab;
+    let as_ = (p3 - p2) - (p2 - p1) - br;
+    let mut ep_dir = direction(p0, p1, p2, p3, 0.0);
+
+    let mut min_distance = non_zero_sign(Vector2::cross_product(ep_dir, qa)) as f64 * qa.length();
+    let mut param = -Vector2::dot_product(qa, ep_dir) / Vector2::dot_product(ep_dir, ep_dir);
+    {
+        ep_dir = direction(p0, p1, p2, p3, 1.0);
+        let distance = (p3 - origin).length();
+        if distance.abs() < min_distance.abs() {
+            min_distance =
+                non_zero_sign(Vector2::cross_product(ep_dir, p3 - origin)) as f64 * distance;
+            param = Vector2::dot_product(ep_dir - (p3 - origin), ep_dir)
+                / Vector2::dot_product(ep_dir, ep_dir);
+        }
+    }
+
+    for i in 0..MSDFGEN_CUBIC_SEARCH_STARTS {
+        let mut t = (i / MSDFGEN_CUBIC_SEARCH_STARTS) as f64;
+        let mut qe = qa + 3.0 * t * ab + 3.0 * t * t * br + t * t * t * as_;
+        for _ in 0..MSDFGEN_CUBIC_SEARCH_STEPS {
+            let d1 = 3.0 * ab + 6.0 * t * br + 3.0 * t * t * as_;
+            let d2 = 6.0 * br + 6.0 * t * as_;
+            t -= Vector2::dot_product(qe, d1)
+                / (Vector2::dot_product(d1, d1) + Vector2::dot_product(qe, d2));
+
+            if t < 0.0 || t > 1.0 {
+                break;
+            }
+
+            qe = qa + 3.0 * t * ab + 3.0 * t * t * br + t * t * t * as_;
+            let distance = qe.length();
+            if distance < min_distance.abs() {
+                min_distance = non_zero_sign(Vector2::cross_product(d1, qe)) as f64 * distance;
+                param = t;
+            }
+
+            // let qpt = point(p0, p1, p2, p3, t) - origin;
+            // let distance = non_zero_sign(Vector2::cross_product(direction(p0, p1, p2, p3, t), qpt))
+            //     as f64
+            //     * qpt.length();
+
+            // if fabs(distance) < fabs(min_distance) {
+            //     min_distance = distance;
+            //     param = t;
+            // }
+            // if step == MSDFGEN_CUBIC_SEARCH_STEPS {
+            //     break;
+            // }
+            // let d1 = 3.0 * as_ * t * t + 6.0 * br * t + 3.0 * ab;
+            // let d2 = 6.0 * as_ * t + 6.0 * br;
+            // t -= Vector2::dot_product(qpt, d1)
+            //     / (Vector2::dot_product(d1, d1) + Vector2::dot_product(qpt, d2));
+            // if t < 0.0 || t > 1.0 {
+            //     break;
+            // }
+        }
+    }
+
+    if param >= 0.0 && param <= 1.0 {
+        return (SignedDistance::new(min_distance, 0.0), param);
+    } else if param < 0.5 {
+        return (
+            SignedDistance::new(
+                min_distance,
+                fabs(Vector2::dot_product(
+                    direction(p0, p1, p2, p3, 0.0),
+                    qa.normalize(false),
+                )),
+            ),
+            param,
+        );
+    } else {
+        return (
+            SignedDistance::new(
+                min_distance,
+                fabs(Vector2::dot_product(
+                    direction(p0, p1, p2, p3, 1.0).normalize(false),
+                    (p3 - origin).normalize(false),
+                )),
+            ),
+            param,
+        );
+    }
+}
diff --git a/kayak_font/src/msdf/edge_segment/equation_solver.rs b/kayak_font/src/msdf/edge_segment/equation_solver.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6e71ee2bfe7a3b19a307819d59e9bc647fd85c09
--- /dev/null
+++ b/kayak_font/src/msdf/edge_segment/equation_solver.rs
@@ -0,0 +1,81 @@
+const EPSILON: f64 = 1.0e-14;
+
+pub fn fabs(v: f64) -> f64 {
+    v.abs()
+}
+
+pub fn solve_quadratic(a: f64, b: f64, c: f64) -> (i32, [f64; 3]) {
+    let mut result = [0.0; 3];
+
+    if fabs(a) < EPSILON {
+        if fabs(b) < EPSILON {
+            if c == 0.0 {
+                return (-1, result);
+            }
+            return (0, result);
+        }
+        result[0] = -c / b;
+        return (1, result);
+    }
+    let mut dscr = b * b - 4.0 * a * c;
+    if dscr > 0.0 {
+        dscr = dscr.sqrt();
+        result[0] = (-b + dscr) / (2.0 * a);
+        result[1] = (-b - dscr) / (2.0 * a);
+        return (2, result);
+    } else if dscr == 0.0 {
+        result[0] = -b / (2.0 * a);
+        return (1, result);
+    } else {
+        return (0, result);
+    }
+}
+
+pub fn solve_cubic_norm(mut a: f64, b: f64, c: f64) -> (i32, [f64; 3]) {
+    let mut result = [0.0; 3];
+    let a2 = a * a;
+    let mut q = (a2 - 3.0 * b) / 9.0;
+    let r = (a * (2.0 * a2 - 9.0 * b) + 27.0 * c) / 54.0;
+    let r2 = r * r;
+    let q3 = q * q * q;
+    let mut result_a;
+    let result_b;
+    if r2 < q3 {
+        let mut t = r / q3.sqrt();
+        if t < -1.0 {
+            t = -1.0;
+        }
+        if t > 1.0 {
+            t = 1.0;
+        }
+        t = t.acos();
+        a /= 3.0;
+        q = -2.0 * q.sqrt();
+        result[0] = q * (t / 3.0).cos() - a;
+        result[1] = q * ((t + 2.0 * std::f64::consts::PI) / 3.0).cos() - a;
+        result[2] = q * ((t - 2.0 * std::f64::consts::PI) / 3.0).cos() - a;
+        return (3, result);
+    } else {
+        result_a = -(fabs(r) + (r2 - q3).sqrt()).powf(1.0 / 3.0);
+        if r < 0.0 {
+            result_a = -result_a
+        };
+        result_b = if result_a == 0.0 { 0.0 } else { q / result_a };
+        a /= 3.0;
+        result[0] = (result_a + result_b) - a;
+        result[1] = -0.5 * (result_a + result_b) - a;
+        result[2] = 0.5 * 3.0f64.sqrt() * (result_a - result_b);
+        if fabs(result[2]) < EPSILON {
+            return (2, result);
+        }
+        return (1, result);
+    }
+}
+
+pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> (i32, [f64; 3]) {
+    if fabs(a) < EPSILON {
+        solve_quadratic(b, c, d)
+    } else {
+        solve_cubic_norm(b / a, c / a, d / a)
+    }
+}
diff --git a/kayak_font/src/msdf/edge_segment/line.rs b/kayak_font/src/msdf/edge_segment/line.rs
new file mode 100644
index 0000000000000000000000000000000000000000..639b262c8f5aa93934e331476ac0bc83dd896c03
--- /dev/null
+++ b/kayak_font/src/msdf/edge_segment/line.rs
@@ -0,0 +1,50 @@
+use crate::msdf::{signed_distance::SignedDistance, vector::Vector2, EdgeColor};
+
+use super::{mix, non_zero_sign, EdgeSegment};
+
+pub fn direction(p0: Vector2, p1: Vector2, _param: f64) -> Vector2 {
+    p1 - p0
+}
+
+pub fn point(p0: Vector2, p1: Vector2, param: f64) -> Vector2 {
+    mix(p0, p1, param)
+}
+
+pub fn find_bounds(p0: Vector2, p1: Vector2, l: &mut f64, b: &mut f64, r: &mut f64, t: &mut f64) {
+    Vector2::point_bounds(p0, l, b, r, t);
+    Vector2::point_bounds(p1, l, b, r, t);
+}
+
+pub fn split_in_thirds(
+    p0: Vector2,
+    p1: Vector2,
+    color: EdgeColor,
+) -> (EdgeSegment, EdgeSegment, EdgeSegment) {
+    (
+        EdgeSegment::new_linear(p0, point(p0, p1, 1.0 / 3.0), color),
+        EdgeSegment::new_linear(point(p0, p1, 1.0 / 3.0), point(p0, p1, 2.0 / 3.0), color),
+        EdgeSegment::new_linear(point(p0, p1, 2.0 / 3.0), p1, color),
+    )
+}
+
+pub fn signed_distance(p0: Vector2, p1: Vector2, origin: Vector2) -> (SignedDistance, f64) {
+    let aq = origin - p0;
+    let ab = p1 - p0;
+    let param = Vector2::dot_product(aq, ab) / Vector2::dot_product(ab, ab);
+
+    let eq = (if param > 0.5 { p1 } else { p0 }) - origin;
+    let endpoint_distance = eq.length();
+    if param > 0.0 && param < 1.0 {
+        let ortho_distance = Vector2::dot_product(ab.get_ortho_normal(false, false), aq);
+        if ortho_distance.abs() < endpoint_distance {
+            return (SignedDistance::new(ortho_distance, 0.0), param);
+        }
+    }
+    return (
+        SignedDistance::new(
+            non_zero_sign(Vector2::cross_product(aq, ab)) as f64 * endpoint_distance,
+            Vector2::dot_product(ab.normalize(false), eq.normalize(false)).abs(),
+        ),
+        param,
+    );
+}
diff --git a/kayak_font/src/msdf/edge_segment/mod.rs b/kayak_font/src/msdf/edge_segment/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..41411b487fe3213046cb41ee2268910f6117192f
--- /dev/null
+++ b/kayak_font/src/msdf/edge_segment/mod.rs
@@ -0,0 +1,190 @@
+use crate::msdf::{signed_distance::SignedDistance, vector::Vector2, EdgeColor};
+
+mod cubic;
+mod equation_solver;
+mod line;
+mod quadratic;
+
+pub fn non_zero_sign(n: f64) -> i32 {
+    return 2 * (if n > 0.0 { 1 } else { 0 }) - 1;
+}
+pub fn mix(a: Vector2, b: Vector2, weight: f64) -> Vector2 {
+    Vector2::new(
+        (1.0 - weight) * a.x + (weight * b.x),
+        (1.0 - weight) * a.y + (weight * b.y),
+    )
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum EdgeSegment {
+    LineSegment {
+        color: EdgeColor,
+        p0: Vector2,
+        p1: Vector2,
+    },
+    QuadraticSegment {
+        color: EdgeColor,
+        p0: Vector2,
+        p1: Vector2,
+        p2: Vector2,
+    },
+    CubicSegment {
+        color: EdgeColor,
+        p0: Vector2,
+        p1: Vector2,
+        p2: Vector2,
+        p3: Vector2,
+    },
+}
+
+impl Default for EdgeSegment {
+    fn default() -> Self {
+        EdgeSegment::LineSegment {
+            color: EdgeColor::WHITE,
+            p0: Vector2::default(),
+            p1: Vector2::default(),
+        }
+    }
+}
+
+impl EdgeSegment {
+    pub fn new_linear(p0: Vector2, p1: Vector2, color: EdgeColor) -> Self {
+        Self::LineSegment { p0, p1, color }
+    }
+
+    pub fn new_quadratic(p0: Vector2, mut p1: Vector2, p2: Vector2, color: EdgeColor) -> Self {
+        if p1 == p0 || p1 == p2 {
+            p1 = 0.5 * (p0 + p2);
+        }
+        Self::QuadraticSegment { p0, p1, p2, color }
+    }
+
+    pub fn new_cubic(
+        p0: Vector2,
+        mut p1: Vector2,
+        mut p2: Vector2,
+        p3: Vector2,
+        color: EdgeColor,
+    ) -> Self {
+        if (p1 == p0 || p1 == p3) && (p2 == p0 || p2 == p3) {
+            p1 = mix(p0, p3, 1.0 / 3.0);
+            p2 = mix(p0, p3, 2.0 / 3.0);
+        }
+        Self::CubicSegment {
+            p0,
+            p1,
+            p2,
+            p3,
+            color,
+        }
+    }
+
+    pub fn distance_to_pseudo_distance(
+        &self,
+        distance: &mut SignedDistance,
+        origin: Vector2,
+        param: f64,
+    ) {
+        if param < 0.0 {
+            let dir = self.direction(0.0).normalize(false);
+            let aq = origin - self.point(0.0);
+            let ts = Vector2::dot_product(aq, dir);
+            if ts < 0.0 {
+                let pseudo_distance = Vector2::cross_product(aq, dir);
+                if pseudo_distance.abs() <= distance.distance.abs() {
+                    *distance = SignedDistance::new(pseudo_distance, 0.0);
+                }
+            }
+        } else if param > 1.0 {
+            let dir = self.direction(1.0).normalize(false);
+            let bq = origin - self.point(1.0);
+            let ts = Vector2::dot_product(bq, dir);
+            if ts > 0.0 {
+                let pseudo_distance = Vector2::cross_product(bq, dir);
+                if pseudo_distance.abs() <= distance.distance.abs() {
+                    *distance = SignedDistance::new(pseudo_distance, 0.0);
+                }
+            }
+        }
+    }
+
+    pub fn direction(&self, param: f64) -> Vector2 {
+        match *self {
+            Self::LineSegment { p0, p1, .. } => line::direction(p0, p1, param),
+            Self::QuadraticSegment { p0, p1, p2, .. } => quadratic::direction(p0, p1, p2, param),
+            Self::CubicSegment { p0, p1, p2, p3, .. } => cubic::direction(p0, p1, p2, p3, param),
+        }
+    }
+
+    pub fn point(&self, param: f64) -> Vector2 {
+        match *self {
+            Self::LineSegment { p0, p1, .. } => line::point(p0, p1, param),
+            Self::QuadraticSegment { p0, p1, p2, .. } => quadratic::point(p0, p1, p2, param),
+            Self::CubicSegment { p0, p1, p2, p3, .. } => cubic::point(p0, p1, p2, p3, param),
+        }
+    }
+
+    pub fn find_bounds(&self, l: &mut f64, b: &mut f64, r: &mut f64, t: &mut f64) {
+        match *self {
+            Self::LineSegment { p0, p1, .. } => line::find_bounds(p0, p1, l, b, r, t),
+            Self::QuadraticSegment { p0, p1, p2, .. } => {
+                quadratic::find_bounds(p0, p1, p2, l, b, r, t)
+            }
+            Self::CubicSegment { p0, p1, p2, p3, .. } => {
+                cubic::find_bounds(p0, p1, p2, p3, l, b, r, t)
+            }
+        }
+    }
+
+    pub fn split_in_thirds(&self) -> (EdgeSegment, EdgeSegment, EdgeSegment) {
+        match *self {
+            Self::LineSegment { p0, p1, color } => line::split_in_thirds(p0, p1, color),
+            Self::QuadraticSegment { p0, p1, p2, color } => {
+                quadratic::split_in_thirds(p0, p1, p2, color)
+            }
+            Self::CubicSegment {
+                p0,
+                p1,
+                p2,
+                p3,
+                color,
+            } => cubic::split_in_thirds(p0, p1, p2, p3, color),
+        }
+    }
+
+    pub fn signed_distance(&self, origin: Vector2) -> (SignedDistance, f64) {
+        match *self {
+            Self::LineSegment { p0, p1, .. } => line::signed_distance(p0, p1, origin),
+            Self::QuadraticSegment { p0, p1, p2, .. } => {
+                quadratic::signed_distance(p0, p1, p2, origin)
+            }
+            Self::CubicSegment { p0, p1, p2, p3, .. } => {
+                cubic::signed_distance(p0, p1, p2, p3, origin)
+            }
+        }
+    }
+
+    pub fn has_color(&self, c: EdgeColor) -> bool {
+        match *self {
+            Self::LineSegment { color, .. } => color as usize & c as usize != 0,
+            Self::QuadraticSegment { color, .. } => color as usize & c as usize != 0,
+            Self::CubicSegment { color, .. } => color as usize & c as usize != 0,
+        }
+    }
+
+    pub fn get_color(&self) -> EdgeColor {
+        match self {
+            Self::LineSegment { color, .. } => *color,
+            Self::QuadraticSegment { color, .. } => *color,
+            Self::CubicSegment { color, .. } => *color,
+        }
+    }
+
+    pub fn set_color(&mut self, c: EdgeColor) {
+        match self {
+            Self::LineSegment { color, .. } => *color = c,
+            Self::QuadraticSegment { color, .. } => *color = c,
+            Self::CubicSegment { color, .. } => *color = c,
+        }
+    }
+}
diff --git a/kayak_font/src/msdf/edge_segment/quadratic.rs b/kayak_font/src/msdf/edge_segment/quadratic.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c0edbc6492a27197fbbad4f826ae617ab3fc1862
--- /dev/null
+++ b/kayak_font/src/msdf/edge_segment/quadratic.rs
@@ -0,0 +1,130 @@
+use crate::msdf::{signed_distance::SignedDistance, vector::Vector2, EdgeColor};
+
+use super::{equation_solver, mix, non_zero_sign, EdgeSegment};
+
+pub fn direction(p0: Vector2, p1: Vector2, p2: Vector2, param: f64) -> Vector2 {
+    mix(p1 - p0, p2 - p1, param)
+}
+
+pub fn point(p0: Vector2, p1: Vector2, p2: Vector2, param: f64) -> Vector2 {
+    mix(mix(p0, p1, param), mix(p1, p2, param), param)
+}
+
+pub fn find_bounds(
+    p0: Vector2,
+    p1: Vector2,
+    p2: Vector2,
+    l: &mut f64,
+    b: &mut f64,
+    r: &mut f64,
+    t: &mut f64,
+) {
+    Vector2::point_bounds(p0, l, b, r, t);
+    Vector2::point_bounds(p2, l, b, r, t);
+    let bot = (p1 - p0) - (p2 - p1);
+    if bot.x != 0.0 {
+        let param = (p1.x - p0.x) / bot.x;
+        if param > 0.0 && param < 1.0 {
+            Vector2::point_bounds(point(p0, p1, p2, param), l, b, r, t);
+        }
+    }
+    if bot.y != 0.0 {
+        let param = (p1.y - p0.y) / bot.y;
+        if param > 0.0 && param < 1.0 {
+            Vector2::point_bounds(point(p0, p1, p2, param), l, b, r, t);
+        }
+    }
+}
+
+pub fn split_in_thirds(
+    p0: Vector2,
+    p1: Vector2,
+    p2: Vector2,
+    color: EdgeColor,
+) -> (EdgeSegment, EdgeSegment, EdgeSegment) {
+    (
+        EdgeSegment::new_quadratic(
+            p0,
+            mix(p0, p1, 1.0 / 3.0),
+            point(p0, p1, p2, 1.0 / 3.0),
+            color,
+        ),
+        EdgeSegment::new_quadratic(
+            point(p0, p1, p2, 1.0 / 3.0),
+            mix(mix(p0, p1, 5.0 / 9.0), mix(p1, p2, 4.0 / 9.0), 0.5),
+            point(p0, p1, p2, 2.0 / 3.0),
+            color,
+        ),
+        EdgeSegment::new_quadratic(
+            point(p0, p1, p2, 2.0 / 3.0),
+            mix(p1, p2, 2.0 / 3.0),
+            p2,
+            color,
+        ),
+    )
+}
+
+pub fn signed_distance(
+    p0: Vector2,
+    p1: Vector2,
+    p2: Vector2,
+    origin: Vector2,
+) -> (SignedDistance, f64) {
+    let qa = p0 - origin;
+    let ab = p1 - p0;
+    // let br = p0 + p2 - p1 - p1;
+    let br = p2 - p1 - ab;
+    let a = Vector2::dot_product(br, br);
+    let b = 3.0 * Vector2::dot_product(ab, br);
+    let c = 2.0 * Vector2::dot_product(ab, ab) + Vector2::dot_product(qa, br);
+    let d = Vector2::dot_product(qa, ab);
+
+    let (solutions, t) = equation_solver::solve_cubic(a, b, c, d);
+
+    let mut min_distance = non_zero_sign(Vector2::cross_product(ab, qa)) as f64 * qa.length();
+    let mut param = -Vector2::dot_product(qa, ab) / Vector2::dot_product(ab, ab);
+    {
+        let distance = non_zero_sign(Vector2::cross_product(p2 - p1, p2 - origin)) as f64
+            * (p2 - origin).length();
+        if distance.abs() < min_distance.abs() {
+            min_distance = distance;
+            param =
+                Vector2::dot_product(origin - p1, p2 - p1) / Vector2::dot_product(p2 - p1, p2 - p1);
+        }
+    }
+
+    for i in 0..solutions {
+        let ti = t[i as usize];
+
+        if ti > 0.0 && ti < 1.0 {
+            let endpoint = p0 + 2.0 * ti * ab + ti * ti * br;
+            let distance = non_zero_sign(Vector2::cross_product(p2 - p0, endpoint - origin)) as f64
+                * (endpoint - origin).length();
+            if distance.abs() <= min_distance.abs() {
+                min_distance = distance;
+                param = ti;
+            }
+        }
+    }
+
+    if param >= 0.0 && param <= 1.0 {
+        return (SignedDistance::new(min_distance, 0.0), param);
+    } else if param < 0.5 {
+        return (
+            SignedDistance::new(
+                min_distance,
+                (Vector2::dot_product(ab.normalize(false), qa.normalize(false))).abs(),
+            ),
+            param,
+        );
+    } else {
+        return (
+            SignedDistance::new(
+                min_distance,
+                (Vector2::dot_product((p2 - p1).normalize(false), (p2 - origin).normalize(false)))
+                    .abs(),
+            ),
+            param,
+        );
+    }
+}
diff --git a/kayak_font/src/msdf/gen.rs b/kayak_font/src/msdf/gen.rs
new file mode 100644
index 0000000000000000000000000000000000000000..61cc4f9f581e9af5213fdd022df8267dea1d053a
--- /dev/null
+++ b/kayak_font/src/msdf/gen.rs
@@ -0,0 +1,386 @@
+use crate::msdf::{
+    bitmap::{FloatRGB, FloatRGBBmp},
+    edge_point::EdgePoint,
+    shape::Shape,
+    signed_distance::SignedDistance,
+    vector::Vector2,
+    MultiDistance,
+};
+
+use super::EdgeColor;
+
+fn min<T: PartialOrd>(a: T, b: T) -> T {
+    if a > b {
+        b
+    } else {
+        a
+    }
+}
+
+fn max<T: PartialOrd>(a: T, b: T) -> T {
+    if a > b {
+        a
+    } else {
+        b
+    }
+}
+
+fn median<T: PartialOrd + Copy>(a: T, b: T, c: T) -> T {
+    max(min(a, b), min(max(a, b), c))
+}
+
+pub fn pixel_clash(a: FloatRGB, b: FloatRGB, threshold: f64) -> bool {
+    let mut a0 = a.r;
+    let mut a1 = a.g;
+    let mut a2 = a.b;
+    let mut b0 = b.r;
+    let mut b1 = b.g;
+    let mut b2 = b.b;
+
+    let mut tmp;
+    if (b0 - a0).abs() < (b1 - a1).abs() {
+        tmp = a0;
+        a0 = a1;
+        a1 = tmp;
+        tmp = b0;
+        b0 = b1;
+        b1 = tmp;
+    }
+
+    if (b1 - a1).abs() < (b2 - a2).abs() {
+        tmp = a1;
+        a1 = a2;
+        a2 = tmp;
+        tmp = b1;
+        b1 = b2;
+        b2 = tmp;
+        if (b0 - a0).abs() < (b1 - a1).abs() {
+            tmp = a0;
+            a1 = tmp;
+            tmp = b0;
+            b0 = b1;
+            b1 = tmp;
+        }
+    }
+
+    ((b1 - a1).abs() >= threshold as f32)
+        && !(b0 == b1 && b0 == b2)
+        && (a2 - 0.5).abs() >= (b2 - 0.5).abs()
+
+    // let a_calcd = if a.r > 0.5 { 1.0 } else { 0.0 }
+    //     + if a.g > 0.5 { 1.0 } else { 0.0 }
+    //     + if a.b > 0.5 { 1.0 } else { 0.0 };
+    // let b_calcd = if b.r > 0.5 { 1.0 } else { 0.0 }
+    //     + if b.g > 0.5 { 1.0 } else { 0.0 }
+    //     + if b.b > 0.5 { 1.0 } else { 0.0 };
+    // let a_in = a_calcd >= 2.0;
+    // let b_in = b_calcd >= 2.0;
+
+    // if a_in != b_in {
+    //     return false;
+    // }
+
+    // if (a.r > 0.5 && a.g > 0.5 && a.b > 0.5)
+    //     || (a.r < 0.5 && a.g < 0.5 && a.b < 0.5)
+    //     || (b.r > 0.5 && b.g > 0.5 && b.b > 0.5)
+    //     || (b.r < 0.5 && b.g < 0.5 && b.b < 0.5)
+    // {
+    //     return false;
+    // }
+
+    // let aa;
+    // let ab;
+    // let ba;
+    // let bb;
+    // let ac;
+    // let bc;
+
+    // if (a.r > 0.5) != (b.r > 0.5) && (a.r < 0.5) != (b.r < 0.5) {
+    //     aa = a.r;
+    //     ba = b.r;
+    //     if (a.g > 0.5) != (b.g > 0.5) && (a.g < 0.5) != (b.g < 0.5) {
+    //         ab = a.g;
+    //         bb = b.g;
+    //         ac = a.b;
+    //         bc = b.b;
+    //     } else if (a.b > 0.5) != (b.b > 0.5) && (a.b < 0.5) != (b.b < 0.5) {
+    //         ab = a.b;
+    //         bb = b.b;
+    //         ac = a.g;
+    //         bc = b.g;
+    //     } else {
+    //         return false;
+    //     }
+    // } else if (a.g > 0.5) != (b.g > 0.5)
+    //     && (a.g < 0.5) != (b.g < 0.5)
+    //     && (a.b > 0.5) != (b.b > 0.5)
+    //     && (a.b < 0.5) != (b.b < 0.5)
+    // {
+    //     aa = a.g;
+    //     ba = b.g;
+    //     ab = a.b;
+    //     bb = b.b;
+    //     ac = a.r;
+    //     bc = b.r;
+    // } else {
+    //     return false;
+    // }
+
+    // return ((aa - ba).abs() >= threshold as f32)
+    //     && ((ab - bb).abs() >= threshold as f32)
+    //     && (ac - 0.5).abs() >= (bc - 0.5).abs();
+}
+
+pub fn msdf_error_correction(output: &mut FloatRGBBmp, threshold: Vector2) {
+    let mut clashes: Vec<(usize, usize)> = Vec::new();
+    let w = output.width();
+    let h = output.height();
+    for y in 0..h {
+        for x in 0..w {
+            if (x > 0
+                && pixel_clash(
+                    output.get_pixel(x, y),
+                    output.get_pixel(x - 1, y),
+                    threshold.x,
+                ))
+                || (x < w - 1
+                    && pixel_clash(
+                        output.get_pixel(x, y),
+                        output.get_pixel(x + 1, y),
+                        threshold.x,
+                    ))
+                || (y > 0
+                    && pixel_clash(
+                        output.get_pixel(x, y),
+                        output.get_pixel(x, y - 1),
+                        threshold.y,
+                    ))
+                || (y < h - 1
+                    && pixel_clash(
+                        output.get_pixel(x, y),
+                        output.get_pixel(x, y + 1),
+                        threshold.y,
+                    ))
+            {
+                clashes.push((x, y));
+            }
+        }
+    }
+    let clash_count = clashes.len();
+    for i in 0..clash_count {
+        let clash = clashes[i];
+        let pixel = output.get_pixel(clash.0, clash.1);
+        let med = median(pixel.r, pixel.g, pixel.b);
+        output.set_pixel(clash.0, clash.1, FloatRGB::new(med, med, med));
+    }
+}
+
+pub fn generate_msdf(
+    output: &mut FloatRGBBmp,
+    shape: &Shape,
+    range: f64,
+    scale: Vector2,
+    translate: Vector2,
+    edge_threshold: f64,
+) {
+    let contours = &shape.contours;
+    let contour_count = contours.len();
+    let w = output.width();
+    let h = output.height();
+    let mut windings = Vec::with_capacity(contour_count);
+
+    for contour in contours {
+        windings.push(contour.winding());
+    }
+
+    let mut contour_sd = vec![MultiDistance::default(); contour_count];
+
+    for y in 0..h {
+        let row = if shape.inverse_y_axis { h - y - 1 } else { y };
+        for x in 0..w {
+            let p = (Vector2::new(x as f64 + 0.5, y as f64 + 0.5) / scale) - translate;
+            let mut sr = EdgePoint {
+                min_distance: SignedDistance::infinite(),
+                near_edge: None,
+                near_param: 0.0,
+            };
+            let mut sg = EdgePoint {
+                min_distance: SignedDistance::infinite(),
+                near_edge: None,
+                near_param: 0.0,
+            };
+            let mut sb = EdgePoint {
+                min_distance: SignedDistance::infinite(),
+                near_edge: None,
+                near_param: 0.0,
+            };
+
+            let mut d = SignedDistance::infinite().distance.abs();
+            let mut neg_dist = -SignedDistance::infinite().distance.abs();
+            let mut pos_dist = d;
+
+            let mut winding = 0;
+
+            for (n, contour) in contours.iter().enumerate() {
+                let edges = &contour.edges;
+                let mut r = EdgePoint {
+                    min_distance: SignedDistance::infinite(),
+                    near_edge: None,
+                    near_param: 0.0,
+                };
+                let mut g = EdgePoint {
+                    min_distance: SignedDistance::infinite(),
+                    near_edge: None,
+                    near_param: 0.0,
+                };
+                let mut b = EdgePoint {
+                    min_distance: SignedDistance::infinite(),
+                    near_edge: None,
+                    near_param: 0.0,
+                };
+                for edge in edges {
+                    let (distance, param) = edge.signed_distance(p);
+                    if edge.has_color(EdgeColor::RED) && distance.l(&r.min_distance) {
+                        r.min_distance = distance;
+                        r.near_edge = Some(*edge);
+                        r.near_param = param;
+                    }
+                    if edge.has_color(EdgeColor::GREEN) && distance.l(&g.min_distance) {
+                        g.min_distance = distance;
+                        g.near_edge = Some(*edge);
+                        g.near_param = param;
+                    }
+                    if edge.has_color(EdgeColor::BLUE) && distance.l(&b.min_distance) {
+                        b.min_distance = distance;
+                        b.near_edge = Some(*edge);
+                        b.near_param = param;
+                    }
+                }
+
+                if r.min_distance.l(&sr.min_distance) {
+                    sr = r;
+                }
+                if g.min_distance.l(&sg.min_distance) {
+                    sg = g;
+                }
+                if b.min_distance.l(&sb.min_distance) {
+                    sb = b;
+                }
+
+                let mut med_min_distance = median(
+                    r.min_distance.distance,
+                    g.min_distance.distance,
+                    b.min_distance.distance,
+                )
+                .abs();
+
+                if med_min_distance < d {
+                    d = med_min_distance;
+                    winding = -windings[n];
+                }
+
+                if let Some(near_edge) = &mut r.near_edge {
+                    near_edge.distance_to_pseudo_distance(&mut r.min_distance, p, r.near_param);
+                }
+                if let Some(near_edge) = &mut g.near_edge {
+                    near_edge.distance_to_pseudo_distance(&mut g.min_distance, p, g.near_param);
+                }
+                if let Some(near_edge) = &mut b.near_edge {
+                    near_edge.distance_to_pseudo_distance(&mut b.min_distance, p, b.near_param);
+                }
+
+                med_min_distance = median(
+                    r.min_distance.distance,
+                    g.min_distance.distance,
+                    b.min_distance.distance,
+                );
+                contour_sd[n].r = r.min_distance.distance;
+                contour_sd[n].g = g.min_distance.distance;
+                contour_sd[n].b = b.min_distance.distance;
+                contour_sd[n].med = med_min_distance;
+                if windings[n] > 0
+                    && med_min_distance >= 0.0
+                    && med_min_distance.abs() < pos_dist.abs()
+                {
+                    pos_dist = med_min_distance;
+                }
+                if windings[n] < 0
+                    && med_min_distance <= 0.0
+                    && med_min_distance.abs() < neg_dist.abs()
+                {
+                    neg_dist = med_min_distance;
+                }
+            }
+
+            if let Some(near_edge) = &mut sr.near_edge {
+                near_edge.distance_to_pseudo_distance(&mut sr.min_distance, p, sr.near_param);
+            }
+            if let Some(near_edge) = &mut sg.near_edge {
+                near_edge.distance_to_pseudo_distance(&mut sg.min_distance, p, sg.near_param);
+            }
+            if let Some(near_edge) = &mut sb.near_edge {
+                near_edge.distance_to_pseudo_distance(&mut sb.min_distance, p, sb.near_param);
+            }
+
+            let mut msd = MultiDistance::default();
+            msd.r = SignedDistance::infinite().distance;
+            msd.b = msd.r;
+            msd.g = msd.r;
+            msd.med = msd.r;
+            if pos_dist >= 0.0 && pos_dist.abs() <= neg_dist.abs() {
+                msd.med = SignedDistance::infinite().distance;
+                winding = 1;
+                for i in 0..contours.len() {
+                    if windings[i] > 0
+                        && contour_sd[i].med > msd.med
+                        && contour_sd[i].med.abs() < neg_dist.abs()
+                    {
+                        msd = contour_sd[i];
+                    }
+                }
+            } else if neg_dist <= 0.0 && neg_dist.abs() <= pos_dist.abs() {
+                msd.med = -SignedDistance::infinite().distance;
+                winding = -1;
+                for i in 0..contours.len() {
+                    if windings[i] < 0
+                        && contour_sd[i].med < msd.med
+                        && contour_sd[i].med.abs() < pos_dist.abs()
+                    {
+                        msd = contour_sd[i];
+                    }
+                }
+            }
+
+            for i in 0..contours.len() {
+                if windings[i] != winding && contour_sd[i].med.abs() < msd.med.abs() {
+                    msd = contour_sd[i];
+                }
+            }
+
+            if median(
+                sr.min_distance.distance,
+                sg.min_distance.distance,
+                sb.min_distance.distance,
+            ) == msd.med
+            {
+                msd.r = sr.min_distance.distance;
+                msd.g = sg.min_distance.distance;
+                msd.b = sb.min_distance.distance;
+            }
+
+            output.set_pixel(
+                x,
+                row,
+                FloatRGB {
+                    r: (msd.r / range + 0.5) as f32,
+                    g: (msd.g / range + 0.5) as f32,
+                    b: (msd.b / range + 0.5) as f32,
+                },
+            )
+        }
+
+        if edge_threshold > 0.0 {
+            msdf_error_correction(output, edge_threshold / (scale * range));
+        }
+    }
+}
diff --git a/kayak_font/src/msdf/mod.rs b/kayak_font/src/msdf/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a687ffecfdbf52b72f4129ad4be27da7cbbff080
--- /dev/null
+++ b/kayak_font/src/msdf/mod.rs
@@ -0,0 +1,33 @@
+use num_derive::FromPrimitive;
+
+pub mod bitmap;
+pub mod contour;
+pub mod edge_coloring;
+pub mod edge_point;
+pub mod edge_segment;
+pub mod gen;
+pub mod msdf_params;
+pub mod shape;
+pub mod signed_distance;
+pub mod ttf_parser;
+pub mod vector;
+
+#[derive(Debug, Clone, Copy, PartialEq, FromPrimitive)]
+pub enum EdgeColor {
+    BLACK = 0,
+    RED = 1,
+    GREEN = 2,
+    YELLOW = 3,
+    BLUE = 4,
+    MAGENTA = 5,
+    CYAN = 6,
+    WHITE = 7,
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+pub struct MultiDistance {
+    pub b: f64,
+    pub g: f64,
+    pub med: f64,
+    pub r: f64,
+}
diff --git a/kayak_font/src/msdf/msdf_params.rs b/kayak_font/src/msdf/msdf_params.rs
new file mode 100644
index 0000000000000000000000000000000000000000..cdc33e315eb398b7a853526754e7850d44150360
--- /dev/null
+++ b/kayak_font/src/msdf/msdf_params.rs
@@ -0,0 +1,35 @@
+#![allow(dead_code)]
+
+pub struct MsdfParams {
+    pub scale_x: f32,
+    pub scale_y: f32,
+    pub shape_scale: f32,
+    pub min_image_width: usize,
+    pub min_image_height: usize,
+    pub angle_threshold: f64,
+    pub px_range: f64,
+    pub edge_threshold: f64,
+    pub use_custom_image_size: bool,
+    pub custom_width: usize,
+    pub custom_height: usize,
+    pub custom_border: usize,
+}
+
+impl MsdfParams {
+    pub fn new() -> Self {
+        Self {
+            scale_x: 1.0,
+            scale_y: 1.0,
+            shape_scale: 1.0,
+            min_image_width: 5,
+            min_image_height: 5,
+            angle_threshold: 3.0,
+            px_range: 4.0,
+            edge_threshold: 1.00000001,
+            use_custom_image_size: false,
+            custom_width: 0,
+            custom_height: 0,
+            custom_border: 0,
+        }
+    }
+}
diff --git a/kayak_font/src/msdf/shape.rs b/kayak_font/src/msdf/shape.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5b4d34f3f636dd3bf207d820a3e892e3bacb83a9
--- /dev/null
+++ b/kayak_font/src/msdf/shape.rs
@@ -0,0 +1,59 @@
+#![allow(dead_code)]
+
+use crate::msdf::contour::Contour;
+
+#[derive(Debug, Default, Clone)]
+pub struct Shape {
+    pub contours: Vec<Contour>,
+    pub inverse_y_axis: bool,
+}
+
+impl Shape {
+    pub fn new() -> Self {
+        Self {
+            contours: Vec::new(),
+            inverse_y_axis: false,
+        }
+    }
+
+    pub fn normalized(&mut self) {
+        for contour in self.contours.iter_mut() {
+            let (e0, e1, e2) = contour.edges[0].split_in_thirds();
+            contour.edges.clear();
+            contour.edges.push(e0);
+            contour.edges.push(e1);
+            contour.edges.push(e2);
+        }
+    }
+
+    fn find_bounds(&mut self, left: &mut f64, bottom: &mut f64, right: &mut f64, top: &mut f64) {
+        for contour in self.contours.iter_mut() {
+            contour.find_bounds(left, bottom, right, top);
+        }
+    }
+
+    pub fn bound_miters(
+        &self,
+        l: &mut f64,
+        b: &mut f64,
+        r: &mut f64,
+        t: &mut f64,
+        border: f64,
+        miter_limit: f64,
+        polarity: i32,
+    ) {
+        for contour in self.contours.iter() {
+            contour.bound_miters(l, b, r, t, border, miter_limit, polarity);
+        }
+    }
+
+    pub fn get_bounds(&mut self) -> (f64, f64, f64, f64) {
+        const LARGE_VALUE: f64 = 1e240;
+        let mut left = -LARGE_VALUE;
+        let mut bottom = LARGE_VALUE;
+        let mut right = LARGE_VALUE;
+        let mut top = -LARGE_VALUE;
+        self.find_bounds(&mut left, &mut bottom, &mut right, &mut top);
+        return (left, bottom, right, top);
+    }
+}
diff --git a/kayak_font/src/msdf/signed_distance.rs b/kayak_font/src/msdf/signed_distance.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2ecab9115651ea9f064a805684c6be6cd71f2a13
--- /dev/null
+++ b/kayak_font/src/msdf/signed_distance.rs
@@ -0,0 +1,38 @@
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct SignedDistance {
+    pub distance: f64,
+    pub dot: f64,
+}
+
+impl SignedDistance {
+    pub fn infinite() -> Self {
+        Self {
+            distance: -1e240,
+            dot: 1.0,
+        }
+    }
+
+    pub fn new(distance: f64, dot: f64) -> Self {
+        Self { distance, dot }
+    }
+
+    // pub fn g(&self, other: &SignedDistance) -> bool {
+    //     self.distance.abs() > other.distance.abs()
+    //         || (self.distance.abs() == other.distance.abs() && self.dot > other.dot)
+    // }
+
+    // pub fn ge(&self, other: &SignedDistance) -> bool {
+    //     self.distance.abs() > other.distance.abs()
+    //         || (self.distance.abs() == other.distance.abs() && self.dot >= other.dot)
+    // }
+
+    pub fn l(&self, other: &SignedDistance) -> bool {
+        self.distance.abs() < other.distance.abs()
+            || (self.distance.abs() == other.distance.abs() && self.dot < other.dot)
+    }
+
+    // pub fn le(&self, other: &SignedDistance) -> bool {
+    //     self.distance.abs() < other.distance.abs()
+    //         || (self.distance.abs() == other.distance.abs() && self.dot <= other.dot)
+    // }
+}
diff --git a/kayak_font/src/msdf/ttf_parser.rs b/kayak_font/src/msdf/ttf_parser.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dee277309f9f0cb43caee3f505890d5e2cfbf396
--- /dev/null
+++ b/kayak_font/src/msdf/ttf_parser.rs
@@ -0,0 +1,110 @@
+use crate::msdf::{
+    contour::Contour, edge_segment::EdgeSegment, shape::Shape, vector::Vector2, EdgeColor,
+};
+
+#[derive(Debug, Default)]
+pub struct ContourBuilder {
+    pixel_scale: f64,
+    contour: Contour,
+    point: Vector2,
+}
+
+impl ContourBuilder {
+    pub fn open_at(x: f64, y: f64, pixel_scale: f64) -> Self {
+        Self {
+            contour: Contour::new(),
+            point: Vector2::new(x, y),
+            pixel_scale,
+        }
+    }
+
+    pub fn line_to(&mut self, x: f64, y: f64) {
+        let point = Vector2::new(x, y);
+        self.contour.add_edge(EdgeSegment::new_linear(
+            self.point * self.pixel_scale,
+            point * self.pixel_scale,
+            EdgeColor::WHITE,
+        ));
+        self.point = point;
+    }
+
+    pub fn quad_to(&mut self, cx: f64, cy: f64, x: f64, y: f64) {
+        let cpoint = Vector2::new(cx, cy);
+        let point = Vector2::new(x, y);
+        self.contour.add_edge(EdgeSegment::new_quadratic(
+            self.point * self.pixel_scale,
+            cpoint * self.pixel_scale,
+            point * self.pixel_scale,
+            EdgeColor::WHITE,
+        ));
+        self.point = point;
+    }
+
+    pub fn curve_to(&mut self, c1x: f64, c1y: f64, c2x: f64, c2y: f64, x: f64, y: f64) {
+        let c1point = Vector2::new(c1x, c1y);
+        let c2point = Vector2::new(c2x, c2y);
+        let point = Vector2::new(x, y);
+        self.contour.add_edge(EdgeSegment::new_cubic(
+            self.point * self.pixel_scale,
+            c1point * self.pixel_scale,
+            c2point * self.pixel_scale,
+            point * self.pixel_scale,
+            EdgeColor::WHITE,
+        ));
+        self.point = point;
+    }
+
+    pub fn close(self) -> Contour {
+        self.contour
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct ShapeBuilder {
+    pub pixel_scale: f64,
+    shape: Shape,
+    contour: Option<ContourBuilder>,
+}
+
+impl ShapeBuilder {
+    pub fn build(self) -> Shape {
+        self.shape
+    }
+}
+
+impl ttf_parser::OutlineBuilder for ShapeBuilder {
+    fn move_to(&mut self, x: f32, y: f32) {
+        if self.contour.is_some() {
+            panic!("Unexpected move_to");
+        }
+
+        self.contour = ContourBuilder::open_at(x as _, y as _, self.pixel_scale).into();
+    }
+
+    fn line_to(&mut self, x: f32, y: f32) {
+        self.contour
+            .as_mut()
+            .expect("Opened contour")
+            .line_to(x as _, y as _);
+    }
+
+    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
+        self.contour
+            .as_mut()
+            .expect("Opened contour")
+            .quad_to(x1 as _, y1 as _, x as _, y as _);
+    }
+
+    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
+        self.contour
+            .as_mut()
+            .expect("Opened contour")
+            .curve_to(x1 as _, y1 as _, x2 as _, y2 as _, x as _, y as _);
+    }
+
+    fn close(&mut self) {
+        self.shape
+            .contours
+            .push(self.contour.take().expect("Opened contour").close());
+    }
+}
diff --git a/kayak_font/src/msdf/vector.rs b/kayak_font/src/msdf/vector.rs
new file mode 100644
index 0000000000000000000000000000000000000000..34d7a29534dcbba343328e317b16dc29ac311b05
--- /dev/null
+++ b/kayak_font/src/msdf/vector.rs
@@ -0,0 +1,159 @@
+#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
+pub struct Vector2 {
+    pub x: f64,
+    pub y: f64,
+}
+
+impl Vector2 {
+    pub fn new(x: f64, y: f64) -> Self {
+        Self { x, y }
+    }
+
+    pub fn is_zero(&self) -> bool {
+        self.x == 0.0 && self.y == 0.0
+    }
+
+    pub fn get_ortho_normal(&self, polarity: bool, allow_zero: bool) -> Vector2 {
+        let len = self.length();
+        if len == 0.0 {
+            let allow_zero = if !allow_zero { 1.0 } else { 0.0 };
+            return if polarity {
+                Vector2::new(0.0, allow_zero)
+            } else {
+                Vector2::new(0.0, -allow_zero)
+            };
+        }
+        return if polarity {
+            Vector2::new(-self.y / len, self.x / len)
+        } else {
+            Vector2::new(self.y / len, -self.x / len)
+        };
+    }
+
+    pub fn get_orthogonal(&self, polarity: bool) -> Vector2 {
+        return if polarity {
+            Vector2::new(-self.y, self.x)
+        } else {
+            Vector2::new(self.y, -self.x)
+        };
+    }
+    pub fn dot_product(a: Vector2, b: Vector2) -> f64 {
+        return a.x * b.x + a.y * b.y;
+    }
+    pub fn cross_product(a: Vector2, b: Vector2) -> f64 {
+        return a.x * b.y - a.y * b.x;
+    }
+
+    pub fn normalize(&self, allow_zero: bool) -> Vector2 {
+        let len = self.length();
+        if len == 0.0 {
+            let allow_zero = if !allow_zero { 1.0 } else { 0.0 };
+            return Vector2::new(0.0, allow_zero);
+        }
+        return Vector2::new(self.x / len, self.y / len);
+    }
+
+    pub fn length(&self) -> f64 {
+        return (self.x * self.x + self.y * self.y).sqrt();
+    }
+
+    pub fn clamp(n: i32, b: i32) -> i32 {
+        if n > 0 {
+            return if n <= b { n } else { b };
+        }
+        return 0;
+    }
+
+    pub fn sign(n: f64) -> f64 {
+        return if n == 0.0 {
+            0.0
+        } else if n > 0.0 {
+            1.0
+        } else {
+            -1.0
+        };
+    }
+
+    pub fn shoelace(a: Vector2, b: Vector2) -> f64 {
+        return (b.x - a.x) * (a.y + b.y);
+    }
+
+    pub fn point_bounds(p: Vector2, l: &mut f64, b: &mut f64, r: &mut f64, t: &mut f64) {
+        if p.x < *l {
+            *l = p.x;
+        }
+        if p.y < *b {
+            *b = p.y;
+        }
+        if p.x > *r {
+            *r = p.x;
+        }
+        if p.y > *t {
+            *t = p.y;
+        }
+    }
+}
+
+impl std::ops::Sub for Vector2 {
+    type Output = Vector2;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Vector2::new(self.x - rhs.x, self.y - rhs.y)
+    }
+}
+
+impl std::ops::Add for Vector2 {
+    type Output = Vector2;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Vector2::new(self.x + rhs.x, self.y + rhs.y)
+    }
+}
+
+impl std::ops::Mul for Vector2 {
+    type Output = Vector2;
+
+    fn mul(self, rhs: Self) -> Self::Output {
+        Vector2::new(self.x * rhs.x, self.y * rhs.y)
+    }
+}
+
+impl std::ops::Div for Vector2 {
+    type Output = Vector2;
+
+    fn div(self, rhs: Self) -> Self::Output {
+        Vector2::new(self.x / rhs.x, self.y / rhs.y)
+    }
+}
+
+impl std::ops::Mul<f64> for Vector2 {
+    type Output = Vector2;
+
+    fn mul(self, rhs: f64) -> Self::Output {
+        Vector2::new(self.x * rhs, self.y * rhs)
+    }
+}
+
+impl std::ops::Div<f64> for Vector2 {
+    type Output = Vector2;
+
+    fn div(self, rhs: f64) -> Self::Output {
+        Vector2::new(self.x / rhs, self.y / rhs)
+    }
+}
+
+impl std::ops::Mul<Vector2> for f64 {
+    type Output = Vector2;
+
+    fn mul(self, rhs: Vector2) -> Self::Output {
+        Vector2::new(rhs.x * self, rhs.y * self)
+    }
+}
+
+impl std::ops::Div<Vector2> for f64 {
+    type Output = Vector2;
+
+    fn div(self, rhs: Vector2) -> Self::Output {
+        Vector2::new(rhs.x / self, rhs.y / self)
+    }
+}
diff --git a/kayak_font/src/sdf.rs b/kayak_font/src/sdf.rs
index ec6ed8e5ccb63a14dd280830516dd03a81f64805..dd928c47fa3fdf55faa45ef48163d27009e0b90f 100644
--- a/kayak_font/src/sdf.rs
+++ b/kayak_font/src/sdf.rs
@@ -1,7 +1,7 @@
 use crate::{atlas::Atlas, glyph::Glyph, metrics::Metrics};
 use nanoserde::DeJson;
 
-#[derive(DeJson, Debug, Clone, PartialEq)]
+#[derive(DeJson, Default, Debug, Clone, PartialEq)]
 pub struct Sdf {
     pub atlas: Atlas,
     metrics: Metrics,
@@ -9,7 +9,7 @@ pub struct Sdf {
     kerning: Vec<KerningData>,
 }
 
-#[derive(DeJson, Debug, Clone, Copy, PartialEq)]
+#[derive(DeJson, Default, Debug, Clone, Copy, PartialEq)]
 pub struct KerningData {
     pub unicode1: u32,
     pub unicode2: u32,
@@ -17,6 +17,15 @@ pub struct KerningData {
 }
 
 impl Sdf {
+    pub fn new() -> Self {
+        Self {
+            atlas: Atlas::default(),
+            metrics: Metrics::default(),
+            glyphs: Vec::default(),
+            kerning: Vec::default(),
+        }
+    }
+
     pub fn from_string(data: String) -> Sdf {
         let value: Sdf = match DeJson::deserialize_json(data.as_str()) {
             Ok(v) => v,
diff --git a/kayak_font/src/ttf/loader.rs b/kayak_font/src/ttf/loader.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2c39c09b89517636e9412b1e22e1ddf380e49d21
--- /dev/null
+++ b/kayak_font/src/ttf/loader.rs
@@ -0,0 +1,304 @@
+use bevy::{
+    asset::{AssetLoader, LoadContext, LoadedAsset},
+    render::render_resource::{Extent3d, TextureFormat},
+    utils::{BoxedFuture, HashMap},
+};
+
+#[cfg(not(target_family = "wasm"))]
+use bevy::asset::FileAssetIo;
+
+use image::{EncodableLayout, RgbaImage};
+use nanoserde::DeJson;
+
+use crate::{
+    msdf::{self, bitmap::FloatRGBBmp, shape::Shape, ttf_parser::ShapeBuilder, vector::Vector2},
+    Glyph, ImageType, KayakFont, Rect, Sdf,
+};
+pub struct TTFLoader;
+
+#[derive(DeJson, Default, Debug, Clone)]
+pub struct KTTF {
+    file: String,
+    char_range_start: String,
+    char_range_end: String,
+}
+
+impl AssetLoader for TTFLoader {
+    fn load<'a>(
+        &'a self,
+        bytes: &'a [u8],
+        load_context: &'a mut LoadContext,
+    ) -> BoxedFuture<'a, Result<(), anyhow::Error>> {
+        Box::pin(async move {
+            #[cfg(not(target_family = "wasm"))]
+            let asset_io = load_context
+                .asset_io()
+                .downcast_ref::<FileAssetIo>()
+                .unwrap();
+
+            let kttf: KTTF =
+                nanoserde::DeJson::deserialize_json(std::str::from_utf8(bytes).unwrap()).unwrap();
+
+            let char_range_start =
+                u32::from_str_radix(kttf.char_range_start.trim_start_matches("0x"), 16)?;
+            let char_range_end =
+                u32::from_str_radix(kttf.char_range_end.trim_start_matches("0x"), 16)?;
+            let font_bytes = load_context.read_asset_bytes(kttf.file).await?;
+
+            let mut cache_path = std::path::PathBuf::from(load_context.path());
+            let file_name = load_context
+                .path()
+                .file_name()
+                .unwrap()
+                .to_str()
+                .unwrap()
+                .to_string();
+            cache_path.set_file_name(format!("{}-cached.png", file_name));
+            let cache_image = load_context.read_asset_bytes(&cache_path).await;
+
+            let font_range = char_range_start..char_range_end;
+            let char_count = font_range.len() as u32;
+
+            let size_x = 64usize;
+            let size_y = 128usize;
+            let face = ttf_parser::Face::parse(&font_bytes, 0).unwrap();
+            let image_height = size_y as u32 * char_count;
+            let mut image_builder: RgbaImage = image::ImageBuffer::new(size_x as u32, image_height);
+            let mut yy = 0u32;
+            let mut glyphs = vec![];
+
+            // Build char to glyph mapping..
+            let mut glyph_to_char: HashMap<ttf_parser::GlyphId, char> =
+                HashMap::with_capacity(face.number_of_glyphs() as usize);
+            let mut char_to_glyph: HashMap<char, ttf_parser::GlyphId> =
+                HashMap::with_capacity(face.number_of_glyphs() as usize);
+            if let Some(subtable) = face.tables().cmap {
+                for subtable in subtable.subtables {
+                    subtable.codepoints(|codepoint| {
+                        if let Some(mapping) = subtable.glyph_index(codepoint) {
+                            glyph_to_char
+                                .insert(mapping, unsafe { std::mem::transmute(codepoint) });
+                            char_to_glyph
+                                .insert(unsafe { std::mem::transmute(codepoint) }, mapping);
+                        }
+                    })
+                }
+            }
+
+            for char_u in font_range {
+                let c = char::from_u32(char_u).unwrap();
+                let glyph_id = *char_to_glyph.get(&c).unwrap();
+                let mut output = FloatRGBBmp::new(size_x, size_y);
+                let mut builder = ShapeBuilder::default();
+                let pixel_scale = size_x as f64 / face.units_per_em() as f64;
+                builder.pixel_scale = pixel_scale;
+                let _result = face.outline_glyph(glyph_id, &mut builder);
+
+                let char_bounds = face
+                    .glyph_bounding_box(glyph_id)
+                    .unwrap_or(ttf_parser::Rect {
+                        x_min: 0,
+                        x_max: size_x as i16,
+                        y_min: 0,
+                        y_max: size_y as i16,
+                    });
+
+                let mut shape = builder.build();
+                shape.inverse_y_axis = true;
+                // let (left, bottom, right, top) = shape.get_bounds();
+
+                let scale = Vector2::new(1.0, 1.0);
+                let px_range = 2.0;
+                let range = px_range / scale.x.min(scale.y);
+
+                let (translation, plane) =
+                    calculate_plane(&mut shape, pixel_scale as f32, 1.0, px_range as f32, 1.0);
+                let advance = face.glyph_hor_advance(glyph_id).unwrap_or(0) as f32 / size_x as f32;
+                let c = *glyph_to_char.get(&glyph_id).unwrap();
+                glyphs.push(Glyph {
+                    unicode: c,
+                    advance: advance * pixel_scale as f32,
+                    atlas_bounds: Some(Rect {
+                        left: 0.0,
+                        bottom: 0.0 as f32,
+                        right: size_x as f32,
+                        top: size_y as f32,
+                    }),
+                    plane_bounds: Some(plane),
+                });
+
+                // let frame = Vector2::new(size_x as f64, size_y as f64);
+
+                // dbg!((left, right, top, bottom));
+
+                // left = (left - (size_x as f64 / 8.0)).max(0.0);
+                // right = (right + (size_x as f64 / 8.0)).min(size_x as f64);
+                // top = (top + (size_y as f64 / 8.0)).min(size_y as f64);
+                // bottom = (bottom - (size_y as f64 / 8.0)).max(0.0);
+
+                // dbg!((left, right, top, bottom));
+
+                // let dims = Vector2::new(right - left, top - bottom);
+
+                // let translate = Vector2::new(-left + (frame.x - dims.x), (frame.y - (bottom + dims.y)) - 1.0);
+                if cache_image.is_err() {
+                    msdf::edge_coloring::simple(&mut shape, 3.0, 0);
+                    msdf::gen::generate_msdf(
+                        &mut output,
+                        &shape,
+                        range,
+                        scale,
+                        translation + Vector2::new(0.0, size_x as f64 * 1.25),
+                        1.11111111111111111,
+                    );
+
+                    // let left = (translation.x - char_bounds.x_min as f64 * pixel_scale).max(0.0).floor() as u32;
+                    let right =
+                        (translation.x + char_bounds.x_max as f64 * pixel_scale).floor() as u32;
+                    // let top = (translation.y - char_bounds.y_min as f64 * pixel_scale).max(0.0).floor() as u32;
+                    let bottom =
+                        (translation.y + char_bounds.y_max as f64 * pixel_scale).floor() as u32;
+
+                    for x in 0..(right + 2).min(64) {
+                        for y in 0..bottom + 48 {
+                            // for x in 0..size_x as u32 {
+                            //     for y  in 0..size_y as u32 {
+                            let pixel = output.get_pixel(x as usize, y as usize);
+                            image_builder.put_pixel(
+                                x,
+                                yy + y,
+                                image::Rgba([
+                                    (pixel.r * 255.0) as u8,
+                                    (pixel.g * 255.0) as u8,
+                                    (pixel.b * 255.0) as u8,
+                                    255,
+                                ]),
+                            );
+                        }
+                    }
+                }
+                // if c == '\"' {
+                //     image_builder.save("test.png").unwrap();
+                //     panic!("");
+                // }
+                yy += size_y as u32;
+            }
+
+            let image_bytes = if cache_image.is_err() {
+                #[cfg(not(target_family = "wasm"))]
+                image_builder
+                    .save(asset_io.root_path().join(cache_path))
+                    .unwrap();
+                image_builder.as_bytes().to_vec()
+            } else {
+                let cache_image = cache_image.unwrap();
+                let image = image::load_from_memory(&cache_image).unwrap();
+                image.as_bytes().to_vec()
+            };
+
+            let mut sdf = Sdf::default();
+            sdf.glyphs = glyphs;
+            sdf.atlas.font_size = size_x as f32;
+
+            let mut image = bevy::prelude::Image::new(
+                Extent3d {
+                    width: size_x as u32,
+                    height: image_height,
+                    depth_or_array_layers: 1,
+                },
+                bevy::render::render_resource::TextureDimension::D2,
+                image_bytes,
+                TextureFormat::Rgba8UnormSrgb,
+            );
+            image.reinterpret_stacked_2d_as_array(char_count);
+            let image_handle =
+                load_context.set_labeled_asset("font_image", LoadedAsset::new(image));
+
+            let font = KayakFont::new(sdf, ImageType::Array(image_handle));
+            load_context.set_default_asset(LoadedAsset::new(font));
+
+            Ok(())
+        })
+    }
+
+    fn extensions(&self) -> &[&str] {
+        &["kttf"]
+    }
+}
+
+fn calculate_plane(
+    shape: &mut Shape,
+    geometry_scale: f32,
+    scale: f32,
+    range: f32,
+    miter_limit: f32,
+) -> (Vector2, Rect) {
+    let bounds = shape.get_bounds();
+    let bounds = Rect {
+        left: bounds.0 as f32,
+        bottom: bounds.1 as f32,
+        right: bounds.2 as f32,
+        top: bounds.3 as f32,
+    };
+    let scale = scale * geometry_scale;
+    let range = range / geometry_scale;
+    let (_w, _h, translation_x, translation_y) =
+        if bounds.left < bounds.right && bounds.bottom < bounds.top {
+            let mut l = bounds.left as f64;
+            let mut b = bounds.bottom as f64;
+            let mut r = bounds.right as f64;
+            let mut t = bounds.top as f64;
+
+            l -= 0.5 * range as f64;
+            b -= 0.5 * range as f64;
+            r += 0.5 * range as f64;
+            t += 0.5 * range as f64;
+
+            if miter_limit > 0.0 {
+                shape.bound_miters(
+                    &mut l,
+                    &mut b,
+                    &mut r,
+                    &mut t,
+                    0.5 * range as f64,
+                    miter_limit as f64,
+                    1,
+                );
+            }
+
+            let w = scale as f64 * (r - l);
+            let h = scale as f64 * (t - b);
+            let box_w = w.ceil() as i32 + 1;
+            let box_h = h.ceil() as i32 + 1;
+            (
+                box_w,
+                box_h,
+                -l + 0.5 * (box_w as f64 - w) / scale as f64,
+                -b + 0.5 * (box_h as f64 - h) / scale as f64,
+            )
+        } else {
+            (0, 0, 0.0, 0.0)
+        };
+
+    // let mut l = 0.0;
+    // let mut r = 0.0;
+    // let mut b = 0.0;
+    // let mut t = 0.0;
+    // if w > 0 && h > 0 {
+    //     let inv_box_scale = 1.0 / scale as f64;
+    //     l = geometry_scale as f64 * (-translation_x + 0.5 * inv_box_scale);
+    //     b = geometry_scale as f64 * (-translation_y + 0.5 * inv_box_scale);
+    //     r = geometry_scale as f64 * (-translation_x + (w as f64 - 0.5) * inv_box_scale);
+    //     t = geometry_scale as f64 * (-translation_y + (h as f64 - 0.5) * inv_box_scale);
+    // }
+
+    (
+        Vector2::new(translation_x, translation_y) * geometry_scale as f64,
+        Rect {
+            left: 0.0,                  // l as f32,
+            bottom: 0.0,                // b as f32,
+            right: 0.0,                 // r as f32,
+            top: 24.0 * geometry_scale, // t as f32,
+        },
+    )
+}
diff --git a/kayak_font/src/ttf/mod.rs b/kayak_font/src/ttf/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8340811cbc45b0bac7e66fef0f51cf568e0d9d40
--- /dev/null
+++ b/kayak_font/src/ttf/mod.rs
@@ -0,0 +1 @@
+pub(crate) mod loader;
diff --git a/kayak_font/test.png b/kayak_font/test.png
new file mode 100644
index 0000000000000000000000000000000000000000..91dc5f73aa63baa355b04aa2015970123c2f33cf
Binary files /dev/null and b/kayak_font/test.png differ
diff --git a/src/calculate_nodes.rs b/src/calculate_nodes.rs
index 1a6257e85e29379719e0d037548199e0b47c8184..ad396a671d48b7749c2ad0771a9c1dacdfdb89fa 100644
--- a/src/calculate_nodes.rs
+++ b/src/calculate_nodes.rs
@@ -11,7 +11,7 @@ use crate::{
     prelude::{KStyle, KayakRootContext, Tree},
     render::font::FontMapping,
     render_primitive::RenderPrimitive,
-    styles::{RenderCommand, StyleProp, Units},
+    styles::{ComputedStyles, RenderCommand, StyleProp, Units},
 };
 
 pub fn calculate_nodes(
@@ -20,7 +20,7 @@ pub fn calculate_nodes(
     fonts: Res<Assets<KayakFont>>,
     font_mapping: Res<FontMapping>,
     query: Query<Entity, With<DirtyNode>>,
-    all_styles_query: Query<&KStyle>,
+    all_styles_query: Query<&ComputedStyles>,
     node_query: Query<(Entity, &Node)>,
 ) -> KayakRootContext {
     let mut new_nodes = HashMap::<Entity, (Node, bool)>::default();
@@ -43,6 +43,7 @@ pub fn calculate_nodes(
 
             let styles = all_styles_query
                 .get(dirty_entity.0)
+                .map(|cs| &cs.0)
                 .unwrap_or(&default_styles);
             // Get the parent styles. Will be one of the following:
             // 1. Already-resolved node styles (best)
@@ -54,7 +55,7 @@ pub fn calculate_nodes(
                 } else if let Ok((_, parent_node)) = node_query.get(parent_widget_id.0) {
                     parent_node.resolved_styles.clone()
                 } else if let Ok(parent_styles) = all_styles_query.get(parent_widget_id.0) {
-                    parent_styles.clone()
+                    parent_styles.0.clone()
                 } else {
                     default_styles.clone()
                 }
@@ -211,8 +212,8 @@ fn create_primitive(
     dirty: &Query<Entity, With<DirtyNode>>,
     id: WrappedIndex,
     styles: &mut KStyle,
-    prev_styles: KStyle,
-    all_styles_query: &Query<&KStyle>,
+    _prev_styles: KStyle,
+    all_styles_query: &Query<&ComputedStyles>,
 ) -> (RenderPrimitive, bool) {
     let mut render_primitive = RenderPrimitive::from(&styles.clone());
     let mut needs_layout = true;
@@ -229,20 +230,19 @@ fn create_primitive(
             // --- Bind to Font Asset --- //
             let font_handle = font_mapping.get_handle(font.clone()).unwrap();
             if let Some(font) = fonts.get(&font_handle) {
-                // self.bind(id, &asset);
                 if let Ok(node_tree) = context.tree.try_read() {
                     if let Some(parent_id) =
                         find_not_empty_parent(&node_tree, all_styles_query, &id)
                     {
                         if let Some(parent_layout) = context.get_layout(&parent_id) {
                             let border_x = if let Ok(style) = all_styles_query.get(parent_id.0) {
-                                let border = style.border.resolve();
+                                let border = style.0.border.resolve();
                                 border.left + border.right
                             } else {
                                 0.0
                             };
                             let border_y = if let Ok(style) = all_styles_query.get(parent_id.0) {
-                                let border = style.border.resolve();
+                                let border = style.0.border.resolve();
                                 border.top + border.bottom
                             } else {
                                 0.0
@@ -308,24 +308,24 @@ fn create_primitive(
     }
 
     // If we have data from the previous frame no need to do anything here!
-    if matches!(prev_styles.width, StyleProp::Value(..)) {
-        styles.width = prev_styles.width;
-        styles.height = prev_styles.height;
-        needs_layout = false;
-    }
+    // if matches!(prev_styles.width, StyleProp::Value(..)) {
+    //     styles.width = prev_styles.width;
+    //     styles.height = prev_styles.height;
+    //     needs_layout = false;
+    // }
 
     (render_primitive, needs_layout)
 }
 
 pub fn find_not_empty_parent(
     tree: &Tree,
-    all_styles_query: &Query<&KStyle>,
+    all_styles_query: &Query<&ComputedStyles>,
     node: &WrappedIndex,
 ) -> Option<WrappedIndex> {
     if let Some(parent) = tree.parent(*node) {
         if let Ok(styles) = all_styles_query.get(parent.0) {
-            if matches!(styles.render_command.resolve(), RenderCommand::Empty)
-                || matches!(styles.render_command.resolve(), RenderCommand::Layout)
+            if matches!(styles.0.render_command.resolve(), RenderCommand::Empty)
+                || matches!(styles.0.render_command.resolve(), RenderCommand::Layout)
             {
                 find_not_empty_parent(tree, all_styles_query, &parent)
             } else {
diff --git a/src/context.rs b/src/context.rs
index 0d88087bbce52f6968339a7a58e91b8713d7acde..ac9b8b8e36bdf32896d70b976966ce47c12eb691 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -22,8 +22,8 @@ use crate::{
     prelude::KayakWidgetContext,
     render_primitive::RenderPrimitive,
     styles::{
-        Corner, Edge, KCursorIcon, KPositionType, KStyle, LayoutType, RenderCommand, StyleProp,
-        Units,
+        ComputedStyles, Corner, Edge, KCursorIcon, KPositionType, KStyle, LayoutType,
+        RenderCommand, StyleProp, Units,
     },
     tree::{Change, Tree},
     widget_state::WidgetState,
@@ -821,6 +821,13 @@ fn update_widget(
                                     entity.insert(styles);
                                 }
                             }
+                            if let Some(styles) =
+                                world.entity(entity.0).get::<ComputedStyles>().cloned()
+                            {
+                                if let Some(mut entity) = world.get_entity_mut(*target_entity) {
+                                    entity.insert(styles);
+                                }
+                            }
                             if let Some(children) =
                                 world.entity(entity.0).get::<KChildren>().cloned()
                             {
@@ -928,6 +935,11 @@ fn update_widget(
                         entity.insert(styles);
                     }
                 }
+                if let Some(styles) = world.entity(entity.0).get::<ComputedStyles>().cloned() {
+                    if let Some(mut entity) = world.get_entity_mut(*target_entity) {
+                        entity.insert(styles);
+                    }
+                }
                 if let Some(children) = world.entity(entity.0).get::<KChildren>().cloned() {
                     if let Some(mut entity) = world.get_entity_mut(*target_entity) {
                         entity.insert(children);
@@ -985,7 +997,8 @@ impl Plugin for KayakContextPlugin {
 
         // Register reflection types.
         // A bit annoying..
-        app.register_type::<KStyle>()
+        app.register_type::<ComputedStyles>()
+            .register_type::<KStyle>()
             .register_type::<KChildren>()
             .register_type::<WidgetName>()
             .register_type::<StyleProp<Color>>()
diff --git a/src/event_dispatcher.rs b/src/event_dispatcher.rs
index ac64268c891159b2d5a294f7f75ba48fba21e5ef..e0750a80b84928459de9798232ec6761ce6ff972 100644
--- a/src/event_dispatcher.rs
+++ b/src/event_dispatcher.rs
@@ -14,7 +14,7 @@ use crate::{
     node::{Node, WrappedIndex},
     on_event::OnEvent,
     prelude::KayakWidgetContext,
-    styles::{KStyle, RenderCommand},
+    styles::{ComputedStyles, KStyle, RenderCommand},
     Focusable,
 };
 
@@ -557,9 +557,9 @@ impl EventDispatcher {
                         }
                     }
                     if self.contains_cursor.is_none() || !self.contains_cursor.unwrap_or_default() {
-                        if let Some(styles) = world.get::<KStyle>(node.0) {
+                        if let Some(styles) = world.get::<ComputedStyles>(node.0) {
                             // Check if the cursor moved onto a widget that qualifies as one that can contain it
-                            if ignore_layout || Self::can_contain_cursor(styles) {
+                            if ignore_layout || Self::can_contain_cursor(&styles.0) {
                                 self.contains_cursor = Some(is_contained);
                             }
                         }
@@ -601,9 +601,9 @@ impl EventDispatcher {
                         }
 
                         if self.has_cursor.is_none() {
-                            if let Some(styles) = world.get::<KStyle>(node.0) {
+                            if let Some(styles) = world.get::<ComputedStyles>(node.0) {
                                 // Check if the cursor moved onto a widget that qualifies as one that can contain it
-                                if Self::can_contain_cursor(styles) {
+                                if Self::can_contain_cursor(&styles.0) {
                                     self.has_cursor = Some(node);
                                 }
                             }
@@ -666,8 +666,8 @@ impl EventDispatcher {
 
     fn resolve_pointer_events(index: WrappedIndex, world: &mut World) -> PointerEvents {
         let mut pointer_events = PointerEvents::default();
-        if let Some(styles) = world.get::<KStyle>(index.0) {
-            pointer_events = styles.pointer_events.resolve();
+        if let Some(styles) = world.get::<ComputedStyles>(index.0) {
+            pointer_events = styles.0.pointer_events.resolve();
         }
         pointer_events
     }
diff --git a/src/lib.rs b/src/lib.rs
index 3af641501c0722340bf7f21038dfb47d5c4f3947..d6e61f6eab0c82e6bbe55c357cc04a1579d96faf 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -42,6 +42,7 @@ pub mod prelude {
     pub use crate::children::KChildren;
     pub use crate::clone_component::PreviousWidget;
     pub use crate::context::*;
+    pub use crate::cursor::*;
     pub use crate::event::*;
     pub use crate::event_dispatcher::{EventDispatcher, EventDispatcherContext};
     pub use crate::focus_tree::Focusable;
diff --git a/src/render/font/extract.rs b/src/render/font/extract.rs
index e6051ccf5b7c580325259c9dc4973a65faa5676e..bba0488b75708df768d0621e574348f25e1b95f5 100644
--- a/src/render/font/extract.rs
+++ b/src/render/font/extract.rs
@@ -20,15 +20,17 @@ pub fn extract_texts(
     _dpi: f32,
 ) -> Vec<ExtractQuadBundle> {
     let mut extracted_texts = Vec::new();
-    let (background_color, text_layout, layout, font, properties) = match render_primitive {
+    let (background_color, text_layout, layout, font, properties, subpixel) = match render_primitive
+    {
         RenderPrimitive::Text {
             color,
             text_layout,
             layout,
             font,
             properties,
+            subpixel,
             ..
-        } => (color, text_layout, layout, font, *properties),
+        } => (color, text_layout, layout, font, *properties, subpixel),
         _ => panic!(""),
     };
 
@@ -40,6 +42,8 @@ pub fn extract_texts(
         }
     };
 
+    let forced = font_mapping.get_subpixel_forced(&font_handle);
+
     let base_position = Vec2::new(layout.posx, layout.posy + properties.font_size);
 
     for glyph_rect in text_layout.glyphs() {
@@ -60,7 +64,11 @@ pub fn extract_texts(
                 vertex_index: 0,
                 char_id: font.get_char_id(glyph_rect.content).unwrap(),
                 z_index: layout.z_index,
-                quad_type: UIQuadType::Text,
+                quad_type: if *subpixel || forced {
+                    UIQuadType::TextSubpixel
+                } else {
+                    UIQuadType::Text
+                },
                 type_index: 0,
                 border_radius: Corner::default(),
                 image: None,
diff --git a/src/render/font/font_mapping.rs b/src/render/font/font_mapping.rs
index df015c620ed19324f512cd2c7078a6218a42fd5b..bfd013416f54a760dc45a76ede8a450c565c921c 100644
--- a/src/render/font/font_mapping.rs
+++ b/src/render/font/font_mapping.rs
@@ -1,92 +1,107 @@
-use bevy::{
-    prelude::{Handle, Resource},
-    utils::HashMap,
-};
-use kayak_font::KayakFont;
-
-// use crate::context::Context;
-
-/// A resource used to manage fonts for use in a `KayakContext`
-///
-/// # Example
-///
-/// ```
-/// use bevy::prelude::*;
-/// use bevy_kayak_ui::FontMapping;
-///
-/// fn setup_ui(
-///   # mut commands: Commands,
-///   asset_server: Res<AssetServer>,
-///   mut font_mapping: ResMut<FontMapping>
-/// ) {
-///   # commands.spawn_bundle(UICameraBundle::new());
-///   #
-///   font_mapping.set_default(asset_server.load("roboto.kayak_font"));
-///   // ...
-///   #
-///   # let context = BevyContext::new(|context| {
-///   #   render! {
-///   #     <App>
-///   #       <Text content={"Hello World!".to_string()} />
-///   #     </App>
-///   #   }
-///   # });
-///   #
-///   # commands.insert_resource(context);
-/// }
-/// ```
-#[derive(Resource, Default)]
-pub struct FontMapping {
-    font_ids: HashMap<Handle<KayakFont>, String>,
-    font_handles: HashMap<String, Handle<KayakFont>>,
-    new_fonts: Vec<String>,
-}
-
-impl FontMapping {
-    /// Add a `KayakFont` to be tracked
-    pub fn add(&mut self, key: impl Into<String>, handle: Handle<KayakFont>) {
-        let key = key.into();
-        if !self.font_ids.contains_key(&handle) {
-            self.font_ids.insert(handle.clone(), key.clone());
-            self.new_fonts.push(key.clone());
-            self.font_handles.insert(key, handle);
-        }
-    }
-
-    /// Set a default `KayakFont`
-    pub fn set_default(&mut self, handle: Handle<KayakFont>) {
-        self.add(crate::DEFAULT_FONT, handle);
-    }
-
-    pub(crate) fn mark_all_as_new(&mut self) {
-        self.new_fonts.extend(self.font_handles.keys().cloned());
-    }
-
-    /// Get the handle for the given font name
-    pub fn get_handle(&self, id: String) -> Option<Handle<KayakFont>> {
-        self.font_handles.get(&id).cloned()
-    }
-
-    /// Get the font name for the given handle
-    pub fn get(&self, font: &Handle<KayakFont>) -> Option<String> {
-        self.font_ids.get(font).cloned()
-    }
-
-    // pub(crate) fn add_loaded_to_kayak(
-    //     &mut self,
-    //     fonts: &Res<Assets<KayakFont>>,
-    //     context: &Context,
-    // ) {
-    //     if let Ok(mut kayak_context) = context.kayak_context.write() {
-    //         let new_fonts = self.new_fonts.drain(..).collect::<Vec<_>>();
-    //         for font_key in new_fonts {
-    //             let font_handle = self.font_handles.get(&font_key).unwrap();
-    //             if let Some(font) = fonts.get(font_handle) {
-    //                 kayak_context.set_asset(font_key, font.clone());
-    //             } else {
-    //                 self.new_fonts.push(font_key);
-    //             }
-    //         }
-    //     }
-    // }
-}
+use bevy::{
+    prelude::{Handle, Resource},
+    utils::{HashMap, HashSet},
+};
+use kayak_font::KayakFont;
+
+// use crate::context::Context;
+
+/// A resource used to manage fonts for use in a `KayakContext`
+///
+/// # Example
+///
+/// ```
+/// use bevy::prelude::*;
+/// use bevy_kayak_ui::FontMapping;
+///
+/// fn setup_ui(
+///   # mut commands: Commands,
+///   asset_server: Res<AssetServer>,
+///   mut font_mapping: ResMut<FontMapping>
+/// ) {
+///   # commands.spawn_bundle(UICameraBundle::new());
+///   #
+///   font_mapping.set_default(asset_server.load("roboto.kayak_font"));
+///   // ...
+///   #
+///   # let context = BevyContext::new(|context| {
+///   #   render! {
+///   #     <App>
+///   #       <Text content={"Hello World!".to_string()} />
+///   #     </App>
+///   #   }
+///   # });
+///   #
+///   # commands.insert_resource(context);
+/// }
+/// ```
+#[derive(Resource, Default)]
+pub struct FontMapping {
+    font_ids: HashMap<Handle<KayakFont>, String>,
+    font_handles: HashMap<String, Handle<KayakFont>>,
+    new_fonts: Vec<String>,
+    subpixel: HashSet<Handle<KayakFont>>,
+}
+
+impl FontMapping {
+    /// Add a `KayakFont` to be tracked
+    pub fn add(&mut self, key: impl Into<String>, handle: Handle<KayakFont>) {
+        let key = key.into();
+        if !self.font_ids.contains_key(&handle) {
+            self.font_ids.insert(handle.clone(), key.clone());
+            self.new_fonts.push(key.clone());
+            self.font_handles.insert(key, handle);
+        }
+    }
+
+    /// Set a default `KayakFont`
+    pub fn set_default(&mut self, handle: Handle<KayakFont>) {
+        self.add(crate::DEFAULT_FONT, handle);
+    }
+
+    pub(crate) fn mark_all_as_new(&mut self) {
+        self.new_fonts.extend(self.font_handles.keys().cloned());
+    }
+
+    /// Get the handle for the given font name
+    pub fn get_handle(&self, id: String) -> Option<Handle<KayakFont>> {
+        self.font_handles.get(&id).cloned()
+    }
+
+    /// Get the font name for the given handle
+    pub fn get(&self, font: &Handle<KayakFont>) -> Option<String> {
+        self.font_ids.get(font).cloned()
+    }
+
+    /// Forces any text render commands to use subpixel font rendering for this specific font asset.
+    pub fn force_subpixel(&mut self, font: &Handle<KayakFont>) {
+        self.subpixel.insert(font.clone_weak());
+    }
+
+    /// Turns off the forced subpixel rendering mode for this font.
+    pub fn disable_subpixel(&mut self, font: &Handle<KayakFont>) {
+        self.subpixel.remove(font);
+    }
+
+    pub fn get_subpixel_forced(&self, font: &Handle<KayakFont>) -> bool {
+        self.subpixel.contains(font)
+    }
+
+    // pub(crate) fn add_loaded_to_kayak(
+    //     &mut self,
+    //     fonts: &Res<Assets<KayakFont>>,
+    //     context: &Context,
+    // ) {
+    //     if let Ok(mut kayak_context) = context.kayak_context.write() {
+    //         let new_fonts = self.new_fonts.drain(..).collect::<Vec<_>>();
+    //         for font_key in new_fonts {
+    //             let font_handle = self.font_handles.get(&font_key).unwrap();
+    //             if let Some(font) = fonts.get(font_handle) {
+    //                 kayak_context.set_asset(font_key, font.clone());
+    //             } else {
+    //                 self.new_fonts.push(font_key);
+    //             }
+    //         }
+    //     }
+    // }
+}
diff --git a/src/render/texture_atlas/extract.rs b/src/render/texture_atlas/extract.rs
index 9b1c750c9e02c0669009d890c90334ced90a1275..3e343fec29efb825f68afdd160acb381c25d061a 100644
--- a/src/render/texture_atlas/extract.rs
+++ b/src/render/texture_atlas/extract.rs
@@ -40,8 +40,7 @@ pub fn extract_texture_atlas(
                 i.texture_descriptor.size.height as f32,
             )
         })
-        .unwrap()
-        * dpi;
+        .unwrap();
 
     let quad = ExtractQuadBundle {
         extracted_quad: ExtractedQuad {
diff --git a/src/render/unified/pipeline.rs b/src/render/unified/pipeline.rs
index d8a71309ee8d32b1f574106dfd0a401a57f3d9a2..4d5aa330e4da2f9191d55a252257e925a26ee7c8 100644
--- a/src/render/unified/pipeline.rs
+++ b/src/render/unified/pipeline.rs
@@ -333,6 +333,7 @@ pub struct ExtractQuadBundle {
 pub enum UIQuadType {
     Quad,
     Text,
+    TextSubpixel,
     Image,
     Clip,
 }
@@ -416,18 +417,24 @@ pub fn prepare_quads(
         _padding_2: 0,
         _padding_3: 0,
     });
-    let text_type_offset = sprite_meta.types_buffer.push(QuadType {
+    let text_sub_pixel_type_offset = sprite_meta.types_buffer.push(QuadType {
         t: 1,
         _padding_1: 0,
         _padding_2: 0,
         _padding_3: 0,
     });
-    let image_type_offset = sprite_meta.types_buffer.push(QuadType {
+    let text_type_offset = sprite_meta.types_buffer.push(QuadType {
         t: 2,
         _padding_1: 0,
         _padding_2: 0,
         _padding_3: 0,
     });
+    let image_type_offset = sprite_meta.types_buffer.push(QuadType {
+        t: 3,
+        _padding_1: 0,
+        _padding_2: 0,
+        _padding_3: 0,
+    });
 
     sprite_meta
         .types_buffer
@@ -450,6 +457,7 @@ pub fn prepare_quads(
         match extracted_sprite.quad_type {
             UIQuadType::Quad => extracted_sprite.type_index = quad_type_offset,
             UIQuadType::Text => extracted_sprite.type_index = text_type_offset,
+            UIQuadType::TextSubpixel => extracted_sprite.type_index = text_sub_pixel_type_offset,
             UIQuadType::Image => extracted_sprite.type_index = image_type_offset,
             UIQuadType::Clip => {}
         };
diff --git a/src/render/unified/shader.wgsl b/src/render/unified/shader.wgsl
index 3aac721a0c798a032e70f5400905a0753a5c9705..d2308ad37247e4fa0d580f7414a976c36d5825b9 100644
--- a/src/render/unified/shader.wgsl
+++ b/src/render/unified/shader.wgsl
@@ -61,6 +61,15 @@ fn sdRoundBox(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32 {
     return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0))) - r;
 }
 
+fn median3(v: vec3<f32>) -> f32 {
+    return max(min(v.x, v.y), min(max(v.x, v.y), v.z));
+}
+
+fn sample_sdf(coords: vec2<f32>, arr: i32, scale: f32) -> f32 {
+    let sample = textureSample(font_texture, font_sampler, vec2(coords.xy), arr);
+    return clamp((median3(sample.rgb) - 0.5) * scale + 0.5, 0., 1.);
+}
+
 @fragment
 fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
     if quad_type.t == 0 {
@@ -77,16 +86,30 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
         return vec4<f32>(in.color.rgb, rect_dist * in.color.a);
     }
     if quad_type.t == 1 {
-        var px_range = 5.0;
+        var px_range = 2.5;
         var tex_dimensions = textureDimensions(font_texture);
-        var msdf_unit = vec2<f32>(px_range, px_range) / vec2<f32>(f32(tex_dimensions.x), f32(tex_dimensions.y));
-        var x = textureSample(font_texture, font_sampler, vec2<f32>(in.uv.x, 1.0 - in.uv.y), i32(in.uv.z));
-        var v = max(min(x.r, x.g), min(max(x.r, x.g), x.b));
-        var sig_dist = (v - 0.5) * dot(msdf_unit, 0.5 / fwidth(in.uv.xy));
-        var a = clamp(sig_dist + 0.5, 0.0, 1.0);
-        return vec4<f32>(in.color.rgb, a);
+        var msdf_unit = vec2(px_range, px_range) / vec2(f32(tex_dimensions.x), f32(tex_dimensions.y));
+        let subpixel_width = fwidth(in.uv.x) / 3.;
+        let scale = dot(msdf_unit, 0.5 / fwidth(in.uv.xy));
+        // RGB stripe sub-pixel arrangement
+        let red = sample_sdf(vec2(in.uv.x - subpixel_width, 1. - in.uv.y), i32(in.uv.z), scale);
+        let green = sample_sdf(vec2(in.uv.x, 1. - in.uv.y), i32(in.uv.z), scale);
+        let blue = sample_sdf(vec2(in.uv.x + subpixel_width, 1. - in.uv.y), i32(in.uv.z), scale);
+        // fudge: this really should be somehow blended per-channel, using alpha here is a nasty hack
+        let alpha = clamp(0.4 * (red + green + blue), 0., 1.);
+        return vec4(red * in.color.r, green * in.color.g, blue * in.color.b, alpha);
     }
     if quad_type.t == 2 {
+        var px_range = 2.5;
+        var tex_dimensions = textureDimensions(font_texture);
+        var msdf_unit = vec2(px_range, px_range) / vec2(f32(tex_dimensions.x), f32(tex_dimensions.y));
+        let scale = dot(msdf_unit, 0.5 / fwidth(in.uv.xy));
+
+        let alpha = sample_sdf(vec2(in.uv.x, 1. - in.uv.y), i32(in.uv.z), scale);
+
+        return vec4(in.color.rgb, alpha);
+    }
+    if quad_type.t == 3 {
         var bs = min(in.border_radius, min(in.size.x, in.size.y));
         var mask = sdRoundBox(
             in.pos.xy * 2.0 - (in.size.xy),
diff --git a/src/render_primitive.rs b/src/render_primitive.rs
index 9c3fa2d6820b8bc656199be97f0ffffe9d9e30aa..89835a8ea481c7e38c440a42e830e3870efeb2fc 100644
--- a/src/render_primitive.rs
+++ b/src/render_primitive.rs
@@ -29,6 +29,7 @@ pub enum RenderPrimitive {
         layout: Rect,
         properties: TextProperties,
         word_wrap: bool,
+        subpixel: bool,
     },
     Image {
         border_radius: Corner<f32>,
@@ -111,6 +112,7 @@ impl From<&KStyle> for RenderPrimitive {
                 content,
                 alignment,
                 word_wrap,
+                subpixel,
             } => Self::Text {
                 color: style.color.resolve(),
                 content,
@@ -124,6 +126,7 @@ impl From<&KStyle> for RenderPrimitive {
                     ..Default::default()
                 },
                 word_wrap,
+                subpixel,
             },
             RenderCommand::Image { handle } => Self::Image {
                 border_radius: style.border_radius.resolve(),
diff --git a/src/styles/mod.rs b/src/styles/mod.rs
index 2ffda25864e0c179e0e809fc2f4d3d60982e277c..8e92d3245704e232ac5d1780b02aed37867f554c 100644
--- a/src/styles/mod.rs
+++ b/src/styles/mod.rs
@@ -1,3 +1,5 @@
+use bevy::{prelude::Component, reflect::Reflect};
+
 mod corner;
 mod edge;
 mod options_ref;
@@ -11,3 +13,12 @@ pub use options_ref::AsRefOption;
 pub use render_command::RenderCommand;
 pub use style::*;
 pub use units::*;
+
+#[derive(Component, Reflect, Debug, Default, Clone, PartialEq)]
+pub struct ComputedStyles(pub KStyle);
+
+impl Into<ComputedStyles> for KStyle {
+    fn into(self) -> ComputedStyles {
+        ComputedStyles(self)
+    }
+}
diff --git a/src/styles/render_command.rs b/src/styles/render_command.rs
index d5a2630214bf9738b15858a23017f631f7c9110c..7583648da8fbec097b789cfe9dc5f591f3948c02 100644
--- a/src/styles/render_command.rs
+++ b/src/styles/render_command.rs
@@ -17,6 +17,7 @@ pub enum RenderCommand {
         content: String,
         alignment: Alignment,
         word_wrap: bool,
+        subpixel: bool,
     },
     Image {
         handle: Handle<Image>,
diff --git a/src/widget.rs b/src/widget.rs
index bd4a21b66768af14e0da89c916a93afc0ab6af32..1746669e75bdad6c337b50defbd54d38ec094023 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -7,7 +7,7 @@ use crate::{
     children::KChildren,
     context::{Mounted, WidgetName},
     prelude::KayakWidgetContext,
-    styles::KStyle,
+    styles::{ComputedStyles, KStyle},
 };
 
 pub trait Widget: Send + Sync {
@@ -57,6 +57,7 @@ pub struct WidgetParam<'w, 's, Props: PartialEq + Component, State: PartialEq +
     pub old_props_query: Query<'w, 's, &'static Props>,
     pub mounted_query: Query<'w, 's, Entity, With<Mounted>>,
     pub style_query: Query<'w, 's, &'static KStyle>,
+    pub computed_style_query: Query<'w, 's, &'static ComputedStyles>,
     pub children_query: Query<'w, 's, &'static KChildren>,
     pub state_query: Query<'w, 's, &'static State>,
     pub widget_names: Query<'w, 's, &'static WidgetName>,
@@ -95,6 +96,21 @@ impl<'w, 's, Props: PartialEq + Component, State: PartialEq + Component>
             }
         }
 
+        // Compare computed styles
+        if let (Ok(style), Ok(old_style)) = (
+            self.computed_style_query.get(current_entity),
+            self.computed_style_query.get(previous_entity),
+        ) {
+            if style != old_style {
+                log::trace!(
+                    "Entity computed styles have changed! {}-{}",
+                    self.widget_names.get(current_entity).unwrap().0,
+                    current_entity.index()
+                );
+                return true;
+            }
+        }
+
         // Compare children
         // If children don't exist ignore as mount will add them!
         if let (Ok(children), Ok(old_children)) = (
diff --git a/src/widgets/app.rs b/src/widgets/app.rs
index db871b596648cc32792d3d0623f98a9bd104240d..67b9062199779ba3765624d4c80c386a3294f0b0 100644
--- a/src/widgets/app.rs
+++ b/src/widgets/app.rs
@@ -5,7 +5,7 @@ use crate::{
     children::KChildren,
     context::WidgetName,
     prelude::KayakWidgetContext,
-    styles::{KStyle, RenderCommand, StyleProp, Units},
+    styles::{ComputedStyles, KStyle, RenderCommand, StyleProp, Units},
     widget::{EmptyState, Widget, WidgetParam},
     CameraUIKayak,
 };
@@ -24,6 +24,7 @@ impl Widget for KayakApp {}
 pub struct KayakAppBundle {
     pub app: KayakApp,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub children: KChildren,
     pub widget_name: WidgetName,
 }
@@ -33,6 +34,7 @@ impl Default for KayakAppBundle {
         Self {
             app: Default::default(),
             styles: Default::default(),
+            computed_styles: ComputedStyles::default(),
             children: Default::default(),
             widget_name: KayakApp::default().get_name(),
         }
@@ -47,22 +49,24 @@ pub fn app_update(
 ) -> bool {
     let mut window_change = false;
 
-    if let Ok(app_style) = widget_param.style_query.get(entity) {
+    if let Ok(app_style) = widget_param.computed_style_query.get(entity) {
         if let Some(camera_entity) = widget_context.camera_entity {
             if let Ok(camera) = camera.get(camera_entity) {
                 if let Some(size) = camera.logical_viewport_size() {
-                    if app_style.width != StyleProp::Value(Units::Pixels(size.x)) {
+                    if app_style.0.width != StyleProp::Value(Units::Pixels(size.x)) {
                         window_change = true;
                     }
-                    if app_style.height != StyleProp::Value(Units::Pixels(size.y)) {
+                    if app_style.0.height != StyleProp::Value(Units::Pixels(size.y)) {
                         window_change = true;
                     }
                 } else {
                     let primary_window = windows.get_primary().unwrap();
-                    if app_style.width != StyleProp::Value(Units::Pixels(primary_window.width())) {
+                    if app_style.0.width != StyleProp::Value(Units::Pixels(primary_window.width()))
+                    {
                         window_change = true;
                     }
-                    if app_style.height != StyleProp::Value(Units::Pixels(primary_window.height()))
+                    if app_style.0.height
+                        != StyleProp::Value(Units::Pixels(primary_window.height()))
                     {
                         window_change = true;
                     }
@@ -78,7 +82,7 @@ pub fn app_update(
 pub fn app_render(
     In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
     mut commands: Commands,
-    mut query: Query<(&mut KStyle, &KChildren)>,
+    mut query: Query<(&KStyle, &mut ComputedStyles, &KChildren)>,
     camera: Query<&Camera, With<CameraUIKayak>>,
     windows: Res<Windows>,
     images: Res<Assets<Image>>,
@@ -105,15 +109,17 @@ pub fn app_render(
         }
     }
 
-    if let Ok((mut app_style, children)) = query.get_mut(entity) {
-        if app_style.width != StyleProp::Value(Units::Pixels(width)) {
-            app_style.width = StyleProp::Value(Units::Pixels(width));
-        }
-        if app_style.height != StyleProp::Value(Units::Pixels(height)) {
-            app_style.height = StyleProp::Value(Units::Pixels(height));
-        }
+    if let Ok((app_style, mut computed_styles, children)) = query.get_mut(entity) {
+        *computed_styles = KStyle::default()
+            .with_style(KStyle {
+                render_command: RenderCommand::Layout.into(),
+                width: Units::Pixels(width).into(),
+                height: Units::Pixels(height).into(),
+                ..Default::default()
+            })
+            .with_style(app_style)
+            .into();
 
-        app_style.render_command = StyleProp::Value(RenderCommand::Layout);
         let parent_id = Some(entity);
         rsx! {
             <ClipBundle
diff --git a/src/widgets/background.rs b/src/widgets/background.rs
index c2146047ecb484a374ae80a9c22e5bf5626ef863..533b96e30d3bc2720febb852c2d4b3ded0fb5327 100644
--- a/src/widgets/background.rs
+++ b/src/widgets/background.rs
@@ -5,7 +5,7 @@ use crate::{
     context::WidgetName,
     on_event::OnEvent,
     prelude::KayakWidgetContext,
-    styles::{KStyle, RenderCommand, StyleProp},
+    styles::{ComputedStyles, KStyle, RenderCommand},
     widget::Widget,
 };
 
@@ -24,6 +24,7 @@ impl Widget for Background {}
 pub struct BackgroundBundle {
     pub background: Background,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub children: KChildren,
     pub on_event: OnEvent,
     pub widget_name: WidgetName,
@@ -34,6 +35,7 @@ impl Default for BackgroundBundle {
         Self {
             background: Default::default(),
             styles: Default::default(),
+            computed_styles: Default::default(),
             children: Default::default(),
             on_event: Default::default(),
             widget_name: Background::default().get_name(),
@@ -44,10 +46,16 @@ impl Default for BackgroundBundle {
 pub fn background_render(
     In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
     _: Commands,
-    mut query: Query<(&mut KStyle, &KChildren)>,
+    mut query: Query<(&KStyle, &mut ComputedStyles, &KChildren)>,
 ) -> bool {
-    if let Ok((mut style, children)) = query.get_mut(entity) {
-        style.render_command = StyleProp::Value(RenderCommand::Quad);
+    if let Ok((style, mut computed_styles, children)) = query.get_mut(entity) {
+        *computed_styles = KStyle::default()
+            .with_style(KStyle {
+                render_command: RenderCommand::Quad.into(),
+                ..Default::default()
+            })
+            .with_style(style)
+            .into();
         children.process(&widget_context, Some(entity));
     }
     true
diff --git a/src/widgets/button.rs b/src/widgets/button.rs
index ab772e2de7b527d71a164502a4e650f434858f7c..9187d9070b705b4a456e9b02289c2429f8cff822 100644
--- a/src/widgets/button.rs
+++ b/src/widgets/button.rs
@@ -11,7 +11,7 @@ use crate::{
     event_dispatcher::EventDispatcherContext,
     on_event::OnEvent,
     prelude::{KChildren, KayakWidgetContext, Units},
-    styles::{Corner, Edge, KCursorIcon, KStyle, RenderCommand, StyleProp},
+    styles::{ComputedStyles, Corner, Edge, KCursorIcon, KStyle, RenderCommand, StyleProp},
     widget::Widget,
     widget_state::WidgetState,
 };
@@ -21,7 +21,6 @@ use super::{ElementBundle, TextProps, TextWidgetBundle};
 #[derive(Component, PartialEq, Clone, Default)]
 pub struct KButton {
     pub text: String,
-    pub user_styles: KStyle,
 }
 
 /// Default button widget
@@ -30,6 +29,7 @@ pub struct KButton {
 pub struct KButtonBundle {
     pub button: KButton,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub on_event: OnEvent,
     pub widget_name: WidgetName,
 }
@@ -39,6 +39,7 @@ impl Default for KButtonBundle {
         Self {
             button: Default::default(),
             styles: Default::default(),
+            computed_styles: Default::default(),
             on_event: Default::default(),
             widget_name: KButton::default().get_name(),
         }
@@ -55,22 +56,22 @@ pub struct ButtonState {
 pub fn button_render(
     In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
     mut commands: Commands,
-    mut query: Query<(&KButton, &mut KStyle)>,
+    mut query: Query<(&KButton, &KStyle, &mut ComputedStyles)>,
     state_query: Query<&ButtonState>,
 ) -> bool {
-    if let Ok((button, mut style)) = query.get_mut(entity) {
+    if let Ok((button, styles, mut computed_styles)) = query.get_mut(entity) {
         let hover_color = Color::rgba(0.592, 0.627, 0.749, 1.0); //Color::rgba(0.549, 0.666, 0.933, 1.0);
                                                                  // let color = Color::rgba(0.254, 0.270, 0.349, 1.0);
         let state_entity =
             widget_context.use_state(&mut commands, entity, ButtonState { hovering: false });
 
         if let Ok(state) = state_query.get(state_entity) {
-            *style = KStyle::default()
+            *computed_styles = KStyle::default()
                 .with_style(KStyle {
                     render_command: StyleProp::Value(RenderCommand::Quad),
                     ..Default::default()
                 })
-                .with_style(button.user_styles.clone())
+                .with_style(styles)
                 .with_style(KStyle {
                     background_color: Color::rgba(0.254, 0.270, 0.349, 1.0).into(),
                     border_color: if state.hovering {
@@ -84,7 +85,8 @@ pub fn button_render(
                     width: Units::Stretch(1.0).into(),
                     cursor: StyleProp::Value(KCursorIcon(CursorIcon::Hand)),
                     ..Default::default()
-                });
+                })
+                .into();
 
             let on_event = OnEvent::new(
                 move |In((event_dispatcher_context, _, mut event, _entity)): In<(
@@ -121,15 +123,17 @@ pub fn button_render(
                     on_event={on_event}
                 >
                     <TextWidgetBundle
+                        styles={KStyle {
+                            top: Units::Stretch(1.0).into(),
+                            bottom: Units::Stretch(1.0).into(),
+                            left: Units::Stretch(1.0).into(),
+                            right: Units::Stretch(1.0).into(),
+                            ..Default::default()
+                        }}
                         text={TextProps {
-                            alignment: Alignment::Middle,
+                            alignment: Alignment::Start,
                             content: button.text.clone(),
                             size: 16.0,
-                            user_styles: KStyle {
-                                top: Units::Stretch(1.0).into(),
-                                bottom: Units::Stretch(1.0).into(),
-                                ..Default::default()
-                            },
                             ..Default::default()
                         }}
                     />
diff --git a/src/widgets/clip.rs b/src/widgets/clip.rs
index 3f3e3e570f1f1af74e5ac3bacd6308ec0bb89e23..51f53f01aa64379d544cff2c1a4cbe69f321b0bc 100644
--- a/src/widgets/clip.rs
+++ b/src/widgets/clip.rs
@@ -4,7 +4,7 @@ use crate::{
     children::KChildren,
     context::WidgetName,
     prelude::KayakWidgetContext,
-    styles::{KStyle, RenderCommand, StyleProp, Units},
+    styles::{ComputedStyles, KStyle, RenderCommand, Units},
     widget::Widget,
 };
 
@@ -22,6 +22,7 @@ impl Widget for Clip {}
 pub struct ClipBundle {
     pub clip: Clip,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub children: KChildren,
     pub widget_name: WidgetName,
 }
@@ -30,12 +31,8 @@ impl Default for ClipBundle {
     fn default() -> Self {
         Self {
             clip: Clip::default(),
-            styles: KStyle {
-                render_command: StyleProp::Value(RenderCommand::Clip),
-                height: StyleProp::Value(Units::Stretch(1.0)),
-                width: StyleProp::Value(Units::Stretch(1.0)),
-                ..KStyle::default()
-            },
+            styles: KStyle::default(),
+            computed_styles: ComputedStyles::default(),
             children: KChildren::default(),
             widget_name: Clip::default().get_name(),
         }
@@ -45,10 +42,21 @@ impl Default for ClipBundle {
 pub fn clip_render(
     In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
     _: Commands,
-    mut query: Query<(&mut KStyle, &KChildren)>,
+    mut query: Query<(&KStyle, &mut ComputedStyles, &KChildren)>,
 ) -> bool {
-    if let Ok((mut styles, children)) = query.get_mut(entity) {
-        styles.render_command = StyleProp::Value(RenderCommand::Clip);
+    if let Ok((styles, mut computed_styles, children)) = query.get_mut(entity) {
+        *computed_styles = KStyle::default()
+            .with_style(KStyle {
+                render_command: RenderCommand::Clip.into(),
+                ..Default::default()
+            })
+            .with_style(styles)
+            .with_style(KStyle {
+                width: Units::Stretch(1.0).into(),
+                height: Units::Stretch(1.0).into(),
+                ..Default::default()
+            })
+            .into();
         children.process(&widget_context, Some(entity));
     }
     true
diff --git a/src/widgets/element.rs b/src/widgets/element.rs
index d5663b3bf0a4d4deeea9198d1f85427d928ad3ae..2b97ca13be620fd0bdd671dbb7159047e7568612 100644
--- a/src/widgets/element.rs
+++ b/src/widgets/element.rs
@@ -5,7 +5,7 @@ use crate::{
     context::WidgetName,
     on_event::OnEvent,
     prelude::KayakWidgetContext,
-    styles::{KStyle, RenderCommand, StyleProp},
+    styles::{ComputedStyles, KStyle, RenderCommand, StyleProp},
     widget::Widget,
 };
 
@@ -21,6 +21,7 @@ impl Widget for Element {}
 pub struct ElementBundle {
     pub element: Element,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub on_event: OnEvent,
     pub children: KChildren,
     pub widget_name: WidgetName,
@@ -31,6 +32,7 @@ impl Default for ElementBundle {
         Self {
             element: Default::default(),
             styles: Default::default(),
+            computed_styles: ComputedStyles::default(),
             children: Default::default(),
             on_event: OnEvent::default(),
             widget_name: Element::default().get_name(),
@@ -41,15 +43,16 @@ impl Default for ElementBundle {
 pub fn element_render(
     In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
     _: Commands,
-    mut query: Query<(&mut KStyle, &KChildren)>,
+    mut query: Query<(&KStyle, &mut ComputedStyles, &KChildren)>,
 ) -> bool {
-    if let Ok((mut style, children)) = query.get_mut(entity) {
-        *style = KStyle::default()
-            .with_style(style.clone())
+    if let Ok((style, mut computed_styles, children)) = query.get_mut(entity) {
+        *computed_styles = KStyle::default()
+            .with_style(style)
             .with_style(KStyle {
                 render_command: StyleProp::Value(RenderCommand::Layout),
                 ..Default::default()
-            });
+            })
+            .into();
         children.process(&widget_context, Some(entity));
     }
     true
diff --git a/src/widgets/image.rs b/src/widgets/image.rs
index bc6d5cfe3f2c6085d1c50d99af58630c3b97a64e..a01004b01d9696a8343ac35348ccacfa55c0d726 100644
--- a/src/widgets/image.rs
+++ b/src/widgets/image.rs
@@ -3,7 +3,7 @@ use bevy::prelude::{Bundle, Component, Entity, Handle, In, Query};
 use crate::{
     context::WidgetName,
     prelude::KayakWidgetContext,
-    styles::{KStyle, RenderCommand, StyleProp},
+    styles::{ComputedStyles, KStyle, RenderCommand},
     widget::Widget,
 };
 
@@ -18,6 +18,7 @@ impl Widget for KImage {}
 pub struct KImageBundle {
     pub image: KImage,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub widget_name: WidgetName,
 }
 
@@ -26,6 +27,7 @@ impl Default for KImageBundle {
         Self {
             image: Default::default(),
             styles: Default::default(),
+            computed_styles: ComputedStyles::default(),
             widget_name: KImage::default().get_name(),
         }
     }
@@ -33,12 +35,19 @@ impl Default for KImageBundle {
 
 pub fn image_render(
     In((_widget_context, entity)): In<(KayakWidgetContext, Entity)>,
-    mut query: Query<(&mut KStyle, &KImage)>,
+    mut query: Query<(&KStyle, &mut ComputedStyles, &KImage)>,
 ) -> bool {
-    if let Ok((mut style, image)) = query.get_mut(entity) {
-        style.render_command = StyleProp::Value(RenderCommand::Image {
-            handle: image.0.clone_weak(),
-        });
+    if let Ok((style, mut computed_styles, image)) = query.get_mut(entity) {
+        *computed_styles = KStyle::default()
+            .with_style(KStyle {
+                render_command: RenderCommand::Image {
+                    handle: image.0.clone_weak(),
+                }
+                .into(),
+                ..Default::default()
+            })
+            .with_style(style)
+            .into();
     }
     true
 }
diff --git a/src/widgets/nine_patch.rs b/src/widgets/nine_patch.rs
index 0dd5b91e2fe6b5051845514da22d15f2fb2bf7c4..d3f55728af0185c5f6c3b98ef09ae7bb82756f79 100644
--- a/src/widgets/nine_patch.rs
+++ b/src/widgets/nine_patch.rs
@@ -5,7 +5,7 @@ use crate::{
     context::WidgetName,
     on_event::OnEvent,
     prelude::KayakWidgetContext,
-    styles::{Edge, KStyle, RenderCommand, StyleProp},
+    styles::{ComputedStyles, Edge, KStyle, RenderCommand},
     widget::Widget,
 };
 
@@ -52,6 +52,7 @@ impl Widget for NinePatch {}
 pub struct NinePatchBundle {
     pub nine_patch: NinePatch,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub children: KChildren,
     pub on_event: OnEvent,
     pub widget_name: WidgetName,
@@ -62,6 +63,7 @@ impl Default for NinePatchBundle {
         Self {
             nine_patch: Default::default(),
             styles: Default::default(),
+            computed_styles: ComputedStyles::default(),
             children: KChildren::default(),
             on_event: OnEvent::default(),
             widget_name: NinePatch::default().get_name(),
@@ -72,13 +74,20 @@ impl Default for NinePatchBundle {
 pub fn nine_patch_render(
     In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
     _: Commands,
-    mut query: Query<(&mut KStyle, &NinePatch, &KChildren)>,
+    mut query: Query<(&KStyle, &mut ComputedStyles, &NinePatch, &KChildren)>,
 ) -> bool {
-    if let Ok((mut style, nine_patch, children)) = query.get_mut(entity) {
-        style.render_command = StyleProp::Value(RenderCommand::NinePatch {
-            border: nine_patch.border,
-            handle: nine_patch.handle.clone_weak(),
-        });
+    if let Ok((style, mut computed_styles, nine_patch, children)) = query.get_mut(entity) {
+        *computed_styles = KStyle::default()
+            .with_style(KStyle {
+                render_command: RenderCommand::NinePatch {
+                    border: nine_patch.border,
+                    handle: nine_patch.handle.clone_weak(),
+                }
+                .into(),
+                ..Default::default()
+            })
+            .with_style(style)
+            .into();
         children.process(&widget_context, Some(entity));
     }
 
diff --git a/src/widgets/scroll/scroll_bar.rs b/src/widgets/scroll/scroll_bar.rs
index 5d63688c0f821033014020d579383eb41126f1ce..27ffec41a4dd2703cbb3826d349fed8c5ba2e13c 100644
--- a/src/widgets/scroll/scroll_bar.rs
+++ b/src/widgets/scroll/scroll_bar.rs
@@ -7,7 +7,7 @@ use crate::{
     event_dispatcher::EventDispatcherContext,
     on_event::OnEvent,
     prelude::{KChildren, KayakWidgetContext},
-    styles::{Corner, Edge, KPositionType, KStyle, RenderCommand, Units},
+    styles::{ComputedStyles, Corner, Edge, KPositionType, KStyle, RenderCommand, Units},
     widget::Widget,
     widget_state::WidgetState,
     widgets::{BackgroundBundle, ClipBundle},
@@ -40,6 +40,7 @@ impl Widget for ScrollBarProps {}
 pub struct ScrollBarBundle {
     pub scrollbar_props: ScrollBarProps,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub widget_name: WidgetName,
 }
 
@@ -48,6 +49,7 @@ impl Default for ScrollBarBundle {
         Self {
             scrollbar_props: Default::default(),
             styles: Default::default(),
+            computed_styles: ComputedStyles::default(),
             widget_name: ScrollBarProps::default().get_name(),
         }
     }
@@ -56,10 +58,10 @@ impl Default for ScrollBarBundle {
 pub fn scroll_bar_render(
     In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
     mut commands: Commands,
-    mut query: Query<(&ScrollBarProps, &mut KStyle)>,
+    mut query: Query<(&ScrollBarProps, &KStyle, &mut ComputedStyles)>,
     context_query: Query<&ScrollContext>,
 ) -> bool {
-    if let Ok((scrollbar, mut styles)) = query.get_mut(entity) {
+    if let Ok((scrollbar, styles, mut computed_styles)) = query.get_mut(entity) {
         if let Some(context_entity) = widget_context.get_context_entity::<ScrollContext>(entity) {
             if let Ok(scroll_context) = context_query.get(context_entity) {
                 let scroll_x = scroll_context.scroll_x();
@@ -112,22 +114,25 @@ pub fn scroll_bar_render(
                 );
 
                 // === Styles === //
-                *styles = KStyle::default().with_style(KStyle {
-                    render_command: RenderCommand::Layout.into(),
-                    width: if horizontal {
-                        Units::Stretch(1.0)
-                    } else {
-                        Units::Pixels(thickness)
-                    }
-                    .into(),
-                    height: if horizontal {
-                        Units::Pixels(thickness)
-                    } else {
-                        Units::Stretch(1.0)
-                    }
-                    .into(),
-                    ..Default::default()
-                });
+                *computed_styles = KStyle::default()
+                    .with_style(KStyle {
+                        render_command: RenderCommand::Layout.into(),
+                        width: if horizontal {
+                            Units::Stretch(1.0)
+                        } else {
+                            Units::Pixels(thickness)
+                        }
+                        .into(),
+                        height: if horizontal {
+                            Units::Pixels(thickness)
+                        } else {
+                            Units::Stretch(1.0)
+                        }
+                        .into(),
+                        ..Default::default()
+                    })
+                    .with_style(styles)
+                    .into();
 
                 let mut track_style =
                     KStyle::default()
diff --git a/src/widgets/scroll/scroll_box.rs b/src/widgets/scroll/scroll_box.rs
index 7803295537a7b9e6a2f5662cbffb86ae76d02abd..3a285785b171e880bf5a7607cfbb73fa8cdbd693 100644
--- a/src/widgets/scroll/scroll_box.rs
+++ b/src/widgets/scroll/scroll_box.rs
@@ -10,7 +10,7 @@ use crate::{
     on_event::OnEvent,
     on_layout::OnLayout,
     prelude::{constructor, rsx, KayakWidgetContext},
-    styles::{KPositionType, KStyle, LayoutType, RenderCommand, Units},
+    styles::{ComputedStyles, KPositionType, KStyle, LayoutType, RenderCommand, Units},
     widget::Widget,
     widget_state::WidgetState,
     widgets::{
@@ -59,6 +59,7 @@ impl Widget for ScrollBoxProps {}
 pub struct ScrollBoxBundle {
     pub scroll_box_props: ScrollBoxProps,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub children: KChildren,
     pub on_layout: OnLayout,
     pub widget_name: WidgetName,
@@ -69,6 +70,7 @@ impl Default for ScrollBoxBundle {
         Self {
             scroll_box_props: Default::default(),
             styles: Default::default(),
+            computed_styles: ComputedStyles::default(),
             children: Default::default(),
             on_layout: Default::default(),
             widget_name: ScrollBoxProps::default().get_name(),
@@ -79,10 +81,17 @@ impl Default for ScrollBoxBundle {
 pub fn scroll_box_render(
     In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
     mut commands: Commands,
-    mut query: Query<(&ScrollBoxProps, &mut KStyle, &KChildren, &mut OnLayout)>,
+    mut query: Query<(
+        &ScrollBoxProps,
+        &KStyle,
+        &mut ComputedStyles,
+        &KChildren,
+        &mut OnLayout,
+    )>,
     mut context_query: ParamSet<(Query<&ScrollContext>, Query<&mut ScrollContext>)>,
 ) -> bool {
-    if let Ok((scroll_box, mut styles, scroll_box_children, mut on_layout)) = query.get_mut(entity)
+    if let Ok((scroll_box, styles, mut computed_styles, scroll_box_children, mut on_layout)) =
+        query.get_mut(entity)
     {
         if let Some(context_entity) = widget_context.get_context_entity::<ScrollContext>(entity) {
             if let Ok(scroll_context) = context_query.p0().get(context_entity).cloned() {
@@ -139,17 +148,18 @@ pub fn scroll_box_render(
                 );
 
                 // === Styles === //
-                *styles = KStyle::default()
+                *computed_styles = KStyle::default()
                     .with_style(KStyle {
                         render_command: RenderCommand::Layout.into(),
                         ..Default::default()
                     })
-                    .with_style(styles.clone())
+                    .with_style(styles)
                     .with_style(KStyle {
                         width: Units::Stretch(1.0).into(),
                         height: Units::Stretch(1.0).into(),
                         ..Default::default()
-                    });
+                    })
+                    .into();
 
                 let hbox_styles = KStyle::default().with_style(KStyle {
                     render_command: RenderCommand::Layout.into(),
diff --git a/src/widgets/scroll/scroll_content.rs b/src/widgets/scroll/scroll_content.rs
index 4c11937c57041df47ed75add66e650eb6942dab9..b84188c04dd4deedf2db51dfe615aa7002a40959 100644
--- a/src/widgets/scroll/scroll_content.rs
+++ b/src/widgets/scroll/scroll_content.rs
@@ -7,7 +7,7 @@ use crate::{
     layout::LayoutEvent,
     on_layout::OnLayout,
     prelude::KayakWidgetContext,
-    styles::{KStyle, LayoutType, RenderCommand, Units},
+    styles::{ComputedStyles, KStyle, LayoutType, RenderCommand, Units},
     widget::Widget,
 };
 
@@ -22,6 +22,7 @@ impl Widget for ScrollContentProps {}
 pub struct ScrollContentBundle {
     pub scroll_content_props: ScrollContentProps,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub children: KChildren,
     pub on_layout: OnLayout,
     pub widget_name: WidgetName,
@@ -32,6 +33,7 @@ impl Default for ScrollContentBundle {
         Self {
             scroll_content_props: Default::default(),
             styles: Default::default(),
+            computed_styles: ComputedStyles::default(),
             children: Default::default(),
             on_layout: Default::default(),
             widget_name: ScrollContentProps::default().get_name(),
@@ -41,10 +43,13 @@ impl Default for ScrollContentBundle {
 
 pub fn scroll_content_render(
     In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
-    mut query: Query<(&mut KStyle, &KChildren, &mut OnLayout), With<ScrollContentProps>>,
+    mut query: Query<
+        (&KStyle, &mut ComputedStyles, &KChildren, &mut OnLayout),
+        With<ScrollContentProps>,
+    >,
     context_query: Query<&ScrollContext>,
 ) -> bool {
-    if let Ok((mut styles, children, mut on_layout)) = query.get_mut(entity) {
+    if let Ok((styles, mut computed_styles, children, mut on_layout)) = query.get_mut(entity) {
         if let Some(context_entity) = widget_context.get_context_entity::<ScrollContext>(entity) {
             if let Ok(scroll_context) = context_query.get(context_entity) {
                 // === OnLayout === //
@@ -65,7 +70,7 @@ pub fn scroll_content_render(
                 );
 
                 // === Styles === //
-                *styles = KStyle::default()
+                *computed_styles = KStyle::default()
                     .with_style(KStyle {
                         render_command: RenderCommand::Layout.into(),
                         layout_type: LayoutType::Column.into(),
@@ -81,7 +86,8 @@ pub fn scroll_content_render(
                         height: Units::Auto.into(),
                         ..Default::default()
                     })
-                    .with_style(styles.clone());
+                    .with_style(styles)
+                    .into();
 
                 children.process(&widget_context, Some(entity));
             }
diff --git a/src/widgets/scroll/scroll_context.rs b/src/widgets/scroll/scroll_context.rs
index 5ae99ca79f2cc37c4932c8431141ea17e3e8ee0b..c0cb6d02778665e4ed2aa6e8038de013ee2c6827 100644
--- a/src/widgets/scroll/scroll_context.rs
+++ b/src/widgets/scroll/scroll_context.rs
@@ -1,7 +1,10 @@
 use bevy::prelude::{Bundle, Commands, Component, Entity, In, Query, Vec2};
 
 use crate::{
-    children::KChildren, context::WidgetName, prelude::KayakWidgetContext, styles::KStyle,
+    children::KChildren,
+    context::WidgetName,
+    prelude::KayakWidgetContext,
+    styles::{ComputedStyles, KStyle},
     widget::Widget,
 };
 
@@ -143,6 +146,7 @@ pub struct ScrollContextProviderBundle {
     pub scroll_context_provider: ScrollContextProvider,
     pub children: KChildren,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub widget_name: WidgetName,
 }
 
@@ -152,6 +156,7 @@ impl Default for ScrollContextProviderBundle {
             scroll_context_provider: Default::default(),
             children: KChildren::default(),
             styles: Default::default(),
+            computed_styles: ComputedStyles::default(),
             widget_name: ScrollContextProvider::default().get_name(),
         }
     }
@@ -160,11 +165,17 @@ impl Default for ScrollContextProviderBundle {
 pub fn scroll_context_render(
     In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
     mut commands: Commands,
-    mut query: Query<(&ScrollContextProvider, &KChildren)>,
+    mut query: Query<(
+        &ScrollContextProvider,
+        &KChildren,
+        &KStyle,
+        &mut ComputedStyles,
+    )>,
 ) -> bool {
-    if let Ok((context_provider, children)) = query.get_mut(entity) {
+    if let Ok((context_provider, children, styles, mut computed_styles)) = query.get_mut(entity) {
         let context_entity = commands.spawn(context_provider.initial_value).id();
         widget_context.set_context_entity::<ScrollContext>(Some(entity), context_entity);
+        *computed_styles = styles.clone().into();
         children.process(&widget_context, Some(entity));
     }
 
diff --git a/src/widgets/text.rs b/src/widgets/text.rs
index 574c7c0324fbcd770360f758257be9548683e9ca..6de78c40f61be608c9e8122cdbd801e83d27ac74 100644
--- a/src/widgets/text.rs
+++ b/src/widgets/text.rs
@@ -4,7 +4,7 @@ use kayak_font::Alignment;
 use crate::{
     context::WidgetName,
     prelude::KayakWidgetContext,
-    styles::{KCursorIcon, KStyle, RenderCommand, StyleProp, Units},
+    styles::{ComputedStyles, KCursorIcon, KStyle, RenderCommand, StyleProp},
     widget::Widget,
 };
 
@@ -28,11 +28,11 @@ pub struct TextProps {
     pub size: f32,
     /// Text alignment.
     pub alignment: Alignment,
-    /// Custom styles to pass in.
-    pub user_styles: KStyle,
     /// Basic word wrapping.
     /// Defautls to true
     pub word_wrap: bool,
+    /// Enables subpixel rendering of text. This is useful on smaller low-dpi screens.
+    pub subpixel: bool,
 }
 
 impl Default for TextProps {
@@ -45,7 +45,7 @@ impl Default for TextProps {
             size: -1.0,
             alignment: Alignment::Start,
             word_wrap: true,
-            user_styles: Default::default(),
+            subpixel: false,
         }
     }
 }
@@ -58,6 +58,7 @@ impl Widget for TextProps {}
 pub struct TextWidgetBundle {
     pub text: TextProps,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub widget_name: WidgetName,
 }
 
@@ -65,11 +66,8 @@ impl Default for TextWidgetBundle {
     fn default() -> Self {
         Self {
             text: Default::default(),
-            styles: KStyle {
-                width: Units::Stretch(1.0).into(),
-                height: Units::Stretch(1.0).into(),
-                ..Default::default()
-            },
+            styles: KStyle::default(),
+            computed_styles: ComputedStyles::default(),
             widget_name: TextProps::default().get_name(),
         }
     }
@@ -77,16 +75,17 @@ impl Default for TextWidgetBundle {
 
 pub fn text_render(
     In((_widget_context, entity)): In<(KayakWidgetContext, Entity)>,
-    mut query: Query<(&mut KStyle, &TextProps)>,
+    mut query: Query<(&KStyle, &mut ComputedStyles, &TextProps)>,
 ) -> bool {
-    if let Ok((mut styles, text)) = query.get_mut(entity) {
-        *styles = KStyle::default()
-            .with_style(&text.user_styles)
+    if let Ok((styles, mut computed_styles, text)) = query.get_mut(entity) {
+        *computed_styles = KStyle::default()
+            .with_style(styles)
             .with_style(KStyle {
                 render_command: StyleProp::Value(RenderCommand::Text {
                     content: text.content.clone(),
                     alignment: text.alignment,
                     word_wrap: text.word_wrap,
+                    subpixel: text.subpixel,
                 }),
                 font: if let Some(ref font) = text.font {
                     StyleProp::Value(font.clone())
@@ -108,13 +107,9 @@ pub fn text_render(
                 } else {
                     StyleProp::default()
                 },
-                // bottom: Units::Stretch(1.0).into(),
-                // top: Units::Stretch(1.0).into(),
-                // left: Units::Stretch(0.0).into(),
-                // right: Units::Stretch(0.0).into(),
                 ..Default::default()
-            });
-
+            })
+            .into();
         // style.cursor = StyleProp::Value(KCursorIcon(CursorIcon::Hand));
     }
 
diff --git a/src/widgets/text_box.rs b/src/widgets/text_box.rs
index 2ba680c34861145cffc16d7a237edff0ba6826fd..031a204f5092fcb7a2c0c2441166dd13c2b0e331 100644
--- a/src/widgets/text_box.rs
+++ b/src/widgets/text_box.rs
@@ -12,7 +12,7 @@ use crate::{
     on_layout::OnLayout,
     prelude::{KChildren, KayakWidgetContext, OnChange},
     render::font::FontMapping,
-    styles::{Edge, KPositionType, KStyle, RenderCommand, StyleProp, Units},
+    styles::{ComputedStyles, Edge, KPositionType, KStyle, RenderCommand, StyleProp, Units},
     widget::Widget,
     widget_state::WidgetState,
     widgets::{
@@ -75,6 +75,7 @@ impl Widget for TextBoxProps {}
 pub struct TextBoxBundle {
     pub text_box: TextBoxProps,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub on_event: OnEvent,
     pub on_layout: OnLayout,
     pub on_change: OnChange,
@@ -87,6 +88,7 @@ impl Default for TextBoxBundle {
         Self {
             text_box: Default::default(),
             styles: Default::default(),
+            computed_styles: ComputedStyles::default(),
             on_event: Default::default(),
             on_layout: Default::default(),
             on_change: Default::default(),
@@ -99,12 +101,20 @@ impl Default for TextBoxBundle {
 pub fn text_box_render(
     In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
     mut commands: Commands,
-    mut query: Query<(&mut KStyle, &TextBoxProps, &mut OnEvent, &OnChange)>,
+    mut query: Query<(
+        &KStyle,
+        &mut ComputedStyles,
+        &TextBoxProps,
+        &mut OnEvent,
+        &OnChange,
+    )>,
     mut state_query: ParamSet<(Query<&TextBoxState>, Query<&mut TextBoxState>)>,
     font_assets: Res<Assets<KayakFont>>,
     font_mapping: Res<FontMapping>,
 ) -> bool {
-    if let Ok((mut styles, text_box, mut on_event, on_change)) = query.get_mut(entity) {
+    if let Ok((styles, mut computed_styles, text_box, mut on_event, on_change)) =
+        query.get_mut(entity)
+    {
         let state_entity = widget_context.use_state::<TextBoxState>(
             &mut commands,
             entity,
@@ -136,14 +146,14 @@ pub fn text_box_render(
         }
 
         if let Ok(state) = state_query.p0().get(state_entity) {
-            *styles = KStyle::default()
+            *computed_styles = KStyle::default()
                 // Required styles
                 .with_style(KStyle {
                     render_command: RenderCommand::Layout.into(),
                     ..Default::default()
                 })
                 // Apply any prop-given styles
-                .with_style(&*styles)
+                .with_style(styles)
                 // If not set by props, apply these styles
                 .with_style(KStyle {
                     top: Units::Pixels(0.0).into(),
@@ -151,7 +161,8 @@ pub fn text_box_render(
                     height: Units::Pixels(26.0).into(),
                     // cursor: CursorIcon::Text.into(),
                     ..Default::default()
-                });
+                })
+                .into();
 
             let background_styles = KStyle {
                 render_command: StyleProp::Value(RenderCommand::Quad),
@@ -341,11 +352,11 @@ pub fn text_box_render(
                     }}>
                         <ElementBundle styles={scroll_styles}>
                             <TextWidgetBundle
+                                styles={text_styles}
                                 text={TextProps {
                                     content: text_box.value.clone(),
                                     size: 14.0,
                                     line_height: Some(18.0),
-                                    user_styles: text_styles,
                                     word_wrap: false,
                                     ..Default::default()
                                 }}
diff --git a/src/widgets/texture_atlas.rs b/src/widgets/texture_atlas.rs
index eab78bf4d6cbc1a91b733096dd627f3fc4119d48..d793326349f913f4c516a703ecda1201fa2d4512 100644
--- a/src/widgets/texture_atlas.rs
+++ b/src/widgets/texture_atlas.rs
@@ -3,7 +3,7 @@ use bevy::prelude::{Bundle, Component, Entity, Handle, Image, In, Query, Vec2};
 use crate::{
     context::WidgetName,
     prelude::KayakWidgetContext,
-    styles::{KStyle, RenderCommand, StyleProp},
+    styles::{ComputedStyles, KStyle, RenderCommand},
     widget::Widget,
 };
 
@@ -39,6 +39,7 @@ impl Widget for TextureAtlasProps {}
 pub struct TextureAtlasBundle {
     pub atlas: TextureAtlasProps,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub widget_name: WidgetName,
 }
 
@@ -47,6 +48,7 @@ impl Default for TextureAtlasBundle {
         Self {
             atlas: Default::default(),
             styles: Default::default(),
+            computed_styles: ComputedStyles::default(),
             widget_name: TextureAtlasProps::default().get_name(),
         }
     }
@@ -54,17 +56,20 @@ impl Default for TextureAtlasBundle {
 
 pub fn texture_atlas_render(
     In((_widget_context, entity)): In<(KayakWidgetContext, Entity)>,
-    mut query: Query<(&mut KStyle, &TextureAtlasProps)>,
+    mut query: Query<(&KStyle, &mut ComputedStyles, &TextureAtlasProps)>,
 ) -> bool {
-    if let Ok((mut styles, texture_atlas)) = query.get_mut(entity) {
-        *styles = KStyle {
-            render_command: StyleProp::Value(RenderCommand::TextureAtlas {
+    if let Ok((styles, mut computed_styles, texture_atlas)) = query.get_mut(entity) {
+        *computed_styles = KStyle {
+            render_command: RenderCommand::TextureAtlas {
                 position: texture_atlas.position,
                 size: texture_atlas.tile_size,
                 handle: texture_atlas.handle.clone_weak(),
-            }),
-            ..styles.clone()
-        };
+            }
+            .into(),
+            ..Default::default()
+        }
+        .with_style(styles)
+        .into();
     }
 
     true
diff --git a/src/widgets/window.rs b/src/widgets/window.rs
index 7a2d88b5b342557fb12f2a8357266b2952883aab..3358245db619f33a77922c11149c0b16a761ee0d 100644
--- a/src/widgets/window.rs
+++ b/src/widgets/window.rs
@@ -11,7 +11,10 @@ use crate::{
     event_dispatcher::EventDispatcherContext,
     on_event::OnEvent,
     prelude::KayakWidgetContext,
-    styles::{Corner, Edge, KCursorIcon, KPositionType, KStyle, RenderCommand, StyleProp, Units},
+    styles::{
+        ComputedStyles, Corner, Edge, KCursorIcon, KPositionType, KStyle, RenderCommand, StyleProp,
+        Units,
+    },
     widget::Widget,
     widget_state::WidgetState,
     Focusable,
@@ -58,6 +61,7 @@ impl Widget for KWindow {}
 pub struct WindowBundle {
     pub window: KWindow,
     pub styles: KStyle,
+    pub computed_styles: ComputedStyles,
     pub children: KChildren,
     pub widget_name: WidgetName,
 }
@@ -67,6 +71,7 @@ impl Default for WindowBundle {
         Self {
             window: Default::default(),
             styles: Default::default(),
+            computed_styles: ComputedStyles::default(),
             children: Default::default(),
             widget_name: KWindow::default().get_name(),
         }
@@ -76,11 +81,13 @@ impl Default for WindowBundle {
 pub fn window_render(
     In((widget_context, window_entity)): In<(KayakWidgetContext, Entity)>,
     mut commands: Commands,
-    mut query: Query<(&mut KStyle, &KChildren, &KWindow)>,
+    mut query: Query<(&KStyle, &mut ComputedStyles, &KChildren, &KWindow)>,
     state_query: Query<&KWindowState>,
     mut context_query: Query<&mut WindowContext>,
 ) -> bool {
-    if let Ok((mut window_style, window_children, window)) = query.get_mut(window_entity) {
+    if let Ok((window_style, mut computed_styles, window_children, window)) =
+        query.get_mut(window_entity)
+    {
         let possible_context_entity =
             widget_context.get_context_entity::<WindowContext>(window_entity);
         let z_index = if let Some(window_context_entity) = possible_context_entity {
@@ -93,11 +100,17 @@ pub fn window_render(
             None
         };
 
-        window_style.z_index = if let Some(z_index) = z_index {
-            StyleProp::Value(z_index as i32 * 1000)
-        } else {
-            StyleProp::Default
-        };
+        *computed_styles = KStyle::default()
+            .with_style(KStyle {
+                z_index: if let Some(z_index) = z_index {
+                    StyleProp::Value(z_index as i32 * 1000)
+                } else {
+                    StyleProp::Default
+                },
+                ..Default::default()
+            })
+            .with_style(window_style)
+            .into();
 
         let title = window.title.clone();
 
@@ -190,14 +203,14 @@ pub fn window_render(
                         }}
                     >
                         <TextWidgetBundle
+                            styles={KStyle {
+                                top: Units::Stretch(1.0).into(),
+                                bottom: Units::Stretch(1.0).into(),
+                                ..Default::default()
+                            }}
                             text={TextProps {
                                 content: title,
                                 size: 14.0,
-                                user_styles: KStyle {
-                                    top: Units::Stretch(1.0).into(),
-                                    bottom: Units::Stretch(1.0).into(),
-                                    ..Default::default()
-                                },
                                 ..Default::default()
                             }}
                         />