diff --git a/.gitignore b/.gitignore
index 088ba6ba7d345b76aa2b8dc021dd25e1323189b3..afb05e53627ef9e7c2178feed46908502555e61b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,13 @@
-# Generated by Cargo
-# will have compiled files and executables
-/target/
-
-# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
-# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
-Cargo.lock
+target
 
 # These are backup files generated by rustfmt
 **/*.rs.bk
+
+# MSVC Windows builds of rustc generate these, which store debugging information
+*.pdb
+
+.vscode
+
+*.log
+
+msdfgen
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000000000000000000000000000000000000..4b9e0ca7c5ebf99adaca5c433e566899d60944a0
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,3641 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "Inflector"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
+
+[[package]]
+name = "ab_glyph"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20b228f2c198f98d4337ceb560333fb12cbb2f4948a953bf8c57d09deb219603"
+dependencies = [
+ "ab_glyph_rasterizer",
+ "owned_ttf_parser",
+]
+
+[[package]]
+name = "ab_glyph_rasterizer"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e"
+
+[[package]]
+name = "adler32"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
+
+[[package]]
+name = "ahash"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alsa"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18"
+dependencies = [
+ "alsa-sys",
+ "bitflags",
+ "libc",
+ "nix",
+]
+
+[[package]]
+name = "alsa-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "android_log-sys"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8052e2d8aabbb8d556d6abbcce2a22b9590996c5f849b9c7ce4544a2e3b984e"
+
+[[package]]
+name = "android_log-sys"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e"
+
+[[package]]
+name = "android_logger"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cbd542dd180566fad88fd2729a53a62a734843c626638006a9d63ec0688484e"
+dependencies = [
+ "android_log-sys 0.1.2",
+ "env_logger",
+ "lazy_static",
+ "log",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d9ff5d688f1c13395289f67db01d4826b46dd694e7580accdc3e8430f2d98e"
+
+[[package]]
+name = "approx"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "072df7202e63b127ab55acfe16ce97013d5b97bf160489336d3f1840fd78e99e"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9"
+dependencies = [
+ "nodrop",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+
+[[package]]
+name = "as-any"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b564204d863976c470dbd0e41d1fcc351a8cd1b58222aaaae466f82c493fae4"
+
+[[package]]
+name = "ash"
+version = "0.33.3+1.2.191"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4f1d82f164f838ae413296d1131aa6fa79b917d25bebaa7033d25620c09219"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "once_cell",
+ "slab",
+]
+
+[[package]]
+name = "async-task"
+version = "4.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0"
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base-x"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bevy"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_internal",
+]
+
+[[package]]
+name = "bevy-glsl-to-spirv"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d5f2f58f0aec3c50a20799792c3705e80dd7df327e79791cacec197e84e5e61"
+
+[[package]]
+name = "bevy_app"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_derive",
+ "bevy_ecs",
+ "bevy_reflect",
+ "bevy_utils",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "bevy_asset"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "anyhow",
+ "bevy_app",
+ "bevy_diagnostic",
+ "bevy_ecs",
+ "bevy_log",
+ "bevy_reflect",
+ "bevy_tasks",
+ "bevy_utils",
+ "crossbeam-channel",
+ "downcast-rs",
+ "js-sys",
+ "ndk-glue 0.2.1",
+ "notify",
+ "parking_lot",
+ "rand",
+ "serde",
+ "thiserror",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "bevy_audio"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "anyhow",
+ "bevy_app",
+ "bevy_asset",
+ "bevy_ecs",
+ "bevy_reflect",
+ "bevy_utils",
+ "parking_lot",
+ "rodio",
+]
+
+[[package]]
+name = "bevy_core"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_derive",
+ "bevy_ecs",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_tasks",
+ "bevy_utils",
+ "bytemuck",
+]
+
+[[package]]
+name = "bevy_core_pipeline"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_ecs",
+ "bevy_render2",
+]
+
+[[package]]
+name = "bevy_derive"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "Inflector",
+ "bevy_macro_utils",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "bevy_diagnostic"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_core",
+ "bevy_ecs",
+ "bevy_log",
+ "bevy_utils",
+]
+
+[[package]]
+name = "bevy_dynamic_plugin"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "libloading",
+]
+
+[[package]]
+name = "bevy_ecs"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "async-channel",
+ "bevy_ecs_macros",
+ "bevy_reflect",
+ "bevy_tasks",
+ "bevy_utils",
+ "downcast-rs",
+ "fixedbitset",
+ "fxhash",
+ "rand",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "bevy_ecs_macros"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_macro_utils",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "bevy_gilrs"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_ecs",
+ "bevy_input",
+ "bevy_utils",
+ "gilrs",
+]
+
+[[package]]
+name = "bevy_gltf2"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "anyhow",
+ "base64",
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_ecs",
+ "bevy_log",
+ "bevy_math",
+ "bevy_pbr2",
+ "bevy_reflect",
+ "bevy_render2",
+ "bevy_scene",
+ "bevy_transform",
+ "gltf",
+ "percent-encoding",
+ "thiserror",
+ "wgpu",
+]
+
+[[package]]
+name = "bevy_input"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_ecs",
+ "bevy_math",
+ "bevy_utils",
+]
+
+[[package]]
+name = "bevy_internal"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_asset",
+ "bevy_audio",
+ "bevy_core",
+ "bevy_core_pipeline",
+ "bevy_derive",
+ "bevy_diagnostic",
+ "bevy_dynamic_plugin",
+ "bevy_ecs",
+ "bevy_gilrs",
+ "bevy_gltf2",
+ "bevy_input",
+ "bevy_log",
+ "bevy_math",
+ "bevy_pbr",
+ "bevy_pbr2",
+ "bevy_reflect",
+ "bevy_render",
+ "bevy_render2",
+ "bevy_scene",
+ "bevy_sprite",
+ "bevy_sprite2",
+ "bevy_tasks",
+ "bevy_text",
+ "bevy_transform",
+ "bevy_ui",
+ "bevy_utils",
+ "bevy_wgpu",
+ "bevy_window",
+ "bevy_winit",
+ "ndk-glue 0.2.1",
+]
+
+[[package]]
+name = "bevy_kayak_ui"
+version = "0.0.1"
+dependencies = [
+ "bevy",
+ "bytemuck",
+ "crevice",
+ "kayak_components",
+ "kayak_core",
+ "kayak_font",
+ "kayak_render_macros",
+]
+
+[[package]]
+name = "bevy_log"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "android_log-sys 0.2.0",
+ "bevy_app",
+ "bevy_utils",
+ "console_error_panic_hook",
+ "tracing-log",
+ "tracing-subscriber",
+ "tracing-wasm",
+]
+
+[[package]]
+name = "bevy_macro_utils"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "cargo-manifest",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "bevy_math"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_reflect",
+ "glam",
+]
+
+[[package]]
+name = "bevy_pbr"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_ecs",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_render",
+ "bevy_transform",
+ "bytemuck",
+]
+
+[[package]]
+name = "bevy_pbr2"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_core_pipeline",
+ "bevy_ecs",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_render2",
+ "bevy_transform",
+ "bevy_utils",
+ "bitflags",
+ "bytemuck",
+ "crevice",
+ "wgpu",
+]
+
+[[package]]
+name = "bevy_reflect"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_reflect_derive",
+ "bevy_utils",
+ "downcast-rs",
+ "erased-serde",
+ "glam",
+ "parking_lot",
+ "serde",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "bevy_reflect_derive"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_macro_utils",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+ "uuid",
+]
+
+[[package]]
+name = "bevy_render"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "anyhow",
+ "bevy-glsl-to-spirv",
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_derive",
+ "bevy_ecs",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_transform",
+ "bevy_utils",
+ "bevy_window",
+ "bitflags",
+ "downcast-rs",
+ "hex",
+ "hexasphere",
+ "image",
+ "once_cell",
+ "parking_lot",
+ "serde",
+ "shaderc",
+ "spirv-reflect",
+ "thiserror",
+]
+
+[[package]]
+name = "bevy_render2"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "anyhow",
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_derive",
+ "bevy_ecs",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_transform",
+ "bevy_utils",
+ "bevy_window",
+ "bitflags",
+ "crevice",
+ "downcast-rs",
+ "futures-lite",
+ "hex",
+ "hexasphere",
+ "image",
+ "naga",
+ "once_cell",
+ "parking_lot",
+ "regex",
+ "serde",
+ "smallvec",
+ "thiserror",
+ "wgpu",
+]
+
+[[package]]
+name = "bevy_scene"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "anyhow",
+ "bevy_app",
+ "bevy_asset",
+ "bevy_ecs",
+ "bevy_reflect",
+ "bevy_transform",
+ "bevy_utils",
+ "ron",
+ "serde",
+ "thiserror",
+ "uuid",
+]
+
+[[package]]
+name = "bevy_sprite"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_ecs",
+ "bevy_log",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_render",
+ "bevy_transform",
+ "bevy_utils",
+ "bevy_window",
+ "bytemuck",
+ "guillotiere",
+ "rectangle-pack",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "bevy_sprite2"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_core_pipeline",
+ "bevy_ecs",
+ "bevy_log",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_render2",
+ "bevy_transform",
+ "bevy_utils",
+ "bytemuck",
+ "guillotiere",
+ "rectangle-pack",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "bevy_tasks"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "event-listener",
+ "futures-lite",
+ "num_cpus",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "bevy_text"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "ab_glyph",
+ "anyhow",
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_ecs",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_render",
+ "bevy_sprite",
+ "bevy_transform",
+ "bevy_utils",
+ "bevy_window",
+ "glyph_brush_layout",
+ "thiserror",
+]
+
+[[package]]
+name = "bevy_transform"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_ecs",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_utils",
+ "smallvec",
+]
+
+[[package]]
+name = "bevy_ui"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_derive",
+ "bevy_ecs",
+ "bevy_input",
+ "bevy_log",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_render",
+ "bevy_sprite",
+ "bevy_text",
+ "bevy_transform",
+ "bevy_utils",
+ "bevy_window",
+ "serde",
+ "smallvec",
+ "stretch",
+]
+
+[[package]]
+name = "bevy_utils"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "ahash",
+ "bevy_derive",
+ "getrandom",
+ "instant",
+ "tracing",
+ "uuid",
+]
+
+[[package]]
+name = "bevy_wgpu"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_diagnostic",
+ "bevy_ecs",
+ "bevy_render",
+ "bevy_utils",
+ "bevy_window",
+ "bevy_winit",
+ "crossbeam-channel",
+ "crossbeam-utils",
+ "futures-lite",
+ "parking_lot",
+ "wgpu",
+]
+
+[[package]]
+name = "bevy_window"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_ecs",
+ "bevy_math",
+ "bevy_utils",
+ "raw-window-handle",
+ "web-sys",
+]
+
+[[package]]
+name = "bevy_winit"
+version = "0.5.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bevy_app",
+ "bevy_ecs",
+ "bevy_input",
+ "bevy_math",
+ "bevy_utils",
+ "bevy_window",
+ "raw-window-handle",
+ "wasm-bindgen",
+ "web-sys",
+ "winit",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.56.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "peeking_take_while",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "regex",
+ "rustc-hash",
+ "shlex",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "bumpalo"
+version = "3.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
+
+[[package]]
+name = "bytemuck"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b"
+dependencies = [
+ "bytemuck_derive",
+]
+
+[[package]]
+name = "bytemuck_derive"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e215f8c2f9f79cb53c8335e687ffd07d5bfcb6fe5fc80723762d0be46e7cc54"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "cache-padded"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba"
+
+[[package]]
+name = "cargo-manifest"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af6d65c7592744998c67947ec771c62687c76f00179a83ffd563c0482046bb98"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "toml",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
+dependencies = [
+ "jobserver",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cexpr"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "winapi",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7b858541263efe664aead4a5209a4ae5c5d2811167d4ed4ee0944503f8d2089"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cocoa"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832"
+dependencies = [
+ "bitflags",
+ "block",
+ "cocoa-foundation",
+ "core-foundation 0.9.2",
+ "core-graphics 0.22.3",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "cocoa-foundation"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318"
+dependencies = [
+ "bitflags",
+ "block",
+ "core-foundation 0.9.2",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "combine"
+version = "4.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b2f5d0ee456f3928812dfc8c6d9a1d592b98678f6d56db9b0cd2b7bc6c8db5"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3"
+dependencies = [
+ "cache-padded",
+]
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "copyless"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536"
+
+[[package]]
+name = "core-foundation"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d"
+dependencies = [
+ "core-foundation-sys 0.6.2",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
+dependencies = [
+ "core-foundation-sys 0.7.0",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
+dependencies = [
+ "core-foundation-sys 0.8.3",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "core-graphics"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.7.0",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.9.2",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.9.2",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-video-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828"
+dependencies = [
+ "cfg-if 0.1.10",
+ "core-foundation-sys 0.7.0",
+ "core-graphics 0.19.2",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "coreaudio-rs"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88"
+dependencies = [
+ "bitflags",
+ "coreaudio-sys",
+]
+
+[[package]]
+name = "coreaudio-sys"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa"
+dependencies = [
+ "bindgen",
+]
+
+[[package]]
+name = "cpal"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418"
+dependencies = [
+ "alsa",
+ "core-foundation-sys 0.8.3",
+ "coreaudio-rs",
+ "jni",
+ "js-sys",
+ "lazy_static",
+ "libc",
+ "mach 0.3.2",
+ "ndk 0.3.0",
+ "ndk-glue 0.3.0",
+ "nix",
+ "oboe",
+ "parking_lot",
+ "stdweb 0.1.3",
+ "thiserror",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "crevice"
+version = "0.8.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "bytemuck",
+ "crevice-derive",
+ "glam",
+ "mint",
+]
+
+[[package]]
+name = "crevice-derive"
+version = "0.8.0"
+source = "git+https://github.com/StarArawn/bevy?rev=b26f563b13c267ffe1ee801bd71fd40b98a256e7#b26f563b13c267ffe1ee801bd71fd40b98a256e7"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "crossbeam"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-epoch",
+ "crossbeam-queue",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+ "lazy_static",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b10ddc024425c88c2ad148c1b0fd53f4c6d38db9697c9f1588381212fa657c9"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
+dependencies = [
+ "cfg-if 1.0.0",
+ "lazy_static",
+]
+
+[[package]]
+name = "d3d12"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2daefd788d1e96e0a9d66dee4b828b883509bc3ea9ce30665f04c3246372690c"
+dependencies = [
+ "bitflags",
+ "libloading",
+ "winapi",
+]
+
+[[package]]
+name = "darling"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "strsim",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
+dependencies = [
+ "darling_core",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "deflate"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
+dependencies = [
+ "adler32",
+ "byteorder",
+]
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "diff-struct"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30a0f0aafc8da0d598388b1897310646f45f47f17e31d581b4390f9dd88e826"
+dependencies = [
+ "diff_derive",
+ "serde",
+]
+
+[[package]]
+name = "diff_derive"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f9c37d395456d9c1785499421c6288931b6e3dbc2500acb95b8adaa0e69db3"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "difference"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3304d19798a8e067e48d8e69b2c37f0b5e9b4e462504ad9e27e9f3fce02bba8"
+
+[[package]]
+name = "discard"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
+
+[[package]]
+name = "env_logger"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "erased-serde"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3de9ad4541d99dc22b59134e7ff8dc3d6c988c89ecd7324bf10a8362b07a2afa"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "euclid"
+version = "0.19.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "596b99621b9477e7a5f94d2d8dd13a9c5c302ac358b822c67a42b6f1054450e1"
+dependencies = [
+ "euclid_macros",
+ "num-traits",
+]
+
+[[package]]
+name = "euclid"
+version = "0.22.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da96828553a086d7b18dcebfc579bd9628b016f86590d7453c115e490fa74b80"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "euclid_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdcb84c18ea5037a1c5a23039b4ff29403abce2e0d6b1daa11cf0bde2b30be15"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59"
+
+[[package]]
+name = "fastrand"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "redox_syscall",
+ "winapi",
+]
+
+[[package]]
+name = "fixedbitset"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "398ea4fabe40b9b0d885340a2a991a44c8a645624075ad966d21f88688e2b69e"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "fontdue"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8669141fd879e023ac6fcfe1d90dfe3f83ca7e322781ddf3f5be59147e4b8113"
+dependencies = [
+ "hashbrown",
+ "ttf-parser 0.12.3",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "fsevent-sys"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c0e564d24da983c053beff1bb7178e237501206840a3e6bf4e267b9e8ae734a"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
+
+[[package]]
+name = "futures-lite"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gilrs"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e986f911d937f4395dfc2a39618dcef452773d32dcdbe0828c623f76588f749"
+dependencies = [
+ "fnv",
+ "gilrs-core",
+ "log",
+ "uuid",
+ "vec_map",
+]
+
+[[package]]
+name = "gilrs-core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a5e5bb97bf9a0d9519a28cf38839cf1d6d9bb572b48e3c67202271fec2ed5e7"
+dependencies = [
+ "core-foundation 0.6.4",
+ "io-kit-sys",
+ "libc",
+ "libudev-sys",
+ "log",
+ "nix",
+ "rusty-xinput",
+ "stdweb 0.4.20",
+ "uuid",
+ "vec_map",
+ "winapi",
+]
+
+[[package]]
+name = "glam"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2c626156a479bfb5a4362c43f5abadd700af33ebc8db068089d9cb92493d6ab"
+dependencies = [
+ "bytemuck",
+ "mint",
+ "serde",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
+[[package]]
+name = "glow"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f04649123493bc2483cbef4daddb45d40bbdae5adb221a63a23efdb0cc99520"
+dependencies = [
+ "js-sys",
+ "slotmap",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gltf"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ff38b75359a0096dd0a8599b6e4f37a6ee41d5df300cc7669e62aafa697f7a2"
+dependencies = [
+ "byteorder",
+ "gltf-json",
+ "lazy_static",
+]
+
+[[package]]
+name = "gltf-derive"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f2a9333e0f9c7bca94dfc20bcf44fa12a61eeec662d6e007563ff748aa59c70"
+dependencies = [
+ "inflections",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "gltf-json"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1414d3a98cbaabdb2f134328b1f6036d14b282febc1df51952a435d2ca17fb6"
+dependencies = [
+ "gltf-derive",
+ "serde",
+ "serde_derive",
+ "serde_json",
+]
+
+[[package]]
+name = "glyph_brush_layout"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc32c2334f00ca5ac3695c5009ae35da21da8c62d255b5b96d56e2597a637a38"
+dependencies = [
+ "ab_glyph",
+ "approx",
+ "xi-unicode",
+]
+
+[[package]]
+name = "gpu-alloc"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e64cbb8d36508d3e19da95e56e196a84f674fc190881f2cc010000798838aa6"
+dependencies = [
+ "bitflags",
+ "gpu-alloc-types",
+]
+
+[[package]]
+name = "gpu-alloc-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54804d0d6bc9d7f26db4eaec1ad10def69b599315f487d32c334a80d1efe67a5"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "gpu-descriptor"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a538f217be4d405ff4719a283ca68323cc2384003eca5baaa87501e821c81dda"
+dependencies = [
+ "bitflags",
+ "gpu-descriptor-types",
+ "hashbrown",
+]
+
+[[package]]
+name = "gpu-descriptor-types"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "363e3677e55ad168fef68cf9de3a4a310b53124c5e784c53a1d70e92d23f2126"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "guillotiere"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782"
+dependencies = [
+ "euclid 0.22.6",
+ "svg_fmt",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hexasphere"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dc62dcfd68ec810c4707804556f2e88655012b1a373b0e0bbbe88a9db366627"
+dependencies = [
+ "glam",
+ "lazy_static",
+]
+
+[[package]]
+name = "hexf-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "image"
+version = "0.23.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+ "png",
+ "scoped_threadpool",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "inflections"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
+
+[[package]]
+name = "inotify"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
+dependencies = [
+ "bitflags",
+ "inotify-sys",
+ "libc",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "inplace_it"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90953f308a79fe6d62a4643e51f848fbfddcd05975a38e69fdf4ab86a7baf7ca"
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "io-kit-sys"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f21dcc74995dd4cd090b147e79789f8d65959cbfb5f0b118002db869ea3bd0a0"
+dependencies = [
+ "core-foundation-sys 0.6.2",
+ "mach 0.2.3",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "jni"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "jobserver"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kayak_components"
+version = "0.1.0"
+dependencies = [
+ "kayak_core",
+]
+
+[[package]]
+name = "kayak_core"
+version = "0.1.0"
+dependencies = [
+ "as-any",
+ "derivative",
+ "diff-struct",
+ "fontdue",
+ "kayak_render_macros",
+ "morphorm",
+ "resources",
+]
+
+[[package]]
+name = "kayak_font"
+version = "0.1.0"
+dependencies = [
+ "arrayvec 0.7.2",
+ "bitflags",
+ "fontdue",
+ "lyon_geom",
+ "lyon_path",
+ "png",
+ "ttf-parser 0.13.3",
+]
+
+[[package]]
+name = "kayak_render_macros"
+version = "0.1.0"
+dependencies = [
+ "kayak_core",
+ "pretty_assertions",
+ "proc-macro-error",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "kayak_ui"
+version = "0.1.0"
+dependencies = [
+ "bevy",
+ "bevy_kayak_ui",
+ "kayak_components",
+ "kayak_core",
+]
+
+[[package]]
+name = "khronos-egl"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c2352bd1d0bceb871cb9d40f24360c8133c11d7486b68b5381c1dd1a32015e3"
+dependencies = [
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
+
+[[package]]
+name = "libloading"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52"
+dependencies = [
+ "cfg-if 1.0.0",
+ "winapi",
+]
+
+[[package]]
+name = "libm"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a"
+
+[[package]]
+name = "libudev-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "lyon_geom"
+version = "0.12.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdb9bf1f1d43be9a9cc2343a7a096dc113cc25337a13e8f99721b01d1d548b60"
+dependencies = [
+ "arrayvec 0.4.12",
+ "euclid 0.19.9",
+ "num-traits",
+]
+
+[[package]]
+name = "lyon_path"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9dc8e0746b7cca11960b602f7fe037bb067746a01eab4aa502fed1494544843"
+dependencies = [
+ "lyon_geom",
+]
+
+[[package]]
+name = "mach"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86dd2487cdfea56def77b88438a2c915fb45113c5319bfe7e14306ca4cd0b0e1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "mach"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "matchers"
+version = "0.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "memoffset"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "metal"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0514f491f4cc03632ab399ee01e2c1c1b12d3e1cf2d667c1ff5f87d6dcd2084"
+dependencies = [
+ "bitflags",
+ "block",
+ "core-graphics-types",
+ "foreign-types",
+ "log",
+ "objc",
+]
+
+[[package]]
+name = "minimp3"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372"
+dependencies = [
+ "minimp3-sys",
+ "slice-deque",
+ "thiserror",
+]
+
+[[package]]
+name = "minimp3-sys"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
+dependencies = [
+ "adler32",
+]
+
+[[package]]
+name = "mint"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162e591484b4b8fe9e1ca16ebf07ab584fdc3334508d76a788cd54d89cfc20dc"
+
+[[package]]
+name = "mio"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "winapi",
+]
+
+[[package]]
+name = "mio-misc"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ddf05411bb159cdb5801bb10002afb66cb4572be656044315e363460ce69dc2"
+dependencies = [
+ "crossbeam",
+ "crossbeam-queue",
+ "log",
+ "mio",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "morphorm"
+version = "0.2.0"
+source = "git+https://github.com/geom3trik/morphorm#df4129d0e4fa390e92ede1a303aeeae71be5d3f4"
+dependencies = [
+ "bitflags",
+ "smallvec",
+]
+
+[[package]]
+name = "naga"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eda66d09f712e1f0a6ab436137da4fac312f78301f6d4ac7cb8bfe96e988734f"
+dependencies = [
+ "bit-set",
+ "bitflags",
+ "codespan-reporting",
+ "fxhash",
+ "hexf-parse",
+ "indexmap",
+ "log",
+ "num-traits",
+ "petgraph",
+ "pp-rs",
+ "spirv",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73"
+dependencies = [
+ "jni-sys",
+ "ndk-sys",
+ "num_enum 0.4.3",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab"
+dependencies = [
+ "jni-sys",
+ "ndk-sys",
+ "num_enum 0.5.4",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c"
+dependencies = [
+ "bitflags",
+ "jni-sys",
+ "ndk-sys",
+ "num_enum 0.5.4",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk-glue"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241"
+dependencies = [
+ "android_logger",
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk 0.2.1",
+ "ndk-macro",
+ "ndk-sys",
+]
+
+[[package]]
+name = "ndk-glue"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk 0.3.0",
+ "ndk-macro",
+ "ndk-sys",
+]
+
+[[package]]
+name = "ndk-glue"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk 0.4.0",
+ "ndk-macro",
+ "ndk-sys",
+]
+
+[[package]]
+name = "ndk-macro"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d"
+dependencies = [
+ "darling",
+ "proc-macro-crate 0.1.5",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "ndk-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121"
+
+[[package]]
+name = "nix"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a"
+dependencies = [
+ "bitflags",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+]
+
+[[package]]
+name = "nodrop"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
+
+[[package]]
+name = "nom"
+version = "5.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
+dependencies = [
+ "memchr",
+ "version_check",
+]
+
+[[package]]
+name = "notify"
+version = "5.0.0-pre.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51f18203a26893ca1d3526cf58084025d5639f91c44f8b70ab3b724f60e819a0"
+dependencies = [
+ "bitflags",
+ "crossbeam-channel",
+ "filetime",
+ "fsevent-sys",
+ "inotify",
+ "libc",
+ "mio",
+ "walkdir",
+ "winapi",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num-derive"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4"
+dependencies = [
+ "derivative",
+ "num_enum_derive 0.4.3",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f"
+dependencies = [
+ "derivative",
+ "num_enum_derive 0.5.4",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d"
+dependencies = [
+ "proc-macro-crate 0.1.5",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9"
+dependencies = [
+ "proc-macro-crate 1.1.0",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+ "objc_exception",
+]
+
+[[package]]
+name = "objc_exception"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "oboe"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1"
+dependencies = [
+ "jni",
+ "ndk 0.4.0",
+ "ndk-glue 0.4.0",
+ "num-derive",
+ "num-traits",
+ "oboe-sys",
+]
+
+[[package]]
+name = "oboe-sys"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "338142ae5ab0aaedc8275aa8f67f460e43ae0fca76a695a742d56da0a269eadc"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+
+[[package]]
+name = "owned_ttf_parser"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65ee3f72636e6f164cc41c9f9057f4e58c4e13507699ea7f5e5242b64b8198ee"
+dependencies = [
+ "ttf-parser 0.13.3",
+]
+
+[[package]]
+name = "parking"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+dependencies = [
+ "cfg-if 1.0.0",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "petgraph"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f"
+dependencies = [
+ "fixedbitset",
+ "indexmap",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f"
+
+[[package]]
+name = "png"
+version = "0.16.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "deflate",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "pp-rs"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb458bb7f6e250e6eb79d5026badc10a3ebb8f9a15d1fff0f13d17c71f4d6dee"
+dependencies = [
+ "unicode-xid 0.2.2",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
+
+[[package]]
+name = "pretty_assertions"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5dc44a7f8ba9e19aeb6f900eb518343170f2aae27d3c9c78e4d5c3db623638d"
+dependencies = [
+ "ansi_term 0.9.0",
+ "difference",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
+dependencies = [
+ "toml",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83"
+dependencies = [
+ "thiserror",
+ "toml",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
+dependencies = [
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
+dependencies = [
+ "unicode-xid 0.2.2",
+]
+
+[[package]]
+name = "profiling"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9926767b8b8244d7b6b64546585121d193c3d0b4856ccd656b7bfa9deb91ab6a"
+
+[[package]]
+name = "quote"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
+dependencies = [
+ "proc-macro2 0.4.30",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
+dependencies = [
+ "proc-macro2 1.0.32",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "range-alloc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e935c45e09cc6dcf00d2f0b2d630a58f4095320223d47fc68918722f0538b6"
+
+[[package]]
+name = "raw-window-handle"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a441a7a6c80ad6473bd4b74ec1c9a4c951794285bf941c2126f607c72e48211"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "rectangle-pack"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb"
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "renderdoc-sys"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157"
+
+[[package]]
+name = "resources"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42070ea13709eb92d2977b48c7d3bd44866fa328e14248f8d1f00d6ea14d5066"
+dependencies = [
+ "downcast-rs",
+ "fxhash",
+ "parking_lot",
+]
+
+[[package]]
+name = "rodio"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d98f5e557b61525057e2bc142c8cd7f0e70d75dc32852309bec440e6e046bf9"
+dependencies = [
+ "cpal",
+ "minimp3",
+]
+
+[[package]]
+name = "ron"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86018df177b1beef6c7c8ef949969c4f7cb9a9344181b92486b23c79995bdaa4"
+dependencies = [
+ "base64",
+ "bitflags",
+ "serde",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rusty-xinput"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2aa654bc32eb9ca14cce1a084abc9dfe43949a4547c35269a094c39272db3bb"
+dependencies = [
+ "lazy_static",
+ "log",
+ "winapi",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scoped_threadpool"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
+
+[[package]]
+name = "shaderc"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58da8aaf4ad3508598cdf098567114c98d5f455de7d69b1213232ac557bc67ea"
+dependencies = [
+ "libc",
+ "shaderc-sys",
+]
+
+[[package]]
+name = "shaderc-sys"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bd76ec0bd25f2017a65250373485e43cdc81b5cb8fd83c6115375c8d018cdf9"
+dependencies = [
+ "cmake",
+ "libc",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2"
+
+[[package]]
+name = "slab"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
+
+[[package]]
+name = "slice-deque"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25"
+dependencies = [
+ "libc",
+ "mach 0.3.2",
+ "winapi",
+]
+
+[[package]]
+name = "slotmap"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "spirv"
+version = "0.2.0+1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830"
+dependencies = [
+ "bitflags",
+ "num-traits",
+]
+
+[[package]]
+name = "spirv-reflect"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecc7af6a7d3ca6d15f4d6b5077df89c77ad1f4b314d0cabee221656d041dad7"
+dependencies = [
+ "bitflags",
+ "cc",
+ "num-traits",
+ "serde",
+ "serde_derive",
+ "spirv_headers",
+]
+
+[[package]]
+name = "spirv_headers"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f5b132530b1ac069df335577e3581765995cba5a13995cdbbdbc8fb057c532c"
+dependencies = [
+ "bitflags",
+ "num-traits",
+]
+
+[[package]]
+name = "stdweb"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e"
+
+[[package]]
+name = "stdweb"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
+dependencies = [
+ "discard",
+ "rustc_version",
+ "serde",
+ "serde_json",
+ "stdweb-derive",
+ "stdweb-internal-macros",
+ "stdweb-internal-runtime",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "stdweb-derive"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "serde",
+ "serde_derive",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "stdweb-internal-macros"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
+dependencies = [
+ "base-x",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "stdweb-internal-runtime"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
+
+[[package]]
+name = "stretch"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b0dc6d20ce137f302edf90f9cd3d278866fd7fb139efca6f246161222ad6d87"
+dependencies = [
+ "lazy_static",
+ "libm",
+]
+
+[[package]]
+name = "strsim"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
+
+[[package]]
+name = "svg_fmt"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2"
+
+[[package]]
+name = "syn"
+version = "0.15.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "unicode-xid 0.2.2",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "indexmap",
+ "serde",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
+dependencies = [
+ "cfg-if 1.0.0",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
+dependencies = [
+ "lazy_static",
+ "log",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-serde"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd0568dbfe3baf7048b7908d2b32bca0d81cd56bec6d2a8f894b01d74f86be3"
+dependencies = [
+ "ansi_term 0.12.1",
+ "chrono",
+ "lazy_static",
+ "matchers",
+ "regex",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
+]
+
+[[package]]
+name = "tracing-wasm"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae741706df70547fca8715f74a8569677666e7be3454313af70f6e158034485"
+dependencies = [
+ "tracing",
+ "tracing-subscriber",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ttf-parser"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6"
+
+[[package]]
+name = "ttf-parser"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47094e61a758dfddc5bc6de0862c2640ee2643997835e58332a4863bcd5852e9"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
+name = "unicode-xid"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "uuid"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
+dependencies = [
+ "getrandom",
+ "serde",
+]
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "waker-fn"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
+dependencies = [
+ "cfg-if 1.0.0",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
+dependencies = [
+ "quote 1.0.10",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
+dependencies = [
+ "proc-macro2 1.0.32",
+ "quote 1.0.10",
+ "syn 1.0.81",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
+
+[[package]]
+name = "web-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wgpu"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1577ecc4f6992b9e965878ac594efb24eed2bdf089c11f45b3d1c5f216e2e30"
+dependencies = [
+ "arrayvec 0.7.2",
+ "js-sys",
+ "log",
+ "naga",
+ "parking_lot",
+ "raw-window-handle",
+ "smallvec",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "wgpu-core",
+ "wgpu-hal",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-core"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bdcbfa4885b32c2b1feb2faeb8b6a76065b752b8f08751b82f994e937687f46"
+dependencies = [
+ "arrayvec 0.7.2",
+ "bitflags",
+ "cfg_aliases",
+ "copyless",
+ "fxhash",
+ "log",
+ "naga",
+ "parking_lot",
+ "profiling",
+ "raw-window-handle",
+ "smallvec",
+ "thiserror",
+ "wgpu-hal",
+ "wgpu-types",
+]
+
+[[package]]
+name = "wgpu-hal"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e493835d9edb153d5c8a9d8d016e1811dbe32ddb707a110be1453c7b051d3ec"
+dependencies = [
+ "arrayvec 0.7.2",
+ "ash",
+ "bit-set",
+ "bitflags",
+ "block",
+ "core-graphics-types",
+ "d3d12",
+ "foreign-types",
+ "fxhash",
+ "glow",
+ "gpu-alloc",
+ "gpu-descriptor",
+ "inplace_it",
+ "js-sys",
+ "khronos-egl",
+ "libloading",
+ "log",
+ "metal",
+ "naga",
+ "objc",
+ "parking_lot",
+ "profiling",
+ "range-alloc",
+ "raw-window-handle",
+ "renderdoc-sys",
+ "thiserror",
+ "wasm-bindgen",
+ "web-sys",
+ "wgpu-types",
+ "winapi",
+]
+
+[[package]]
+name = "wgpu-types"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e15e44ba88ec415466e18e91881319e7c9e96cb905dc623305168aea65b85ccc"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winit"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79610794594d5e86be473ef7763f604f2159cbac8c94debd00df8fb41e86c2f8"
+dependencies = [
+ "bitflags",
+ "cocoa",
+ "core-foundation 0.9.2",
+ "core-graphics 0.22.3",
+ "core-video-sys",
+ "dispatch",
+ "instant",
+ "lazy_static",
+ "libc",
+ "log",
+ "mio",
+ "mio-misc",
+ "ndk 0.3.0",
+ "ndk-glue 0.3.0",
+ "ndk-sys",
+ "objc",
+ "parking_lot",
+ "percent-encoding",
+ "raw-window-handle",
+ "scopeguard",
+ "wasm-bindgen",
+ "web-sys",
+ "winapi",
+ "x11-dl",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "xi-unicode"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a"
diff --git a/bevy_kayak_ui/Cargo.toml b/bevy_kayak_ui/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..1ffcf7e7de740ee391bff8892cd6eaffecf43563
--- /dev/null
+++ b/bevy_kayak_ui/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "bevy_kayak_ui"
+version = "0.0.1"
+edition = "2021"
+
+[dependencies]
+bytemuck = "1.7.2"
+bevy = { git = "https://github.com/StarArawn/bevy", rev = "b26f563b13c267ffe1ee801bd71fd40b98a256e7" }
+kayak_core = { path = "../kayak_core" }
+kayak_components = { path = "../kayak_components" }
+kayak_render_macros = { path = "../kayak_render_macros" }
+kayak_font = { path = "../kayak_font" }
+crevice = { git = "https://github.com/StarArawn/bevy", rev = "b26f563b13c267ffe1ee801bd71fd40b98a256e7" }
diff --git a/bevy_kayak_ui/src/bevy_context.rs b/bevy_kayak_ui/src/bevy_context.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9322b73836bd23de91d7daaa90e3ffd807513802
--- /dev/null
+++ b/bevy_kayak_ui/src/bevy_context.rs
@@ -0,0 +1,32 @@
+use std::sync::{Arc, RwLock};
+
+use kayak_core::{
+    context::KayakContext,
+    styles::{Style, StyleProp, Units},
+};
+
+pub struct BevyContext {
+    pub kayak_context: Arc<RwLock<KayakContext>>,
+}
+
+impl BevyContext {
+    pub fn new<F: Fn(&mut Style, &mut KayakContext)>(width: f32, height: f32, f: F) -> Self {
+        let mut app_styles = Style {
+            width: StyleProp::Value(Units::Pixels(width)),
+            height: StyleProp::Value(Units::Pixels(height)),
+            ..Style::default()
+        };
+
+        let kayak_context = Arc::new(RwLock::new(KayakContext::new()));
+
+        if let Ok(mut kayak_context) = kayak_context.write() {
+            f(&mut app_styles, &mut kayak_context);
+
+            kayak_context.render();
+
+            kayak_context.widget_manager.dirty(true);
+        }
+
+        Self { kayak_context }
+    }
+}
diff --git a/bevy_kayak_ui/src/camera/camera.rs b/bevy_kayak_ui/src/camera/camera.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f0ed588de77ad2ad5f58925e4a8cbfabb9e6bbcd
--- /dev/null
+++ b/bevy_kayak_ui/src/camera/camera.rs
@@ -0,0 +1,58 @@
+use bevy::{
+    prelude::{Bundle, GlobalTransform, Transform},
+    render2::{
+        camera::{Camera, CameraPlugin, CameraProjection, DepthCalculation, WindowOrigin},
+        primitives::Frustum,
+        view::VisibleEntities,
+    },
+};
+
+use super::ortho::UIOrthographicProjection;
+
+#[derive(Bundle)]
+pub struct UICameraBundle {
+    pub camera: Camera,
+    pub orthographic_projection: UIOrthographicProjection,
+    pub visible_entities: VisibleEntities,
+    pub frustum: Frustum,
+    pub transform: Transform,
+    pub global_transform: GlobalTransform,
+}
+
+impl UICameraBundle {
+    pub const UI_CAMERA: &'static str = "KAYAK_UI_CAMERA";
+    pub fn new() -> Self {
+        // we want 0 to be "closest" and +far to be "farthest" in 2d, so we offset
+        // the camera's translation by far and use a right handed coordinate system
+        let far = 1000.0;
+
+        let orthographic_projection = UIOrthographicProjection {
+            far,
+            depth_calculation: DepthCalculation::ZDifference,
+            window_origin: WindowOrigin::BottomLeft,
+            ..Default::default()
+        };
+
+        let transform = Transform::from_xyz(0.0, 0.0, far - 0.1);
+
+        let view_projection =
+            orthographic_projection.get_projection_matrix() * transform.compute_matrix().inverse();
+        let frustum = Frustum::from_view_projection(
+            &view_projection,
+            &transform.translation,
+            &transform.back(),
+            orthographic_projection.far(),
+        );
+        UICameraBundle {
+            camera: Camera {
+                name: Some(Self::UI_CAMERA.to_string()),
+                ..Default::default()
+            },
+            orthographic_projection,
+            frustum,
+            visible_entities: VisibleEntities::default(),
+            transform,
+            global_transform: Default::default(),
+        }
+    }
+}
diff --git a/bevy_kayak_ui/src/camera/mod.rs b/bevy_kayak_ui/src/camera/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2d0c1db51dc1a20ef30678ae681a8f951ee14106
--- /dev/null
+++ b/bevy_kayak_ui/src/camera/mod.rs
@@ -0,0 +1,23 @@
+use bevy::{
+    prelude::{CoreStage, Plugin},
+    render2::camera::ActiveCameras,
+};
+
+mod camera;
+mod ortho;
+
+pub use camera::UICameraBundle;
+pub(crate) use ortho::UIOrthographicProjection;
+
+pub struct KayakUICameraPlugin;
+
+impl Plugin for KayakUICameraPlugin {
+    fn build(&self, app: &mut bevy::prelude::App) {
+        let mut active_cameras = app.world.get_resource_mut::<ActiveCameras>().unwrap();
+        active_cameras.add(UICameraBundle::UI_CAMERA);
+        app.add_system_to_stage(
+            CoreStage::PostUpdate,
+            bevy::render2::camera::camera_system::<UIOrthographicProjection>,
+        );
+    }
+}
diff --git a/bevy_kayak_ui/src/camera/ortho.rs b/bevy_kayak_ui/src/camera/ortho.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e4e6621f259008610d897e843f8268c625ae51c0
--- /dev/null
+++ b/bevy_kayak_ui/src/camera/ortho.rs
@@ -0,0 +1,73 @@
+use bevy::ecs::reflect::ReflectComponent;
+use bevy::{
+    math::Mat4,
+    reflect::Reflect,
+    render2::camera::{CameraProjection, DepthCalculation, ScalingMode, WindowOrigin},
+};
+
+#[derive(Debug, Clone, Reflect)]
+#[reflect(Component)]
+pub struct UIOrthographicProjection {
+    pub left: f32,
+    pub right: f32,
+    pub bottom: f32,
+    pub top: f32,
+    pub near: f32,
+    pub far: f32,
+    pub window_origin: WindowOrigin,
+    pub scaling_mode: ScalingMode,
+    pub scale: f32,
+    pub depth_calculation: DepthCalculation,
+}
+
+impl CameraProjection for UIOrthographicProjection {
+    fn get_projection_matrix(&self) -> Mat4 {
+        Mat4::orthographic_rh(
+            self.left * self.scale,
+            self.right * self.scale,
+            self.bottom * self.scale,
+            self.top * self.scale,
+            // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0]
+            // This is for interoperability with pipelines using infinite reverse perspective projections.
+            self.far,
+            self.near,
+        )
+    }
+
+    fn update(&mut self, width: f32, height: f32) {
+        match (&self.scaling_mode, &self.window_origin) {
+            (ScalingMode::WindowSize, WindowOrigin::BottomLeft) => {
+                self.left = 0.0;
+                self.right = width;
+                self.top = 0.0;
+                self.bottom = height;
+            }
+            _ => {}
+        }
+    }
+
+    fn depth_calculation(&self) -> DepthCalculation {
+        self.depth_calculation
+    }
+
+    fn far(&self) -> f32 {
+        self.far
+    }
+}
+
+impl Default for UIOrthographicProjection {
+    fn default() -> Self {
+        UIOrthographicProjection {
+            left: -1.0,
+            right: 1.0,
+            bottom: -1.0,
+            top: 1.0,
+            near: 0.0,
+            far: 1000.0,
+            window_origin: WindowOrigin::Center,
+            scaling_mode: ScalingMode::WindowSize,
+            scale: 1.0,
+            depth_calculation: DepthCalculation::Distance,
+        }
+    }
+}
diff --git a/bevy_kayak_ui/src/lib.rs b/bevy_kayak_ui/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c5f8e11b1bfb03c0dce3d49c79410e528b273348
--- /dev/null
+++ b/bevy_kayak_ui/src/lib.rs
@@ -0,0 +1,29 @@
+use bevy::{prelude::{Plugin, ResMut}, render2::color::Color};
+
+mod bevy_context;
+mod camera;
+mod render;
+
+pub use bevy_context::BevyContext;
+pub use camera::*;
+
+#[derive(Default)]
+pub struct BevyKayakUIPlugin;
+
+impl Plugin for BevyKayakUIPlugin {
+    fn build(&self, app: &mut bevy::prelude::App) {
+        app.add_plugin(render::BevyKayakUIRenderPlugin)
+            .add_plugin(camera::KayakUICameraPlugin)
+            .add_system(update);
+    }
+}
+
+pub(crate) fn to_bevy_color(color: &kayak_core::color::Color) -> Color {
+    Color::rgba(color.r, color.g, color.b, color.a)
+}
+
+pub fn update(bevy_context: ResMut<BevyContext>) {
+    if let Ok(mut context) = bevy_context.kayak_context.write() {
+        context.render();
+    }
+}
diff --git a/bevy_kayak_ui/src/render/mod.rs b/bevy_kayak_ui/src/render/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..051005c4963194bfc8fc6eb797147c92d81b2503
--- /dev/null
+++ b/bevy_kayak_ui/src/render/mod.rs
@@ -0,0 +1,95 @@
+use bevy::{
+    prelude::{Commands, Entity, Plugin, Query, Res, ResMut, With},
+    render2::{
+        camera::ActiveCameras,
+        render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType},
+        render_phase::{sort_phase_system, DrawFunctions, RenderPhase},
+        render_resource::{
+            Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
+        },
+        renderer::RenderDevice,
+        texture::TextureCache,
+        view::{ExtractedView, ViewDepthTexture},
+        RenderApp, RenderStage,
+    },
+};
+
+use crate::{
+    render::{
+        ui_pass::MainPassUINode, ui_pass_driver::UIPassDriverNode, unified::UnifiedRenderPlugin,
+    },
+    UICameraBundle,
+};
+
+use self::ui_pass::TransparentUI;
+
+mod ui_pass;
+mod ui_pass_driver;
+mod unified;
+
+pub mod node {
+    pub const UI_PASS_DEPENDENCIES: &str = "ui_pass_dependencies";
+    pub const UI_PASS_DRIVER: &str = "ui_pass_driver";
+}
+
+pub mod draw_ui_graph {
+    pub const NAME: &str = "draw_ui";
+    pub mod input {
+        pub const VIEW_ENTITY: &str = "view_entity";
+    }
+    pub mod node {
+        pub const MAIN_PASS: &str = "ui_pass";
+    }
+}
+
+pub struct BevyKayakUIRenderPlugin;
+
+impl Plugin for BevyKayakUIRenderPlugin {
+    fn build(&self, app: &mut bevy::prelude::App) {
+        let render_app = app.sub_app(RenderApp);
+        render_app
+            .init_resource::<DrawFunctions<TransparentUI>>()
+            .add_system_to_stage(RenderStage::Extract, extract_core_pipeline_camera_phases)
+            .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<TransparentUI>);
+
+        let pass_node_ui = MainPassUINode::new(&mut render_app.world);
+        let mut graph = render_app.world.get_resource_mut::<RenderGraph>().unwrap();
+
+        let mut draw_ui_graph = RenderGraph::default();
+        draw_ui_graph.add_node(draw_ui_graph::node::MAIN_PASS, pass_node_ui);
+        let input_node_id = draw_ui_graph.set_input(vec![SlotInfo::new(
+            draw_ui_graph::input::VIEW_ENTITY,
+            SlotType::Entity,
+        )]);
+        draw_ui_graph
+            .add_slot_edge(
+                input_node_id,
+                draw_ui_graph::input::VIEW_ENTITY,
+                draw_ui_graph::node::MAIN_PASS,
+                MainPassUINode::IN_VIEW,
+            )
+            .unwrap();
+        graph.add_sub_graph(draw_ui_graph::NAME, draw_ui_graph);
+
+        graph.add_node(node::UI_PASS_DEPENDENCIES, EmptyNode);
+        graph.add_node(node::UI_PASS_DRIVER, UIPassDriverNode);
+        graph
+            .add_node_edge(node::UI_PASS_DEPENDENCIES, node::UI_PASS_DRIVER)
+            .unwrap();
+
+        app.add_plugin(UnifiedRenderPlugin);
+    }
+}
+
+pub fn extract_core_pipeline_camera_phases(
+    mut commands: Commands,
+    active_cameras: Res<ActiveCameras>,
+) {
+    if let Some(camera_2d) = active_cameras.get(UICameraBundle::UI_CAMERA) {
+        if let Some(entity) = camera_2d.entity {
+            commands
+                .get_or_spawn(entity)
+                .insert(RenderPhase::<TransparentUI>::default());
+        }
+    }
+}
diff --git a/bevy_kayak_ui/src/render/quad/mod.rs b/bevy_kayak_ui/src/render/quad/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..222f9839359376c87164dc92f5293db512cfc5a9
--- /dev/null
+++ b/bevy_kayak_ui/src/render/quad/mod.rs
@@ -0,0 +1,43 @@
+use bevy::{
+    prelude::{Assets, HandleUntyped, Plugin},
+    reflect::TypeUuid,
+    render2::{render_phase::DrawFunctions, render_resource::Shader, RenderApp, RenderStage},
+};
+
+use crate::render::{
+    quad::pipeline::{DrawQuad, QuadMeta, QuadPipeline},
+    ui_pass::TransparentUI,
+};
+
+mod pipeline;
+
+pub const QUAD_SHADER_HANDLE: HandleUntyped =
+    HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 7604018236855288450);
+
+#[derive(Default)]
+pub struct QuadRendererPlugin;
+
+impl Plugin for QuadRendererPlugin {
+    fn build(&self, app: &mut bevy::prelude::App) {
+        let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap();
+        let quad_shader = Shader::from_wgsl(include_str!("shader.wgsl"));
+        shaders.set_untracked(QUAD_SHADER_HANDLE, quad_shader);
+
+        let render_app = app.sub_app(RenderApp);
+        render_app
+            .init_resource::<QuadPipeline>()
+            .init_resource::<QuadMeta>()
+            .add_system_to_stage(RenderStage::Extract, pipeline::extract_quads)
+            .add_system_to_stage(RenderStage::Prepare, pipeline::prepare_quads)
+            .add_system_to_stage(RenderStage::Queue, pipeline::queue_quads);
+
+        let draw_quad = DrawQuad::new(&mut render_app.world);
+
+        render_app
+            .world
+            .get_resource::<DrawFunctions<TransparentUI>>()
+            .unwrap()
+            .write()
+            .add(draw_quad);
+    }
+}
diff --git a/bevy_kayak_ui/src/render/quad/pipeline.rs b/bevy_kayak_ui/src/render/quad/pipeline.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8c5d8575be447550679ac7c756a99292185d64af
--- /dev/null
+++ b/bevy_kayak_ui/src/render/quad/pipeline.rs
@@ -0,0 +1,344 @@
+use bevy::{
+    core::FloatOrd,
+    ecs::system::{
+        lifetimeless::{Read, SQuery, SRes},
+        SystemState,
+    },
+    math::{const_vec3, Mat4, Quat, Vec2, Vec3},
+    prelude::{Bundle, Color, Commands, Entity, FromWorld, Query, Res, ResMut, World},
+    render2::{
+        render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass},
+        render_resource::{
+            BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
+            BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendComponent,
+            BlendFactor, BlendOperation, BlendState, BufferBindingType, BufferSize, BufferUsages,
+            BufferVec, CachedPipelineId, ColorTargetState, ColorWrites, CompareFunction,
+            DepthBiasState, DepthStencilState, FragmentState, FrontFace, MultisampleState,
+            PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipelineCache,
+            RenderPipelineDescriptor, Shader, ShaderStages, StencilFaceState, StencilState,
+            TextureFormat, VertexAttribute, VertexBufferLayout, VertexFormat, VertexState,
+            VertexStepMode,
+        },
+        renderer::{RenderDevice, RenderQueue},
+        texture::BevyDefault,
+        view::{ViewUniformOffset, ViewUniforms},
+    },
+    sprite2::Rect,
+};
+use bytemuck::{Pod, Zeroable};
+use kayak_core::render_primitive::RenderPrimitive;
+
+use crate::{
+    render::{quad::QUAD_SHADER_HANDLE, ui_pass::TransparentUI},
+    to_bevy_color, BevyContext,
+};
+
+pub struct QuadPipeline {
+    view_layout: BindGroupLayout,
+    pipeline: CachedPipelineId,
+}
+
+const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
+    const_vec3!([0.0, 0.0, 0.0]),
+    const_vec3!([1.0, 1.0, 0.0]),
+    const_vec3!([0.0, 1.0, 0.0]),
+    const_vec3!([0.0, 0.0, 0.0]),
+    const_vec3!([1.0, 0.0, 0.0]),
+    const_vec3!([1.0, 1.0, 0.0]),
+];
+
+impl FromWorld for QuadPipeline {
+    fn from_world(world: &mut World) -> Self {
+        let world = world.cell();
+        let render_device = world.get_resource::<RenderDevice>().unwrap();
+        let mut pipeline_cache = world.get_resource_mut::<RenderPipelineCache>().unwrap();
+
+        let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
+            entries: &[BindGroupLayoutEntry {
+                binding: 0,
+                visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                ty: BindingType::Buffer {
+                    ty: BufferBindingType::Uniform,
+                    has_dynamic_offset: true,
+                    // TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
+                    // Context: https://github.com/LPGhatguy/crevice/issues/29
+                    min_binding_size: BufferSize::new(144),
+                },
+                count: None,
+            }],
+            label: Some("quad_view_layout"),
+        });
+
+        let vertex_buffer_layout = VertexBufferLayout {
+            array_stride: 28,
+            step_mode: VertexStepMode::Vertex,
+            attributes: vec![
+                VertexAttribute {
+                    format: VertexFormat::Float32x3,
+                    offset: 0,
+                    shader_location: 0,
+                },
+                VertexAttribute {
+                    format: VertexFormat::Float32x4,
+                    offset: 12,
+                    shader_location: 1,
+                },
+            ],
+        };
+
+        let pipeline_desc = RenderPipelineDescriptor {
+            vertex: VertexState {
+                shader: QUAD_SHADER_HANDLE.typed::<Shader>(),
+                entry_point: "vertex".into(),
+                shader_defs: vec![],
+                buffers: vec![vertex_buffer_layout],
+            },
+            fragment: Some(FragmentState {
+                shader: QUAD_SHADER_HANDLE.typed::<Shader>(),
+                shader_defs: vec![],
+                entry_point: "fragment".into(),
+                targets: vec![ColorTargetState {
+                    format: TextureFormat::bevy_default(),
+                    blend: Some(BlendState {
+                        color: BlendComponent {
+                            src_factor: BlendFactor::SrcAlpha,
+                            dst_factor: BlendFactor::OneMinusSrcAlpha,
+                            operation: BlendOperation::Add,
+                        },
+                        alpha: BlendComponent {
+                            src_factor: BlendFactor::One,
+                            dst_factor: BlendFactor::One,
+                            operation: BlendOperation::Add,
+                        },
+                    }),
+                    write_mask: ColorWrites::ALL,
+                }],
+            }),
+            layout: Some(vec![view_layout.clone()]),
+            primitive: PrimitiveState {
+                front_face: FrontFace::Ccw,
+                cull_mode: None,
+                polygon_mode: PolygonMode::Fill,
+                clamp_depth: false,
+                conservative: false,
+                topology: PrimitiveTopology::TriangleList,
+                strip_index_format: None,
+            },
+            depth_stencil: Some(DepthStencilState {
+                format: TextureFormat::Depth32Float,
+                depth_write_enabled: true,
+                depth_compare: CompareFunction::Greater,
+                stencil: StencilState {
+                    front: StencilFaceState::IGNORE,
+                    back: StencilFaceState::IGNORE,
+                    read_mask: 0,
+                    write_mask: 0,
+                },
+                bias: DepthBiasState {
+                    constant: 0,
+                    slope_scale: 0.0,
+                    clamp: 0.0,
+                },
+            }),
+            multisample: MultisampleState {
+                count: 1,
+                mask: !0,
+                alpha_to_coverage_enabled: false,
+            },
+            label: Some("quad_pipeline".into()),
+        };
+
+        QuadPipeline {
+            pipeline: pipeline_cache.queue(pipeline_desc),
+            view_layout,
+        }
+    }
+}
+
+#[derive(Bundle)]
+pub struct ExtractQuadBundle {
+    extracted_quad: ExtractedQuad,
+}
+
+pub struct ExtractedQuad {
+    rect: Rect,
+    background_color: Color,
+    vertex_index: usize,
+}
+
+pub fn extract_quads(mut commands: Commands, context: Res<BevyContext>) {
+    let render_commands = if let Ok(context) = context.kayak_context.read() {
+        context.widget_manager.build_render_primitives()
+    } else {
+        vec![]
+    };
+
+    let quad_commands: Vec<&RenderPrimitive> = render_commands
+        .iter()
+        .filter(|command| matches!(command, RenderPrimitive::Quad { .. }))
+        .collect::<Vec<_>>();
+
+    let mut extracted_quads = Vec::new();
+    for render_primitive in quad_commands {
+        let (background_color, layout) = match render_primitive {
+            RenderPrimitive::Quad {
+                background_color,
+                layout,
+            } => (background_color, layout),
+            _ => panic!(""),
+        };
+
+        extracted_quads.push(ExtractQuadBundle {
+            extracted_quad: ExtractedQuad {
+                rect: Rect {
+                    min: Vec2::new(layout.posx, layout.posy),
+                    max: Vec2::new(layout.width, layout.height),
+                },
+                background_color: to_bevy_color(background_color),
+                vertex_index: 0,
+            },
+        });
+    }
+    commands.spawn_batch(extracted_quads);
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Pod, Zeroable)]
+struct QuadVertex {
+    pub position: [f32; 3],
+    pub color: [f32; 4],
+}
+
+pub struct QuadMeta {
+    vertices: BufferVec<QuadVertex>,
+    view_bind_group: Option<BindGroup>,
+}
+
+impl Default for QuadMeta {
+    fn default() -> Self {
+        Self {
+            vertices: BufferVec::new(BufferUsages::VERTEX),
+            view_bind_group: None,
+        }
+    }
+}
+
+pub fn prepare_quads(
+    render_device: Res<RenderDevice>,
+    render_queue: Res<RenderQueue>,
+    mut sprite_meta: ResMut<QuadMeta>,
+    mut extracted_sprites: Query<&mut ExtractedQuad>,
+) {
+    let extracted_sprite_len = extracted_sprites.iter_mut().len();
+    // dont create buffers when there are no quads
+    if extracted_sprite_len == 0 {
+        return;
+    }
+
+    sprite_meta.vertices.clear();
+    sprite_meta.vertices.reserve(
+        extracted_sprite_len * QUAD_VERTEX_POSITIONS.len(),
+        &render_device,
+    );
+
+    for (i, mut extracted_sprite) in extracted_sprites.iter_mut().enumerate() {
+        let sprite_rect = extracted_sprite.rect;
+        let color = extracted_sprite.background_color.as_linear_rgba_f32();
+
+        extracted_sprite.vertex_index = i;
+        for vertex_position in QUAD_VERTEX_POSITIONS.iter() {
+            let world = Mat4::from_scale_rotation_translation(
+                sprite_rect.size().extend(1.0),
+                Quat::default(),
+                sprite_rect.min.extend(0.0),
+            );
+            let final_position = (world * Vec3::from(*vertex_position).extend(1.0)).truncate();
+            sprite_meta.vertices.push(QuadVertex {
+                position: final_position.into(),
+                color,
+            });
+        }
+    }
+    sprite_meta
+        .vertices
+        .write_buffer(&render_device, &render_queue);
+}
+
+pub fn queue_quads(
+    draw_functions: Res<DrawFunctions<TransparentUI>>,
+    render_device: Res<RenderDevice>,
+    mut sprite_meta: ResMut<QuadMeta>,
+    view_uniforms: Res<ViewUniforms>,
+    quad_pipeline: Res<QuadPipeline>,
+    mut extracted_sprites: Query<(Entity, &ExtractedQuad)>,
+    mut views: Query<&mut RenderPhase<TransparentUI>>,
+) {
+    if let Some(view_binding) = view_uniforms.uniforms.binding() {
+        sprite_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor {
+            entries: &[BindGroupEntry {
+                binding: 0,
+                resource: view_binding,
+            }],
+            label: Some("quad_view_bind_group"),
+            layout: &quad_pipeline.view_layout,
+        }));
+        let draw_quad = draw_functions.read().get_id::<DrawQuad>().unwrap();
+        for mut transparent_phase in views.iter_mut() {
+            for (entity, quad) in extracted_sprites.iter_mut() {
+                transparent_phase.add(TransparentUI {
+                    draw_function: draw_quad,
+                    pipeline: quad_pipeline.pipeline,
+                    entity,
+                    sort_key: FloatOrd(0.0),
+                });
+            }
+        }
+    }
+}
+
+pub struct DrawQuad {
+    params: SystemState<(
+        SRes<QuadMeta>,
+        SRes<RenderPipelineCache>,
+        SQuery<Read<ViewUniformOffset>>,
+        SQuery<Read<ExtractedQuad>>,
+    )>,
+}
+
+impl DrawQuad {
+    pub fn new(world: &mut World) -> Self {
+        Self {
+            params: SystemState::new(world),
+        }
+    }
+}
+
+impl Draw<TransparentUI> for DrawQuad {
+    fn draw<'w>(
+        &mut self,
+        world: &'w World,
+        pass: &mut TrackedRenderPass<'w>,
+        view: Entity,
+        item: &TransparentUI,
+    ) {
+        let (quad_meta, pipelines, views, quads) = self.params.get(world);
+        let view_uniform = views.get(view).unwrap();
+        let quad_meta = quad_meta.into_inner();
+        let extracted_quad = quads.get(item.entity).unwrap();
+        if let Some(pipeline) = pipelines.into_inner().get(item.pipeline) {
+            pass.set_render_pipeline(pipeline);
+            pass.set_vertex_buffer(0, quad_meta.vertices.buffer().unwrap().slice(..));
+            pass.set_bind_group(
+                0,
+                quad_meta.view_bind_group.as_ref().unwrap(),
+                &[view_uniform.offset],
+            );
+
+            pass.draw(
+                (extracted_quad.vertex_index * QUAD_VERTEX_POSITIONS.len()) as u32
+                    ..((extracted_quad.vertex_index + 1) * QUAD_VERTEX_POSITIONS.len()) as u32,
+                0..1,
+            );
+        }
+    }
+}
diff --git a/bevy_kayak_ui/src/render/quad/shader.wgsl b/bevy_kayak_ui/src/render/quad/shader.wgsl
new file mode 100644
index 0000000000000000000000000000000000000000..df4e649349c713520cbd6d0882bb958df250c4b3
--- /dev/null
+++ b/bevy_kayak_ui/src/render/quad/shader.wgsl
@@ -0,0 +1,30 @@
+[[block]]
+struct View {
+    view_proj: mat4x4<f32>;
+    world_position: vec3<f32>;
+};
+[[group(0), binding(0)]]
+var<uniform> view: View;
+
+struct VertexOutput {
+    [[builtin(position)]] position: vec4<f32>;
+    [[location(0)]] color: vec4<f32>;
+    [[location(1)]] uv: vec3<f32>;
+};
+
+[[stage(vertex)]]
+fn vertex(
+    [[location(0)]] vertex_position: vec3<f32>,
+    [[location(1)]] vertex_color: vec4<f32>,
+    [[location(2)]] vertex_uv: vec3<f32>;
+) -> VertexOutput {
+    var out: VertexOutput;
+    out.color = vertex_color;
+    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
+    return out;
+}
+
+[[stage(fragment)]]
+fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> {
+    return in.color;
+}
\ No newline at end of file
diff --git a/bevy_kayak_ui/src/render/text/font.rs b/bevy_kayak_ui/src/render/text/font.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7a4424cdbbe7f29e72b778830d29aae73886ea24
--- /dev/null
+++ b/bevy_kayak_ui/src/render/text/font.rs
@@ -0,0 +1,8 @@
+use bevy::reflect::TypeUuid;
+use kayak_font::Font;
+
+#[derive(Debug, Clone, TypeUuid)]
+#[uuid = "4fe4732c-6731-49bb-bafc-4690d636b848"]
+pub struct KayakFont {
+    pub font: Font,
+}
diff --git a/bevy_kayak_ui/src/render/text/font_mapping.rs b/bevy_kayak_ui/src/render/text/font_mapping.rs
new file mode 100644
index 0000000000000000000000000000000000000000..eeb405ab60d8c6d6d48ab722c9cf0caaa31a33f1
--- /dev/null
+++ b/bevy_kayak_ui/src/render/text/font_mapping.rs
@@ -0,0 +1,40 @@
+use bevy::{prelude::Handle, utils::HashMap};
+
+use super::font::KayakFont;
+
+pub struct FontMapping {
+    count: u16,
+    font_ids: HashMap<Handle<KayakFont>, u16>,
+    font_handles: HashMap<u16, Handle<KayakFont>>,
+}
+
+impl Default for FontMapping {
+    fn default() -> Self {
+        Self {
+            count: 0,
+            font_ids: HashMap::default(),
+            font_handles: HashMap::default(),
+        }
+    }
+}
+
+impl FontMapping {
+    pub(crate) fn add(&mut self, handle: Handle<KayakFont>) -> u16 {
+        if !self.font_ids.contains_key(&handle) {
+            let id = self.count;
+            self.font_ids.insert(handle.clone(), id);
+            self.font_handles.insert(id, handle);
+            self.count += 1;
+
+            id
+        } else {
+            *self.font_ids.get(&handle).unwrap()
+        }
+    }
+
+    pub(crate) fn get_handle(&self, id: u16) -> Option<Handle<KayakFont>> {
+        self.font_handles
+            .get(&id)
+            .and_then(|item| Some(item.clone()))
+    }
+}
diff --git a/bevy_kayak_ui/src/render/text/font_texture_cache.rs b/bevy_kayak_ui/src/render/text/font_texture_cache.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a15bb07e81734b5b7794e40cbc0172d927a447c2
--- /dev/null
+++ b/bevy_kayak_ui/src/render/text/font_texture_cache.rs
@@ -0,0 +1,201 @@
+use bevy::{
+    prelude::Handle,
+    render2::{
+        render_resource::{
+            AddressMode, BindGroup, BindGroupDescriptor, BindGroupEntry, BindingResource, Extent3d,
+            FilterMode, ImageCopyTexture, ImageDataLayout, Origin3d, SamplerDescriptor,
+            TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
+            TextureViewDescriptor, TextureViewDimension,
+        },
+        renderer::{RenderDevice, RenderQueue},
+        texture::{GpuImage, TextureFormatPixelInfo},
+    },
+    utils::HashMap,
+};
+
+use super::{font::KayakFont, pipeline::TextPipeline};
+
+pub const MAX_CHARACTERS: u32 = 100;
+
+pub struct FontTextureCache {
+    images: HashMap<Handle<KayakFont>, GpuImage>,
+    pub(crate) bind_groups: HashMap<Handle<KayakFont>, BindGroup>,
+    fonts: HashMap<Handle<KayakFont>, KayakFont>,
+    new_fonts: Vec<Handle<KayakFont>>,
+    updated_fonts: Vec<Handle<KayakFont>>,
+}
+
+impl Default for FontTextureCache {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl FontTextureCache {
+    pub fn new() -> Self {
+        Self {
+            images: HashMap::default(),
+            bind_groups: HashMap::default(),
+            fonts: HashMap::default(),
+            new_fonts: Vec::new(),
+            updated_fonts: Vec::new(),
+        }
+    }
+
+    pub fn add(&mut self, kayak_font_handle: Handle<KayakFont>, font: KayakFont) {
+        if !self.fonts.contains_key(&kayak_font_handle) {
+            self.fonts.insert(kayak_font_handle.clone(), font);
+            self.new_fonts.push(kayak_font_handle);
+        } else {
+            if let Some(old_font) = self.fonts.get_mut(&kayak_font_handle) {
+                *old_font = font;
+                self.updated_fonts.push(kayak_font_handle);
+            }
+        }
+    }
+
+    pub fn process_new(&mut self, device: &RenderDevice, pipeline: &TextPipeline) {
+        let new_fonts = self.new_fonts.drain(..);
+        for kayak_font_handle in new_fonts {
+            if let Some(font) = self.fonts.get(&kayak_font_handle) {
+                Self::create_texture(
+                    &mut self.images,
+                    kayak_font_handle.clone_weak(),
+                    font,
+                    device,
+                );
+
+                let gpu_image = self.images.get(&kayak_font_handle).unwrap();
+
+                // create bind group
+                let binding = device.create_bind_group(&BindGroupDescriptor {
+                    label: Some("text_image_bind_group"),
+                    entries: &[
+                        BindGroupEntry {
+                            binding: 0,
+                            resource: BindingResource::TextureView(&gpu_image.texture_view),
+                        },
+                        BindGroupEntry {
+                            binding: 1,
+                            resource: BindingResource::Sampler(&gpu_image.sampler),
+                        },
+                    ],
+                    layout: &pipeline.image_layout,
+                });
+
+                self.bind_groups.insert(kayak_font_handle, binding);
+            }
+        }
+    }
+
+    pub fn process_updated(&mut self, queue: &RenderQueue) {
+        let updated_fonts = self.updated_fonts.drain(..);
+        for kayak_font_handle in updated_fonts {
+            if let Some(font) = self.fonts.get_mut(&kayak_font_handle) {
+                Self::process_new_chars_into_texture(
+                    &mut self.images,
+                    kayak_font_handle,
+                    font,
+                    queue,
+                );
+            }
+        }
+    }
+
+    fn create_texture(
+        images: &mut HashMap<Handle<KayakFont>, GpuImage>,
+        font_handle: Handle<KayakFont>,
+        font: &KayakFont,
+        device: &RenderDevice,
+    ) {
+        let texture_descriptor = TextureDescriptor {
+            label: Some("font_texture_array"),
+            size: Extent3d {
+                width: font.font.cache.dimensions,
+                height: font.font.cache.dimensions,
+                depth_or_array_layers: MAX_CHARACTERS,
+            },
+            mip_level_count: 1,
+            sample_count: 1,
+            dimension: TextureDimension::D2,
+            format: TextureFormat::Rgba32Float,
+            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
+        };
+
+        let sampler_descriptor = SamplerDescriptor {
+            label: Some("font_texture_array_sampler"),
+            address_mode_u: AddressMode::ClampToEdge,
+            address_mode_v: AddressMode::ClampToEdge,
+            address_mode_w: AddressMode::ClampToEdge,
+            mag_filter: FilterMode::Linear,
+            min_filter: FilterMode::Linear,
+            mipmap_filter: FilterMode::Nearest,
+            lod_min_clamp: 0.0,
+            lod_max_clamp: std::f32::MAX,
+            compare: None,
+            anisotropy_clamp: None,
+            border_color: None,
+        };
+
+        let texture = device.create_texture(&texture_descriptor);
+        let sampler = device.create_sampler(&sampler_descriptor);
+
+        let texture_view = texture.create_view(&TextureViewDescriptor {
+            label: Some("font_texture_array_view"),
+            format: None,
+            dimension: Some(TextureViewDimension::D2Array),
+            aspect: bevy::render2::render_resource::TextureAspect::All,
+            base_mip_level: 0,
+            base_array_layer: 0,
+            mip_level_count: None,
+            array_layer_count: std::num::NonZeroU32::new(MAX_CHARACTERS),
+        });
+
+        let image = GpuImage {
+            texture,
+            sampler,
+            texture_view,
+        };
+
+        images.insert(font_handle, image);
+    }
+
+    pub fn process_new_chars_into_texture(
+        images: &mut HashMap<Handle<KayakFont>, GpuImage>,
+        kayak_font_handle: Handle<KayakFont>,
+        font: &mut KayakFont,
+        queue: &RenderQueue,
+    ) {
+        let size = font.font.cache.dimensions;
+        if let Some(gpu_image) = images.get_mut(&kayak_font_handle) {
+            for (_, id, pixels) in font.font.get_data_to_process() {
+                let format_size = TextureFormat::Rgba32Float.pixel_size();
+                queue.write_texture(
+                    ImageCopyTexture {
+                        texture: &gpu_image.texture,
+                        mip_level: 0,
+                        origin: Origin3d {
+                            x: 0,
+                            y: 0,
+                            z: id as u32,
+                        },
+                        aspect: TextureAspect::All,
+                    },
+                    &pixels,
+                    ImageDataLayout {
+                        offset: 0,
+                        bytes_per_row: Some(
+                            std::num::NonZeroU32::new(size * format_size as u32).unwrap(),
+                        ),
+                        rows_per_image: None,
+                    },
+                    Extent3d {
+                        width: size,
+                        height: size,
+                        depth_or_array_layers: 1,
+                    },
+                );
+            }
+        }
+    }
+}
diff --git a/bevy_kayak_ui/src/render/text/mod.rs b/bevy_kayak_ui/src/render/text/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..bfe438125d34f0c313bd981f756e130738d0707d
--- /dev/null
+++ b/bevy_kayak_ui/src/render/text/mod.rs
@@ -0,0 +1,141 @@
+use bevy::{
+    prelude::{
+        AddAsset, AssetEvent, Assets, Commands, EventReader, Handle, HandleUntyped, Plugin, Res,
+        ResMut,
+    },
+    reflect::TypeUuid,
+    render2::{
+        render_phase::DrawFunctions,
+        render_resource::Shader,
+        renderer::{RenderDevice, RenderQueue},
+        RenderApp, RenderStage,
+    },
+    utils::HashSet,
+};
+
+use crate::render::{
+    text::{
+        font_mapping::FontMapping,
+        pipeline::{DrawText, TextMeta, TextPipeline},
+    },
+    ui_pass::TransparentUI,
+};
+
+use self::{font::KayakFont, font_texture_cache::FontTextureCache};
+
+mod font;
+mod font_mapping;
+mod font_texture_cache;
+mod pipeline;
+
+pub const TEXT_SHADER_HANDLE: HandleUntyped =
+    HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 1561765330850392701);
+
+#[derive(Default)]
+pub struct TextRendererPlugin;
+
+impl Plugin for TextRendererPlugin {
+    fn build(&self, app: &mut bevy::prelude::App) {
+        let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap();
+        let text_shader = Shader::from_wgsl(include_str!("shader.wgsl"));
+        shaders.set_untracked(TEXT_SHADER_HANDLE, text_shader);
+
+        let render_app = app.sub_app(RenderApp);
+        render_app
+            .init_resource::<TextPipeline>()
+            .init_resource::<TextMeta>()
+            .add_system_to_stage(RenderStage::Extract, pipeline::extract_texts)
+            .add_system_to_stage(RenderStage::Prepare, pipeline::prepare_texts)
+            .add_system_to_stage(RenderStage::Queue, pipeline::queue_texts);
+
+        let draw_text = DrawText::new(&mut render_app.world);
+
+        render_app
+            .world
+            .get_resource::<DrawFunctions<TransparentUI>>()
+            .unwrap()
+            .write()
+            .add(draw_text);
+
+        render_app
+            .init_resource::<FontTextureCache>()
+            .init_resource::<ExtractedFonts>()
+            .add_system_to_stage(RenderStage::Extract, extract_fonts)
+            .add_system_to_stage(RenderStage::Prepare, prepare_fonts)
+            .add_system_to_stage(RenderStage::Queue, create_and_update_font_cache_texture);
+
+        app.add_asset::<KayakFont>()
+            .init_resource::<FontMapping>()
+            .add_startup_system(load_fonts);
+    }
+}
+
+#[derive(Default)]
+pub struct ExtractedFonts {
+    pub fonts: Vec<(Handle<KayakFont>, KayakFont)>,
+}
+
+fn load_fonts(mut font_assets: ResMut<Assets<KayakFont>>, mut font_mapping: ResMut<FontMapping>) {
+    let font_bytes = include_bytes!("../../../../resources/Roboto-Regular.ttf");
+    let font = kayak_font::Font::new(font_bytes, 128);
+
+    let handle = font_assets.add(KayakFont { font });
+    font_mapping.add(handle);
+
+    dbg!("Loaded base font!");
+}
+
+fn extract_fonts(
+    mut commands: Commands,
+    font_assets: Res<Assets<KayakFont>>,
+    mut events: EventReader<AssetEvent<KayakFont>>,
+) {
+    let mut extracted_fonts = ExtractedFonts { fonts: Vec::new() };
+    let mut changed_assets = HashSet::default();
+    let mut removed = Vec::new();
+    for event in events.iter() {
+        match event {
+            AssetEvent::Created { handle } => {
+                changed_assets.insert(handle);
+                dbg!("New font added!");
+            }
+            AssetEvent::Modified { handle } => {
+                changed_assets.insert(handle);
+                dbg!("Font changed!");
+            }
+            AssetEvent::Removed { handle } => {
+                if !changed_assets.remove(handle) {
+                    removed.push(handle.clone_weak());
+                }
+            }
+        }
+    }
+
+    for handle in changed_assets {
+        let font_asset = font_assets.get(handle).unwrap();
+        let font = font_asset.clone();
+
+        extracted_fonts.fonts.push((handle.clone_weak(), font));
+    }
+
+    commands.insert_resource(extracted_fonts);
+}
+
+fn prepare_fonts(
+    mut extracted_fonts: ResMut<ExtractedFonts>,
+    mut font_texture_cache: ResMut<FontTextureCache>,
+) {
+    for (handle, font) in extracted_fonts.fonts.drain(..) {
+        font_texture_cache.add(handle, font);
+    }
+}
+
+fn create_and_update_font_cache_texture(
+    device: Res<RenderDevice>,
+    queue: Res<RenderQueue>,
+    pipeline: Res<TextPipeline>,
+    mut font_texture_cache: ResMut<FontTextureCache>,
+) {
+    font_texture_cache.process_new(&device, &pipeline);
+    font_texture_cache.process_updated(&queue);
+}
diff --git a/bevy_kayak_ui/src/render/text/pipeline.rs b/bevy_kayak_ui/src/render/text/pipeline.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6b2691eaada2b655a96baf2ea4b49af9d739391f
--- /dev/null
+++ b/bevy_kayak_ui/src/render/text/pipeline.rs
@@ -0,0 +1,360 @@
+use bevy::{
+    core::FloatOrd,
+    ecs::system::{
+        lifetimeless::{Read, SQuery, SRes},
+        SystemState,
+    },
+    math::{const_vec3, Mat4, Quat, Vec2, Vec3},
+    prelude::{
+        Assets, Bundle, Color, Commands, Entity, FromWorld, Handle, Query, Res, ResMut, World,
+    },
+    render2::{
+        render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass},
+        render_resource::{
+            BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
+            BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendComponent,
+            BlendFactor, BlendOperation, BlendState, BufferBindingType, BufferSize, BufferUsages,
+            BufferVec, CachedPipelineId, ColorTargetState, ColorWrites, CompareFunction,
+            DepthBiasState, DepthStencilState, FragmentState, FrontFace, MultisampleState,
+            PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipelineCache,
+            RenderPipelineDescriptor, Shader, ShaderStages, StencilFaceState, StencilState,
+            TextureFormat, TextureSampleType, TextureViewDimension, VertexAttribute,
+            VertexBufferLayout, VertexFormat, VertexState, VertexStepMode,
+        },
+        renderer::{RenderDevice, RenderQueue},
+        texture::BevyDefault,
+        view::{ViewUniformOffset, ViewUniforms},
+    },
+    sprite2::Rect,
+};
+use bytemuck::{Pod, Zeroable};
+use kayak_core::render_primitive::RenderPrimitive;
+
+use crate::{
+    render::{text::TEXT_SHADER_HANDLE, ui_pass::TransparentUI},
+    to_bevy_color, BevyContext,
+};
+
+use super::{font::KayakFont, font_mapping::FontMapping, font_texture_cache::FontTextureCache};
+
+pub struct TextPipeline {
+    view_layout: BindGroupLayout,
+    pub(crate) image_layout: BindGroupLayout,
+    pipeline: CachedPipelineId,
+}
+
+const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
+    const_vec3!([0.0, 0.0, 0.0]),
+    const_vec3!([1.0, 1.0, 0.0]),
+    const_vec3!([0.0, 1.0, 0.0]),
+    const_vec3!([0.0, 0.0, 0.0]),
+    const_vec3!([1.0, 0.0, 0.0]),
+    const_vec3!([1.0, 1.0, 0.0]),
+];
+
+impl FromWorld for TextPipeline {
+    fn from_world(world: &mut World) -> Self {
+        let world = world.cell();
+        let render_device = world.get_resource::<RenderDevice>().unwrap();
+        let mut pipeline_cache = world.get_resource_mut::<RenderPipelineCache>().unwrap();
+
+        let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
+            entries: &[BindGroupLayoutEntry {
+                binding: 0,
+                visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                ty: BindingType::Buffer {
+                    ty: BufferBindingType::Uniform,
+                    has_dynamic_offset: true,
+                    // TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
+                    // Context: https://github.com/LPGhatguy/crevice/issues/29
+                    min_binding_size: BufferSize::new(144),
+                },
+                count: None,
+            }],
+            label: Some("text_view_layout"),
+        });
+
+        let image_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
+            entries: &[
+                BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: ShaderStages::FRAGMENT,
+                    ty: BindingType::Texture {
+                        multisampled: false,
+                        sample_type: TextureSampleType::Float { filterable: false },
+                        view_dimension: TextureViewDimension::D2Array,
+                    },
+                    count: None,
+                },
+                BindGroupLayoutEntry {
+                    binding: 1,
+                    visibility: ShaderStages::FRAGMENT,
+                    ty: BindingType::Sampler {
+                        comparison: false,
+                        filtering: true,
+                    },
+                    count: None,
+                },
+            ],
+            label: Some("text_image_layout"),
+        });
+
+        let vertex_buffer_layout = VertexBufferLayout {
+            array_stride: 40,
+            step_mode: VertexStepMode::Vertex,
+            attributes: vec![
+                VertexAttribute {
+                    format: VertexFormat::Float32x3,
+                    offset: 0,
+                    shader_location: 0,
+                },
+                VertexAttribute {
+                    format: VertexFormat::Float32x4,
+                    offset: 12,
+                    shader_location: 1,
+                },
+                VertexAttribute {
+                    format: VertexFormat::Float32x3,
+                    offset: 28,
+                    shader_location: 2,
+                },
+            ],
+        };
+
+        let pipeline_desc = RenderPipelineDescriptor {
+            vertex: VertexState {
+                shader: TEXT_SHADER_HANDLE.typed::<Shader>(),
+                entry_point: "vertex".into(),
+                shader_defs: vec![],
+                buffers: vec![vertex_buffer_layout],
+            },
+            fragment: Some(FragmentState {
+                shader: TEXT_SHADER_HANDLE.typed::<Shader>(),
+                shader_defs: vec![],
+                entry_point: "fragment".into(),
+                targets: vec![ColorTargetState {
+                    format: TextureFormat::bevy_default(),
+                    blend: Some(BlendState {
+                        color: BlendComponent {
+                            src_factor: BlendFactor::SrcAlpha,
+                            dst_factor: BlendFactor::OneMinusSrcAlpha,
+                            operation: BlendOperation::Add,
+                        },
+                        alpha: BlendComponent {
+                            src_factor: BlendFactor::One,
+                            dst_factor: BlendFactor::One,
+                            operation: BlendOperation::Add,
+                        },
+                    }),
+                    write_mask: ColorWrites::ALL,
+                }],
+            }),
+            layout: Some(vec![view_layout.clone(), image_layout.clone()]),
+            primitive: PrimitiveState {
+                front_face: FrontFace::Ccw,
+                cull_mode: None,
+                polygon_mode: PolygonMode::Fill,
+                clamp_depth: false,
+                conservative: false,
+                topology: PrimitiveTopology::TriangleList,
+                strip_index_format: None,
+            },
+            depth_stencil: Some(DepthStencilState {
+                format: TextureFormat::Depth32Float,
+                depth_write_enabled: false,
+                depth_compare: CompareFunction::Greater,
+                stencil: StencilState {
+                    front: StencilFaceState::IGNORE,
+                    back: StencilFaceState::IGNORE,
+                    read_mask: 0,
+                    write_mask: 0,
+                },
+                bias: DepthBiasState {
+                    constant: 0,
+                    slope_scale: 0.0,
+                    clamp: 0.0,
+                },
+            }),
+            multisample: MultisampleState {
+                count: 1,
+                mask: !0,
+                alpha_to_coverage_enabled: false,
+            },
+            label: Some("text_pipeline".into()),
+        };
+
+        TextPipeline {
+            pipeline: pipeline_cache.queue(pipeline_desc),
+            view_layout,
+            image_layout,
+        }
+    }
+}
+
+
+
+#[repr(C)]
+#[derive(Copy, Clone, Pod, Zeroable)]
+struct TextVertex {
+    pub position: [f32; 3],
+    pub color: [f32; 4],
+    pub uv: [f32; 3],
+}
+
+pub struct TextMeta {
+    vertices: BufferVec<TextVertex>,
+    view_bind_group: Option<BindGroup>,
+}
+
+impl Default for TextMeta {
+    fn default() -> Self {
+        Self {
+            vertices: BufferVec::new(BufferUsages::VERTEX),
+            view_bind_group: None,
+        }
+    }
+}
+
+pub fn prepare_texts(
+    render_device: Res<RenderDevice>,
+    render_queue: Res<RenderQueue>,
+    mut sprite_meta: ResMut<TextMeta>,
+    mut extracted_sprites: Query<&mut ExtractedText>,
+) {
+    let extracted_sprite_len = extracted_sprites.iter_mut().len();
+    // dont create buffers when there are no texts
+    if extracted_sprite_len == 0 {
+        return;
+    }
+
+    sprite_meta.vertices.clear();
+    sprite_meta.vertices.reserve(
+        extracted_sprite_len * QUAD_VERTEX_POSITIONS.len(),
+        &render_device,
+    );
+
+    for (i, mut extracted_sprite) in extracted_sprites.iter_mut().enumerate() {
+        let sprite_rect = extracted_sprite.rect;
+        let color = extracted_sprite.background_color.as_linear_rgba_f32();
+
+        let bottom_left = Vec3::new(0.0, 1.0, extracted_sprite.char_id as f32);
+        let top_left = Vec3::new(0.0, 0.0, extracted_sprite.char_id as f32);
+        let top_right = Vec3::new(1.0, 0.0, extracted_sprite.char_id as f32);
+        let bottom_right = Vec3::new(1.0, 1.0, extracted_sprite.char_id as f32);
+
+        let uvs: [[f32; 3]; 6] = [
+            bottom_left.into(),
+            top_right.into(),
+            top_left.into(),
+            bottom_left.into(),
+            bottom_right.into(),
+            top_right.into(),
+        ];
+
+        extracted_sprite.vertex_index = i;
+        for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() {
+            let world = Mat4::from_scale_rotation_translation(
+                sprite_rect.size().extend(1.0),
+                Quat::default(),
+                sprite_rect.min.extend(extracted_sprite.z_index),
+            );
+            let final_position = (world * Vec3::from(*vertex_position).extend(1.0)).truncate();
+            sprite_meta.vertices.push(TextVertex {
+                position: final_position.into(),
+                color,
+                uv: uvs[index],
+            });
+        }
+    }
+    sprite_meta
+        .vertices
+        .write_buffer(&render_device, &render_queue);
+}
+
+pub fn queue_texts(
+    draw_functions: Res<DrawFunctions<TransparentUI>>,
+    render_device: Res<RenderDevice>,
+    mut sprite_meta: ResMut<TextMeta>,
+    view_uniforms: Res<ViewUniforms>,
+    text_pipeline: Res<TextPipeline>,
+    mut extracted_sprites: Query<(Entity, &ExtractedText)>,
+    mut views: Query<&mut RenderPhase<TransparentUI>>,
+) {
+    if let Some(view_binding) = view_uniforms.uniforms.binding() {
+        sprite_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor {
+            entries: &[BindGroupEntry {
+                binding: 0,
+                resource: view_binding,
+            }],
+            label: Some("text_view_bind_group"),
+            layout: &text_pipeline.view_layout,
+        }));
+        let draw_text = draw_functions.read().get_id::<DrawText>().unwrap();
+        for mut transparent_phase in views.iter_mut() {
+            for (entity, _) in extracted_sprites.iter_mut() {
+                transparent_phase.add(TransparentUI {
+                    draw_function: draw_text,
+                    pipeline: text_pipeline.pipeline,
+                    entity,
+                    sort_key: FloatOrd(0.0),
+                });
+            }
+        }
+    }
+}
+
+pub struct DrawText {
+    params: SystemState<(
+        SRes<TextMeta>,
+        SRes<RenderPipelineCache>,
+        SRes<FontTextureCache>,
+        SQuery<Read<ViewUniformOffset>>,
+        SQuery<Read<ExtractedText>>,
+    )>,
+}
+
+impl DrawText {
+    pub fn new(world: &mut World) -> Self {
+        Self {
+            params: SystemState::new(world),
+        }
+    }
+}
+
+impl Draw<TransparentUI> for DrawText {
+    fn draw<'w>(
+        &mut self,
+        world: &'w World,
+        pass: &mut TrackedRenderPass<'w>,
+        view: Entity,
+        item: &TransparentUI,
+    ) {
+        let (text_meta, pipelines, font_texture_cache, views, texts) = self.params.get(world);
+        let view_uniform = views.get(view).unwrap();
+        let text_meta = text_meta.into_inner();
+        let extracted_text = texts.get(item.entity).unwrap();
+        if let Some(pipeline) = pipelines.into_inner().get(item.pipeline) {
+            pass.set_render_pipeline(pipeline);
+            pass.set_vertex_buffer(0, text_meta.vertices.buffer().unwrap().slice(..));
+            pass.set_bind_group(
+                0,
+                text_meta.view_bind_group.as_ref().unwrap(),
+                &[view_uniform.offset],
+            );
+
+            if let Some(image_bindings) = font_texture_cache
+                .into_inner()
+                .bind_groups
+                .get(&extracted_text.font_handle)
+            {
+                pass.set_bind_group(1, image_bindings, &[]);
+
+                pass.draw(
+                    (extracted_text.vertex_index * QUAD_VERTEX_POSITIONS.len()) as u32
+                        ..((extracted_text.vertex_index + 1) * QUAD_VERTEX_POSITIONS.len()) as u32,
+                    0..1,
+                );
+            }
+        }
+    }
+}
diff --git a/bevy_kayak_ui/src/render/text/shader.wgsl b/bevy_kayak_ui/src/render/text/shader.wgsl
new file mode 100644
index 0000000000000000000000000000000000000000..5a2b1ec6afcc802661957767a5269cc4db9732c4
--- /dev/null
+++ b/bevy_kayak_ui/src/render/text/shader.wgsl
@@ -0,0 +1,45 @@
+[[block]]
+struct View {
+    view_proj: mat4x4<f32>;
+    world_position: vec3<f32>;
+};
+[[group(0), binding(0)]]
+var<uniform> view: View;
+
+struct VertexOutput {
+    [[builtin(position)]] position: vec4<f32>;
+    [[location(0)]] color: vec4<f32>;
+    [[location(1)]] uv: vec3<f32>;
+};
+
+[[stage(vertex)]]
+fn vertex(
+    [[location(0)]] vertex_position: vec3<f32>,
+    [[location(1)]] vertex_color: vec4<f32>,
+    [[location(2)]] vertex_uv: vec3<f32>,
+) -> VertexOutput {
+    var out: VertexOutput;
+    out.color = vertex_color;
+    out.uv = vertex_uv;
+    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
+    return out;
+}
+
+[[group(1), binding(0)]]
+var sprite_texture: texture_2d_array<f32>;
+[[group(1), binding(1)]]
+var sprite_sampler: sampler;
+
+let RADIUS: f32 = 0.5;
+
+[[stage(fragment)]]
+fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> {
+    var x = textureSample(sprite_texture, sprite_sampler, in.uv.xy, i32(in.uv.z)); 
+    var v = max(min(x.r, x.g), min(max(x.r, x.g), x.b));
+    var c = v; //remap(v);
+
+    var v2 = c / fwidth( c );
+    var a = v2 + RADIUS; //clamp( v2 + RADIUS, 0.0, 1.0 );
+
+    return vec4<f32>(in.color.rgb, a);
+}
\ No newline at end of file
diff --git a/bevy_kayak_ui/src/render/ui_pass.rs b/bevy_kayak_ui/src/render/ui_pass.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d04383c8aa4f42ecc40b632f7a5f29d83ac1e7e8
--- /dev/null
+++ b/bevy_kayak_ui/src/render/ui_pass.rs
@@ -0,0 +1,102 @@
+use bevy::core::FloatOrd;
+use bevy::core_pipeline::ClearColor;
+use bevy::ecs::prelude::*;
+use bevy::render2::render_phase::{DrawFunctionId, PhaseItem};
+use bevy::render2::render_resource::{CachedPipelineId, RenderPassColorAttachment};
+use bevy::render2::{
+    render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
+    render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
+    render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor},
+    renderer::RenderContext,
+    view::{ExtractedView, ViewDepthTexture, ViewTarget},
+};
+
+pub struct TransparentUI {
+    pub sort_key: FloatOrd,
+    pub entity: Entity,
+    pub pipeline: CachedPipelineId,
+    pub draw_function: DrawFunctionId,
+}
+
+impl PhaseItem for TransparentUI {
+    type SortKey = FloatOrd;
+
+    #[inline]
+    fn sort_key(&self) -> Self::SortKey {
+        self.sort_key
+    }
+
+    #[inline]
+    fn draw_function(&self) -> DrawFunctionId {
+        self.draw_function
+    }
+}
+
+pub struct MainPassUINode {
+    query:
+        QueryState<(&'static RenderPhase<TransparentUI>, &'static ViewTarget), With<ExtractedView>>,
+}
+
+impl MainPassUINode {
+    pub const IN_VIEW: &'static str = "view";
+
+    pub fn new(world: &mut World) -> Self {
+        Self {
+            query: QueryState::new(world),
+        }
+    }
+}
+
+impl Node for MainPassUINode {
+    fn input(&self) -> Vec<SlotInfo> {
+        vec![SlotInfo::new(MainPassUINode::IN_VIEW, SlotType::Entity)]
+    }
+
+    fn update(&mut self, world: &mut World) {
+        self.query.update_archetypes(world);
+    }
+
+    fn run(
+        &self,
+        graph: &mut RenderGraphContext,
+        render_context: &mut RenderContext,
+        world: &World,
+    ) -> Result<(), NodeRunError> {
+        let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
+        let (transparent_phase, target) = self
+            .query
+            .get_manual(world, view_entity)
+            .expect("view entity should exist");
+        let clear_color = world.get_resource::<ClearColor>().unwrap();
+        {
+            let pass_descriptor = RenderPassDescriptor {
+                label: Some("main_transparent_pass_UI"),
+                color_attachments: &[RenderPassColorAttachment {
+                    view: &target.view,
+                    resolve_target: None,
+                    ops: Operations {
+                        load: LoadOp::Clear(clear_color.0.into()),
+                        store: true,
+                    },
+                }],
+                depth_stencil_attachment: None,
+            };
+
+            let draw_functions = world
+                .get_resource::<DrawFunctions<TransparentUI>>()
+                .unwrap();
+
+            let render_pass = render_context
+                .command_encoder
+                .begin_render_pass(&pass_descriptor);
+            let mut draw_functions = draw_functions.write();
+            let mut tracked_pass = TrackedRenderPass::new(render_pass);
+            for item in transparent_phase.items.iter() {
+                let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
+                draw_function.draw(world, &mut tracked_pass, view_entity, item);
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/bevy_kayak_ui/src/render/ui_pass_driver.rs b/bevy_kayak_ui/src/render/ui_pass_driver.rs
new file mode 100644
index 0000000000000000000000000000000000000000..edce5b8932df92bc3ebf014d05cec6394a121975
--- /dev/null
+++ b/bevy_kayak_ui/src/render/ui_pass_driver.rs
@@ -0,0 +1,29 @@
+use bevy::ecs::world::World;
+use bevy::render2::{
+    camera::{CameraPlugin, ExtractedCameraNames},
+    render_graph::{Node, NodeRunError, RenderGraphContext, SlotValue},
+    renderer::RenderContext,
+};
+
+use crate::UICameraBundle;
+
+pub struct UIPassDriverNode;
+
+impl Node for UIPassDriverNode {
+    fn run(
+        &self,
+        graph: &mut RenderGraphContext,
+        _render_context: &mut RenderContext,
+        world: &World,
+    ) -> Result<(), NodeRunError> {
+        let extracted_cameras = world.get_resource::<ExtractedCameraNames>().unwrap();
+        if let Some(camera_ui) = extracted_cameras.entities.get(UICameraBundle::UI_CAMERA) {
+            graph.run_sub_graph(
+                super::draw_ui_graph::NAME,
+                vec![SlotValue::Entity(*camera_ui)],
+            )?;
+        }
+
+        Ok(())
+    }
+}
diff --git a/bevy_kayak_ui/src/render/unified/font/extract.rs b/bevy_kayak_ui/src/render/unified/font/extract.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2b553164966ee4fa8287c1ea9a93345544715129
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/font/extract.rs
@@ -0,0 +1,87 @@
+use bevy::{
+    math::Vec2,
+    prelude::{Assets, Bundle, Commands, Handle, Res, ResMut},
+    render2::color::Color,
+    sprite2::Rect,
+};
+use kayak_core::render_primitive::RenderPrimitive;
+
+use crate::{
+    render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType},
+    to_bevy_color, BevyContext,
+};
+
+use super::{font::KayakFont, font_mapping::FontMapping};
+
+pub fn extract_texts(
+    mut commands: Commands,
+    context: Res<BevyContext>,
+    mut fonts: ResMut<Assets<KayakFont>>,
+    font_mapping: Res<FontMapping>,
+) {
+    let render_commands = if let Ok(context) = context.kayak_context.read() {
+        context.widget_manager.build_render_primitives()
+    } else {
+        vec![]
+    };
+
+    let text_commands: Vec<&RenderPrimitive> = render_commands
+        .iter()
+        .filter(|command| matches!(command, RenderPrimitive::Text { .. }))
+        .collect::<Vec<_>>();
+
+    let mut extracted_texts = Vec::new();
+    for render_primitive in text_commands {
+        let (background_color, layout, font_size, content, font) = match render_primitive {
+            RenderPrimitive::Text {
+                color,
+                layout,
+                size,
+                content,
+                font,
+            } => (color, layout, *size, content, *font),
+            _ => panic!(""),
+        };
+
+        let font_handle = font_mapping.get_handle(font).unwrap();
+        let new_chars = {
+            let font = fonts.get(font_handle.clone()).unwrap();
+            font.font.check_chars(content.chars())
+        };
+        // Filter out non-renderable spaces.
+        let new_chars: Vec<_> = new_chars.into_iter().filter(|c| *c != ' ').collect();
+        // Add chars to font.
+        if new_chars.len() > 0 {
+            let font = fonts.get_mut(font_handle.clone()).unwrap();
+            for c in new_chars {
+                dbg!("Adding char: ");
+                dbg!(c);
+                font.font.add_character(c);
+            }
+        }
+        let font = fonts.get(font_handle.clone()).unwrap();
+        let char_layouts = font.font.get_layout(content, font_size);
+
+        for (c, (x, y), (width, height)) in char_layouts {
+            // let size = font.font.get_size(c, font_size);
+            let position_x = layout.posx + x;
+            let position_y = layout.posy + y;
+            extracted_texts.push(ExtractQuadBundle {
+                extracted_quad: ExtractedQuad {
+                    font_handle: Some(font_handle.clone()),
+                    rect: Rect {
+                        min: Vec2::new(position_x, position_y),
+                        max: Vec2::new(position_x + width, position_y + height),
+                    },
+                    color: to_bevy_color(background_color),
+                    vertex_index: 0,
+                    char_id: font.font.get_char_id(c),
+                    z_index: layout.z_index,
+                    quad_type: UIQuadType::Text,
+                    type_index: 0,
+                },
+            });
+        }
+    }
+    commands.spawn_batch(extracted_texts);
+}
diff --git a/bevy_kayak_ui/src/render/unified/font/font.rs b/bevy_kayak_ui/src/render/unified/font/font.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7a4424cdbbe7f29e72b778830d29aae73886ea24
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/font/font.rs
@@ -0,0 +1,8 @@
+use bevy::reflect::TypeUuid;
+use kayak_font::Font;
+
+#[derive(Debug, Clone, TypeUuid)]
+#[uuid = "4fe4732c-6731-49bb-bafc-4690d636b848"]
+pub struct KayakFont {
+    pub font: Font,
+}
diff --git a/bevy_kayak_ui/src/render/unified/font/font_mapping.rs b/bevy_kayak_ui/src/render/unified/font/font_mapping.rs
new file mode 100644
index 0000000000000000000000000000000000000000..eeb405ab60d8c6d6d48ab722c9cf0caaa31a33f1
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/font/font_mapping.rs
@@ -0,0 +1,40 @@
+use bevy::{prelude::Handle, utils::HashMap};
+
+use super::font::KayakFont;
+
+pub struct FontMapping {
+    count: u16,
+    font_ids: HashMap<Handle<KayakFont>, u16>,
+    font_handles: HashMap<u16, Handle<KayakFont>>,
+}
+
+impl Default for FontMapping {
+    fn default() -> Self {
+        Self {
+            count: 0,
+            font_ids: HashMap::default(),
+            font_handles: HashMap::default(),
+        }
+    }
+}
+
+impl FontMapping {
+    pub(crate) fn add(&mut self, handle: Handle<KayakFont>) -> u16 {
+        if !self.font_ids.contains_key(&handle) {
+            let id = self.count;
+            self.font_ids.insert(handle.clone(), id);
+            self.font_handles.insert(id, handle);
+            self.count += 1;
+
+            id
+        } else {
+            *self.font_ids.get(&handle).unwrap()
+        }
+    }
+
+    pub(crate) fn get_handle(&self, id: u16) -> Option<Handle<KayakFont>> {
+        self.font_handles
+            .get(&id)
+            .and_then(|item| Some(item.clone()))
+    }
+}
diff --git a/bevy_kayak_ui/src/render/unified/font/font_texture_cache.rs b/bevy_kayak_ui/src/render/unified/font/font_texture_cache.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d96b6d0c87385e833398f902ea3835465962098f
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/font/font_texture_cache.rs
@@ -0,0 +1,248 @@
+use bevy::{prelude::Handle, render2::{render_resource::{AddressMode, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindingResource, Extent3d, FilterMode, ImageCopyTexture, ImageDataLayout, Origin3d, SamplerDescriptor, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureViewDescriptor, TextureViewDimension}, renderer::{RenderDevice, RenderQueue}, texture::{GpuImage, TextureFormatPixelInfo}}, utils::HashMap};
+
+use crate::render::unified::pipeline::UnifiedPipeline;
+
+use super::font::KayakFont;
+
+pub const MAX_CHARACTERS: u32 = 100;
+
+pub struct FontTextureCache {
+    images: HashMap<Handle<KayakFont>, GpuImage>,
+    pub(crate) bind_groups: HashMap<Handle<KayakFont>, BindGroup>,
+    fonts: HashMap<Handle<KayakFont>, KayakFont>,
+    new_fonts: Vec<Handle<KayakFont>>,
+    updated_fonts: Vec<Handle<KayakFont>>,
+}
+
+impl Default for FontTextureCache {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl FontTextureCache {
+    pub fn new() -> Self {
+        Self {
+            images: HashMap::default(),
+            bind_groups: HashMap::default(),
+            fonts: HashMap::default(),
+            new_fonts: Vec::new(),
+            updated_fonts: Vec::new(),
+        }
+    }
+
+    pub fn add(&mut self, kayak_font_handle: Handle<KayakFont>, font: KayakFont) {
+        if !self.fonts.contains_key(&kayak_font_handle) {
+            self.fonts.insert(kayak_font_handle.clone(), font);
+            self.new_fonts.push(kayak_font_handle);
+        } else {
+            if let Some(old_font) = self.fonts.get_mut(&kayak_font_handle) {
+                *old_font = font;
+                self.updated_fonts.push(kayak_font_handle);
+            }
+        }
+    }
+
+    pub fn process_new(&mut self, device: &RenderDevice, pipeline: &UnifiedPipeline) {
+        let new_fonts = self.new_fonts.drain(..);
+        for kayak_font_handle in new_fonts {
+            if let Some(font) = self.fonts.get(&kayak_font_handle) {
+                Self::create_texture(
+                    &mut self.images,
+                    kayak_font_handle.clone_weak(),
+                    font,
+                    device,
+                );
+
+                let gpu_image = self.images.get(&kayak_font_handle).unwrap();
+
+                // create bind group
+                let binding = device.create_bind_group(&BindGroupDescriptor {
+                    label: Some("text_image_bind_group"),
+                    entries: &[
+                        BindGroupEntry {
+                            binding: 0,
+                            resource: BindingResource::TextureView(&gpu_image.texture_view),
+                        },
+                        BindGroupEntry {
+                            binding: 1,
+                            resource: BindingResource::Sampler(&gpu_image.sampler),
+                        },
+                    ],
+                    layout: &pipeline.image_layout,
+                });
+
+                self.bind_groups.insert(kayak_font_handle, binding);
+            }
+        }
+    }
+
+    pub fn process_updated(&mut self, queue: &RenderQueue) {
+        let updated_fonts = self.updated_fonts.drain(..);
+        for kayak_font_handle in updated_fonts {
+            if let Some(font) = self.fonts.get_mut(&kayak_font_handle) {
+                Self::process_new_chars_into_texture(
+                    &mut self.images,
+                    kayak_font_handle,
+                    font,
+                    queue,
+                );
+            }
+        }
+    }
+
+    fn create_texture(
+        images: &mut HashMap<Handle<KayakFont>, GpuImage>,
+        font_handle: Handle<KayakFont>,
+        font: &KayakFont,
+        device: &RenderDevice,
+    ) {
+        let texture_descriptor = TextureDescriptor {
+            label: Some("font_texture_array"),
+            size: Extent3d {
+                width: font.font.cache.dimensions,
+                height: font.font.cache.dimensions,
+                depth_or_array_layers: MAX_CHARACTERS,
+            },
+            mip_level_count: 1,
+            sample_count: 1,
+            dimension: TextureDimension::D2,
+            format: TextureFormat::Rgba32Float,
+            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
+        };
+
+        let sampler_descriptor = SamplerDescriptor {
+            label: Some("font_texture_array_sampler"),
+            address_mode_u: AddressMode::ClampToEdge,
+            address_mode_v: AddressMode::ClampToEdge,
+            address_mode_w: AddressMode::ClampToEdge,
+            mag_filter: FilterMode::Linear,
+            min_filter: FilterMode::Linear,
+            mipmap_filter: FilterMode::Nearest,
+            lod_min_clamp: 0.0,
+            lod_max_clamp: std::f32::MAX,
+            compare: None,
+            anisotropy_clamp: None,
+            border_color: None,
+        };
+
+        let texture = device.create_texture(&texture_descriptor);
+        let sampler = device.create_sampler(&sampler_descriptor);
+
+        let texture_view = texture.create_view(&TextureViewDescriptor {
+            label: Some("font_texture_array_view"),
+            format: None,
+            dimension: Some(TextureViewDimension::D2Array),
+            aspect: bevy::render2::render_resource::TextureAspect::All,
+            base_mip_level: 0,
+            base_array_layer: 0,
+            mip_level_count: None,
+            array_layer_count: std::num::NonZeroU32::new(MAX_CHARACTERS),
+        });
+
+        let image = GpuImage {
+            texture,
+            sampler,
+            texture_view,
+        };
+
+        images.insert(font_handle, image);
+    }
+
+    pub fn process_new_chars_into_texture(
+        images: &mut HashMap<Handle<KayakFont>, GpuImage>,
+        kayak_font_handle: Handle<KayakFont>,
+        font: &mut KayakFont,
+        queue: &RenderQueue,
+    ) {
+        let size = font.font.cache.dimensions;
+        if let Some(gpu_image) = images.get_mut(&kayak_font_handle) {
+            for (_, id, pixels) in font.font.get_data_to_process() {
+                let format_size = TextureFormat::Rgba32Float.pixel_size();
+                queue.write_texture(
+                    ImageCopyTexture {
+                        texture: &gpu_image.texture,
+                        mip_level: 0,
+                        origin: Origin3d {
+                            x: 0,
+                            y: 0,
+                            z: id as u32,
+                        },
+                        aspect: TextureAspect::All,
+                    },
+                    &pixels,
+                    ImageDataLayout {
+                        offset: 0,
+                        bytes_per_row: Some(
+                            std::num::NonZeroU32::new(size * format_size as u32).unwrap(),
+                        ),
+                        rows_per_image: None,
+                    },
+                    Extent3d {
+                        width: size,
+                        height: size,
+                        depth_or_array_layers: 1,
+                    },
+                );
+            }
+        }
+    }
+
+    pub fn get_empty(
+        device: &RenderDevice,
+        image_layout: &BindGroupLayout,
+    ) -> (GpuImage, BindGroup) {
+        let texture_descriptor = TextureDescriptor {
+            label: Some("font_texture_array"),
+            size: Extent3d {
+                width: 1,
+                height: 1,
+                depth_or_array_layers: MAX_CHARACTERS,
+            },
+            mip_level_count: 1,
+            sample_count: 1,
+            dimension: TextureDimension::D2,
+            format: TextureFormat::Rgba32Float,
+            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
+        };
+
+        let sampler_descriptor = SamplerDescriptor::default();
+
+        let texture = device.create_texture(&texture_descriptor);
+        let sampler = device.create_sampler(&sampler_descriptor);
+
+        let texture_view = texture.create_view(&TextureViewDescriptor {
+            label: Some("font_texture_array_view"),
+            format: None,
+            dimension: Some(TextureViewDimension::D2Array),
+            aspect: bevy::render2::render_resource::TextureAspect::All,
+            base_mip_level: 0,
+            base_array_layer: 0,
+            mip_level_count: None,
+            array_layer_count: std::num::NonZeroU32::new(MAX_CHARACTERS),
+        });
+
+        let image = GpuImage {
+            texture,
+            sampler,
+            texture_view,
+        };
+
+        let binding = device.create_bind_group(&BindGroupDescriptor {
+            label: Some("text_image_bind_group"),
+            entries: &[
+                BindGroupEntry {
+                    binding: 0,
+                    resource: BindingResource::TextureView(&image.texture_view),
+                },
+                BindGroupEntry {
+                    binding: 1,
+                    resource: BindingResource::Sampler(&image.sampler),
+                },
+            ],
+            layout: image_layout,
+        });
+
+        (image, binding)
+    }
+}
diff --git a/bevy_kayak_ui/src/render/unified/font/mod.rs b/bevy_kayak_ui/src/render/unified/font/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e356d38dd34ab007fa251c4b179d900b8656d25c
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/font/mod.rs
@@ -0,0 +1,112 @@
+use bevy::{
+    prelude::{AddAsset, AssetEvent, Assets, Commands, EventReader, Handle, Plugin, Res, ResMut},
+    render2::{
+        renderer::{RenderDevice, RenderQueue},
+        RenderApp, RenderStage,
+    },
+    utils::HashSet,
+};
+
+use self::extract::extract_texts;
+
+use super::pipeline::UnifiedPipeline;
+
+mod extract;
+mod font;
+mod font_mapping;
+mod font_texture_cache;
+
+pub use font::*;
+pub use font_mapping::*;
+pub(crate) use font_texture_cache::FontTextureCache;
+
+#[derive(Default)]
+pub struct TextRendererPlugin;
+
+impl Plugin for TextRendererPlugin {
+    fn build(&self, app: &mut bevy::prelude::App) {
+        app.add_asset::<KayakFont>()
+            .init_resource::<FontMapping>()
+            .add_startup_system(load_fonts);
+
+        let render_app = app.sub_app(RenderApp);
+        render_app.add_system_to_stage(RenderStage::Extract, extract_texts);
+
+        render_app
+            .init_resource::<FontTextureCache>()
+            .init_resource::<ExtractedFonts>()
+            .add_system_to_stage(RenderStage::Extract, extract_fonts)
+            .add_system_to_stage(RenderStage::Prepare, prepare_fonts)
+            .add_system_to_stage(RenderStage::Queue, create_and_update_font_cache_texture);
+    }
+}
+
+#[derive(Default)]
+pub struct ExtractedFonts {
+    pub fonts: Vec<(Handle<KayakFont>, KayakFont)>,
+}
+
+fn load_fonts(mut font_assets: ResMut<Assets<KayakFont>>, mut font_mapping: ResMut<FontMapping>) {
+    let font_bytes = include_bytes!("../../../../../resources/Roboto-Regular.ttf");
+    let font = kayak_font::Font::new(font_bytes, 128);
+
+    let handle = font_assets.add(KayakFont { font });
+    font_mapping.add(handle);
+
+    dbg!("Loaded base font!");
+}
+
+fn extract_fonts(
+    mut commands: Commands,
+    font_assets: Res<Assets<KayakFont>>,
+    mut events: EventReader<AssetEvent<KayakFont>>,
+) {
+    let mut extracted_fonts = ExtractedFonts { fonts: Vec::new() };
+    let mut changed_assets = HashSet::default();
+    let mut removed = Vec::new();
+    for event in events.iter() {
+        match event {
+            AssetEvent::Created { handle } => {
+                changed_assets.insert(handle);
+                dbg!("New font added!");
+            }
+            AssetEvent::Modified { handle } => {
+                changed_assets.insert(handle);
+                dbg!("Font changed!");
+            }
+            AssetEvent::Removed { handle } => {
+                if !changed_assets.remove(handle) {
+                    removed.push(handle.clone_weak());
+                }
+            }
+        }
+    }
+
+    for handle in changed_assets {
+        let font_asset = font_assets.get(handle).unwrap();
+        let font = font_asset.clone();
+
+        extracted_fonts.fonts.push((handle.clone_weak(), font));
+    }
+
+    commands.insert_resource(extracted_fonts);
+}
+
+fn prepare_fonts(
+    mut extracted_fonts: ResMut<ExtractedFonts>,
+    mut font_texture_cache: ResMut<FontTextureCache>,
+) {
+    for (handle, font) in extracted_fonts.fonts.drain(..) {
+        font_texture_cache.add(handle, font);
+    }
+}
+
+fn create_and_update_font_cache_texture(
+    device: Res<RenderDevice>,
+    queue: Res<RenderQueue>,
+    pipeline: Res<UnifiedPipeline>,
+    mut font_texture_cache: ResMut<FontTextureCache>,
+) {
+    font_texture_cache.process_new(&device, &pipeline);
+    font_texture_cache.process_updated(&queue);
+}
diff --git a/bevy_kayak_ui/src/render/unified/mod.rs b/bevy_kayak_ui/src/render/unified/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..37aeb58243aa976470811d76a83d949f8754ca34
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/mod.rs
@@ -0,0 +1,46 @@
+use bevy::{
+    prelude::{Assets, HandleUntyped, Plugin},
+    reflect::TypeUuid,
+    render2::{render_phase::DrawFunctions, render_resource::Shader, RenderApp, RenderStage},
+};
+
+use crate::render::{
+    ui_pass::TransparentUI,
+    unified::pipeline::{DrawUI, QuadMeta, UnifiedPipeline},
+};
+
+pub mod font;
+mod pipeline;
+mod quad;
+
+pub const UNIFIED_SHADER_HANDLE: HandleUntyped =
+    HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 7604018236855288450);
+
+pub struct UnifiedRenderPlugin;
+
+impl Plugin for UnifiedRenderPlugin {
+    fn build(&self, app: &mut bevy::prelude::App) {
+        let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap();
+        let unified_shader = Shader::from_wgsl(include_str!("shader.wgsl"));
+        shaders.set_untracked(UNIFIED_SHADER_HANDLE, unified_shader);
+
+        let render_app = app.sub_app(RenderApp);
+        render_app
+            .init_resource::<UnifiedPipeline>()
+            .init_resource::<QuadMeta>()
+            .add_system_to_stage(RenderStage::Prepare, pipeline::prepare_quads)
+            .add_system_to_stage(RenderStage::Queue, pipeline::queue_quads);
+
+        let draw_quad = DrawUI::new(&mut render_app.world);
+
+        render_app
+            .world
+            .get_resource::<DrawFunctions<TransparentUI>>()
+            .unwrap()
+            .write()
+            .add(draw_quad);
+
+        app.add_plugin(font::TextRendererPlugin)
+            .add_plugin(quad::QuadRendererPlugin);
+    }
+}
diff --git a/bevy_kayak_ui/src/render/unified/pipeline.rs b/bevy_kayak_ui/src/render/unified/pipeline.rs
new file mode 100644
index 0000000000000000000000000000000000000000..043f0c546c58bca1278d9f13d765b9f146b1914d
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/pipeline.rs
@@ -0,0 +1,438 @@
+use bevy::{
+    core::FloatOrd,
+    ecs::system::{
+        lifetimeless::{Read, SQuery, SRes},
+        SystemState,
+    },
+    math::{const_vec3, Mat4, Quat, Vec2, Vec3},
+    prelude::{Bundle, Commands, Entity, FromWorld, Handle, Query, Res, ResMut, World},
+    render2::{
+        color::Color,
+        render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass},
+        render_resource::{
+            BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
+            BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendComponent,
+            BlendFactor, BlendOperation, BlendState, BufferBindingType, BufferSize, BufferUsages,
+            BufferVec, CachedPipelineId, ColorTargetState, ColorWrites, CompareFunction,
+            DepthBiasState, DepthStencilState, DynamicUniformVec, FragmentState, FrontFace,
+            MultisampleState, PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipelineCache,
+            RenderPipelineDescriptor, Shader, ShaderStages, StencilFaceState, StencilState,
+            TextureFormat, TextureSampleType, TextureViewDimension, VertexAttribute,
+            VertexBufferLayout, VertexFormat, VertexState, VertexStepMode,
+        },
+        renderer::{RenderDevice, RenderQueue},
+        texture::{BevyDefault, GpuImage},
+        view::{ViewUniformOffset, ViewUniforms},
+    },
+    sprite2::Rect,
+};
+use bytemuck::{Pod, Zeroable};
+use crevice::std140::AsStd140;
+
+use super::font::{FontTextureCache, KayakFont};
+use super::UNIFIED_SHADER_HANDLE;
+use crate::{render::ui_pass::TransparentUI, BevyContext};
+
+pub struct UnifiedPipeline {
+    view_layout: BindGroupLayout,
+    types_layout: BindGroupLayout,
+    pub(crate) image_layout: BindGroupLayout,
+    pipeline: CachedPipelineId,
+    empty_font_texture: (GpuImage, BindGroup),
+}
+
+const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
+    const_vec3!([0.0, 0.0, 0.0]),
+    const_vec3!([1.0, 1.0, 0.0]),
+    const_vec3!([0.0, 1.0, 0.0]),
+    const_vec3!([0.0, 0.0, 0.0]),
+    const_vec3!([1.0, 0.0, 0.0]),
+    const_vec3!([1.0, 1.0, 0.0]),
+];
+
+impl FromWorld for UnifiedPipeline {
+    fn from_world(world: &mut World) -> Self {
+        let world = world.cell();
+        let render_device = world.get_resource::<RenderDevice>().unwrap();
+        let mut pipeline_cache = world.get_resource_mut::<RenderPipelineCache>().unwrap();
+
+        let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
+            entries: &[BindGroupLayoutEntry {
+                binding: 0,
+                visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                ty: BindingType::Buffer {
+                    ty: BufferBindingType::Uniform,
+                    has_dynamic_offset: true,
+                    // TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
+                    // Context: https://github.com/LPGhatguy/crevice/issues/29
+                    min_binding_size: BufferSize::new(144),
+                },
+                count: None,
+            }],
+            label: Some("ui_view_layout"),
+        });
+
+        let types_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
+            entries: &[BindGroupLayoutEntry {
+                binding: 0,
+                visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                ty: BindingType::Buffer {
+                    ty: BufferBindingType::Uniform,
+                    has_dynamic_offset: true,
+                    // TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
+                    // Context: https://github.com/LPGhatguy/crevice/issues/29
+                    min_binding_size: BufferSize::new(4),
+                },
+                count: None,
+            }],
+            label: Some("ui_types_layout"),
+        });
+
+        let image_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
+            entries: &[
+                BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: ShaderStages::FRAGMENT,
+                    ty: BindingType::Texture {
+                        multisampled: false,
+                        sample_type: TextureSampleType::Float { filterable: false },
+                        view_dimension: TextureViewDimension::D2Array,
+                    },
+                    count: None,
+                },
+                BindGroupLayoutEntry {
+                    binding: 1,
+                    visibility: ShaderStages::FRAGMENT,
+                    ty: BindingType::Sampler {
+                        comparison: false,
+                        filtering: true,
+                    },
+                    count: None,
+                },
+            ],
+            label: Some("text_image_layout"),
+        });
+
+        let vertex_buffer_layout = VertexBufferLayout {
+            array_stride: 40,
+            step_mode: VertexStepMode::Vertex,
+            attributes: vec![
+                VertexAttribute {
+                    format: VertexFormat::Float32x3,
+                    offset: 0,
+                    shader_location: 0,
+                },
+                VertexAttribute {
+                    format: VertexFormat::Float32x4,
+                    offset: 12,
+                    shader_location: 1,
+                },
+                VertexAttribute {
+                    format: VertexFormat::Float32x3,
+                    offset: 28,
+                    shader_location: 2,
+                },
+            ],
+        };
+
+        let empty_font_texture = FontTextureCache::get_empty(&render_device, &image_layout);
+
+        let pipeline_desc = RenderPipelineDescriptor {
+            vertex: VertexState {
+                shader: UNIFIED_SHADER_HANDLE.typed::<Shader>(),
+                entry_point: "vertex".into(),
+                shader_defs: vec![],
+                buffers: vec![vertex_buffer_layout],
+            },
+            fragment: Some(FragmentState {
+                shader: UNIFIED_SHADER_HANDLE.typed::<Shader>(),
+                shader_defs: vec![],
+                entry_point: "fragment".into(),
+                targets: vec![ColorTargetState {
+                    format: TextureFormat::bevy_default(),
+                    blend: Some(BlendState {
+                        color: BlendComponent {
+                            src_factor: BlendFactor::SrcAlpha,
+                            dst_factor: BlendFactor::OneMinusSrcAlpha,
+                            operation: BlendOperation::Add,
+                        },
+                        alpha: BlendComponent {
+                            src_factor: BlendFactor::One,
+                            dst_factor: BlendFactor::One,
+                            operation: BlendOperation::Add,
+                        },
+                    }),
+                    write_mask: ColorWrites::ALL,
+                }],
+            }),
+            layout: Some(vec![
+                view_layout.clone(),
+                image_layout.clone(),
+                types_layout.clone(),
+            ]),
+            primitive: PrimitiveState {
+                front_face: FrontFace::Ccw,
+                cull_mode: None,
+                polygon_mode: PolygonMode::Fill,
+                clamp_depth: false,
+                conservative: false,
+                topology: PrimitiveTopology::TriangleList,
+                strip_index_format: None,
+            },
+            depth_stencil: None,
+            multisample: MultisampleState {
+                count: 1,
+                mask: !0,
+                alpha_to_coverage_enabled: false,
+            },
+            label: Some("quad_pipeline".into()),
+        };
+
+        UnifiedPipeline {
+            pipeline: pipeline_cache.queue(pipeline_desc),
+            view_layout,
+            image_layout,
+            empty_font_texture,
+            types_layout,
+        }
+    }
+}
+
+#[derive(Bundle)]
+pub struct ExtractQuadBundle {
+    pub(crate) extracted_quad: ExtractedQuad,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum UIQuadType {
+    Quad,
+    Text,
+}
+
+pub struct ExtractedQuad {
+    pub rect: Rect,
+    pub color: Color,
+    pub vertex_index: usize,
+    pub char_id: usize,
+    pub z_index: f32,
+    pub font_handle: Option<Handle<KayakFont>>,
+    pub quad_type: UIQuadType,
+    pub type_index: u32,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Pod, Zeroable)]
+struct QuadVertex {
+    pub position: [f32; 3],
+    pub color: [f32; 4],
+    pub uv: [f32; 3],
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, AsStd140)]
+struct QuadType {
+    pub t: i32,
+}
+
+pub struct QuadMeta {
+    vertices: BufferVec<QuadVertex>,
+    view_bind_group: Option<BindGroup>,
+    types_buffer: DynamicUniformVec<QuadType>,
+    types_bind_group: Option<BindGroup>,
+}
+
+impl Default for QuadMeta {
+    fn default() -> Self {
+        Self {
+            vertices: BufferVec::new(BufferUsages::VERTEX),
+            view_bind_group: None,
+            types_buffer: DynamicUniformVec::default(),
+            types_bind_group: None,
+        }
+    }
+}
+
+pub fn prepare_quads(
+    render_device: Res<RenderDevice>,
+    render_queue: Res<RenderQueue>,
+    mut sprite_meta: ResMut<QuadMeta>,
+    mut extracted_quads: Query<&mut ExtractedQuad>,
+) {
+    let extracted_sprite_len = extracted_quads.iter_mut().len();
+    // dont create buffers when there are no quads
+    if extracted_sprite_len == 0 {
+        return;
+    }
+
+    sprite_meta.types_buffer.clear();
+    sprite_meta.types_buffer.reserve(2, &render_device);
+    let quad_type_offset = sprite_meta.types_buffer.push(QuadType { t: 0 });
+    let text_type_offset = sprite_meta.types_buffer.push(QuadType { t: 1 });
+    sprite_meta
+        .types_buffer
+        .write_buffer(&render_device, &render_queue);
+
+    sprite_meta.vertices.clear();
+    sprite_meta.vertices.reserve(
+        extracted_sprite_len * QUAD_VERTEX_POSITIONS.len(),
+        &render_device,
+    );
+
+    for (i, mut extracted_sprite) in extracted_quads.iter_mut().enumerate() {
+        let sprite_rect = extracted_sprite.rect;
+        let color = extracted_sprite.color.as_linear_rgba_f32();
+
+        match extracted_sprite.quad_type {
+            UIQuadType::Quad => extracted_sprite.type_index = quad_type_offset,
+            UIQuadType::Text => extracted_sprite.type_index = text_type_offset,
+        };
+
+        let bottom_left = Vec3::new(0.0, 1.0, extracted_sprite.char_id as f32);
+        let top_left = Vec3::new(0.0, 0.0, extracted_sprite.char_id as f32);
+        let top_right = Vec3::new(1.0, 0.0, extracted_sprite.char_id as f32);
+        let bottom_right = Vec3::new(1.0, 1.0, extracted_sprite.char_id as f32);
+
+        let uvs: [[f32; 3]; 6] = [
+            bottom_left.into(),
+            top_right.into(),
+            top_left.into(),
+            bottom_left.into(),
+            bottom_right.into(),
+            top_right.into(),
+        ];
+
+        extracted_sprite.vertex_index = i;
+        for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() {
+            let world = Mat4::from_scale_rotation_translation(
+                sprite_rect.size().extend(1.0),
+                Quat::default(),
+                sprite_rect.min.extend(0.0),
+            );
+            let final_position = (world * Vec3::from(*vertex_position).extend(1.0)).truncate();
+            sprite_meta.vertices.push(QuadVertex {
+                position: final_position.into(),
+                color,
+                uv: uvs[index],
+            });
+        }
+    }
+    sprite_meta
+        .vertices
+        .write_buffer(&render_device, &render_queue);
+}
+
+pub fn queue_quads(
+    draw_functions: Res<DrawFunctions<TransparentUI>>,
+    render_device: Res<RenderDevice>,
+    mut sprite_meta: ResMut<QuadMeta>,
+    view_uniforms: Res<ViewUniforms>,
+    quad_pipeline: Res<UnifiedPipeline>,
+    mut extracted_sprites: Query<(Entity, &ExtractedQuad)>,
+    mut views: Query<&mut RenderPhase<TransparentUI>>,
+) {
+    if let Some(type_binding) = sprite_meta.types_buffer.binding() {
+        sprite_meta.types_bind_group =
+            Some(render_device.create_bind_group(&BindGroupDescriptor {
+                entries: &[BindGroupEntry {
+                    binding: 0,
+                    resource: type_binding,
+                }],
+                label: Some("quad_type_bind_group"),
+                layout: &quad_pipeline.types_layout,
+            }));
+    }
+
+    if let Some(view_binding) = view_uniforms.uniforms.binding() {
+        sprite_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor {
+            entries: &[BindGroupEntry {
+                binding: 0,
+                resource: view_binding,
+            }],
+            label: Some("quad_view_bind_group"),
+            layout: &quad_pipeline.view_layout,
+        }));
+        let draw_quad = draw_functions.read().get_id::<DrawUI>().unwrap();
+        for mut transparent_phase in views.iter_mut() {
+            for (entity, quad) in extracted_sprites.iter_mut() {
+                transparent_phase.add(TransparentUI {
+                    draw_function: draw_quad,
+                    pipeline: quad_pipeline.pipeline,
+                    entity,
+                    sort_key: FloatOrd(quad.z_index),
+                });
+            }
+        }
+    }
+}
+
+pub struct DrawUI {
+    params: SystemState<(
+        SRes<QuadMeta>,
+        SRes<UnifiedPipeline>,
+        SRes<RenderPipelineCache>,
+        SRes<FontTextureCache>,
+        SQuery<Read<ViewUniformOffset>>,
+        SQuery<Read<ExtractedQuad>>,
+    )>,
+}
+
+impl DrawUI {
+    pub fn new(world: &mut World) -> Self {
+        Self {
+            params: SystemState::new(world),
+        }
+    }
+}
+
+impl Draw<TransparentUI> for DrawUI {
+    fn draw<'w>(
+        &mut self,
+        world: &'w World,
+        pass: &mut TrackedRenderPass<'w>,
+        view: Entity,
+        item: &TransparentUI,
+    ) {
+        let (quad_meta, unified_pipeline, pipelines, font_texture_cache, views, quads) =
+            self.params.get(world);
+        let view_uniform = views.get(view).unwrap();
+        let quad_meta = quad_meta.into_inner();
+        let extracted_quad = quads.get(item.entity).unwrap();
+        if let Some(pipeline) = pipelines.into_inner().get(item.pipeline) {
+            pass.set_render_pipeline(pipeline);
+            pass.set_vertex_buffer(0, quad_meta.vertices.buffer().unwrap().slice(..));
+            pass.set_bind_group(
+                0,
+                quad_meta.view_bind_group.as_ref().unwrap(),
+                &[view_uniform.offset],
+            );
+
+            pass.set_bind_group(
+                2,
+                quad_meta.types_bind_group.as_ref().unwrap(),
+                &[extracted_quad.type_index],
+            );
+
+            if let Some(font_handle) = extracted_quad.font_handle.as_ref() {
+                if let Some(image_bindings) =
+                    font_texture_cache.into_inner().bind_groups.get(font_handle)
+                {
+                    pass.set_bind_group(1, image_bindings, &[]);
+                } else {
+                    pass.set_bind_group(
+                        1,
+                        &unified_pipeline.into_inner().empty_font_texture.1,
+                        &[],
+                    );
+                }
+            } else {
+                pass.set_bind_group(1, &unified_pipeline.into_inner().empty_font_texture.1, &[]);
+            }
+
+            pass.draw(
+                (extracted_quad.vertex_index * QUAD_VERTEX_POSITIONS.len()) as u32
+                    ..((extracted_quad.vertex_index + 1) * QUAD_VERTEX_POSITIONS.len()) as u32,
+                0..1,
+            );
+        }
+    }
+}
diff --git a/bevy_kayak_ui/src/render/unified/quad/extract.rs b/bevy_kayak_ui/src/render/unified/quad/extract.rs
new file mode 100644
index 0000000000000000000000000000000000000000..523ed83e5983a1a9323a1683f601d1504cc0a8ab
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/quad/extract.rs
@@ -0,0 +1,52 @@
+use bevy::{
+    math::Vec2,
+    prelude::{Commands, Res},
+    sprite2::Rect,
+};
+use kayak_core::render_primitive::RenderPrimitive;
+
+use crate::{
+    render::unified::pipeline::{ExtractQuadBundle, ExtractedQuad, UIQuadType},
+    to_bevy_color, BevyContext,
+};
+
+pub fn extract_quads(mut commands: Commands, context: Res<BevyContext>) {
+    let render_commands = if let Ok(context) = context.kayak_context.read() {
+        context.widget_manager.build_render_primitives()
+    } else {
+        vec![]
+    };
+
+    let quad_commands: Vec<&RenderPrimitive> = render_commands
+        .iter()
+        .filter(|command| matches!(command, RenderPrimitive::Quad { .. }))
+        .collect::<Vec<_>>();
+
+    let mut extracted_quads = Vec::new();
+    for render_primitive in quad_commands {
+        let (background_color, layout) = match render_primitive {
+            RenderPrimitive::Quad {
+                background_color,
+                layout,
+            } => (background_color, layout),
+            _ => panic!(""),
+        };
+
+        extracted_quads.push(ExtractQuadBundle {
+            extracted_quad: ExtractedQuad {
+                rect: Rect {
+                    min: Vec2::new(layout.posx, layout.posy),
+                    max: Vec2::new(layout.width, layout.height),
+                },
+                color: to_bevy_color(background_color),
+                vertex_index: 0,
+                char_id: 0,
+                z_index: layout.z_index,
+                font_handle: None,
+                quad_type: UIQuadType::Quad,
+                type_index: 0,
+            },
+        });
+    }
+    commands.spawn_batch(extracted_quads);
+}
diff --git a/bevy_kayak_ui/src/render/unified/quad/mod.rs b/bevy_kayak_ui/src/render/unified/quad/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6d9ba6526f170e2f47d404cf9097e5581f17f3d5
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/quad/mod.rs
@@ -0,0 +1,15 @@
+use bevy::{
+    prelude::Plugin,
+    render2::{RenderApp, RenderStage},
+};
+
+mod extract;
+
+pub struct QuadRendererPlugin;
+
+impl Plugin for QuadRendererPlugin {
+    fn build(&self, app: &mut bevy::prelude::App) {
+        let render_app = app.sub_app(RenderApp);
+        render_app.add_system_to_stage(RenderStage::Extract, extract::extract_quads);
+    }
+}
diff --git a/bevy_kayak_ui/src/render/unified/shader.wgsl b/bevy_kayak_ui/src/render/unified/shader.wgsl
new file mode 100644
index 0000000000000000000000000000000000000000..0032221714c236b71c256a3f3b923a6e1515de11
--- /dev/null
+++ b/bevy_kayak_ui/src/render/unified/shader.wgsl
@@ -0,0 +1,56 @@
+[[block]]
+struct View {
+    view_proj: mat4x4<f32>;
+    world_position: vec3<f32>;
+};
+[[group(0), binding(0)]]
+var<uniform> view: View;
+
+[[block]]
+struct QuadType {
+    t: i32;
+};
+[[group(2), binding(0)]]
+var<uniform> quad_type: QuadType;
+
+
+struct VertexOutput {
+    [[builtin(position)]] position: vec4<f32>;
+    [[location(0)]] color: vec4<f32>;
+    [[location(1)]] uv: vec3<f32>;
+};
+
+[[stage(vertex)]]
+fn vertex(
+    [[location(0)]] vertex_position: vec3<f32>,
+    [[location(1)]] vertex_color: vec4<f32>,
+    [[location(2)]] vertex_uv: vec3<f32>,
+) -> VertexOutput {
+    var out: VertexOutput;
+    out.color = vertex_color;
+    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
+    out.uv = vertex_uv;
+    return out;
+}
+
+[[group(1), binding(0)]]
+var sprite_texture: texture_2d_array<f32>;
+[[group(1), binding(1)]]
+var sprite_sampler: sampler;
+
+let RADIUS: f32 = 0.5;
+
+[[stage(fragment)]]
+fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> {
+    if (quad_type.t == 1) {
+         var x = textureSample(sprite_texture, sprite_sampler, in.uv.xy, i32(in.uv.z)); 
+        var v = max(min(x.r, x.g), min(max(x.r, x.g), x.b));
+        var c = v; //remap(v);
+
+        var v2 = c / fwidth( c );
+        var a = v2 + RADIUS; //clamp( v2 + RADIUS, 0.0, 1.0 );
+
+        return vec4<f32>(in.color.rgb, a);
+    }
+    return in.color;
+}
\ No newline at end of file
diff --git a/cargo.toml b/cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..86833ba5ae799e4fd16fbd0d1789cae2b3d9024d
--- /dev/null
+++ b/cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "kayak_ui"
+version = "0.1.0"
+edition = "2021"
+resolver = "2"
+
+[workspace]
+members = [
+    "kayak_components",
+    "kayak_core",
+    "kayak_render_macros",
+    "kayak_font",
+]
+
+[dependencies]
+kayak_components = { path = "kayak_components" }
+kayak_core = { path = "kayak_core" }
+
+[dev-dependencies]
+bevy = { git = "https://github.com/StarArawn/bevy", rev = "b26f563b13c267ffe1ee801bd71fd40b98a256e7" }
+bevy_kayak_ui = { path = "bevy_kayak_ui" }
diff --git a/examples/bevy.rs b/examples/bevy.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dedfde9891893b0c6eeb1009784d64ecc66d366c
--- /dev/null
+++ b/examples/bevy.rs
@@ -0,0 +1,65 @@
+use bevy::{
+    math::Vec2,
+    prelude::{App as BevyApp, Commands, Res},
+    window::{WindowDescriptor, Windows},
+    PipelinedDefaultPlugins,
+};
+use bevy_kayak_ui::{BevyContext, BevyKayakUIPlugin, UICameraBundle};
+use kayak_components::Window;
+use kayak_core::Index;
+use kayak_ui::components::App;
+use kayak_ui::core::{rsx, widget};
+
+#[widget]
+fn TestState() {
+    let _new_x = {
+        let x = context.create_state(0.0f32).unwrap();
+        *x + 0.1
+    };
+    rsx! {
+        <>
+            <Window position={(50.0, 50.0)} size={(300.0, 300.0)} title={"Window 1".to_string()}>
+                {}
+            </Window>
+            <Window position={(550.0, 50.0)} size={(200.0, 200.0)} title={"Window 2".to_string()}>
+                {}
+            </Window>
+        </>
+    }
+}
+
+fn startup(mut commands: Commands, windows: Res<Windows>) {
+    commands.spawn_bundle(UICameraBundle::new());
+
+    let window_size = if let Some(window) = windows.get_primary() {
+        Vec2::new(window.width(), window.height())
+    } else {
+        panic!("Couldn't find primary window!");
+    };
+
+    let context = BevyContext::new(window_size.x, window_size.y, |styles, context| {
+        // Hack to trick the proc macro for right now..
+        let parent_id: Option<Index> = None;
+        rsx! {
+            <App styles={Some(styles.clone())}>
+                <TestState>{}</TestState>
+            </App>
+        }
+    });
+
+    commands.insert_resource(context);
+}
+
+fn main() {
+    BevyApp::new()
+        .insert_resource(WindowDescriptor {
+            width: 1270.0,
+            height: 720.0,
+            title: String::from("UI Example"),
+            ..Default::default()
+        })
+        .add_plugins(PipelinedDefaultPlugins)
+        .add_plugin(BevyKayakUIPlugin)
+        .add_startup_system(startup)
+        .run();
+}
diff --git a/kayak_components/.gitignore b/kayak_components/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..cdd43adbcc41cc49dcf80afe3af9f0b5e67fee4c
--- /dev/null
+++ b/kayak_components/.gitignore
@@ -0,0 +1,7 @@
+target
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# MSVC Windows builds of rustc generate these, which store debugging information
+*.pdb
\ No newline at end of file
diff --git a/kayak_components/Cargo.toml b/kayak_components/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..4f8fea6fe0dedaa63025911fbfea63bdc272c724
--- /dev/null
+++ b/kayak_components/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "kayak_components"
+version = "0.1.0"
+edition = "2021"
+resolver = "2"
+
+[dependencies]
+kayak_core = { path = "../kayak_core", version = "0.1.0" }
diff --git a/kayak_components/src/app.rs b/kayak_components/src/app.rs
new file mode 100644
index 0000000000000000000000000000000000000000..31167baa8b0d4dca42593a6e21de0231991ce020
--- /dev/null
+++ b/kayak_components/src/app.rs
@@ -0,0 +1,12 @@
+use kayak_core::{rsx, widget, Children};
+
+use kayak_core::derivative::*;
+
+#[widget]
+pub fn App(children: Children) {
+    rsx! {
+        <>
+            {children}
+        </>
+    }
+}
diff --git a/kayak_components/src/background.rs b/kayak_components/src/background.rs
new file mode 100644
index 0000000000000000000000000000000000000000..bd1ad1f895e568a3bb8ef37ecd3c68364e202994
--- /dev/null
+++ b/kayak_components/src/background.rs
@@ -0,0 +1,19 @@
+use kayak_core::{
+    render_command::RenderCommand,
+    rsx,
+    styles::{Style, StyleProp},
+    widget, Children, Fragment,
+};
+
+#[widget]
+pub fn Background(children: Children, styles: Option<Style>) {
+    if styles.is_none() {
+        *styles = Some(Style::default())
+    }
+    styles.as_mut().unwrap().render_command = StyleProp::Value(RenderCommand::Quad);
+    rsx! {
+        <Fragment>
+            {children}
+        </Fragment>
+    }
+}
diff --git a/kayak_components/src/clip.rs b/kayak_components/src/clip.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d5883f67dfc6f053a100564d11dc27df49fa6219
--- /dev/null
+++ b/kayak_components/src/clip.rs
@@ -0,0 +1,19 @@
+use kayak_core::{
+    render_command::RenderCommand,
+    rsx,
+    styles::{Style, StyleProp},
+    widget, Children,
+};
+
+#[widget]
+pub fn Clip(children: Children, styles: Option<Style>) {
+    if styles.is_none() {
+        *styles = Some(Style::default())
+    }
+    styles.as_mut().unwrap().render_command = StyleProp::Value(RenderCommand::Clip);
+    rsx! {
+        <>
+            {children}
+        </>
+    }
+}
diff --git a/kayak_components/src/element.rs b/kayak_components/src/element.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2972b9473ffa462d91f5ea25021ecc51dbf440d0
--- /dev/null
+++ b/kayak_components/src/element.rs
@@ -0,0 +1,10 @@
+use kayak_core::{component, rsx, Render, Update};
+
+#[component]
+pub fn Element<Children: Render + Update + Clone>(children: Children) {
+    rsx! {
+        <>
+            {children}
+        </>
+    }
+}
diff --git a/kayak_components/src/lib.rs b/kayak_components/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9e72b31390ce26c450d96de9655ed159dc0a5355
--- /dev/null
+++ b/kayak_components/src/lib.rs
@@ -0,0 +1,11 @@
+mod app;
+mod background;
+mod clip;
+mod text;
+mod window;
+
+pub use app::*;
+pub use background::*;
+pub use clip::*;
+pub use text::*;
+pub use window::*;
diff --git a/kayak_components/src/text.rs b/kayak_components/src/text.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8fd1a9802f100fbe3278f1df8e0fe7e36aabf30f
--- /dev/null
+++ b/kayak_components/src/text.rs
@@ -0,0 +1,24 @@
+use kayak_core::{
+    render_command::RenderCommand,
+    rsx,
+    styles::{Style, StyleProp},
+    widget,
+};
+
+#[widget]
+pub fn Text(size: f32, content: String, styles: Option<Style>) {
+    let render_command = RenderCommand::Text {
+        content: content.clone(),
+        size: *size,
+        font: 0, // TODO: Support font passing here. Perhaps move to style?
+    };
+    *styles = Some(Style {
+        render_command: StyleProp::Value(render_command),
+        ..styles.clone().unwrap_or_default()
+    });
+    rsx! {
+        <>
+            {}
+        </>
+    }
+}
diff --git a/kayak_components/src/window.rs b/kayak_components/src/window.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3abf6e4b91a1320de1bf3a72b0d9b929bd350440
--- /dev/null
+++ b/kayak_components/src/window.rs
@@ -0,0 +1,72 @@
+use kayak_core::{
+    color::Color,
+    render_command::RenderCommand,
+    rsx,
+    styles::{PositionType, Style, StyleProp, Units},
+    widget, Children, Fragment,
+};
+
+use crate::{Background, Clip, Text};
+
+#[widget]
+pub fn Window(
+    children: Children,
+    styles: Option<Style>,
+    position: (f32, f32),
+    size: (f32, f32),
+    title: String,
+) {
+    // let mut changed_styles = styles.clone().unwrap_or_default();
+    // changed_styles.render_command = RenderCommand::Quad;
+    // changed_styles.position_type = Some(PositionType::Absolute);
+    // changed_styles.background = Some(Color::new(0.0588, 0.0588, 0.588, 1.0));
+    // changed_styles.position = Some(Rect {
+    //     start: Dimension::Points(position.x),
+    //     end: Dimension::Points(position.x + size.width),
+    //     top: Dimension::Points(position.y),
+    //     bottom: Dimension::Points(position.y + size.height),
+    // });
+    // changed_styles.size = Some(Size {
+    //     width: Dimension::Points(size.width),
+    //     height: Dimension::Points(size.height),
+    // });
+    // styles = Some(changed_styles);
+
+    *styles = Some(Style {
+        background_color: StyleProp::Value(Color::new(0.125, 0.125, 0.125, 1.0)),
+        render_command: StyleProp::Value(RenderCommand::Quad),
+        position_type: StyleProp::Value(PositionType::SelfDirected),
+        left: StyleProp::Value(Units::Pixels(position.0)),
+        top: StyleProp::Value(Units::Pixels(position.1)),
+        width: StyleProp::Value(Units::Pixels(size.0)),
+        height: StyleProp::Value(Units::Pixels(size.1)),
+        ..styles.clone().unwrap_or_default()
+    });
+
+    let title_background_styles = Style {
+        background_color: StyleProp::Value(Color::new(0.0781, 0.0898, 0.101, 1.0)),
+        height: StyleProp::Value(Units::Pixels(24.0)),
+        ..Style::default()
+    };
+
+    let title_text_styles = Style {
+        position_type: StyleProp::Value(PositionType::SelfDirected),
+        top: StyleProp::Value(Units::Pixels(-22.0)),
+        left: StyleProp::Value(Units::Pixels(5.0)),
+        ..Style::default()
+    };
+
+    let title = title.clone();
+    rsx! {
+        <Fragment>
+            <Clip>
+                <Background styles={Some(title_background_styles)}>
+                    <Text styles={Some(title_text_styles)} size={14.0} content={title}>{}</Text>
+                </Background>
+            </Clip>
+            <Clip>
+                {children}
+            </Clip>
+        </Fragment>
+    }
+}
diff --git a/kayak_core/cargo.toml b/kayak_core/cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..1f1eb406add78c7fc40e27691abdd060d8716e45
--- /dev/null
+++ b/kayak_core/cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "kayak_core"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+as-any = "0.2"
+diff-struct = "0.3"
+derivative = "2.2"
+fontdue = "0.6"
+
+kayak_render_macros = { path = "../kayak_render_macros" }
+morphorm = { git = "https://github.com/geom3trik/morphorm" }
+# dyn_partial_eq = "0.1"
+resources = "1.1"
diff --git a/kayak_core/examples/test3.rs b/kayak_core/examples/test3.rs
new file mode 100644
index 0000000000000000000000000000000000000000..34079bcc18ff2f0b476db53211570a431fc3ba87
--- /dev/null
+++ b/kayak_core/examples/test3.rs
@@ -0,0 +1,63 @@
+use kayak_core::color::Color;
+use kayak_core::context::KayakContext;
+use kayak_core::render_command::RenderCommand;
+use kayak_core::styles::{Style, StyleProp};
+use kayak_core::{rsx, widget, Children, Index};
+use morphorm::{PositionType, Units};
+
+#[widget]
+fn MyWidget(context: &mut KayakContext, children: Children) {
+    let number = *context.create_state::<u32>(0).unwrap();
+    let my_styles = Style {
+        render_command: StyleProp::Value(RenderCommand::Quad),
+        width: StyleProp::Value(Units::Pixels(300.0)),
+        height: StyleProp::Value(Units::Pixels(300.0)),
+        background_color: StyleProp::Value(Color::BLACK),
+        ..Style::default()
+    };
+    rsx! {
+        <MyWidget2 styles={Some(my_styles)} test={number}>
+            {children}
+        </MyWidget2>
+    }
+}
+
+#[widget]
+fn MyWidget2(test: u32, children: Children) {
+    dbg!(test);
+    rsx! {
+        <>
+            {children}
+        </>
+    }
+}
+
+fn main() {
+    let mut context = KayakContext::new();
+
+    let my_widget = MyWidget {
+        id: Index::default(),
+        children: None,
+        styles: Some(Style {
+            position_type: StyleProp::Value(PositionType::SelfDirected),
+            width: StyleProp::Value(Units::Pixels(1280.0)),
+            height: StyleProp::Value(Units::Pixels(720.0)),
+            ..Style::default()
+        }),
+    };
+
+    let (_, widget_id) = context.widget_manager.create_widget(0, my_widget, None);
+
+    let mut my_widget = context.widget_manager.take(widget_id);
+    my_widget.render(&mut context);
+    context.set_current_id(widget_id);
+    context.set_state::<u32>(1);
+    my_widget.render(&mut context);
+    context.widget_manager.repossess(my_widget);
+
+    context.widget_manager.render();
+
+    context.widget_manager.calculate_layout();
+
+    dbg!(context.widget_manager.build_render_primitives());
+}
diff --git a/kayak_core/src/color.rs b/kayak_core/src/color.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b71a107f2f4f7e529d7953a2326871a18c3316a2
--- /dev/null
+++ b/kayak_core/src/color.rs
@@ -0,0 +1,48 @@
+/// A color in the sRGB color space.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Color {
+    /// Red component, 0.0 - 1.0
+    pub r: f32,
+    /// Green component, 0.0 - 1.0
+    pub g: f32,
+    /// Blue component, 0.0 - 1.0
+    pub b: f32,
+    /// Transparency, 0.0 - 1.0
+    pub a: f32,
+}
+
+impl Default for Color {
+    fn default() -> Self {
+        Self::WHITE
+    }
+}
+
+impl Color {
+    /// The black color.
+    pub const BLACK: Color = Color {
+        r: 0.0,
+        g: 0.0,
+        b: 0.0,
+        a: 1.0,
+    };
+
+    /// The white color.
+    pub const WHITE: Color = Color {
+        r: 1.0,
+        g: 1.0,
+        b: 1.0,
+        a: 1.0,
+    };
+
+    /// A color with no opacity.
+    pub const TRANSPARENT: Color = Color {
+        r: 0.0,
+        g: 0.0,
+        b: 0.0,
+        a: 0.0,
+    };
+
+    pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
+        Self { r, g, b, a }
+    }
+}
diff --git a/kayak_core/src/context.rs b/kayak_core/src/context.rs
new file mode 100644
index 0000000000000000000000000000000000000000..74cd09b3f76340ca262933350cc84a0fcf7f92c1
--- /dev/null
+++ b/kayak_core/src/context.rs
@@ -0,0 +1,102 @@
+use std::collections::HashMap;
+
+use resources::Ref;
+
+use crate::widget_manager::WidgetManager;
+
+pub struct KayakContext {
+    component_states: HashMap<crate::Index, resources::Resources>,
+    current_id: crate::Index,
+    pub widget_manager: WidgetManager,
+}
+
+impl std::fmt::Debug for KayakContext {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("KayakContext")
+            .field("current_id", &self.current_id)
+            .finish()
+    }
+}
+
+impl KayakContext {
+    pub fn new() -> Self {
+        Self {
+            component_states: HashMap::new(),
+            current_id: crate::Index::default(),
+            widget_manager: WidgetManager::new(),
+        }
+    }
+
+    pub fn set_current_id(&mut self, id: crate::Index) {
+        self.current_id = id;
+    }
+
+    pub fn create_state<T: resources::Resource + Clone>(
+        &mut self,
+        initial_state: T,
+    ) -> Option<Ref<T>> {
+        if self.component_states.contains_key(&self.current_id) {
+            let states = self.component_states.get_mut(&self.current_id).unwrap();
+            if !states.contains::<T>() {
+                states.insert(initial_state);
+            }
+        } else {
+            let mut states = resources::Resources::default();
+            states.insert(initial_state);
+            self.component_states.insert(self.current_id, states);
+        }
+        return self.get_state();
+    }
+
+    fn get_state<T: resources::Resource + Clone>(&self) -> Option<Ref<T>> {
+        if self.component_states.contains_key(&self.current_id) {
+            let states = self.component_states.get(&self.current_id).unwrap();
+            if let Ok(state) = states.get::<T>() {
+                return Some(state);
+            }
+        }
+        return None;
+    }
+
+    pub fn set_state<T: resources::Resource + Clone>(&mut self, state: T) {
+        if self.component_states.contains_key(&self.current_id) {
+            let states = self.component_states.get(&self.current_id).unwrap();
+            if states.contains::<T>() {
+                let mut mutate_t = states.get_mut::<T>().unwrap();
+                self.widget_manager.dirty_nodes.push(self.current_id);
+                *mutate_t = state;
+            } else {
+                panic!(
+                    "No specific state created for component with id: {:?}!",
+                    self.current_id
+                );
+            }
+        } else {
+            panic!(
+                "No state created for component with id: {:?}!",
+                self.current_id
+            );
+        }
+    }
+
+    pub fn render(&mut self) {
+        let dirty_nodes = self.widget_manager.dirty_nodes.clone();
+        for node_index in dirty_nodes {
+            if self
+                .widget_manager
+                .dirty_nodes
+                .iter()
+                .any(|dirty_index| node_index == *dirty_index)
+            {
+                let mut widget = self.widget_manager.take(node_index);
+                widget.render(self);
+                self.widget_manager.repossess(widget);
+            }
+        }
+
+        self.widget_manager.dirty_nodes.clear();
+
+        self.widget_manager.render();
+        self.widget_manager.calculate_layout();
+    }
+}
diff --git a/kayak_core/src/fragment.rs b/kayak_core/src/fragment.rs
new file mode 100644
index 0000000000000000000000000000000000000000..837a5e6145f6ca947ef8d11e824f885226284248
--- /dev/null
+++ b/kayak_core/src/fragment.rs
@@ -0,0 +1,33 @@
+use derivative::*;
+
+use crate::{context::KayakContext, styles::Style, Index, Widget};
+
+#[derive(Derivative)]
+#[derivative(Debug, PartialEq)]
+pub struct Fragment {
+    pub id: Index,
+    #[derivative(Debug = "ignore", PartialEq = "ignore")]
+    pub children: crate::Children,
+    pub styles: Option<Style>,
+}
+
+impl Widget for Fragment {
+    fn get_id(&self) -> Index {
+        self.id
+    }
+
+    fn set_id(&mut self, id: Index) {
+        self.id = id;
+    }
+
+    fn get_styles(&self) -> Option<Style> {
+        self.styles.clone()
+    }
+
+    fn render(&mut self, context: &mut KayakContext) {
+        dbg!("Rendering fragment children!");
+        if let Some(children) = self.children.as_ref() {
+            children(Some(self.get_id()), context);
+        }
+    }
+}
diff --git a/kayak_core/src/generational_arena.rs b/kayak_core/src/generational_arena.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e363ea592505460321d5968ea98ca9d9652147bd
--- /dev/null
+++ b/kayak_core/src/generational_arena.rs
@@ -0,0 +1,1326 @@
+/*!
+[![](https://docs.rs/generational-arena/badge.svg)](https://docs.rs/generational-arena/)
+[![](https://img.shields.io/crates/v/generational-arena.svg)](https://crates.io/crates/generational-arena)
+[![](https://img.shields.io/crates/d/generational-arena.svg)](https://crates.io/crates/generational-arena)
+[![Travis CI Build Status](https://travis-ci.org/fitzgen/generational-arena.svg?branch=master)](https://travis-ci.org/fitzgen/generational-arena)
+A safe arena allocator that allows deletion without suffering from [the ABA
+problem](https://en.wikipedia.org/wiki/ABA_problem) by using generational
+indices.
+Inspired by [Catherine West's closing keynote at RustConf
+2018](https://www.youtube.com/watch?v=aKLntZcp27M), where these ideas (and many
+more!) were presented in the context of an Entity-Component-System for games
+programming.
+## What? Why?
+Imagine you are working with a graph and you want to add and delete individual
+nodes at a time, or you are writing a game and its world consists of many
+inter-referencing objects with dynamic lifetimes that depend on user
+input. These are situations where matching Rust's ownership and lifetime rules
+can get tricky.
+It doesn't make sense to use shared ownership with interior mutability (i.e.
+`Rc<RefCell<T>>` or `Arc<Mutex<T>>`) nor borrowed references (ie `&'a T` or `&'a
+mut T`) for structures. The cycles rule out reference counted types, and the
+required shared mutability rules out borrows. Furthermore, lifetimes are dynamic
+and don't follow the borrowed-data-outlives-the-borrower discipline.
+In these situations, it is tempting to store objects in a `Vec<T>` and have them
+reference each other via their indices. No more borrow checker or ownership
+problems! Often, this solution is good enough.
+However, now we can't delete individual items from that `Vec<T>` when we no
+longer need them, because we end up either
+* messing up the indices of every element that follows the deleted one, or
+* suffering from the [ABA
+  problem](https://en.wikipedia.org/wiki/ABA_problem). To elaborate further, if
+  we tried to replace the `Vec<T>` with a `Vec<Option<T>>`, and delete an
+  element by setting it to `None`, then we create the possibility for this buggy
+  sequence:
+    * `obj1` references `obj2` at index `i`
+    * someone else deletes `obj2` from index `i`, setting that element to `None`
+    * a third thing allocates `obj3`, which ends up at index `i`, because the
+      element at that index is `None` and therefore available for allocation
+    * `obj1` attempts to get `obj2` at index `i`, but incorrectly is given
+      `obj3`, when instead the get should fail.
+By introducing a monotonically increasing generation counter to the collection,
+associating each element in the collection with the generation when it was
+inserted, and getting elements from the collection with the *pair* of index and
+the generation at the time when the element was inserted, then we can solve the
+aforementioned ABA problem. When indexing into the collection, if the index
+pair's generation does not match the generation of the element at that index,
+then the operation fails.
+## Features
+* Zero `unsafe`
+* Well tested, including quickchecks
+* `no_std` compatibility
+* All the trait implementations you expect: `IntoIterator`, `FromIterator`,
+  `Extend`, etc...
+## Usage
+First, add `generational-arena` to your `Cargo.toml`:
+```toml
+[dependencies]
+generational-arena = "0.2"
+```
+Then, import the crate and use the
+[`generational_arena::Arena`](./struct.Arena.html) type!
+```rust
+extern crate generational_arena;
+use generational_arena::Arena;
+let mut arena = Arena::new();
+// Insert some elements into the arena.
+let rza = arena.insert("Robert Fitzgerald Diggs");
+let gza = arena.insert("Gary Grice");
+let bill = arena.insert("Bill Gates");
+// Inserted elements can be accessed infallibly via indexing (and missing
+// entries will panic).
+assert_eq!(arena[rza], "Robert Fitzgerald Diggs");
+// Alternatively, the `get` and `get_mut` methods provide fallible lookup.
+if let Some(genius) = arena.get(gza) {
+    println!("The gza gza genius: {}", genius);
+}
+if let Some(val) = arena.get_mut(bill) {
+    *val = "Bill Gates doesn't belong in this set...";
+}
+// We can remove elements.
+arena.remove(bill);
+// Insert a new one.
+let murray = arena.insert("Bill Murray");
+// The arena does not contain `bill` anymore, but it does contain `murray`, even
+// though they are almost certainly at the same index within the arena in
+// practice. Ambiguities are resolved with an associated generation tag.
+assert!(!arena.contains(bill));
+assert!(arena.contains(murray));
+// Iterate over everything inside the arena.
+for (idx, value) in &arena {
+    println!("{:?} is at {:?}", value, idx);
+}
+```
+## `no_std`
+To enable `no_std` compatibility, disable the on-by-default "std" feature.
+```toml
+[dependencies]
+generational-arena = { version = "0.2", default-features = false }
+```
+### Serialization and Deserialization with [`serde`](https://crates.io/crates/serde)
+To enable serialization/deserialization support, enable the "serde" feature.
+```toml
+[dependencies]
+generational-arena = { version = "0.2", features = ["serde"] }
+```
+ */
+
+#![forbid(unsafe_code, missing_docs, missing_debug_implementations)]
+
+use std::vec::{self, Vec};
+
+use core::cmp;
+use core::iter::{self, Extend, FromIterator, FusedIterator};
+use core::mem;
+use core::ops;
+use core::slice;
+
+#[cfg(feature = "serde")]
+mod serde_impl;
+
+/// The `Arena` allows inserting and removing elements that are referred to by
+/// `Index`.
+///
+/// [See the module-level documentation for example usage and motivation.](./index.html)
+#[derive(Clone, Debug)]
+pub struct Arena<T> {
+    items: Vec<Entry<T>>,
+    generation: u64,
+    free_list_head: Option<usize>,
+    len: usize,
+}
+
+#[derive(Clone, Debug)]
+enum Entry<T> {
+    Free { next_free: Option<usize> },
+    Occupied { generation: u64, value: T },
+}
+
+/// An index (and generation) into an `Arena`.
+///
+/// To get an `Index`, insert an element into an `Arena`, and the `Index` for
+/// that element will be returned.
+///
+/// # Examples
+///
+/// ```
+/// use generational_arena::Arena;
+///
+/// let mut arena = Arena::new();
+/// let idx = arena.insert(123);
+/// assert_eq!(arena[idx], 123);
+/// ```
+#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Index {
+    index: usize,
+    generation: u64,
+}
+
+impl Index {
+    /// Create a new `Index` from its raw parts.
+    ///
+    /// The parts must have been returned from an earlier call to
+    /// `into_raw_parts`.
+    ///
+    /// Providing arbitrary values will lead to malformed indices and ultimately
+    /// panics.
+    pub fn from_raw_parts(a: usize, b: u64) -> Index {
+        Index {
+            index: a,
+            generation: b,
+        }
+    }
+
+    /// Convert this `Index` into its raw parts.
+    ///
+    /// This niche method is useful for converting an `Index` into another
+    /// identifier type. Usually, you should prefer a newtype wrapper around
+    /// `Index` like `pub struct MyIdentifier(Index);`.  However, for external
+    /// types whose definition you can't customize, but which you can construct
+    /// instances of, this method can be useful.
+    pub fn into_raw_parts(self) -> (usize, u64) {
+        (self.index, self.generation)
+    }
+}
+
+const DEFAULT_CAPACITY: usize = 4;
+
+impl<T> Default for Arena<T> {
+    fn default() -> Arena<T> {
+        Arena::new()
+    }
+}
+
+impl<T> Arena<T> {
+    /// Constructs a new, empty `Arena`.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::<usize>::new();
+    /// # let _ = arena;
+    /// ```
+    pub fn new() -> Arena<T> {
+        Arena::with_capacity(DEFAULT_CAPACITY)
+    }
+
+    /// Constructs a new, empty `Arena<T>` with the specified capacity.
+    ///
+    /// The `Arena<T>` will be able to hold `n` elements without further allocation.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::with_capacity(10);
+    ///
+    /// // These insertions will not require further allocation.
+    /// for i in 0..10 {
+    ///     assert!(arena.try_insert(i).is_ok());
+    /// }
+    ///
+    /// // But now we are at capacity, and there is no more room.
+    /// assert!(arena.try_insert(99).is_err());
+    /// ```
+    pub fn with_capacity(n: usize) -> Arena<T> {
+        let n = cmp::max(n, 1);
+        let mut arena = Arena {
+            items: Vec::new(),
+            generation: 0,
+            free_list_head: None,
+            len: 0,
+        };
+        arena.reserve(n);
+        arena
+    }
+
+    /// Clear all the items inside the arena, but keep its allocation.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::with_capacity(1);
+    /// arena.insert(42);
+    /// arena.insert(43);
+    ///
+    /// arena.clear();
+    ///
+    /// assert_eq!(arena.capacity(), 2);
+    /// ```
+    pub fn clear(&mut self) {
+        self.items.clear();
+
+        let end = self.items.capacity();
+        self.items.extend((0..end).map(|i| {
+            if i == end - 1 {
+                Entry::Free { next_free: None }
+            } else {
+                Entry::Free {
+                    next_free: Some(i + 1),
+                }
+            }
+        }));
+        if !self.is_empty() {
+            // Increment generation, but if there are no elements, do nothing to
+            // avoid unnecessary incrementing generation.
+            self.generation += 1;
+        }
+        self.free_list_head = Some(0);
+        self.len = 0;
+    }
+
+    /// Attempts to insert `value` into the arena using existing capacity.
+    ///
+    /// This method will never allocate new capacity in the arena.
+    ///
+    /// If insertion succeeds, then the `value`'s index is returned. If
+    /// insertion fails, then `Err(value)` is returned to give ownership of
+    /// `value` back to the caller.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    ///
+    /// match arena.try_insert(42) {
+    ///     Ok(idx) => {
+    ///         // Insertion succeeded.
+    ///         assert_eq!(arena[idx], 42);
+    ///     }
+    ///     Err(x) => {
+    ///         // Insertion failed.
+    ///         assert_eq!(x, 42);
+    ///     }
+    /// };
+    /// ```
+    #[inline]
+    pub fn try_insert(&mut self, value: T) -> Result<Index, T> {
+        match self.try_alloc_next_index() {
+            None => Err(value),
+            Some(index) => {
+                self.items[index.index] = Entry::Occupied {
+                    generation: self.generation,
+                    value,
+                };
+                Ok(index)
+            }
+        }
+    }
+
+    /// Attempts to insert the value returned by `create` into the arena using existing capacity.
+    /// `create` is called with the new value's associated index, allowing values that know their own index.
+    ///
+    /// This method will never allocate new capacity in the arena.
+    ///
+    /// If insertion succeeds, then the new index is returned. If
+    /// insertion fails, then `Err(create)` is returned to give ownership of
+    /// `create` back to the caller.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::{Arena, Index};
+    ///
+    /// let mut arena = Arena::new();
+    ///
+    /// match arena.try_insert_with(|idx| (42, idx)) {
+    ///     Ok(idx) => {
+    ///         // Insertion succeeded.
+    ///         assert_eq!(arena[idx].0, 42);
+    ///         assert_eq!(arena[idx].1, idx);
+    ///     }
+    ///     Err(x) => {
+    ///         // Insertion failed.
+    ///     }
+    /// };
+    /// ```
+    #[inline]
+    pub fn try_insert_with<F: FnOnce(Index) -> T>(&mut self, create: F) -> Result<Index, F> {
+        match self.try_alloc_next_index() {
+            None => Err(create),
+            Some(index) => {
+                self.items[index.index] = Entry::Occupied {
+                    generation: self.generation,
+                    value: create(index),
+                };
+                Ok(index)
+            }
+        }
+    }
+
+    #[inline]
+    fn try_alloc_next_index(&mut self) -> Option<Index> {
+        match self.free_list_head {
+            None => None,
+            Some(i) => match self.items[i] {
+                Entry::Occupied { .. } => panic!("corrupt free list"),
+                Entry::Free { next_free } => {
+                    self.free_list_head = next_free;
+                    self.len += 1;
+                    Some(Index {
+                        index: i,
+                        generation: self.generation,
+                    })
+                }
+            },
+        }
+    }
+
+    /// Insert `value` into the arena, allocating more capacity if necessary.
+    ///
+    /// The `value`'s associated index in the arena is returned.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    ///
+    /// let idx = arena.insert(42);
+    /// assert_eq!(arena[idx], 42);
+    /// ```
+    #[inline]
+    pub fn insert(&mut self, value: T) -> Index {
+        match self.try_insert(value) {
+            Ok(i) => i,
+            Err(value) => self.insert_slow_path(value),
+        }
+    }
+
+    /// Insert the value returned by `create` into the arena, allocating more capacity if necessary.
+    /// `create` is called with the new value's associated index, allowing values that know their own index.
+    ///
+    /// The new value's associated index in the arena is returned.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::{Arena, Index};
+    ///
+    /// let mut arena = Arena::new();
+    ///
+    /// let idx = arena.insert_with(|idx| (42, idx));
+    /// assert_eq!(arena[idx].0, 42);
+    /// assert_eq!(arena[idx].1, idx);
+    /// ```
+    #[inline]
+    pub fn insert_with(&mut self, create: impl FnOnce(Index) -> T) -> Index {
+        match self.try_insert_with(create) {
+            Ok(i) => i,
+            Err(create) => self.insert_with_slow_path(create),
+        }
+    }
+
+    #[inline(never)]
+    fn insert_slow_path(&mut self, value: T) -> Index {
+        let len = if self.capacity() == 0 {
+            // `drain()` sets the capacity to 0 and if the capacity is 0, the
+            // next `try_insert() `will refer to an out-of-range index because
+            // the next `reserve()` does not add element, resulting in a panic.
+            // So ensure that `self` have at least 1 capacity here.
+            //
+            // Ideally, this problem should be handled within `drain()`,but
+            // this problem cannot be handled within `drain()` because `drain()`
+            // returns an iterator that borrows `self` mutably.
+            1
+        } else {
+            self.items.len()
+        };
+        self.reserve(len);
+        self.try_insert(value)
+            .map_err(|_| ())
+            .expect("inserting will always succeed after reserving additional space")
+    }
+
+    #[inline(never)]
+    fn insert_with_slow_path(&mut self, create: impl FnOnce(Index) -> T) -> Index {
+        let len = self.items.len();
+        self.reserve(len);
+        self.try_insert_with(create)
+            .map_err(|_| ())
+            .expect("inserting will always succeed after reserving additional space")
+    }
+
+    /// Remove the element at index `i` from the arena.
+    ///
+    /// If the element at index `i` is still in the arena, then it is
+    /// returned. If it is not in the arena, then `None` is returned.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    /// let idx = arena.insert(42);
+    ///
+    /// assert_eq!(arena.remove(idx), Some(42));
+    /// assert_eq!(arena.remove(idx), None);
+    /// ```
+    pub fn remove(&mut self, i: Index) -> Option<T> {
+        if i.index >= self.items.len() {
+            return None;
+        }
+
+        match self.items[i.index] {
+            Entry::Occupied { generation, .. } if i.generation == generation => {
+                let entry = mem::replace(
+                    &mut self.items[i.index],
+                    Entry::Free {
+                        next_free: self.free_list_head,
+                    },
+                );
+                self.generation += 1;
+                self.free_list_head = Some(i.index);
+                self.len -= 1;
+
+                match entry {
+                    Entry::Occupied {
+                        generation: _,
+                        value,
+                    } => Some(value),
+                    _ => unreachable!(),
+                }
+            }
+            _ => None,
+        }
+    }
+
+    /// Retains only the elements specified by the predicate.
+    ///
+    /// In other words, remove all indices such that `predicate(index, &value)` returns `false`.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut crew = Arena::new();
+    /// crew.extend(&["Jim Hawkins", "John Silver", "Alexander Smollett", "Israel Hands"]);
+    /// let pirates = ["John Silver", "Israel Hands"]; // too dangerous to keep them around
+    /// crew.retain(|_index, member| !pirates.contains(member));
+    /// let mut crew_members = crew.iter().map(|(_, member)| **member);
+    /// assert_eq!(crew_members.next(), Some("Jim Hawkins"));
+    /// assert_eq!(crew_members.next(), Some("Alexander Smollett"));
+    /// assert!(crew_members.next().is_none());
+    /// ```
+    pub fn retain(&mut self, mut predicate: impl FnMut(Index, &mut T) -> bool) {
+        for i in 0..self.capacity() {
+            let remove = match &mut self.items[i] {
+                Entry::Occupied { generation, value } => {
+                    let index = Index {
+                        index: i,
+                        generation: *generation,
+                    };
+                    if predicate(index, value) {
+                        None
+                    } else {
+                        Some(index)
+                    }
+                }
+
+                _ => None,
+            };
+            if let Some(index) = remove {
+                self.remove(index);
+            }
+        }
+    }
+
+    /// Is the element at index `i` in the arena?
+    ///
+    /// Returns `true` if the element at `i` is in the arena, `false` otherwise.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    /// let idx = arena.insert(42);
+    ///
+    /// assert!(arena.contains(idx));
+    /// arena.remove(idx);
+    /// assert!(!arena.contains(idx));
+    /// ```
+    pub fn contains(&self, i: Index) -> bool {
+        self.get(i).is_some()
+    }
+
+    /// Get a shared reference to the element at index `i` if it is in the
+    /// arena.
+    ///
+    /// If the element at index `i` is not in the arena, then `None` is returned.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    /// let idx = arena.insert(42);
+    ///
+    /// assert_eq!(arena.get(idx), Some(&42));
+    /// arena.remove(idx);
+    /// assert!(arena.get(idx).is_none());
+    /// ```
+    pub fn get(&self, i: Index) -> Option<&T> {
+        match self.items.get(i.index) {
+            Some(Entry::Occupied { generation, value }) if *generation == i.generation => {
+                Some(value)
+            }
+            _ => None,
+        }
+    }
+
+    /// Get an exclusive reference to the element at index `i` if it is in the
+    /// arena.
+    ///
+    /// If the element at index `i` is not in the arena, then `None` is returned.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    /// let idx = arena.insert(42);
+    ///
+    /// *arena.get_mut(idx).unwrap() += 1;
+    /// assert_eq!(arena.remove(idx), Some(43));
+    /// assert!(arena.get_mut(idx).is_none());
+    /// ```
+    pub fn get_mut(&mut self, i: Index) -> Option<&mut T> {
+        match self.items.get_mut(i.index) {
+            Some(Entry::Occupied { generation, value }) if *generation == i.generation => {
+                Some(value)
+            }
+            _ => None,
+        }
+    }
+
+    /// Get a pair of exclusive references to the elements at index `i1` and `i2` if it is in the
+    /// arena.
+    ///
+    /// If the element at index `i1` or `i2` is not in the arena, then `None` is returned for this
+    /// element.
+    ///
+    /// # Panics
+    ///
+    /// Panics if `i1` and `i2` are pointing to the same item of the arena.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    /// let idx1 = arena.insert(0);
+    /// let idx2 = arena.insert(1);
+    ///
+    /// {
+    ///     let (item1, item2) = arena.get2_mut(idx1, idx2);
+    ///
+    ///     *item1.unwrap() = 3;
+    ///     *item2.unwrap() = 4;
+    /// }
+    ///
+    /// assert_eq!(arena[idx1], 3);
+    /// assert_eq!(arena[idx2], 4);
+    /// ```
+    pub fn get2_mut(&mut self, i1: Index, i2: Index) -> (Option<&mut T>, Option<&mut T>) {
+        let len = self.items.len();
+
+        if i1.index == i2.index {
+            assert!(i1.generation != i2.generation);
+
+            if i1.generation > i2.generation {
+                return (self.get_mut(i1), None);
+            }
+            return (None, self.get_mut(i2));
+        }
+
+        if i1.index >= len {
+            return (None, self.get_mut(i2));
+        } else if i2.index >= len {
+            return (self.get_mut(i1), None);
+        }
+
+        let (raw_item1, raw_item2) = {
+            let (xs, ys) = self.items.split_at_mut(cmp::max(i1.index, i2.index));
+            if i1.index < i2.index {
+                (&mut xs[i1.index], &mut ys[0])
+            } else {
+                (&mut ys[0], &mut xs[i2.index])
+            }
+        };
+
+        let item1 = match raw_item1 {
+            Entry::Occupied { generation, value } if *generation == i1.generation => Some(value),
+            _ => None,
+        };
+
+        let item2 = match raw_item2 {
+            Entry::Occupied { generation, value } if *generation == i2.generation => Some(value),
+            _ => None,
+        };
+
+        (item1, item2)
+    }
+
+    /// Get the length of this arena.
+    ///
+    /// The length is the number of elements the arena holds.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    /// assert_eq!(arena.len(), 0);
+    ///
+    /// let idx = arena.insert(42);
+    /// assert_eq!(arena.len(), 1);
+    ///
+    /// let _ = arena.insert(0);
+    /// assert_eq!(arena.len(), 2);
+    ///
+    /// assert_eq!(arena.remove(idx), Some(42));
+    /// assert_eq!(arena.len(), 1);
+    /// ```
+    pub fn len(&self) -> usize {
+        self.len
+    }
+
+    /// Returns true if the arena contains no elements
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    /// assert!(arena.is_empty());
+    ///
+    /// let idx = arena.insert(42);
+    /// assert!(!arena.is_empty());
+    ///
+    /// assert_eq!(arena.remove(idx), Some(42));
+    /// assert!(arena.is_empty());
+    /// ```
+    pub fn is_empty(&self) -> bool {
+        self.len == 0
+    }
+
+    /// Get the capacity of this arena.
+    ///
+    /// The capacity is the maximum number of elements the arena can hold
+    /// without further allocation, including however many it currently
+    /// contains.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::with_capacity(10);
+    /// assert_eq!(arena.capacity(), 10);
+    ///
+    /// // `try_insert` does not allocate new capacity.
+    /// for i in 0..10 {
+    ///     assert!(arena.try_insert(1).is_ok());
+    ///     assert_eq!(arena.capacity(), 10);
+    /// }
+    ///
+    /// // But `insert` will if the arena is already at capacity.
+    /// arena.insert(0);
+    /// assert!(arena.capacity() > 10);
+    /// ```
+    pub fn capacity(&self) -> usize {
+        self.items.len()
+    }
+
+    /// Allocate space for `additional_capacity` more elements in the arena.
+    ///
+    /// # Panics
+    ///
+    /// Panics if this causes the capacity to overflow.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::with_capacity(10);
+    /// arena.reserve(5);
+    /// assert_eq!(arena.capacity(), 15);
+    /// # let _: Arena<usize> = arena;
+    /// ```
+    pub fn reserve(&mut self, additional_capacity: usize) {
+        let start = self.items.len();
+        let end = self.items.len() + additional_capacity;
+        let old_head = self.free_list_head;
+        self.items.reserve_exact(additional_capacity);
+        self.items.extend((start..end).map(|i| {
+            if i == end - 1 {
+                Entry::Free {
+                    next_free: old_head,
+                }
+            } else {
+                Entry::Free {
+                    next_free: Some(i + 1),
+                }
+            }
+        }));
+        self.free_list_head = Some(start);
+    }
+
+    /// Iterate over shared references to the elements in this arena.
+    ///
+    /// Yields pairs of `(Index, &T)` items.
+    ///
+    /// Order of iteration is not defined.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    /// for i in 0..10 {
+    ///     arena.insert(i * i);
+    /// }
+    ///
+    /// for (idx, value) in arena.iter() {
+    ///     println!("{} is at index {:?}", value, idx);
+    /// }
+    /// ```
+    pub fn iter(&self) -> Iter<T> {
+        Iter {
+            len: self.len,
+            inner: self.items.iter().enumerate(),
+        }
+    }
+
+    /// Iterate over exclusive references to the elements in this arena.
+    ///
+    /// Yields pairs of `(Index, &mut T)` items.
+    ///
+    /// Order of iteration is not defined.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    /// for i in 0..10 {
+    ///     arena.insert(i * i);
+    /// }
+    ///
+    /// for (_idx, value) in arena.iter_mut() {
+    ///     *value += 5;
+    /// }
+    /// ```
+    pub fn iter_mut(&mut self) -> IterMut<T> {
+        IterMut {
+            len: self.len,
+            inner: self.items.iter_mut().enumerate(),
+        }
+    }
+
+    /// Iterate over elements of the arena and remove them.
+    ///
+    /// Yields pairs of `(Index, T)` items.
+    ///
+    /// Order of iteration is not defined.
+    ///
+    /// Note: All elements are removed even if the iterator is only partially consumed or not consumed at all.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use generational_arena::Arena;
+    ///
+    /// let mut arena = Arena::new();
+    /// let idx_1 = arena.insert("hello");
+    /// let idx_2 = arena.insert("world");
+    ///
+    /// assert!(arena.get(idx_1).is_some());
+    /// assert!(arena.get(idx_2).is_some());
+    /// for (idx, value) in arena.drain() {
+    ///     assert!((idx == idx_1 && value == "hello") || (idx == idx_2 && value == "world"));
+    /// }
+    /// assert!(arena.get(idx_1).is_none());
+    /// assert!(arena.get(idx_2).is_none());
+    /// ```
+    pub fn drain(&mut self) -> Drain<T> {
+        let old_len = self.len;
+        if !self.is_empty() {
+            // Increment generation, but if there are no elements, do nothing to
+            // avoid unnecessary incrementing generation.
+            self.generation += 1;
+        }
+        self.free_list_head = None;
+        self.len = 0;
+        Drain {
+            len: old_len,
+            inner: self.items.drain(..).enumerate(),
+        }
+    }
+
+    /// Given an i of `usize` without a generation, get a shared reference
+    /// to the element and the matching `Index` of the entry behind `i`.
+    ///
+    /// This method is useful when you know there might be an element at the
+    /// position i, but don't know its generation or precise Index.
+    ///
+    /// Use cases include using indexing such as Hierarchical BitMap Indexing or
+    /// other kinds of bit-efficient indexing.
+    ///
+    /// You should use the `get` method instead most of the time.
+    pub fn get_unknown_gen(&self, i: usize) -> Option<(&T, Index)> {
+        match self.items.get(i) {
+            Some(Entry::Occupied { generation, value }) => Some((
+                value,
+                Index {
+                    generation: *generation,
+                    index: i,
+                },
+            )),
+            _ => None,
+        }
+    }
+
+    /// Given an i of `usize` without a generation, get an exclusive reference
+    /// to the element and the matching `Index` of the entry behind `i`.
+    ///
+    /// This method is useful when you know there might be an element at the
+    /// position i, but don't know its generation or precise Index.
+    ///
+    /// Use cases include using indexing such as Hierarchical BitMap Indexing or
+    /// other kinds of bit-efficient indexing.
+    ///
+    /// You should use the `get_mut` method instead most of the time.
+    pub fn get_unknown_gen_mut(&mut self, i: usize) -> Option<(&mut T, Index)> {
+        match self.items.get_mut(i) {
+            Some(Entry::Occupied { generation, value }) => Some((
+                value,
+                Index {
+                    generation: *generation,
+                    index: i,
+                },
+            )),
+            _ => None,
+        }
+    }
+}
+
+impl<T> IntoIterator for Arena<T> {
+    type Item = T;
+    type IntoIter = IntoIter<T>;
+    fn into_iter(self) -> Self::IntoIter {
+        IntoIter {
+            len: self.len,
+            inner: self.items.into_iter(),
+        }
+    }
+}
+
+/// An iterator over the elements in an arena.
+///
+/// Yields `T` items.
+///
+/// Order of iteration is not defined.
+///
+/// # Examples
+///
+/// ```
+/// use generational_arena::Arena;
+///
+/// let mut arena = Arena::new();
+/// for i in 0..10 {
+///     arena.insert(i * i);
+/// }
+///
+/// for value in arena {
+///     assert!(value < 100);
+/// }
+/// ```
+#[derive(Clone, Debug)]
+pub struct IntoIter<T> {
+    len: usize,
+    inner: vec::IntoIter<Entry<T>>,
+}
+
+impl<T> Iterator for IntoIter<T> {
+    type Item = T;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        loop {
+            match self.inner.next() {
+                Some(Entry::Free { .. }) => continue,
+                Some(Entry::Occupied { value, .. }) => {
+                    self.len -= 1;
+                    return Some(value);
+                }
+                None => {
+                    debug_assert_eq!(self.len, 0);
+                    return None;
+                }
+            }
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        (self.len, Some(self.len))
+    }
+}
+
+impl<T> DoubleEndedIterator for IntoIter<T> {
+    fn next_back(&mut self) -> Option<Self::Item> {
+        loop {
+            match self.inner.next_back() {
+                Some(Entry::Free { .. }) => continue,
+                Some(Entry::Occupied { value, .. }) => {
+                    self.len -= 1;
+                    return Some(value);
+                }
+                None => {
+                    debug_assert_eq!(self.len, 0);
+                    return None;
+                }
+            }
+        }
+    }
+}
+
+impl<T> ExactSizeIterator for IntoIter<T> {
+    fn len(&self) -> usize {
+        self.len
+    }
+}
+
+impl<T> FusedIterator for IntoIter<T> {}
+
+impl<'a, T> IntoIterator for &'a Arena<T> {
+    type Item = (Index, &'a T);
+    type IntoIter = Iter<'a, T>;
+    fn into_iter(self) -> Self::IntoIter {
+        self.iter()
+    }
+}
+
+/// An iterator over shared references to the elements in an arena.
+///
+/// Yields pairs of `(Index, &T)` items.
+///
+/// Order of iteration is not defined.
+///
+/// # Examples
+///
+/// ```
+/// use generational_arena::Arena;
+///
+/// let mut arena = Arena::new();
+/// for i in 0..10 {
+///     arena.insert(i * i);
+/// }
+///
+/// for (idx, value) in &arena {
+///     println!("{} is at index {:?}", value, idx);
+/// }
+/// ```
+#[derive(Clone, Debug)]
+pub struct Iter<'a, T: 'a> {
+    len: usize,
+    inner: iter::Enumerate<slice::Iter<'a, Entry<T>>>,
+}
+
+impl<'a, T> Iterator for Iter<'a, T> {
+    type Item = (Index, &'a T);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        loop {
+            match self.inner.next() {
+                Some((_, &Entry::Free { .. })) => continue,
+                Some((
+                    index,
+                    &Entry::Occupied {
+                        generation,
+                        ref value,
+                    },
+                )) => {
+                    self.len -= 1;
+                    let idx = Index { index, generation };
+                    return Some((idx, value));
+                }
+                None => {
+                    debug_assert_eq!(self.len, 0);
+                    return None;
+                }
+            }
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        (self.len, Some(self.len))
+    }
+}
+
+impl<'a, T> DoubleEndedIterator for Iter<'a, T> {
+    fn next_back(&mut self) -> Option<Self::Item> {
+        loop {
+            match self.inner.next_back() {
+                Some((_, &Entry::Free { .. })) => continue,
+                Some((
+                    index,
+                    &Entry::Occupied {
+                        generation,
+                        ref value,
+                    },
+                )) => {
+                    self.len -= 1;
+                    let idx = Index { index, generation };
+                    return Some((idx, value));
+                }
+                None => {
+                    debug_assert_eq!(self.len, 0);
+                    return None;
+                }
+            }
+        }
+    }
+}
+
+impl<'a, T> ExactSizeIterator for Iter<'a, T> {
+    fn len(&self) -> usize {
+        self.len
+    }
+}
+
+impl<'a, T> FusedIterator for Iter<'a, T> {}
+
+impl<'a, T> IntoIterator for &'a mut Arena<T> {
+    type Item = (Index, &'a mut T);
+    type IntoIter = IterMut<'a, T>;
+    fn into_iter(self) -> Self::IntoIter {
+        self.iter_mut()
+    }
+}
+
+/// An iterator over exclusive references to elements in this arena.
+///
+/// Yields pairs of `(Index, &mut T)` items.
+///
+/// Order of iteration is not defined.
+///
+/// # Examples
+///
+/// ```
+/// use generational_arena::Arena;
+///
+/// let mut arena = Arena::new();
+/// for i in 0..10 {
+///     arena.insert(i * i);
+/// }
+///
+/// for (_idx, value) in &mut arena {
+///     *value += 5;
+/// }
+/// ```
+#[derive(Debug)]
+pub struct IterMut<'a, T: 'a> {
+    len: usize,
+    inner: iter::Enumerate<slice::IterMut<'a, Entry<T>>>,
+}
+
+impl<'a, T> Iterator for IterMut<'a, T> {
+    type Item = (Index, &'a mut T);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        loop {
+            match self.inner.next() {
+                Some((_, &mut Entry::Free { .. })) => continue,
+                Some((
+                    index,
+                    &mut Entry::Occupied {
+                        generation,
+                        ref mut value,
+                    },
+                )) => {
+                    self.len -= 1;
+                    let idx = Index { index, generation };
+                    return Some((idx, value));
+                }
+                None => {
+                    debug_assert_eq!(self.len, 0);
+                    return None;
+                }
+            }
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        (self.len, Some(self.len))
+    }
+}
+
+impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
+    fn next_back(&mut self) -> Option<Self::Item> {
+        loop {
+            match self.inner.next_back() {
+                Some((_, &mut Entry::Free { .. })) => continue,
+                Some((
+                    index,
+                    &mut Entry::Occupied {
+                        generation,
+                        ref mut value,
+                    },
+                )) => {
+                    self.len -= 1;
+                    let idx = Index { index, generation };
+                    return Some((idx, value));
+                }
+                None => {
+                    debug_assert_eq!(self.len, 0);
+                    return None;
+                }
+            }
+        }
+    }
+}
+
+impl<'a, T> ExactSizeIterator for IterMut<'a, T> {
+    fn len(&self) -> usize {
+        self.len
+    }
+}
+
+impl<'a, T> FusedIterator for IterMut<'a, T> {}
+
+/// An iterator that removes elements from the arena.
+///
+/// Yields pairs of `(Index, T)` items.
+///
+/// Order of iteration is not defined.
+///
+/// Note: All elements are removed even if the iterator is only partially consumed or not consumed at all.
+///
+/// # Examples
+///
+/// ```
+/// use generational_arena::Arena;
+///
+/// let mut arena = Arena::new();
+/// let idx_1 = arena.insert("hello");
+/// let idx_2 = arena.insert("world");
+///
+/// assert!(arena.get(idx_1).is_some());
+/// assert!(arena.get(idx_2).is_some());
+/// for (idx, value) in arena.drain() {
+///     assert!((idx == idx_1 && value == "hello") || (idx == idx_2 && value == "world"));
+/// }
+/// assert!(arena.get(idx_1).is_none());
+/// assert!(arena.get(idx_2).is_none());
+/// ```
+#[derive(Debug)]
+pub struct Drain<'a, T: 'a> {
+    len: usize,
+    inner: iter::Enumerate<vec::Drain<'a, Entry<T>>>,
+}
+
+impl<'a, T> Iterator for Drain<'a, T> {
+    type Item = (Index, T);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        loop {
+            match self.inner.next() {
+                Some((_, Entry::Free { .. })) => continue,
+                Some((index, Entry::Occupied { generation, value })) => {
+                    let idx = Index { index, generation };
+                    self.len -= 1;
+                    return Some((idx, value));
+                }
+                None => {
+                    debug_assert_eq!(self.len, 0);
+                    return None;
+                }
+            }
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        (self.len, Some(self.len))
+    }
+}
+
+impl<'a, T> DoubleEndedIterator for Drain<'a, T> {
+    fn next_back(&mut self) -> Option<Self::Item> {
+        loop {
+            match self.inner.next_back() {
+                Some((_, Entry::Free { .. })) => continue,
+                Some((index, Entry::Occupied { generation, value })) => {
+                    let idx = Index { index, generation };
+                    self.len -= 1;
+                    return Some((idx, value));
+                }
+                None => {
+                    debug_assert_eq!(self.len, 0);
+                    return None;
+                }
+            }
+        }
+    }
+}
+
+impl<'a, T> ExactSizeIterator for Drain<'a, T> {
+    fn len(&self) -> usize {
+        self.len
+    }
+}
+
+impl<'a, T> FusedIterator for Drain<'a, T> {}
+
+impl<T> Extend<T> for Arena<T> {
+    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
+        for t in iter {
+            self.insert(t);
+        }
+    }
+}
+
+impl<T> FromIterator<T> for Arena<T> {
+    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
+        let iter = iter.into_iter();
+        let (lower, upper) = iter.size_hint();
+        let cap = upper.unwrap_or(lower);
+        let cap = cmp::max(cap, 1);
+        let mut arena = Arena::with_capacity(cap);
+        arena.extend(iter);
+        arena
+    }
+}
+
+impl<T> ops::Index<Index> for Arena<T> {
+    type Output = T;
+
+    fn index(&self, index: Index) -> &Self::Output {
+        self.get(index).expect("No element at index")
+    }
+}
+
+impl<T> ops::IndexMut<Index> for Arena<T> {
+    fn index_mut(&mut self, index: Index) -> &mut Self::Output {
+        self.get_mut(index).expect("No element at index")
+    }
+}
diff --git a/kayak_core/src/layout_cache.rs b/kayak_core/src/layout_cache.rs
new file mode 100644
index 0000000000000000000000000000000000000000..59f251ecbf8c322d0a44f8e043955d5157c4b746
--- /dev/null
+++ b/kayak_core/src/layout_cache.rs
@@ -0,0 +1,358 @@
+use std::collections::HashMap;
+
+use morphorm::{Cache, GeometryChanged};
+
+use crate::node::NodeIndex;
+
+#[derive(Debug, Default, Clone, Copy, PartialEq)]
+pub struct Rect {
+    pub posx: f32,
+    pub posy: f32,
+    pub width: f32,
+    pub height: f32,
+    pub z_index: f32,
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+pub struct Space {
+    pub left: f32,
+    pub right: f32,
+    pub top: f32,
+    pub bottom: f32,
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+pub struct Size {
+    pub width: f32,
+    pub height: f32,
+}
+
+#[derive(Default, Debug)]
+pub struct LayoutCache {
+    // Computed Outputs
+    pub rect: HashMap<NodeIndex, Rect>,
+
+    // Intermediate Values
+    space: HashMap<NodeIndex, Space>,
+    size: HashMap<NodeIndex, Size>,
+
+    child_width_max: HashMap<NodeIndex, f32>,
+    child_height_max: HashMap<NodeIndex, f32>,
+    child_width_sum: HashMap<NodeIndex, f32>,
+    child_height_sum: HashMap<NodeIndex, f32>,
+
+    grid_row_max: HashMap<NodeIndex, f32>,
+    grid_col_max: HashMap<NodeIndex, f32>,
+
+    horizontal_free_space: HashMap<NodeIndex, f32>,
+    horizontal_stretch_sum: HashMap<NodeIndex, f32>,
+
+    vertical_free_space: HashMap<NodeIndex, f32>,
+    vertical_stretch_sum: HashMap<NodeIndex, f32>,
+
+    stack_first_child: HashMap<NodeIndex, bool>,
+    stack_last_child: HashMap<NodeIndex, bool>,
+
+    geometry_changed: HashMap<NodeIndex, GeometryChanged>,
+
+    visible: HashMap<NodeIndex, bool>,
+}
+
+impl LayoutCache {
+    pub fn add(&mut self, node_index: NodeIndex) {
+        self.rect.insert(node_index, Default::default());
+
+        self.space.insert(node_index, Default::default());
+
+        self.child_width_max.insert(node_index, Default::default());
+        self.child_height_max.insert(node_index, Default::default());
+        self.child_width_sum.insert(node_index, Default::default());
+        self.child_height_sum.insert(node_index, Default::default());
+
+        self.grid_row_max.insert(node_index, Default::default());
+        self.grid_col_max.insert(node_index, Default::default());
+
+        self.horizontal_free_space
+            .insert(node_index, Default::default());
+        self.horizontal_stretch_sum
+            .insert(node_index, Default::default());
+
+        self.vertical_free_space
+            .insert(node_index, Default::default());
+        self.vertical_stretch_sum
+            .insert(node_index, Default::default());
+
+        self.stack_first_child
+            .insert(node_index, Default::default());
+        self.stack_last_child.insert(node_index, Default::default());
+
+        self.size.insert(node_index, Default::default());
+
+        self.geometry_changed.insert(node_index, Default::default());
+
+        self.visible.insert(node_index, true);
+    }
+}
+
+impl Cache for LayoutCache {
+    type Item = NodeIndex;
+
+    fn visible(&self, node: Self::Item) -> bool {
+        if let Some(value) = self.visible.get(&node) {
+            return *value;
+        }
+
+        true
+    }
+
+    fn geometry_changed(&self, node: Self::Item) -> GeometryChanged {
+        if let Some(geometry_changed) = self.geometry_changed.get(&node) {
+            return *geometry_changed;
+        }
+
+        GeometryChanged::default()
+    }
+
+    fn set_geo_changed(&mut self, node: Self::Item, flag: GeometryChanged, value: bool) {
+        if let Some(geometry_changed) = self.geometry_changed.get_mut(&node) {
+            geometry_changed.set(flag, value);
+        }
+    }
+
+    fn width(&self, node: Self::Item) -> f32 {
+        if let Some(rect) = self.rect.get(&node) {
+            return rect.width;
+        }
+
+        0.0
+    }
+
+    fn height(&self, node: Self::Item) -> f32 {
+        if let Some(rect) = self.rect.get(&node) {
+            return rect.height;
+        }
+
+        0.0
+    }
+
+    fn posx(&self, node: Self::Item) -> f32 {
+        if let Some(rect) = self.rect.get(&node) {
+            return rect.posx;
+        }
+
+        0.0
+    }
+
+    fn posy(&self, node: Self::Item) -> f32 {
+        if let Some(rect) = self.rect.get(&node) {
+            return rect.posy;
+        }
+
+        0.0
+    }
+
+    fn left(&self, node: Self::Item) -> f32 {
+        if let Some(space) = self.space.get(&node) {
+            return space.left;
+        }
+
+        0.0
+    }
+
+    fn right(&self, node: Self::Item) -> f32 {
+        if let Some(space) = self.space.get(&node) {
+            return space.right;
+        }
+
+        0.0
+    }
+
+    fn top(&self, node: Self::Item) -> f32 {
+        if let Some(space) = self.space.get(&node) {
+            return space.top;
+        }
+
+        0.0
+    }
+
+    fn bottom(&self, node: Self::Item) -> f32 {
+        if let Some(space) = self.space.get(&node) {
+            return space.bottom;
+        }
+
+        0.0
+    }
+
+    fn new_width(&self, node: Self::Item) -> f32 {
+        if let Some(size) = self.size.get(&node) {
+            return size.width;
+        }
+
+        0.0
+    }
+
+    fn new_height(&self, node: Self::Item) -> f32 {
+        if let Some(size) = self.size.get(&node) {
+            return size.height;
+        }
+
+        0.0
+    }
+
+    fn child_width_max(&self, node: Self::Item) -> f32 {
+        *self.child_width_max.get(&node).unwrap()
+    }
+
+    /// Get the computed sum of the widths of the child nodes
+    fn child_width_sum(&self, node: Self::Item) -> f32 {
+        *self.child_width_sum.get(&node).unwrap()
+    }
+
+    /// Get the computed maximum width of the child nodes
+    fn child_height_max(&self, node: Self::Item) -> f32 {
+        *self.child_height_max.get(&node).unwrap()
+    }
+
+    /// Get the computed sum of the widths of the child nodes
+    fn child_height_sum(&self, node: Self::Item) -> f32 {
+        *self.child_height_sum.get(&node).unwrap()
+    }
+
+    /// Get the computed maximum grid row
+    fn grid_row_max(&self, node: Self::Item) -> f32 {
+        *self.grid_row_max.get(&node).unwrap()
+    }
+
+    /// Get the computed maximum grid column
+    fn grid_col_max(&self, node: Self::Item) -> f32 {
+        *self.grid_col_max.get(&node).unwrap()
+    }
+
+    // Setters
+    fn set_visible(&mut self, node: Self::Item, value: bool) {
+        *self.visible.get_mut(&node).unwrap() = value;
+    }
+
+    fn set_child_width_sum(&mut self, node: Self::Item, value: f32) {
+        *self.child_width_sum.get_mut(&node).unwrap() = value;
+    }
+
+    fn set_child_height_sum(&mut self, node: Self::Item, value: f32) {
+        *self.child_height_sum.get_mut(&node).unwrap() = value;
+    }
+
+    fn set_child_width_max(&mut self, node: Self::Item, value: f32) {
+        *self.child_width_max.get_mut(&node).unwrap() = value;
+    }
+
+    fn set_child_height_max(&mut self, node: Self::Item, value: f32) {
+        *self.child_height_max.get_mut(&node).unwrap() = value;
+    }
+
+    fn horizontal_free_space(&self, node: Self::Item) -> f32 {
+        *self.horizontal_free_space.get(&node).unwrap()
+    }
+    fn set_horizontal_free_space(&mut self, node: Self::Item, value: f32) {
+        *self.horizontal_free_space.get_mut(&node).unwrap() = value;
+    }
+    fn vertical_free_space(&self, node: Self::Item) -> f32 {
+        *self.vertical_free_space.get(&node).unwrap()
+    }
+    fn set_vertical_free_space(&mut self, node: Self::Item, value: f32) {
+        *self.vertical_free_space.get_mut(&node).unwrap() = value;
+    }
+
+    fn horizontal_stretch_sum(&self, node: Self::Item) -> f32 {
+        *self.horizontal_stretch_sum.get(&node).unwrap()
+    }
+    fn set_horizontal_stretch_sum(&mut self, node: Self::Item, value: f32) {
+        *self.horizontal_stretch_sum.get_mut(&node).unwrap() = value;
+    }
+    fn vertical_stretch_sum(&self, node: Self::Item) -> f32 {
+        *self.vertical_stretch_sum.get(&node).unwrap()
+    }
+    fn set_vertical_stretch_sum(&mut self, node: Self::Item, value: f32) {
+        *self.vertical_stretch_sum.get_mut(&node).unwrap() = value;
+    }
+
+    fn set_grid_row_max(&mut self, node: Self::Item, value: f32) {
+        *self.grid_row_max.get_mut(&node).unwrap() = value;
+    }
+
+    fn set_grid_col_max(&mut self, node: Self::Item, value: f32) {
+        *self.grid_row_max.get_mut(&node).unwrap() = value;
+    }
+
+    fn set_width(&mut self, node: Self::Item, value: f32) {
+        if let Some(rect) = self.rect.get_mut(&node) {
+            rect.width = value;
+        }
+    }
+    fn set_height(&mut self, node: Self::Item, value: f32) {
+        if let Some(rect) = self.rect.get_mut(&node) {
+            rect.height = value;
+        }
+    }
+    fn set_posx(&mut self, node: Self::Item, value: f32) {
+        if let Some(rect) = self.rect.get_mut(&node) {
+            rect.posx = value;
+        }
+    }
+    fn set_posy(&mut self, node: Self::Item, value: f32) {
+        if let Some(rect) = self.rect.get_mut(&node) {
+            rect.posy = value;
+        }
+    }
+
+    fn set_left(&mut self, node: Self::Item, value: f32) {
+        if let Some(space) = self.space.get_mut(&node) {
+            space.left = value;
+        }
+    }
+
+    fn set_right(&mut self, node: Self::Item, value: f32) {
+        if let Some(space) = self.space.get_mut(&node) {
+            space.right = value;
+        }
+    }
+
+    fn set_top(&mut self, node: Self::Item, value: f32) {
+        if let Some(space) = self.space.get_mut(&node) {
+            space.top = value;
+        }
+    }
+
+    fn set_bottom(&mut self, node: Self::Item, value: f32) {
+        if let Some(space) = self.space.get_mut(&node) {
+            space.bottom = value;
+        }
+    }
+
+    fn set_new_width(&mut self, node: Self::Item, value: f32) {
+        if let Some(size) = self.size.get_mut(&node) {
+            size.width = value;
+        }
+    }
+
+    fn set_new_height(&mut self, node: Self::Item, value: f32) {
+        if let Some(size) = self.size.get_mut(&node) {
+            size.height = value;
+        }
+    }
+
+    fn stack_first_child(&self, node: Self::Item) -> bool {
+        *self.stack_first_child.get(&node).unwrap()
+    }
+
+    fn set_stack_first_child(&mut self, node: Self::Item, value: bool) {
+        *self.stack_first_child.get_mut(&node).unwrap() = value;
+    }
+
+    fn stack_last_child(&self, node: Self::Item) -> bool {
+        *self.stack_last_child.get(&node).unwrap()
+    }
+
+    fn set_stack_last_child(&mut self, node: Self::Item, value: bool) {
+        *self.stack_last_child.get_mut(&node).unwrap() = value;
+    }
+}
diff --git a/kayak_core/src/lib.rs b/kayak_core/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..73ad0d8d8aa726d2518b9962414c421951dc738f
--- /dev/null
+++ b/kayak_core/src/lib.rs
@@ -0,0 +1,29 @@
+pub mod color;
+pub mod context;
+pub mod fragment;
+pub mod layout_cache;
+pub mod node;
+pub mod render_command;
+pub mod render_primitive;
+pub mod styles;
+pub mod tree;
+pub mod widget;
+pub mod widget_manager;
+
+pub(crate) mod generational_arena;
+
+pub use generational_arena::{Arena, Index};
+
+pub use widget::Widget;
+
+pub use kayak_render_macros::{render, rsx, widget};
+
+pub use fragment::Fragment;
+
+pub type Children = Option<
+    std::sync::Arc<dyn Fn(Option<crate::Index>, &mut crate::context::KayakContext) + Send + Sync>,
+>;
+
+pub mod derivative {
+    pub use derivative::*;
+}
diff --git a/kayak_core/src/node.rs b/kayak_core/src/node.rs
new file mode 100644
index 0000000000000000000000000000000000000000..704cc010bed46b1df679c313a6a6bf89807d1391
--- /dev/null
+++ b/kayak_core/src/node.rs
@@ -0,0 +1,285 @@
+use crate::{
+    styles::{Style, StyleProp},
+    Arena, Index,
+};
+
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub struct NodeIndex(pub Index);
+
+#[derive(Debug, Clone)]
+pub struct Node {
+    pub children: Vec<Index>,
+    pub id: Index,
+    pub styles: Style,
+    pub z: f32,
+}
+
+impl Node {}
+
+pub struct NodeBuilder {
+    children: Vec<Index>,
+    id: Index,
+    styles: Style,
+}
+
+impl NodeBuilder {
+    pub fn empty() -> Self {
+        Self {
+            children: Vec::new(),
+            id: Index::default(),
+            styles: Style::default(),
+        }
+    }
+
+    pub fn new(id: Index, styles: Style) -> Self {
+        Self {
+            children: Vec::new(),
+            id,
+            styles,
+        }
+    }
+
+    pub fn with_id(mut self, id: Index) -> Self {
+        self.id = id;
+        self
+    }
+
+    pub fn with_children(mut self, children: Vec<Index>) -> Self {
+        self.children.extend(children);
+        self
+    }
+
+    pub fn with_styles(mut self, styles: Style) -> Self {
+        self.styles = styles;
+        self
+    }
+
+    pub fn build(self) -> Node {
+        Node {
+            children: self.children,
+            id: self.id,
+            styles: self.styles,
+            z: 0.0,
+        }
+    }
+}
+
+impl<'a> morphorm::Node<'a> for NodeIndex {
+    type Data = Arena<Option<Node>>;
+
+    fn layout_type(&self, store: &'_ Self::Data) -> Option<morphorm::LayoutType> {
+        if let Some(node) = store.get(self.0) {
+            if let Some(node) = node {
+                return match node.styles.layout_type {
+                    StyleProp::Default => Some(morphorm::LayoutType::default()),
+                    StyleProp::Value(prop) => Some(prop),
+                    _ => None,
+                };
+            }
+        }
+        return None;
+    }
+
+    fn position_type(&self, store: &'_ Self::Data) -> Option<morphorm::PositionType> {
+        if let Some(node) = store.get(self.0) {
+            if let Some(node) = node {
+                return match node.styles.position_type {
+                    StyleProp::Default => Some(morphorm::PositionType::default()),
+                    StyleProp::Value(prop) => Some(prop),
+                    _ => None,
+                };
+            }
+        }
+        return None;
+    }
+
+    fn width(&self, store: &'_ Self::Data) -> Option<morphorm::Units> {
+        if let Some(node) = store.get(self.0) {
+            if let Some(node) = node {
+                return match node.styles.width {
+                    StyleProp::Default => Some(morphorm::Units::Stretch(1.0)),
+                    StyleProp::Value(prop) => Some(prop),
+                    _ => None,
+                };
+            }
+        }
+        return None;
+    }
+
+    fn height(&self, store: &'_ Self::Data) -> Option<morphorm::Units> {
+        if let Some(node) = store.get(self.0) {
+            if let Some(node) = node {
+                return match node.styles.height {
+                    StyleProp::Default => Some(morphorm::Units::Stretch(1.0)),
+                    StyleProp::Value(prop) => Some(prop),
+                    _ => None,
+                };
+            }
+        }
+        return None;
+    }
+
+    fn min_width(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn min_height(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn max_width(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn max_height(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn left(&self, store: &'_ Self::Data) -> Option<morphorm::Units> {
+        if let Some(node) = store.get(self.0) {
+            if let Some(node) = node {
+                return match node.styles.left {
+                    StyleProp::Default => Some(morphorm::Units::Auto),
+                    StyleProp::Value(prop) => Some(prop),
+                    _ => None,
+                };
+            }
+        }
+        return None;
+    }
+
+    fn right(&self, store: &'_ Self::Data) -> Option<morphorm::Units> {
+        if let Some(node) = store.get(self.0) {
+            if let Some(node) = node {
+                return match node.styles.right {
+                    StyleProp::Default => Some(morphorm::Units::Auto),
+                    StyleProp::Value(prop) => Some(prop),
+                    _ => None,
+                };
+            }
+        }
+        return None;
+    }
+
+    fn top(&self, store: &'_ Self::Data) -> Option<morphorm::Units> {
+        if let Some(node) = store.get(self.0) {
+            if let Some(node) = node {
+                return match node.styles.top {
+                    StyleProp::Default => Some(morphorm::Units::Auto),
+                    StyleProp::Value(prop) => Some(prop),
+                    _ => None,
+                };
+            }
+        }
+        return None;
+    }
+
+    fn bottom(&self, store: &'_ Self::Data) -> Option<morphorm::Units> {
+        if let Some(node) = store.get(self.0) {
+            if let Some(node) = node {
+                return match node.styles.bottom {
+                    StyleProp::Default => Some(morphorm::Units::Auto),
+                    StyleProp::Value(prop) => Some(prop),
+                    _ => None,
+                };
+            }
+        }
+        return None;
+    }
+
+    fn min_left(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn max_left(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn min_right(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn max_right(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn min_top(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn max_top(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn min_bottom(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn max_bottom(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn child_left(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn child_right(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn child_top(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn child_bottom(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn row_between(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn col_between(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn grid_rows(&self, _store: &'_ Self::Data) -> Option<Vec<morphorm::Units>> {
+        Some(vec![])
+    }
+
+    fn grid_cols(&self, _store: &'_ Self::Data) -> Option<Vec<morphorm::Units>> {
+        Some(vec![])
+    }
+
+    fn row_index(&self, _store: &'_ Self::Data) -> Option<usize> {
+        Some(0)
+    }
+
+    fn col_index(&self, _store: &'_ Self::Data) -> Option<usize> {
+        Some(0)
+    }
+
+    fn row_span(&self, _store: &'_ Self::Data) -> Option<usize> {
+        Some(1)
+    }
+
+    fn col_span(&self, _store: &'_ Self::Data) -> Option<usize> {
+        Some(1)
+    }
+
+    fn border_left(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn border_right(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn border_top(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+
+    fn border_bottom(&self, _store: &'_ Self::Data) -> Option<morphorm::Units> {
+        Some(morphorm::Units::Auto)
+    }
+}
diff --git a/kayak_core/src/render_command.rs b/kayak_core/src/render_command.rs
new file mode 100644
index 0000000000000000000000000000000000000000..eb75d0b435e68a96ed89707ef33303c681247d90
--- /dev/null
+++ b/kayak_core/src/render_command.rs
@@ -0,0 +1,17 @@
+#[derive(Debug, Clone, PartialEq)]
+pub enum RenderCommand {
+    Empty,
+    Clip,
+    Quad,
+    Text {
+        content: String,
+        size: f32,
+        font: u16,
+    },
+}
+
+impl Default for RenderCommand {
+    fn default() -> Self {
+        Self::Empty
+    }
+}
diff --git a/kayak_core/src/render_primitive.rs b/kayak_core/src/render_primitive.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1c540547861fa4e6b2f8202ca7ecea1f3e80c1a4
--- /dev/null
+++ b/kayak_core/src/render_primitive.rs
@@ -0,0 +1,59 @@
+use crate::{color::Color, layout_cache::Rect, render_command::RenderCommand, styles::Style};
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum RenderPrimitive {
+    Empty,
+    Clip {
+        layout: Rect,
+    },
+    Quad {
+        layout: Rect,
+        background_color: Color,
+    },
+    Text {
+        layout: Rect,
+        color: Color,
+        size: f32,
+        content: String,
+        font: u16,
+    },
+}
+
+impl RenderPrimitive {
+    pub fn set_layout(&mut self, new_layout: Rect) {
+        match self {
+            RenderPrimitive::Clip { layout, .. } => *layout = new_layout,
+            RenderPrimitive::Quad { layout, .. } => *layout = new_layout,
+            RenderPrimitive::Text { layout, .. } => *layout = new_layout,
+            _ => (),
+        }
+    }
+}
+
+impl From<&Style> for RenderPrimitive {
+    fn from(style: &Style) -> Self {
+        let render_command = style.render_command.resolve();
+
+        match render_command {
+            RenderCommand::Empty => Self::Empty,
+            RenderCommand::Clip => Self::Clip {
+                layout: Rect::default(),
+            },
+            RenderCommand::Quad => Self::Quad {
+                background_color: style.background_color.resolve(),
+                layout: Rect::default(),
+            },
+            RenderCommand::Text {
+                content,
+                size,
+                font,
+            } => Self::Text {
+                layout: Rect::default(),
+                color: style.color.resolve(),
+                size,
+                content,
+                font,
+            },
+        }
+    }
+}
diff --git a/kayak_core/src/styles.rs b/kayak_core/src/styles.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d75533a9d49f4e20fb5b41fae137eec3cfa2ac40
--- /dev/null
+++ b/kayak_core/src/styles.rs
@@ -0,0 +1,136 @@
+pub use morphorm::{LayoutType, PositionType, Units};
+
+use crate::{color::Color, render_command::RenderCommand};
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum StyleProp<T: Default + Clone> {
+    Default,
+    Inherit,
+    Value(T),
+}
+
+impl<T> Default for StyleProp<T>
+where
+    T: Default + Clone,
+{
+    fn default() -> Self {
+        Self::Default
+    }
+}
+
+impl<T> StyleProp<T>
+where
+    T: Default + Clone,
+{
+    pub fn resolve(&self) -> T {
+        match self {
+            StyleProp::Default => T::default(),
+            StyleProp::Value(value) => value.clone(),
+            StyleProp::Inherit => panic!("All styles should be merged before resolving!"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Style {
+    pub background_color: StyleProp<Color>,
+    pub bottom: StyleProp<Units>,
+    pub color: StyleProp<Color>,
+    pub height: StyleProp<Units>,
+    pub layout_type: StyleProp<LayoutType>,
+    pub left: StyleProp<Units>,
+    pub position_type: StyleProp<PositionType>,
+    pub render_command: StyleProp<RenderCommand>,
+    pub right: StyleProp<Units>,
+    pub top: StyleProp<Units>,
+    pub width: StyleProp<Units>,
+}
+
+impl Default for Style {
+    fn default() -> Self {
+        Self {
+            background_color: StyleProp::Value(Color::TRANSPARENT),
+            render_command: StyleProp::Value(RenderCommand::Empty),
+            bottom: StyleProp::Default,
+            color: StyleProp::Inherit,
+            height: StyleProp::Default,
+            layout_type: StyleProp::Default,
+            left: StyleProp::Default,
+            position_type: StyleProp::Default,
+            right: StyleProp::Default,
+            top: StyleProp::Default,
+            width: StyleProp::Default,
+        }
+    }
+}
+
+impl Style {
+    pub fn merge(&mut self, other: &Self) {
+        match self.background_color {
+            StyleProp::Inherit => {
+                self.background_color = other.background_color.clone();
+            }
+            _ => (),
+        }
+        match self.bottom {
+            StyleProp::Inherit => {
+                self.bottom = other.bottom.clone();
+            }
+            _ => (),
+        }
+        match self.color {
+            StyleProp::Inherit => {
+                self.color = other.color.clone();
+            }
+            _ => (),
+        }
+        match self.height {
+            StyleProp::Inherit => {
+                self.height = other.height.clone();
+            }
+            _ => (),
+        }
+        match self.layout_type {
+            StyleProp::Inherit => {
+                self.layout_type = other.layout_type.clone();
+            }
+            _ => (),
+        }
+        match self.left {
+            StyleProp::Inherit => {
+                self.left = other.left.clone();
+            }
+            _ => (),
+        }
+        match self.position_type {
+            StyleProp::Inherit => {
+                self.position_type = other.position_type.clone();
+            }
+            _ => (),
+        }
+        match self.render_command {
+            StyleProp::Inherit => {
+                self.render_command = other.render_command.clone();
+            }
+            _ => (),
+        }
+        match self.right {
+            StyleProp::Inherit => {
+                self.right = other.right.clone();
+            }
+            _ => (),
+        }
+        match self.top {
+            StyleProp::Inherit => {
+                self.top = other.top.clone();
+            }
+            _ => (),
+        }
+        match self.width {
+            StyleProp::Inherit => {
+                self.width = other.width.clone();
+            }
+            _ => (),
+        }
+    }
+}
diff --git a/kayak_core/src/tree.rs b/kayak_core/src/tree.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b9e2fe4bfb0f332852746a4f9a4fda9453223be7
--- /dev/null
+++ b/kayak_core/src/tree.rs
@@ -0,0 +1,257 @@
+use std::{collections::HashMap, iter::Rev};
+
+use morphorm::Hierarchy;
+
+use crate::{node::NodeIndex, Index};
+
+#[derive(Default, Debug)]
+pub struct Tree {
+    pub children: HashMap<Index, Vec<Index>>,
+    pub parents: HashMap<Index, Index>,
+    pub root_node: Index,
+}
+
+impl Tree {
+    pub fn add(&mut self, _child_index: usize, index: Index, parent: Option<Index>) {
+        if let Some(parent_index) = parent {
+            self.parents.insert(index, parent_index);
+            if let Some(parent_children) = self.children.get_mut(&parent_index) {
+                parent_children.push(index);
+            } else {
+                self.children.insert(parent_index, vec![index]);
+            }
+        } else {
+            self.root_node = index;
+        }
+    }
+
+    pub fn flatten(&self) -> Vec<NodeIndex> {
+        let iterator = DownwardIterator {
+            tree: &self,
+            current_node: Some(NodeIndex(self.root_node)),
+            starting: true,
+        };
+
+        iterator.collect::<Vec<_>>()
+    }
+
+    pub fn get_parent(&self, index: NodeIndex) -> Option<NodeIndex> {
+        self.parents
+            .get(&index.0)
+            .map_or(None, |parent| Some(NodeIndex(*parent)))
+    }
+
+    pub fn get_first_child(&self, index: NodeIndex) -> Option<NodeIndex> {
+        self.children.get(&index.0).map_or(None, |children| {
+            children
+                .first()
+                .map_or(None, |first_child| Some(NodeIndex(*first_child)))
+        })
+    }
+
+    pub fn get_last_child(&self, _index: NodeIndex) -> Option<NodeIndex> {
+        todo!()
+    }
+
+    pub fn get_next_sibling(&self, index: NodeIndex) -> Option<NodeIndex> {
+        if let Some(parent_index) = self.get_parent(index) {
+            self.children.get(&parent_index.0).map_or(None, |children| {
+                children
+                    .iter()
+                    .position(|child| *child == index.0)
+                    .map_or(None, |child_index| {
+                        children
+                            .get(child_index + 1)
+                            .map_or(None, |next_child| Some(NodeIndex(*next_child)))
+                    })
+            })
+        } else {
+            None
+        }
+    }
+
+    pub fn get_prev_sibling(&self, index: NodeIndex) -> Option<NodeIndex> {
+        self.children.get(&index.0).map_or(None, |children| {
+            children
+                .iter()
+                .position(|child| *child == index.0)
+                .map_or(None, |child_index| {
+                    children
+                        .get(child_index - 1)
+                        .map_or(None, |next_child| Some(NodeIndex(*next_child)))
+                })
+        })
+    }
+}
+
+pub struct DownwardIterator<'a> {
+    tree: &'a Tree,
+    current_node: Option<NodeIndex>,
+    starting: bool,
+}
+
+impl<'a> DownwardIterator<'a> {}
+
+impl<'a> Iterator for DownwardIterator<'a> {
+    type Item = NodeIndex;
+    fn next(&mut self) -> Option<NodeIndex> {
+        if self.starting {
+            self.starting = false;
+            return self.current_node;
+        }
+
+        if let Some(current_index) = self.current_node {
+            if let Some(first_child) = self.tree.get_first_child(current_index) {
+                self.current_node = Some(first_child);
+                return Some(first_child);
+            } else if let Some(next_sibling) = self.tree.get_next_sibling(current_index) {
+                self.current_node = Some(next_sibling);
+                return Some(next_sibling);
+            } else {
+                let mut current_parent = self.tree.get_parent(current_index);
+                while current_parent.is_some() {
+                    if let Some(current_parent) = current_parent {
+                        if let Some(next_parent_sibling) =
+                            self.tree.get_next_sibling(current_parent)
+                        {
+                            self.current_node = Some(next_parent_sibling);
+                            return Some(next_parent_sibling);
+                        }
+                    }
+                    current_parent = self.tree.get_parent(current_parent.unwrap());
+                }
+            }
+        }
+
+        return None;
+    }
+}
+
+// pub struct UpwardIterator<'a> {
+//     tree: &'a Tree,
+//     current_node: Option<NodeIndex>,
+// }
+
+// impl<'a> Iterator for UpwardIterator<'a> {
+//     type Item = NodeIndex;
+
+//     // TODO - Needs Testing
+//     fn next(&mut self) -> Option<NodeIndex> {
+//         None
+//     }
+// }
+
+pub struct ChildIterator<'a> {
+    pub tree: &'a Tree,
+    pub current_node: Option<NodeIndex>,
+}
+
+impl<'a> Iterator for ChildIterator<'a> {
+    type Item = NodeIndex;
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(entity) = self.current_node {
+            self.current_node = self.tree.get_next_sibling(entity);
+            return Some(entity);
+        }
+
+        None
+    }
+}
+
+impl<'a> Hierarchy<'a> for Tree {
+    type Item = NodeIndex;
+    type DownIter = std::vec::IntoIter<NodeIndex>;
+    type UpIter = Rev<std::vec::IntoIter<NodeIndex>>;
+    type ChildIter = ChildIterator<'a>;
+
+    fn up_iter(&'a self) -> Self::UpIter {
+        self.flatten().into_iter().rev()
+    }
+
+    fn down_iter(&'a self) -> Self::DownIter {
+        self.flatten().into_iter()
+    }
+
+    fn child_iter(&'a self, node: Self::Item) -> Self::ChildIter {
+        let first_child = self.get_first_child(node);
+        ChildIterator {
+            tree: self,
+            current_node: first_child,
+        }
+    }
+
+    fn parent(&self, node: Self::Item) -> Option<Self::Item> {
+        if let Some(parent_index) = self.parents.get(&node.0) {
+            return Some(NodeIndex(*parent_index));
+        }
+
+        None
+    }
+
+    fn is_first_child(&self, node: Self::Item) -> bool {
+        if let Some(parent) = self.parent(node) {
+            if let Some(first_child) = self.get_first_child(parent) {
+                if first_child == node {
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+        }
+
+        false
+    }
+
+    fn is_last_child(&self, node: Self::Item) -> bool {
+        if let Some(parent) = self.parent(node) {
+            if let Some(parent_children) = self.children.get(&parent.0) {
+                if let Some(last_child) = parent_children.last() {
+                    return *last_child == node.0;
+                }
+            }
+        }
+
+        false
+    }
+}
+
+#[test]
+fn test_tree() {
+    use crate::node::NodeBuilder;
+    use crate::Arena;
+    let mut store = Arena::new();
+    let root = store.insert(NodeBuilder::empty().build());
+    // Child 1 of root
+    let index1 = store.insert(NodeBuilder::empty().build());
+    // Children of child 1.
+    let index2 = store.insert(NodeBuilder::empty().build());
+    let index3 = store.insert(NodeBuilder::empty().build());
+    // Child 2 of root
+    let index4 = store.insert(NodeBuilder::empty().build());
+
+    let mut tree = Tree::default();
+    tree.root_node = root;
+
+    // Setup Parents..
+    tree.parents.insert(index1, root);
+    tree.parents.insert(index4, root);
+
+    tree.parents.insert(index2, index1);
+    tree.parents.insert(index3, index1);
+
+    tree.children.insert(root, vec![index1, index4]);
+    tree.children.insert(index1, vec![index2, index3]);
+
+    let flattened = tree.flatten();
+
+    let mapped = flattened
+        .iter()
+        .map(|x| x.0.into_raw_parts().0)
+        .collect::<Vec<_>>();
+
+    assert!(mapped[0] == 0);
+    assert!(mapped[1] == 1);
+    assert!(mapped[2] == 2);
+    assert!(mapped[3] == 3);
+    assert!(mapped[4] == 4);
+}
diff --git a/kayak_core/src/widget.rs b/kayak_core/src/widget.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3e63d8701992694cb94cea79cb767e678d462c81
--- /dev/null
+++ b/kayak_core/src/widget.rs
@@ -0,0 +1,15 @@
+use as_any::AsAny;
+
+use crate::{context::KayakContext, styles::Style, Index};
+
+pub trait Widget: std::fmt::Debug + AsAny + Send + Sync {
+    fn get_id(&self) -> Index;
+    fn set_id(&mut self, id: Index);
+    fn get_styles(&self) -> Option<Style>;
+    fn render(&mut self, context: &mut KayakContext);
+}
+
+impl as_any::Downcast for dyn Widget {}
+impl as_any::Downcast for dyn Widget + Send {}
+impl as_any::Downcast for dyn Widget + Sync {}
+impl as_any::Downcast for dyn Widget + Send + Sync {}
diff --git a/kayak_core/src/widget_manager.rs b/kayak_core/src/widget_manager.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f4e3b702d4fa4eb6e8f3c418332e8f3e7481fcd6
--- /dev/null
+++ b/kayak_core/src/widget_manager.rs
@@ -0,0 +1,216 @@
+use crate::{
+    layout_cache::LayoutCache,
+    node::{Node, NodeBuilder, NodeIndex},
+    render_primitive::RenderPrimitive,
+    styles::Style,
+    tree::Tree,
+    Arena, Index, Widget,
+};
+use as_any::Downcast;
+
+#[derive(Debug)]
+pub struct WidgetManager {
+    current_widgets: Arena<Option<Box<dyn Widget>>>,
+    pub(crate) dirty_render_nodes: Vec<Index>,
+    pub(crate) dirty_nodes: Vec<Index>,
+    pub(crate) nodes: Arena<Option<Node>>,
+    pub tree: Tree,
+    pub layout_cache: LayoutCache,
+    current_z: f32,
+}
+
+impl WidgetManager {
+    pub fn new() -> Self {
+        Self {
+            current_widgets: Arena::new(),
+            dirty_render_nodes: Vec::new(),
+            dirty_nodes: Vec::new(),
+            nodes: Arena::new(),
+            tree: Tree::default(),
+            layout_cache: LayoutCache::default(),
+            current_z: 0.0,
+        }
+    }
+
+    /// Re-renders from the root.
+    /// If force is true sets ALL nodes to re-render.
+    /// Can be slow.
+    pub fn dirty(&mut self, force: bool) {
+        // Force tree to re-render from root.
+        self.dirty_nodes.push(self.tree.root_node);
+
+        if force {
+            for (node_index, _) in self.current_widgets.iter() {
+                self.dirty_nodes.push(node_index);
+                self.dirty_render_nodes.push(node_index);
+            }
+        }
+    }
+
+    pub fn create_widget<T: Widget + PartialEq + 'static>(
+        &mut self,
+        index: usize,
+        mut widget: T,
+        parent: Option<Index>,
+    ) -> (bool, Index) {
+        if let Some(parent) = parent.clone() {
+            if let Some(parent_children) = self.tree.children.get_mut(&parent) {
+                // Pull child and update.
+                if let Some(widget_id) = parent_children.get(index) {
+                    widget.set_id(*widget_id);
+                    // Remove from the dirty nodes lists.
+                    if let Some(index) = self.dirty_nodes.iter().position(|id| *widget_id == *id) {
+                        self.dirty_nodes.remove(index);
+                    }
+
+                    if &widget
+                        != self.current_widgets[*widget_id]
+                            .as_ref()
+                            .unwrap()
+                            .downcast_ref::<T>()
+                            .unwrap()
+                    {
+                        let boxed_widget: Box<dyn Widget> = Box::new(widget);
+                        *self.current_widgets[*widget_id].as_mut().unwrap() = boxed_widget;
+                        dbg!("Widget changed!");
+                        // Tell renderer that the nodes changed.
+                        self.dirty_render_nodes.push(*widget_id);
+                        return (true, *widget_id);
+                    } else {
+                        dbg!("No widget changes!");
+                        return (false, *widget_id);
+                    }
+                }
+            }
+        }
+
+        // Create Flow
+        // We should only have one widget that doesn't have a parent.
+        // The root widget.
+        let widget_id = self.current_widgets.insert(Some(Box::new(widget)));
+        self.nodes.insert(None);
+        self.current_widgets[widget_id]
+            .as_mut()
+            .unwrap()
+            .set_id(widget_id);
+
+        // Tell renderer that the nodes changed.
+        self.dirty_render_nodes.push(widget_id);
+
+        // Remove from the dirty nodes lists.
+        if let Some(index) = self.dirty_nodes.iter().position(|id| widget_id == *id) {
+            self.dirty_nodes.remove(index);
+        }
+
+        self.tree.add(0, widget_id, parent);
+        self.layout_cache.add(NodeIndex(widget_id));
+
+        (true, widget_id)
+    }
+
+    pub fn take(&mut self, id: Index) -> Box<dyn Widget> {
+        self.current_widgets[id].take().unwrap()
+    }
+
+    pub fn repossess(&mut self, widget: Box<dyn Widget>) {
+        let widget_id = widget.get_id();
+        self.current_widgets[widget_id] = Some(widget);
+    }
+
+    pub fn render(&mut self) {
+        let default_styles = Style {
+            background_color: crate::styles::StyleProp::Default,
+            bottom: crate::styles::StyleProp::Default,
+            color: crate::styles::StyleProp::Default,
+            height: crate::styles::StyleProp::Default,
+            layout_type: crate::styles::StyleProp::Default,
+            left: crate::styles::StyleProp::Default,
+            position_type: crate::styles::StyleProp::Default,
+            render_command: crate::styles::StyleProp::Default,
+            right: crate::styles::StyleProp::Default,
+            top: crate::styles::StyleProp::Default,
+            width: crate::styles::StyleProp::Default,
+        };
+        for dirty_node_index in self.dirty_render_nodes.drain(..) {
+            let dirty_widget = self.current_widgets[dirty_node_index].as_ref().unwrap();
+            let parent_styles =
+                if let Some(parent_widget_id) = self.tree.parents.get(&dirty_node_index) {
+                    if let Some(parent) = self.current_widgets[*parent_widget_id].as_ref() {
+                        if let Some(styles) = parent.get_styles() {
+                            styles
+                        } else {
+                            default_styles.clone()
+                        }
+                    } else {
+                        default_styles.clone()
+                    }
+                } else {
+                    default_styles.clone()
+                };
+
+            // Get parent Z
+            let parent_z = if let Some(parent_widget_id) = self.tree.parents.get(&dirty_node_index)
+            {
+                if let Some(parent) = &self.nodes[*parent_widget_id] {
+                    parent.z
+                } else {
+                    -1.0
+                }
+            } else {
+                -1.0
+            };
+
+            let current_z = {
+                if parent_z > -1.0 {
+                    parent_z + 1.0
+                } else {
+                    let z = self.current_z;
+                    self.current_z += 1.0;
+                    z
+                }
+            };
+            dbg!(current_z);
+
+            let mut styles = dirty_widget.get_styles();
+            if styles.is_some() {
+                styles.as_mut().unwrap().merge(&parent_styles);
+            }
+            let children = self
+                .tree
+                .children
+                .get(&dirty_node_index)
+                .cloned()
+                .unwrap_or(vec![]);
+            let mut node = NodeBuilder::empty()
+                .with_id(dirty_node_index)
+                .with_styles(styles.unwrap_or(default_styles.clone()))
+                .with_children(children)
+                .build();
+            node.z = current_z;
+
+            self.nodes[dirty_node_index] = Some(node);
+        }
+    }
+
+    pub fn calculate_layout(&mut self) {
+        morphorm::layout(&mut self.layout_cache, &self.tree, &self.nodes);
+    }
+
+    pub fn build_render_primitives(&self) -> Vec<RenderPrimitive> {
+        let mut render_primitives = Vec::new();
+
+        for (index, node) in self.nodes.iter() {
+            if let Some(layout) = self.layout_cache.rect.get(&NodeIndex(index)) {
+                if let Some(node) = node {
+                    let mut render_primitive: RenderPrimitive = (&node.styles).into();
+                    let mut layout = *layout;
+                    layout.z_index = node.z;
+                    render_primitive.set_layout(layout);
+                    render_primitives.push(render_primitive);
+                }
+            }
+        }
+
+        render_primitives
+    }
+}
diff --git a/kayak_font/.gitignore b/kayak_font/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..aab52d906fa2b84b60b9aa27be530440a3c97962
--- /dev/null
+++ b/kayak_font/.gitignore
@@ -0,0 +1 @@
+*.png
\ No newline at end of file
diff --git a/kayak_font/Cargo.toml b/kayak_font/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..79436b77847607641ead408d6144d56d548ac08b
--- /dev/null
+++ b/kayak_font/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "kayak_font"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+arrayvec = "0.7"
+bitflags = "1.0"
+lyon_path = "0.12"
+lyon_geom = "0.12"
+fontdue = "0.6"
+ttf-parser = "0.13"
+
+[dev-dependencies]
+png = "0.16.3"
diff --git a/kayak_font/examples/main.rs b/kayak_font/examples/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2bed82802396a4971c6a7b678c0fcbce6819365f
--- /dev/null
+++ b/kayak_font/examples/main.rs
@@ -0,0 +1,54 @@
+use std::{fs::File, io::BufWriter};
+
+use kayak_font::{
+    compute_msdf, recolor_contours, rescale_contours, Angle, FlatPathBuilder, PathCollector, Point,
+    Rect,
+};
+
+pub fn main() {
+    let font_data = include_bytes!("../resources/Roboto-Regular.ttf");
+    let face = ttf_parser::Face::from_slice(font_data, 0).unwrap();
+
+    let char_dim: u32 = 64;
+
+    let c = 'A';
+    if let Some(glyph) = face.glyph_index(c) {
+        let mut path_collector = PathCollector::new();
+        path_collector.scale = 1.0; //0.0001; //1024 as f32 / face.units_per_em() as f32;
+        let rect = face.outline_glyph(glyph, &mut path_collector).unwrap();
+        let contours = path_collector.build();
+        let uv_rect = Rect::new(Point::new(0.0, 0.0), lyon_geom::math::Size::new(1.0, 1.0));
+
+        let font_rect = Rect::new(
+            Point::new(rect.x_min as f32, rect.y_min as f32),
+            lyon_geom::math::Size::new(rect.width() as f32, rect.height() as f32),
+        );
+
+        let (contours, _) = rescale_contours(contours, font_rect, uv_rect, 0);
+
+        let contours = recolor_contours(contours, Angle::degrees(3.0), 1);
+        let msdf = compute_msdf(&contours, char_dim as usize);
+
+        let file = File::create(format!("./test-{}.png", c)).unwrap();
+        let ref mut w = BufWriter::new(file);
+
+        let pixels: Vec<u8> = msdf
+            .iter()
+            .flat_map(|y| {
+                y.iter().flat_map(|pixel| {
+                    vec![
+                        (pixel.0 * 255.0) as u8,
+                        (pixel.1 * 255.0) as u8,
+                        (pixel.2 * 255.0) as u8,
+                        255u8,
+                    ]
+                })
+            })
+            .collect();
+
+        let mut encoder = png::Encoder::new(w, char_dim, char_dim);
+        encoder.set_color(png::ColorType::RGBA);
+        let mut writer = encoder.write_header().unwrap();
+        writer.write_image_data(&pixels).unwrap();
+    }
+}
diff --git a/kayak_font/examples/simple.rs b/kayak_font/examples/simple.rs
new file mode 100644
index 0000000000000000000000000000000000000000..09f8903e9eb757c0a80b715e86e58d9296a477c3
--- /dev/null
+++ b/kayak_font/examples/simple.rs
@@ -0,0 +1,30 @@
+use std::{fs::File, io::BufWriter};
+
+use kayak_font::Font;
+
+fn main() {
+    let font_bytes = include_bytes!("../resources/Roboto-Regular.ttf");
+    let mut font = Font::new(font_bytes, 64);
+
+    font.add_character('A');
+    font.add_character('B');
+    font.add_character('C');
+    font.add_character('!');
+    font.add_character('&');
+
+    // Characters that have already been calculated wont be calculated again!
+    for _ in 0..1000000 {
+        font.add_character('A');
+    }
+
+    let dimensions = font.cache.dimensions;
+    for (c, _, pixels) in font.get_data_to_process() {
+        let file = File::create(format!("./test-{}.png", c)).unwrap();
+        let ref mut w = BufWriter::new(file);
+
+        let mut encoder = png::Encoder::new(w, dimensions, dimensions);
+        encoder.set_color(png::ColorType::RGBA);
+        let mut writer = encoder.write_header().unwrap();
+        writer.write_image_data(&pixels).unwrap();
+    }
+}
diff --git a/kayak_font/resources/Roboto-Regular.ttf b/kayak_font/resources/Roboto-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..2b6392ffe8712b9c5450733320cd220d6c0f4bce
Binary files /dev/null and b/kayak_font/resources/Roboto-Regular.ttf differ
diff --git a/kayak_font/src/color_flags.rs b/kayak_font/src/color_flags.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7322912b4e0c849822302ba5bcc85f374b1af552
--- /dev/null
+++ b/kayak_font/src/color_flags.rs
@@ -0,0 +1,62 @@
+bitflags::bitflags! {
+    pub struct ColorFlags: u32 {
+        const BLACK = 0b000;
+        const RED = 0b001;
+        const GREEN = 0b010;
+        const BLUE = 0b100;
+        const YELLOW = 0b011;
+        const MAGENTA = 0b101;
+        const CYAN = 0b110;
+        const WHITE = 0b111;
+    }
+}
+
+impl ColorFlags {
+    pub(crate) fn switch(self, seed: &mut u64) -> Self {
+        match self {
+            ColorFlags::WHITE | ColorFlags::BLACK => {
+                const START: [ColorFlags; 3] =
+                    [ColorFlags::CYAN, ColorFlags::MAGENTA, ColorFlags::YELLOW];
+                let tr = START[(*seed % 3) as usize];
+                *seed /= 3;
+                tr
+            }
+            ColorFlags::RED | ColorFlags::GREEN | ColorFlags::BLUE => self ^ ColorFlags::WHITE,
+            _ => {
+                let v = self.bits();
+                let v = (v << (1 + (*seed & 1))) & 0b111;
+                let v = match v.count_ones() {
+                    0 => 0b11,           /* Somehow we lost all the bits. Default to yellow */
+                    1 => v | 0b001, /* We just shifted a bit off the left side, add one on the right */
+                    2 => v,         /* We already have 2 bits, nothing to do */
+                    _ => unreachable!(), /* There should never be 3+ bits set */
+                };
+                *seed >>= 1;
+
+                Self::from_bits_truncate(v)
+            }
+        }
+    }
+
+    pub(crate) fn switch_banned(self, seed: &mut u64, banned: ColorFlags) -> Self {
+        let combined = self & banned;
+        match combined {
+            ColorFlags::RED | ColorFlags::GREEN | ColorFlags::BLUE => combined ^ ColorFlags::WHITE,
+            _ => self.switch(seed),
+        }
+    }
+
+    pub fn float_color(self) -> [f32; 3] {
+        match self {
+            ColorFlags::BLACK => [0.0f32, 0.0f32, 0.0f32],
+            ColorFlags::RED => [1.0f32, 0.0f32, 0.0f32],
+            ColorFlags::GREEN => [0.0f32, 1.0f32, 0.0f32],
+            ColorFlags::BLUE => [0.0f32, 0.0f32, 1.0f32],
+            ColorFlags::CYAN => [0.0f32, 1.0f32, 1.0f32],
+            ColorFlags::MAGENTA => [1.0f32, 0.0f32, 1.0f32],
+            ColorFlags::YELLOW => [1.0f32, 1.0f32, 0.0f32],
+            ColorFlags::WHITE => [1.0f32, 1.0f32, 1.0f32],
+            _ => [0.5, 0.7, 0.5],
+        }
+    }
+}
diff --git a/kayak_font/src/contour.rs b/kayak_font/src/contour.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c963de3a0ae6a100704608a867c4f173b6fb0778
--- /dev/null
+++ b/kayak_font/src/contour.rs
@@ -0,0 +1,108 @@
+use lyon_geom::math::{Point, Rect};
+use lyon_path::Segment;
+
+use crate::path_element::PathElement;
+
+/// A list of path elements forming a closed loop
+#[derive(Clone, Debug)]
+pub struct Contour {
+    pub elements: Vec<PathElement>,
+}
+
+impl Contour {
+    pub fn winding(&self) -> f32 {
+        let shoelace = |a: Point, b: Point| (b.x - a.x) * (a.y + b.y);
+        let n = self.elements.len();
+        match n {
+            0 => 0.0,
+            1 => {
+                let a = self.elements[0].sample(0.0);
+                let b = self.elements[0].sample(1.0 / 3.0);
+                let c = self.elements[0].sample(2.0 / 3.0);
+
+                shoelace(a, b) + shoelace(b, c) + shoelace(c, a)
+            }
+            2 => {
+                let a = self.elements[0].sample(0.0);
+                let b = self.elements[0].sample(0.5);
+                let c = self.elements[1].sample(0.0);
+                let d = self.elements[1].sample(0.5);
+
+                shoelace(a, b) + shoelace(b, c) + shoelace(c, d) + shoelace(d, a)
+            }
+            _ => {
+                let mut total = 0.0;
+                let mut prev = self.elements[n - 1].sample(0.0);
+
+                for e in &self.elements {
+                    let curr = e.sample(0.0);
+                    total += shoelace(prev, curr);
+                    prev = curr;
+                }
+
+                total
+            }
+        }
+        .signum()
+    }
+}
+
+/// Rescale contours so they fit in the provided rectangle.
+/// Returns the scaled contours along with the transformation used to rescale the contours
+pub fn rescale_contours(
+    mut contours: Vec<Contour>,
+    initial_bounds: Rect,
+    bounds: Rect,
+    _units_per_em: u16,
+) -> (Vec<Contour>, lyon_geom::math::Transform2D) {
+    // let (new_width, new_height) = if initial_bounds.size.width > initial_bounds.size.height {
+    //     let new_width = 1.0;
+    //     let new_height = aspect_ratio_height(
+    //         initial_bounds.size.height,
+    //         initial_bounds.size.width,
+    //         new_width,
+    //     );
+
+    //     (new_width, new_height)
+    // } else {
+    //     let new_height = 1.0;
+    //     let new_width = aspect_ratio_width(
+    //         initial_bounds.size.height,
+    //         initial_bounds.size.width,
+    //         new_height,
+    //     );
+
+    //     (new_width, new_height)
+    // };
+
+    // dbg!(new_width, new_height);
+
+    // let x_scale = new_width / initial_bounds.size.width;
+    // let y_scale = new_height / initial_bounds.size.height;
+
+    let initial_scale = initial_bounds.size.height.max(initial_bounds.size.width);
+    let bounds_scale = bounds.size.width.max(bounds.size.height);
+
+    // let size = 128.0 / units_per_em as f32;
+    let transformation = lyon_geom::math::Transform2D::create_translation(
+        -initial_bounds.origin.x as f32,
+        -initial_bounds.origin.y as f32,
+    )
+    .post_scale(bounds_scale / initial_scale, bounds_scale / initial_scale)
+    .post_translate(bounds.origin.to_vector());
+    for contour in &mut contours {
+        for mut elem in &mut contour.elements {
+            elem.segment = match elem.segment {
+                Segment::Line(s) => Segment::Line(s.transform(&transformation)),
+                Segment::Quadratic(s) => Segment::Quadratic(s.transform(&transformation)),
+                Segment::Cubic(s) => Segment::Cubic(s.transform(&transformation)),
+                Segment::Arc(s) => Segment::Arc(lyon_geom::Arc {
+                    center: transformation.transform_point(&s.center),
+                    ..s
+                }),
+            }
+        }
+    }
+
+    (contours, transformation)
+}
diff --git a/kayak_font/src/font.rs b/kayak_font/src/font.rs
new file mode 100644
index 0000000000000000000000000000000000000000..806d47e1169949f0bc3d50c3c2b92b2f8bd9d837
--- /dev/null
+++ b/kayak_font/src/font.rs
@@ -0,0 +1,253 @@
+use std::collections::{HashMap, HashSet};
+
+use lyon_geom::math::{Angle, Point, Rect, Size};
+use lyon_path::builder::FlatPathBuilder;
+
+use crate::{compute_msdf, recolor_contours, rescale_contours, PathCollector};
+
+#[derive(Debug, Clone)]
+pub struct FontCache {
+    count: usize,
+    pub dimensions: u32,
+    chars: HashMap<char, (usize, Vec<u8>)>,
+    needs_processing: HashSet<usize>,
+    id_to_char_mappings: HashMap<usize, char>,
+}
+
+impl FontCache {
+    pub fn new(texture_size: u32) -> Self {
+        Self {
+            count: 0,
+            dimensions: texture_size,
+            chars: HashMap::default(),
+            needs_processing: HashSet::default(),
+            id_to_char_mappings: HashMap::default(),
+        }
+    }
+
+    fn set_texture(&mut self, c: char, texture_data: Vec<Vec<(f32, f32, f32)>>) {
+        // let pixels: Vec<u8> = texture_data
+        //     .iter()
+        //     .flat_map(|y| {
+        //         y.iter().flat_map(|pixel| {
+        //             vec![
+        //                 (pixel.0 * 255.0) as u8,
+        //                 (pixel.1 * 255.0) as u8,
+        //                 (pixel.2 * 255.0) as u8,
+        //                 255u8,
+        //             ]
+        //         })
+        //     })
+        //     .collect();
+        let pixels = texture_data
+            .iter()
+            .flat_map(|x| {
+                x.iter()
+                    .flat_map(|p| {
+                        vec![
+                            p.0.to_le_bytes(),
+                            p.1.to_le_bytes(),
+                            p.2.to_le_bytes(),
+                            1.0f32.to_le_bytes(),
+                        ]
+                        .into_iter()
+                        .flatten()
+                        .collect::<Vec<u8>>()
+                    })
+                    .collect::<Vec<u8>>()
+            })
+            .collect();
+        self.chars.insert(c, (self.count, pixels));
+        self.needs_processing.insert(self.count);
+        self.id_to_char_mappings.insert(self.count, c);
+        self.count += 1;
+    }
+
+    fn has_character(&self, c: char) -> bool {
+        self.chars.contains_key(&c)
+    }
+
+    fn get_dimensions(&self) -> u32 {
+        self.dimensions
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct Font {
+    internal_face: ttf_parser::Face<'static>,
+    font: fontdue::Font,
+    pub cache: FontCache,
+}
+
+impl Font {
+    pub fn new(font_data: &'static [u8], texture_size: u32) -> Font {
+        Font {
+            internal_face: ttf_parser::Face::from_slice(&font_data, 0).unwrap(),
+            font: fontdue::Font::from_bytes(font_data.clone(), fontdue::FontSettings::default())
+                .unwrap(),
+            cache: FontCache::new(texture_size),
+        }
+    }
+
+    /// Adds all of the common known characters.
+    pub fn add_all_common(&mut self) {
+        let chars = vec![
+            '`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '~', '!', '@', '#',
+            '$', '%', '^', '&', '*', '(', ')', '_', '+', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i',
+            'o', 'p', '[', ']', '\\', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}',
+            '|', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', 'A', 'S', 'D', 'F', 'G',
+            'H', 'J', 'K', 'L', ':', '"', 'z', 'x', 'c', 'v', 'b', 'n', 'n', 'm', ',', '.', '/',
+            'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?',
+        ];
+
+        for char in chars {
+            self.add_character(char);
+        }
+    }
+
+    pub fn get_layout(
+        &self,
+        content: &String,
+        font_size: f32,
+    ) -> Vec<(char, (f32, f32), (f32, f32))> {
+        let mut layout =
+            fontdue::layout::Layout::new(fontdue::layout::CoordinateSystem::PositiveYDown);
+        layout.append(
+            &[&self.font],
+            &fontdue::layout::TextStyle::new(content, font_size, 0),
+        );
+
+        let glyphs = layout.glyphs();
+        let glyphs: Vec<_> = glyphs
+            .iter()
+            .filter_map(|glyph_position| {
+                if glyph_position.parent == ' ' {
+                    return None;
+                }
+                // let metrics = self.font.metrics(glyph_position.parent, font_size);
+                let font_scale = font_size / self.font.units_per_em() as f32;
+                let scale =
+                    if let Some(glyph) = self.internal_face.glyph_index(glyph_position.parent) {
+                        // TODO: Cache this in add_character..
+                        let mut path_collector = PathCollector::new();
+                        let rect = self
+                            .internal_face
+                            .outline_glyph(glyph, &mut path_collector)
+                            .unwrap();
+                        let width = rect.width();
+                        let height = rect.height();
+                        width.max(height) as f32 * font_scale
+                    } else {
+                        0.0
+                    };
+
+                // let shift_x = if scale > glyph_position.width as f32 {
+                //     scale - glyph_position.width as f32
+                // } else {
+                //     // glyph_position.width as f32 - scale
+                //     0.0
+                // };
+                let shift_y = if scale > glyph_position.height as f32 {
+                    scale - glyph_position.height as f32
+                } else {
+                    0.0
+                };
+                Some((
+                    glyph_position.parent,
+                    (glyph_position.x, glyph_position.y - shift_y),
+                    (scale, scale),
+                ))
+            })
+            .collect();
+        glyphs
+    }
+
+    // pub fn get_size(&self, c: char, font_size: f32) -> (f32, f32) {
+    //     if let Some(glyph) = self.internal_face.glyph_index(c) {
+    //         // Collect our path's from the glyph's outline shape.
+    //         let mut path_collector = PathCollector::new();
+    //         let rect = self
+    //             .internal_face
+    //             .outline_glyph(glyph, &mut path_collector)
+    //             .unwrap();
+    //         let metrics = font_size / self.font.units_per_em();
+
+    //         (width as f32 * metrics, height as f32 * metrics)
+    //     } else {
+    //         panic!("")
+    //     }
+    // }
+
+    pub fn get_char_id(&self, c: char) -> usize {
+        if self.cache.has_character(c) {
+            if let Some((id, _)) = self.cache.chars.get(&c) {
+                return *id;
+            }
+        }
+        panic!("No char found!");
+    }
+
+    pub fn add_character(&mut self, c: char) {
+        if !self.cache.has_character(c) {
+            if let Some(glyph) = self.internal_face.glyph_index(c) {
+                // Collect our path's from the glyph's outline shape.
+                let mut path_collector = PathCollector::new();
+                let rect = self
+                    .internal_face
+                    .outline_glyph(glyph, &mut path_collector)
+                    .unwrap();
+                let contours = path_collector.build();
+
+                // Bounds of our texture in UV's
+                // TODO: Allow this to change because some people may want texture atlases instead.
+                let uv_rect = Rect::new(Point::new(0.0, 0.0), Size::new(1.0, 1.0));
+
+                // Bounds of our rect in font space coords.
+                let font_rect = Rect::new(
+                    Point::new(rect.x_min as f32, rect.y_min as f32),
+                    Size::new(rect.width() as f32, rect.height() as f32),
+                );
+
+                let (contours, _transform) = rescale_contours(
+                    contours,
+                    font_rect,
+                    uv_rect,
+                    self.internal_face.units_per_em(),
+                );
+                let contours = recolor_contours(contours, Angle::degrees(3.0), 1);
+                let msdf = compute_msdf(&contours, self.cache.get_dimensions() as usize);
+
+                self.cache.set_texture(c, msdf);
+            }
+        }
+    }
+
+    pub fn get_data_to_process<'b>(&'b mut self) -> Vec<(char, usize, &'b Vec<u8>)> {
+        let data = self
+            .cache
+            .needs_processing
+            .iter()
+            .filter_map(|unprocessed_id| {
+                if let Some(c) = self.cache.id_to_char_mappings.get(unprocessed_id) {
+                    if let Some((_, data)) = self.cache.chars.get(c) {
+                        return Some((*c, *unprocessed_id, data));
+                    }
+                }
+
+                None
+            })
+            .collect();
+
+        self.cache.needs_processing.clear();
+
+        data
+    }
+
+    // Checks the given chars and returns ones that haven't been seen before.
+    pub fn check_chars(&self, chars: std::str::Chars<'_>) -> Vec<char> {
+        chars
+            .into_iter()
+            .filter(|c| !self.cache.chars.contains_key(&c))
+            .collect()
+    }
+}
diff --git a/kayak_font/src/lib.rs b/kayak_font/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..07499ca10ba58fe9523356b58c8b10e5d9f8b379
--- /dev/null
+++ b/kayak_font/src/lib.rs
@@ -0,0 +1,27 @@
+mod color_flags;
+mod contour;
+mod font;
+mod msdf;
+mod path_collector;
+mod path_element;
+mod recolor;
+mod sdf;
+mod ttf_parser;
+mod utils;
+
+pub use color_flags::ColorFlags;
+pub use contour::{rescale_contours, Contour};
+pub use font::{Font, FontCache};
+pub use lyon_geom::math::{Angle, Point, Rect, Vector};
+pub use lyon_path::builder::FlatPathBuilder;
+pub use msdf::compute_msdf;
+pub use path_collector::PathCollector;
+pub use path_element::PathElement;
+pub use recolor::recolor_contours;
+pub use sdf::compute_sdf;
+
+pub(crate) fn median(a: f32, b: f32, c: f32) -> f32 {
+    let min = |a: f32, b: f32| a.min(b);
+    let max = |a: f32, b: f32| a.max(b);
+    max(min(a, b), min(max(a, b), c))
+}
diff --git a/kayak_font/src/msdf.rs b/kayak_font/src/msdf.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8429e93d4e4fa611b56a998517245cd3676abb84
--- /dev/null
+++ b/kayak_font/src/msdf.rs
@@ -0,0 +1,171 @@
+use lyon_geom::math::Vector;
+
+use crate::{contour::Contour, median, utils::EdgeDistance, ColorFlags};
+
+/// Computes an MSDF from a list of contours. The returned vectors are a `dim` by `dim`
+/// matrix of signed distance values. The output represents the signed distances to the contours
+/// within [0, 1] x [0, 1]
+pub fn compute_msdf(contours: &[Contour], dim: usize) -> Vec<Vec<(f32, f32, f32)>> {
+    #[derive(Copy, Clone, PartialEq)]
+    struct MultiDistance {
+        r: f32,
+        g: f32,
+        b: f32,
+        med: f32,
+    }
+    impl MultiDistance {
+        fn new(v: f32) -> Self {
+            Self {
+                r: v,
+                g: v,
+                b: v,
+                med: v,
+            }
+        }
+    }
+    let scale: f32 = 1.0 / (dim as f32);
+    let windings: Vec<i32> = contours.iter().map(|c| c.winding() as i32).collect();
+
+    (0..dim)
+        .map(|y| {
+            let py = (y as f32 + 0.5) * scale;
+            (0..dim)
+                .map(|x| {
+                    // We assume there is at least 1 contour
+                    // If there isn't make everything magenta
+                    if contours.len() == 0 {
+                        return (1.0f32, 0.0, 1.0);
+                    }
+
+                    let px = (x as f32 + 0.5) * scale;
+                    let p = Vector::new(px, py);
+
+                    let mut neg_dist = 1e24f32;
+                    let mut pos_dist = -1e24f32;
+                    let mut d = 1e24f32;
+                    let mut winding = 0;
+                    let mut contour_distances = Vec::new();
+                    contour_distances.reserve(contours.len());
+
+                    let mut sr = EdgeDistance::new();
+                    let mut sg = EdgeDistance::new();
+                    let mut sb = EdgeDistance::new();
+
+                    for (i, contour) in contours.iter().enumerate() {
+                        let mut contour_min_r = EdgeDistance::new();
+                        let mut contour_min_g = EdgeDistance::new();
+                        let mut contour_min_b = EdgeDistance::new();
+
+                        for elem in &contour.elements {
+                            let (d, na) = elem.distance(p.to_point());
+
+                            if elem.color.contains(ColorFlags::RED) && d < contour_min_r.dist {
+                                contour_min_r.dist = d;
+                                contour_min_r.edge = Some(&elem);
+                                contour_min_r.nearest_approach = na;
+                            }
+                            if elem.color.contains(ColorFlags::GREEN) && d < contour_min_g.dist {
+                                contour_min_g.dist = d;
+                                contour_min_g.edge = Some(&elem);
+                                contour_min_g.nearest_approach = na;
+                            }
+                            if elem.color.contains(ColorFlags::BLUE) && d < contour_min_b.dist {
+                                contour_min_b.dist = d;
+                                contour_min_b.edge = Some(&elem);
+                                contour_min_b.nearest_approach = na;
+                            }
+                        }
+
+                        if contour_min_r.dist < sr.dist {
+                            sr = contour_min_r;
+                        }
+                        if contour_min_g.dist < sg.dist {
+                            sg = contour_min_g;
+                        }
+                        if contour_min_b.dist < sb.dist {
+                            sb = contour_min_b;
+                        }
+
+                        let med_min_dist = median(
+                            contour_min_r.dist.distance,
+                            contour_min_g.dist.distance,
+                            contour_min_b.dist.distance,
+                        )
+                        .abs();
+                        if med_min_dist < d {
+                            d = med_min_dist;
+                            winding = -windings[i];
+                        }
+
+                        contour_min_r.to_pseudodistance(p);
+                        contour_min_g.to_pseudodistance(p);
+                        contour_min_b.to_pseudodistance(p);
+
+                        let med_min_dist = median(
+                            contour_min_r.dist.distance,
+                            contour_min_g.dist.distance,
+                            contour_min_b.dist.distance,
+                        );
+
+                        let mut msd = MultiDistance::new(med_min_dist);
+                        msd.r = contour_min_r.dist.distance;
+                        msd.g = contour_min_g.dist.distance;
+                        msd.b = contour_min_b.dist.distance;
+                        msd.med = med_min_dist;
+                        contour_distances.push(msd);
+                        if windings[i] > 0
+                            && med_min_dist >= 0.0
+                            && med_min_dist.abs() < pos_dist.abs()
+                        {
+                            pos_dist = med_min_dist;
+                        }
+                        if windings[i] < 0
+                            && med_min_dist <= 0.0
+                            && med_min_dist.abs() < neg_dist.abs()
+                        {
+                            neg_dist = med_min_dist;
+                        }
+                    }
+
+                    assert!(contour_distances.len() == windings.len());
+
+                    sr.to_pseudodistance(p);
+                    sg.to_pseudodistance(p);
+                    sb.to_pseudodistance(p);
+
+                    let mut mmsd = MultiDistance::new(-1e24);
+                    if pos_dist >= 0.0 && pos_dist.abs() <= neg_dist.abs() {
+                        mmsd.med = -1e24;
+                        winding = 1;
+                        for (csd, cw) in contour_distances.iter().zip(windings.iter()) {
+                            if *cw > 0 && csd.med > mmsd.med && csd.med.abs() < neg_dist.abs() {
+                                mmsd = *csd;
+                            }
+                        }
+                    } else if neg_dist <= 0.0 && neg_dist.abs() <= pos_dist.abs() {
+                        mmsd.med = 1e24;
+                        winding = -1;
+                        for (csd, cw) in contour_distances.iter().zip(windings.iter()) {
+                            if *cw < 0 && csd.med < mmsd.med && csd.med.abs() < pos_dist.abs() {
+                                mmsd = *csd;
+                            }
+                        }
+                    }
+                    for (csd, w) in contour_distances.iter().zip(windings.iter()) {
+                        if *w != winding && csd.med.abs() < mmsd.med.abs() {
+                            mmsd = *csd;
+                        }
+                    }
+
+                    if median(sr.dist.distance, sg.dist.distance, sb.dist.distance) == mmsd.med {
+                        mmsd.r = sr.dist.distance;
+                        mmsd.g = sg.dist.distance;
+                        mmsd.b = sb.dist.distance;
+                    }
+
+                    (mmsd.r / 0.5, mmsd.g / 0.5, mmsd.b / 0.5)
+                })
+                .collect()
+        })
+        .collect()
+}
diff --git a/kayak_font/src/path_collector.rs b/kayak_font/src/path_collector.rs
new file mode 100644
index 0000000000000000000000000000000000000000..077bc44911cf62a3f6db4483a20852c31a446495
--- /dev/null
+++ b/kayak_font/src/path_collector.rs
@@ -0,0 +1,134 @@
+use lyon_geom::math::{Angle, Point, Vector};
+use lyon_path::{
+    builder::{FlatPathBuilder, PathBuilder},
+    Segment,
+};
+
+use crate::{color_flags::ColorFlags, contour::Contour, path_element::PathElement};
+
+/// This is a path collector which produces our custom contour type.
+pub struct PathCollector {
+    /// The start point of the last contour
+    pub(crate) contour_start: Point,
+    /// The current pen location
+    pub(crate) pen: Point,
+    /// in-flight path elements
+    pub(crate) elements: Vec<PathElement>,
+    /// Completed contours
+    pub(crate) contours: Vec<Contour>,
+    pub scale: f32,
+}
+
+impl PathCollector {
+    pub fn new() -> Self {
+        Self {
+            contour_start: Point::new(0.0, 0.0),
+            pen: Point::new(0.0, 0.0),
+            elements: Vec::new(),
+            contours: Vec::new(),
+            scale: 1.0,
+        }
+    }
+}
+
+impl PathBuilder for PathCollector {
+    fn quadratic_bezier_to(&mut self, ctrl: Point, to: Point) {
+        self.elements.push(PathElement::new(
+            Segment::Quadratic(lyon_geom::QuadraticBezierSegment {
+                from: self.pen * self.scale,
+                to: to * self.scale,
+                ctrl: ctrl * self.scale,
+            }),
+            ColorFlags::WHITE,
+        ));
+        self.pen = to;
+    }
+
+    fn cubic_bezier_to(&mut self, ctrl1: Point, ctrl2: Point, to: Point) {
+        self.elements.push(PathElement::new(
+            Segment::Cubic(lyon_geom::CubicBezierSegment {
+                from: self.pen * self.scale,
+                to: to * self.scale,
+                ctrl1: ctrl1 * self.scale,
+                ctrl2: ctrl2 * self.scale,
+            }),
+            ColorFlags::WHITE,
+        ));
+
+        self.pen = to;
+    }
+
+    fn arc(&mut self, _center: Point, _radii: Vector, _sweep_angle: Angle, _x_rotation: Angle) {
+        unimplemented!()
+    }
+}
+
+impl FlatPathBuilder for PathCollector {
+    type PathType = Vec<Contour>;
+
+    fn move_to(&mut self, to: Point) {
+        self.pen = to * self.scale;
+        self.contour_start = to * self.scale;
+    }
+
+    fn line_to(&mut self, to: Point) {
+        self.elements.push(PathElement::new(
+            Segment::Line(lyon_geom::LineSegment {
+                from: self.pen * self.scale,
+                to: to * self.scale,
+            }),
+            ColorFlags::WHITE,
+        ));
+        self.pen = to * self.scale;
+    }
+
+    fn close(&mut self) {
+        if (self.pen - self.contour_start).length() > 1E-14 {
+            self.elements.push(PathElement::new(
+                Segment::Line(lyon_geom::LineSegment {
+                    from: self.pen * self.scale,
+                    to: self.contour_start * self.scale,
+                }),
+                ColorFlags::WHITE,
+            ));
+        }
+
+        self.pen = self.contour_start;
+        let elements = std::mem::replace(&mut self.elements, Vec::new());
+
+        self.contours.push(Contour { elements });
+    }
+
+    fn build(self) -> Self::PathType {
+        let mut contours = self.contours;
+        if self.elements.len() > 0 {
+            let final_contour = Contour {
+                elements: self.elements,
+            };
+
+            contours.push(final_contour);
+        }
+
+        contours
+    }
+
+    fn build_and_reset(&mut self) -> Self::PathType {
+        let elements = std::mem::replace(&mut self.elements, Vec::new());
+        if elements.len() > 0 {
+            let final_contour = Contour { elements };
+
+            self.contours.push(final_contour);
+        }
+
+        let tr = std::mem::replace(&mut self.contours, Vec::new());
+
+        self.contour_start = Point::new(0.0, 0.0);
+        self.pen = Point::new(0.0, 0.0);
+
+        tr
+    }
+
+    fn current_position(&self) -> Point {
+        self.pen
+    }
+}
diff --git a/kayak_font/src/path_element.rs b/kayak_font/src/path_element.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7500fccbbf88b412570d39a17ab2517b3c213e20
--- /dev/null
+++ b/kayak_font/src/path_element.rs
@@ -0,0 +1,191 @@
+use crate::{color_flags::ColorFlags, utils::AugmentedDistance};
+use lyon_geom::math::{Point, Vector};
+use lyon_path::Segment;
+
+#[derive(Clone, Debug, Copy)]
+pub struct PathElement {
+    pub segment: Segment,
+    pub color: ColorFlags,
+}
+
+impl PathElement {
+    pub fn new(segment: Segment, color: ColorFlags) -> PathElement {
+        Self { segment, color }
+    }
+
+    pub fn sample(&self, t: f32) -> Point {
+        match self.segment {
+            Segment::Line(s) => s.sample(t),
+            Segment::Quadratic(s) => s.sample(t),
+            Segment::Cubic(s) => s.sample(t),
+            Segment::Arc(s) => s.sample(t),
+        }
+    }
+
+    pub fn direction(&self, f: f32) -> Vector {
+        use lyon_geom::Segment as SegmentTrait;
+        let f = f.min(1.0).max(0.0);
+        match self.segment {
+            Segment::Line(s) => s.derivative(f),
+            Segment::Quadratic(s) => s.derivative(f),
+            Segment::Cubic(s) => s.derivative(f),
+            Segment::Arc(s) => s.derivative(f),
+        }
+    }
+
+    /// Split a path element into 3rds
+    pub fn split_in_thirds(&self) -> [PathElement; 3] {
+        macro_rules! segment_case {
+            ($i:expr, $s:expr) => {{
+                let (a, b) = ($i).split(1.0 / 3.0);
+                let (b, c) = b.split(1.0 / 2.0);
+
+                [a, b, c]
+                    .into_iter()
+                    .map(|x| PathElement::new(($s)(x), self.color))
+                    .collect()
+            }};
+        }
+        let segments: arrayvec::ArrayVec<PathElement, 3> = match self.segment {
+            Segment::Line(s) => segment_case!(s, Segment::Line),
+            Segment::Quadratic(s) => segment_case!(s, Segment::Quadratic),
+            Segment::Cubic(s) => segment_case!(s, Segment::Cubic),
+            Segment::Arc(s) => segment_case!(s, Segment::Arc),
+        };
+
+        segments
+            .into_inner()
+            .expect("We should have precisely the right capacity")
+    }
+
+    /// Computes the distance from p to this path element
+    /// Returns the distance from the point to this path element,
+    /// and the distance along this element to the closest point.
+    pub fn distance(&self, p: Point) -> (AugmentedDistance, f32) {
+        use lyon_geom::{LineSegment, QuadraticBezierSegment};
+        match self.segment {
+            Segment::Line(LineSegment { from: s, to: e }) => {
+                let aq = p - s;
+                let ab = e - s;
+                let f = aq.dot(ab) / ab.dot(ab);
+                let eq = if f >= 0.5 { p - e } else { p - s };
+
+                let dist_to_endpoint = eq.length();
+                let endpoint_sd = AugmentedDistance::new(
+                    aq.cross(ab).signum() * dist_to_endpoint,
+                    // ab.normalize().cross(eq.normalize()),
+                    ab.normalize().dot(eq.normalize()).abs(),
+                );
+
+                if 0.0 < f && f < 1.0 {
+                    let ortho = Vector::new(ab.y, -ab.x).normalize();
+                    let ortho_dist = ortho.dot(aq);
+                    if ortho_dist.abs() < endpoint_sd.distance.abs() {
+                        (AugmentedDistance::new(ortho_dist, 0.0), f)
+                    } else {
+                        (endpoint_sd, f)
+                    }
+                } else {
+                    (endpoint_sd, f)
+                }
+            }
+
+            Segment::Quadratic(QuadraticBezierSegment {
+                from: p0,
+                ctrl: p1,
+                to: p2,
+            }) => {
+                use lyon_geom::utils::cubic_polynomial_roots;
+                let qa = p0 - p;
+                let ab = p1 - p0;
+                let br = (p0 - p1) + (p2 - p1);
+                let a = br.dot(br);
+                let b = 3.0 * ab.dot(br);
+                let c = 2.0 * ab.dot(ab) + qa.dot(br);
+                let d = qa.dot(ab);
+                let solutions = cubic_polynomial_roots(a, b, c, d);
+
+                let mut min_dist = ab.cross(qa).signum() * qa.length();
+
+                let mut f = -qa.dot(ab) / ab.dot(ab);
+                {
+                    let ec = p2 - p1;
+                    let ep = p2 - p;
+                    let dist = ec.cross(ep).signum() * ep.length();
+                    if dist.abs() < min_dist.abs() {
+                        min_dist = dist;
+                        f = (p - p1).dot(ec) / ec.dot(ec);
+                    }
+                }
+                for t in solutions {
+                    if t <= 0.0 || 1.0 <= t {
+                        continue;
+                    }
+                    let endpoint = p0 + (ab * 2.0 * t) + (br * t * t);
+                    let delta = endpoint - p;
+                    let dist = (p2 - p0).cross(delta).signum() * delta.length();
+
+                    if dist.abs() < min_dist.abs() {
+                        min_dist = dist;
+                        f = t;
+                    }
+                }
+
+                if 0.0 <= f && f <= 1.0 {
+                    (AugmentedDistance::new(min_dist, 0.0), f)
+                // (AugmentedDistance::new(200f32, 0.0), f)
+                } else if f < 0.5 {
+                    (
+                        AugmentedDistance::new(min_dist, ab.normalize().dot(qa.normalize()).abs()),
+                        f,
+                    )
+                } else {
+                    (
+                        AugmentedDistance::new(
+                            min_dist,
+                            (p2 - p1).normalize().dot((p2 - p).normalize()).abs(),
+                        ),
+                        f,
+                    )
+                }
+            }
+
+            _ => unimplemented!(),
+        }
+    }
+
+    pub(crate) fn to_psuedodistance(
+        &self,
+        dist: AugmentedDistance,
+        p: Vector,
+        near: f32,
+    ) -> AugmentedDistance {
+        if near <= 0.0 {
+            let dir = self.direction(0.0).normalize();
+            let aq = p - self.sample(0.0).to_vector();
+            let ts = aq.dot(dir);
+            if ts < 0.0 {
+                let ds = aq.cross(dir);
+                if ds.abs() <= dist.distance.abs() {
+                    return AugmentedDistance::new(ds, 0.0);
+                }
+            }
+
+            dist
+        } else if near >= 1.0 {
+            let dir = self.direction(1.0).normalize();
+            let aq = p - self.sample(1.0).to_vector();
+            let ts = aq.dot(dir);
+            if ts > 0.0 {
+                let ds = aq.cross(dir);
+                if ds.abs() <= dist.distance.abs() {
+                    return AugmentedDistance::new(ds, 0.0);
+                }
+            }
+
+            dist
+        } else {
+            dist
+        }
+    }
+}
diff --git a/kayak_font/src/recolor.rs b/kayak_font/src/recolor.rs
new file mode 100644
index 0000000000000000000000000000000000000000..61cbbe89dea590befa760dc7b36d9ea46451a277
--- /dev/null
+++ b/kayak_font/src/recolor.rs
@@ -0,0 +1,124 @@
+use lyon_geom::math::{Angle, Vector};
+
+use crate::contour::Contour;
+use crate::ColorFlags;
+
+/// Recolor the contours prior to MSDF computation.
+/// This function uses a simple technique,
+/// again based on Viktor's implementation.
+/// It is left as a separate step so you can implement more complex techniques as desired.
+pub fn recolor_contours(contours: Vec<Contour>, threshold: Angle, mut seed: u64) -> Vec<Contour> {
+    let (threshold, _) = threshold.sin_cos();
+
+    // Determine if a point is a corner, assuming i and o are incoming and
+    // outgoing normalized direction vectors
+    let is_corner = |i: Vector, o: Vector| {
+        let d = i.dot(o); /* |i| |o| cos(t) */
+        let c = i.cross(o).abs(); /* |i| |o| sin(t) */
+
+        // if this corner turns more than 90 degrees (detected by dot product/cos)
+        // or if it turns more than the threshold angle (detected by cross product/sin)
+        (d <= 0.0) || (c > threshold)
+    };
+    contours
+        .into_iter()
+        .map(|mut c| {
+            let mut corners = Vec::new();
+            let n = c.elements.len();
+            // Find all the corners
+            if n != 0 {
+                let mut prev_dir = c.elements[n - 1].direction(1.0).normalize();
+                for (i, e) in c.elements.iter().enumerate() {
+                    let c_dir = e.direction(0.0).normalize();
+                    if is_corner(prev_dir, c_dir) {
+                        corners.push(i)
+                    }
+                    prev_dir = e.direction(1.0).normalize();
+                }
+            }
+
+            match corners.len() {
+                0 => {
+                    // The whole contour is smooth, and we initialized all colors to white.
+                    // No work to do
+                    c
+                }
+                1 => {
+                    // "Teardrop" case: there is only one sharp corner so we
+                    // just pick 3 colors up front and cycle through them
+                    let mut colors = [
+                        (ColorFlags::WHITE).switch(&mut seed),
+                        ColorFlags::WHITE,
+                        ColorFlags::WHITE,
+                    ];
+                    colors[1] = colors[0].switch(&mut seed);
+                    colors[2] = colors[1].switch(&mut seed);
+                    let corner = corners[0];
+                    match n {
+                        0 => {
+                            unreachable!();
+                        }
+                        1 => {
+                            // Only a single edge segment, but it's a teardrop.
+                            // We split it in 3 to make the colors happen
+                            let mut split = c.elements[0].split_in_thirds();
+                            split[0].color = colors[0];
+                            split[1].color = colors[1];
+                            split[2].color = colors[2];
+
+                            c.elements.clear();
+                            c.elements.extend_from_slice(&split);
+                        }
+                        2 => {
+                            // 2 segments. We split it into 6, and assign colors by hand
+                            let mut split0 = c.elements[0].split_in_thirds();
+                            let mut split1 = c.elements[1].split_in_thirds();
+                            split0[0].color = colors[0];
+                            split0[1].color = colors[0];
+                            split0[2].color = colors[1];
+                            split1[0].color = colors[1];
+                            split1[1].color = colors[2];
+                            split1[2].color = colors[2];
+
+                            c.elements.clear();
+                            c.elements.extend_from_slice(&split0);
+                            c.elements.extend_from_slice(&split1);
+                        }
+                        _ => {
+                            // We have more than 3 edges to rotate colors through
+                            for (i, e) in c.elements.iter_mut().enumerate() {
+                                // ported from this cursed C++ code:
+                                // contour->edges[(corner+i)%m]->color = (colors+1)[int(3+2.875*i/(m-1)-1.4375+.5)-3];
+                                let i = (n + i - corner) % n; // Emulate the ( corner + i) % m
+                                let idx_fractional =
+                                    3.5f32 + 2.875f32 * (i as f32) / ((n - 1) as f32) - 1.4375f32;
+                                let idx = idx_fractional.floor() as usize - 2;
+                                e.color = colors[idx];
+                            }
+                        }
+                    }
+                    c
+                }
+                _ => {
+                    // We have 2 or more corners
+                    // Cycle through colors, switching whenever we hit another corner
+                    let n_corners = corners.len();
+                    let mut spline = 0;
+                    let start = corners[0];
+                    let mut color = ColorFlags::WHITE;
+                    color = color.switch(&mut seed);
+                    let initial_color = color;
+                    for i in 0..n {
+                        let i = (start + i) % n;
+                        if spline + 1 < n_corners && corners[spline + 1] == i {
+                            spline = spline + 1;
+                            color = color.switch_banned(&mut seed, initial_color);
+                        }
+                        c.elements[i].color = color;
+                    }
+                    c
+                }
+            }
+        })
+        .collect()
+}
diff --git a/kayak_font/src/sdf.rs b/kayak_font/src/sdf.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c1cd976de29931814a94af5857e10d47bbf65f0a
--- /dev/null
+++ b/kayak_font/src/sdf.rs
@@ -0,0 +1,87 @@
+use lyon_geom::math::Vector;
+
+use crate::{contour::Contour, utils::EdgeDistance};
+
+/// Computes a SDF from a list of contours. The returned vectors are a `dim` by `dim`
+/// matrix of signed distances. The output represents the signed distances to the contours
+/// within [0, 1] x [0, 1]
+pub fn compute_sdf(contours: &[Contour], dim: usize) -> Vec<Vec<f32>> {
+    let scale: f32 = 1.0 / (dim as f32);
+    let windings: Vec<i32> = contours.iter().map(|c| c.winding() as i32).collect();
+
+    (0..dim)
+        .map(|y| {
+            let py = (y as f32 + 0.5) * scale;
+            (0..dim)
+                .map(|x| {
+                    if contours.len() == 0 {
+                        return 1.0f32;
+                    }
+
+                    let px = (x as f32 + 0.5) * scale;
+                    let p = Vector::new(px, py);
+
+                    let mut neg_dist = 1e24f32;
+                    let mut pos_dist = -1e24f32;
+                    let mut winding = 0;
+                    let mut contour_distances = Vec::new();
+                    contour_distances.reserve(contours.len());
+
+                    for (i, contour) in contours.iter().enumerate() {
+                        let mut contour_min = EdgeDistance::new();
+
+                        for elem in contour.elements.iter() {
+                            let (d, na) = elem.distance(p.to_point());
+
+                            if d < contour_min.dist {
+                                contour_min.dist = d;
+                                contour_min.edge = Some(&elem);
+                                contour_min.nearest_approach = na;
+                            }
+                        }
+
+                        // contour_min.to_pseudodistance(p);
+                        let cmdd = contour_min.dist.distance;
+
+                        contour_distances.push(cmdd);
+
+                        if windings[i] > 0 && cmdd >= 0.0 && cmdd.abs() < pos_dist.abs() {
+                            pos_dist = cmdd;
+                        }
+                        if windings[i] < 0 && cmdd <= 0.0 && cmdd.abs() < neg_dist.abs() {
+                            neg_dist = cmdd;
+                        }
+                    }
+
+                    assert!(contour_distances.len() == windings.len());
+
+                    let mut md = -1e24;
+                    if pos_dist >= 0.0 && pos_dist.abs() <= neg_dist.abs() {
+                        md = pos_dist;
+                        winding = 1;
+                        for (d, w) in contour_distances.iter().zip(windings.iter()) {
+                            if *w > 0 && *d > md && d.abs() < neg_dist.abs() {
+                                md = *d;
+                            }
+                        }
+                    } else if neg_dist <= 0.0 && neg_dist.abs() <= pos_dist.abs() {
+                        md = neg_dist;
+                        winding = -1;
+                        for (d, w) in contour_distances.iter().zip(windings.iter()) {
+                            if *w < 0 && *d < md && d.abs() < pos_dist.abs() {
+                                md = *d;
+                            }
+                        }
+                    }
+                    for (c, w) in contour_distances.iter().zip(windings.iter()) {
+                        if *w != winding && c.abs() < md.abs() {
+                            md = *c;
+                        }
+                    }
+
+                    md / 0.5
+                })
+                .collect()
+        })
+        .collect()
+}
diff --git a/kayak_font/src/ttf_parser.rs b/kayak_font/src/ttf_parser.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4c8607ca5ee2ae16ae07e9ac8914d18bbb59a615
--- /dev/null
+++ b/kayak_font/src/ttf_parser.rs
@@ -0,0 +1,69 @@
+use lyon_geom::math::Point;
+use lyon_path::Segment;
+
+use crate::{ColorFlags, Contour, PathCollector, PathElement};
+
+impl ttf_parser::OutlineBuilder for PathCollector {
+    fn move_to(&mut self, x: f32, y: f32) {
+        let to = Point::new(x, y);
+        self.pen = to;
+        self.contour_start = to;
+    }
+
+    fn line_to(&mut self, x: f32, y: f32) {
+        let to = Point::new(x, y) * self.scale;
+        self.elements.push(PathElement::new(
+            Segment::Line(lyon_geom::LineSegment { from: self.pen, to }),
+            ColorFlags::WHITE,
+        ));
+        self.pen = to;
+    }
+
+    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
+        let ctrl = Point::new(x1, y1) * self.scale;
+        let to = Point::new(x, y) * self.scale;
+        self.elements.push(PathElement::new(
+            Segment::Quadratic(lyon_geom::QuadraticBezierSegment {
+                from: self.pen,
+                to,
+                ctrl,
+            }),
+            ColorFlags::WHITE,
+        ));
+        self.pen = to;
+    }
+
+    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
+        let ctrl1 = Point::new(x1, y1) * self.scale;
+        let ctrl2 = Point::new(x2, y2) * self.scale;
+        let to = Point::new(x, y) * self.scale;
+        self.elements.push(PathElement::new(
+            Segment::Cubic(lyon_geom::CubicBezierSegment {
+                from: self.pen,
+                to,
+                ctrl1,
+                ctrl2,
+            }),
+            ColorFlags::WHITE,
+        ));
+
+        self.pen = to;
+    }
+
+    fn close(&mut self) {
+        if (self.pen - self.contour_start).length() > 1E-14 {
+            self.elements.push(PathElement::new(
+                Segment::Line(lyon_geom::LineSegment {
+                    from: self.pen * self.scale,
+                    to: self.contour_start * self.scale,
+                }),
+                ColorFlags::WHITE,
+            ));
+        }
+
+        self.pen = self.contour_start;
+        let elements = std::mem::replace(&mut self.elements, Vec::new());
+
+        self.contours.push(Contour { elements });
+    }
+}
diff --git a/kayak_font/src/utils.rs b/kayak_font/src/utils.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5e8e35fc6e0a8cbc7f0da38abb0c571a0039a86c
--- /dev/null
+++ b/kayak_font/src/utils.rs
@@ -0,0 +1,61 @@
+use lyon_path::math::Vector;
+
+use crate::path_element::PathElement;
+
+/// Represents a distance to an edge segment
+#[derive(Copy, Clone)]
+pub(crate) struct EdgeDistance<'a> {
+    pub(crate) dist: AugmentedDistance,
+    pub(crate) edge: Option<&'a PathElement>,
+    pub(crate) nearest_approach: f32,
+}
+
+impl<'a> EdgeDistance<'a> {
+    /// Create a new edge point, initialized to infinite distance
+    pub(crate) fn new() -> Self {
+        Self {
+            dist: AugmentedDistance::new(-1e24, 0.0),
+            edge: None,
+            nearest_approach: 0.0,
+        }
+    }
+
+    pub(crate) fn to_pseudodistance(&mut self, p: Vector) {
+        match self.edge {
+            Some(edge) => self.dist = edge.to_psuedodistance(self.dist, p, self.nearest_approach),
+            None => {}
+        }
+    }
+}
+
+/// A signed distance, augmented with the cosine of the angle
+/// between the tangent of the edge and the vector from the
+/// point of nearest approach to the measured point.
+#[derive(Copy, Clone, PartialEq, Debug)]
+pub struct AugmentedDistance {
+    /// The actual distance
+    pub(crate) distance: f32,
+    /// The cosine of the angle between the tangent vector of the path segment
+    /// at the point of closest approach and the vector from the point of
+    /// closest approach to the point to which distance was measured. This is used to
+    dot: f32,
+}
+
+impl AugmentedDistance {
+    pub(crate) fn new(distance: f32, dot: f32) -> Self {
+        Self { distance, dot }
+    }
+}
+
+impl std::cmp::PartialOrd for AugmentedDistance {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        use std::cmp::Ordering;
+
+        match self.distance.abs().partial_cmp(&other.distance.abs()) {
+            Some(Ordering::Less) => Some(Ordering::Less),
+            Some(Ordering::Greater) => Some(Ordering::Greater),
+            Some(Ordering::Equal) => self.dot.partial_cmp(&other.dot),
+            None => None,
+        }
+    }
+}
diff --git a/kayak_render_macros/Cargo.toml b/kayak_render_macros/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..8707f9acbe7899b9d3173147ff3e927b76b78136
--- /dev/null
+++ b/kayak_render_macros/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "kayak_render_macros"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = { version = "1.0", features = ["full"] }
+quote = "1.0"
+proc-macro2 = "1.0"
+proc-macro-error = "1.0"
+
+[dev-dependencies]
+kayak_core = { path = "../kayak_core", version = "0.1.0" }
+pretty_assertions = "0.3.4"
diff --git a/kayak_render_macros/examples/main.rs b/kayak_render_macros/examples/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..75d7cccc218b6da20316f5068594358dc9b1fa02
--- /dev/null
+++ b/kayak_render_macros/examples/main.rs
@@ -0,0 +1,70 @@
+use kayak_core::{context::KayakContext, styles::Style, Children, Index};
+use kayak_core::{derivative::*, Fragment, Widget};
+use kayak_render_macros::rsx;
+
+#[derive(Derivative)]
+#[derivative(Debug, PartialEq)]
+#[allow(dead_code)]
+struct Test {
+    id: Index,
+    styles: Option<Style>,
+    foo: u32,
+    #[derivative(Debug = "ignore", PartialEq = "ignore")]
+    children: Children,
+}
+
+impl Widget for Test {
+    fn get_id(&self) -> Index {
+        todo!()
+    }
+
+    fn set_id(&mut self, _id: Index) {
+        todo!()
+    }
+
+    fn get_styles(&self) -> Option<Style> {
+        todo!()
+    }
+
+    fn render(&mut self, _context: &mut KayakContext) {
+        todo!()
+    }
+}
+
+fn main() {
+    let mut context = KayakContext::new();
+    {
+        let context = &mut context;
+        let foo = 10;
+        let test_styles = Style::default();
+        let parent_id: Option<Index> = None;
+        rsx! {
+            <Fragment>
+                <Test foo={10}>
+                    <Test foo={1}>
+                        <Test foo={5}>
+                            <Test foo={foo} styles={Some(test_styles)}>
+                                {}
+                            </Test>
+                        </Test>
+                    </Test>
+                </Test>
+            </Fragment>
+        };
+
+        let foo = 10;
+        let test_styles = Style::default();
+
+        let parent_id: Option<Index> = None;
+        rsx! {
+            <Fragment>
+                <Test foo={foo} styles={Some(test_styles)}>
+                    {}
+                </Test>
+                <Test foo={5} styles={Some(test_styles)}>
+                    {}
+                </Test>
+            </Fragment>
+        }
+    }
+}
diff --git a/kayak_render_macros/src/arc_function.rs b/kayak_render_macros/src/arc_function.rs
new file mode 100644
index 0000000000000000000000000000000000000000..bc5c1ad9a04775e78349608ea68ca35390162608
--- /dev/null
+++ b/kayak_render_macros/src/arc_function.rs
@@ -0,0 +1,28 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+
+pub fn build_arc_function(
+    widget_name: TokenStream,
+    children_quotes: TokenStream,
+    has_parent: bool,
+    index: usize,
+) -> TokenStream {
+    let parent = if has_parent {
+        quote! { parent_id }
+    } else {
+        quote! { None }
+    };
+
+    quote! {
+        let #widget_name = #children_quotes;
+        let (should_rerender, child_id) =
+            context
+                .widget_manager
+                .create_widget(#index, #widget_name, #parent);
+        if should_rerender {
+            let mut child_widget = context.widget_manager.take(child_id);
+            child_widget.render(context);
+            context.widget_manager.repossess(child_widget);
+        }
+    }
+}
diff --git a/kayak_render_macros/src/attribute.rs b/kayak_render_macros/src/attribute.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5cedf71fc2f739d7c361a75ad9b8a674b8108558
--- /dev/null
+++ b/kayak_render_macros/src/attribute.rs
@@ -0,0 +1,95 @@
+use quote::quote;
+use std::hash::{Hash, Hasher};
+
+use syn::{
+    ext::IdentExt,
+    parse::{Parse, ParseStream, Result},
+    spanned::Spanned,
+};
+
+pub type AttributeKey = syn::punctuated::Punctuated<syn::Ident, syn::Token![-]>;
+
+#[derive(Debug, Clone)]
+pub enum Attribute {
+    Punned(AttributeKey),
+    WithValue(AttributeKey, syn::Block),
+}
+
+impl Attribute {
+    pub fn ident(&self) -> &AttributeKey {
+        match self {
+            Self::Punned(ident) | Self::WithValue(ident, _) => ident,
+        }
+    }
+
+    pub fn value_tokens(&self) -> proc_macro2::TokenStream {
+        match self {
+            Self::WithValue(_, value) => {
+                if value.stmts.len() == 1 {
+                    let first = &value.stmts[0];
+                    quote!(#first)
+                } else {
+                    quote!(#value)
+                }
+            }
+            Self::Punned(ident) => quote!(#ident),
+        }
+    }
+
+    pub fn idents(&self) -> Vec<&syn::Ident> {
+        self.ident().iter().collect::<Vec<_>>()
+    }
+
+    pub(crate) fn validate(self) -> Result<Self> {
+        if self.idents().len() < 2 {
+            Ok(self)
+        } else {
+            let alternative_name = self
+                .idents()
+                .iter()
+                .map(|x| x.to_string())
+                .collect::<Vec<_>>()
+                .join("_");
+
+            let error_message = format!(
+                "Can't use dash-delimited values on custom components. Did you mean `{}`?",
+                alternative_name
+            );
+
+            Err(syn::Error::new(self.ident().span(), error_message))
+        }
+    }
+}
+
+impl PartialEq for Attribute {
+    fn eq(&self, other: &Self) -> bool {
+        let self_idents: Vec<_> = self.ident().iter().collect();
+        let other_idents: Vec<_> = other.ident().iter().collect();
+        self_idents == other_idents
+    }
+}
+
+impl Eq for Attribute {}
+
+impl Hash for Attribute {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        let ident = self.idents();
+        Hash::hash(&ident, state)
+    }
+}
+
+impl Parse for Attribute {
+    fn parse(input: ParseStream) -> Result<Self> {
+        let name = AttributeKey::parse_separated_nonempty_with(input, syn::Ident::parse_any)?;
+        let not_punned = input.peek(syn::Token![=]);
+
+        if !not_punned {
+            return Ok(Self::Punned(name));
+        }
+
+        input.parse::<syn::Token![=]>()?;
+        let value = input.parse::<syn::Block>()?;
+
+        Ok(Self::WithValue(name, value))
+    }
+}
diff --git a/kayak_render_macros/src/child.rs b/kayak_render_macros/src/child.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ed2c17fac8195413bb8e3e848a9a33e540390b24
--- /dev/null
+++ b/kayak_render_macros/src/child.rs
@@ -0,0 +1,58 @@
+use quote::{quote, ToTokens};
+use syn::parse::{Parse, ParseStream, Result};
+
+use crate::widget::Widget;
+
+#[derive(Debug, Clone)]
+pub enum Child {
+    Widget(Widget),
+    RawBlock(syn::Block),
+}
+
+impl ToTokens for Child {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        match self {
+            Self::Widget(widget) => widget.to_tokens(tokens),
+            Self::RawBlock(block) => {
+                let ts = if block.stmts.len() == 1 {
+                    let first = &block.stmts[0];
+                    quote!(#first)
+                } else {
+                    quote!(#block)
+                };
+                ts.to_tokens(tokens);
+            }
+        }
+    }
+}
+
+impl Parse for Child {
+    fn parse(input: ParseStream) -> Result<Self> {
+        match Widget::custom_parse(input, true, true) {
+            Ok(widget) => Ok(Self::Widget(widget)),
+            Err(_) => {
+                let block = input.parse::<syn::Block>()?;
+                Ok(Self::RawBlock(block))
+            }
+        }
+    }
+}
+
+pub fn walk_block_to_variable(block: &syn::Block) -> Option<proc_macro2::TokenStream> {
+    if let Some(statement) = block.stmts.first() {
+        return walk_statement(statement);
+    }
+
+    return None;
+}
+
+pub fn walk_statement(statement: &syn::Stmt) -> Option<proc_macro2::TokenStream> {
+    match statement {
+        syn::Stmt::Expr(expr) => match expr {
+            syn::Expr::Call(call) => Some(call.args.to_token_stream()),
+            syn::Expr::Path(path) => Some(path.to_token_stream()),
+            _ => None,
+        },
+        _ => None,
+    }
+}
diff --git a/kayak_render_macros/src/children.rs b/kayak_render_macros/src/children.rs
new file mode 100644
index 0000000000000000000000000000000000000000..db5093c63243fa731c745bfd83d6454fa6f4614b
--- /dev/null
+++ b/kayak_render_macros/src/children.rs
@@ -0,0 +1,219 @@
+use crate::{
+    arc_function::build_arc_function,
+    attribute::Attribute,
+    child::{walk_block_to_variable, Child},
+};
+use quote::{quote, ToTokens};
+use syn::parse::{Parse, ParseStream, Result};
+
+#[derive(Debug, Clone)]
+pub struct Children {
+    pub nodes: Vec<Child>,
+}
+
+impl Children {
+    pub fn new(nodes: Vec<Child>) -> Self {
+        Children { nodes }
+    }
+
+    pub fn len(&self) -> usize {
+        self.nodes.len()
+    }
+
+    pub fn get_clonable_attributes(&self, index: usize) -> Vec<proc_macro2::TokenStream> {
+        let mut tokens = Vec::new();
+
+        let regular_tokens: Vec<_> = match &self.nodes[index] {
+            Child::Widget(widget) => widget
+                .attributes
+                .attributes
+                .iter()
+                .filter_map(|attr| match attr {
+                    Attribute::WithValue(_, block) => walk_block_to_variable(block),
+                    _ => None,
+                })
+                .collect(),
+            _ => vec![],
+        };
+        tokens.extend(regular_tokens);
+
+        let children_tokens: Vec<proc_macro2::TokenStream> = match &self.nodes[index] {
+            Child::Widget(widget) => (0..widget.children.nodes.len())
+                .into_iter()
+                .map(|child_id| widget.children.get_clonable_attributes(child_id))
+                .flatten()
+                .collect(),
+            _ => vec![],
+        };
+
+        tokens.extend(children_tokens);
+
+        tokens.dedup_by(|a, b| a.to_string().eq(&b.to_string()));
+
+        tokens
+    }
+
+    pub fn as_option_of_tuples_tokens(&self) -> proc_macro2::TokenStream {
+        let children_quotes: Vec<_> = self
+            .nodes
+            .iter()
+            .map(|child| {
+                quote! { #child }
+            })
+            .collect();
+
+        match children_quotes.len() {
+            0 => quote! { Option::<()>::None },
+            1 => {
+                let child = if children_quotes[0].to_string() == "{ }" {
+                    quote! { None }
+                } else {
+                    let children_attributes: Vec<_> = self.get_clonable_attributes(0);
+
+                    let cloned_attrs = quote! {
+                        #(let #children_attributes = #children_attributes.clone();)*;
+                    };
+                    if children_quotes[0].to_string() == "children" {
+                        quote! {
+                            #(#children_quotes)*.clone()
+                        }
+                    } else {
+                        let children_builder = build_arc_function(
+                            quote! { child_widget },
+                            quote! { #(#children_quotes),* },
+                            true,
+                            0,
+                        );
+                        quote! {
+                            Some(std::sync::Arc::new(move |parent_id: Option<kayak_core::Index>, context: &mut kayak_core::context::KayakContext| {
+                                #cloned_attrs
+                                #children_builder
+                            }))
+                        }
+                    }
+                };
+                quote! {
+                    #child
+                }
+            }
+            _ => {
+                let mut iter = children_quotes.iter();
+
+                let first = iter.next().unwrap();
+                let second = iter.next().unwrap();
+
+                let first = build_arc_function(quote! { child1 }, first.clone(), true, 0);
+                let second = build_arc_function(quote! { child2 }, second.clone(), true, 1);
+
+                let children_attributes0: Vec<_> = self.get_clonable_attributes(0);
+                let children_attributes1: Vec<_> = self.get_clonable_attributes(1);
+                let (children_attributes0, children_attributes1, matching) =
+                    handle_tuple_attributes(&children_attributes0, &children_attributes1);
+
+                let base_matching: Vec<proc_macro2::TokenStream> = matching
+                    .iter()
+                    .map(|a| {
+                        format!("base_{}", a.to_string())
+                            .to_string()
+                            .parse()
+                            .unwrap()
+                    })
+                    .collect();
+
+                let base_clone = quote! {
+                    #(let #base_matching = #matching.clone();)*
+                };
+
+                let base_clones_inner = quote! {
+                    #(let #matching = #base_matching.clone();)*
+                };
+
+                let cloned_attrs0 = quote! {
+                    #(let #children_attributes0 = #children_attributes0.clone();)*
+                };
+                let cloned_attrs1 = quote! {
+                    #(let #children_attributes1 = #children_attributes1.clone();)*
+                };
+
+                let tuple_of_tuples = iter.fold(
+                    quote! {
+                        #base_clone
+                        #cloned_attrs0
+                        #base_clones_inner
+                        #first
+                        #base_clones_inner
+                        #cloned_attrs1
+                        #second
+                    },
+                    |renderable, current| quote!((#renderable, #current)),
+                );
+
+                quote! {
+                    Some(std::sync::Arc::new(move |parent_id: Option<kayak_core::Index>, context: &mut kayak_core::context::KayakContext| {
+                        #tuple_of_tuples
+                    }))
+                }
+            }
+        }
+    }
+}
+
+impl Parse for Children {
+    fn parse(input: ParseStream) -> Result<Self> {
+        let mut nodes = vec![];
+
+        while !input.peek(syn::Token![<]) || !input.peek2(syn::Token![/]) {
+            let child = input.parse::<Child>()?;
+            nodes.push(child);
+        }
+
+        Ok(Self::new(nodes))
+    }
+}
+
+impl ToTokens for Children {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        self.as_option_of_tuples_tokens().to_tokens(tokens);
+    }
+}
+
+// Takes two incoming attribute streams like: "styles foo bar" and "styles" and resolves
+// them into three separate lists like: "foo bar", "", and "styles"
+fn handle_tuple_attributes(
+    a: &Vec<proc_macro2::TokenStream>,
+    b: &Vec<proc_macro2::TokenStream>,
+) -> (
+    Vec<proc_macro2::TokenStream>,
+    Vec<proc_macro2::TokenStream>,
+    Vec<proc_macro2::TokenStream>,
+) {
+    let mut stream1: Vec<String> = a.iter().map(|a| a.to_string()).collect();
+    let mut stream2: Vec<String> = b.iter().map(|b| b.to_string()).collect();
+    let matching1: Vec<&String> = stream1
+        .iter()
+        .filter(|a| stream2.iter().any(|b| *a == b))
+        .collect();
+    let matching2: Vec<&String> = stream2
+        .iter()
+        .filter(|a| stream1.iter().any(|b| *a == b))
+        .collect();
+    let mut matching: Vec<String> = Vec::new();
+    matching.extend(matching1.iter().map(|x| (*x).clone()).collect::<Vec<_>>());
+    matching.extend(matching2.iter().map(|x| (*x).clone()).collect::<Vec<_>>());
+    matching.sort_unstable();
+    matching.dedup_by(|a, b| a.eq(&b));
+
+    stream1 = stream1
+        .into_iter()
+        .filter(|a| !matching.iter().any(|b| a == b))
+        .collect();
+    stream2 = stream2
+        .into_iter()
+        .filter(|a| !matching.iter().any(|b| a == b))
+        .collect();
+
+    let matching = matching.iter().map(|m| m.parse().unwrap()).collect();
+    let stream1 = stream1.iter().map(|m| m.parse().unwrap()).collect();
+    let stream2 = stream2.iter().map(|m| m.parse().unwrap()).collect();
+    (stream1, stream2, matching)
+}
diff --git a/kayak_render_macros/src/function_component.rs b/kayak_render_macros/src/function_component.rs
new file mode 100644
index 0000000000000000000000000000000000000000..55c82f3f44f0d6f323412c60d132bcbee13e4dd9
--- /dev/null
+++ b/kayak_render_macros/src/function_component.rs
@@ -0,0 +1,192 @@
+use proc_macro::TokenStream;
+use proc_macro_error::emit_error;
+use quote::{quote, ToTokens};
+use syn::spanned::Spanned;
+
+pub fn create_function_component(f: syn::ItemFn) -> TokenStream {
+    let struct_name = f.sig.ident;
+    let (impl_generics, ty_generics, where_clause) = f.sig.generics.split_for_impl();
+    let inputs = f.sig.inputs;
+    let block = f.block;
+    let vis = f.vis;
+
+    let mut input_names: Vec<_> = inputs
+        .iter()
+        .filter_map(|argument| match argument {
+            syn::FnArg::Typed(typed) => {
+                let typed_info = typed.ty.to_token_stream().to_string();
+                let attr_info = typed.pat.to_token_stream().to_string();
+                if (typed_info.contains("KayakContext") && !typed_info.contains("Fn"))
+                    || (attr_info.contains("styles") && typed_info.contains("Style"))
+                {
+                    None
+                } else {
+                    Some(typed)
+                }
+            }
+            syn::FnArg::Receiver(rec) => {
+                emit_error!(rec.span(), "Don't use `self` on components");
+                None
+            }
+        })
+        .map(|value| {
+            let pat = &value.pat;
+            quote!(#pat)
+        })
+        .collect();
+
+    // let missing_names = vec!["styles"];
+    // missing_names.iter().for_each(|missing| {
+    //     if !input_names
+    //         .iter()
+    //         .any(|input| input.to_string() == missing.to_string())
+    //     {
+    //         input_names.push(quote! { #missing });
+    //     }
+    // });
+
+    let mut input_block_names: Vec<_> = inputs
+        .iter()
+        .filter(|input| {
+            let input = (quote! { #input }).to_string();
+            !(input.contains("parent_styles")
+                || (input.contains("KayakContext") && !input.contains("Fn")))
+        })
+        .map(|item| quote! { #item })
+        .collect();
+    input_block_names.iter_mut().for_each(|input| {
+        let input_string = (quote! { #input }).to_string();
+        if input_string.contains("children : Children") {
+            *input = quote! {
+                 #[derivative(Debug = "ignore", PartialEq = "ignore")]
+                 pub children: Children
+            };
+        } else {
+            *input = quote! {
+                pub #input
+            }
+        }
+    });
+
+    let missing_struct_inputs = vec![
+        (
+            vec![
+                "styles : Option < Style >",
+                "styles : Option< kayak_core :: styles :: Style >",
+            ],
+            quote! {
+                pub styles: Option<kayak_core::styles::Style>
+            },
+        ),
+        (
+            vec!["children : Children"],
+            quote! {
+                #[derivative(Debug = "ignore", PartialEq = "ignore")]
+                pub children: kayak_core::Children
+            },
+        ),
+    ];
+
+    for (names, token) in missing_struct_inputs {
+        if !input_block_names.iter().any(|block_name| {
+            names
+                .iter()
+                .any(|name| block_name.to_string().contains(name))
+        }) {
+            input_block_names.push(token);
+        } else {
+        }
+    }
+
+    let inputs_block = quote!(
+        #(#input_block_names),*
+    );
+
+    if !input_names
+        .iter()
+        .any(|item_name| item_name.to_string().contains("children"))
+    {
+        input_names.push(quote! {
+            children
+        });
+    }
+
+    let inputs_reading_ref = if inputs.len() == 0 {
+        quote! {
+            let #struct_name { children, styles, .. } = self;
+        }
+    } else {
+        quote!(
+            let #struct_name { #(#input_names),*, styles, .. } = self;
+        )
+    };
+
+    TokenStream::from(quote! {
+        use kayak_core::derivative::*;
+
+        #[derive(Derivative)]
+        #[derivative(Debug, PartialEq)]
+        #vis struct #struct_name #impl_generics {
+            pub id: ::kayak_core::Index,
+            #inputs_block
+        }
+
+        impl #impl_generics ::kayak_core::Widget for #struct_name #ty_generics #where_clause {
+            fn get_id(&self) -> ::kayak_core::Index {
+                self.id
+            }
+
+            fn set_id(&mut self, id: ::kayak_core::Index) {
+                self.id = id;
+            }
+
+            fn get_styles(&self) -> Option<::kayak_core::styles::Style> {
+                self.styles.clone()
+            }
+
+            fn render(&mut self, context: &mut ::kayak_core::context::KayakContext) {
+                dbg!(stringify!(Rendering widget: #struct_name));
+                let parent_id = self.get_id();
+                context.set_current_id(parent_id);
+                let parent_id = Some(parent_id);
+                #inputs_reading_ref
+                let children = children.clone();
+                #block
+            }
+        }
+
+        // impl #impl_generics ::kayak_core::Render for #struct_name #ty_generics #where_clause {
+        //     fn render_into(&self, nodes: &mut Vec<::kayak_core::Node>, context: ::std::sync::Arc<::std::sync::RwLock<::kayak_core::KayakContext>>, parent_styles: Option<kayak_core::Style>) -> Option<usize> {
+        //         let _ = rsx! {
+        //             <>{}</>
+        //         }; // Used to fake out the compiler into thinking we require rsx! still.
+        //         let result = {
+        //             #inputs_reading_ref
+        //             let mut styles = styles.clone();
+        //             let result = if let Ok(mut context) = context.write() {
+        //                 context.set_current_id(self.component_id);
+        //                 #ref_block
+        //             } else { panic!("Couldn't get write lock for context!"); };
+
+        //             (result, styles)
+        //         };
+        //         let child_index = ::kayak_core::Render::render_into(&result.0, nodes, context, result.1.clone());
+        //         let mut builder = ::kayak_core::NodeBuilder::empty()
+        //             .with_id(self.component_id)
+        //             .with_children(vec![
+        //                 child_index
+        //             ].iter().filter_map(|x| *x).collect());
+        //         if let Some(styles) = result.1 {
+        //             let new_styles = if let Some(parent_styles) = parent_styles {
+        //                 styles // parent_styles.merge(&styles)
+        //             } else { styles };
+        //             builder = builder.with_styles(new_styles)
+        //         }
+        //         let node = builder.build();
+        //         let node_index = nodes.len();
+        //         nodes.push(node);
+        //         Some(node_index)
+        // }
+        // }
+    })
+}
diff --git a/kayak_render_macros/src/lib.rs b/kayak_render_macros/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dcedd3dffbd299ec280bf2cac86171f869e12d9e
--- /dev/null
+++ b/kayak_render_macros/src/lib.rs
@@ -0,0 +1,100 @@
+extern crate proc_macro;
+
+mod function_component;
+mod tags;
+
+mod arc_function;
+mod attribute;
+mod partial_eq;
+mod widget;
+mod widget_attributes;
+mod child;
+mod children;
+
+use partial_eq::impl_dyn_partial_eq;
+use proc_macro::TokenStream;
+use proc_macro_error::proc_macro_error;
+use quote::quote;
+use syn::{parse_macro_input, parse_quote, token::Comma};
+
+use crate::widget::Widget;
+
+// use crate::prebuilt::element::Element;
+
+#[proc_macro]
+#[proc_macro_error]
+pub fn render(input: TokenStream) -> TokenStream {
+    let mut input = input.into_iter();
+    let context = proc_macro2::TokenStream::from(TokenStream::from(input.next().unwrap()));
+    let comma_input = TokenStream::from(input.next().unwrap());
+    let _ = parse_macro_input!(comma_input as Comma);
+    let rsx_data = proc_macro2::TokenStream::from_iter(
+        input.map(|token_tree| proc_macro2::TokenStream::from(TokenStream::from(token_tree))),
+    );
+    let el = proc_macro2::TokenStream::from(rsx(TokenStream::from(rsx_data)));
+    let result = quote! { ::kayak_core::Render::render_into(&#el, #context, None) };
+    TokenStream::from(result)
+}
+
+/// Generate a renderable component tree, before rendering it
+#[proc_macro]
+#[proc_macro_error]
+pub fn rsx(input: TokenStream) -> TokenStream {
+    let el = parse_macro_input!(input as Widget);
+    let result = quote! { #el };
+    TokenStream::from(result)
+}
+
+// #[proc_macro]
+// #[proc_macro_error]
+// pub fn rsx_create(input: TokenStream) -> TokenStream {
+//     let el = parse_macro_input!(input as WidgetElement);
+//     let result = quote! { #el };
+//     TokenStream::from(result)
+// }
+
+// #[proc_macro]
+// #[proc_macro_error]
+// pub fn rsx_update(input: TokenStream) -> TokenStream {
+//     let el = parse_macro_input!(input as Element);
+//     let result = quote! { #el };
+//     TokenStream::from(result)
+// }
+
+#[proc_macro_attribute]
+#[proc_macro_error]
+pub fn widget(_attr: TokenStream, item: TokenStream) -> TokenStream {
+    let f = parse_macro_input!(item as syn::ItemFn);
+    function_component::create_function_component(f)
+}
+
+#[proc_macro_derive(DynPartialEq)]
+pub fn dyn_partial_eq_macro_derive(input: TokenStream) -> TokenStream {
+    let ast = syn::parse(input).unwrap();
+
+    impl_dyn_partial_eq(&ast)
+}
+
+#[proc_macro_attribute]
+pub fn dyn_partial_eq(_: TokenStream, input: TokenStream) -> TokenStream {
+    let mut input = parse_macro_input!(input as syn::ItemTrait);
+
+    let name = &input.ident;
+
+    let bound: syn::TypeParamBound = parse_quote! {
+      DynPartialEq
+    };
+
+    input.supertraits.push(bound);
+
+    (quote! {
+      #input
+
+      impl core::cmp::PartialEq for Box<dyn #name> {
+        fn eq(&self, other: &Self) -> bool {
+          self.box_eq(other.as_any())
+        }
+      }
+    })
+    .into()
+}
diff --git a/kayak_render_macros/src/partial_eq.rs b/kayak_render_macros/src/partial_eq.rs
new file mode 100644
index 0000000000000000000000000000000000000000..aa2650084fcc8a1091c5e42084606cd9cc26f6cf
--- /dev/null
+++ b/kayak_render_macros/src/partial_eq.rs
@@ -0,0 +1,16 @@
+extern crate proc_macro;
+
+use proc_macro::TokenStream;
+use quote::quote;
+
+pub fn impl_dyn_partial_eq(ast: &syn::DeriveInput) -> TokenStream {
+  let name = &ast.ident;
+  let gen = quote! {
+      impl DynPartialEq for #name {
+          fn box_eq(&self, other: &dyn core::any::Any) -> bool {
+            other.downcast_ref::<Self>().map_or(false, |a| self == a)
+          }
+      }
+  };
+  gen.into()
+}
diff --git a/kayak_render_macros/src/tags.rs b/kayak_render_macros/src/tags.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fffe931c335ee681fa763b4305381bb724f29c72
--- /dev/null
+++ b/kayak_render_macros/src/tags.rs
@@ -0,0 +1,78 @@
+use crate::widget_attributes::WidgetAttributes;
+use proc_macro_error::abort;
+use quote::quote;
+use syn::parse::{Parse, ParseStream, Result};
+use syn::spanned::Spanned;
+
+pub struct OpenTag {
+    pub name: syn::Path,
+    pub attributes: WidgetAttributes,
+    pub self_closing: bool,
+    pub is_custom_element: bool,
+}
+
+fn name_or_fragment(maybe_name: Result<syn::Path>) -> syn::Path {
+    maybe_name.unwrap_or_else(|_| syn::parse_str::<syn::Path>("::kayak_core::Fragment").unwrap())
+}
+
+fn is_custom_element_name(path: &syn::Path) -> bool {
+    match path.get_ident() {
+        None => true,
+        Some(ident) => {
+            let name = ident.to_string();
+            let first_letter = name.get(0..1).unwrap();
+            first_letter.to_uppercase() == first_letter
+        }
+    }
+}
+
+impl Parse for OpenTag {
+    fn parse(input: ParseStream) -> Result<Self> {
+        input.parse::<syn::Token![<]>()?;
+        let maybe_name = syn::Path::parse_mod_style(input);
+        let name = name_or_fragment(maybe_name);
+        let is_custom_element = is_custom_element_name(&name);
+        let attributes = WidgetAttributes::custom_parse(input)?;
+        let self_closing = input.parse::<syn::Token![/]>().is_ok();
+        input.parse::<syn::Token![>]>()?;
+
+        Ok(Self {
+            name,
+            attributes,
+            self_closing,
+            is_custom_element,
+        })
+    }
+}
+
+pub struct ClosingTag {
+    name: syn::Path,
+}
+
+impl ClosingTag {
+    pub fn validate(&self, open_tag: &OpenTag) {
+        let open_tag_path = &open_tag.name;
+        let open_tag_path_str = quote!(#open_tag_path).to_string();
+        let self_path = &self.name;
+        let self_path_str = quote!(#self_path).to_string();
+        if self_path_str != open_tag_path_str {
+            abort!(
+                self.name.span(),
+                "Expected closing tag for: <{}>",
+                &open_tag_path_str
+            );
+        }
+    }
+}
+
+impl Parse for ClosingTag {
+    fn parse(input: ParseStream) -> Result<Self> {
+        input.parse::<syn::Token![<]>()?;
+        input.parse::<syn::Token![/]>()?;
+        let maybe_name = input.parse::<syn::Path>();
+        input.parse::<syn::Token![>]>()?;
+        Ok(Self {
+            name: name_or_fragment(maybe_name),
+        })
+    }
+}
diff --git a/kayak_render_macros/src/widget.rs b/kayak_render_macros/src/widget.rs
new file mode 100644
index 0000000000000000000000000000000000000000..96d0c703d7621ead8ad5486a71f92e408038bbb9
--- /dev/null
+++ b/kayak_render_macros/src/widget.rs
@@ -0,0 +1,97 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use quote::ToTokens;
+use syn::parse::{Parse, ParseStream, Result};
+
+use crate::arc_function::build_arc_function;
+use crate::children::Children;
+use crate::tags::ClosingTag;
+use crate::{tags::OpenTag, widget_attributes::WidgetAttributes};
+
+#[derive(Debug, Clone)]
+pub struct Widget {
+    name: syn::Path,
+    pub attributes: WidgetAttributes,
+    pub children: Children,
+    declaration: TokenStream,
+}
+
+impl Parse for Widget {
+    fn parse(input: ParseStream) -> Result<Self> {
+        Self::custom_parse(input, false, true)
+    }
+}
+
+impl Widget {
+    pub fn is_custom_element(name: &syn::Path) -> bool {
+        match name.get_ident() {
+            None => true,
+            Some(ident) => {
+                let name = ident.to_string();
+                let first_letter = name.get(0..1).unwrap();
+                first_letter.to_uppercase() == first_letter
+            }
+        }
+    }
+
+    pub fn custom_parse(input: ParseStream, as_prop: bool, has_parent: bool) -> Result<Widget> {
+        let open_tag = input.parse::<OpenTag>()?;
+
+        let children = if open_tag.self_closing {
+            Children::new(vec![])
+        } else {
+            let children = input.parse::<Children>()?;
+            let closing_tag = input.parse::<ClosingTag>()?;
+            closing_tag.validate(&open_tag);
+            children
+        };
+
+        let name = open_tag.name;
+        let declaration = if Self::is_custom_element(&name) {
+            let attrs = &open_tag.attributes.for_custom_element(&children);
+            let attrs = attrs.to_token_stream();
+            // let builder = quote! {
+            //     let built_widget = #name #attrs;
+            //     let (should_rerender, child_id) =
+            //         context
+            //             .widget_manager
+            //             .create_widget(0, built_widget, Some(parent_id));
+            //     if should_rerender {
+            //         let mut child_widget = context.widget_manager.take(child_id);
+            //         child_widget.render(context);
+            //         context.widget_manager.repossess(child_widget);
+            //     }
+            // };
+            // quote! {
+            //     #builder
+            // }
+            if !as_prop {
+                let attrs = quote! { #name #attrs };
+                let widget_block =
+                    build_arc_function(quote! { built_widget }, attrs, has_parent, 0);
+                quote! {
+                    #widget_block
+                }
+            } else {
+                quote! {
+                    #name #attrs
+                }
+            }
+        } else {
+            panic!("Couldn't find widget!");
+        };
+
+        Ok(Widget {
+            name,
+            attributes: open_tag.attributes,
+            children,
+            declaration,
+        })
+    }
+}
+
+impl ToTokens for Widget {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        self.declaration.to_tokens(tokens);
+    }
+}
diff --git a/kayak_render_macros/src/widget_attributes.rs b/kayak_render_macros/src/widget_attributes.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3f1c9db7ba677d7a7e15e0080f8a9d30b2e74551
--- /dev/null
+++ b/kayak_render_macros/src/widget_attributes.rs
@@ -0,0 +1,108 @@
+use proc_macro_error::emit_error;
+use quote::{quote, ToTokens};
+use std::collections::HashSet;
+use syn::{
+    ext::IdentExt,
+    parse::{Parse, ParseStream, Result},
+    spanned::Spanned,
+};
+
+use crate::{attribute::Attribute, children::Children};
+
+#[derive(Debug, Clone)]
+pub struct WidgetAttributes {
+    pub attributes: HashSet<Attribute>,
+}
+
+impl WidgetAttributes {
+    pub fn new(attributes: HashSet<Attribute>) -> Self {
+        Self { attributes }
+    }
+
+    pub fn for_custom_element<'c>(&self, children: &'c Children) -> CustomWidgetAttributes<'_, 'c> {
+        CustomWidgetAttributes {
+            attributes: &self.attributes,
+            children,
+        }
+    }
+
+    pub fn custom_parse(input: ParseStream) -> Result<Self> {
+        let mut parsed_self = input.parse::<Self>()?;
+        let new_attributes: HashSet<Attribute> = parsed_self
+            .attributes
+            .drain()
+            .filter_map(|attribute| match attribute.validate() {
+                Ok(x) => Some(x),
+                Err(err) => {
+                    emit_error!(err.span(), "Invalid attribute: {}", err);
+                    None
+                }
+            })
+            .collect();
+
+        Ok(WidgetAttributes::new(new_attributes))
+    }
+}
+
+impl Parse for WidgetAttributes {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let mut attributes: HashSet<Attribute> = HashSet::new();
+        while input.peek(syn::Ident::peek_any) {
+            let attribute = input.parse::<Attribute>()?;
+            let ident = attribute.ident();
+            if attributes.contains(&attribute) {
+                emit_error!(
+                    ident.span(),
+                    "There is a previous definition of the {} attribute",
+                    quote!(#ident)
+                );
+            }
+            attributes.insert(attribute);
+        }
+        Ok(WidgetAttributes::new(attributes))
+    }
+}
+
+pub struct CustomWidgetAttributes<'a, 'c> {
+    attributes: &'a HashSet<Attribute>,
+    children: &'c Children,
+}
+
+impl<'a, 'c> ToTokens for CustomWidgetAttributes<'a, 'c> {
+    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
+        let mut attrs: Vec<_> = self
+            .attributes
+            .iter()
+            .map(|attribute| {
+                let ident = attribute.ident();
+                let value = attribute.value_tokens();
+
+                quote! {
+                    #ident: #value
+                }
+            })
+            .collect();
+
+        if self.children.len() > 0 {
+            let children_tuple = self.children.as_option_of_tuples_tokens();
+            attrs.push(quote! {
+                children: #children_tuple
+            });
+        }
+        let quoted = if attrs.len() == 0 {
+            quote!({ id: kayak_core::Index::default(), styles: None, children: None })
+        } else {
+            if !self
+                .attributes
+                .iter()
+                .any(|attribute| attribute.ident().to_token_stream().to_string() == "styles")
+            {
+                quote!({ #(#attrs),*, id: kayak_core::Index::default(), styles: None })
+            } else {
+                quote!({ #(#attrs),*, id: kayak_core::Index::default() })
+            }
+        };
+
+        quoted.to_tokens(tokens);
+    }
+}
diff --git a/resources/Roboto-Regular.ttf b/resources/Roboto-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..2b6392ffe8712b9c5450733320cd220d6c0f4bce
Binary files /dev/null and b/resources/Roboto-Regular.ttf differ
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fa4be9fa97d4c8007dbddaf2eb4795472191c51c
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,7 @@
+pub mod components {
+    pub use kayak_components::*;
+}
+
+pub mod core {
+    pub use kayak_core::*;
+}