diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..608db70d773ee717187a3e5c3395b942ef251279
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Changelog 
+
+## Version 0.17.0 (2024)
+
+- Update to cosmic-text 0.11.2
+- Drop a lot of functionalities like placeholders, password fields, undo/redo, etc to keep small core. All functionalities can be restored using internal plugins if users demand (PRs are welcome)
diff --git a/Cargo.lock b/Cargo.lock
index 6083f8c811df97a0b491166c5596e7e6558051ff..7622718f8802d485f2ec8b119f81db720bcc886c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -406,7 +406,7 @@ dependencies = [
 
 [[package]]
 name = "bevy_cosmic_edit"
-version = "0.16.0"
+version = "0.17.0"
 dependencies = [
  "arboard",
  "bevy",
@@ -416,6 +416,7 @@ dependencies = [
  "insta",
  "js-sys",
  "sys-locale",
+ "util",
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "web-sys",
@@ -1323,10 +1324,11 @@ dependencies = [
 
 [[package]]
 name = "cosmic-text"
-version = "0.10.0"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75acbfb314aeb4f5210d379af45ed1ec2c98c7f1790bf57b8a4c562ac0c51b71"
+checksum = "c578f2b9abb4d5f3fbb12aba4008084d435dc6a8425c195cfe0b3594bfea0c25"
 dependencies = [
+ "bitflags 2.4.2",
  "fontdb",
  "libm",
  "log",
@@ -1336,6 +1338,7 @@ dependencies = [
  "self_cell",
  "swash",
  "sys-locale",
+ "ttf-parser",
  "unicode-bidi",
  "unicode-linebreak",
  "unicode-script",
@@ -1640,16 +1643,16 @@ dependencies = [
 
 [[package]]
 name = "fontdb"
-version = "0.15.0"
+version = "0.16.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "020e203f177c0fb250fb19455a252e838d2bbbce1f80f25ecc42402aafa8cd38"
+checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3"
 dependencies = [
  "fontconfig-parser",
  "log",
  "memmap2",
  "slotmap",
  "tinyvec",
- "ttf-parser 0.19.2",
+ "ttf-parser",
 ]
 
 [[package]]
@@ -2137,9 +2140,9 @@ checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
 
 [[package]]
 name = "memmap2"
-version = "0.8.0"
+version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed"
+checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322"
 dependencies = [
  "libc",
 ]
@@ -2409,7 +2412,7 @@ version = "0.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7"
 dependencies = [
- "ttf-parser 0.20.0",
+ "ttf-parser",
 ]
 
 [[package]]
@@ -2736,15 +2739,15 @@ dependencies = [
 
 [[package]]
 name = "rustybuzz"
-version = "0.11.0"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ee8fe2a8461a0854a37101fe7a1b13998d0cfa987e43248e81d2a5f4570f6fa"
+checksum = "f0ae5692c5beaad6a9e22830deeed7874eae8a4e3ba4076fb48e12c56856222c"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.4.2",
  "bytemuck",
  "libm",
  "smallvec",
- "ttf-parser 0.20.0",
+ "ttf-parser",
  "unicode-bidi-mirroring",
  "unicode-ccc",
  "unicode-properties",
@@ -3112,12 +3115,6 @@ dependencies = [
  "wasm-bindgen",
 ]
 
-[[package]]
-name = "ttf-parser"
-version = "0.19.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1"
-
 [[package]]
 name = "ttf-parser"
 version = "0.20.0"
@@ -3184,6 +3181,14 @@ version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
 
+[[package]]
+name = "util"
+version = "0.1.0"
+dependencies = [
+ "bevy",
+ "bevy_cosmic_edit",
+]
+
 [[package]]
 name = "uuid"
 version = "1.7.0"
diff --git a/Cargo.toml b/Cargo.toml
index ef8be70a87bebbc3b01ebfeb2c934ce0d3c7bb3c..c47d624d13b835c3da7c01022953ba56b2733acf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
+workspace = { members = ["util"] }
 [package]
 name = "bevy_cosmic_edit"
-version = "0.16.0"
+version = "0.17.0"
 edition = "2021"
 license = "MIT OR Apache-2.0"
 description = "Bevy cosmic-text multiline text input"
@@ -29,7 +30,7 @@ bevy = { version = "0.13", default-features = false, features = [
   "x11",
   "webgpu",
 ] }
-cosmic-text = { version = "0.10" }
+cosmic-text = { version = "0.11.2" }
 # TODO: move crossbeam to wasm32, once input.rs has separate wasm copy/paste fn
 crossbeam-channel = "0.5.8"
 image = "0.24.6"
@@ -46,6 +47,4 @@ web-sys = { version = "0.3.67", features = ["Clipboard", "Navigator", "Window"]
 
 [dev-dependencies]
 insta = "1.29.0"
-
-[[example]]
-name = "text_input"
+util = {path="./util/"}
diff --git a/examples/basic_sprite.rs b/examples/basic_sprite.rs
index 3b560cd81e11393f45c372e2a9ac0329aed70b8d..bfdbefb28115ce1a89a059afc05b7ba2c01c572a 100644
--- a/examples/basic_sprite.rs
+++ b/examples/basic_sprite.rs
@@ -1,7 +1,12 @@
 use bevy::{prelude::*, window::PrimaryWindow};
 use bevy_cosmic_edit::*;
+use util::{change_active_editor_sprite, deselect_editor_on_esc, print_editor_text};
 
-fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
+fn setup(
+    mut commands: Commands,
+    windows: Query<&Window, With<PrimaryWindow>>,
+    mut font_system: ResMut<CosmicFontSystem>,
+) {
     let primary_window = windows.single();
     let camera_bundle = Camera2dBundle {
         camera: Camera {
@@ -16,17 +21,13 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
     attrs = attrs.family(Family::Name("Victor Mono"));
     attrs = attrs.color(CosmicColor::rgb(0x94, 0x00, 0xD3));
 
-    let scale_factor = primary_window.scale_factor() as f32;
-
     let cosmic_edit = (CosmicEditBundle {
-        metrics: CosmicMetrics {
-            font_size: 14.,
-            line_height: 18.,
-            scale_factor,
-        },
+        buffer: CosmicBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text(
+            &mut font_system,
+            "馃榾馃榾馃榾 x => y",
+            attrs,
+        ),
         text_position: CosmicTextPosition::Center,
-        attrs: CosmicAttrs(AttrsOwned::new(attrs)),
-        text_setter: CosmicText::OneStyle("馃榾馃榾馃榾 x => y".to_string()),
         sprite_bundle: SpriteBundle {
             sprite: Sprite {
                 custom_size: Some(Vec2::new(primary_window.width(), primary_window.height())),
@@ -39,7 +40,7 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
 
     let cosmic_edit = commands.spawn(cosmic_edit).id();
 
-    commands.insert_resource(Focus(Some(cosmic_edit)));
+    commands.insert_resource(FocusedWidget(Some(cosmic_edit)));
 }
 
 fn main() {
@@ -57,5 +58,13 @@ fn main() {
             ..default()
         })
         .add_systems(Startup, setup)
+        .add_systems(
+            Update,
+            (
+                print_editor_text,
+                change_active_editor_sprite,
+                deselect_editor_on_esc,
+            ),
+        )
         .run();
 }
diff --git a/examples/basic_ui.rs b/examples/basic_ui.rs
index 9679eddfbcf66138d18f9b20b2a1b363f6fc72a9..2584d4c57c19414a58f3c2e64d08bf754d66acbe 100644
--- a/examples/basic_ui.rs
+++ b/examples/basic_ui.rs
@@ -1,11 +1,11 @@
-use bevy::{prelude::*, window::PrimaryWindow};
+use bevy::prelude::*;
 use bevy_cosmic_edit::*;
+use util::{change_active_editor_ui, deselect_editor_on_esc, print_editor_text};
 
-fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
-    let primary_window = windows.single();
+fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
     let camera_bundle = Camera2dBundle {
         camera: Camera {
-            clear_color: ClearColorConfig::Custom(Color::WHITE),
+            clear_color: ClearColorConfig::Custom(Color::PINK),
             ..default()
         },
         ..default()
@@ -16,20 +16,16 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
     attrs = attrs.family(Family::Name("Victor Mono"));
     attrs = attrs.color(CosmicColor::rgb(0x94, 0x00, 0xD3));
 
-    let scale_factor = primary_window.scale_factor() as f32;
-
     let cosmic_edit = commands
-        .spawn(CosmicEditBundle {
-            metrics: CosmicMetrics {
-                font_size: 14.,
-                line_height: 18.,
-                scale_factor,
-            },
+        .spawn((CosmicEditBundle {
+            buffer: CosmicBuffer::new(&mut font_system, Metrics::new(20., 20.)).with_rich_text(
+                &mut font_system,
+                vec![("Banana", attrs)],
+                attrs,
+            ),
             text_position: CosmicTextPosition::Center,
-            attrs: CosmicAttrs(AttrsOwned::new(attrs)),
-            text_setter: CosmicText::OneStyle("馃榾馃榾馃榾 x => y".to_string()),
             ..default()
-        })
+        },))
         .id();
 
     commands
@@ -42,8 +38,6 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
                     height: Val::Percent(100.),
                     ..default()
                 },
-                // Needs to be set to prevent a bug where nothing is displayed
-                background_color: Color::WHITE.into(),
                 ..default()
             },
         )
@@ -52,21 +46,7 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
         // texture to the editor's rendered image
         .insert(CosmicSource(cosmic_edit));
 
-    commands.insert_resource(Focus(Some(cosmic_edit)));
-}
-
-fn print_text(
-    text_inputs_q: Query<&CosmicEditor, With<CosmicEditor>>,
-    mut previous_value: Local<String>,
-) {
-    for text_input in text_inputs_q.iter() {
-        let current_text = text_input.get_text();
-        if current_text == *previous_value {
-            return;
-        }
-        *previous_value = current_text.clone();
-        info!("Widget text: {}", current_text);
-    }
+    commands.insert_resource(FocusedWidget(Some(cosmic_edit)));
 }
 
 fn main() {
@@ -84,6 +64,13 @@ fn main() {
             ..default()
         })
         .add_systems(Startup, setup)
-        .add_systems(Update, print_text)
+        .add_systems(
+            Update,
+            (
+                print_editor_text,
+                change_active_editor_ui,
+                deselect_editor_on_esc,
+            ),
+        )
         .run();
 }
diff --git a/examples/bevy_api_testing.rs b/examples/bevy_api_testing.rs
deleted file mode 100644
index 3f72e5ae84f75a85534f6c30a71bb20e0568d9ca..0000000000000000000000000000000000000000
--- a/examples/bevy_api_testing.rs
+++ /dev/null
@@ -1,142 +0,0 @@
-use bevy::{prelude::*, window::PrimaryWindow};
-use bevy_cosmic_edit::*;
-
-fn setup(mut commands: Commands) {
-    commands.spawn(Camera2dBundle::default());
-
-    // UI editor
-    let ui_editor = commands
-        .spawn(CosmicEditBundle {
-            attrs: CosmicAttrs(AttrsOwned::new(
-                Attrs::new().color(bevy_color_to_cosmic(Color::GREEN)),
-            )),
-            max_lines: CosmicMaxLines(1),
-            ..default()
-        })
-        .insert(CosmicEditPlaceholderBundle {
-            text_setter: PlaceholderText(CosmicText::OneStyle("Placeholder".into())),
-            attrs: PlaceholderAttrs(AttrsOwned::new(
-                Attrs::new().color(bevy_color_to_cosmic(Color::rgb_u8(128, 128, 128))),
-            )),
-        })
-        .id();
-
-    commands
-        .spawn(ButtonBundle {
-            style: Style {
-                // Size and position of text box
-                width: Val::Px(300.),
-                height: Val::Px(50.),
-                left: Val::Px(100.),
-                top: Val::Px(100.),
-                ..default()
-            },
-            // needs to be set to prevent a bug where nothing is displayed
-            background_color: BackgroundColor(Color::WHITE),
-            ..default()
-        })
-        .insert(CosmicSource(ui_editor));
-
-    // Sprite editor
-    commands.spawn((CosmicEditBundle {
-        sprite_bundle: SpriteBundle {
-            // Sets size of text box
-            sprite: Sprite {
-                custom_size: Some(Vec2::new(300., 100.)),
-                ..default()
-            },
-            // Position of text box
-            transform: Transform::from_xyz(0., 100., 0.),
-            ..default()
-        },
-        ..default()
-    },));
-
-    commands.insert_resource(Focus(Some(ui_editor)));
-}
-
-fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor {
-    CosmicColor::rgba(
-        (color.r() * 255.) as u8,
-        (color.g() * 255.) as u8,
-        (color.b() * 255.) as u8,
-        (color.a() * 255.) as u8,
-    )
-}
-
-fn change_active_editor_ui(
-    mut commands: Commands,
-    mut interaction_query: Query<
-        (&Interaction, &CosmicSource),
-        (Changed<Interaction>, Without<ReadOnly>),
-    >,
-) {
-    for (interaction, source) in interaction_query.iter_mut() {
-        if let Interaction::Pressed = interaction {
-            commands.insert_resource(Focus(Some(source.0)));
-        }
-    }
-}
-
-fn change_active_editor_sprite(
-    mut commands: Commands,
-    windows: Query<&Window, With<PrimaryWindow>>,
-    buttons: Res<ButtonInput<MouseButton>>,
-    mut cosmic_edit_query: Query<
-        (&mut Sprite, &GlobalTransform, &Visibility, Entity),
-        (With<CosmicEditor>, Without<ReadOnly>),
-    >,
-    camera_q: Query<(&Camera, &GlobalTransform)>,
-) {
-    let window = windows.single();
-    let (camera, camera_transform) = camera_q.single();
-    if buttons.just_pressed(MouseButton::Left) {
-        for (sprite, node_transform, visibility, entity) in &mut cosmic_edit_query.iter_mut() {
-            if visibility == Visibility::Hidden {
-                continue;
-            }
-            let size = sprite.custom_size.unwrap_or(Vec2::ONE);
-            let x_min = node_transform.affine().translation.x - size.x / 2.;
-            let y_min = node_transform.affine().translation.y - size.y / 2.;
-            let x_max = node_transform.affine().translation.x + size.x / 2.;
-            let y_max = node_transform.affine().translation.y + size.y / 2.;
-            if let Some(pos) = window.cursor_position() {
-                if let Some(pos) = camera.viewport_to_world_2d(camera_transform, pos) {
-                    if x_min < pos.x && pos.x < x_max && y_min < pos.y && pos.y < y_max {
-                        commands.insert_resource(Focus(Some(entity)))
-                    };
-                }
-            };
-        }
-    }
-}
-
-fn ev_test(
-    mut evr_on: EventReader<TextHoverIn>,
-    mut evr_out: EventReader<TextHoverOut>,
-    mut evr_type: EventReader<CosmicTextChanged>,
-) {
-    for _ev in evr_on.read() {
-        println!("IN");
-    }
-    for _ev in evr_out.read() {
-        println!("OUT");
-    }
-    for _ev in evr_type.read() {
-        println!("TYPE");
-    }
-}
-
-fn main() {
-    App::new()
-        .add_plugins(DefaultPlugins)
-        .add_plugins(CosmicEditPlugin {
-            change_cursor: CursorConfig::Default,
-            ..default()
-        })
-        .add_systems(Startup, setup)
-        .add_systems(Update, change_active_editor_ui)
-        .add_systems(Update, change_active_editor_sprite)
-        .add_systems(Update, ev_test)
-        .run();
-}
diff --git a/examples/every_option.rs b/examples/every_option.rs
index 015dcc0b044bdb0dc3b9cb62921613cd5ec58c63..b6e3ba90beb6c7e9ed13ca60c355aff9dde211ae 100644
--- a/examples/every_option.rs
+++ b/examples/every_option.rs
@@ -1,30 +1,31 @@
-use bevy::{prelude::*, window::PrimaryWindow};
+use bevy::prelude::*;
 use bevy_cosmic_edit::*;
+use util::{bevy_color_to_cosmic, change_active_editor_ui, deselect_editor_on_esc};
 
 #[derive(Resource)]
 struct TextChangeTimer(pub Timer);
 
-fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
+fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
     commands.spawn(Camera2dBundle::default());
 
-    let attrs =
-        AttrsOwned::new(Attrs::new().color(bevy_color_to_cosmic(Color::rgb(0.27, 0.27, 0.27))));
-    let primary_window = windows.single();
+    let attrs = Attrs::new().color(bevy_color_to_cosmic(Color::rgb(0.27, 0.27, 0.27)));
 
     let editor = commands
         .spawn(CosmicEditBundle {
+            buffer: CosmicBuffer::new(&mut font_system, Metrics::new(16., 16.)).with_text(
+                &mut font_system,
+                "Begin counting.",
+                attrs,
+            ),
+            cursor_color: CursorColor(Color::GREEN),
+            selection_color: SelectionColor(Color::PINK),
+            fill_color: FillColor(Color::YELLOW_GREEN),
+            x_offset: XOffset::default(),
             text_position: CosmicTextPosition::default(),
-            fill_color: FillColor::default(),
             background_image: CosmicBackground::default(),
-            attrs: CosmicAttrs(attrs.clone()),
-            metrics: CosmicMetrics {
-                font_size: 16.,
-                line_height: 16.,
-                scale_factor: primary_window.scale_factor() as f32,
-            },
+            default_attrs: DefaultAttrs(AttrsOwned::new(attrs)),
             max_chars: CosmicMaxChars(15),
             max_lines: CosmicMaxLines(1),
-            text_setter: CosmicText::OneStyle("BANANA IS THE CODEWORD!".into()),
             mode: CosmicMode::Wrap,
             // CosmicEdit draws to this spritebundle
             sprite_bundle: SpriteBundle {
@@ -43,12 +44,6 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
             padding: Default::default(),
             widget_size: Default::default(),
         })
-        .insert(CosmicEditPlaceholderBundle {
-            text_setter: PlaceholderText(CosmicText::OneStyle("Placeholder".into())),
-            attrs: PlaceholderAttrs(AttrsOwned::new(
-                Attrs::new().color(CosmicColor::rgb(88, 88, 88)),
-            )),
-        })
         .id();
 
     commands
@@ -68,30 +63,19 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
         })
         .insert(CosmicSource(editor));
 
-    commands.insert_resource(Focus(Some(editor)));
-
     commands.insert_resource(TextChangeTimer(Timer::from_seconds(
         1.,
         TimerMode::Repeating,
     )));
 }
 
-pub fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor {
-    CosmicColor::rgba(
-        (color.r() * 255.) as u8,
-        (color.g() * 255.) as u8,
-        (color.b() * 255.) as u8,
-        (color.a() * 255.) as u8,
-    )
-}
-
 // Test for update_buffer_text
 fn text_swapper(
     mut timer: ResMut<TextChangeTimer>,
     time: Res<Time>,
-    mut cosmic_q: Query<&mut CosmicText>,
+    mut cosmic_q: Query<(&mut CosmicBuffer, &DefaultAttrs)>,
     mut count: Local<usize>,
-    editor_q: Query<&CosmicEditor>,
+    mut font_system: ResMut<CosmicFontSystem>,
 ) {
     timer.0.tick(time.delta());
     if !timer.0.just_finished() {
@@ -99,12 +83,13 @@ fn text_swapper(
     }
 
     *count += 1;
-    for mut text in cosmic_q.iter_mut() {
-        text.set_if_neq(CosmicText::OneStyle(format!("TIMER {}", *count)));
+    for (mut buffer, attrs) in cosmic_q.iter_mut() {
+        buffer.set_text(
+            &mut font_system,
+            format!("Counting... {}", *count).as_str(),
+            attrs.as_attrs(),
+        );
     }
-
-    let editor = editor_q.single();
-    println!("X OFFSET: {}", get_x_offset_center(50., editor.0.buffer()));
 }
 
 fn main() {
@@ -113,5 +98,6 @@ fn main() {
         .add_plugins(CosmicEditPlugin::default())
         .add_systems(Startup, setup)
         .add_systems(Update, text_swapper)
+        .add_systems(Update, (change_active_editor_ui, deselect_editor_on_esc))
         .run();
 }
diff --git a/examples/font_per_widget.rs b/examples/font_per_widget.rs
index f71241dcef6633374909e3de005c060774efac2d..f6f6959d825d7800134a9b529e9f8b8ba8ce5bc9 100644
--- a/examples/font_per_widget.rs
+++ b/examples/font_per_widget.rs
@@ -1,9 +1,10 @@
 #![allow(clippy::type_complexity)]
 
-use bevy::{prelude::*, window::PrimaryWindow};
+use bevy::prelude::*;
 use bevy_cosmic_edit::*;
+use util::{bevy_color_to_cosmic, change_active_editor_ui, deselect_editor_on_esc};
 
-fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
+fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
     commands.spawn(Camera2dBundle::default());
     let root = commands
         .spawn(NodeBundle {
@@ -16,207 +17,91 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
             ..default()
         })
         .id();
-    let primary_window = windows.single();
 
     let attrs = Attrs::new();
     let serif_attrs = attrs.family(Family::Serif);
     let mono_attrs = attrs.family(Family::Monospace);
     let comic_attrs = attrs.family(Family::Name("Comic Neue"));
-    let lines: Vec<Vec<(String, AttrsOwned)>> = vec![
-        vec![
-            (
-                String::from("B"),
-                AttrsOwned::new(attrs.weight(FontWeight::BOLD)),
-            ),
-            (String::from("old "), AttrsOwned::new(attrs)),
-            (
-                String::from("I"),
-                AttrsOwned::new(attrs.style(FontStyle::Italic)),
-            ),
-            (String::from("talic "), AttrsOwned::new(attrs)),
-            (String::from("f"), AttrsOwned::new(attrs)),
-            (String::from("i "), AttrsOwned::new(attrs)),
-            (
-                String::from("f"),
-                AttrsOwned::new(attrs.weight(FontWeight::BOLD)),
-            ),
-            (String::from("i "), AttrsOwned::new(attrs)),
-            (
-                String::from("f"),
-                AttrsOwned::new(attrs.style(FontStyle::Italic)),
-            ),
-            (String::from("i "), AttrsOwned::new(attrs)),
-        ],
-        vec![
-            (String::from("Sans-Serif Normal "), AttrsOwned::new(attrs)),
-            (
-                String::from("Sans-Serif Bold "),
-                AttrsOwned::new(attrs.weight(FontWeight::BOLD)),
-            ),
-            (
-                String::from("Sans-Serif Italic "),
-                AttrsOwned::new(attrs.style(FontStyle::Italic)),
-            ),
-            (
-                String::from("Sans-Serif Bold Italic"),
-                AttrsOwned::new(attrs.weight(FontWeight::BOLD).style(FontStyle::Italic)),
-            ),
-        ],
-        vec![
-            (String::from("Serif Normal "), AttrsOwned::new(serif_attrs)),
-            (
-                String::from("Serif Bold "),
-                AttrsOwned::new(serif_attrs.weight(FontWeight::BOLD)),
-            ),
-            (
-                String::from("Serif Italic "),
-                AttrsOwned::new(serif_attrs.style(FontStyle::Italic)),
-            ),
-            (
-                String::from("Serif Bold Italic"),
-                AttrsOwned::new(
-                    serif_attrs
-                        .weight(FontWeight::BOLD)
-                        .style(FontStyle::Italic),
-                ),
-            ),
-        ],
-        vec![
-            (String::from("Mono Normal "), AttrsOwned::new(mono_attrs)),
-            (
-                String::from("Mono Bold "),
-                AttrsOwned::new(mono_attrs.weight(FontWeight::BOLD)),
-            ),
-            (
-                String::from("Mono Italic "),
-                AttrsOwned::new(mono_attrs.style(FontStyle::Italic)),
-            ),
-            (
-                String::from("Mono Bold Italic"),
-                AttrsOwned::new(mono_attrs.weight(FontWeight::BOLD).style(FontStyle::Italic)),
-            ),
-        ],
-        vec![
-            (String::from("Comic Normal "), AttrsOwned::new(comic_attrs)),
-            (
-                String::from("Comic Bold "),
-                AttrsOwned::new(comic_attrs.weight(FontWeight::BOLD)),
-            ),
-            (
-                String::from("Comic Italic "),
-                AttrsOwned::new(comic_attrs.style(FontStyle::Italic)),
-            ),
-            (
-                String::from("Comic Bold Italic"),
-                AttrsOwned::new(
-                    comic_attrs
-                        .weight(FontWeight::BOLD)
-                        .style(FontStyle::Italic),
-                ),
-            ),
-        ],
-        vec![
-            (
-                String::from("R"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::RED))),
-            ),
-            (
-                String::from("A"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::ORANGE))),
-            ),
-            (
-                String::from("I"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::YELLOW))),
-            ),
-            (
-                String::from("N"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::GREEN))),
-            ),
-            (
-                String::from("B"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::BLUE))),
-            ),
-            (
-                String::from("O"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::INDIGO))),
-            ),
-            (
-                String::from("W "),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::PURPLE))),
-            ),
-            (
-                String::from("Red "),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::RED))),
-            ),
-            (
-                String::from("Orange "),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::ORANGE))),
-            ),
-            (
-                String::from("Yellow "),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::YELLOW))),
-            ),
-            (
-                String::from("Green "),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::GREEN))),
-            ),
-            (
-                String::from("Blue "),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::BLUE))),
-            ),
-            (
-                String::from("Indigo "),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::INDIGO))),
-            ),
-            (
-                String::from("Violet "),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::PURPLE))),
-            ),
-            (
-                String::from("U"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::PURPLE))),
-            ),
-            (
-                String::from("N"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::INDIGO))),
-            ),
-            (
-                String::from("I"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::BLUE))),
-            ),
-            (
-                String::from("C"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::GREEN))),
-            ),
-            (
-                String::from("O"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::YELLOW))),
-            ),
-            (
-                String::from("R"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::ORANGE))),
-            ),
-            (
-                String::from("N"),
-                AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::RED))),
-            ),
-        ],
-        vec![(
-            String::from("鐢熸椿,靷�,啶溹た啶傕う啶椸 馃榾 FPS"),
-            AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::RED))),
-        )],
+    let lines = vec![
+        ("B", attrs.weight(FontWeight::BOLD)),
+        ("old ", attrs),
+        ("I", attrs.style(FontStyle::Italic)),
+        ("talic ", attrs),
+        ("f", attrs),
+        ("i ", attrs),
+        ("f", attrs.weight(FontWeight::BOLD)),
+        ("i ", attrs),
+        ("f", attrs.style(FontStyle::Italic)),
+        ("i ", attrs),
+        ("Sans-Serif Normal ", attrs),
+        ("Sans-Serif Bold ", attrs.weight(FontWeight::BOLD)),
+        ("Sans-Serif Italic ", attrs.style(FontStyle::Italic)),
+        (
+            "Sans-Serif Bold Italic",
+            attrs.weight(FontWeight::BOLD).style(FontStyle::Italic),
+        ),
+        ("Serif Normal ", serif_attrs),
+        ("Serif Bold ", serif_attrs.weight(FontWeight::BOLD)),
+        ("Serif Italic ", serif_attrs.style(FontStyle::Italic)),
+        (
+            "Serif Bold Italic",
+            serif_attrs
+                .weight(FontWeight::BOLD)
+                .style(FontStyle::Italic),
+        ),
+        ("\n", attrs),
+        ("Mono Normal ", mono_attrs),
+        ("Mono Bold ", mono_attrs.weight(FontWeight::BOLD)),
+        ("Mono Italic ", mono_attrs.style(FontStyle::Italic)),
+        (
+            "Mono Bold Italic",
+            mono_attrs.weight(FontWeight::BOLD).style(FontStyle::Italic),
+        ),
+        ("Comic Normal ", comic_attrs),
+        ("Comic Bold ", comic_attrs.weight(FontWeight::BOLD)),
+        ("Comic Italic ", comic_attrs.style(FontStyle::Italic)),
+        (
+            "Comic Bold Italic",
+            comic_attrs
+                .weight(FontWeight::BOLD)
+                .style(FontStyle::Italic),
+        ),
+        ("\n", attrs),
+        ("R", attrs.color(bevy_color_to_cosmic(Color::RED))),
+        ("A", attrs.color(bevy_color_to_cosmic(Color::ORANGE))),
+        ("I", attrs.color(bevy_color_to_cosmic(Color::YELLOW))),
+        ("N", attrs.color(bevy_color_to_cosmic(Color::GREEN))),
+        ("B", attrs.color(bevy_color_to_cosmic(Color::BLUE))),
+        ("O", attrs.color(bevy_color_to_cosmic(Color::INDIGO))),
+        ("W ", attrs.color(bevy_color_to_cosmic(Color::PURPLE))),
+        ("Red ", attrs.color(bevy_color_to_cosmic(Color::RED))),
+        ("Orange ", attrs.color(bevy_color_to_cosmic(Color::ORANGE))),
+        ("Yellow ", attrs.color(bevy_color_to_cosmic(Color::YELLOW))),
+        ("Green ", attrs.color(bevy_color_to_cosmic(Color::GREEN))),
+        ("Blue ", attrs.color(bevy_color_to_cosmic(Color::BLUE))),
+        ("Indigo ", attrs.color(bevy_color_to_cosmic(Color::INDIGO))),
+        ("Violet ", attrs.color(bevy_color_to_cosmic(Color::PURPLE))),
+        ("U", attrs.color(bevy_color_to_cosmic(Color::PURPLE))),
+        ("N", attrs.color(bevy_color_to_cosmic(Color::INDIGO))),
+        ("I", attrs.color(bevy_color_to_cosmic(Color::BLUE))),
+        ("C", attrs.color(bevy_color_to_cosmic(Color::GREEN))),
+        ("O", attrs.color(bevy_color_to_cosmic(Color::YELLOW))),
+        ("R", attrs.color(bevy_color_to_cosmic(Color::ORANGE))),
+        ("N", attrs.color(bevy_color_to_cosmic(Color::RED))),
+        (
+            "鐢熸椿,靷�,啶溹た啶傕う啶椸 馃榾 FPS",
+            attrs.color(bevy_color_to_cosmic(Color::RED)),
+        ),
     ];
 
     let cosmic_edit_1 = commands
         .spawn(CosmicEditBundle {
+            buffer: CosmicBuffer::new(&mut font_system, Metrics::new(18., 22.)).with_rich_text(
+                &mut font_system,
+                lines,
+                attrs,
+            ),
             text_position: bevy_cosmic_edit::CosmicTextPosition::Center,
-            attrs: CosmicAttrs(AttrsOwned::new(attrs)),
-            metrics: CosmicMetrics {
-                font_size: 18.,
-                line_height: 22.,
-                scale_factor: primary_window.scale_factor() as f32,
-            },
-            text_setter: CosmicText::MultiStyle(lines),
             ..default()
         })
         .id();
@@ -227,14 +112,12 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
 
     let cosmic_edit_2 = commands
         .spawn(CosmicEditBundle {
-            attrs: CosmicAttrs(AttrsOwned::new(attrs_2)),
-            metrics: CosmicMetrics {
-                font_size: 28.,
-                line_height: 36.,
-                scale_factor: primary_window.scale_factor() as f32,
-            },
+            buffer: CosmicBuffer::new(&mut font_system, Metrics::new(28., 36.)).with_text(
+                &mut font_system,
+                "Widget 2.\nClick on me =>",
+                attrs_2,
+            ),
             text_position: CosmicTextPosition::Center,
-            text_setter: CosmicText::OneStyle("Widget 2.\nClick on me =>".to_string()),
             ..default()
         })
         .id();
@@ -265,48 +148,13 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
             })
             .insert(CosmicSource(cosmic_edit_2));
     });
-
-    // Set active editor
-    commands.insert_resource(Focus(Some(cosmic_edit_1)));
-}
-
-fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor {
-    CosmicColor::rgba(
-        (color.r() * 255.) as u8,
-        (color.g() * 255.) as u8,
-        (color.b() * 255.) as u8,
-        (color.a() * 255.) as u8,
-    )
-}
-
-fn change_active_editor_ui(
-    mut commands: Commands,
-    mut interaction_query: Query<
-        (&Interaction, &CosmicSource),
-        (Changed<Interaction>, Without<ReadOnly>),
-    >,
-) {
-    for (interaction, source) in interaction_query.iter_mut() {
-        if let Interaction::Pressed = interaction {
-            commands.insert_resource(Focus(Some(source.0)));
-        }
-    }
 }
 
 fn main() {
-    let font_config = CosmicFontConfig {
-        fonts_dir_path: None,
-        font_bytes: None,
-        load_system_fonts: true,
-    };
-
     App::new()
         .add_plugins(DefaultPlugins)
-        .add_plugins(CosmicEditPlugin {
-            font_config,
-            ..default()
-        })
+        .add_plugins(CosmicEditPlugin { ..default() })
         .add_systems(Startup, setup)
-        .add_systems(Update, change_active_editor_ui)
+        .add_systems(Update, (change_active_editor_ui, deselect_editor_on_esc))
         .run();
 }
diff --git a/examples/image_background.rs b/examples/image_background.rs
index 830126b85c38b6b396f37ada428b9a110dc0f7ac..3d9b404260569b5f6c9194f76e8fc3d786a2b30f 100644
--- a/examples/image_background.rs
+++ b/examples/image_background.rs
@@ -1,5 +1,6 @@
 use bevy::prelude::*;
 use bevy_cosmic_edit::*;
+use util::{bevy_color_to_cosmic, change_active_editor_ui, deselect_editor_on_esc};
 
 fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
     commands.spawn(Camera2dBundle::default());
@@ -8,7 +9,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
 
     let editor = commands
         .spawn(CosmicEditBundle {
-            attrs: CosmicAttrs(AttrsOwned::new(
+            default_attrs: DefaultAttrs(AttrsOwned::new(
                 Attrs::new().color(bevy_color_to_cosmic(Color::GREEN)),
             )),
             background_image: CosmicBackground(Some(bg_image_handle)),
@@ -30,17 +31,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
             ..default()
         })
         .insert(CosmicSource(editor));
-
-    commands.insert_resource(Focus(Some(editor)));
-}
-
-pub fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor {
-    CosmicColor::rgba(
-        (color.r() * 255.) as u8,
-        (color.g() * 255.) as u8,
-        (color.b() * 255.) as u8,
-        (color.a() * 255.) as u8,
-    )
 }
 
 fn main() {
@@ -48,5 +38,6 @@ fn main() {
         .add_plugins(DefaultPlugins)
         .add_plugins(CosmicEditPlugin::default())
         .add_systems(Startup, setup)
+        .add_systems(Update, (change_active_editor_ui, deselect_editor_on_esc))
         .run();
 }
diff --git a/examples/login.rs b/examples/login.rs
deleted file mode 100644
index 79b86b0ccd48da030af2ad507066a82a7d71501f..0000000000000000000000000000000000000000
--- a/examples/login.rs
+++ /dev/null
@@ -1,269 +0,0 @@
-use bevy::{
-    prelude::*,
-    window::{PrimaryWindow, WindowResolution},
-};
-use bevy_cosmic_edit::*;
-
-#[derive(Component)]
-struct SubmitButton;
-
-#[derive(Component)]
-struct UsernameTag;
-
-#[derive(Component)]
-struct PasswordTag;
-
-#[derive(Component)]
-struct DisplayTag;
-
-fn setup(mut commands: Commands, window: Query<&Window, With<PrimaryWindow>>) {
-    let window = window.single();
-
-    commands.spawn(Camera2dBundle::default());
-
-    let login_editor = commands
-        .spawn(CosmicEditBundle {
-            max_lines: CosmicMaxLines(1),
-            metrics: CosmicMetrics {
-                scale_factor: window.scale_factor() as f32,
-                ..default()
-            },
-            sprite_bundle: SpriteBundle {
-                sprite: Sprite {
-                    custom_size: Some(Vec2::new(300.0, 50.0)),
-                    ..default()
-                },
-                visibility: Visibility::Hidden,
-                ..default()
-            },
-            ..default()
-        })
-        .insert(CosmicEditPlaceholderBundle {
-            text_setter: PlaceholderText(CosmicText::OneStyle("Username".into())),
-            attrs: PlaceholderAttrs(AttrsOwned::new(
-                Attrs::new().color(bevy_color_to_cosmic(Color::rgb_u8(128, 128, 128))),
-            )),
-        })
-        .insert(UsernameTag)
-        .id();
-
-    let password_editor = commands
-        .spawn(CosmicEditBundle {
-            max_lines: CosmicMaxLines(1),
-            metrics: CosmicMetrics {
-                scale_factor: window.scale_factor() as f32,
-                ..default()
-            },
-            ..default()
-        })
-        .insert(CosmicEditPlaceholderBundle {
-            text_setter: PlaceholderText(CosmicText::OneStyle("Password".into())),
-            attrs: PlaceholderAttrs(AttrsOwned::new(
-                Attrs::new().color(bevy_color_to_cosmic(Color::rgb_u8(128, 128, 128))),
-            )),
-        })
-        .insert(PasswordTag)
-        .insert(PasswordInput::default())
-        .id();
-
-    let submit_editor = commands
-        .spawn(CosmicEditBundle {
-            max_lines: CosmicMaxLines(1),
-            metrics: CosmicMetrics {
-                font_size: 25.0,
-                line_height: 25.0,
-                scale_factor: window.scale_factor() as f32,
-                ..default()
-            },
-            attrs: CosmicAttrs(AttrsOwned::new(
-                Attrs::new().color(bevy_color_to_cosmic(Color::WHITE)),
-            )),
-            text_setter: CosmicText::OneStyle("Submit".into()),
-            fill_color: FillColor(Color::GREEN),
-            ..default()
-        })
-        .insert(ReadOnly)
-        .id();
-
-    let display_editor = commands
-        .spawn(CosmicEditBundle {
-            metrics: CosmicMetrics {
-                scale_factor: window.scale_factor() as f32,
-                ..default()
-            },
-            ..default()
-        })
-        .insert(CosmicEditPlaceholderBundle {
-            text_setter: PlaceholderText(CosmicText::OneStyle("Output".into())),
-            attrs: PlaceholderAttrs(AttrsOwned::new(
-                Attrs::new().color(bevy_color_to_cosmic(Color::rgb_u8(128, 128, 128))),
-            )),
-        })
-        .insert((ReadOnly, DisplayTag))
-        .id();
-
-    commands.insert_resource(Focus(Some(login_editor)));
-
-    // Spawn UI
-    commands
-        .spawn(NodeBundle {
-            style: Style {
-                flex_direction: FlexDirection::Column,
-                align_items: AlignItems::Center,
-                padding: UiRect::all(Val::Px(15.0)),
-                width: Val::Px(330.0),
-
-                ..default()
-            },
-            ..default()
-        })
-        .with_children(|root| {
-            root.spawn(ButtonBundle {
-                style: Style {
-                    // Size and position of text box
-                    width: Val::Px(300.),
-                    height: Val::Px(50.),
-                    margin: UiRect::all(Val::Px(15.0)),
-                    ..default()
-                },
-                background_color: BackgroundColor(Color::WHITE),
-                ..default()
-            })
-            .insert(CosmicSource(login_editor));
-
-            root.spawn(ButtonBundle {
-                style: Style {
-                    // Size and position of text box
-                    width: Val::Px(300.),
-                    height: Val::Px(50.),
-                    margin: UiRect::all(Val::Px(15.0)),
-                    ..default()
-                },
-                background_color: BackgroundColor(Color::WHITE),
-                ..default()
-            })
-            .insert(CosmicSource(password_editor));
-
-            root.spawn(ButtonBundle {
-                style: Style {
-                    // Size and position of text box
-                    width: Val::Px(150.),
-                    height: Val::Px(50.),
-                    margin: UiRect::all(Val::Px(15.0)),
-                    border: UiRect::all(Val::Px(3.0)),
-                    ..default()
-                },
-                background_color: BackgroundColor(Color::WHITE),
-                border_color: Color::DARK_GREEN.into(),
-
-                ..default()
-            })
-            .insert(SubmitButton)
-            .insert(CosmicSource(submit_editor));
-
-            root.spawn(ButtonBundle {
-                style: Style {
-                    // Size and position of text box
-                    width: Val::Px(300.),
-                    height: Val::Px(100.),
-                    margin: UiRect::all(Val::Px(15.0)),
-                    ..default()
-                },
-                background_color: BackgroundColor(Color::WHITE),
-                ..default()
-            })
-            .insert(CosmicSource(display_editor));
-        });
-}
-
-fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor {
-    CosmicColor::rgba(
-        (color.r() * 255.) as u8,
-        (color.g() * 255.) as u8,
-        (color.b() * 255.) as u8,
-        (color.a() * 255.) as u8,
-    )
-}
-
-fn change_active_editor_ui(
-    mut commands: Commands,
-    mut interaction_query: Query<
-        (&Interaction, &CosmicSource),
-        (Changed<Interaction>, Without<ReadOnly>),
-    >,
-) {
-    for (interaction, source) in interaction_query.iter_mut() {
-        if let Interaction::Pressed = interaction {
-            commands.insert_resource(Focus(Some(source.0)));
-        }
-    }
-}
-
-fn print_changed_input(mut evr_type: EventReader<CosmicTextChanged>) {
-    for ev in evr_type.read() {
-        println!("Changed: {}", ev.0 .1);
-    }
-}
-
-fn submit_button(
-    button_q: Query<&Interaction, With<SubmitButton>>,
-    username_q: Query<
-        &CosmicEditor,
-        (With<UsernameTag>, Without<PasswordTag>, Without<DisplayTag>),
-    >,
-    password_q: Query<
-        &CosmicEditor,
-        (With<PasswordTag>, Without<UsernameTag>, Without<DisplayTag>),
-    >,
-    mut display_q: Query<
-        (&mut CosmicEditor, &CosmicAttrs),
-        (With<DisplayTag>, Without<UsernameTag>, Without<PasswordTag>),
-    >,
-    mut font_system: ResMut<CosmicFontSystem>,
-    mut window: Query<&mut Window, With<PrimaryWindow>>,
-) {
-    for interaction in button_q.iter() {
-        match interaction {
-            Interaction::None => {}
-            Interaction::Pressed => {
-                let u = username_q.single();
-                let p = password_q.single();
-                let (mut d, attrs) = display_q.single_mut();
-
-                let text = format!(
-                    "Submitted!\nUsername: {}\nPassword: {}\n",
-                    u.get_text(),
-                    p.get_text()
-                );
-
-                d.set_text(
-                    CosmicText::OneStyle(text),
-                    attrs.0.clone(),
-                    &mut font_system.0,
-                );
-            }
-            Interaction::Hovered => {
-                window.single_mut().cursor.icon = CursorIcon::Pointer;
-            }
-        }
-    }
-}
-
-fn main() {
-    App::new()
-        .add_plugins(DefaultPlugins.set(WindowPlugin {
-            primary_window: Some(Window {
-                resolution: WindowResolution::new(330., 480.),
-                ..default()
-            }),
-            ..default()
-        }))
-        .add_plugins(CosmicEditPlugin {
-            change_cursor: CursorConfig::Default,
-            ..default()
-        })
-        .add_systems(Startup, setup)
-        .add_systems(Update, change_active_editor_ui)
-        .add_systems(Update, (print_changed_input, submit_button))
-        .run();
-}
diff --git a/examples/multiple_sprites.rs b/examples/multiple_sprites.rs
index 04f5f867c589fbd916802d110b6b3c344bb2be3f..13d15f419143813b983dc5ec700b9e95ca2dc179 100644
--- a/examples/multiple_sprites.rs
+++ b/examples/multiple_sprites.rs
@@ -1,7 +1,12 @@
 use bevy::{prelude::*, window::PrimaryWindow};
 use bevy_cosmic_edit::*;
+use util::{bevy_color_to_cosmic, change_active_editor_sprite};
 
-fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
+fn setup(
+    mut commands: Commands,
+    windows: Query<&Window, With<PrimaryWindow>>,
+    mut font_system: ResMut<CosmicFontSystem>,
+) {
     let primary_window = windows.single();
     let camera_bundle = Camera2dBundle {
         camera: Camera {
@@ -15,18 +20,15 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
     let mut attrs = Attrs::new();
     attrs = attrs.family(Family::Name("Victor Mono"));
     attrs = attrs.color(bevy_color_to_cosmic(Color::PURPLE));
-    let metrics = CosmicMetrics {
-        font_size: 14.,
-        line_height: 18.,
-        scale_factor: primary_window.scale_factor() as f32,
-    };
 
-    let cosmic_edit_1 = (CosmicEditBundle {
-        attrs: CosmicAttrs(AttrsOwned::new(attrs)),
-        metrics: metrics.clone(),
+    commands.spawn(CosmicEditBundle {
         text_position: CosmicTextPosition::Center,
         fill_color: FillColor(Color::ALICE_BLUE),
-        text_setter: CosmicText::OneStyle("馃榾馃榾馃榾 x => y".to_string()),
+        buffer: CosmicBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text(
+            &mut font_system,
+            "馃榾馃榾馃榾 x => y",
+            attrs,
+        ),
         sprite_bundle: SpriteBundle {
             sprite: Sprite {
                 custom_size: Some(Vec2 {
@@ -39,14 +41,16 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
             ..default()
         },
         ..default()
-    },);
+    });
 
-    let cosmic_edit_2 = (CosmicEditBundle {
-        attrs: CosmicAttrs(AttrsOwned::new(attrs)),
-        metrics,
+    commands.spawn(CosmicEditBundle {
         text_position: CosmicTextPosition::Center,
         fill_color: FillColor(Color::GRAY.with_a(0.5)),
-        text_setter: CosmicText::OneStyle("Widget_2. Click on me".to_string()),
+        buffer: CosmicBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text(
+            &mut font_system,
+            "Widget_2. Click on me",
+            attrs,
+        ),
         sprite_bundle: SpriteBundle {
             sprite: Sprite {
                 custom_size: Some(Vec2 {
@@ -63,52 +67,7 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
             ..default()
         },
         ..default()
-    },);
-
-    let id = commands.spawn(cosmic_edit_1).id();
-
-    commands.insert_resource(Focus(Some(id)));
-
-    commands.spawn(cosmic_edit_2);
-}
-
-fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor {
-    CosmicColor::rgba(
-        (color.r() * 255.) as u8,
-        (color.g() * 255.) as u8,
-        (color.b() * 255.) as u8,
-        (color.a() * 255.) as u8,
-    )
-}
-
-fn change_active_editor_sprite(
-    mut commands: Commands,
-    windows: Query<&Window, With<PrimaryWindow>>,
-    buttons: Res<ButtonInput<MouseButton>>,
-    mut cosmic_edit_query: Query<
-        (&mut Sprite, &GlobalTransform, Entity),
-        (With<CosmicEditor>, Without<ReadOnly>),
-    >,
-    camera_q: Query<(&Camera, &GlobalTransform)>,
-) {
-    let window = windows.single();
-    let (camera, camera_transform) = camera_q.single();
-    if buttons.just_pressed(MouseButton::Left) {
-        for (sprite, node_transform, entity) in &mut cosmic_edit_query.iter_mut() {
-            let size = sprite.custom_size.unwrap_or(Vec2::new(1., 1.));
-            let x_min = node_transform.affine().translation.x - size.x / 2.;
-            let y_min = node_transform.affine().translation.y - size.y / 2.;
-            let x_max = node_transform.affine().translation.x + size.x / 2.;
-            let y_max = node_transform.affine().translation.y + size.y / 2.;
-            if let Some(pos) = window.cursor_position() {
-                if let Some(pos) = camera.viewport_to_world_2d(camera_transform, pos) {
-                    if x_min < pos.x && pos.x < x_max && y_min < pos.y && pos.y < y_max {
-                        commands.insert_resource(Focus(Some(entity)))
-                    };
-                }
-            };
-        }
-    }
+    });
 }
 
 fn main() {
diff --git a/examples/readonly.rs b/examples/readonly.rs
index 0f50de8a37d76f1d31cdeecc688f7c7b538ec86e..b41ad9f393a78a68bc971b1911bca9f1c7a1eb52 100644
--- a/examples/readonly.rs
+++ b/examples/readonly.rs
@@ -1,8 +1,8 @@
-use bevy::{prelude::*, window::PrimaryWindow};
+use bevy::prelude::*;
 use bevy_cosmic_edit::*;
+use util::{bevy_color_to_cosmic, change_active_editor_ui};
 
-fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
-    let primary_window = windows.single();
+fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
     commands.spawn(Camera2dBundle::default());
     let root = commands
         .spawn(NodeBundle {
@@ -23,14 +23,12 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
     // spawn editor
     let cosmic_edit = commands
         .spawn(CosmicEditBundle {
-            attrs: CosmicAttrs(AttrsOwned::new(attrs)),
             text_position: CosmicTextPosition::Center,
-            metrics: CosmicMetrics {
-                font_size: 14.,
-                line_height: 18.,
-                scale_factor: primary_window.scale_factor() as f32,
-            },
-            text_setter: CosmicText::OneStyle("馃榾馃榾馃榾 x => y\nRead only widget".to_string()),
+            buffer: CosmicBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text(
+                &mut font_system,
+                "馃榾馃榾馃榾 x => y\nRead only widget",
+                attrs,
+            ),
             ..default()
         })
         .insert(ReadOnly)
@@ -51,17 +49,6 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
             // add cosmic source
             .insert(CosmicSource(cosmic_edit));
     });
-
-    commands.insert_resource(Focus(Some(cosmic_edit)));
-}
-
-pub fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor {
-    CosmicColor::rgba(
-        (color.r() * 255.) as u8,
-        (color.g() * 255.) as u8,
-        (color.b() * 255.) as u8,
-        (color.a() * 255.) as u8,
-    )
 }
 
 fn main() {
@@ -79,5 +66,6 @@ fn main() {
             ..default()
         })
         .add_systems(Startup, setup)
+        .add_systems(Update, change_active_editor_ui)
         .run();
 }
diff --git a/examples/sprite_and_ui_clickable.rs b/examples/sprite_and_ui_clickable.rs
new file mode 100644
index 0000000000000000000000000000000000000000..31d4e76ce8f5f033d2ca9879082fc40ab209681a
--- /dev/null
+++ b/examples/sprite_and_ui_clickable.rs
@@ -0,0 +1,86 @@
+use bevy::prelude::*;
+use bevy_cosmic_edit::*;
+use util::{
+    bevy_color_to_cosmic, change_active_editor_sprite, change_active_editor_ui,
+    deselect_editor_on_esc,
+};
+
+fn setup(mut commands: Commands) {
+    commands.spawn(Camera2dBundle::default());
+
+    // UI editor
+    let ui_editor = commands
+        .spawn(CosmicEditBundle {
+            default_attrs: DefaultAttrs(AttrsOwned::new(
+                Attrs::new().color(bevy_color_to_cosmic(Color::GREEN)),
+            )),
+            max_lines: CosmicMaxLines(1),
+            ..default()
+        })
+        .id();
+
+    commands
+        .spawn(ButtonBundle {
+            style: Style {
+                // Size and position of text box
+                width: Val::Px(300.),
+                height: Val::Px(50.),
+                left: Val::Px(100.),
+                top: Val::Px(100.),
+                ..default()
+            },
+            ..default()
+        })
+        .insert(CosmicSource(ui_editor));
+
+    // Sprite editor
+    commands.spawn((CosmicEditBundle {
+        sprite_bundle: SpriteBundle {
+            // Sets size of text box
+            sprite: Sprite {
+                custom_size: Some(Vec2::new(300., 100.)),
+                ..default()
+            },
+            // Position of text box
+            transform: Transform::from_xyz(0., 100., 0.),
+            ..default()
+        },
+        ..default()
+    },));
+}
+
+fn ev_test(
+    mut evr_on: EventReader<TextHoverIn>,
+    mut evr_out: EventReader<TextHoverOut>,
+    mut evr_type: EventReader<CosmicTextChanged>,
+) {
+    for _ev in evr_on.read() {
+        println!("IN");
+    }
+    for _ev in evr_out.read() {
+        println!("OUT");
+    }
+    for _ev in evr_type.read() {
+        println!("TYPE");
+    }
+}
+
+fn main() {
+    App::new()
+        .add_plugins(DefaultPlugins)
+        .add_plugins(CosmicEditPlugin {
+            change_cursor: CursorConfig::Default,
+            ..default()
+        })
+        .add_systems(Startup, setup)
+        .add_systems(
+            Update,
+            (
+                change_active_editor_ui,
+                change_active_editor_sprite,
+                deselect_editor_on_esc,
+            ),
+        )
+        .add_systems(Update, ev_test)
+        .run();
+}
diff --git a/examples/text_input.rs b/examples/text_input.rs
deleted file mode 100644
index 89dce6d96c9d8bab8113d52eabc8c7d933d7ecfe..0000000000000000000000000000000000000000
--- a/examples/text_input.rs
+++ /dev/null
@@ -1,162 +0,0 @@
-use bevy::{
-    prelude::*,
-    window::{PresentMode, PrimaryWindow},
-};
-use bevy_cosmic_edit::*;
-
-fn create_editable_widget(commands: &mut Commands, scale_factor: f32, text: String) -> Entity {
-    let attrs =
-        AttrsOwned::new(Attrs::new().color(bevy_color_to_cosmic(Color::hex("4d4d4d").unwrap())));
-    let placeholder_attrs =
-        AttrsOwned::new(Attrs::new().color(bevy_color_to_cosmic(Color::hex("#e6e6e6").unwrap())));
-    let editor = commands
-        .spawn((
-            CosmicEditBundle {
-                attrs: CosmicAttrs(attrs.clone()),
-                metrics: CosmicMetrics {
-                    font_size: 18.,
-                    line_height: 18. * 1.2,
-                    scale_factor,
-                },
-                max_lines: CosmicMaxLines(1),
-                text_setter: CosmicText::OneStyle(text),
-                text_position: CosmicTextPosition::Left { padding: 20 },
-                mode: CosmicMode::InfiniteLine,
-                ..default()
-            },
-            CosmicEditPlaceholderBundle {
-                text_setter: PlaceholderText(CosmicText::OneStyle("Type something...".into())),
-                attrs: PlaceholderAttrs(placeholder_attrs.clone()),
-            },
-        ))
-        .id();
-    commands
-        .spawn(ButtonBundle {
-            border_color: Color::hex("#ededed").unwrap().into(),
-            style: Style {
-                border: UiRect::all(Val::Px(3.)),
-                width: Val::Percent(20.),
-                height: Val::Px(50.),
-                left: Val::Percent(40.),
-                top: Val::Px(100.),
-                ..default()
-            },
-            background_color: Color::WHITE.into(),
-            ..default()
-        })
-        .insert(CosmicSource(editor));
-
-    editor
-}
-
-fn create_readonly_widget(commands: &mut Commands, scale_factor: f32, text: String) -> Entity {
-    let attrs =
-        AttrsOwned::new(Attrs::new().color(bevy_color_to_cosmic(Color::hex("4d4d4d").unwrap())));
-
-    let editor = commands
-        .spawn((
-            CosmicEditBundle {
-                attrs: CosmicAttrs(attrs.clone()),
-                metrics: CosmicMetrics {
-                    font_size: 18.,
-                    line_height: 18. * 1.2,
-                    scale_factor,
-                },
-                text_setter: CosmicText::OneStyle(text),
-                text_position: CosmicTextPosition::Left { padding: 20 },
-                mode: CosmicMode::AutoHeight,
-                ..default()
-            },
-            ReadOnly,
-        ))
-        .id();
-
-    commands
-        .spawn(ButtonBundle {
-            border_color: Color::hex("#ededed").unwrap().into(),
-            style: Style {
-                border: UiRect::all(Val::Px(3.)),
-                width: Val::Percent(20.),
-                height: Val::Px(50.),
-                left: Val::Percent(40.),
-                top: Val::Px(100.),
-                ..default()
-            },
-            background_color: Color::WHITE.into(),
-            ..default()
-        })
-        .insert(CosmicSource(editor));
-
-    editor
-}
-
-fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) {
-    commands.spawn(Camera2dBundle::default());
-    let primary_window = windows.single();
-    let editor = create_editable_widget(
-        &mut commands,
-        primary_window.scale_factor() as f32,
-        "".to_string(),
-    );
-    commands.insert_resource(Focus(Some(editor)));
-}
-
-fn handle_enter(
-    mut commands: Commands,
-    keys: Res<ButtonInput<KeyCode>>,
-    mut query_dest: Query<(Entity, &CosmicSource)>,
-    mut query_source: Query<(Entity, &CosmicEditor, &CosmicMode)>,
-    windows: Query<&Window, With<PrimaryWindow>>,
-) {
-    if keys.just_pressed(KeyCode::Enter) {
-        let scale_factor = windows.single().scale_factor() as f32;
-        for (entity, editor, mode) in query_source.iter_mut() {
-            // Remove UI elements
-            for (dest_entity, source) in query_dest.iter_mut() {
-                if source.0 == entity {
-                    commands.entity(dest_entity).despawn_recursive();
-                }
-            }
-
-            let text = editor.get_text();
-            commands.entity(entity).despawn_recursive();
-            if *mode == CosmicMode::AutoHeight {
-                let editor = create_editable_widget(&mut commands, scale_factor, text);
-                commands.insert_resource(Focus(Some(editor)));
-            } else {
-                let editor = create_readonly_widget(&mut commands, scale_factor, text);
-                commands.insert_resource(Focus(Some(editor)));
-            };
-        }
-    }
-}
-
-fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor {
-    CosmicColor::rgba(
-        (color.r() * 255.) as u8,
-        (color.g() * 255.) as u8,
-        (color.b() * 255.) as u8,
-        (color.a() * 255.) as u8,
-    )
-}
-
-fn main() {
-    App::new()
-        .add_plugins(
-            DefaultPlugins
-                .set(WindowPlugin {
-                    primary_window: Some(Window {
-                        title: "bevy 鈥� text_input".into(),
-                        present_mode: PresentMode::AutoVsync,
-                        // TODO reimplement fit to parent
-                        ..default()
-                    }),
-                    ..default()
-                })
-                .build(),
-        )
-        .add_plugins(CosmicEditPlugin::default())
-        .add_systems(Update, handle_enter)
-        .add_systems(Startup, setup)
-        .run();
-}
diff --git a/src/buffer.rs b/src/buffer.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8f58564057c5fbf66dda42fba81c57c10aedbc02
--- /dev/null
+++ b/src/buffer.rs
@@ -0,0 +1,220 @@
+use crate::*;
+use bevy::{prelude::*, window::PrimaryWindow};
+
+#[derive(Component, Deref, DerefMut)]
+pub struct CosmicBuffer(pub Buffer);
+
+impl Default for CosmicBuffer {
+    fn default() -> Self {
+        CosmicBuffer(Buffer::new_empty(Metrics::new(20., 20.)))
+    }
+}
+
+impl<'s, 'r> CosmicBuffer {
+    pub fn new(font_system: &mut FontSystem, metrics: Metrics) -> Self {
+        Self(Buffer::new(font_system, metrics))
+    }
+
+    // TODO: Set redraw when setting text
+
+    // Das a lotta boilerplate just to hide the shaping argument
+    pub fn with_text(
+        mut self,
+        font_system: &mut FontSystem,
+        text: &'s str,
+        attrs: Attrs<'r>,
+    ) -> Self {
+        self.0.set_text(font_system, text, attrs, Shaping::Advanced);
+        self
+    }
+
+    pub fn with_rich_text<I>(
+        mut self,
+        font_system: &mut FontSystem,
+        spans: I,
+        attrs: Attrs<'r>,
+    ) -> Self
+    where
+        I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
+    {
+        self.0
+            .set_rich_text(font_system, spans, attrs, Shaping::Advanced);
+        self
+    }
+
+    pub fn set_text(
+        &mut self,
+        font_system: &mut FontSystem,
+        text: &'s str,
+        attrs: Attrs<'r>,
+    ) -> &mut Self {
+        self.0.set_text(font_system, text, attrs, Shaping::Advanced);
+        self.set_redraw(true);
+        self
+    }
+
+    pub fn set_rich_text<I>(
+        &mut self,
+        font_system: &mut FontSystem,
+        spans: I,
+        attrs: Attrs<'r>,
+    ) -> &mut Self
+    where
+        I: IntoIterator<Item = (&'s str, Attrs<'r>)>,
+    {
+        self.0
+            .set_rich_text(font_system, spans, attrs, Shaping::Advanced);
+        self.set_redraw(true);
+        self
+    }
+
+    /// Retrieves the cosmic text content from a buffer.
+    ///
+    /// # Arguments
+    ///
+    /// * none, takes the rust magic ref to self
+    ///
+    /// # Returns
+    ///
+    /// A `String` containing the cosmic text content.
+    pub fn get_text(&self) -> String {
+        let mut text = String::new();
+        let line_count = self.lines.len();
+
+        for (i, line) in self.lines.iter().enumerate() {
+            text.push_str(line.text());
+
+            if i < line_count - 1 {
+                text.push('\n');
+            }
+        }
+
+        text
+    }
+    /// Returns texts from a MultiStyle buffer
+    pub fn get_text_spans(&self, default_attrs: AttrsOwned) -> Vec<Vec<(String, AttrsOwned)>> {
+        // TODO: untested!
+
+        let buffer = self;
+
+        let mut spans = Vec::new();
+        for line in buffer.lines.iter() {
+            let mut line_spans = Vec::new();
+            let line_text = line.text();
+            let line_attrs = line.attrs_list();
+            if line_attrs.spans().is_empty() {
+                line_spans.push((line_text.to_string(), default_attrs.clone()));
+            } else {
+                let mut current_pos = 0;
+                for span in line_attrs.spans() {
+                    let span_range = span.0;
+                    let span_attrs = span.1.clone();
+                    let start_index = span_range.start;
+                    let end_index = span_range.end;
+                    if start_index > current_pos {
+                        // Add the text between the current position and the start of the span
+                        let non_span_text = line_text[current_pos..start_index].to_string();
+                        line_spans.push((non_span_text, default_attrs.clone()));
+                    }
+                    let span_text = line_text[start_index..end_index].to_string();
+                    line_spans.push((span_text.clone(), span_attrs));
+                    current_pos = end_index;
+                }
+                if current_pos < line_text.len() {
+                    // Add the remaining text after the last span
+                    let remaining_text = line_text[current_pos..].to_string();
+                    line_spans.push((remaining_text, default_attrs.clone()));
+                }
+            }
+            spans.push(line_spans);
+        }
+        spans
+    }
+}
+
+pub fn add_font_system(
+    mut font_system: ResMut<CosmicFontSystem>,
+    mut q: Query<&mut CosmicBuffer, Added<CosmicBuffer>>,
+) {
+    for mut b in q.iter_mut() {
+        if !b.lines.is_empty() {
+            continue;
+        }
+        b.0.set_text(&mut font_system, "", Attrs::new(), Shaping::Advanced);
+        b.set_redraw(true);
+    }
+}
+
+pub fn set_initial_scale(
+    window_q: Query<&Window, With<PrimaryWindow>>,
+    mut cosmic_query: Query<&mut CosmicBuffer, Added<CosmicBuffer>>,
+    mut font_system: ResMut<CosmicFontSystem>,
+) {
+    let w_scale = window_q.single().scale_factor();
+
+    for mut b in &mut cosmic_query.iter_mut() {
+        let m = b.metrics().scale(w_scale);
+        b.set_metrics(&mut font_system, m);
+    }
+}
+
+pub fn set_redraw(mut q: Query<&mut CosmicBuffer, Added<CosmicBuffer>>) {
+    for mut b in q.iter_mut() {
+        b.set_redraw(true);
+    }
+}
+
+pub fn set_editor_redraw(mut q: Query<&mut CosmicEditor, Added<CosmicEditor>>) {
+    for mut b in q.iter_mut() {
+        b.set_redraw(true);
+    }
+}
+
+pub(crate) fn swap_target_handle(
+    source_q: Query<&Handle<Image>, With<CosmicBuffer>>,
+    mut dest_q: Query<
+        (
+            Option<&mut Handle<Image>>,
+            Option<&mut UiImage>,
+            &CosmicSource,
+        ),
+        Without<CosmicBuffer>,
+    >,
+) {
+    // TODO: do this once
+    for (dest_handle_opt, dest_ui_opt, source_entity) in dest_q.iter_mut() {
+        if let Ok(source_handle) = source_q.get(source_entity.0) {
+            if let Some(mut dest_handle) = dest_handle_opt {
+                *dest_handle = source_handle.clone_weak();
+            }
+            if let Some(mut dest_ui) = dest_ui_opt {
+                dest_ui.texture = source_handle.clone_weak();
+            }
+        }
+    }
+}
+
+// TODO put this on impl CosmicBuffer
+
+pub fn get_text_size(buffer: &Buffer) -> (f32, f32) {
+    if buffer.layout_runs().count() == 0 {
+        return (0., buffer.metrics().line_height);
+    }
+    let width = buffer
+        .layout_runs()
+        .map(|run| run.line_w)
+        .reduce(f32::max)
+        .unwrap();
+    let height = buffer.layout_runs().count() as f32 * buffer.metrics().line_height;
+    (width, height)
+}
+
+pub fn get_y_offset_center(widget_height: f32, buffer: &Buffer) -> i32 {
+    let (_, text_height) = get_text_size(buffer);
+    ((widget_height - text_height) / 2.0) as i32
+}
+
+pub fn get_x_offset_center(widget_width: f32, buffer: &Buffer) -> i32 {
+    let (text_width, _) = get_text_size(buffer);
+    ((widget_width - text_width) / 2.0) as i32
+}
diff --git a/src/cursor.rs b/src/cursor.rs
index 52fbfa866f63ab7feb1121f4de08a75a0dde5232..a044bcfb7d63a78b2c415dbc684e19f0ab9f42ff 100644
--- a/src/cursor.rs
+++ b/src/cursor.rs
@@ -1,6 +1,6 @@
 use bevy::{input::mouse::MouseMotion, prelude::*, window::PrimaryWindow};
 
-use crate::{CosmicEditor, CosmicSource, CosmicTextChanged};
+use crate::{CosmicBuffer, CosmicSource, CosmicTextChanged};
 
 #[cfg(feature = "multicam")]
 use crate::CosmicPrimaryCamera;
@@ -48,7 +48,7 @@ type CameraQuery<'a, 'b, 'c, 'd> = Query<'a, 'b, (&'c Camera, &'d GlobalTransfor
 
 pub fn hover_sprites(
     windows: Query<&Window, With<PrimaryWindow>>,
-    mut cosmic_edit_query: Query<(&mut Sprite, &Visibility, &GlobalTransform), With<CosmicEditor>>,
+    mut cosmic_edit_query: Query<(&mut Sprite, &Visibility, &GlobalTransform), With<CosmicBuffer>>,
     camera_q: CameraQuery,
     mut hovered: Local<bool>,
     mut last_hovered: Local<bool>,
@@ -65,6 +65,7 @@ pub fn hover_sprites(
         if visibility == Visibility::Hidden {
             continue;
         }
+
         let size = sprite.custom_size.unwrap_or(Vec2::ONE);
         let x_min = node_transform.affine().translation.x - size.x / 2.;
         let y_min = node_transform.affine().translation.y - size.y / 2.;
diff --git a/src/focus.rs b/src/focus.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6520f0dd77491b2d01848d6c85b3869149067f99
--- /dev/null
+++ b/src/focus.rs
@@ -0,0 +1,45 @@
+use bevy::prelude::*;
+use cosmic_text::{Edit, Editor};
+
+use crate::{CosmicBuffer, CosmicEditor};
+
+/// Resource struct that keeps track of the currently active editor entity.
+#[derive(Resource, Default, Deref, DerefMut)]
+pub struct FocusedWidget(pub Option<Entity>);
+
+pub(crate) fn add_editor_to_focused(
+    mut commands: Commands,
+    active_editor: Res<FocusedWidget>,
+    q: Query<&CosmicBuffer, Without<CosmicEditor>>,
+) {
+    if let Some(e) = active_editor.0 {
+        let Ok(b) = q.get(e) else {
+            return;
+        };
+        let mut editor = Editor::new(b.0.clone());
+        editor.set_redraw(true);
+        commands.entity(e).insert(CosmicEditor::new(editor));
+    }
+}
+
+pub(crate) fn drop_editor_unfocused(
+    mut commands: Commands,
+    active_editor: Res<FocusedWidget>,
+    mut q: Query<(Entity, &mut CosmicBuffer, &CosmicEditor)>,
+) {
+    if active_editor.0.is_none() {
+        for (e, mut b, ed) in q.iter_mut() {
+            b.lines = ed.with_buffer(|buf| buf.lines.clone());
+            b.set_redraw(true);
+            commands.entity(e).remove::<CosmicEditor>();
+        }
+    } else if let Some(focused) = active_editor.0 {
+        for (e, mut b, ed) in q.iter_mut() {
+            if e != focused {
+                b.lines = ed.with_buffer(|buf| buf.lines.clone());
+                b.set_redraw(true);
+                commands.entity(e).remove::<CosmicEditor>();
+            }
+        }
+    }
+}
diff --git a/src/input.rs b/src/input.rs
index 5d0192c02410a7268b2652607ef605e6a7eba527..5052197350ad62e46c0a5af8e40515745a518bdd 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -1,7 +1,5 @@
 #![allow(clippy::too_many_arguments, clippy::type_complexity)]
 
-use std::time::Duration;
-
 #[cfg(target_arch = "wasm32")]
 use bevy::tasks::AsyncComputeTaskPool;
 
@@ -10,8 +8,10 @@ use bevy::{
     prelude::*,
     window::PrimaryWindow,
 };
-use cosmic_text::{Action, AttrsList, BufferLine, Cursor, Edit, Shaping};
+use cosmic_text::{Action, Cursor, Edit, Motion, Selection};
 
+#[cfg(target_arch = "wasm32")]
+use crate::DefaultAttrs;
 #[cfg(target_arch = "wasm32")]
 use js_sys::Promise;
 #[cfg(target_arch = "wasm32")]
@@ -20,10 +20,10 @@ use wasm_bindgen::prelude::*;
 use wasm_bindgen_futures::JsFuture;
 
 use crate::{
-    get_node_cursor_pos, get_timestamp, get_x_offset_center, get_y_offset_center,
-    save_edit_history, CosmicAttrs, CosmicEditHistory, CosmicEditor, CosmicFontSystem,
-    CosmicMaxChars, CosmicMaxLines, CosmicSource, CosmicTextChanged, CosmicTextPosition, Focus,
-    PasswordInput, ReadOnly, XOffset,
+    buffer::{get_x_offset_center, get_y_offset_center},
+    get_node_cursor_pos, CosmicBuffer, CosmicEditor, CosmicFontSystem, CosmicMaxChars,
+    CosmicMaxLines, CosmicSource, CosmicTextChanged, CosmicTextPosition, FocusedWidget, ReadOnly,
+    XOffset,
 };
 
 #[derive(Resource)]
@@ -44,7 +44,7 @@ pub struct WasmPasteAsyncChannel {
 
 pub(crate) fn input_mouse(
     windows: Query<&Window, With<PrimaryWindow>>,
-    active_editor: Res<Focus>,
+    active_editor: Res<FocusedWidget>,
     keys: Res<ButtonInput<KeyCode>>,
     buttons: Res<ButtonInput<MouseButton>>,
     mut editor_q: Query<(
@@ -95,6 +95,8 @@ pub(crate) fn input_mouse(
     if let Ok((mut editor, sprite_transform, text_position, entity, x_offset, sprite)) =
         editor_q.get_mut(active_editor_entity)
     {
+        let buffer = editor.with_buffer(|b| b.clone());
+
         let mut is_ui_node = false;
         let mut transform = sprite_transform;
         let (mut width, mut height) =
@@ -114,21 +116,21 @@ pub(crate) fn input_mouse(
         let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
 
         // if shift key is pressed
-        let already_has_selection = editor.0.select_opt().is_some();
+        let already_has_selection = editor.selection() != Selection::None;
         if shift && !already_has_selection {
-            let cursor = editor.0.cursor();
-            editor.0.set_select_opt(Some(cursor));
+            let cursor = editor.cursor();
+            editor.set_selection(Selection::Normal(cursor));
         }
 
         let (padding_x, padding_y) = match text_position {
             CosmicTextPosition::Center => (
-                get_x_offset_center(width * scale_factor, editor.0.buffer()),
-                get_y_offset_center(height * scale_factor, editor.0.buffer()),
+                get_x_offset_center(width * scale_factor, &buffer),
+                get_y_offset_center(height * scale_factor, &buffer),
             ),
             CosmicTextPosition::TopLeft { padding } => (*padding, *padding),
             CosmicTextPosition::Left { padding } => (
                 *padding,
-                get_y_offset_center(height * scale_factor, editor.0.buffer()),
+                get_y_offset_center(height * scale_factor, &buffer),
             ),
         };
         let point = |node_cursor_pos: (f32, f32)| {
@@ -139,6 +141,9 @@ pub(crate) fn input_mouse(
         };
 
         if buttons.just_pressed(MouseButton::Left) {
+            editor.cursor_visible = true;
+            editor.cursor_timer.reset();
+
             if let Some(node_cursor_pos) = get_node_cursor_pos(
                 primary_window,
                 transform,
@@ -150,25 +155,26 @@ pub(crate) fn input_mouse(
                 let (mut x, y) = point(node_cursor_pos);
                 x += x_offset.0.unwrap_or((0., 0.)).0 as i32;
                 if shift {
-                    editor.0.action(&mut font_system.0, Action::Drag { x, y });
+                    editor.action(&mut font_system.0, Action::Drag { x, y });
                 } else {
                     match *click_count {
                         1 => {
-                            editor.0.action(&mut font_system.0, Action::Click { x, y });
+                            editor.action(&mut font_system.0, Action::Click { x, y });
                         }
                         2 => {
                             // select word
-                            editor.0.action(&mut font_system.0, Action::LeftWord);
-                            let cursor = editor.0.cursor();
-                            editor.0.set_select_opt(Some(cursor));
-                            editor.0.action(&mut font_system.0, Action::RightWord);
+                            editor.action(&mut font_system.0, Action::Motion(Motion::LeftWord));
+                            let cursor = editor.cursor();
+                            editor.set_selection(Selection::Normal(cursor));
+                            editor.action(&mut font_system.0, Action::Motion(Motion::RightWord));
                         }
                         3 => {
                             // select paragraph
-                            editor.0.action(&mut font_system.0, Action::ParagraphStart);
-                            let cursor = editor.0.cursor();
-                            editor.0.set_select_opt(Some(cursor));
-                            editor.0.action(&mut font_system.0, Action::ParagraphEnd);
+                            editor
+                                .action(&mut font_system.0, Action::Motion(Motion::ParagraphStart));
+                            let cursor = editor.cursor();
+                            editor.set_selection(Selection::Normal(cursor));
+                            editor.action(&mut font_system.0, Action::Motion(Motion::ParagraphEnd));
                         }
                         _ => {}
                     }
@@ -189,9 +195,9 @@ pub(crate) fn input_mouse(
                 let (mut x, y) = point(node_cursor_pos);
                 x += x_offset.0.unwrap_or((0., 0.)).0 as i32;
                 if active_editor.is_changed() && !shift {
-                    editor.0.action(&mut font_system.0, Action::Click { x, y });
+                    editor.action(&mut font_system.0, Action::Click { x, y });
                 } else {
-                    editor.0.action(&mut font_system.0, Action::Drag { x, y });
+                    editor.action(&mut font_system.0, Action::Drag { x, y });
                 }
             }
             return;
@@ -200,7 +206,7 @@ pub(crate) fn input_mouse(
         for ev in scroll_evr.read() {
             match ev.unit {
                 MouseScrollUnit::Line => {
-                    editor.0.action(
+                    editor.action(
                         &mut font_system.0,
                         Action::Scroll {
                             lines: -ev.y as i32,
@@ -208,8 +214,8 @@ pub(crate) fn input_mouse(
                     );
                 }
                 MouseScrollUnit::Pixel => {
-                    let line_height = editor.0.buffer().metrics().line_height;
-                    editor.0.action(
+                    let line_height = buffer.metrics().line_height;
+                    editor.action(
                         &mut font_system.0,
                         Action::Scroll {
                             lines: -(ev.y / line_height) as i32,
@@ -221,15 +227,17 @@ pub(crate) fn input_mouse(
     }
 }
 
+#[derive(Component)]
+pub struct PasswordInput; // PLACEHOLDER bc this fn uses it's presence
+
 // TODO: split copy/paste into own fn, separate fn for wasm
 pub(crate) fn input_kb(
-    active_editor: Res<Focus>,
+    active_editor: Res<FocusedWidget>,
     keys: Res<ButtonInput<KeyCode>>,
     mut char_evr: EventReader<ReceivedCharacter>,
     mut cosmic_edit_query: Query<(
         &mut CosmicEditor,
-        &mut CosmicEditHistory,
-        &CosmicAttrs,
+        &mut CosmicBuffer,
         &CosmicMaxLines,
         &CosmicMaxChars,
         Entity,
@@ -239,30 +247,21 @@ pub(crate) fn input_kb(
     mut evw_changed: EventWriter<CosmicTextChanged>,
     mut font_system: ResMut<CosmicFontSystem>,
     mut is_deleting: Local<bool>,
-    mut edits_duration: Local<Option<Duration>>,
     _channel: Option<Res<WasmPasteAsyncChannel>>,
 ) {
     let Some(active_editor_entity) = active_editor.0 else {
         return;
     };
 
-    if let Ok((
-        mut editor,
-        mut edit_history,
-        attrs,
-        max_lines,
-        max_chars,
-        entity,
-        readonly_opt,
-        password_opt,
-    )) = cosmic_edit_query.get_mut(active_editor_entity)
+    if let Ok((mut editor, buffer, max_lines, max_chars, entity, readonly_opt, password_opt)) =
+        cosmic_edit_query.get_mut(active_editor_entity)
     {
+        if keys.get_just_pressed().len() != 0 {
+            editor.cursor_visible = true;
+            editor.cursor_timer.reset();
+        }
         let readonly = readonly_opt.is_some();
 
-        let attrs = &attrs.0;
-
-        let now_ms = get_timestamp();
-
         let command = keypress_command(&keys);
 
         #[cfg(target_arch = "wasm32")]
@@ -284,10 +283,10 @@ pub(crate) fn input_kb(
         let option = keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]);
 
         // if shift key is pressed
-        let already_has_selection = editor.0.select_opt().is_some();
+        let already_has_selection = editor.selection() != Selection::None;
         if shift && !already_has_selection {
-            let cursor = editor.0.cursor();
-            editor.0.set_select_opt(Some(cursor));
+            let cursor = editor.cursor();
+            editor.set_selection(Selection::Normal(cursor));
         }
 
         #[cfg(target_os = "macos")]
@@ -296,126 +295,124 @@ pub(crate) fn input_kb(
         let should_jump = command;
 
         if should_jump && keys.just_pressed(KeyCode::ArrowLeft) {
-            editor.0.action(&mut font_system.0, Action::PreviousWord);
+            editor.action(&mut font_system.0, Action::Motion(Motion::PreviousWord));
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
         if should_jump && keys.just_pressed(KeyCode::ArrowRight) {
-            editor.0.action(&mut font_system.0, Action::NextWord);
+            editor.action(&mut font_system.0, Action::Motion(Motion::NextWord));
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
         if should_jump && keys.just_pressed(KeyCode::Home) {
-            editor.0.action(&mut font_system.0, Action::BufferStart);
-            // there's a bug with cosmic text where it doesn't update the visual cursor for this action
-            // TODO: fix upstream
-            editor.0.buffer_mut().set_redraw(true);
+            editor.action(&mut font_system.0, Action::Motion(Motion::BufferStart));
+            editor.set_redraw(true);
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
         if should_jump && keys.just_pressed(KeyCode::End) {
-            editor.0.action(&mut font_system.0, Action::BufferEnd);
-            // there's a bug with cosmic text where it doesn't update the visual cursor for this action
-            // TODO: fix upstream
-            editor.0.buffer_mut().set_redraw(true);
+            editor.action(&mut font_system.0, Action::Motion(Motion::BufferEnd));
+            editor.set_redraw(true);
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
 
         if keys.just_pressed(KeyCode::ArrowLeft) {
-            editor.0.action(&mut font_system.0, Action::Left);
+            editor.action(&mut font_system.0, Action::Motion(Motion::Left));
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
         if keys.just_pressed(KeyCode::ArrowRight) {
-            editor.0.action(&mut font_system.0, Action::Right);
+            editor.action(&mut font_system.0, Action::Motion(Motion::Right));
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
         if keys.just_pressed(KeyCode::ArrowUp) {
-            editor.0.action(&mut font_system.0, Action::Up);
+            editor.action(&mut font_system.0, Action::Motion(Motion::Up));
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
         if keys.just_pressed(KeyCode::ArrowDown) {
-            editor.0.action(&mut font_system.0, Action::Down);
+            editor.action(&mut font_system.0, Action::Motion(Motion::Down));
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
 
         if keys.just_pressed(KeyCode::Backspace) & !readonly {
             // fix for issue #8
-            if let Some(select) = editor.0.select_opt() {
-                if editor.0.cursor().line == select.line && editor.0.cursor().index == select.index
-                {
-                    editor.0.set_select_opt(None);
-                }
+            let select = editor.selection();
+            if select != Selection::None {
+                // TODO: fix zero-width selections if needed
+                //
+                // if editor.cursor().line == select.line && editor.cursor().index == select.index {
+                //     editor.set_selection(Selection::None);
+                // }
             }
             *is_deleting = true;
-            editor.0.action(&mut font_system.0, Action::Backspace);
+            #[cfg(target_arch = "wasm32")]
+            editor.action(&mut font_system.0, Action::Backspace);
         }
 
         if keys.just_released(KeyCode::Backspace) {
             *is_deleting = false;
         }
         if keys.just_pressed(KeyCode::Delete) && !readonly {
-            editor.0.action(&mut font_system.0, Action::Delete);
+            editor.action(&mut font_system.0, Action::Delete);
         }
         if keys.just_pressed(KeyCode::Escape) {
-            editor.0.action(&mut font_system.0, Action::Escape);
+            editor.action(&mut font_system.0, Action::Escape);
         }
         if command && keys.just_pressed(KeyCode::KeyA) {
-            editor.0.action(&mut font_system.0, Action::BufferEnd);
-            let current_cursor = editor.0.cursor();
-            editor.0.set_select_opt(Some(Cursor {
+            editor.action(&mut font_system.0, Action::Motion(Motion::BufferEnd));
+            let current_cursor = editor.cursor();
+            editor.set_selection(Selection::Normal(Cursor {
                 line: 0,
                 index: 0,
                 affinity: current_cursor.affinity,
-                color: current_cursor.color,
             }));
             return;
         }
         if keys.just_pressed(KeyCode::Home) {
-            editor.0.action(&mut font_system.0, Action::Home);
+            editor.action(&mut font_system.0, Action::Motion(Motion::Home));
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
         if keys.just_pressed(KeyCode::End) {
-            editor.0.action(&mut font_system.0, Action::End);
+            editor.action(&mut font_system.0, Action::Motion(Motion::End));
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
         if keys.just_pressed(KeyCode::PageUp) {
-            editor.0.action(&mut font_system.0, Action::PageUp);
+            editor.action(&mut font_system.0, Action::Motion(Motion::PageUp));
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
         if keys.just_pressed(KeyCode::PageDown) {
-            editor.0.action(&mut font_system.0, Action::PageDown);
+            editor.action(&mut font_system.0, Action::Motion(Motion::PageDown));
             if !shift {
-                editor.0.set_select_opt(None);
+                editor.set_selection(Selection::None);
             }
             return;
         }
@@ -428,7 +425,7 @@ pub(crate) fn input_kb(
                     if password_opt.is_some() {
                         return;
                     }
-                    if let Some(text) = editor.0.copy_selection() {
+                    if let Some(text) = editor.copy_selection() {
                         clipboard.set_text(text).unwrap();
                         return;
                     }
@@ -437,28 +434,26 @@ pub(crate) fn input_kb(
                     if password_opt.is_some() {
                         return;
                     }
-                    if let Some(text) = editor.0.copy_selection() {
+                    if let Some(text) = editor.copy_selection() {
                         clipboard.set_text(text).unwrap();
-                        editor.0.delete_selection();
+                        editor.delete_selection();
                     }
                     is_clipboard = true;
                 }
                 if command && keys.just_pressed(KeyCode::KeyV) && !readonly {
                     if let Ok(text) = clipboard.get_text() {
                         for c in text.chars() {
-                            if max_chars.0 == 0 || editor.get_text().len() < max_chars.0 {
+                            if max_chars.0 == 0 || buffer.get_text().len() < max_chars.0 {
                                 if c == 0xA as char {
-                                    if max_lines.0 == 0
-                                        || editor.0.buffer().lines.len() < max_lines.0
-                                    {
-                                        editor.0.action(&mut font_system.0, Action::Insert(c));
+                                    if max_lines.0 == 0 || buffer.lines.len() < max_lines.0 {
+                                        editor.action(&mut font_system.0, Action::Insert(c));
                                     }
                                 } else {
                                     if password_opt.is_some() && c.len_utf8() > 1 {
                                         println!("Cannot input multi-byte char '{}' to password field! See https://github.com/StaffEngineer/bevy_cosmic_edit/pull/99#issuecomment-1782607486",c);
                                         continue;
                                     }
-                                    editor.0.action(&mut font_system.0, Action::Insert(c));
+                                    editor.action(&mut font_system.0, Action::Insert(c));
                                 }
                             }
                         }
@@ -474,7 +469,7 @@ pub(crate) fn input_kb(
                 if password_opt.is_some() {
                     return;
                 }
-                if let Some(text) = editor.0.copy_selection() {
+                if let Some(text) = editor.copy_selection() {
                     write_clipboard_wasm(text.as_str());
                     return;
                 }
@@ -484,9 +479,9 @@ pub(crate) fn input_kb(
                 if password_opt.is_some() {
                     return;
                 }
-                if let Some(text) = editor.0.copy_selection() {
+                if let Some(text) = editor.copy_selection() {
                     write_clipboard_wasm(text.as_str());
-                    editor.0.delete_selection();
+                    editor.delete_selection();
                 }
                 is_clipboard = true;
             }
@@ -516,12 +511,12 @@ pub(crate) fn input_kb(
         let mut is_return = false;
         if keys.just_pressed(KeyCode::Enter) {
             is_return = true;
-            if (max_lines.0 == 0 || editor.0.buffer().lines.len() < max_lines.0)
-                && (max_chars.0 == 0 || editor.get_text().len() < max_chars.0)
+            if (max_lines.0 == 0 || buffer.lines.len() < max_lines.0)
+                && (max_chars.0 == 0 || buffer.get_text().len() < max_chars.0)
             {
                 // to have new line on wasm rather than E
                 is_edit = true;
-                editor.0.action(&mut font_system.0, Action::Insert('\n'));
+                editor.action(&mut font_system.0, Action::Insert('\n'));
             }
         }
 
@@ -529,8 +524,8 @@ pub(crate) fn input_kb(
             for char_ev in char_evr.read() {
                 is_edit = true;
                 if *is_deleting {
-                    editor.0.action(&mut font_system.0, Action::Backspace);
-                } else if !command && (max_chars.0 == 0 || editor.get_text().len() < max_chars.0) {
+                    editor.action(&mut font_system.0, Action::Backspace);
+                } else if !command && (max_chars.0 == 0 || buffer.get_text().len() < max_chars.0) {
                     if password_opt.is_some() && char_ev.char.len() > 1 {
                         println!("Cannot input multi-byte char '{}' to password field! See https://github.com/StaffEngineer/bevy_cosmic_edit/pull/99#issuecomment-1782607486",char_ev.char);
                         continue;
@@ -538,35 +533,18 @@ pub(crate) fn input_kb(
                     let b = char_ev.char.as_bytes();
                     for c in b {
                         let c: char = (*c).into();
-                        editor.0.action(&mut font_system.0, Action::Insert(c));
+                        editor.action(&mut font_system.0, Action::Insert(c));
+                        editor.set_redraw(true);
                     }
                 }
             }
         }
 
-        // skip event + history if undo/redo keys pressed
-
-        let requested_redo = keypress_redo(&keys);
-        let requested_undo =
-            command && (keys.pressed(KeyCode::KeyZ) || keys.just_pressed(KeyCode::KeyZ));
-
-        if requested_redo || requested_undo || !is_edit {
+        if !is_edit {
             return;
         }
 
-        evw_changed.send(CosmicTextChanged((entity, editor.get_text())));
-
-        if let Some(last_edit_duration) = *edits_duration {
-            if Duration::from_millis(now_ms as u64) - last_edit_duration
-                > Duration::from_millis(150)
-            {
-                save_edit_history(&mut editor.0, attrs, &mut edit_history);
-                *edits_duration = Some(Duration::from_millis(now_ms as u64));
-            }
-        } else {
-            save_edit_history(&mut editor.0, attrs, &mut edit_history);
-            *edits_duration = Some(Duration::from_millis(now_ms as u64));
-        }
+        evw_changed.send(CosmicTextChanged((entity, buffer.get_text())));
     }
 }
 
@@ -593,96 +571,6 @@ fn keypress_command(keys: &ButtonInput<KeyCode>) -> bool {
     command
 }
 
-fn keypress_redo(keys: &ButtonInput<KeyCode>) -> bool {
-    let command = keypress_command(keys);
-    let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
-
-    #[cfg(not(target_os = "windows"))]
-    let requested_redo = command && shift && keys.just_pressed(KeyCode::KeyZ);
-
-    // TODO: windows OS detection for wasm here
-    #[cfg(target_os = "windows")]
-    let requested_redo = command && keys.just_pressed(KeyCode::KeyY);
-
-    requested_redo
-}
-
-pub(crate) fn undo_redo(
-    active_editor: Res<Focus>,
-    keys: Res<ButtonInput<KeyCode>>,
-    mut editor_q: Query<
-        (&mut CosmicEditor, &CosmicAttrs, &mut CosmicEditHistory),
-        Without<ReadOnly>,
-    >,
-    mut evw_changed: EventWriter<CosmicTextChanged>,
-) {
-    let entity = match active_editor.0 {
-        Some(entity) => entity,
-        None => return,
-    };
-
-    let (mut editor, attrs, mut edit_history) = match editor_q.get_mut(entity) {
-        Ok(components) => components,
-        Err(_) => return,
-    };
-
-    let command = keypress_command(&keys);
-
-    let attrs = &attrs.0;
-
-    let requested_redo = keypress_redo(&keys);
-    let requested_undo = command && keys.just_pressed(KeyCode::KeyZ);
-
-    if !(requested_redo || requested_undo) {
-        return;
-    }
-
-    let edits = &edit_history.edits;
-
-    if edits.is_empty() {
-        return;
-    }
-
-    // use not redo rather than undo, cos undo will be true when redo is
-    if !requested_redo && edit_history.current_edit == 0 {
-        return;
-    }
-
-    if requested_redo && edit_history.current_edit == edits.len() - 1 {
-        return;
-    }
-
-    let index = if requested_redo {
-        edit_history.current_edit + 1
-    } else {
-        edit_history.current_edit - 1
-    };
-
-    if let Some(current_edit) = edits.get(index) {
-        editor.0.buffer_mut().lines.clear();
-        for line in current_edit.lines.iter() {
-            let mut line_text = String::new();
-            let mut attrs_list = AttrsList::new(attrs.as_attrs());
-            for (text, attrs) in line.iter() {
-                let start = line_text.len();
-                line_text.push_str(text);
-                let end = line_text.len();
-                attrs_list.add_span(start..end, attrs.as_attrs());
-            }
-            editor.0.buffer_mut().lines.push(BufferLine::new(
-                line_text,
-                attrs_list,
-                Shaping::Advanced,
-            ));
-        }
-        editor.0.set_cursor(current_edit.cursor);
-        editor.0.set_select_opt(None); // prevent auto selection of redo-inserted text
-        editor.0.buffer_mut().set_redraw(true);
-        edit_history.current_edit = index;
-        evw_changed.send(CosmicTextChanged((entity, editor.get_text())));
-    }
-}
-
 #[cfg(target_arch = "wasm32")]
 #[wasm_bindgen]
 pub fn write_clipboard_wasm(text: &str) {
@@ -711,10 +599,10 @@ pub fn poll_wasm_paste(
     mut editor_q: Query<
         (
             &mut CosmicEditor,
-            &CosmicAttrs,
+            &mut CosmicBuffer,
+            &crate::DefaultAttrs,
             &CosmicMaxChars,
             &CosmicMaxChars,
-            &mut CosmicEditHistory,
             Option<&PasswordInput>,
         ),
         Without<ReadOnly>,
@@ -726,30 +614,28 @@ pub fn poll_wasm_paste(
     match inlet {
         Ok(inlet) => {
             let entity = inlet.entity;
-            if let Ok((mut editor, attrs, max_chars, max_lines, mut edit_history, password_opt)) =
+            if let Ok((mut editor, mut buffer, attrs, max_chars, max_lines, password_opt)) =
                 editor_q.get_mut(entity)
             {
                 let text = inlet.text;
                 let attrs = &attrs.0;
                 for c in text.chars() {
-                    if max_chars.0 == 0 || editor.get_text().len() < max_chars.0 {
+                    if max_chars.0 == 0 || buffer.get_text().len() < max_chars.0 {
                         if c == 0xA as char {
-                            if max_lines.0 == 0 || editor.0.buffer().lines.len() < max_lines.0 {
-                                editor.0.action(&mut font_system.0, Action::Insert(c));
+                            if max_lines.0 == 0 || buffer.lines.len() < max_lines.0 {
+                                editor.action(&mut font_system.0, Action::Insert(c));
                             }
                         } else {
                             if password_opt.is_some() && c.len_utf8() > 1 {
-                                // TODO: console.log here instead
-                                println!("Cannot input multi-byte char '{}' to password field! See https://github.com/StaffEngineer/bevy_cosmic_edit/pull/99#issuecomment-1782607486",c);
+                                info!("Cannot input multi-byte char '{}' to password field! See https://github.com/StaffEngineer/bevy_cosmic_edit/pull/99#issuecomment-1782607486",c);
                                 continue;
                             }
-                            editor.0.action(&mut font_system.0, Action::Insert(c));
+                            editor.action(&mut font_system.0, Action::Insert(c));
                         }
                     }
                 }
 
-                evw_changed.send(CosmicTextChanged((entity, editor.get_text())));
-                save_edit_history(&mut editor.0, attrs, &mut edit_history);
+                evw_changed.send(CosmicTextChanged((entity, buffer.get_text())));
             }
         }
         Err(_) => {}
diff --git a/src/layout.rs b/src/layout.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2665d9ae1b0dcc12c28c6a6beb83b1bdd35960eb
--- /dev/null
+++ b/src/layout.rs
@@ -0,0 +1,171 @@
+use crate::*;
+use bevy::{prelude::*, window::PrimaryWindow};
+use cosmic_text::Affinity;
+
+use self::buffer::{get_x_offset_center, get_y_offset_center};
+
+#[derive(Component, Default)]
+pub struct CosmicPadding(pub Vec2);
+
+#[derive(Component, Default)]
+pub struct CosmicWidgetSize(pub Vec2);
+
+pub fn reshape(mut query: Query<&mut CosmicEditor>, mut font_system: ResMut<CosmicFontSystem>) {
+    for mut cosmic_editor in query.iter_mut() {
+        cosmic_editor.shape_as_needed(&mut font_system.0, false);
+    }
+}
+
+pub fn set_padding(
+    mut query: Query<
+        (
+            &mut CosmicPadding,
+            &CosmicTextPosition,
+            &CosmicBuffer,
+            &CosmicWidgetSize,
+            Option<&CosmicEditor>,
+        ),
+        Or<(
+            With<CosmicEditor>,
+            Changed<CosmicTextPosition>,
+            Changed<CosmicBuffer>,
+            Changed<CosmicWidgetSize>,
+        )>,
+    >,
+) {
+    for (mut padding, position, buffer, size, editor_opt) in query.iter_mut() {
+        // TODO: At least one of these clones is uneccessary
+        let mut buffer = buffer.0.clone();
+
+        if let Some(editor) = editor_opt {
+            buffer = editor.with_buffer(|b| b.clone());
+        }
+
+        if !buffer.redraw() {
+            continue;
+        }
+
+        padding.0 = match position {
+            CosmicTextPosition::Center => Vec2::new(
+                get_x_offset_center(size.0.x, &buffer) as f32,
+                get_y_offset_center(size.0.y, &buffer) as f32,
+            ),
+            CosmicTextPosition::TopLeft { padding } => Vec2::new(*padding as f32, *padding as f32),
+            CosmicTextPosition::Left { padding } => Vec2::new(
+                *padding as f32,
+                get_y_offset_center(size.0.y, &buffer) as f32,
+            ),
+        }
+    }
+}
+
+pub fn set_widget_size(
+    mut query: Query<(&mut CosmicWidgetSize, &Sprite), Changed<Sprite>>,
+    windows: Query<&Window, With<PrimaryWindow>>,
+) {
+    if windows.iter().len() == 0 {
+        return;
+    }
+    // TODO: early return if sprite size is unchanged
+    let scale = windows.single().scale_factor();
+    for (mut size, sprite) in query.iter_mut() {
+        size.0 = sprite.custom_size.unwrap().ceil() * scale;
+    }
+}
+
+pub fn set_buffer_size(
+    mut query: Query<
+        (
+            &mut CosmicBuffer,
+            &CosmicMode,
+            &CosmicWidgetSize,
+            &CosmicTextPosition,
+        ),
+        Or<(
+            Changed<CosmicMode>,
+            Changed<CosmicWidgetSize>,
+            Changed<CosmicTextPosition>,
+        )>,
+    >,
+    mut font_system: ResMut<CosmicFontSystem>,
+) {
+    for (mut buffer, mode, size, position) in query.iter_mut() {
+        let padding_x = match position {
+            CosmicTextPosition::Center => 0.,
+            CosmicTextPosition::TopLeft { padding } => *padding as f32,
+            CosmicTextPosition::Left { padding } => *padding as f32,
+        };
+
+        let (buffer_width, buffer_height) = match mode {
+            CosmicMode::InfiniteLine => (f32::MAX, size.0.y),
+            CosmicMode::Wrap => (size.0.x - padding_x, size.0.y),
+        };
+
+        buffer.set_size(&mut font_system.0, buffer_width, buffer_height);
+    }
+}
+
+pub fn new_image_from_default(
+    mut query: Query<&mut Handle<Image>, Added<CosmicBuffer>>,
+    mut images: ResMut<Assets<Image>>,
+) {
+    for mut canvas in query.iter_mut() {
+        *canvas = images.add(Image::default());
+    }
+}
+
+pub fn set_cursor(
+    mut query: Query<(
+        &mut XOffset,
+        &CosmicMode,
+        &CosmicEditor,
+        &CosmicBuffer,
+        &CosmicWidgetSize,
+        &CosmicPadding,
+    )>,
+) {
+    for (mut x_offset, mode, editor, buffer, size, padding) in query.iter_mut() {
+        let mut cursor_x = 0.;
+        if mode == &CosmicMode::InfiniteLine {
+            if let Some(line) = buffer.layout_runs().next() {
+                for (idx, glyph) in line.glyphs.iter().enumerate() {
+                    if editor.cursor().affinity == Affinity::Before {
+                        if idx <= editor.cursor().index {
+                            cursor_x += glyph.w;
+                        }
+                    } else if idx < editor.cursor().index {
+                        cursor_x += glyph.w;
+                    } else {
+                        break;
+                    }
+                }
+            }
+        }
+
+        if mode == &CosmicMode::InfiniteLine && x_offset.0.is_none() {
+            *x_offset = XOffset(Some((0., size.0.x - 2. * padding.0.x)));
+        }
+
+        if let Some((x_min, x_max)) = x_offset.0 {
+            if cursor_x > x_max {
+                let diff = cursor_x - x_max;
+                *x_offset = XOffset(Some((x_min + diff, cursor_x)));
+            }
+            if cursor_x < x_min {
+                let diff = x_min - cursor_x;
+                *x_offset = XOffset(Some((cursor_x, x_max - diff)));
+            }
+        }
+    }
+}
+
+pub fn set_sprite_size_from_ui(
+    mut source_q: Query<&mut Sprite, With<CosmicBuffer>>,
+    dest_q: Query<(&Node, &CosmicSource), Changed<Node>>,
+) {
+    for (node, source) in dest_q.iter() {
+        if let Ok(mut sprite) = source_q.get_mut(source.0) {
+            sprite.custom_size = Some(node.size().ceil().max(Vec2::ONE));
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 8bb9ec2fe0413aa8a91b1f7db056b7de78048dd7..0cc9cf3c8ddce3fbb9ce532758cc5ae45fecb383 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,51 +1,43 @@
 #![allow(clippy::type_complexity)]
 
+mod buffer;
 mod cursor;
+pub mod focus;
 mod input;
+mod layout;
 mod render;
 
-use std::{collections::VecDeque, path::PathBuf};
+use std::{path::PathBuf, time::Duration};
 
 use bevy::{prelude::*, transform::TransformSystem};
-pub use cosmic_text::{
-    Action, Attrs, AttrsOwned, Color as CosmicColor, Cursor, Edit, Family, Style as FontStyle,
-    Weight as FontWeight,
+use buffer::{
+    add_font_system, set_editor_redraw, set_initial_scale, set_redraw, swap_target_handle,
 };
-use cosmic_text::{
-    AttrsList, Buffer, BufferLine, Editor, FontSystem, Metrics, Shaping, SwashCache,
+pub use buffer::{get_x_offset_center, get_y_offset_center, CosmicBuffer};
+pub use cosmic_text::{
+    Action, Attrs, AttrsOwned, Color as CosmicColor, Cursor, Edit, Family, Metrics, Shaping,
+    Style as FontStyle, Weight as FontWeight,
 };
+use cosmic_text::{Buffer, Editor, FontSystem, SwashCache};
 use cursor::{change_cursor, hover_sprites, hover_ui};
 pub use cursor::{TextHoverIn, TextHoverOut};
-use input::{input_kb, input_mouse, undo_redo, ClickTimer};
+pub use focus::*;
+use input::{input_kb, input_mouse, ClickTimer};
 #[cfg(target_arch = "wasm32")]
 use input::{poll_wasm_paste, WasmPaste, WasmPasteAsyncChannel};
-use render::{
-    blink_cursor, freeze_cursor_blink, hide_inactive_or_readonly_cursor, hide_password_text,
-    on_scale_factor_change, restore_password_text, restore_placeholder_text, set_initial_scale,
-    show_placeholder, CosmicPadding, CosmicRenderSet, CosmicWidgetSize, CursorBlinkTimer,
-    CursorVisibility, PasswordValues, SwashCacheState,
+use layout::{
+    new_image_from_default, reshape, set_buffer_size, set_cursor, set_padding,
+    set_sprite_size_from_ui, set_widget_size, CosmicPadding, CosmicWidgetSize,
 };
+use render::{blink_cursor, render_texture, SwashCacheState};
 
 #[cfg(feature = "multicam")]
 #[derive(Component)]
 pub struct CosmicPrimaryCamera;
 
-#[derive(Clone, Component, PartialEq, Debug)]
-pub enum CosmicText {
-    OneStyle(String),
-    MultiStyle(Vec<Vec<(String, AttrsOwned)>>),
-}
-
-impl Default for CosmicText {
-    fn default() -> Self {
-        Self::OneStyle(String::new())
-    }
-}
-
 #[derive(Clone, Component, PartialEq, Default)]
 pub enum CosmicMode {
     InfiniteLine,
-    AutoHeight,
     #[default]
     Wrap,
 }
@@ -74,151 +66,59 @@ pub enum CosmicTextPosition {
 #[derive(Event, Debug)]
 pub struct CosmicTextChanged(pub (Entity, String));
 
-// TODO docs
-const DEFAULT_SCALE_PLACEHOLDER: f32 = 0.696969;
-
-#[derive(Clone, Component)]
-pub struct CosmicMetrics {
-    pub font_size: f32,
-    pub line_height: f32,
-    pub scale_factor: f32,
-}
-
-impl Default for CosmicMetrics {
-    fn default() -> Self {
-        Self {
-            font_size: 12.,
-            line_height: 12.,
-            scale_factor: DEFAULT_SCALE_PLACEHOLDER,
-        }
-    }
-}
-
-#[derive(Resource)]
+#[derive(Resource, Deref, DerefMut)]
 pub struct CosmicFontSystem(pub FontSystem);
 
 #[derive(Component)]
 pub struct ReadOnly; // tag component
 
-#[derive(Component, Debug)]
-struct XOffset(Option<(f32, f32)>);
+#[derive(Component, Debug, Default)]
+pub struct XOffset(Option<(f32, f32)>);
 
-#[derive(Component)]
-pub struct CosmicEditor(pub Editor);
+#[derive(Component, Deref, DerefMut)]
+pub struct CosmicEditor {
+    #[deref]
+    pub editor: Editor<'static>,
+    pub cursor_visible: bool,
+    pub cursor_timer: Timer,
+}
 
 impl CosmicEditor {
-    pub fn set_text(
-        &mut self,
-        text: CosmicText,
-        attrs: AttrsOwned,
-        font_system: &mut FontSystem,
-    ) -> &mut Self {
-        // TODO: invoke trim_text here
-        let editor = &mut self.0;
-        editor.buffer_mut().lines.clear();
-        match text {
-            CosmicText::OneStyle(text) => {
-                editor.buffer_mut().set_text(
-                    font_system,
-                    text.as_str(),
-                    attrs.as_attrs(),
-                    Shaping::Advanced,
-                );
-                let mut cursor = editor.cursor();
-                cursor.line = editor.buffer_mut().lines.len() - 1;
-                cursor.index = editor.buffer_mut().lines[cursor.line].text().len();
-                editor.set_cursor(cursor);
-            }
-            CosmicText::MultiStyle(lines) => {
-                for line in lines {
-                    let mut line_text = String::new();
-                    let mut attrs_list = AttrsList::new(attrs.as_attrs());
-                    for (text, attrs) in line.iter() {
-                        let start = line_text.len();
-                        line_text.push_str(text);
-                        let end = line_text.len();
-                        attrs_list.add_span(start..end, attrs.as_attrs());
-                    }
-                    editor.buffer_mut().lines.push(BufferLine::new(
-                        line_text,
-                        attrs_list,
-                        Shaping::Advanced,
-                    ));
-                }
-            }
+    fn new(editor: Editor<'static>) -> Self {
+        Self {
+            editor,
+            cursor_visible: true,
+            cursor_timer: Timer::new(Duration::from_millis(530), TimerMode::Repeating),
         }
-        self
-    }
-
-    /// Retrieves the cosmic text content from an editor.
-    ///
-    /// # Arguments
-    ///
-    /// * none, takes the rust magic ref to self
-    ///
-    /// # Returns
-    ///
-    /// A `String` containing the cosmic text content.
-    pub fn get_text(&self) -> String {
-        let buffer = self.0.buffer();
-        let mut text = String::new();
-        let line_count = buffer.lines.len();
-
-        for (i, line) in buffer.lines.iter().enumerate() {
-            text.push_str(line.text());
-
-            if i < line_count - 1 {
-                text.push('\n');
-            }
-        }
-
-        text
     }
 }
 
-#[derive(Component)]
-pub struct CosmicAttrs(pub AttrsOwned);
+#[derive(Component, Deref, DerefMut)]
+pub struct DefaultAttrs(pub AttrsOwned);
 
-impl Default for CosmicAttrs {
+impl Default for DefaultAttrs {
     fn default() -> Self {
-        CosmicAttrs(AttrsOwned::new(Attrs::new()))
+        DefaultAttrs(AttrsOwned::new(Attrs::new()))
     }
 }
 
 #[derive(Component, Default)]
 pub struct CosmicBackground(pub Option<Handle<Image>>);
 
-#[derive(Component, Default)]
-pub struct CosmicMaxLines(pub usize);
-
-#[derive(Component, Default)]
-pub struct CosmicMaxChars(pub usize);
-
-#[derive(Component, Default)]
+#[derive(Component, Default, Deref)]
 pub struct FillColor(pub Color);
 
-#[derive(Component, Default)]
-pub struct PlaceholderText(pub CosmicText);
+#[derive(Component, Default, Deref)]
+pub struct CursorColor(pub Color);
 
-#[derive(Component)]
-pub struct PlaceholderAttrs(pub AttrsOwned);
+#[derive(Component, Default, Deref)]
+pub struct SelectionColor(pub Color);
 
-impl Default for PlaceholderAttrs {
-    fn default() -> Self {
-        Self(AttrsOwned::new(
-            Attrs::new().color(CosmicColor::rgb(128, 128, 128)),
-        ))
-    }
-}
-
-#[derive(Component)]
-pub struct PasswordInput(pub char);
+#[derive(Component, Default)]
+pub struct CosmicMaxLines(pub usize);
 
-impl Default for PasswordInput {
-    fn default() -> Self {
-        PasswordInput("鈥�".chars().next().unwrap())
-    }
-}
+#[derive(Component, Default)]
+pub struct CosmicMaxChars(pub usize);
 
 #[derive(Component)]
 pub struct CosmicSource(pub Entity);
@@ -226,17 +126,21 @@ pub struct CosmicSource(pub Entity);
 #[derive(Bundle)]
 pub struct CosmicEditBundle {
     // cosmic bits
+    pub buffer: CosmicBuffer,
+    // render bits
     pub fill_color: FillColor,
-    pub text_position: CosmicTextPosition,
-    pub metrics: CosmicMetrics,
-    pub attrs: CosmicAttrs,
+    pub cursor_color: CursorColor,
+    pub selection_color: SelectionColor,
+    pub default_attrs: DefaultAttrs,
     pub background_image: CosmicBackground,
+    pub sprite_bundle: SpriteBundle,
+    // restriction bits
     pub max_lines: CosmicMaxLines,
     pub max_chars: CosmicMaxChars,
-    pub text_setter: CosmicText,
+    // layout bits
+    pub x_offset: XOffset,
     pub mode: CosmicMode,
-    pub sprite_bundle: SpriteBundle,
-    // render bits
+    pub text_position: CosmicTextPosition,
     pub padding: CosmicPadding,
     pub widget_size: CosmicWidgetSize,
 }
@@ -244,14 +148,15 @@ pub struct CosmicEditBundle {
 impl Default for CosmicEditBundle {
     fn default() -> Self {
         CosmicEditBundle {
+            buffer: Default::default(),
             fill_color: Default::default(),
+            cursor_color: CursorColor(Color::BLACK),
+            selection_color: SelectionColor(Color::GRAY),
             text_position: Default::default(),
-            metrics: Default::default(),
-            attrs: Default::default(),
+            default_attrs: Default::default(),
             background_image: Default::default(),
             max_lines: Default::default(),
             max_chars: Default::default(),
-            text_setter: Default::default(),
             mode: Default::default(),
             sprite_bundle: SpriteBundle {
                 sprite: Sprite {
@@ -261,35 +166,13 @@ impl Default for CosmicEditBundle {
                 visibility: Visibility::Hidden,
                 ..default()
             },
+            x_offset: XOffset(None),
             padding: Default::default(),
             widget_size: Default::default(),
         }
     }
 }
 
-#[derive(Bundle)]
-pub struct CosmicEditPlaceholderBundle {
-    /// set this to update placeholder text
-    pub text_setter: PlaceholderText,
-    pub attrs: PlaceholderAttrs,
-}
-
-#[derive(Clone)]
-pub struct EditHistoryItem {
-    pub cursor: Cursor,
-    pub lines: Vec<Vec<(String, AttrsOwned)>>,
-}
-
-#[derive(Component, Default)]
-pub struct CosmicEditHistory {
-    pub edits: VecDeque<EditHistoryItem>,
-    pub current_edit: usize,
-}
-
-/// Resource struct that keeps track of the currently active editor entity.
-#[derive(Resource, Default, Deref, DerefMut)]
-pub struct Focus(pub Option<Entity>);
-
 /// Resource struct that holds configuration options for cosmic fonts.
 #[derive(Resource, Clone)]
 pub struct CosmicFontConfig {
@@ -302,7 +185,7 @@ impl Default for CosmicFontConfig {
     fn default() -> Self {
         let fallback_font = include_bytes!("./font/FiraMono-Regular-subset.ttf");
         Self {
-            load_system_fonts: false,
+            load_system_fonts: true,
             font_bytes: Some(vec![fallback_font]),
             fonts_dir_path: None,
         }
@@ -320,80 +203,40 @@ impl Plugin for CosmicEditPlugin {
     fn build(&self, app: &mut App) {
         let font_system = create_cosmic_font_system(self.font_config.clone());
 
-        let main_unordered = (
-            blink_cursor,
-            freeze_cursor_blink,
-            hide_inactive_or_readonly_cursor,
-            clear_inactive_selection,
-        );
-
-        let render_systems = (
-            render::new_image_from_default.in_set(CosmicRenderSet::Setup),
-            render::set_size_from_ui.in_set(CosmicRenderSet::Setup),
-            render::cosmic_reshape.in_set(CosmicRenderSet::Shaping),
-            render::cosmic_widget_size.in_set(CosmicRenderSet::Sizing),
-            render::cosmic_buffer_size.in_set(CosmicRenderSet::Sizing),
-            render::auto_height
-                .after(CosmicRenderSet::Sizing)
-                .before(CosmicRenderSet::Draw),
-            render::cosmic_padding.in_set(CosmicRenderSet::Padding),
-            render::set_cursor.in_set(CosmicRenderSet::Cursor),
-            render::render_texture.in_set(CosmicRenderSet::Draw),
-        );
+        let layout_systems = (
+            (new_image_from_default, set_sprite_size_from_ui),
+            set_widget_size,
+            set_buffer_size,
+            set_padding,
+            set_cursor,
+        )
+            .chain();
 
         app.add_systems(
             First,
             (
+                add_font_system,
                 set_initial_scale,
-                (cosmic_editor_builder, on_scale_factor_change).after(set_initial_scale),
-                render::swap_target_handle,
-            ),
-        )
-        .add_systems(
-            PreUpdate,
-            (
-                update_buffer_text,
-                init_history,
-                main_unordered,
-                hide_password_text,
-                input_mouse,
-                restore_password_text,
+                set_redraw,
+                set_editor_redraw,
+                swap_target_handle,
             )
                 .chain(),
         )
-        .add_systems(Update, (input_kb, undo_redo).chain())
-        .configure_sets(
+        .add_systems(PreUpdate, (input_mouse,).chain())
+        .add_systems(Update, (input_kb, reshape, blink_cursor).chain())
+        .add_systems(
             PostUpdate,
             (
-                CosmicRenderSet::Setup,
-                CosmicRenderSet::Shaping,
-                CosmicRenderSet::Sizing,
-                CosmicRenderSet::Cursor,
-                CosmicRenderSet::Padding,
-                CosmicRenderSet::Draw,
+                layout_systems,
+                drop_editor_unfocused,
+                add_editor_to_focused,
+                render_texture,
             )
                 .chain()
                 .after(TransformSystem::TransformPropagate),
         )
-        .add_systems(
-            PostUpdate,
-            (
-                hide_password_text,
-                show_placeholder,
-                render_systems,
-                apply_deferred, // Prevents one-frame inputs adding placeholder to editor
-                restore_password_text,
-                restore_placeholder_text,
-            )
-                .chain(),
-        )
-        .init_resource::<Focus>()
-        .init_resource::<PasswordValues>()
-        .insert_resource(CursorBlinkTimer(Timer::from_seconds(
-            0.53,
-            TimerMode::Repeating,
-        )))
-        .insert_resource(CursorVisibility(true))
+        .init_resource::<FocusedWidget>()
         .insert_resource(SwashCacheState {
             swash_cache: SwashCache::new(),
         })
@@ -424,64 +267,6 @@ impl Plugin for CosmicEditPlugin {
     }
 }
 
-fn save_edit_history(
-    editor: &mut Editor,
-    attrs: &AttrsOwned,
-    edit_history: &mut CosmicEditHistory,
-) {
-    let edits = &edit_history.edits;
-    let current_lines = get_text_spans(editor.buffer(), attrs.clone());
-    let current_edit = edit_history.current_edit;
-    let mut new_edits = VecDeque::new();
-    new_edits.extend(edits.iter().take(current_edit + 1).cloned());
-    // remove old edits
-    if new_edits.len() > 1000 {
-        new_edits.drain(0..100);
-    }
-    new_edits.push_back(EditHistoryItem {
-        cursor: editor.cursor(),
-        lines: current_lines,
-    });
-    let len = new_edits.len();
-    *edit_history = CosmicEditHistory {
-        edits: new_edits,
-        current_edit: len - 1,
-    };
-}
-
-fn init_history(
-    mut q: Query<(&mut CosmicEditor, &CosmicAttrs, &mut CosmicEditHistory), Added<CosmicEditor>>,
-) {
-    for (mut editor, attrs, mut history) in q.iter_mut() {
-        save_edit_history(&mut editor.0, &attrs.0, &mut history);
-    }
-}
-
-/// Adds the font system to each editor when added
-fn cosmic_editor_builder(
-    mut added_editors: Query<(Entity, &CosmicMetrics), Added<CosmicText>>,
-    mut font_system: ResMut<CosmicFontSystem>,
-    mut commands: Commands,
-) {
-    for (entity, metrics) in added_editors.iter_mut() {
-        let mut buffer = Buffer::new(
-            &mut font_system.0,
-            Metrics::new(metrics.font_size, metrics.line_height).scale(metrics.scale_factor),
-        );
-        // buffer.set_wrap(&mut font_system.0, cosmic_text::Wrap::None);
-        buffer.set_redraw(true);
-        let mut editor = Editor::new(buffer);
-
-        let mut cursor = editor.cursor();
-        cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
-        editor.set_cursor(cursor);
-
-        commands.entity(entity).insert(CosmicEditor(editor));
-        commands.entity(entity).insert(CosmicEditHistory::default());
-        commands.entity(entity).insert(XOffset(None));
-    }
-}
-
 fn create_cosmic_font_system(cosmic_font_config: CosmicFontConfig) -> FontSystem {
     let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
     let mut db = cosmic_text::fontdb::Database::new();
@@ -535,185 +320,6 @@ pub fn get_node_cursor_pos(
     })
 }
 
-/// Updates editor buffer when text component changes
-fn update_buffer_text(
-    mut editor_q: Query<
-        (
-            &mut CosmicEditor,
-            &mut CosmicText,
-            &CosmicAttrs,
-            &CosmicMaxChars,
-            &CosmicMaxLines,
-        ),
-        Changed<CosmicText>,
-    >,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    for (mut editor, text, attrs, max_chars, max_lines) in editor_q.iter_mut() {
-        let text = trim_text(text.to_owned(), max_chars.0, max_lines.0);
-        editor.set_text(text, attrs.0.clone(), &mut font_system.0);
-    }
-}
-
-fn trim_text(text: CosmicText, max_chars: usize, max_lines: usize) -> CosmicText {
-    if max_chars == 0 && max_lines == 0 {
-        // no limits, no work to do
-        return text;
-    }
-
-    match text {
-        CosmicText::OneStyle(mut string) => {
-            if max_chars != 0 {
-                string.truncate(max_chars);
-            }
-
-            if max_lines == 0 {
-                return CosmicText::OneStyle(string);
-            }
-
-            let mut line_acc = 0;
-            let mut char_pos = 0;
-            for c in string.chars() {
-                char_pos += 1;
-                if c == 0xA as char {
-                    line_acc += 1;
-                    if line_acc >= max_lines {
-                        // break text to pos
-                        string.truncate(char_pos);
-                        break;
-                    }
-                }
-            }
-
-            CosmicText::OneStyle(string)
-        }
-        CosmicText::MultiStyle(lines) => {
-            let mut char_acc = 0;
-            let mut line_acc = 0;
-
-            let mut trimmed_styles = vec![];
-
-            for line in lines.iter() {
-                line_acc += 1;
-                char_acc += 1; // count newlines for consistent behaviour
-
-                if (line_acc >= max_lines && max_lines > 0)
-                    || (char_acc >= max_chars && max_chars > 0)
-                {
-                    break;
-                }
-
-                let mut strs = vec![];
-
-                for (string, attrs) in line.iter() {
-                    if char_acc >= max_chars && max_chars > 0 {
-                        break;
-                    }
-
-                    let mut string = string.clone();
-
-                    if max_chars > 0 {
-                        string.truncate(max_chars - char_acc);
-                        char_acc += string.len();
-                    }
-
-                    if max_lines > 0 {
-                        for c in string.chars() {
-                            if c == 0xA as char {
-                                line_acc += 1;
-                                char_acc += 1; // count newlines for consistent behaviour
-                                if line_acc >= max_lines {
-                                    break;
-                                }
-                            }
-                        }
-                    }
-
-                    strs.push((string, attrs.clone()));
-                }
-                trimmed_styles.push(strs);
-            }
-            CosmicText::MultiStyle(trimmed_styles)
-        }
-    }
-}
-/// Returns texts from a MultiStyle buffer
-pub fn get_text_spans(
-    buffer: &Buffer,
-    default_attrs: AttrsOwned,
-) -> Vec<Vec<(String, AttrsOwned)>> {
-    let mut spans = Vec::new();
-    for line in buffer.lines.iter() {
-        let mut line_spans = Vec::new();
-        let line_text = line.text();
-        let line_attrs = line.attrs_list();
-        if line_attrs.spans().is_empty() {
-            line_spans.push((line_text.to_string(), default_attrs.clone()));
-        } else {
-            let mut current_pos = 0;
-            for span in line_attrs.spans() {
-                let span_range = span.0;
-                let span_attrs = span.1.clone();
-                let start_index = span_range.start;
-                let end_index = span_range.end;
-                if start_index > current_pos {
-                    // Add the text between the current position and the start of the span
-                    let non_span_text = line_text[current_pos..start_index].to_string();
-                    line_spans.push((non_span_text, default_attrs.clone()));
-                }
-                let span_text = line_text[start_index..end_index].to_string();
-                line_spans.push((span_text.clone(), span_attrs));
-                current_pos = end_index;
-            }
-            if current_pos < line_text.len() {
-                // Add the remaining text after the last span
-                let remaining_text = line_text[current_pos..].to_string();
-                line_spans.push((remaining_text, default_attrs.clone()));
-            }
-        }
-        spans.push(line_spans);
-    }
-    spans
-}
-
-fn get_text_size(buffer: &Buffer) -> (f32, f32) {
-    if buffer.layout_runs().count() == 0 {
-        return (0., buffer.metrics().line_height);
-    }
-    let width = buffer
-        .layout_runs()
-        .map(|run| run.line_w)
-        .reduce(f32::max)
-        .unwrap();
-    let height = buffer.layout_runs().count() as f32 * buffer.metrics().line_height;
-    (width, height)
-}
-
-pub fn get_y_offset_center(widget_height: f32, buffer: &Buffer) -> i32 {
-    let (_, text_height) = get_text_size(buffer);
-    ((widget_height - text_height) / 2.0) as i32
-}
-
-pub fn get_x_offset_center(widget_width: f32, buffer: &Buffer) -> i32 {
-    let (text_width, _) = get_text_size(buffer);
-    ((widget_width - text_width) / 2.0) as i32
-}
-
-fn clear_inactive_selection(
-    mut cosmic_editor_q: Query<(Entity, &mut CosmicEditor)>,
-    active_editor: Res<Focus>,
-) {
-    if !active_editor.is_changed() || active_editor.0.is_none() {
-        return;
-    }
-
-    for (e, mut editor) in &mut cosmic_editor_q.iter_mut() {
-        if e != active_editor.0.unwrap() {
-            editor.0.set_select_opt(None);
-        }
-    }
-}
-
 #[cfg(target_arch = "wasm32")]
 pub fn get_timestamp() -> f64 {
     js_sys::Date::now()
@@ -731,9 +337,19 @@ pub fn get_timestamp() -> f64 {
 mod tests {
     use crate::*;
 
-    fn test_spawn_cosmic_edit_system(mut commands: Commands) {
+    use self::buffer::CosmicBuffer;
+
+    fn test_spawn_cosmic_edit_system(
+        mut commands: Commands,
+        mut font_system: ResMut<CosmicFontSystem>,
+    ) {
+        let attrs = Attrs::new();
         commands.spawn(CosmicEditBundle {
-            text_setter: CosmicText::OneStyle("Blah".into()),
+            buffer: CosmicBuffer::new(&mut font_system, Metrics::new(20., 20.)).with_rich_text(
+                &mut font_system,
+                vec![("Blah", attrs)],
+                attrs,
+            ),
             ..Default::default()
         });
     }
@@ -757,11 +373,9 @@ mod tests {
 
         app.update();
 
-        let mut text_nodes_query = app.world.query::<&CosmicEditor>();
+        let mut text_nodes_query = app.world.query::<&CosmicBuffer>();
         for cosmic_editor in text_nodes_query.iter(&app.world) {
             insta::assert_debug_snapshot!(cosmic_editor
-                .0
-                .buffer()
                 .lines
                 .iter()
                 .map(|line| line.text())
diff --git a/src/render.rs b/src/render.rs
index 8e6fac489455cd3037350961eacf8774c3a3c912..730d8c08d4680f641f6ea2d58eedd6dd8c4c53a0 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -1,151 +1,102 @@
-use std::time::Duration;
-
-use bevy::{
-    prelude::*,
-    render::render_resource::Extent3d,
-    utils::HashMap,
-    window::{PrimaryWindow, WindowScaleFactorChanged},
-};
-use cosmic_text::{Affinity, Edit, Metrics, SwashCache};
+use bevy::{prelude::*, render::render_resource::Extent3d};
+use cosmic_text::{Color, Edit, SwashCache};
 use image::{imageops::FilterType, GenericImageView};
 
 use crate::{
-    get_text_size, get_x_offset_center, get_y_offset_center, CosmicAttrs, CosmicBackground,
-    CosmicEditor, CosmicFontSystem, CosmicMetrics, CosmicMode, CosmicSource, CosmicText,
-    CosmicTextPosition, FillColor, Focus, PasswordInput, PlaceholderAttrs, PlaceholderText,
-    ReadOnly, XOffset, DEFAULT_SCALE_PLACEHOLDER,
+    layout::{CosmicPadding, CosmicWidgetSize},
+    CosmicBackground, CosmicBuffer, CosmicEditor, CosmicFontSystem, CursorColor, DefaultAttrs,
+    FillColor, ReadOnly, SelectionColor, XOffset,
 };
 
-#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
-pub enum CosmicRenderSet {
-    Setup,
-    Shaping,
-    Sizing,
-    Cursor,
-    Padding,
-    Draw,
-}
-
 #[derive(Resource)]
 pub(crate) struct SwashCacheState {
     pub swash_cache: SwashCache,
 }
 
-#[derive(Resource)]
-pub(crate) struct CursorBlinkTimer(pub Timer);
-
-#[derive(Resource)]
-pub(crate) struct CursorVisibility(pub bool);
-
-#[derive(Resource, Default)]
-pub(crate) struct PasswordValues(pub HashMap<Entity, (String, usize)>);
-
-#[derive(Component)]
-pub(crate) struct Placeholder;
-
-#[derive(Component, Default)]
-pub struct CosmicPadding(pub Vec2);
-
-#[derive(Component, Default)]
-pub struct CosmicWidgetSize(pub Vec2);
-
-pub(crate) fn cosmic_padding(
-    mut query: Query<(
-        &mut CosmicPadding,
-        &CosmicTextPosition,
-        &CosmicEditor,
-        &CosmicWidgetSize,
-    )>,
-) {
-    for (mut padding, position, editor, size) in query.iter_mut() {
-        padding.0 = match position {
-            CosmicTextPosition::Center => Vec2::new(
-                get_x_offset_center(size.0.x, editor.0.buffer()) as f32,
-                get_y_offset_center(size.0.y, editor.0.buffer()) as f32,
-            ),
-            CosmicTextPosition::TopLeft { padding } => Vec2::new(*padding as f32, *padding as f32),
-            CosmicTextPosition::Left { padding } => Vec2::new(
-                *padding as f32,
-                get_y_offset_center(size.0.y, editor.0.buffer()) as f32,
-            ),
+pub fn blink_cursor(mut q: Query<&mut CosmicEditor, Without<ReadOnly>>, time: Res<Time>) {
+    for mut e in q.iter_mut() {
+        e.cursor_timer.tick(time.delta());
+        if e.cursor_timer.just_finished() {
+            e.cursor_visible = !e.cursor_visible;
+            e.set_redraw(true);
         }
     }
 }
 
-pub(crate) fn cosmic_widget_size(
-    mut query: Query<(&mut CosmicWidgetSize, &Sprite), Changed<Sprite>>,
-    windows: Query<&Window, With<PrimaryWindow>>,
-) {
-    if windows.iter().len() == 0 {
+fn draw_pixel(buffer: &mut [u8], width: i32, height: i32, x: i32, y: i32, color: Color) {
+    let a_a = color.a() as u32;
+    if a_a == 0 {
+        // Do not draw if alpha is zero
         return;
     }
-    let scale = windows.single().scale_factor();
-    for (mut size, sprite) in query.iter_mut() {
-        size.0 = sprite.custom_size.unwrap().ceil() * scale;
+
+    if y < 0 || y >= height {
+        // Skip if y out of bounds
+        return;
     }
-}
 
-pub(crate) fn cosmic_buffer_size(
-    mut query: Query<(
-        &mut CosmicEditor,
-        &CosmicMode,
-        &CosmicWidgetSize,
-        &CosmicTextPosition,
-    )>,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    for (mut editor, mode, size, position) in query.iter_mut() {
-        let padding_x = match position {
-            CosmicTextPosition::Center => 0.,
-            CosmicTextPosition::TopLeft { padding } => *padding as f32,
-            CosmicTextPosition::Left { padding } => *padding as f32,
-        };
+    if x < 0 || x >= width {
+        // Skip if x out of bounds
+        return;
+    }
 
-        let (buffer_width, buffer_height) = match mode {
-            CosmicMode::InfiniteLine => (f32::MAX, size.0.y),
-            CosmicMode::AutoHeight => (size.0.x - padding_x, (i32::MAX / 2) as f32),
-            CosmicMode::Wrap => (size.0.x - padding_x, size.0.y),
-        };
+    let offset = (y as usize * width as usize + x as usize) * 4;
 
-        editor
-            .0
-            .buffer_mut()
-            .set_size(&mut font_system.0, buffer_width, buffer_height);
-    }
-}
+    let bg = bevy::prelude::Color::rgba_u8(
+        buffer[offset],
+        buffer[offset + 1],
+        buffer[offset + 2],
+        buffer[offset + 3],
+    );
 
-pub(crate) fn cosmic_reshape(
-    mut query: Query<&mut CosmicEditor>,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    for mut cosmic_editor in query.iter_mut() {
-        cosmic_editor.0.shape_as_needed(&mut font_system.0);
-    }
+    // TODO: if alpha is 100% or bg is empty skip blending
+
+    let fg = bevy::prelude::Color::rgba_u8(color.r(), color.g(), color.b(), color.a());
+
+    let premul = fg * Vec3::splat(color.a() as f32 / 255.0);
+
+    let out = premul + bg * (1.0 - fg.a());
+
+    buffer[offset + 2] = (out.b() * 255.0) as u8;
+    buffer[offset + 1] = (out.g() * 255.0) as u8;
+    buffer[offset] = (out.r() * 255.0) as u8;
+    buffer[offset + 3] = (out.a() * 255.0) as u8;
 }
 
 pub(crate) fn render_texture(
     mut query: Query<(
-        &mut CosmicEditor,
-        &CosmicAttrs,
+        Option<&mut CosmicEditor>,
+        &mut CosmicBuffer,
+        &DefaultAttrs,
         &CosmicBackground,
         &FillColor,
+        &CursorColor,
+        &SelectionColor,
         &Handle<Image>,
         &CosmicWidgetSize,
         &CosmicPadding,
         &XOffset,
+        Option<&ReadOnly>,
     )>,
     mut font_system: ResMut<CosmicFontSystem>,
     mut images: ResMut<Assets<Image>>,
     mut swash_cache_state: ResMut<SwashCacheState>,
 ) {
-    for (mut cosmic_editor, attrs, background_image, fill_color, canvas, size, padding, x_offset) in
-        query.iter_mut()
+    for (
+        editor,
+        mut buffer,
+        attrs,
+        background_image,
+        fill_color,
+        cursor_color,
+        selection_color,
+        canvas,
+        size,
+        padding,
+        x_offset,
+        readonly_opt,
+    ) in query.iter_mut()
     {
-        // TODO: redraw tag component
-        if !cosmic_editor.0.buffer().redraw() {
-            continue;
-        }
-
         // Draw background
         let mut pixels = vec![0; size.0.x as usize * size.0.y as usize * 4];
         if let Some(bg_image) = background_image.0.clone() {
@@ -182,476 +133,77 @@ pub(crate) fn render_texture(
             .color_opt
             .unwrap_or(cosmic_text::Color::rgb(0, 0, 0));
 
-        // Draw glyphs
-        cosmic_editor.0.draw(
-            &mut font_system.0,
-            &mut swash_cache_state.swash_cache,
-            font_color,
-            |x, y, w, h, color| {
-                for row in 0..h as i32 {
-                    for col in 0..w as i32 {
-                        draw_pixel(
-                            &mut pixels,
-                            size.0.x as i32,
-                            size.0.y as i32,
-                            x + col + padding.0.x as i32 - x_offset.0.unwrap_or((0., 0.)).0 as i32,
-                            y + row + padding.0.y as i32,
-                            color,
-                        );
-                    }
-                }
-            },
-        );
-
-        if let Some(prev_image) = images.get_mut(canvas) {
-            prev_image.data.clear();
-            prev_image.data.extend_from_slice(pixels.as_slice());
-            prev_image.resize(Extent3d {
-                width: size.0.x as u32,
-                height: size.0.y as u32,
-                depth_or_array_layers: 1,
-            });
-        }
-
-        cosmic_editor.0.buffer_mut().set_redraw(false);
-    }
-}
-
-pub(crate) fn new_image_from_default(
-    mut query: Query<&mut Handle<Image>, Added<CosmicEditor>>,
-    mut images: ResMut<Assets<Image>>,
-) {
-    for mut canvas in query.iter_mut() {
-        *canvas = images.add(Image::default());
-    }
-}
-
-pub(crate) fn set_cursor(
-    mut query: Query<(
-        &mut XOffset,
-        &CosmicMode,
-        &CosmicEditor,
-        &CosmicWidgetSize,
-        &CosmicPadding,
-    )>,
-) {
-    for (mut x_offset, mode, cosmic_editor, size, padding) in query.iter_mut() {
-        let mut cursor_x = 0.;
-        if mode == &CosmicMode::InfiniteLine {
-            if let Some(line) = cosmic_editor.0.buffer().layout_runs().next() {
-                for (idx, glyph) in line.glyphs.iter().enumerate() {
-                    if cosmic_editor.0.cursor().affinity == Affinity::Before {
-                        if idx <= cosmic_editor.0.cursor().index {
-                            cursor_x += glyph.w;
-                        }
-                    } else if idx < cosmic_editor.0.cursor().index {
-                        cursor_x += glyph.w;
-                    } else {
-                        break;
-                    }
-                }
-            }
-        }
-
-        if mode == &CosmicMode::InfiniteLine && x_offset.0.is_none() {
-            *x_offset = XOffset(Some((0., size.0.x - 2. * padding.0.x)));
-        }
-
-        if let Some((x_min, x_max)) = x_offset.0 {
-            if cursor_x > x_max {
-                let diff = cursor_x - x_max;
-                *x_offset = XOffset(Some((x_min + diff, cursor_x)));
-            }
-            if cursor_x < x_min {
-                let diff = x_min - cursor_x;
-                *x_offset = XOffset(Some((cursor_x, x_max - diff)));
-            }
-        }
-    }
-}
-
-pub(crate) fn auto_height(
-    mut query: Query<(
-        Entity,
-        &mut Sprite,
-        &CosmicMode,
-        &mut CosmicEditor,
-        &CosmicWidgetSize,
-    )>,
-    mut style_q: Query<(&mut Style, &CosmicSource)>,
-    windows: Query<&Window, With<PrimaryWindow>>,
-) {
-    if windows.iter().len() == 0 {
-        return;
-    }
-
-    let scale = windows.single().scale_factor();
-
-    for (entity, mut sprite, mode, mut cosmic_editor, size) in query.iter_mut() {
-        if mode == &CosmicMode::AutoHeight {
-            let text_size = get_text_size(cosmic_editor.0.buffer());
-            let text_height = (text_size.1 + 30.) / scale;
-            if text_height > size.0.y / scale {
-                let mut new_size = sprite.custom_size.unwrap();
-                new_size.y = text_height.ceil();
-                // TODO this gets set automatically in UI cases but needs to be done for all other cases.
-                // redundant work but easier to just set on all sprites
-                sprite.custom_size = Some(new_size);
-
-                cosmic_editor.0.buffer_mut().set_redraw(true);
-
-                // TODO: bad loop nesting
-                for (mut style, source) in style_q.iter_mut() {
-                    if source.0 != entity {
-                        continue;
-                    }
-                    style.height = Val::Px(text_height.ceil());
+        let draw_closure = |x, y, w, h, color| {
+            for row in 0..h as i32 {
+                for col in 0..w as i32 {
+                    draw_pixel(
+                        &mut pixels,
+                        size.0.x as i32,
+                        size.0.y as i32,
+                        x + col + padding.0.x as i32 - x_offset.0.unwrap_or((0., 0.)).0 as i32,
+                        y + row + padding.0.y as i32,
+                        color,
+                    );
                 }
             }
-        }
-    }
-}
-
-pub(crate) fn set_size_from_ui(
-    mut source_q: Query<&mut Sprite, With<CosmicEditor>>,
-    dest_q: Query<(&Node, &CosmicSource)>,
-) {
-    for (node, source) in dest_q.iter() {
-        if let Ok(mut sprite) = source_q.get_mut(source.0) {
-            sprite.custom_size = Some(node.size().ceil().max(Vec2::ONE));
-        }
-    }
-}
-
-pub(crate) fn _set_size_from_mesh() {
-    // TODO
-}
-
-fn draw_pixel(
-    buffer: &mut [u8],
-    width: i32,
-    height: i32,
-    x: i32,
-    y: i32,
-    color: cosmic_text::Color,
-) {
-    // TODO: perftest this fn against previous iteration
-    let a_a = color.a() as u32;
-    if a_a == 0 {
-        // Do not draw if alpha is zero
-        return;
-    }
-
-    if y < 0 || y >= height {
-        // Skip if y out of bounds
-        return;
-    }
-
-    if x < 0 || x >= width {
-        // Skip if x out of bounds
-        return;
-    }
-
-    let offset = (y as usize * width as usize + x as usize) * 4;
-
-    let bg = Color::rgba_u8(
-        buffer[offset],
-        buffer[offset + 1],
-        buffer[offset + 2],
-        buffer[offset + 3],
-    );
-
-    // TODO: if alpha is 100% or bg is empty skip blending
-
-    let fg = Color::rgba_u8(color.r(), color.g(), color.b(), color.a());
-
-    let premul = fg * Vec3::splat(color.a() as f32 / 255.0);
-
-    let out = premul + bg * (1.0 - fg.a());
-
-    buffer[offset + 2] = (out.b() * 255.0) as u8;
-    buffer[offset + 1] = (out.g() * 255.0) as u8;
-    buffer[offset] = (out.r() * 255.0) as u8;
-    buffer[offset + 3] = (out.a() * 255.0) as u8;
-}
-
-pub(crate) fn blink_cursor(
-    mut visibility: ResMut<CursorVisibility>,
-    mut timer: ResMut<CursorBlinkTimer>,
-    time: Res<Time>,
-    active_editor: ResMut<Focus>,
-    mut cosmic_editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>,
-) {
-    if let Some(e) = active_editor.0 {
-        timer.0.tick(time.delta());
-        if !timer.0.just_finished() && !active_editor.is_changed() {
-            return;
-        }
-        visibility.0 = !visibility.0;
-
-        // always start cursor visible on focus
-        if active_editor.is_changed() {
-            visibility.0 = true;
-            timer.0.set_elapsed(Duration::ZERO);
-        }
-
-        let new_color = if visibility.0 {
-            None
-        } else {
-            Some(cosmic_text::Color::rgba(0, 0, 0, 0))
         };
 
-        if let Ok(mut editor) = cosmic_editor_q.get_mut(e) {
-            let editor = &mut editor.0;
-            let mut cursor = editor.cursor();
-            cursor.color = new_color;
-            editor.set_cursor(cursor);
-            editor.buffer_mut().set_redraw(true);
-        }
-    }
-}
-
-pub(crate) fn freeze_cursor_blink(
-    mut visibility: ResMut<CursorVisibility>,
-    mut timer: ResMut<CursorBlinkTimer>,
-    active_editor: Res<Focus>,
-    keys: Res<ButtonInput<KeyCode>>,
-    char_evr: EventReader<ReceivedCharacter>,
-    mut editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>,
-) {
-    let inputs = [
-        KeyCode::ArrowLeft,
-        KeyCode::ArrowRight,
-        KeyCode::ArrowUp,
-        KeyCode::ArrowDown,
-        KeyCode::Backspace,
-        KeyCode::Enter,
-    ];
-    if !keys.any_pressed(inputs) && char_evr.is_empty() {
-        return;
-    }
-
-    if let Some(e) = active_editor.0 {
-        if let Ok(mut editor) = editor_q.get_mut(e) {
-            timer.0.set_elapsed(Duration::ZERO);
-            visibility.0 = true;
-            let mut cursor = editor.0.cursor();
-            cursor.color = None;
-            editor.0.set_cursor(cursor);
-            editor.0.buffer_mut().set_redraw(true);
-        }
-    }
-}
-
-pub(crate) fn hide_inactive_or_readonly_cursor(
-    mut cosmic_editor_q_readonly: Query<&mut CosmicEditor, With<ReadOnly>>,
-    mut cosmic_editor_q_editable: Query<(Entity, &mut CosmicEditor), Without<ReadOnly>>,
-    active_editor: Res<Focus>,
-) {
-    for mut editor in &mut cosmic_editor_q_readonly.iter_mut() {
-        let mut cursor = editor.0.cursor();
-        cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
-        editor.0.set_cursor(cursor);
-        editor.0.buffer_mut().set_redraw(true);
-    }
-
-    for (e, mut editor) in &mut cosmic_editor_q_editable.iter_mut() {
-        if active_editor.is_none() || e != active_editor.0.unwrap() {
-            let mut cursor = editor.0.cursor();
-            if cursor.color == Some(cosmic_text::Color::rgba(0, 0, 0, 0)) {
+        // Draw glyphs
+        if let Some(mut editor) = editor {
+            if !editor.redraw() {
                 continue;
             }
-            cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
-            editor.0.set_cursor(cursor);
-            editor.0.buffer_mut().set_redraw(true);
-        }
-    }
-}
-
-pub(crate) fn set_initial_scale(
-    window_q: Query<&Window, With<PrimaryWindow>>,
-    mut cosmic_query: Query<&mut CosmicMetrics, Added<CosmicMetrics>>,
-) {
-    let scale = window_q.single().scale_factor();
-
-    for mut metrics in &mut cosmic_query.iter_mut() {
-        if metrics.scale_factor != DEFAULT_SCALE_PLACEHOLDER {
-            continue;
-        }
-
-        metrics.scale_factor = scale;
-    }
-}
-
-pub(crate) fn on_scale_factor_change(
-    mut scale_factor_changed: EventReader<WindowScaleFactorChanged>,
-    mut cosmic_query: Query<(&mut CosmicEditor, &CosmicMetrics, &mut XOffset)>,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    if !scale_factor_changed.is_empty() {
-        let new_scale_factor = scale_factor_changed.read().last().unwrap().scale_factor as f32;
-        for (mut editor, metrics, mut x_offset) in &mut cosmic_query.iter_mut() {
-            let font_system = &mut font_system.0;
-            let metrics =
-                Metrics::new(metrics.font_size, metrics.line_height).scale(new_scale_factor);
 
-            editor.0.buffer_mut().set_metrics(font_system, metrics);
-            editor.0.buffer_mut().set_redraw(true);
-
-            *x_offset = XOffset(None);
-        }
-    }
-}
+            let cursor_opacity = if editor.cursor_visible && readonly_opt.is_none() {
+                (cursor_color.0.a() * 255.) as u8
+            } else {
+                0
+            };
 
-pub(crate) fn swap_target_handle(
-    source_q: Query<&Handle<Image>, (Changed<Handle<Image>>, With<CosmicEditor>)>,
-    mut dest_q: Query<
-        (
-            Option<&mut Handle<Image>>,
-            Option<&mut UiImage>,
-            &CosmicSource,
-        ),
-        Without<CosmicEditor>,
-    >,
-) {
-    // TODO: do this once
-    for (dest_handle_opt, dest_ui_opt, source_entity) in dest_q.iter_mut() {
-        if let Ok(source_handle) = source_q.get(source_entity.0) {
-            if let Some(mut dest_handle) = dest_handle_opt {
-                *dest_handle = source_handle.clone_weak();
-            }
-            if let Some(mut dest_ui) = dest_ui_opt {
-                dest_ui.texture = source_handle.clone_weak();
-            }
-        }
-    }
-}
+            let cursor_color = Color::rgba(
+                (cursor_color.r() * 255.) as u8,
+                (cursor_color.g() * 255.) as u8,
+                (cursor_color.b() * 255.) as u8,
+                cursor_opacity,
+            );
 
-pub(crate) fn hide_password_text(
-    mut editor_q: Query<(Entity, &mut CosmicEditor, &CosmicAttrs, &PasswordInput)>,
-    mut font_system: ResMut<CosmicFontSystem>,
-    mut password_input_states: ResMut<PasswordValues>,
-    active_editor: Res<Focus>,
-) {
-    for (entity, mut cosmic_editor, attrs, password) in editor_q.iter_mut() {
-        let text = cosmic_editor.get_text();
-        let select_opt = cosmic_editor.0.select_opt();
-        let mut cursor = cosmic_editor.0.cursor();
+            let selection_color = Color::rgba(
+                (selection_color.r() * 255.) as u8,
+                (selection_color.g() * 255.) as u8,
+                (selection_color.b() * 255.) as u8,
+                (selection_color.a() * 255.) as u8,
+            );
 
-        if !text.is_empty() {
-            cosmic_editor.set_text(
-                CosmicText::OneStyle(format!("{}", password.0).repeat(text.chars().count())),
-                attrs.0.clone(),
+            editor.draw(
                 &mut font_system.0,
+                &mut swash_cache_state.swash_cache,
+                font_color,
+                cursor_color,
+                selection_color,
+                draw_closure,
             );
-
-            // multiply cursor idx and select_opt end point by password char length
-            // the actual char length cos '鈼�' is 3x as long as 'a'
-            // This operation will need to be undone when resetting.
-            //
-            // Currently breaks entering multi-byte chars
-
-            let char_len = password.0.len_utf8();
-
-            let select_opt = match select_opt {
-                Some(mut select) => {
-                    select.index *= char_len;
-                    Some(select)
-                }
-                None => None,
-            };
-
-            cursor.index *= char_len;
-
-            cosmic_editor.0.set_select_opt(select_opt);
-
-            // Fixes stuck cursor on password inputs
-            if let Some(active) = active_editor.0 {
-                if entity != active {
-                    cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
-                }
-            }
-
-            cosmic_editor.0.set_cursor(cursor);
-        }
-
-        let glyph_idx = match cosmic_editor.0.buffer().lines[0].layout_opt() {
-            Some(_) => cosmic_editor.0.buffer().layout_cursor(&cursor).glyph,
-            None => 0,
-        };
-
-        password_input_states.0.insert(entity, (text, glyph_idx));
-    }
-}
-
-pub(crate) fn restore_password_text(
-    mut editor_q: Query<(Entity, &mut CosmicEditor, &CosmicAttrs, &PasswordInput)>,
-    mut font_system: ResMut<CosmicFontSystem>,
-    password_input_states: Res<PasswordValues>,
-) {
-    for (entity, mut cosmic_editor, attrs, password) in editor_q.iter_mut() {
-        if let Some((text, _glyph_idx)) = password_input_states.0.get(&entity) {
-            if !text.is_empty() {
-                let char_len = password.0.len_utf8();
-
-                let mut cursor = cosmic_editor.0.cursor();
-                let select_opt = match cosmic_editor.0.select_opt() {
-                    Some(mut select) => {
-                        select.index /= char_len;
-                        Some(select)
-                    }
-                    None => None,
-                };
-
-                cursor.index /= char_len;
-
-                cosmic_editor.set_text(
-                    crate::CosmicText::OneStyle(text.clone()),
-                    attrs.0.clone(),
-                    &mut font_system.0,
-                );
-
-                cosmic_editor.0.set_select_opt(select_opt);
-                cosmic_editor.0.set_cursor(cursor);
+            editor.set_redraw(false);
+        } else {
+            if !buffer.redraw() {
+                continue;
             }
+            buffer.draw(
+                &mut font_system.0,
+                &mut swash_cache_state.swash_cache,
+                font_color,
+                draw_closure,
+            );
+            buffer.set_redraw(false);
         }
-    }
-}
-
-pub(crate) fn show_placeholder(
-    mut editor_q: Query<(
-        Entity,
-        &mut CosmicEditor,
-        &PlaceholderText,
-        &PlaceholderAttrs,
-    )>,
-    mut font_system: ResMut<CosmicFontSystem>,
-    mut commands: Commands,
-) {
-    for (entity, mut cosmic_editor, placeholder, attrs) in editor_q.iter_mut() {
-        if cosmic_editor.get_text().is_empty() {
-            cosmic_editor.set_text(placeholder.0.clone(), attrs.0.clone(), &mut font_system.0);
-
-            let mut cursor = cosmic_editor.0.cursor();
-            cursor.index = 0;
-            cosmic_editor.0.set_cursor(cursor);
 
-            commands.entity(entity).insert(Placeholder);
-        } else {
-            commands.entity(entity).remove::<Placeholder>();
+        if let Some(prev_image) = images.get_mut(canvas) {
+            prev_image.data.clear();
+            prev_image.data.extend_from_slice(pixels.as_slice());
+            prev_image.resize(Extent3d {
+                width: size.0.x as u32,
+                height: size.0.y as u32,
+                depth_or_array_layers: 1,
+            });
         }
     }
 }
-
-pub(crate) fn restore_placeholder_text(
-    mut editor_q: Query<(&mut CosmicEditor, &CosmicAttrs), With<Placeholder>>,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    for (mut cosmic_editor, attrs) in editor_q.iter_mut() {
-        cosmic_editor.set_text(
-            CosmicText::OneStyle("".into()),
-            attrs.0.clone(),
-            &mut font_system.0,
-        );
-    }
-}
diff --git a/util/Cargo.toml b/util/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..51de83365dfe6af657fa3258f3c808e5a1af1b7c
--- /dev/null
+++ b/util/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "util"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+bevy = { version = "0.13", default-features = false, features = [
+  "bevy_asset",
+  "bevy_core_pipeline",
+  "bevy_render",
+  "bevy_scene",
+  "bevy_sprite",
+  "bevy_text",
+  "bevy_ui",
+  "bevy_winit",
+  "png",
+  "x11",
+  "webgpu",
+] }
+bevy_cosmic_edit = { version = "*", path = "../" }
diff --git a/util/src/lib.rs b/util/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..405152cd2ac7aaa293d14eb85d459cc88788238a
--- /dev/null
+++ b/util/src/lib.rs
@@ -0,0 +1,84 @@
+// Common functions for examples
+use bevy::{prelude::*, window::PrimaryWindow};
+use bevy_cosmic_edit::*;
+
+pub fn deselect_editor_on_esc(i: Res<ButtonInput<KeyCode>>, mut focus: ResMut<FocusedWidget>) {
+    if i.just_pressed(KeyCode::Escape) {
+        focus.0 = None;
+    }
+}
+
+pub fn change_active_editor_sprite(
+    mut commands: Commands,
+    windows: Query<&Window, With<PrimaryWindow>>,
+    buttons: Res<ButtonInput<MouseButton>>,
+    mut cosmic_edit_query: Query<
+        (&mut Sprite, &GlobalTransform, &Visibility, Entity),
+        (With<CosmicBuffer>, Without<ReadOnly>),
+    >,
+    camera_q: Query<(&Camera, &GlobalTransform)>,
+) {
+    let window = windows.single();
+    let (camera, camera_transform) = camera_q.single();
+    if buttons.just_pressed(MouseButton::Left) {
+        for (sprite, node_transform, visibility, entity) in &mut cosmic_edit_query.iter_mut() {
+            if visibility == Visibility::Hidden {
+                continue;
+            }
+            let size = sprite.custom_size.unwrap_or(Vec2::ONE);
+            let x_min = node_transform.affine().translation.x - size.x / 2.;
+            let y_min = node_transform.affine().translation.y - size.y / 2.;
+            let x_max = node_transform.affine().translation.x + size.x / 2.;
+            let y_max = node_transform.affine().translation.y + size.y / 2.;
+            if let Some(pos) = window.cursor_position() {
+                if let Some(pos) = camera.viewport_to_world_2d(camera_transform, pos) {
+                    if x_min < pos.x && pos.x < x_max && y_min < pos.y && pos.y < y_max {
+                        commands.insert_resource(FocusedWidget(Some(entity)))
+                    };
+                }
+            };
+        }
+    }
+}
+
+pub fn change_active_editor_ui(
+    mut commands: Commands,
+    mut interaction_query: Query<
+        (&Interaction, &CosmicSource),
+        (Changed<Interaction>, Without<ReadOnly>),
+    >,
+) {
+    for (interaction, source) in interaction_query.iter_mut() {
+        if let Interaction::Pressed = interaction {
+            commands.insert_resource(FocusedWidget(Some(source.0)));
+        }
+    }
+}
+
+pub fn print_editor_text(
+    text_inputs_q: Query<&CosmicEditor>,
+    mut previous_value: Local<Vec<String>>,
+) {
+    for text_input in text_inputs_q.iter() {
+        let current_text: Vec<String> = text_input.with_buffer(|buf| {
+            buf.lines
+                .iter()
+                .map(|bl| bl.text().to_string())
+                .collect::<Vec<_>>()
+        });
+        if current_text == *previous_value {
+            return;
+        }
+        *previous_value = current_text.clone();
+        info!("Widget text: {:?}", current_text);
+    }
+}
+
+pub fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor {
+    CosmicColor::rgba(
+        (color.r() * 255.) as u8,
+        (color.g() * 255.) as u8,
+        (color.b() * 255.) as u8,
+        (color.a() * 255.) as u8,
+    )
+}