From e935320718dffb27c0ecaf63718e0f8f1696d853 Mon Sep 17 00:00:00 2001
From: Caleb Yates <calebyates42@gmail.com>
Date: Fri, 13 Dec 2024 00:46:13 +1000
Subject: [PATCH] bevy::picking integration and refactoring (#167)

* wip: seperating cursor event handling
preparing to directly use bevy::picking instead

* mark: removed all padding and offset from render code

* bug: not rendering until focussed?

* feat: text now follows horizontal alignment

* add: example basic_sprite_editor

* feat: now centers vertically as well

* todo: get proper clicking input to work
Translating from widget to buffer coord space is annoying ngl

* feat: top_padding works correctly with click input

* refactor: moved output under render_implemetations module

* bug: for some reason, renders the same image to all editors?

* fix: editors render properly now

* mark: shift key supported in sprites

* feat: all click funcionatlity for sprites work!

* feat: primary input uses bevy::picking

* refactor: moved `RelativeCoord` into render_implementations/coords.rs

* refactor: moved `CosmicWidgetSize` into render_implementations/widget_size.rs

* refactor: Seperating input.rs into submodules

* wip: refactoring input into drag

* todo: finish hover impl

* todo: implement proper hovering

* feat: cursor hovering is well behaved!

* mark: doesn't deselect on dragend

* add: input/cursor_visbility.rs and various module refactors

* feat: focus_on_click observer

* mark: doctests all pass

* refactor: scan.rs in render_implementations and removed editor examples

* fmt

* fix: compiles on wasm now

* fix: ui clicking works now

* todo: use new EditorBuffer API

* todo: finish refactor into editor_buffer.rs module

* todo: refactor all code to use EditorBuffer

* feat: all compiles!

* todo: fix bug where cursor doesn't show on empty editors

* fix: cursor blinks in empty wdigets now

* add: veritcal scrolling only kicks in with canvas larger than render target

* fix: infinite line works

* chore: added CosmicWrap::InfiniteLine where removed before because of pancis

* chore: minor privacy restrictions

* fix: no warnings in examples
---
 .gitignore                                |   3 +-
 Cargo.lock                                |  37 ++
 Cargo.toml                                |   8 +-
 examples/basic_sprite.rs                  |  44 +-
 examples/basic_ui.rs                      |  10 +-
 examples/bevy_editor_pls.rs               |  73 ---
 examples/every_option.rs                  |  90 +--
 examples/font_per_widget.rs               |  63 +-
 examples/image_background.rs              |  36 +-
 examples/multiple_sprites.rs              |  44 +-
 examples/password.rs                      |  38 +-
 examples/placeholder.rs                   |  47 +-
 examples/readonly.rs                      |  34 +-
 examples/scroll.rs                        |  69 ++
 examples/sprite_and_ui_clickable.rs       |  78 +--
 src/cosmic_edit.rs                        | 163 +++--
 src/cursor.rs                             | 107 ---
 src/debug.rs                              |  45 ++
 src/double_click.rs                       |  91 +++
 src/editor_buffer.rs                      | 172 +++++
 src/{ => editor_buffer}/buffer.rs         | 272 ++++----
 src/editor_buffer/editor.rs               |  62 ++
 src/events.rs                             |  20 -
 src/focus.rs                              |  61 +-
 src/input.rs                              | 753 +++-------------------
 src/input/click.rs                        | 144 +++++
 src/input/clipboard.rs                    | 182 ++++++
 src/input/cursor_icon.rs                  |  96 +++
 src/input/cursor_visibility.rs            |  48 ++
 src/input/drag.rs                         | 148 +++++
 src/input/hover.rs                        | 106 +++
 src/input/keyboard.rs                     | 293 +++++++++
 src/input/scroll.rs                       |  35 +
 src/lib.rs                                |  92 +--
 src/password.rs                           | 121 ++--
 src/placeholder.rs                        |   7 +-
 src/primary.rs                            |  70 +-
 src/render.rs                             | 233 +++++--
 src/render_implementations.rs             |  97 +++
 src/render_implementations/coords.rs      |  94 +++
 src/render_implementations/output.rs      |  46 ++
 src/render_implementations/scan.rs        |  43 ++
 src/render_implementations/widget_size.rs |  71 ++
 src/render_targets.rs                     | 242 -------
 src/utils.rs                              |  60 +-
 src/widget.rs                             | 215 ------
 46 files changed, 2826 insertions(+), 2037 deletions(-)
 delete mode 100644 examples/bevy_editor_pls.rs
 create mode 100644 examples/scroll.rs
 delete mode 100644 src/cursor.rs
 create mode 100644 src/debug.rs
 create mode 100644 src/double_click.rs
 create mode 100644 src/editor_buffer.rs
 rename src/{ => editor_buffer}/buffer.rs (52%)
 create mode 100644 src/editor_buffer/editor.rs
 delete mode 100644 src/events.rs
 create mode 100644 src/input/click.rs
 create mode 100644 src/input/clipboard.rs
 create mode 100644 src/input/cursor_icon.rs
 create mode 100644 src/input/cursor_visibility.rs
 create mode 100644 src/input/drag.rs
 create mode 100644 src/input/hover.rs
 create mode 100644 src/input/keyboard.rs
 create mode 100644 src/input/scroll.rs
 create mode 100644 src/render_implementations.rs
 create mode 100644 src/render_implementations/coords.rs
 create mode 100644 src/render_implementations/output.rs
 create mode 100644 src/render_implementations/scan.rs
 create mode 100644 src/render_implementations/widget_size.rs
 delete mode 100644 src/render_targets.rs
 delete mode 100644 src/widget.rs

diff --git a/.gitignore b/.gitignore
index 1a20a17..e58467c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,5 @@
 velo.json
 ichart.json
 data
-out
\ No newline at end of file
+out
+env.nu
diff --git a/Cargo.lock b/Cargo.lock
index 134884d..e5277dd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -505,6 +505,9 @@ dependencies = [
  "image",
  "insta",
  "js-sys",
+ "num",
+ "num-derive",
+ "num-traits",
  "sys-locale",
  "unicode-segmentation",
  "wasm-bindgen",
@@ -2673,6 +2676,20 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
 [[package]]
 name = "num-bigint"
 version = "0.4.6"
@@ -2683,6 +2700,15 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "num-derive"
 version = "0.4.2"
@@ -2703,6 +2729,17 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "num-rational"
 version = "0.4.2"
diff --git a/Cargo.toml b/Cargo.toml
index 0ee3d7d..f9f9d34 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,9 +11,8 @@ keywords = ["bevy"]
 exclude = ["assets/*"]
 
 [features]
-## Enable to avoid panicing when multiple cameras are used in the same world.
-## Requires you to add `CosmicPrimaryCamera` marker component to the primary camera
-multicam = []
+## For internal use only
+internal-debugging = ["bevy/track_change_detection"]
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
@@ -38,6 +37,9 @@ crossbeam-channel = "0.5.8"
 image = "0.25.1"
 sys-locale = "0.3.0"
 document-features = "0.2.8"
+num = "0.4.3"
+num-derive = "0.4.2"
+num-traits = "0.2.19"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 arboard = "3.2.0"
diff --git a/examples/basic_sprite.rs b/examples/basic_sprite.rs
index 99594ba..30e8eb3 100644
--- a/examples/basic_sprite.rs
+++ b/examples/basic_sprite.rs
@@ -12,7 +12,6 @@ fn setup(
     let primary_window = windows.single();
     let camera_bundle = (
         Camera2d,
-        CosmicPrimaryCamera,
         Camera {
             clear_color: ClearColorConfig::Custom(Color::WHITE),
             ..default()
@@ -24,22 +23,23 @@ fn setup(
     attrs = attrs.family(Family::Name("Victor Mono"));
     attrs = attrs.color(CosmicColor::rgb(0x94, 0x00, 0xD3));
 
-    let cosmic_edit = (
-        CosmicEditBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text(
-            &mut font_system,
-            "πŸ˜€πŸ˜€πŸ˜€ x => y",
-            attrs,
-        ),
-        TextEdit2d,
-        Sprite {
-            // You must specify custom size
-            // so the editor knows what size images to render to the sprite
-            custom_size: Some(Vec2::new(primary_window.width(), primary_window.height())),
-            ..default()
-        },
-    );
-
-    let cosmic_edit = commands.spawn(cosmic_edit).id();
+    let cosmic_edit = commands
+        .spawn((
+            CosmicEditBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text(
+                &mut font_system,
+                "πŸ˜€πŸ˜€πŸ˜€ x => y",
+                attrs,
+            ),
+            TextEdit2d,
+            Sprite {
+                // You must specify custom size
+                // so the editor knows what size images to render to the sprite
+                custom_size: Some(Vec2::new(primary_window.width(), primary_window.height())),
+                ..default()
+            },
+        ))
+        .observe(focus_on_click)
+        .id();
 
     commands.insert_resource(FocusedWidget(Some(cosmic_edit)));
 }
@@ -54,16 +54,8 @@ fn main() {
 
     App::new()
         .add_plugins(DefaultPlugins)
-        .add_plugins(bevy_editor_pls::EditorPlugin::default())
         .add_plugins(CosmicEditPlugin { font_config })
         .add_systems(Startup, setup)
-        .add_systems(
-            Update,
-            (
-                print_editor_text,
-                change_active_editor_sprite,
-                deselect_editor_on_esc,
-            ),
-        )
+        .add_systems(Update, (print_editor_text, deselect_editor_on_esc))
         .run();
 }
diff --git a/examples/basic_ui.rs b/examples/basic_ui.rs
index ccb4625..43324ab 100644
--- a/examples/basic_ui.rs
+++ b/examples/basic_ui.rs
@@ -32,6 +32,7 @@ fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
                 ..default()
             },
         ))
+        .observe(focus_on_click)
         .id();
 
     commands.insert_resource(FocusedWidget(Some(cosmic_edit)));
@@ -49,13 +50,6 @@ fn main() {
         .add_plugins(DefaultPlugins)
         .add_plugins(CosmicEditPlugin { font_config })
         .add_systems(Startup, setup)
-        .add_systems(
-            Update,
-            (
-                print_editor_text,
-                change_active_editor_ui,
-                deselect_editor_on_esc,
-            ),
-        )
+        .add_systems(Update, (print_editor_text, deselect_editor_on_esc))
         .run();
 }
diff --git a/examples/bevy_editor_pls.rs b/examples/bevy_editor_pls.rs
deleted file mode 100644
index 5f8f573..0000000
--- a/examples/bevy_editor_pls.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-//! With [bevy_editor_pls] integration
-//! Requires `multicam` features enabled
-
-use bevy::prelude::*;
-use bevy_cosmic_edit::{
-    cosmic_text::{Attrs, Family, Metrics},
-    prelude::*,
-};
-
-fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
-    let camera_bundle = (
-        Camera2d,
-        // marker from bevy_cosmic_edit
-        bevy_cosmic_edit::CosmicPrimaryCamera,
-        // marker from bevy
-        // required else for some reason no UI renders to the screen
-        bevy::prelude::IsDefaultUiCamera,
-        Camera {
-            clear_color: ClearColorConfig::Custom(bevy::color::palettes::css::PINK.into()),
-            ..default()
-        },
-    );
-    commands.spawn(camera_bundle);
-
-    let mut attrs = Attrs::new();
-    attrs = attrs.family(Family::Name("Victor Mono"));
-    attrs = attrs.color(CosmicColor::rgb(0x94, 0x00, 0xD3));
-
-    let cosmic_edit = commands
-        .spawn((
-            TextEdit,
-            CosmicEditBuffer::new(&mut font_system, Metrics::new(40., 40.)).with_rich_text(
-                &mut font_system,
-                vec![("Banana", attrs)],
-                attrs,
-            ),
-            Node {
-                width: Val::Percent(60.),
-                height: Val::Percent(60.),
-                top: Val::Percent(10.),
-                left: Val::Percent(30.),
-                ..default()
-            },
-        ))
-        .id();
-
-    commands.insert_resource(FocusedWidget(Some(cosmic_edit)));
-}
-
-fn main() {
-    let font_bytes: &[u8] = include_bytes!("../assets/fonts/VictorMono-Regular.ttf");
-    let font_config = CosmicFontConfig {
-        fonts_dir_path: None,
-        font_bytes: Some(vec![font_bytes]),
-        load_system_fonts: true,
-    };
-
-    App::new()
-        .add_plugins(DefaultPlugins)
-        .add_plugins(CosmicEditPlugin { font_config })
-        // add editor plugin
-        .add_plugins(bevy_editor_pls::EditorPlugin::default())
-        .add_systems(Startup, setup)
-        .add_systems(
-            Update,
-            (
-                print_editor_text,
-                change_active_editor_ui,
-                deselect_editor_on_esc,
-            ),
-        )
-        .run();
-}
diff --git a/examples/every_option.rs b/examples/every_option.rs
index 8f88213..1e57390 100644
--- a/examples/every_option.rs
+++ b/examples/every_option.rs
@@ -3,7 +3,8 @@ use bevy_cosmic_edit::{
     cosmic_text::{Attrs, AttrsOwned, Metrics},
     prelude::*,
     CosmicBackgroundColor, CosmicBackgroundImage, CosmicTextAlign, CosmicWrap, CursorColor,
-    DefaultAttrs, HoverCursor, MaxChars, MaxLines, SelectedTextColor, SelectionColor,
+    DefaultAttrs, HorizontalAlign, HoverCursor, MaxChars, MaxLines, SelectedTextColor,
+    SelectionColor, VerticalAlign,
 };
 
 #[derive(Resource)]
@@ -14,47 +15,52 @@ fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
 
     let attrs = Attrs::new().color(Color::srgb(0.27, 0.27, 0.27).to_cosmic());
 
-    commands.spawn((
-        (
-            // cosmic edit components
-            CosmicEditBuffer::new(&mut font_system, Metrics::new(16., 16.)).with_text(
-                &mut font_system,
-                "Begin counting.",
-                attrs,
+    commands
+        .spawn((
+            (
+                // cosmic edit components
+                CosmicEditBuffer::new(&mut font_system, Metrics::new(16., 16.)).with_text(
+                    &mut font_system,
+                    "Begin counting.",
+                    attrs,
+                ),
+                CursorColor(bevy::color::palettes::css::LIME.into()),
+                SelectionColor(bevy::color::palettes::css::DEEP_PINK.into()),
+                CosmicBackgroundColor(bevy::color::palettes::css::YELLOW_GREEN.into()),
+                CosmicTextAlign {
+                    horizontal: Some(HorizontalAlign::Center),
+                    vertical: VerticalAlign::Center,
+                },
+                CosmicBackgroundImage(None),
+                DefaultAttrs(AttrsOwned::new(attrs)),
+                MaxChars(15),
+                MaxLines(1),
+                CosmicWrap::Wrap,
+                HoverCursor(CursorIcon::System(SystemCursorIcon::Pointer)),
+                SelectedTextColor(Color::WHITE),
             ),
-            CursorColor(bevy::color::palettes::css::LIME.into()),
-            SelectionColor(bevy::color::palettes::css::DEEP_PINK.into()),
-            CosmicBackgroundColor(bevy::color::palettes::css::YELLOW_GREEN.into()),
-            CosmicTextAlign::Center { padding: 0 },
-            CosmicBackgroundImage(None),
-            DefaultAttrs(AttrsOwned::new(attrs)),
-            MaxChars(15),
-            MaxLines(1),
-            CosmicWrap::Wrap,
-            HoverCursor(CursorIcon::System(SystemCursorIcon::Pointer)),
-            SelectedTextColor(Color::WHITE),
-        ),
-        (
-            TextEdit,
-            // the image mode is optional, but due to bevy 0.15 mechanics is required to
-            // render the border within the `ImageNode`
-            // See bevy issue https://github.com/bevyengine/bevy/issues/16643#issuecomment-2518163688
-            ImageNode::default().with_mode(bevy::ui::widget::NodeImageMode::Stretch),
-            Node {
-                // Size and position of text box
-                border: UiRect::all(Val::Px(4.)),
-                width: Val::Percent(20.),
-                height: Val::Px(50.),
-                left: Val::Percent(40.),
-                top: Val::Px(100.),
-                ..default()
-            },
-            BorderColor(bevy::color::palettes::css::LIMEGREEN.into()),
-            BorderRadius::all(Val::Px(10.)),
-            // This is overriden by setting `CosmicBackgroundColor` so you don't see any white
-            BackgroundColor(Color::WHITE),
-        ),
-    ));
+            (
+                TextEdit,
+                // the image mode is optional, but due to bevy 0.15 mechanics is required to
+                // render the border within the `ImageNode`
+                // See bevy issue https://github.com/bevyengine/bevy/issues/16643#issuecomment-2518163688
+                ImageNode::default().with_mode(bevy::ui::widget::NodeImageMode::Stretch),
+                Node {
+                    // Size and position of text box
+                    border: UiRect::all(Val::Px(4.)),
+                    width: Val::Percent(20.),
+                    height: Val::Px(50.),
+                    left: Val::Percent(40.),
+                    top: Val::Px(100.),
+                    ..default()
+                },
+                BorderColor(bevy::color::palettes::css::LIMEGREEN.into()),
+                BorderRadius::all(Val::Px(10.)),
+                // This is overriden by setting `CosmicBackgroundColor` so you don't see any white
+                BackgroundColor(Color::WHITE),
+            ),
+        ))
+        .observe(focus_on_click);
 
     commands.insert_resource(TextChangeTimer(Timer::from_seconds(
         1.,
@@ -91,6 +97,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))
+        .add_systems(Update, deselect_editor_on_esc)
         .run();
 }
diff --git a/examples/font_per_widget.rs b/examples/font_per_widget.rs
index caf65fd..8b08a47 100644
--- a/examples/font_per_widget.rs
+++ b/examples/font_per_widget.rs
@@ -157,41 +157,44 @@ fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
     ];
 
     commands.entity(root).with_children(|parent| {
-        parent.spawn((
-            CosmicEditBuffer::new(&mut font_system, Metrics::new(18., 22.)).with_rich_text(
-                &mut font_system,
-                lines,
-                attrs,
-            ),
-            TextEdit,
-            Node {
-                width: Val::Percent(50.),
-                height: Val::Percent(100.),
-                ..default()
-            },
-            BackgroundColor(Color::WHITE),
-        ));
+        parent
+            .spawn((
+                TextEdit,
+                CosmicEditBuffer::new(&mut font_system, Metrics::new(18., 22.)).with_rich_text(
+                    &mut font_system,
+                    lines,
+                    attrs,
+                ),
+                Node {
+                    width: Val::Percent(50.),
+                    height: Val::Percent(100.),
+                    ..default()
+                },
+                BackgroundColor(Color::WHITE),
+            ))
+            .observe(focus_on_click);
     });
 
     let mut attrs_2 = Attrs::new();
     attrs_2 = attrs_2.family(Family::Name("Times New Roman"));
     attrs_2.color_opt = Some(bevy::color::palettes::css::PURPLE.to_cosmic());
     commands.entity(root).with_children(|parent| {
-        parent.spawn((
-            CosmicEditBuffer::new(&mut font_system, Metrics::new(28., 36.)).with_text(
-                &mut font_system,
-                "Widget 2.\nClick on me =>",
-                attrs_2,
-            ),
-            ImageNode::default(),
-            Button,
-            Node {
-                width: Val::Percent(50.),
-                height: Val::Percent(100.),
-                ..default()
-            },
-            BackgroundColor(Color::WHITE.with_alpha(0.8)),
-        ));
+        parent
+            .spawn((
+                TextEdit,
+                CosmicEditBuffer::new(&mut font_system, Metrics::new(28., 36.)).with_text(
+                    &mut font_system,
+                    "Widget 2.\nClick on me =>",
+                    attrs_2,
+                ),
+                Node {
+                    width: Val::Percent(50.),
+                    height: Val::Percent(100.),
+                    ..default()
+                },
+                BackgroundColor(Color::WHITE.with_alpha(0.8)),
+            ))
+            .observe(focus_on_click);
     });
 }
 
@@ -200,6 +203,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))
+        .add_systems(Update, deselect_editor_on_esc)
         .run();
 }
diff --git a/examples/image_background.rs b/examples/image_background.rs
index 19ab10b..ef5fa86 100644
--- a/examples/image_background.rs
+++ b/examples/image_background.rs
@@ -10,22 +10,24 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
 
     let bg_image_handle = asset_server.load("img/bevy_logo_light.png");
 
-    commands.spawn((
-        TextEdit,
-        CosmicEditBuffer::default(),
-        DefaultAttrs(AttrsOwned::new(
-            Attrs::new().color(bevy::color::palettes::basic::LIME.to_cosmic()),
-        )),
-        CosmicBackgroundImage(Some(bg_image_handle)),
-        Node {
-            // Size and position of text box
-            width: Val::Px(300.),
-            height: Val::Px(50.),
-            left: Val::Px(100.),
-            top: Val::Px(100.),
-            ..default()
-        },
-    ));
+    commands
+        .spawn((
+            TextEdit,
+            CosmicEditBuffer::default(),
+            DefaultAttrs(AttrsOwned::new(
+                Attrs::new().color(bevy::color::palettes::basic::LIME.to_cosmic()),
+            )),
+            CosmicBackgroundImage(Some(bg_image_handle)),
+            Node {
+                // Size and position of text box
+                width: Val::Px(300.),
+                height: Val::Px(50.),
+                left: Val::Px(100.),
+                top: Val::Px(100.),
+                ..default()
+            },
+        ))
+        .observe(focus_on_click);
 }
 
 fn main() {
@@ -33,6 +35,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))
+        .add_systems(Update, deselect_editor_on_esc)
         .run();
 }
diff --git a/examples/multiple_sprites.rs b/examples/multiple_sprites.rs
index e787e3c..b5174a6 100644
--- a/examples/multiple_sprites.rs
+++ b/examples/multiple_sprites.rs
@@ -42,26 +42,29 @@ fn setup(
         Transform::from_translation(Vec3::new(-primary_window.width() / 4., 0., 1.)),
     ));
 
-    commands.spawn((
-        CosmicEditBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text(
-            &mut font_system,
-            "Widget_2. Click on me",
-            attrs,
-        ),
-        CosmicBackgroundColor(bevy::color::palettes::basic::GRAY.with_alpha(0.5).into()),
-        Sprite {
-            custom_size: Some(Vec2 {
-                x: primary_window.width() / 2.,
-                y: primary_window.height() / 2.,
-            }),
-            ..default()
-        },
-        Transform::from_translation(Vec3::new(
-            primary_window.width() / 4.,
-            -primary_window.height() / 4.,
-            1.,
-        )),
-    ));
+    commands
+        .spawn((
+            TextEdit2d,
+            CosmicEditBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text(
+                &mut font_system,
+                "Widget_2. Click on me",
+                attrs,
+            ),
+            CosmicBackgroundColor(bevy::color::palettes::basic::GRAY.with_alpha(0.5).into()),
+            Sprite {
+                custom_size: Some(Vec2 {
+                    x: primary_window.width() / 2.,
+                    y: primary_window.height() / 2.,
+                }),
+                ..default()
+            },
+            Transform::from_translation(Vec3::new(
+                primary_window.width() / 4.,
+                -primary_window.height() / 4.,
+                1.,
+            )),
+        ))
+        .observe(focus_on_click);
 }
 
 fn main() {
@@ -76,6 +79,5 @@ fn main() {
         .add_plugins(DefaultPlugins)
         .add_plugins(CosmicEditPlugin { font_config })
         .add_systems(Startup, setup)
-        .add_systems(Update, change_active_editor_sprite)
         .run();
 }
diff --git a/examples/password.rs b/examples/password.rs
index e13af7e..90c2b62 100644
--- a/examples/password.rs
+++ b/examples/password.rs
@@ -1,27 +1,30 @@
 use bevy::prelude::*;
 use bevy_cosmic_edit::{
-    cosmic_text::Attrs, prelude::*, CosmicWrap, InputSet, MaxLines, Password, Placeholder,
+    cosmic_text::Attrs, password::Password, placeholder::Placeholder, prelude::*, CosmicWrap,
+    MaxLines,
 };
 
 fn setup(mut commands: Commands) {
     commands.spawn(Camera2d);
 
     // Sprite editor
-    commands.spawn((
-        TextEdit2d,
-        CosmicEditBuffer::default(),
-        MaxLines(1),
-        CosmicWrap::InfiniteLine,
-        // Sets size of text box
-        Sprite {
-            custom_size: Some(Vec2::new(300., 100.)),
-            ..default()
-        },
-        // Position of text box
-        Transform::from_xyz(0., 100., 0.),
-        Password::default(),
-        Placeholder::new("Password", Attrs::new()),
-    ));
+    commands
+        .spawn((
+            TextEdit2d,
+            CosmicEditBuffer::default(),
+            MaxLines(1),
+            CosmicWrap::InfiniteLine,
+            // Sets size of text box
+            Sprite {
+                custom_size: Some(Vec2::new(300., 100.)),
+                ..default()
+            },
+            // Position of text box
+            Transform::from_xyz(0., 100., 0.),
+            Password::default(),
+            Placeholder::new("Password", Attrs::new()),
+        ))
+        .observe(focus_on_click);
 }
 
 fn main() {
@@ -32,10 +35,9 @@ fn main() {
         .add_systems(
             Update,
             (
-                change_active_editor_sprite,
                 deselect_editor_on_esc,
                 // If you don't .after(InputSet) you'll just see the hashed-out safe text
-                print_editor_text.after(InputSet),
+                print_editor_text.after(bevy_cosmic_edit::input::InputSet),
             ),
         )
         .run();
diff --git a/examples/placeholder.rs b/examples/placeholder.rs
index 1a09fbf..4509566 100644
--- a/examples/placeholder.rs
+++ b/examples/placeholder.rs
@@ -1,8 +1,8 @@
 use bevy::prelude::*;
 use bevy_cosmic_edit::{
     cosmic_text::{Attrs, Family, Metrics},
+    placeholder::Placeholder,
     prelude::*,
-    Placeholder,
 };
 
 fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
@@ -19,23 +19,25 @@ fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
     attrs = attrs.family(Family::Name("Victor Mono"));
     attrs = attrs.color(CosmicColor::rgb(0x94, 0x00, 0xD3));
 
-    commands.spawn((
-        TextEdit,
-        CosmicEditBuffer::new(&mut font_system, Metrics::new(20., 20.)).with_rich_text(
-            &mut font_system,
-            vec![("", attrs)],
-            attrs,
-        ),
-        Placeholder::new(
-            "Placeholder",
-            attrs.color(bevy::color::palettes::basic::GRAY.to_cosmic()),
-        ),
-        Node {
-            width: Val::Percent(100.),
-            height: Val::Percent(100.),
-            ..default()
-        },
-    ));
+    commands
+        .spawn((
+            TextEdit,
+            CosmicEditBuffer::new(&mut font_system, Metrics::new(20., 20.)).with_rich_text(
+                &mut font_system,
+                vec![("", attrs)],
+                attrs,
+            ),
+            Placeholder::new(
+                "Placeholder",
+                attrs.color(bevy::color::palettes::basic::GRAY.to_cosmic()),
+            ),
+            Node {
+                width: Val::Percent(100.),
+                height: Val::Percent(100.),
+                ..default()
+            },
+        ))
+        .observe(focus_on_click);
 }
 
 fn main() {
@@ -50,13 +52,6 @@ fn main() {
         .add_plugins(DefaultPlugins)
         .add_plugins(CosmicEditPlugin { font_config })
         .add_systems(Startup, setup)
-        .add_systems(
-            Update,
-            (
-                print_editor_text,
-                change_active_editor_ui,
-                deselect_editor_on_esc,
-            ),
-        )
+        .add_systems(Update, (print_editor_text, deselect_editor_on_esc))
         .run();
 }
diff --git a/examples/readonly.rs b/examples/readonly.rs
index dd04535..1c9a5c2 100644
--- a/examples/readonly.rs
+++ b/examples/readonly.rs
@@ -12,21 +12,23 @@ fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
     attrs = attrs.color(bevy::color::palettes::basic::PURPLE.to_cosmic());
 
     // spawn editor
-    commands.spawn((
-        TextEdit,
-        ReadOnly,
-        CosmicEditBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text(
-            &mut font_system,
-            "πŸ˜€πŸ˜€πŸ˜€ x => y\nRead only widget",
-            attrs,
-        ),
-        Node {
-            width: Val::Percent(100.),
-            height: Val::Percent(100.),
-            ..default()
-        },
-        BackgroundColor(Color::WHITE),
-    ));
+    commands
+        .spawn((
+            TextEdit,
+            ReadOnly,
+            CosmicEditBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text(
+                &mut font_system,
+                "πŸ˜€πŸ˜€πŸ˜€ x => y\nRead only widget",
+                attrs,
+            ),
+            Node {
+                width: Val::Percent(100.),
+                height: Val::Percent(100.),
+                ..default()
+            },
+            BackgroundColor(Color::WHITE),
+        ))
+        .observe(focus_on_click);
 }
 
 fn main() {
@@ -41,6 +43,6 @@ fn main() {
         .add_plugins(DefaultPlugins)
         .add_plugins(CosmicEditPlugin { font_config })
         .add_systems(Startup, setup)
-        .add_systems(Update, (change_active_editor_ui, deselect_editor_on_esc))
+        .add_systems(Update, deselect_editor_on_esc)
         .run();
 }
diff --git a/examples/scroll.rs b/examples/scroll.rs
new file mode 100644
index 0000000..47d8e00
--- /dev/null
+++ b/examples/scroll.rs
@@ -0,0 +1,69 @@
+//! Currently scrolling is bugged
+
+use bevy::prelude::*;
+use bevy_cosmic_edit::{
+    cosmic_text::{Attrs, AttrsOwned, Metrics},
+    prelude::*,
+    CosmicTextAlign, ScrollEnabled,
+};
+
+fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) {
+    commands.spawn(Camera2d);
+
+    let attrs = Attrs::new().color(CosmicColor::rgb(0, 50, 200));
+
+    // Ui editor
+    commands
+        .spawn((
+            TextEdit,
+            CosmicEditBuffer::new(&mut font_system, Metrics::new(20., 18.)).with_text(
+                &mut font_system,
+                format!(
+                    "Top line\n{}BottomLine",
+                    "UI editor that is long vertical\n".repeat(7)
+                )
+                .as_str(),
+                attrs,
+            ),
+            ScrollEnabled::Enabled,
+            CosmicTextAlign::top_left(),
+            DefaultAttrs(AttrsOwned::new(
+                Attrs::new().color(bevy::color::palettes::css::LIMEGREEN.to_cosmic()),
+            )),
+            // CosmicWrap::InfiniteLine,
+            Node {
+                // Size and position of text box
+                width: Val::Px(300.),
+                height: Val::Px(150.),
+                left: Val::Px(100.),
+                top: Val::Px(100.),
+                ..default()
+            },
+        ))
+        .observe(focus_on_click);
+
+    // Sprite editor
+    commands
+        .spawn((
+            TextEdit2d,
+            // MaxLines(1),
+            CosmicWrap::InfiniteLine,
+            // Sets size of text box
+            Sprite {
+                custom_size: Some(Vec2::new(300., 100.)),
+                ..default()
+            },
+            // Position of text box
+            Transform::from_xyz(0., 100., 0.),
+        ))
+        .observe(focus_on_click);
+}
+
+fn main() {
+    App::new()
+        .add_plugins(DefaultPlugins)
+        .add_plugins(CosmicEditPlugin::default())
+        .add_systems(Startup, setup)
+        .add_systems(Update, deselect_editor_on_esc)
+        .run();
+}
diff --git a/examples/sprite_and_ui_clickable.rs b/examples/sprite_and_ui_clickable.rs
index 4a01b17..79a5755 100644
--- a/examples/sprite_and_ui_clickable.rs
+++ b/examples/sprite_and_ui_clickable.rs
@@ -1,46 +1,53 @@
 use bevy::prelude::*;
 use bevy_cosmic_edit::{
     cosmic_text::{Attrs, AttrsOwned},
+    input::hover::{TextHoverIn, TextHoverOut},
+    input::CosmicTextChanged,
     prelude::*,
-    CosmicTextAlign, CosmicTextChanged, CosmicWrap, MaxLines, TextHoverIn, TextHoverOut,
+    CosmicTextAlign, MaxLines,
 };
 
 fn setup(mut commands: Commands) {
     commands.spawn(Camera2d);
 
     // Ui editor
-    commands.spawn((
-        TextEdit,
-        CosmicEditBuffer::default(),
-        DefaultAttrs(AttrsOwned::new(
-            Attrs::new().color(bevy::color::palettes::css::LIMEGREEN.to_cosmic()),
-        )),
-        MaxLines(1),
-        CosmicWrap::InfiniteLine,
-        CosmicTextAlign::Left { padding: 5 },
-        Node {
-            // Size and position of text box
-            width: Val::Px(300.),
-            height: Val::Px(50.),
-            left: Val::Px(100.),
-            top: Val::Px(100.),
-            ..default()
-        },
-    ));
+    commands
+        .spawn((
+            TextEdit,
+            CosmicEditBuffer::default(),
+            DefaultAttrs(AttrsOwned::new(
+                Attrs::new().color(bevy::color::palettes::css::LIMEGREEN.to_cosmic()),
+            )),
+            MaxLines(1),
+            CosmicWrap::InfiniteLine,
+            CosmicTextAlign::left_center(),
+            Node {
+                // Size and position of text box
+                width: Val::Px(300.),
+                height: Val::Px(50.),
+                left: Val::Px(100.),
+                top: Val::Px(100.),
+                ..default()
+            },
+        ))
+        .observe(focus_on_click);
 
     // Sprite editor
-    commands.spawn((
-        CosmicEditBuffer::default(),
-        MaxLines(1),
-        CosmicWrap::InfiniteLine,
-        // Sets size of text box
-        Sprite {
-            custom_size: Some(Vec2::new(300., 100.)),
-            ..default()
-        },
-        // Position of text box
-        Transform::from_xyz(0., 100., 0.),
-    ));
+    commands
+        .spawn((
+            TextEdit2d,
+            MaxLines(1),
+            CosmicWrap::InfiniteLine,
+            CosmicTextAlign::left_center(),
+            // Sets size of text box
+            Sprite {
+                custom_size: Some(Vec2::new(300., 100.)),
+                ..default()
+            },
+            // Position of text box
+            Transform::from_xyz(0., 100., 0.),
+        ))
+        .observe(focus_on_click);
 }
 
 fn ev_test(
@@ -64,14 +71,7 @@ fn main() {
         .add_plugins(DefaultPlugins)
         .add_plugins(CosmicEditPlugin { ..default() })
         .add_systems(Startup, setup)
-        .add_systems(
-            Update,
-            (
-                change_active_editor_ui,
-                change_active_editor_sprite,
-                deselect_editor_on_esc,
-            ),
-        )
+        .add_systems(Update, deselect_editor_on_esc)
         .add_systems(Update, ev_test)
         .run();
 }
diff --git a/src/cosmic_edit.rs b/src/cosmic_edit.rs
index 83aa1c0..0de3284 100644
--- a/src/cosmic_edit.rs
+++ b/src/cosmic_edit.rs
@@ -1,10 +1,9 @@
 use crate::prelude::*;
-use cosmic_text::{Attrs, AttrsOwned, Editor, FontSystem};
+use cosmic_text::{Align, Attrs, AttrsOwned, FontSystem};
 
 pub(crate) fn plugin(app: &mut App) {
     app.register_type::<CosmicWrap>()
         .register_type::<CosmicTextAlign>()
-        .register_type::<XOffset>()
         .register_type::<CosmicBackgroundImage>()
         .register_type::<CosmicBackgroundColor>()
         .register_type::<CursorColor>()
@@ -16,24 +15,130 @@ pub(crate) fn plugin(app: &mut App) {
 
 /// Enum representing text wrapping in a cosmic [`Buffer`]
 #[derive(Component, Reflect, Clone, PartialEq, Default)]
+#[component(on_add = check_align_sanity)]
 pub enum CosmicWrap {
     InfiniteLine,
     #[default]
     Wrap,
 }
 
-/// Enum representing the text alignment in a cosmic [`Buffer`].
-/// Defaults to [`CosmicTextAlign::Center`]
-#[derive(Component, Reflect, Clone)]
-pub enum CosmicTextAlign {
-    Center { padding: i32 },
-    TopLeft { padding: i32 },
-    Left { padding: i32 },
+fn check_align_sanity(
+    world: bevy::ecs::world::DeferredWorld,
+    target: Entity,
+    _: bevy::ecs::component::ComponentId,
+) {
+    if let Some(CosmicWrap::InfiniteLine) = world.get(target) {
+        let Some(align) = world.get::<CosmicTextAlign>(target) else {
+            return;
+        };
+        if matches!(
+            align.horizontal,
+            Some(HorizontalAlign::End | HorizontalAlign::Right | HorizontalAlign::Center)
+        ) {
+            warn!(message = "Having a widget with `CosmicWrap::InfiniteLine` while using a horizontal alignment like `HorizontalAlign::Center` will likely obscure the text");
+        }
+    }
+}
+
+/// Where to render the [`CosmicEditBuffer`] within the given size.
+///
+/// [`cosmic_text`] can [`Align`](cosmic_text::Align) items per line already,
+/// e.g. [`Align::Center`], but this only works horizontally.
+/// To place the text in the direct center vertically, [bevy_cosmic_edit]
+/// manually calculates the vertical offset as configured by
+/// [`CosmicTextAlign.vertical`]
+#[derive(Component, Reflect)]
+pub struct CosmicTextAlign {
+    /// Managed by [bevy_cosmic_edit].
+    /// Will place the text in the direct center vertically.
+    pub vertical: VerticalAlign,
+
+    /// Defaults to `Some(HorizontalAlign::Center)`.
+    ///
+    /// If this `.is_some()`, every frame each line will have this alignment
+    /// set for it. Set this to `None` to apply your own manual
+    /// [cosmic_text::Align]ments.
+    pub horizontal: Option<HorizontalAlign>,
 }
 
 impl Default for CosmicTextAlign {
     fn default() -> Self {
-        CosmicTextAlign::Center { padding: 5 }
+        Self::center()
+    }
+}
+
+impl CosmicTextAlign {
+    pub fn new(horizontal: HorizontalAlign, vertical: VerticalAlign) -> Self {
+        CosmicTextAlign {
+            vertical,
+            horizontal: Some(horizontal),
+        }
+    }
+
+    pub fn center() -> Self {
+        CosmicTextAlign {
+            vertical: VerticalAlign::Center,
+            horizontal: Some(HorizontalAlign::Center),
+        }
+    }
+
+    pub fn top_left() -> Self {
+        CosmicTextAlign {
+            vertical: VerticalAlign::Top,
+            horizontal: Some(HorizontalAlign::Left),
+        }
+    }
+
+    /// Horizontally left, vertically center
+    pub fn left_center() -> Self {
+        CosmicTextAlign {
+            vertical: VerticalAlign::Center,
+            horizontal: Some(HorizontalAlign::Left),
+        }
+    }
+
+    pub fn bottom_center() -> Self {
+        CosmicTextAlign {
+            vertical: VerticalAlign::Bottom,
+            horizontal: Some(HorizontalAlign::Center),
+        }
+    }
+}
+
+/// Enum representing the text alignment in a cosmic [`Buffer`].
+/// Defaults to [`CosmicTextAlign::Center`]
+#[derive(Reflect, Default, Clone, Copy, PartialEq, Eq)]
+pub enum VerticalAlign {
+    /// If [bevy_cosmic_edit] made no manual calcualtions, this would
+    /// effecively be the default
+    Top,
+
+    /// Default
+    #[default]
+    Center,
+
+    Bottom,
+}
+
+/// Mirrors [`cosmic_text::Align`]
+#[derive(Reflect, Debug, Clone, Copy, PartialEq, Eq)]
+pub enum HorizontalAlign {
+    Left,
+    Center,
+    Right,
+    End,
+    Justified,
+}
+
+impl From<HorizontalAlign> for Align {
+    fn from(h: HorizontalAlign) -> Self {
+        match h {
+            HorizontalAlign::Left => Align::Left,
+            HorizontalAlign::Center => Align::Center,
+            HorizontalAlign::Right => Align::Right,
+            HorizontalAlign::End => Align::End,
+            HorizontalAlign::Justified => Align::Justified,
+        }
     }
 }
 
@@ -42,20 +147,6 @@ impl Default for CosmicTextAlign {
 #[derive(Component, Default)]
 pub struct ReadOnly; // tag component
 
-/// Internal value used to decide what section of a [`Buffer`] to render
-#[derive(Component, Reflect, Debug, Default)]
-pub(crate) struct XOffset {
-    /// How much space in logical units from the left of the [`Buffer`]
-    /// to start rendering text.
-    pub left: f32,
-
-    /// Width of buffer that includes text that should be rendered,
-    /// in logical units.
-    ///
-    /// Should only be [None] if in default state
-    pub width: Option<f32>,
-}
-
 /// Default text attributes to be used on a [`CosmicEditBuffer`]
 #[derive(Component, Deref, DerefMut)]
 pub struct DefaultAttrs(pub AttrsOwned);
@@ -130,27 +221,3 @@ impl ScrollEnabled {
 /// this should be merged with its builtin resource
 #[derive(Resource, Deref, DerefMut)]
 pub struct CosmicFontSystem(pub FontSystem);
-
-/// Wrapper component for an [`Editor`] with a few helpful values for cursor blinking
-///
-/// [`cosmic_text::Editor`] is basically a mutable version of [`cosmic_text::Buffer`].
-///
-/// This component should be on a focussed [`CosmicEditBuffer`]
-// Managed by crate::focus::add_editor_to_focussed and similar systems
-#[derive(Component, Deref, DerefMut)]
-pub struct CosmicEditor {
-    #[deref]
-    pub editor: Editor<'static>,
-    pub cursor_visible: bool,
-    pub cursor_timer: Timer,
-}
-
-impl CosmicEditor {
-    pub fn new(editor: Editor<'static>) -> Self {
-        Self {
-            editor,
-            cursor_visible: true,
-            cursor_timer: Timer::new(std::time::Duration::from_millis(530), TimerMode::Repeating),
-        }
-    }
-}
diff --git a/src/cursor.rs b/src/cursor.rs
deleted file mode 100644
index ee3504e..0000000
--- a/src/cursor.rs
+++ /dev/null
@@ -1,107 +0,0 @@
-// This will all be rewritten soon, looking toward per-widget cursor control
-// Rewrite should address issue #93 too
-
-use crate::prelude::*;
-use bevy::{
-    input::mouse::MouseMotion,
-    window::{PrimaryWindow, SystemCursorIcon},
-    winit::cursor::CursorIcon,
-};
-
-pub(crate) struct CursorPlugin;
-
-/// Unit resource whose existence in the world disables the cursor plugin systems.
-#[derive(Resource)]
-pub struct CursorPluginDisabled;
-
-impl Plugin for CursorPlugin {
-    fn build(&self, app: &mut App) {
-        app.add_systems(
-            Update,
-            (
-                (crate::render_targets::hover_sprites, hover_ui),
-                change_cursor,
-            )
-                .chain()
-                .run_if(not(resource_exists::<CursorPluginDisabled>)),
-        )
-        .add_event::<TextHoverIn>()
-        .register_type::<TextHoverIn>()
-        .add_event::<TextHoverOut>()
-        .register_type::<TextHoverOut>()
-        .register_type::<HoverCursor>();
-    }
-}
-
-/// What cursor icon to show when hovering over a widget
-///
-/// By default is [`CursorIcon::System(SystemCursorIcon::Text)`]
-#[derive(Component, Reflect, Deref)]
-pub struct HoverCursor(pub CursorIcon);
-
-impl Default for HoverCursor {
-    fn default() -> Self {
-        Self(CursorIcon::System(SystemCursorIcon::Text))
-    }
-}
-
-/// For use with custom cursor control
-///
-/// Event is emitted when cursor enters a text widget.
-/// Event contains the cursor from the buffer's [`HoverCursor`]
-#[derive(Event, Reflect, Deref, Debug)]
-pub struct TextHoverIn(pub CursorIcon);
-
-/// For use with custom cursor control
-/// Event is emitted when cursor leaves a text widget
-#[derive(Event, Reflect, Debug)]
-pub struct TextHoverOut;
-
-pub(crate) fn change_cursor(
-    mut evr_hover_in: EventReader<TextHoverIn>,
-    evr_hover_out: EventReader<TextHoverOut>,
-    evr_text_changed: EventReader<crate::events::CosmicTextChanged>,
-    evr_mouse_motion: EventReader<MouseMotion>,
-    mouse_buttons: Res<ButtonInput<MouseButton>>,
-    mut windows: Query<(&mut Window, &mut CursorIcon), With<PrimaryWindow>>,
-) {
-    if windows.iter().len() == 0 {
-        return;
-    }
-    let (mut window, mut window_cursor_icon) = windows.single_mut();
-
-    if let Some(ev) = evr_hover_in.read().last() {
-        *window_cursor_icon = ev.0.clone();
-    } else if !evr_hover_out.is_empty() {
-        *window_cursor_icon = CursorIcon::System(SystemCursorIcon::Default);
-    }
-
-    if !evr_text_changed.is_empty() {
-        window.cursor_options.visible = false;
-    }
-
-    if mouse_buttons.get_just_pressed().len() != 0 || !evr_mouse_motion.is_empty() {
-        window.cursor_options.visible = true;
-    }
-}
-
-pub(crate) fn hover_ui(
-    interaction_query: Query<
-        (&Interaction, &HoverCursor),
-        (With<CosmicEditBuffer>, Changed<Interaction>),
-    >,
-    mut evw_hover_in: EventWriter<TextHoverIn>,
-    mut evw_hover_out: EventWriter<TextHoverOut>,
-) {
-    for (interaction, hover) in interaction_query.iter() {
-        match interaction {
-            Interaction::None => {
-                evw_hover_out.send(TextHoverOut);
-            }
-            Interaction::Hovered => {
-                evw_hover_in.send(TextHoverIn(hover.0.clone()));
-            }
-            _ => {}
-        }
-    }
-}
diff --git a/src/debug.rs b/src/debug.rs
new file mode 100644
index 0000000..b5478c2
--- /dev/null
+++ b/src/debug.rs
@@ -0,0 +1,45 @@
+//! Internal debugging only
+
+use crate::prelude::*;
+
+pub(crate) fn plugin(app: &mut App) {
+    app.add_systems(Last, change_detection)
+        .add_systems(PreUpdate, change_detection);
+}
+
+/// Query filters like [`Changed<T>`] and [`Added<T>`] ensure only entities matching these filters
+/// will be returned by the query.
+///
+/// Using the [`Ref<T>`] system param allows you to access change detection information, but does
+/// not filter the query.
+fn change_detection(
+    changed_components: Query<Ref<CosmicRenderOutput>, Changed<CosmicRenderOutput>>,
+    // my_resource: Res<MyResource>,
+) {
+    for component in &changed_components {
+        // By default, you can only tell that a component was changed.
+        //
+        // This is useful, but what if you have multiple systems modifying the same component, how
+        // will you know which system is causing the component to change?
+        warn!(
+            "Change detected!\n\t-> value: {:?}\n\t-> added: {}\n\t-> changed: {}\n\t-> changed by: {}",
+            component,
+            component.is_added(),
+            component.is_changed(),
+            // If you enable the `track_change_detection` feature, you can unlock the `changed_by()`
+            // method. It returns the file and line number that the component or resource was
+            // changed in. It's not recommended for released games, but great for debugging!
+            component.changed_by()
+        );
+    }
+
+    // if my_resource.is_changed() {
+    //     warn!(
+    //         "Change detected!\n\t-> value: {:?}\n\t-> added: {}\n\t-> changed: {}\n\t-> changed by: {}",
+    //         my_resource,
+    //         my_resource.is_added(),
+    //         my_resource.is_changed(),
+    //         my_resource.changed_by() // Like components, requires `track_change_detection` feature.
+    //     );
+    // }
+}
diff --git a/src/double_click.rs b/src/double_click.rs
new file mode 100644
index 0000000..f2e25c1
--- /dev/null
+++ b/src/double_click.rs
@@ -0,0 +1,91 @@
+use bevy::ecs::system::SystemParam;
+
+use crate::prelude::*;
+use std::{fmt::Debug, time::Duration};
+
+pub(crate) fn plugin(app: &mut App) {
+    app.init_resource::<ClickStateRes>()
+        .register_type::<ClickStateRes>()
+        .add_systems(Update, tick);
+}
+
+#[derive(SystemParam)]
+pub struct ClickState<'w> {
+    res: ResMut<'w, ClickStateRes>,
+}
+
+/// Actual state struct
+#[derive(Resource, Reflect)]
+#[reflect(Resource)]
+struct ClickStateRes {
+    timer_since_last_click: Timer,
+    click_state: Option<ClickCount>,
+}
+
+impl Default for ClickStateRes {
+    fn default() -> Self {
+        Self {
+            timer_since_last_click: Timer::from_seconds(0.5, TimerMode::Once),
+            click_state: None,
+        }
+    }
+}
+
+#[derive(Reflect, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum ClickCount {
+    Single,
+    Double,
+    Triple,
+    MoreThanTriple,
+}
+
+impl ClickCount {
+    fn advance(self) -> Self {
+        match self {
+            Self::Single => Self::Double,
+            Self::Double => Self::Triple,
+            Self::Triple => Self::MoreThanTriple,
+            Self::MoreThanTriple => Self::MoreThanTriple,
+        }
+    }
+}
+
+impl ClickState<'_> {
+    /// Makes [ClickState] aware of a click event.
+    pub fn feed_click(&mut self) -> ClickCount {
+        self.res.timer_since_last_click.reset();
+        match self.res.click_state {
+            None => {
+                self.res.click_state = Some(ClickCount::Single);
+                ClickCount::Single
+            }
+            Some(click_count) => {
+                let new_click_count = click_count.advance();
+                self.res.click_state = Some(new_click_count);
+                new_click_count
+            }
+        }
+    }
+
+    /// Get the current click state.
+    ///
+    /// `None` means no clicks have been registered recently
+    #[allow(dead_code)]
+    pub fn get(&self) -> Option<ClickCount> {
+        self.res.click_state
+    }
+
+    /// You must call this every frame
+    pub(crate) fn tick(&mut self, delta: Duration) {
+        self.res.timer_since_last_click.tick(delta);
+
+        if self.res.timer_since_last_click.just_finished() {
+            self.res.click_state = None;
+            // debug!("Resetting click timer");
+        }
+    }
+}
+
+fn tick(mut state: ClickState, time: Res<Time>) {
+    state.tick(time.delta());
+}
diff --git a/src/editor_buffer.rs b/src/editor_buffer.rs
new file mode 100644
index 0000000..abedefc
--- /dev/null
+++ b/src/editor_buffer.rs
@@ -0,0 +1,172 @@
+//! Provides an API for mutating [`cosmic_text::Editor`] and [`cosmic_text::Buffer`]
+//!
+//! This module is the privacy boundary for the abitrary construction
+//! of [`CosmicEditor`], which is the primary interface for mutating [`Buffer`].
+
+use bevy::ecs::query::QueryData;
+use cosmic_text::{Attrs, BufferRef, FontSystem, Shaping};
+
+use crate::prelude::*;
+
+pub(crate) struct EditorBufferPlugin;
+
+impl Plugin for EditorBufferPlugin {
+    fn build(&self, app: &mut App) {
+        app.add_systems(First, (buffer::set_initial_scale, buffer::add_font_system))
+            .add_systems(Update, editor::blink_cursor);
+    }
+}
+
+pub mod buffer;
+pub mod editor;
+
+/// Primary interface for accessing the [`cosmic_text::Buffer`] of a widget.
+///
+/// This will check for the (optional) presence of a [`CosmicEditor`] component
+/// and mutate its [`Buffer`] instead of [`CosmicEditBuffer`] by default,
+/// which is always what you want.
+///
+/// This is the required alternative to manually querying [`&mut CosmicEditBuffer`]
+/// to uphold the invariant that [`CosmicEditBuffer`] is basically immutable
+/// and the source of truth without a [`CosmicEditor`], **but [`CosmicEditor`] is
+/// the source of truth when present** (to allow mutation).
+#[derive(QueryData)]
+#[query_data(mutable)]
+pub struct EditorBuffer {
+    editor: Option<&'static mut CosmicEditor>,
+    buffer: &'static mut CosmicEditBuffer,
+}
+
+impl std::ops::Deref for EditorBufferItem<'_> {
+    type Target = Buffer;
+
+    fn deref(&self) -> &Self::Target {
+        self.get_raw_buffer()
+    }
+}
+
+impl std::ops::DerefMut for EditorBufferItem<'_> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.get_raw_buffer_mut()
+    }
+}
+
+impl<'r, 's> EditorBufferItem<'_> {
+    pub fn editor(&mut self) -> Option<&mut CosmicEditor> {
+        self.editor.as_deref_mut()
+    }
+
+    /// Replace buffer text
+    pub fn set_text(
+        &mut self,
+        font_system: &mut FontSystem,
+        text: &'s str,
+        attrs: Attrs<'r>,
+    ) -> &mut Self {
+        self.get_raw_buffer_mut()
+            .set_text(font_system, text, attrs, Shaping::Advanced);
+        self.set_redraw(true);
+        self
+    }
+
+    /// Replace buffer text with rich text
+    ///
+    /// Rich text is an iterable of `(&'s str, Attrs<'r>)`
+    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.get_raw_buffer_mut()
+            .set_rich_text(font_system, spans, attrs, Shaping::Advanced);
+        self.set_redraw(true);
+        self
+    }
+
+    pub fn with_buffer_mut<F: FnOnce(&mut Buffer) -> T, T>(&mut self, f: F) -> T {
+        match self.editor.as_mut() {
+            Some(editor) => editor.with_buffer_mut(f),
+            None => f(&mut self.buffer.0),
+        }
+    }
+
+    pub fn with_buffer<F: FnOnce(&Buffer) -> T, T>(&self, f: F) -> T {
+        match self.editor.as_ref() {
+            Some(editor) => editor.with_buffer(f),
+            None => f(&self.buffer.0),
+        }
+    }
+
+    pub fn get_raw_buffer(&self) -> &Buffer {
+        match self.editor.as_ref() {
+            Some(editor) => match editor.editor.buffer_ref() {
+                BufferRef::Owned(buffer) => buffer,
+                BufferRef::Borrowed(buffer) => buffer,
+                BufferRef::Arc(arc) => arc,
+            },
+            None => &self.buffer.0,
+        }
+    }
+
+    pub fn get_raw_buffer_mut(&mut self) -> &mut Buffer {
+        match self.editor.as_mut() {
+            Some(editor) => match editor.editor.buffer_ref_mut() {
+                BufferRef::Owned(buffer) => buffer,
+                BufferRef::Borrowed(buffer) => buffer,
+                BufferRef::Arc(arc) => std::sync::Arc::make_mut(arc),
+            },
+            None => &mut self.buffer.0,
+        }
+    }
+
+    pub fn borrow_with<'a>(
+        &'a mut self,
+        font_system: &'a mut cosmic_text::FontSystem,
+    ) -> ManuallyBorrowedWithFontSystem<'a, Self> {
+        ManuallyBorrowedWithFontSystem {
+            font_system,
+            inner: self,
+        }
+    }
+}
+
+impl buffer::BufferRefExtras for EditorBufferItem<'_> {
+    fn get_text(&self) -> String {
+        self.with_buffer(|b| b.get_text())
+    }
+}
+
+pub struct ManuallyBorrowedWithFontSystem<'a, T> {
+    font_system: &'a mut cosmic_text::FontSystem,
+    inner: &'a mut T,
+}
+
+impl ManuallyBorrowedWithFontSystem<'_, EditorBufferItem<'_>> {
+    pub fn with_buffer_mut<F: FnOnce(&mut cosmic_text::BorrowedWithFontSystem<Buffer>) -> T, T>(
+        &mut self,
+        f: F,
+    ) -> T {
+        match self.inner.editor.as_mut() {
+            Some(editor) => editor.borrow_with(self.font_system).with_buffer_mut(f),
+            None => f(&mut self.inner.buffer.0.borrow_with(self.font_system)),
+        }
+    }
+}
+
+impl buffer::BufferMutExtras for ManuallyBorrowedWithFontSystem<'_, EditorBufferItem<'_>> {
+    fn width(&mut self) -> f32 {
+        self.with_buffer_mut(|b| b.width())
+    }
+
+    fn height(&mut self) -> f32 {
+        self.with_buffer_mut(|b| b.height())
+    }
+
+    fn compute_everything(&mut self) {
+        self.with_buffer_mut(|b| b.compute_everything())
+    }
+}
diff --git a/src/buffer.rs b/src/editor_buffer/buffer.rs
similarity index 52%
rename from src/buffer.rs
rename to src/editor_buffer/buffer.rs
index 5205038..1062754 100644
--- a/src/buffer.rs
+++ b/src/editor_buffer/buffer.rs
@@ -1,48 +1,38 @@
-use crate::{
-    cosmic_edit::{ScrollEnabled, XOffset},
-    prelude::*,
-    widget::CosmicPadding,
-    CosmicBackgroundColor, CosmicBackgroundImage, CosmicTextAlign, CosmicWrap, CursorColor,
-    HoverCursor, MaxChars, MaxLines, SelectionColor,
-};
-use bevy::{
-    ecs::{component::ComponentId, query::QueryData, world::DeferredWorld},
-    window::PrimaryWindow,
-};
-use cosmic_text::{Attrs, AttrsOwned, Buffer, Edit, FontSystem, Metrics, Shaping};
-
-pub(crate) struct BufferPlugin;
-
-impl Plugin for BufferPlugin {
-    fn build(&self, app: &mut App) {
-        app.add_systems(
-            First,
-            (
-                add_font_system,
-                set_initial_scale,
-                set_redraw,
-                set_editor_redraw,
-                update_internal_target_handles,
-            )
-                .chain(),
-        );
-    }
-}
+//! API for [`cosmic_text::Buffer`].
+//!
+//! Primarily stored in [`CosmicEditBuffer`]
+
+use cosmic_text::Attrs;
+use cosmic_text::AttrsOwned;
+use cosmic_text::BorrowedWithFontSystem;
+use cosmic_text::FontSystem;
+use cosmic_text::Metrics;
+use cosmic_text::Shaping;
 
-pub trait BufferExtras {
+use crate::cosmic_edit::*;
+use crate::prelude::*;
+
+pub trait BufferRefExtras {
     fn get_text(&self) -> String;
 }
 
-impl BufferExtras for Buffer {
-    /// Retrieves the text content from a buffer.
-    ///
-    /// # Arguments
-    ///
-    /// * none, takes the rust magic ref to self
+pub trait BufferMutExtras {
+    fn compute_everything(&mut self);
+
+    /// Height that buffer text would take up if rendered
     ///
-    /// # Returns
-    ///input
-    /// A [`String`] containing the cosmic text content.
+    /// Used for [`VerticalAlign`](crate::VerticalAlign)
+    fn height(&mut self) -> f32;
+
+    fn width(&mut self) -> f32;
+
+    fn expected_size(&mut self) -> Vec2 {
+        Vec2::new(self.width(), self.height())
+    }
+}
+
+impl BufferRefExtras for Buffer {
+    /// Retrieves the text content from a buffer.
     fn get_text(&self) -> String {
         let mut text = String::new();
         let line_count = self.lines.len();
@@ -59,9 +49,52 @@ impl BufferExtras for Buffer {
     }
 }
 
-/// Component wrapper for [`Buffer`]
-#[derive(Component, Deref, DerefMut)]
-#[component(on_remove = remove_focus_from_entity)]
+impl BufferMutExtras for BorrowedWithFontSystem<'_, Buffer> {
+    fn height(&mut self) -> f32 {
+        self.compute_everything();
+        // TODO: which implementation is correct?
+        // self.metrics().line_height * self.layout_runs().count() as f32
+        self.layout_runs().map(|line| line.line_height).sum()
+    }
+
+    fn width(&mut self) -> f32 {
+        self.compute_everything();
+        // get max line width
+        self.layout_runs()
+            .map(|line| line.line_w)
+            .reduce(f32::max)
+            .unwrap_or(0.0)
+    }
+
+    fn compute_everything(&mut self) {
+        let last_line_num = self.lines.len() - 1;
+        let last_line_width = self.lines[last_line_num].text().len();
+        let end_cursor = cosmic_text::Cursor::new(last_line_num, last_line_width);
+        self.shape_until_cursor(end_cursor, false);
+    }
+}
+
+impl BufferMutExtras for BorrowedWithFontSystem<'_, cosmic_text::Editor<'_>> {
+    fn height(&mut self) -> f32 {
+        self.with_buffer_mut(|b| b.height())
+    }
+
+    fn width(&mut self) -> f32 {
+        self.with_buffer_mut(|b| b.width())
+    }
+
+    fn compute_everything(&mut self) {
+        self.with_buffer_mut(|b| b.compute_everything());
+        // self.shape_as_needed(false)
+    }
+}
+
+/// Component wrapper for [`cosmic_text::Buffer`]
+///
+/// To access the underlying [`Buffer`], use [`EditorBuffer`](crate::editor_buffer:EditorBuffer).
+///
+#[derive(Component, Debug)]
+#[component(on_add = on_buffer_add, on_remove = crate::focus::remove_focus_from_entity)]
 #[require(
     CosmicBackgroundColor,
     CursorColor,
@@ -71,24 +104,12 @@ impl BufferExtras for Buffer {
     CosmicRenderOutput,
     MaxLines,
     MaxChars,
-    XOffset,
     CosmicWrap,
     CosmicTextAlign,
-    CosmicPadding,
-    HoverCursor,
-    ScrollEnabled
+    crate::input::hover::HoverCursor,
+    crate::input::InputState
 )]
-pub struct CosmicEditBuffer(pub Buffer);
-
-fn remove_focus_from_entity(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
-    if let Some(mut focused_widget) = world.get_resource_mut::<FocusedWidget>() {
-        if let Some(focused) = focused_widget.0 {
-            if focused == entity {
-                focused_widget.0 = None;
-            }
-        }
-    }
-}
+pub struct CosmicEditBuffer(pub(super) Buffer);
 
 impl Default for CosmicEditBuffer {
     fn default() -> Self {
@@ -96,10 +117,31 @@ impl Default for CosmicEditBuffer {
     }
 }
 
+fn on_buffer_add(
+    mut world: bevy::ecs::world::DeferredWorld,
+    target: Entity,
+    _: bevy::ecs::component::ComponentId,
+) {
+    // set redraw
+    world
+        .get_mut::<CosmicEditBuffer>(target)
+        .unwrap()
+        .0
+        .set_redraw(true);
+}
+
+/// Should be partly mirrored on [`EditorBuffer`]
 impl<'s, 'r> CosmicEditBuffer {
     /// Create a new buffer with a font system
     pub fn new(font_system: &mut FontSystem, metrics: Metrics) -> Self {
-        Self(Buffer::new(font_system, metrics))
+        let mut buffer = Buffer::new(font_system, metrics);
+        buffer.set_redraw(true);
+        Self(buffer)
+    }
+
+    #[cfg(test)]
+    pub(crate) fn inner(&self) -> &Buffer {
+        &self.0
     }
 
     // Das a lotta boilerplate just to hide the shaping argument
@@ -111,6 +153,7 @@ impl<'s, 'r> CosmicEditBuffer {
         attrs: Attrs<'r>,
     ) -> Self {
         self.0.set_text(font_system, text, attrs, Shaping::Advanced);
+        self.0.set_redraw(true);
         self
     }
 
@@ -139,7 +182,7 @@ impl<'s, 'r> CosmicEditBuffer {
         attrs: Attrs<'r>,
     ) -> &mut Self {
         self.0.set_text(font_system, text, attrs, Shaping::Advanced);
-        self.set_redraw(true);
+        self.0.set_redraw(true);
         self
     }
 
@@ -157,10 +200,15 @@ impl<'s, 'r> CosmicEditBuffer {
     {
         self.0
             .set_rich_text(font_system, spans, attrs, Shaping::Advanced);
-        self.set_redraw(true);
+        self.0.set_redraw(true);
         self
     }
 
+    pub fn from_raw_buffer(mut buffer: Buffer) -> CosmicEditBuffer {
+        buffer.set_redraw(true);
+        Self(buffer)
+    }
+
     /// Returns texts from a MultiStyle buffer
     pub fn get_text_spans(&self, default_attrs: AttrsOwned) -> Vec<Vec<(String, AttrsOwned)>> {
         // TODO: untested!
@@ -168,7 +216,7 @@ impl<'s, 'r> CosmicEditBuffer {
         let buffer = self;
 
         let mut spans = Vec::new();
-        for line in buffer.lines.iter() {
+        for line in buffer.0.lines.iter() {
             let mut line_spans = Vec::new();
             let line_text = line.text();
             let line_attrs = line.attrs_list();
@@ -200,25 +248,33 @@ impl<'s, 'r> CosmicEditBuffer {
         }
         spans
     }
+
+    pub(crate) fn from_downgrading_editor(removed_editor: &CosmicEditor) -> CosmicEditBuffer {
+        // maybe clone only lines?
+        let buffer = removed_editor.with_buffer(|buf| buf.clone());
+        CosmicEditBuffer::from_raw_buffer(buffer)
+    }
 }
 
+/// Sets a default text value of "".
 /// Adds a [`FontSystem`] to a newly created [`CosmicEditBuffer`] if one was not provided
-pub(crate) fn add_font_system(
+///
+/// This fixes the bug where an empty buffer won't show a blinking cursor when focused
+pub(in crate::editor_buffer) fn add_font_system(
     mut font_system: ResMut<CosmicFontSystem>,
     mut q: Query<&mut CosmicEditBuffer, Added<CosmicEditBuffer>>,
 ) {
     for mut b in q.iter_mut() {
-        if !b.lines.is_empty() {
-            continue;
+        if b.0.lines.is_empty() {
+            b.0.set_text(&mut font_system, "", Attrs::new(), Shaping::Advanced);
+            b.0.set_redraw(true);
         }
-        b.0.set_text(&mut font_system, "", Attrs::new(), Shaping::Advanced);
-        b.set_redraw(true);
     }
 }
 
 /// Initialises [`CosmicEditBuffer`] scale factor
-pub(crate) fn set_initial_scale(
-    window_q: Query<&Window, With<PrimaryWindow>>,
+pub(in crate::editor_buffer) fn set_initial_scale(
+    window_q: Query<&Window, With<bevy::window::PrimaryWindow>>,
     mut cosmic_query: Query<&mut CosmicEditBuffer, Added<CosmicEditBuffer>>,
     mut font_system: ResMut<CosmicFontSystem>,
 ) {
@@ -226,82 +282,8 @@ pub(crate) fn set_initial_scale(
         let w_scale = window.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);
-        }
-    }
-}
-
-/// Initialises new [`CosmicEditBuffer`] redraw flag to true
-pub(crate) fn set_redraw(mut q: Query<&mut CosmicEditBuffer, Added<CosmicEditBuffer>>) {
-    for mut b in q.iter_mut() {
-        b.set_redraw(true);
-    }
-}
-
-/// Initialises new [`CosmicEditor`] redraw flag to true
-pub(crate) fn set_editor_redraw(mut q: Query<&mut CosmicEditor, Added<CosmicEditor>>) {
-    for mut ed in q.iter_mut() {
-        ed.set_redraw(true);
-    }
-}
-
-/// Will attempt to find a place on the receiving entity to place
-/// a [`Handle<Image>`]
-#[derive(QueryData)]
-#[query_data(mutable)]
-pub(crate) struct OutputToEntity {
-    sprite_target: Option<&'static mut Sprite>,
-    image_node_target: Option<&'static mut ImageNode>,
-}
-
-impl OutputToEntityItem<'_> {
-    pub fn write_image_data(&mut self, image: &Handle<Image>) {
-        if let Some(sprite) = self.sprite_target.as_mut() {
-            sprite.image = image.clone_weak();
-        }
-        if let Some(image_node) = self.image_node_target.as_mut() {
-            image_node.image = image.clone_weak();
+            let m = b.0.metrics().scale(w_scale);
+            b.0.set_metrics(&mut font_system, m);
         }
     }
 }
-
-/// Every frame updates the output (in [`CosmicRenderOutput`]) to its receiver
-/// on the same entity, e.g. [`Sprite`]
-pub(crate) fn update_internal_target_handles(
-    mut buffers_q: Query<(&CosmicRenderOutput, OutputToEntity), With<CosmicEditBuffer>>,
-) {
-    for (output_data, mut output_components) in buffers_q.iter_mut() {
-        output_components.write_image_data(&output_data.0);
-    }
-}
-
-// TODO put this on impl CosmicBuffer
-
-/// Returns in physical pixels
-pub(crate) fn get_text_size(buffer: &Buffer) -> Vec2 {
-    if buffer.layout_runs().count() == 0 {
-        return Vec2::new(0., buffer.metrics().line_height);
-    }
-    // get max width
-    let width = buffer
-        .layout_runs()
-        .map(|run| run.line_w)
-        .reduce(f32::max)
-        .unwrap();
-    // get total height
-    let height = buffer.layout_runs().count() as f32 * buffer.metrics().line_height;
-    Vec2::new(width, height)
-}
-
-/// Returns in physical pixels
-pub(crate) fn get_y_offset_center(widget_height: f32, buffer: &Buffer) -> i32 {
-    let text_height = get_text_size(buffer).y;
-    ((widget_height - text_height) / 2.0) as i32
-}
-
-/// Returns in physical pixels
-pub(crate) fn get_x_offset_center(widget_width: f32, buffer: &Buffer) -> i32 {
-    let text_width = get_text_size(buffer).x;
-    ((widget_width - text_width) / 2.0) as i32
-}
diff --git a/src/editor_buffer/editor.rs b/src/editor_buffer/editor.rs
new file mode 100644
index 0000000..0a34214
--- /dev/null
+++ b/src/editor_buffer/editor.rs
@@ -0,0 +1,62 @@
+use std::time::Duration;
+
+use cosmic_text::Editor;
+
+use crate::prelude::*;
+
+/// Wrapper component for an [`Editor`] with a few helpful values for cursor blinking.
+/// [`cosmic_text::Editor`] is basically a mutable version of [`cosmic_text::Buffer`].
+///
+/// This component shouldn't be manually added or constructed, and is automatically
+/// managed by the [`crate::focus`]
+#[derive(Component, Deref, DerefMut)]
+#[non_exhaustive]
+pub struct CosmicEditor {
+    #[deref]
+    pub editor: Editor<'static>,
+    pub cursor_visible: bool,
+    pub cursor_timer: Timer,
+}
+
+pub(super) 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;
+            // trace!("Toggling cursor");
+            e.set_redraw(true);
+        }
+    }
+}
+
+impl CosmicEditor {
+    /// The only way to create a new [`CosmicEditor`] outside of `crate::editor_buffer::editor`
+    pub(crate) fn clone_from_buffer(old_buffer: &CosmicEditBuffer) -> Self {
+        let buffer = old_buffer.0.clone();
+        let editor = Editor::new(buffer);
+        Self::new(editor)
+    }
+
+    fn new(mut editor: Editor<'static>) -> Self {
+        // this makes sure when switching between editors,
+        // the cursor doesn't immediately blink at the start
+        // before its position has been updated
+        let duration = Duration::from_millis(530);
+        let mut cursor_timer = Timer::new(Duration::from_millis(530), TimerMode::Repeating);
+        cursor_timer.tick(duration - Duration::from_millis(80));
+
+        editor.set_redraw(true);
+
+        Self {
+            editor,
+            cursor_visible: false,
+            cursor_timer,
+        }
+    }
+}
+
+impl super::buffer::BufferRefExtras for CosmicEditor {
+    fn get_text(&self) -> String {
+        self.with_buffer(|b| b.get_text())
+    }
+}
diff --git a/src/events.rs b/src/events.rs
deleted file mode 100644
index ecbb82d..0000000
--- a/src/events.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-// File for all events, meant for easy documentation
-
-use bevy::prelude::*;
-
-/// Registers internal events
-pub(crate) struct EventsPlugin;
-
-impl Plugin for EventsPlugin {
-    fn build(&self, app: &mut App) {
-        app.add_event::<CosmicTextChanged>()
-            .register_type::<CosmicTextChanged>();
-    }
-}
-
-/// Text change events
-///
-/// Sent when text is changed in a cosmic buffer
-/// Contains the entity on which the text was changed, and the new text as a [`String`]
-#[derive(Event, Reflect, Debug)]
-pub struct CosmicTextChanged(pub (Entity, String));
diff --git a/src/focus.rs b/src/focus.rs
index 04df1ab..e61fbd0 100644
--- a/src/focus.rs
+++ b/src/focus.rs
@@ -1,5 +1,9 @@
-use crate::{prelude::*, widget::WidgetSet};
-use cosmic_text::{Edit, Editor};
+//! Manages the [`FocusedWidget`] resource
+//!
+//! Makes sure that the focused widget has a [`CosmicEditor`] component
+//! if its focused
+
+use crate::prelude::*;
 
 /// System set for focus systems. Runs in `PostUpdate`
 #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
@@ -13,8 +17,7 @@ impl Plugin for FocusPlugin {
             PostUpdate,
             (drop_editor_unfocused, add_editor_to_focused)
                 .chain()
-                .in_set(FocusSet)
-                .after(WidgetSet),
+                .in_set(FocusSet),
         )
         .init_resource::<FocusedWidget>()
         .register_type::<FocusedWidget>();
@@ -36,12 +39,12 @@ pub(crate) fn add_editor_to_focused(
     q: Query<&CosmicEditBuffer, Without<CosmicEditor>>,
 ) {
     if let Some(e) = active_editor.0 {
-        let Ok(b) = q.get(e) else {
+        let Ok(buffer) = q.get(e) else {
             return;
         };
-        let mut editor = Editor::new(b.0.clone());
-        editor.set_redraw(true);
-        commands.entity(e).insert(CosmicEditor::new(editor));
+        let editor = CosmicEditor::clone_from_buffer(buffer);
+        trace!("Adding editor to focused widget");
+        commands.entity(e).insert(editor);
     }
 }
 
@@ -51,19 +54,39 @@ pub(crate) fn drop_editor_unfocused(
     active_editor: Res<FocusedWidget>,
     mut q: Query<(Entity, &mut CosmicEditBuffer, &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);
+    match active_editor.0 {
+        None => {
+            for (e, mut buffer, editor) in q.iter_mut() {
+                // buffer.lines = editor.with_buffer(|buf| buf.lines.clone());
+                // buffer.set_redraw(true);
+                *buffer = CosmicEditBuffer::from_downgrading_editor(editor);
+                trace!("Removing editor from all entities as there is no focussed widget",);
                 commands.entity(e).remove::<CosmicEditor>();
             }
         }
+        Some(focused) => {
+            for (e, mut b, editor) in q.iter_mut() {
+                if e != focused {
+                    *b = CosmicEditBuffer::from_downgrading_editor(editor);
+                    trace!("Removing editor from entity as its not focussed anymore",);
+                    commands.entity(e).remove::<CosmicEditor>();
+                }
+            }
+        }
+    }
+}
+
+/// Placed as on_remove hook for [`CosmicEditBuffer`] and [`CosmicEditor`]
+pub(crate) fn remove_focus_from_entity(
+    mut world: bevy::ecs::world::DeferredWorld,
+    entity: Entity,
+    _: bevy::ecs::component::ComponentId,
+) {
+    if let Some(mut focused_widget) = world.get_resource_mut::<FocusedWidget>() {
+        if let Some(focused) = focused_widget.0 {
+            if focused == entity {
+                focused_widget.0 = None;
+            }
+        }
     }
 }
diff --git a/src/input.rs b/src/input.rs
index 124abfe..0d97a14 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -1,30 +1,16 @@
 #![allow(clippy::too_many_arguments, clippy::type_complexity)]
 
-use crate::{
-    buffer::{get_x_offset_center, get_y_offset_center},
-    cosmic_edit::{CosmicTextAlign, MaxChars, MaxLines, ReadOnly, ScrollEnabled, XOffset},
-    events::CosmicTextChanged,
-    prelude::*,
-    CosmicWidgetSize,
-};
-use bevy::{
-    input::{
-        keyboard::{Key, KeyboardInput},
-        mouse::{MouseMotion, MouseScrollUnit, MouseWheel},
-    },
-    window::PrimaryWindow,
-};
-use cosmic_text::{Action, Cursor, Edit, Motion, Selection};
-
-#[cfg(target_arch = "wasm32")]
-use bevy::tasks::AsyncComputeTaskPool;
-#[cfg(target_arch = "wasm32")]
-#[allow(unused_imports)]
-use js_sys::Promise;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::prelude::*;
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen_futures::JsFuture;
+use crate::{cosmic_edit::ScrollEnabled, prelude::*};
+use bevy::ecs::{component::ComponentId, world::DeferredWorld};
+
+pub mod click;
+pub mod clipboard;
+pub mod cursor_icon;
+pub mod cursor_visibility;
+pub mod drag;
+pub mod hover;
+pub mod keyboard;
+pub mod scroll;
 
 /// System set for mouse and keyboard input events. Runs in [`PreUpdate`] and [`Update`]
 #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
@@ -34,671 +20,110 @@ pub(crate) struct InputPlugin;
 
 impl Plugin for InputPlugin {
     fn build(&self, app: &mut App) {
-        app.add_systems(PreUpdate, input_mouse.in_set(InputSet))
+        app.add_systems(PreUpdate, scroll::scroll.in_set(InputSet))
             .add_systems(
                 Update,
-                (kb_move_cursor, kb_input_text, kb_clipboard)
+                (
+                    keyboard::kb_move_cursor,
+                    keyboard::kb_input_text,
+                    clipboard::kb_clipboard,
+                    (
+                        cursor_icon::update_cursor_icon,
+                        cursor_visibility::update_cursor_visibility,
+                    ),
+                )
                     .chain()
                     .in_set(InputSet),
             )
-            .insert_resource(ClickTimer(Timer::from_seconds(0.5, TimerMode::Once)));
+            .add_event::<hover::TextHoverIn>()
+            .add_event::<hover::TextHoverOut>()
+            .add_event::<CosmicTextChanged>()
+            .register_type::<hover::TextHoverIn>()
+            .register_type::<hover::TextHoverOut>()
+            .register_type::<CosmicTextChanged>();
 
         #[cfg(target_arch = "wasm32")]
         {
-            let (tx, rx) = crossbeam_channel::bounded::<WasmPaste>(1);
-            app.insert_resource(WasmPasteAsyncChannel { tx, rx })
-                .add_systems(Update, poll_wasm_paste);
+            let (tx, rx) = crossbeam_channel::bounded::<clipboard::WasmPaste>(1);
+            app.insert_resource(clipboard::WasmPasteAsyncChannel { tx, rx })
+                .add_systems(Update, clipboard::poll_wasm_paste);
         }
     }
 }
 
-/// Timer for double / triple clicks
-#[derive(Resource)]
-pub(crate) struct ClickTimer(pub(crate) Timer);
-
-// TODO: hide this behind #cfg wasm, depends on wasm having own copy/paste fn
-/// Crossbeam channel struct for Wasm clipboard data
-#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
-pub(crate) struct WasmPaste {
-    text: String,
-    entity: Entity,
-}
-
-/// Async channel for receiving from the clipboard in Wasm
-#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
-#[derive(Resource)]
-pub(crate) struct WasmPasteAsyncChannel {
-    pub tx: crossbeam_channel::Sender<WasmPaste>,
-    pub rx: crossbeam_channel::Receiver<WasmPaste>,
+/// Text change events
+///
+/// Sent when text is changed in a cosmic buffer
+/// Contains the entity on which the text was changed, and the new text as a [`String`]
+#[derive(Event, Reflect, Debug)]
+pub struct CosmicTextChanged(pub (Entity, String));
+
+/// First variant is least important, last is most important
+#[derive(Component, Default, Debug)]
+#[require(ScrollEnabled)]
+#[component(on_add = add_event_handlers)]
+pub(crate) enum InputState {
+    #[default]
+    Idle,
+    Hovering,
+    Dragging {
+        initial_buffer_coord: Vec2,
+    },
 }
 
-pub(crate) fn input_mouse(
-    windows: Query<&Window, With<PrimaryWindow>>,
-    active_editor: Res<FocusedWidget>,
-    keys: Res<ButtonInput<KeyCode>>,
-    buttons: Res<ButtonInput<MouseButton>>,
-    mut editor_q: Query<(
-        &mut CosmicEditor,
-        &GlobalTransform,
-        &CosmicTextAlign,
-        &XOffset,
-        &ScrollEnabled,
-        CosmicWidgetSize,
-    )>,
-    mut font_system: ResMut<CosmicFontSystem>,
-    mut scroll_evr: EventReader<MouseWheel>,
-    camera_q: Query<(&Camera, &GlobalTransform)>,
-    mut click_timer: ResMut<ClickTimer>,
-    mut click_count: Local<usize>,
-    time: Res<Time>,
-    evr_mouse_motion: EventReader<MouseMotion>,
+fn add_event_handlers(
+    mut world: DeferredWorld,
+    targeted_entity: Entity,
+    _component_id: ComponentId,
 ) {
-    // handle click timer and click_count
-    click_timer.0.tick(time.delta());
-
-    if click_timer.0.finished() || !evr_mouse_motion.is_empty() {
-        *click_count = 0;
-    }
-
-    if buttons.just_pressed(MouseButton::Left) {
-        click_timer.0.reset();
-        *click_count += 1;
-    }
-
-    if *click_count > 3 {
-        *click_count = 0;
-    }
-
-    // unwrap resources
-    let Some(active_editor_entity) = active_editor.0 else {
-        return;
-    };
-
-    let Ok(primary_window) = windows.get_single() else {
-        return;
-    };
-
-    let scale_factor = primary_window.scale_factor();
-    let Some((camera, camera_transform)) = camera_q.iter().find(|(c, _)| c.is_active) else {
-        return;
-    };
-
-    // TODO: generalize this over UI and sprite
-    if let Ok((mut editor, transform, text_position, x_offset, scroll_disabled, target_size)) =
-        editor_q.get_mut(active_editor_entity)
-    {
-        let buffer = editor.with_buffer(|b| b.clone());
-
-        // get size of render target
-        let Ok(source_type) = target_size.scan() else {
-            return;
-        };
-        let Ok(size) = target_size.logical_size() else {
-            return;
-        };
-        let (width, height) = (size.x, size.y);
-
-        let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
-
-        // if shift key is pressed
-        let already_has_selection = editor.selection() != Selection::None;
-        if shift && !already_has_selection {
-            let cursor = editor.cursor();
-            editor.set_selection(Selection::Normal(cursor));
-        }
-
-        let (padding_x, padding_y) = match text_position {
-            CosmicTextAlign::Center { padding: _ } => (
-                get_x_offset_center(width * scale_factor, &buffer),
-                get_y_offset_center(height * scale_factor, &buffer),
-            ),
-            CosmicTextAlign::TopLeft { padding } => (*padding, *padding),
-            CosmicTextAlign::Left { padding } => (
-                *padding,
-                get_y_offset_center(height * scale_factor, &buffer),
-            ),
-        };
-        // Converts a node-relative space coordinate to a screen space physical coord
-        let screen_physical = |node_cursor_pos: Vec2| {
-            (
-                (node_cursor_pos.x * scale_factor) as i32 - padding_x,
-                (node_cursor_pos.y * scale_factor) as i32 - padding_y,
-            )
-        };
-
-        if buttons.just_pressed(MouseButton::Left) {
-            editor.cursor_visible = true;
-            editor.cursor_timer.reset();
-
-            if let Some(node_cursor_pos) = crate::render_targets::get_node_cursor_pos(
-                primary_window,
-                transform,
-                Vec2::new(width, height),
-                source_type,
-                camera,
-                camera_transform,
-            ) {
-                let (mut x, y) = screen_physical(node_cursor_pos);
-                x += x_offset.left as i32;
-                if shift {
-                    editor.action(&mut font_system.0, Action::Drag { x, y });
-                } else {
-                    match *click_count {
-                        1 => {
-                            editor.action(&mut font_system.0, Action::Click { x, y });
-                        }
-                        2 => {
-                            // select word
-                            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
-                                .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));
-                        }
-                        _ => {}
-                    }
-                }
-            }
-            return;
-        }
-
-        if buttons.pressed(MouseButton::Left) && *click_count == 0 {
-            if let Some(node_cursor_pos) = crate::render_targets::get_node_cursor_pos(
-                primary_window,
-                transform,
-                Vec2::new(width, height),
-                source_type,
-                camera,
-                camera_transform,
-            ) {
-                let (mut x, y) = screen_physical(node_cursor_pos);
-                x += x_offset.left as i32;
-                if active_editor.is_changed() && !shift {
-                    editor.action(&mut font_system.0, Action::Click { x, y });
-                } else {
-                    editor.action(&mut font_system.0, Action::Drag { x, y });
-                }
-            }
-            return;
-        }
-
-        if scroll_disabled.should_scroll() {
-            for ev in scroll_evr.read() {
-                match ev.unit {
-                    MouseScrollUnit::Line => {
-                        editor.action(
-                            &mut font_system.0,
-                            Action::Scroll {
-                                lines: -ev.y as i32,
-                            },
-                        );
-                    }
-                    MouseScrollUnit::Pixel => {
-                        let line_height = buffer.metrics().line_height;
-                        editor.action(
-                            &mut font_system.0,
-                            Action::Scroll {
-                                lines: -(ev.y / line_height) as i32,
-                            },
-                        );
-                    }
-                }
-            }
-        }
+    let mut observers = [
+        Observer::new(click::handle_focussed_click.pipe(render_implementations::debug_error)),
+        Observer::new(drag::handle_dragstart.pipe(render_implementations::debug_error)),
+        Observer::new(drag::handle_drag_continue),
+        Observer::new(drag::handle_dragend),
+        Observer::new(hover::handle_hover_start),
+        Observer::new(hover::handle_hover_continue),
+        Observer::new(hover::handle_hover_end),
+        Observer::new(cancel::handle_cancel),
+    ];
+    for observer in &mut observers {
+        observer.watch_entity(targeted_entity);
     }
+    world.commands().spawn_batch(observers);
 }
 
-pub(crate) fn kb_move_cursor(
-    active_editor: Res<FocusedWidget>,
-    keys: Res<ButtonInput<KeyCode>>,
-    mut cosmic_edit_query: Query<(&mut CosmicEditor,)>,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    let Some(active_editor_entity) = active_editor.0 else {
-        return;
-    };
-    if let Ok((mut editor,)) = cosmic_edit_query.get_mut(active_editor_entity) {
-        if keys.get_just_pressed().len() != 0 {
-            editor.cursor_visible = true;
-            editor.cursor_timer.reset();
-        }
-
-        let command = keypress_command(&keys);
-
-        #[cfg(target_arch = "wasm32")]
-        let command = if web_sys::window()
-            .unwrap()
-            .navigator()
-            .user_agent()
-            .unwrap_or("NoUA".into())
-            .contains("Macintosh")
-        {
-            keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight])
-        } else {
-            command
-        };
-
-        #[cfg(target_os = "macos")]
-        let option = keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]);
-
-        let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
-
-        // if shift key is pressed
-        let already_has_selection = editor.selection() != Selection::None;
-        if shift && !already_has_selection {
-            let cursor = editor.cursor();
-            editor.set_selection(Selection::Normal(cursor));
-        }
-
-        #[cfg(target_os = "macos")]
-        let should_jump = command && option;
-        #[cfg(not(target_os = "macos"))]
-        let should_jump = command;
-
-        if should_jump && keys.just_pressed(KeyCode::ArrowLeft) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::PreviousWord));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-            return;
-        }
-        if should_jump && keys.just_pressed(KeyCode::ArrowRight) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::NextWord));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-            return;
-        }
-        if should_jump && keys.just_pressed(KeyCode::Home) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::BufferStart));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-            return;
-        }
-        if should_jump && keys.just_pressed(KeyCode::End) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::BufferEnd));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-            return;
-        }
-
-        if keys.just_pressed(KeyCode::ArrowLeft) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::Left));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-            return;
-        }
-        if keys.just_pressed(KeyCode::ArrowRight) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::Right));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-            return;
-        }
-        if keys.just_pressed(KeyCode::ArrowUp) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::Up));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-            return;
-        }
-        if keys.just_pressed(KeyCode::ArrowDown) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::Down));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-            return;
-        }
-        if keys.just_pressed(KeyCode::Escape) {
-            editor.action(&mut font_system.0, Action::Escape);
-        }
-        if command && keys.just_pressed(KeyCode::KeyA) {
-            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,
-            }));
-            return;
-        }
-        if keys.just_pressed(KeyCode::Home) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::Home));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-            return;
-        }
-        if keys.just_pressed(KeyCode::End) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::End));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-            return;
-        }
-        if keys.just_pressed(KeyCode::PageUp) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::PageUp));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-            return;
-        }
-        if keys.just_pressed(KeyCode::PageDown) {
-            editor.action(&mut font_system.0, Action::Motion(Motion::PageDown));
-            if !shift {
-                editor.set_selection(Selection::None);
-            }
-        }
-    }
+// todo: avoid these warnings on ReadOnly
+fn warn_no_editor_on_picking_event(job: &'static str) {
+    debug!(
+        note = "This is a false alarm for ReadOnly buffers",
+        note = "Please only use the `InputState` component on entities with a `CosmicEditor` component",
+        note = "`CosmicEditor` components should be automatically added to focussed `CosmicEditBuffer` entities",
+        "Failed to get editor from picking event while {job}"
+    );
 }
 
-pub(crate) fn kb_input_text(
-    active_editor: Res<FocusedWidget>,
-    keys: Res<ButtonInput<KeyCode>>,
-    mut char_evr: EventReader<KeyboardInput>,
-    mut cosmic_edit_query: Query<(
-        &mut CosmicEditor,
-        &mut CosmicEditBuffer,
-        &MaxLines,
-        &MaxChars,
-        Entity,
-        Option<&ReadOnly>,
-    )>,
-    mut evw_changed: EventWriter<CosmicTextChanged>,
-    mut font_system: ResMut<CosmicFontSystem>,
-    mut is_deleting: Local<bool>,
-) {
-    let Some(active_editor_entity) = active_editor.0 else {
-        return;
-    };
-
-    if let Ok((mut editor, buffer, max_lines, max_chars, entity, readonly_opt)) =
-        cosmic_edit_query.get_mut(active_editor_entity)
-    {
-        let command = keypress_command(&keys);
-        if keys.get_just_pressed().len() != 0 {
-            editor.cursor_visible = true;
-            editor.cursor_timer.reset();
-        }
-        let readonly = readonly_opt.is_some();
-
-        if keys.just_pressed(KeyCode::Backspace) & !readonly {
-            // fix for issue #8
-            let select = editor.selection();
-            match select {
-                Selection::Line(cursor) => {
-                    if editor.cursor().line == cursor.line && editor.cursor().index == cursor.index
-                    {
-                        editor.set_selection(Selection::None);
-                    }
-                }
-                Selection::Normal(cursor) => {
-                    if editor.cursor().line == cursor.line && editor.cursor().index == cursor.index
-                    {
-                        editor.set_selection(Selection::None);
-                    }
-                }
-                Selection::Word(cursor) => {
-                    if editor.cursor().line == cursor.line && editor.cursor().index == cursor.index
-                    {
-                        editor.set_selection(Selection::None);
-                    }
-                }
-                Selection::None => {}
-            }
-
-            *is_deleting = true;
-        }
-
-        if keys.just_released(KeyCode::Backspace) {
-            *is_deleting = false;
-        }
-        if keys.just_pressed(KeyCode::Delete) && !readonly {
-            editor.action(&mut font_system.0, Action::Delete);
-            editor.with_buffer_mut(|b| b.set_redraw(true));
-        }
-
-        if readonly {
-            return;
-        }
-
-        let mut is_edit = false;
-        let mut is_return = false;
-        if keys.just_pressed(KeyCode::Enter) {
-            is_return = true;
-            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.action(&mut font_system.0, Action::Insert('\n'));
-            }
-        }
+pub mod cancel {
+    use crate::prelude::*;
 
-        if !is_return {
-            for char_ev in char_evr.read() {
-                is_edit = true;
-                if *is_deleting {
-                    editor.action(&mut font_system.0, Action::Backspace);
-                } else if !command
-                    && (max_chars.0 == 0 || buffer.get_text().len() < max_chars.0)
-                    && matches!(char_ev.state, bevy::input::ButtonState::Pressed)
-                {
-                    match &char_ev.logical_key {
-                        Key::Character(char) => {
-                            let b = char.as_bytes();
-                            for c in b {
-                                let c: char = (*c).into();
-                                editor.action(&mut font_system.0, Action::Insert(c));
-                            }
-                        }
-                        Key::Space => {
-                            editor.action(&mut font_system.0, Action::Insert(' '));
-                        }
-                        _ => (),
-                    }
-                }
-            }
-        }
+    use super::{warn_no_editor_on_picking_event, InputState};
 
-        if !is_edit {
-            return;
+    impl InputState {
+        /// `Cancel` event handler
+        pub fn cancel(&mut self) {
+            trace!("Cancelling a pointer");
+            *self = InputState::Idle;
         }
-
-        evw_changed.send(CosmicTextChanged((
-            entity,
-            editor.with_buffer_mut(|b| b.get_text()),
-        )));
     }
-}
 
-pub(crate) fn kb_clipboard(
-    active_editor: Res<FocusedWidget>,
-    keys: Res<ButtonInput<KeyCode>>,
-    mut evw_changed: EventWriter<CosmicTextChanged>,
-    mut font_system: ResMut<CosmicFontSystem>,
-    mut cosmic_edit_query: Query<(
-        &mut CosmicEditor,
-        &mut CosmicEditBuffer,
-        &MaxLines,
-        &MaxChars,
-        Entity,
-        Option<&ReadOnly>,
-    )>,
-    _channel: Option<Res<WasmPasteAsyncChannel>>,
-) {
-    let Some(active_editor_entity) = active_editor.0 else {
-        return;
-    };
-
-    if let Ok((mut editor, buffer, max_lines, max_chars, entity, readonly_opt)) =
-        cosmic_edit_query.get_mut(active_editor_entity)
-    {
-        let command = keypress_command(&keys);
-
-        let readonly = readonly_opt.is_some();
-
-        let mut is_clipboard = false;
-        #[cfg(not(target_arch = "wasm32"))]
-        {
-            if let Ok(mut clipboard) = arboard::Clipboard::new() {
-                if command && keys.just_pressed(KeyCode::KeyC) {
-                    if let Some(text) = editor.copy_selection() {
-                        clipboard.set_text(text).unwrap();
-                        return;
-                    }
-                }
-                if command && keys.just_pressed(KeyCode::KeyX) && !readonly {
-                    if let Some(text) = editor.copy_selection() {
-                        clipboard.set_text(text).unwrap();
-                        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 || buffer.get_text().len() < max_chars.0 {
-                                if c == 0xA as char {
-                                    if max_lines.0 == 0 || buffer.lines.len() < max_lines.0 {
-                                        editor.action(&mut font_system.0, Action::Insert(c));
-                                    }
-                                } else {
-                                    editor.action(&mut font_system.0, Action::Insert(c));
-                                }
-                            }
-                        }
-                    }
-                    is_clipboard = true;
-                }
-            }
-        }
-
-        #[cfg(target_arch = "wasm32")]
-        {
-            if command && keys.just_pressed(KeyCode::KeyC) {
-                if let Some(text) = editor.copy_selection() {
-                    write_clipboard_wasm(text.as_str());
-                    return;
-                }
-            }
-
-            if command && keys.just_pressed(KeyCode::KeyX) && !readonly {
-                if let Some(text) = editor.copy_selection() {
-                    write_clipboard_wasm(text.as_str());
-                    editor.delete_selection();
-                }
-                is_clipboard = true;
-            }
-            if command && keys.just_pressed(KeyCode::KeyV) && !readonly {
-                let tx = _channel.unwrap().tx.clone();
-                let _task = AsyncComputeTaskPool::get().spawn(async move {
-                    let promise = read_clipboard_wasm();
-
-                    let result = JsFuture::from(promise).await;
-
-                    if let Ok(js_text) = result {
-                        if let Some(text) = js_text.as_string() {
-                            let _ = tx.try_send(WasmPaste { text, entity });
-                        }
-                    }
-                });
-
-                return;
-            }
-        }
-
-        if !is_clipboard {
+    pub(super) fn handle_cancel(
+        trigger: Trigger<Pointer<Cancel>>,
+        mut editor: Query<&mut InputState, With<CosmicEditBuffer>>,
+    ) {
+        let Ok(mut input_state) = editor.get_mut(trigger.target) else {
+            warn_no_editor_on_picking_event("handling cursor `Cancel` event");
             return;
-        }
-
-        evw_changed.send(CosmicTextChanged((entity, buffer.get_text())));
-    }
-}
-
-fn keypress_command(keys: &ButtonInput<KeyCode>) -> bool {
-    #[cfg(target_os = "macos")]
-    let command = keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight]);
-
-    #[cfg(not(target_os = "macos"))]
-    let command = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
-
-    #[cfg(target_arch = "wasm32")]
-    let command = if web_sys::window()
-        .unwrap()
-        .navigator()
-        .user_agent()
-        .unwrap_or("NoUA".into())
-        .contains("Macintosh")
-    {
-        keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight])
-    } else {
-        command
-    };
-
-    command
-}
-
-#[cfg(target_arch = "wasm32")]
-#[wasm_bindgen]
-pub fn write_clipboard_wasm(text: &str) {
-    let clipboard = web_sys::window().unwrap().navigator().clipboard();
-    let _result = clipboard.write_text(text);
-}
-
-#[cfg(target_arch = "wasm32")]
-#[wasm_bindgen]
-pub fn read_clipboard_wasm() -> Promise {
-    let clipboard = web_sys::window().unwrap().navigator().clipboard();
-    clipboard.read_text()
-}
-
-#[cfg(target_arch = "wasm32")]
-pub(crate) fn poll_wasm_paste(
-    channel: Res<WasmPasteAsyncChannel>,
-    mut editor_q: Query<
-        (
-            &mut CosmicEditor,
-            &mut CosmicEditBuffer,
-            &MaxChars,
-            &MaxChars,
-        ),
-        Without<ReadOnly>,
-    >,
-    mut evw_changed: EventWriter<CosmicTextChanged>,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    let inlet = channel.rx.try_recv();
-    match inlet {
-        Ok(inlet) => {
-            let entity = inlet.entity;
-            if let Ok((mut editor, buffer, max_chars, max_lines)) = editor_q.get_mut(entity) {
-                let text = inlet.text;
-                for c in text.chars() {
-                    if max_chars.0 == 0 || buffer.get_text().len() < max_chars.0 {
-                        if c == 0xA as char {
-                            if max_lines.0 == 0 || buffer.lines.len() < max_lines.0 {
-                                editor.action(&mut font_system.0, Action::Insert(c));
-                            }
-                        } else {
-                            editor.action(&mut font_system.0, Action::Insert(c));
-                        }
-                    }
-                }
+        };
 
-                evw_changed.send(CosmicTextChanged((entity, buffer.get_text())));
-            }
-        }
-        Err(_) => {}
+        input_state.cancel();
     }
 }
diff --git a/src/input/click.rs b/src/input/click.rs
new file mode 100644
index 0000000..c056212
--- /dev/null
+++ b/src/input/click.rs
@@ -0,0 +1,144 @@
+use crate::{
+    double_click::{ClickCount, ClickState},
+    prelude::*,
+};
+
+use super::InputState;
+use cosmic_text::{Action, Motion, Selection};
+use render_implementations::{RelativeQuery, RenderTargetError, RenderTypeScan};
+
+impl InputState {
+    /// Handler for [`Click`] event
+    pub fn handle_click(&self) {
+        trace!("Clicked");
+        match self {
+            InputState::Idle | InputState::Hovering => {}
+            InputState::Dragging { .. } => {
+                // warn!(
+                //     message = "Click event received while dragging",
+                //     state = ?self,
+                // )
+            }
+        }
+    }
+
+    /// Should only [`Action::Click`] when not already dragging
+    pub fn should_click(&self) -> bool {
+        !matches!(self, InputState::Dragging { .. })
+    }
+}
+
+/// An [`Observer`] that focuses on the desired editor when clicked
+pub fn focus_on_click(
+    trigger: Trigger<Pointer<Click>>,
+    mut focused: ResMut<FocusedWidget>,
+    editor_confirmation: Query<RenderTypeScan, With<CosmicEditBuffer>>,
+) {
+    let Ok(scan) = editor_confirmation.get(trigger.target) else {
+        warn!(
+            "An entity with the `focus_on_click` observer added was clicked, but didn't have a `CosmicEditBuffer` component",
+        );
+        return;
+    };
+
+    match scan.confirm_conformance() {
+        Ok(_) => {
+            focused.0 = Some(trigger.target);
+        }
+        Err(RenderTargetError::NoTargetsAvailable) => {
+            warn!("Please use a high-level driver component from `bevy_cosmic_edit::render_implementations` to add the `CosmicEditBuffer` component, e.g. `TextEdit` or `TextEdit2d`");
+        }
+        Err(err) => {
+            warn!(message = "For some reason, the entity that `focus_on_click` was triggered for isn't a valid `CosmicEditor`", ?err);
+            // render_implementations::debug_error::<()>(In(Err(err)));
+        }
+    }
+}
+
+/// Handles [`CosmicEditor`] widgets that are already focussed
+pub(super) fn handle_focussed_click(
+    trigger: Trigger<Pointer<Click>>,
+    focused: Res<FocusedWidget>,
+    mut editor: Query<(&mut InputState, &mut CosmicEditor, RelativeQuery)>,
+    mut font_system: ResMut<CosmicFontSystem>,
+    buttons: Res<ButtonInput<KeyCode>>,
+    mut click_state: ClickState,
+) -> render_implementations::Result<()> {
+    let font_system = &mut font_system.0;
+    let target = trigger.target;
+    let click = trigger.event();
+
+    // must be focused
+    if focused.0 != Some(target) {
+        return Ok(());
+    }
+
+    if click.button != PointerButton::Primary {
+        return Ok(());
+    }
+
+    let Ok((input_state, mut editor, buffer_relative)) = editor.get_mut(target) else {
+        // this is expected on first click, idk order of observers
+        // warn_no_editor_on_picking_event("handling focussed cursor `Click` event");
+        return Ok(());
+    };
+    let mut editor = editor.borrow_with(font_system);
+    input_state.handle_click();
+
+    let buffer_coord = buffer_relative.compute_buffer_coord(&click.hit, editor.expected_size())?;
+
+    if !input_state.should_click() {
+        return Ok(());
+    }
+
+    match click_state.feed_click() {
+        ClickCount::Single => {
+            let shift_pressed = buttons.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
+
+            if shift_pressed {
+                editor.action(Action::Drag {
+                    x: buffer_coord.x as i32,
+                    y: buffer_coord.y as i32,
+                });
+            } else {
+                editor.action(Action::Click {
+                    x: buffer_coord.x as i32,
+                    y: buffer_coord.y as i32,
+                });
+            }
+        }
+        ClickCount::Double => {
+            // selects word
+            editor.action(Action::DoubleClick {
+                x: buffer_coord.x as i32,
+                y: buffer_coord.y as i32,
+            });
+            // // select word
+            // editor.action(Action::Motion(Motion::LeftWord));
+            // let cursor = editor.cursor();
+            // editor.set_selection(Selection::Normal(cursor));
+            // editor.action(Action::Motion(Motion::RightWord));
+        }
+        ClickCount::Triple => {
+            // selects line
+            editor.action(Action::TripleClick {
+                x: buffer_coord.x as i32,
+                y: buffer_coord.y as i32,
+            });
+            // // select paragraph
+            // editor.action(Action::Motion(Motion::ParagraphStart));
+            // let cursor = editor.cursor();
+            // editor.set_selection(Selection::Normal(cursor));
+            // editor.action(Action::Motion(Motion::ParagraphEnd));
+        }
+        ClickCount::MoreThanTriple => {
+            // select all
+            editor.action(Action::Motion(Motion::BufferStart));
+            let cursor = editor.cursor();
+            editor.set_selection(Selection::Normal(cursor));
+            editor.action(Action::Motion(Motion::BufferEnd));
+        }
+    }
+
+    Ok(())
+}
diff --git a/src/input/clipboard.rs b/src/input/clipboard.rs
new file mode 100644
index 0000000..fbfe6a8
--- /dev/null
+++ b/src/input/clipboard.rs
@@ -0,0 +1,182 @@
+use crate::{input::CosmicTextChanged, prelude::*, MaxChars, MaxLines};
+
+#[cfg(target_arch = "wasm32")]
+use bevy::tasks::AsyncComputeTaskPool;
+use cosmic_text::{Action, Edit};
+#[cfg(target_arch = "wasm32")]
+#[allow(unused_imports)]
+use js_sys::Promise;
+#[cfg(target_arch = "wasm32")]
+use wasm_bindgen::prelude::*;
+#[cfg(target_arch = "wasm32")]
+use wasm_bindgen_futures::JsFuture;
+
+// TODO: hide this behind #cfg wasm, depends on wasm having own copy/paste fn
+/// Crossbeam channel struct for Wasm clipboard data
+#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
+pub(crate) struct WasmPaste {
+    text: String,
+    entity: Entity,
+}
+
+/// Async channel for receiving from the clipboard in Wasm
+#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
+#[derive(Resource)]
+pub(crate) struct WasmPasteAsyncChannel {
+    pub tx: crossbeam_channel::Sender<WasmPaste>,
+    pub rx: crossbeam_channel::Receiver<WasmPaste>,
+}
+
+pub(crate) fn kb_clipboard(
+    active_editor: Res<FocusedWidget>,
+    keys: Res<ButtonInput<KeyCode>>,
+    mut evw_changed: EventWriter<CosmicTextChanged>,
+    #[allow(unused_variables, unused_mut)] mut font_system: ResMut<CosmicFontSystem>,
+    mut cosmic_edit_query: Query<(
+        &mut CosmicEditor,
+        &MaxLines,
+        &MaxChars,
+        Entity,
+        Option<&ReadOnly>,
+    )>,
+    _channel: Option<Res<WasmPasteAsyncChannel>>,
+) {
+    let Some(active_editor_entity) = active_editor.0 else {
+        return;
+    };
+
+    #[allow(unused_variables)]
+    if let Ok((mut editor, max_lines, max_chars, entity, readonly_opt)) =
+        cosmic_edit_query.get_mut(active_editor_entity)
+    {
+        let command = crate::input::keyboard::keypress_command(&keys);
+
+        let readonly = readonly_opt.is_some();
+
+        let mut is_clipboard = false;
+        #[cfg(not(target_arch = "wasm32"))]
+        {
+            if let Ok(mut clipboard) = arboard::Clipboard::new() {
+                if command && keys.just_pressed(KeyCode::KeyC) {
+                    if let Some(text) = editor.copy_selection() {
+                        clipboard.set_text(text).unwrap();
+                        return;
+                    }
+                }
+                if command && keys.just_pressed(KeyCode::KeyX) && !readonly {
+                    if let Some(text) = editor.copy_selection() {
+                        clipboard.set_text(text).unwrap();
+                        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 c == 0xA as char {
+                                    if max_lines.0 == 0
+                                        || editor.with_buffer(|b| b.lines.len()) < max_lines.0
+                                    {
+                                        editor.action(&mut font_system.0, Action::Insert(c));
+                                    }
+                                } else {
+                                    editor.action(&mut font_system.0, Action::Insert(c));
+                                }
+                            }
+                        }
+                    }
+                    is_clipboard = true;
+                }
+            }
+        }
+
+        #[cfg(target_arch = "wasm32")]
+        {
+            if command && keys.just_pressed(KeyCode::KeyC) {
+                if let Some(text) = editor.copy_selection() {
+                    write_clipboard_wasm(text.as_str());
+                    return;
+                }
+            }
+
+            if command && keys.just_pressed(KeyCode::KeyX) && !readonly {
+                if let Some(text) = editor.copy_selection() {
+                    write_clipboard_wasm(text.as_str());
+                    editor.delete_selection();
+                }
+                is_clipboard = true;
+            }
+            if command && keys.just_pressed(KeyCode::KeyV) && !readonly {
+                let tx = _channel.unwrap().tx.clone();
+                let _task = AsyncComputeTaskPool::get().spawn(async move {
+                    let promise = read_clipboard_wasm();
+
+                    let result = JsFuture::from(promise).await;
+
+                    if let Ok(js_text) = result {
+                        if let Some(text) = js_text.as_string() {
+                            let _ = tx.try_send(WasmPaste { text, entity });
+                        }
+                    }
+                });
+
+                return;
+            }
+        }
+
+        if !is_clipboard {
+            return;
+        }
+
+        evw_changed.send(CosmicTextChanged((entity, editor.get_text())));
+    }
+}
+
+#[cfg(target_arch = "wasm32")]
+#[wasm_bindgen]
+pub fn write_clipboard_wasm(text: &str) {
+    let clipboard = web_sys::window().unwrap().navigator().clipboard();
+    let _result = clipboard.write_text(text);
+}
+
+#[cfg(target_arch = "wasm32")]
+#[wasm_bindgen]
+pub fn read_clipboard_wasm() -> Promise {
+    let clipboard = web_sys::window().unwrap().navigator().clipboard();
+    clipboard.read_text()
+}
+
+#[cfg(target_arch = "wasm32")]
+pub(crate) fn poll_wasm_paste(
+    channel: Res<WasmPasteAsyncChannel>,
+    mut editor_q: Query<(&mut CosmicEditor, &MaxChars, &MaxChars), Without<ReadOnly>>,
+    mut evw_changed: EventWriter<CosmicTextChanged>,
+    mut font_system: ResMut<CosmicFontSystem>,
+) {
+    let inlet = channel.rx.try_recv();
+    match inlet {
+        Ok(inlet) => {
+            let entity = inlet.entity;
+            if let Ok((mut editor, max_chars, max_lines)) = editor_q.get_mut(entity) {
+                let text = inlet.text;
+                for c in text.chars() {
+                    if max_chars.0 == 0 || editor.get_text().len() < max_chars.0 {
+                        if c == 0xA as char {
+                            if max_lines.0 == 0
+                                || editor.with_buffer(|b| b.lines.len()) < max_lines.0
+                            {
+                                editor.action(&mut font_system.0, Action::Insert(c));
+                            }
+                        } else {
+                            editor.action(&mut font_system.0, Action::Insert(c));
+                        }
+                    }
+                }
+
+                evw_changed.send(CosmicTextChanged((entity, editor.get_text())));
+            }
+        }
+        Err(_) => {}
+    }
+}
diff --git a/src/input/cursor_icon.rs b/src/input/cursor_icon.rs
new file mode 100644
index 0000000..fa05f3b
--- /dev/null
+++ b/src/input/cursor_icon.rs
@@ -0,0 +1,96 @@
+use bevy::{
+    ecs::system::SystemParam,
+    window::{PrimaryWindow, SystemCursorIcon},
+    winit::cursor::CursorIcon,
+};
+
+use crate::prelude::*;
+
+use super::{hover::HoverCursor, InputState};
+
+#[derive(SystemParam)]
+pub(crate) struct CursorIconUpdate<'w, 's> {
+    window: Single<'w, Entity, With<PrimaryWindow>>,
+    commands: Commands<'w, 's>,
+}
+
+impl CursorIconUpdate<'_, '_> {
+    pub fn set_cursor(&mut self, icon: CursorIcon) {
+        // trace!(message = "Setting window icon", ?icon);
+        self.commands.entity(*self.window).insert(icon);
+    }
+
+    /// Resets to default
+    pub fn reset_cursor(&mut self) {
+        // could also set to default value
+        // trace!("Resetting window icon");
+        self.commands.entity(*self.window).remove::<CursorIcon>();
+    }
+}
+
+enum GlobalCursorState {
+    Nothing,
+    BufferHovered(CursorIcon),
+    FocussedEditorDragging,
+}
+
+impl GlobalCursorState {
+    pub fn account_for_hovered_buffer(&mut self, hover_cursor: CursorIcon) {
+        match self {
+            GlobalCursorState::Nothing => *self = GlobalCursorState::BufferHovered(hover_cursor),
+            GlobalCursorState::BufferHovered(_) => {
+                warn_once!(
+                    message = "Multiple buffers hovered at the same time",
+                    note = "What to do in this case is not yet implemented"
+                );
+            }
+            GlobalCursorState::FocussedEditorDragging => {}
+        }
+    }
+
+    pub fn account_for_dragging_focussed_editor(&mut self) {
+        *self = GlobalCursorState::FocussedEditorDragging;
+    }
+
+    /// `None` indicates use the default icon
+    pub fn decide_on_icon(self) -> Option<CursorIcon> {
+        match self {
+            GlobalCursorState::Nothing => None,
+            GlobalCursorState::BufferHovered(icon) => Some(icon),
+            GlobalCursorState::FocussedEditorDragging => {
+                Some(CursorIcon::System(SystemCursorIcon::Text))
+            }
+        }
+    }
+}
+
+/// Doesn't take into account [`crate::UserSelectNone`] or [`crate::ReadOnly`]
+pub(super) fn update_cursor_icon(
+    editors: Query<(&InputState, &HoverCursor, Entity, Has<CosmicEditor>), With<CosmicEditBuffer>>,
+    focused_widget: Res<FocusedWidget>,
+    mut cursor_icon: CursorIconUpdate,
+) {
+    // if an editor is being hovered, prioritize its hover cursor
+    // else, reset to default
+    let mut cursor_state = GlobalCursorState::Nothing;
+    for (input_state, hover_cursor, buffer_entity, is_editor) in editors.iter() {
+        match *input_state {
+            InputState::Hovering => {
+                cursor_state.account_for_hovered_buffer(hover_cursor.0.clone());
+            }
+            InputState::Idle => {}
+            InputState::Dragging { .. } => {
+                if is_editor && focused_widget.0 == Some(buffer_entity) {
+                    cursor_state.account_for_dragging_focussed_editor();
+                }
+                // only a readonly non-editor buffer could be dragged,
+                // this ignores such a case
+            }
+        }
+    }
+
+    match cursor_state.decide_on_icon() {
+        Some(icon) => cursor_icon.set_cursor(icon),
+        None => cursor_icon.reset_cursor(),
+    }
+}
diff --git a/src/input/cursor_visibility.rs b/src/input/cursor_visibility.rs
new file mode 100644
index 0000000..ee23f30
--- /dev/null
+++ b/src/input/cursor_visibility.rs
@@ -0,0 +1,48 @@
+//! Manages the OS-level cursor aka mouse pointer visibility
+
+use bevy::input::mouse::MouseMotion;
+use bevy::{ecs::system::SystemParam, window::PrimaryWindow};
+
+use crate::prelude::*;
+
+use crate::input::CosmicTextChanged;
+
+// if text changedd
+// if !evr_text_changed.is_empty() {
+//     window.cursor_options.visible = false;
+// }
+
+// if pressing or moving mouse
+// if mouse_buttons.get_just_pressed().len() != 0 || !evr_mouse_motion.is_empty() {
+//     window.cursor_options.visible = true;
+// }
+//
+
+#[derive(SystemParam)]
+pub(crate) struct CursorVisibility<'w> {
+    window: Single<'w, &'static mut Window, With<PrimaryWindow>>,
+}
+
+impl CursorVisibility<'_> {
+    pub fn set_cursor_visibility(&mut self, visible: bool) {
+        self.window.cursor_options.visible = visible;
+    }
+}
+
+pub(super) fn update_cursor_visibility(
+    editors_text_changed: EventReader<CosmicTextChanged>,
+    mouse_moved: EventReader<MouseMotion>,
+    mouse_clicked: Res<ButtonInput<MouseButton>>,
+    mut cursor_visibility: CursorVisibility,
+) {
+    let text_changed_at_all = !editors_text_changed.is_empty();
+    if text_changed_at_all {
+        cursor_visibility.set_cursor_visibility(false);
+    }
+
+    let mouse_moved_at_all = !mouse_moved.is_empty();
+    let mouse_clicked_at_all = mouse_clicked.get_just_pressed().len() != 0;
+    if mouse_moved_at_all || mouse_clicked_at_all {
+        cursor_visibility.set_cursor_visibility(true);
+    }
+}
diff --git a/src/input/drag.rs b/src/input/drag.rs
new file mode 100644
index 0000000..0256a68
--- /dev/null
+++ b/src/input/drag.rs
@@ -0,0 +1,148 @@
+use crate::prelude::*;
+
+use super::{warn_no_editor_on_picking_event, InputState};
+use cosmic_text::Action;
+use render_implementations::RelativeQuery;
+
+impl InputState {
+    pub fn is_dragging(&self) -> bool {
+        matches!(self, InputState::Dragging { .. })
+    }
+
+    /// Handler for [`DragStart`] event
+    pub fn start_dragging(&mut self, initial_buffer_coord: Vec2) {
+        trace!("Starting a drag");
+        match self {
+            InputState::Idle | InputState::Hovering => {
+                *self = InputState::Dragging {
+                    initial_buffer_coord,
+                };
+            }
+            InputState::Dragging { .. } => {
+                // warn!(
+                //     message = "Somehow, a `DragStart` event was received before a previous `DragStart` event was ended with a `DragEnd`",
+                //     note = "Ignoring",
+                // );
+            }
+        }
+    }
+
+    /// Handler for [`Move`]
+    pub fn continue_dragging(&self) {
+        match self {
+            InputState::Dragging { .. } => {}
+            InputState::Idle | InputState::Hovering => {
+                // warn!(
+                //     message = "Somehow, a `Move` event was received before a previous `DragStart` event was received",
+                //     note = "Ignoring",
+                // );
+            }
+        }
+    }
+
+    /// Handler for [`Out`] event
+    pub fn end_dragging(&mut self) {
+        trace!("Ending drag");
+        match self {
+            InputState::Dragging { .. } => {
+                *self = InputState::Idle;
+            }
+            InputState::Idle | InputState::Hovering => {
+                // warn!(
+                //     message = "Somehow, a `DragEnd` event was received before a previous `DragStart` event was received",
+                //     note = "Ignoring",
+                // );
+            }
+        }
+    }
+}
+
+pub(super) fn handle_dragstart(
+    trigger: Trigger<Pointer<DragStart>>,
+    mut editor: Query<(&mut InputState, &mut CosmicEditor, RelativeQuery), With<CosmicEditBuffer>>,
+    mut font_system: ResMut<CosmicFontSystem>,
+) -> render_implementations::Result<()> {
+    let font_system = &mut font_system.0;
+    let event = trigger.event();
+    let Ok((mut input_state, mut editor, sprite_relative)) = editor.get_mut(trigger.target) else {
+        warn_no_editor_on_picking_event("handling cursor `DragStart` event");
+        return Ok(());
+    };
+    let buffer_size = editor.with_buffer_mut(|b| b.borrow_with(font_system).expected_size());
+    let buffer_coord = sprite_relative.compute_buffer_coord(&event.hit, buffer_size)?;
+    let mut editor = editor.borrow_with(font_system);
+
+    if event.button != PointerButton::Primary {
+        return Ok(());
+    }
+
+    input_state.start_dragging(buffer_coord);
+
+    if input_state.is_dragging() {
+        editor.action(Action::Click {
+            x: buffer_coord.x as i32,
+            y: buffer_coord.y as i32,
+        });
+        editor.action(Action::Drag {
+            x: buffer_coord.x as i32,
+            y: buffer_coord.y as i32,
+        });
+    }
+
+    Ok(())
+}
+
+pub(super) fn handle_drag_continue(
+    trigger: Trigger<Pointer<Drag>>,
+    mut editor: Query<(&InputState, &mut CosmicEditor)>,
+    mut font_system: ResMut<CosmicFontSystem>,
+) {
+    let font_system = &mut font_system.0;
+    let event = &trigger.event;
+    let entity = trigger.target;
+
+    if event.button != PointerButton::Primary {
+        return;
+    }
+
+    let Ok((input_state, mut editor)) = editor.get_mut(entity) else {
+        warn_no_editor_on_picking_event("handling cursor `Drag` event");
+        return;
+    };
+
+    input_state.continue_dragging();
+
+    if let InputState::Dragging {
+        initial_buffer_coord,
+    } = *input_state
+    {
+        let new_buffer_coord = initial_buffer_coord + event.distance;
+        editor.action(
+            font_system,
+            Action::Drag {
+                x: new_buffer_coord.x as i32,
+                y: new_buffer_coord.y as i32,
+            },
+        );
+    }
+}
+
+pub(super) fn handle_dragend(
+    trigger: Trigger<Pointer<DragEnd>>,
+    mut editor: Query<&mut InputState, With<CosmicEditBuffer>>,
+) {
+    let event = &trigger.event;
+    let entity = trigger.target;
+
+    if event.button != PointerButton::Primary {
+        return;
+    }
+
+    let Ok(entity_mut) = editor.get_mut(entity) else {
+        warn_no_editor_on_picking_event("handling cursor `DragEnd` event");
+        return;
+    };
+    let input_state = entity_mut.into_inner();
+
+    input_state.end_dragging();
+}
diff --git a/src/input/hover.rs b/src/input/hover.rs
new file mode 100644
index 0000000..44b9788
--- /dev/null
+++ b/src/input/hover.rs
@@ -0,0 +1,106 @@
+use bevy::{window::SystemCursorIcon, winit::cursor::CursorIcon};
+
+use crate::prelude::*;
+
+use super::{warn_no_editor_on_picking_event, InputState};
+
+/// Whenever a pointer enters a widget
+#[derive(Event, Debug, Reflect)]
+pub struct TextHoverIn;
+
+/// Whenever a pointer exits a widget
+#[derive(Event, Debug, Reflect)]
+pub struct TextHoverOut;
+
+/// What cursor icon to show when hovering over a widget
+///
+/// By default is [`CursorIcon::System(SystemCursorIcon::Text)`]
+#[derive(Component, Reflect, Deref)]
+pub struct HoverCursor(pub CursorIcon);
+
+impl Default for HoverCursor {
+    fn default() -> Self {
+        Self(CursorIcon::System(SystemCursorIcon::Text))
+    }
+}
+
+impl InputState {
+    /// `Over` event handler
+    pub fn start_hovering(&mut self) {
+        trace!("Starting hover");
+        match self {
+            InputState::Idle => *self = InputState::Hovering,
+            InputState::Hovering | InputState::Dragging { .. } => {}
+        }
+    }
+
+    pub fn is_hovering(&self) -> bool {
+        matches!(self, InputState::Hovering)
+    }
+
+    /// Handler for [`Move`] event
+    pub fn continue_hovering(&mut self) {
+        match self {
+            InputState::Hovering | InputState::Dragging { .. } => {}
+            InputState::Idle => {
+                // handles that case that a drag is finished
+                *self = InputState::Hovering;
+            }
+        }
+    }
+
+    /// `Out` event handler
+    pub fn end_hovering(&mut self) {
+        trace!("Ending hoverr");
+        match self {
+            InputState::Hovering => *self = InputState::Idle,
+            InputState::Idle | InputState::Dragging { .. } => {}
+        }
+    }
+}
+
+pub(super) fn handle_hover_start(
+    trigger: Trigger<Pointer<Over>>,
+    mut editor: Query<&mut InputState, With<CosmicEditBuffer>>,
+    mut hover_in_evw: EventWriter<TextHoverIn>,
+) {
+    let Ok(mut input_state) = editor.get_mut(trigger.target) else {
+        warn_no_editor_on_picking_event("handling cursor `Over` event");
+        return;
+    };
+
+    input_state.start_hovering();
+
+    if input_state.is_hovering() {
+        hover_in_evw.send(TextHoverIn);
+    }
+}
+
+pub(super) fn handle_hover_continue(
+    trigger: Trigger<Pointer<Move>>,
+    mut editor: Query<&mut InputState, With<CosmicEditBuffer>>,
+) {
+    let Ok(mut input_state) = editor.get_mut(trigger.target) else {
+        warn_no_editor_on_picking_event("handling cursor `Move` event");
+        return;
+    };
+
+    input_state.continue_hovering();
+}
+
+pub(super) fn handle_hover_end(
+    trigger: Trigger<Pointer<Out>>,
+    mut editor: Query<&mut InputState, With<CosmicEditBuffer>>,
+    mut hover_out_evw: EventWriter<TextHoverOut>,
+) {
+    let Ok(mut input_state) = editor.get_mut(trigger.target) else {
+        warn_no_editor_on_picking_event("handling cursor `Out` event");
+        return;
+    };
+
+    input_state.end_hovering();
+
+    if !input_state.is_hovering() {
+        hover_out_evw.send(TextHoverOut);
+    }
+}
diff --git a/src/input/keyboard.rs b/src/input/keyboard.rs
new file mode 100644
index 0000000..87bd065
--- /dev/null
+++ b/src/input/keyboard.rs
@@ -0,0 +1,293 @@
+use bevy::input::keyboard::{Key, KeyboardInput};
+use cosmic_text::{Action, Cursor, Motion, Selection};
+
+use crate::{input::CosmicTextChanged, prelude::*, MaxChars, MaxLines};
+
+pub(super) fn keypress_command(keys: &ButtonInput<KeyCode>) -> bool {
+    #[cfg(target_os = "macos")]
+    let command = keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight]);
+
+    #[cfg(not(target_os = "macos"))]
+    let command = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
+
+    #[cfg(target_arch = "wasm32")]
+    let command = if web_sys::window()
+        .unwrap()
+        .navigator()
+        .user_agent()
+        .unwrap_or("NoUA".into())
+        .contains("Macintosh")
+    {
+        keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight])
+    } else {
+        command
+    };
+
+    command
+}
+
+pub(crate) fn kb_move_cursor(
+    active_editor: Res<FocusedWidget>,
+    keys: Res<ButtonInput<KeyCode>>,
+    mut cosmic_edit_query: Query<(&mut CosmicEditor,)>,
+    mut font_system: ResMut<CosmicFontSystem>,
+) {
+    let Some(active_editor_entity) = active_editor.0 else {
+        return;
+    };
+    if let Ok((mut editor,)) = cosmic_edit_query.get_mut(active_editor_entity) {
+        if keys.get_just_pressed().len() != 0 {
+            editor.cursor_visible = true;
+            editor.cursor_timer.reset();
+        }
+
+        let command = keypress_command(&keys);
+
+        #[cfg(target_arch = "wasm32")]
+        let command = if web_sys::window()
+            .unwrap()
+            .navigator()
+            .user_agent()
+            .unwrap_or("NoUA".into())
+            .contains("Macintosh")
+        {
+            keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight])
+        } else {
+            command
+        };
+
+        #[cfg(target_os = "macos")]
+        let option = keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]);
+
+        let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
+
+        // if shift key is pressed
+        let already_has_selection = editor.selection() != Selection::None;
+        if shift && !already_has_selection {
+            let cursor = editor.cursor();
+            editor.set_selection(Selection::Normal(cursor));
+        }
+
+        #[cfg(target_os = "macos")]
+        let should_jump = command && option;
+        #[cfg(not(target_os = "macos"))]
+        let should_jump = command;
+
+        if should_jump && keys.just_pressed(KeyCode::ArrowLeft) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::PreviousWord));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+            return;
+        }
+        if should_jump && keys.just_pressed(KeyCode::ArrowRight) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::NextWord));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+            return;
+        }
+        if should_jump && keys.just_pressed(KeyCode::Home) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::BufferStart));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+            return;
+        }
+        if should_jump && keys.just_pressed(KeyCode::End) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::BufferEnd));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+            return;
+        }
+
+        if keys.just_pressed(KeyCode::ArrowLeft) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::Left));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::ArrowRight) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::Right));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::ArrowUp) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::Up));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::ArrowDown) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::Down));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::Escape) {
+            editor.action(&mut font_system.0, Action::Escape);
+        }
+        if command && keys.just_pressed(KeyCode::KeyA) {
+            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,
+            }));
+            return;
+        }
+        if keys.just_pressed(KeyCode::Home) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::Home));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::End) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::End));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::PageUp) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::PageUp));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::PageDown) {
+            editor.action(&mut font_system.0, Action::Motion(Motion::PageDown));
+            if !shift {
+                editor.set_selection(Selection::None);
+            }
+        }
+    }
+}
+
+pub(crate) fn kb_input_text(
+    active_editor: Res<FocusedWidget>,
+    keys: Res<ButtonInput<KeyCode>>,
+    mut char_evr: EventReader<KeyboardInput>,
+    mut cosmic_edit_query: Query<(
+        &mut CosmicEditor,
+        &MaxLines,
+        &MaxChars,
+        Entity,
+        Option<&ReadOnly>,
+    )>,
+    mut evw_changed: EventWriter<CosmicTextChanged>,
+    mut font_system: ResMut<CosmicFontSystem>,
+    mut is_deleting: Local<bool>,
+) {
+    let Some(active_editor_entity) = active_editor.0 else {
+        return;
+    };
+
+    if let Ok((mut editor, max_lines, max_chars, entity, readonly_opt)) =
+        cosmic_edit_query.get_mut(active_editor_entity)
+    {
+        let command = keypress_command(&keys);
+        if keys.get_just_pressed().len() != 0 {
+            editor.cursor_visible = true;
+            editor.cursor_timer.reset();
+        }
+        let readonly = readonly_opt.is_some();
+
+        if keys.just_pressed(KeyCode::Backspace) & !readonly {
+            // fix for issue #8
+            let select = editor.selection();
+            match select {
+                Selection::Line(cursor) => {
+                    if editor.cursor().line == cursor.line && editor.cursor().index == cursor.index
+                    {
+                        editor.set_selection(Selection::None);
+                    }
+                }
+                Selection::Normal(cursor) => {
+                    if editor.cursor().line == cursor.line && editor.cursor().index == cursor.index
+                    {
+                        editor.set_selection(Selection::None);
+                    }
+                }
+                Selection::Word(cursor) => {
+                    if editor.cursor().line == cursor.line && editor.cursor().index == cursor.index
+                    {
+                        editor.set_selection(Selection::None);
+                    }
+                }
+                Selection::None => {}
+            }
+
+            *is_deleting = true;
+        }
+
+        if keys.just_released(KeyCode::Backspace) {
+            *is_deleting = false;
+        }
+        if keys.just_pressed(KeyCode::Delete) && !readonly {
+            editor.action(&mut font_system.0, Action::Delete);
+            editor.with_buffer_mut(|b| b.set_redraw(true));
+        }
+
+        if readonly {
+            return;
+        }
+
+        let mut is_edit = false;
+        let mut is_return = false;
+        if keys.just_pressed(KeyCode::Enter) {
+            is_return = true;
+            if (max_lines.0 == 0 || editor.with_buffer(|b| b.lines.len()) < max_lines.0)
+                && (max_chars.0 == 0 || editor.get_text().len() < max_chars.0)
+            {
+                // to have new line on wasm rather than E
+                is_edit = true;
+                editor.action(&mut font_system.0, Action::Insert('\n'));
+            }
+        }
+
+        if !is_return {
+            for char_ev in char_evr.read() {
+                is_edit = true;
+                if *is_deleting {
+                    editor.action(&mut font_system.0, Action::Backspace);
+                } else if !command
+                    && (max_chars.0 == 0 || editor.get_text().len() < max_chars.0)
+                    && matches!(char_ev.state, bevy::input::ButtonState::Pressed)
+                {
+                    match &char_ev.logical_key {
+                        Key::Character(char) => {
+                            let b = char.as_bytes();
+                            for c in b {
+                                let c: char = (*c).into();
+                                editor.action(&mut font_system.0, Action::Insert(c));
+                            }
+                        }
+                        Key::Space => {
+                            editor.action(&mut font_system.0, Action::Insert(' '));
+                        }
+                        _ => (),
+                    }
+                }
+            }
+        }
+
+        if !is_edit {
+            return;
+        }
+
+        evw_changed.send(CosmicTextChanged((
+            entity,
+            editor.with_buffer_mut(|b| b.get_text()),
+        )));
+    }
+}
diff --git a/src/input/scroll.rs b/src/input/scroll.rs
new file mode 100644
index 0000000..5d77adf
--- /dev/null
+++ b/src/input/scroll.rs
@@ -0,0 +1,35 @@
+use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
+use cosmic_text::Action;
+
+use crate::{prelude::*, ScrollEnabled};
+
+pub(crate) fn scroll(
+    mut editor: Query<(&mut CosmicEditor, &ScrollEnabled)>,
+    mut font_system: ResMut<CosmicFontSystem>,
+    mut scroll_evr: EventReader<MouseWheel>,
+) {
+    let font_system = &mut font_system.0;
+    for (mut editor, scroll_enabled) in editor.iter_mut() {
+        let mut editor = editor.borrow_with(font_system);
+
+        if scroll_enabled.should_scroll() {
+            for ev in scroll_evr.read() {
+                match ev.unit {
+                    MouseScrollUnit::Line => {
+                        // trace!(?ev, "Line");
+                        editor.action(Action::Scroll {
+                            lines: -ev.y as i32,
+                        });
+                    }
+                    MouseScrollUnit::Pixel => {
+                        // trace!(?ev, "Pixel");
+                        let line_height = editor.with_buffer(|b| b.metrics().line_height);
+                        editor.action(Action::Scroll {
+                            lines: -(ev.y / line_height) as i32,
+                        });
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 8052432..3af3c29 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -43,71 +43,73 @@
 //! ## Feature flags
 #![doc = document_features::document_features!()]
 //!
+//! ## Implementation details
+//!
+//! See [render_implementations](crate::render_implementations)
+//!
 //! ## License
 //!
 //! MIT or Apache-2.0
 #![allow(clippy::type_complexity)]
 
+pub use bevy::text::cosmic_text;
+pub use primary::*;
+/// Contains the library global important types you probably want to explore first
+mod primary;
+
 pub mod prelude {
-    // external re-exports
+    // non-pub external re-exports
     pub(crate) use bevy::prelude::*;
     pub(crate) use bevy::text::SwashCache;
-    #[cfg_attr(not(doc), allow(unused_imports))]
     pub(crate) use cosmic_text::Buffer;
+    pub(crate) use cosmic_text::Edit as _;
+    #[allow(unused_imports)]
+    pub(crate) use std::ops::{Deref as _, DerefMut as _};
 
-    // internal re-exports
-    pub(crate) use crate::buffer::BufferExtras as _;
+    // non-pub internal re-exports
+    pub(crate) use crate::buffer::{BufferMutExtras as _, BufferRefExtras as _};
     pub(crate) use crate::cosmic_text;
     pub(crate) use crate::primary::CosmicRenderOutput;
+    pub(crate) use crate::render_implementations;
     pub(crate) use crate::utils::*;
 
     // public internal re-exports
     pub use crate::buffer::CosmicEditBuffer; // todo: migrate to builtin bevy CosmicBuffer
     pub use crate::cosmic_edit::CosmicFontSystem; // todo: migrate to using builtin bevy cosmic font system
-    pub use crate::cosmic_edit::{CosmicEditor, DefaultAttrs, ReadOnly};
+    pub use crate::cosmic_edit::{CosmicWrap, DefaultAttrs, ReadOnly};
+    pub use crate::cosmic_text::{Color as CosmicColor, Style as FontStyle, Weight as FontWeight};
+    pub use crate::editor::CosmicEditor;
+    pub use crate::editor_buffer::EditorBuffer;
     pub use crate::focus::FocusedWidget;
-    pub use crate::primary::{CosmicEditPlugin, CosmicFontConfig, CosmicPrimaryCamera};
-    pub use crate::render_targets::{TextEdit, TextEdit2d};
-    pub use crate::utils::{
-        change_active_editor_sprite, change_active_editor_ui, deselect_editor_on_esc,
-        print_editor_text, ColorExtras as _,
-    };
-    #[doc(no_inline)]
-    pub use bevy::text::cosmic_text::{
-        Color as CosmicColor, Style as FontStyle, Weight as FontWeight,
-    };
+    pub use crate::input::click::focus_on_click;
+    pub use crate::primary::{CosmicEditPlugin, CosmicFontConfig};
+    pub use crate::render_implementations::{TextEdit, TextEdit2d};
+    pub use crate::utils::{deselect_editor_on_esc, print_editor_text, ColorExtras as _};
 }
 
-pub use bevy::text::cosmic_text;
-
-pub use primary::{CosmicEditPlugin, CosmicFontConfig, CosmicPrimaryCamera};
-/// Contains the library global important types you probably want to explore first
-mod primary;
-
-pub use buffer::CosmicEditBuffer;
-mod buffer;
-pub use cosmic_edit::{
-    CosmicBackgroundColor, CosmicBackgroundImage, CosmicEditor, CosmicFontSystem, CosmicTextAlign,
-    CosmicWrap, CursorColor, DefaultAttrs, MaxChars, MaxLines, ReadOnly, ScrollEnabled,
-    SelectedTextColor, SelectionColor,
-};
+// required modules
+// non-pub required
+pub use buffer::*;
+pub use cosmic_edit::*;
+pub use editor_buffer::*;
+pub use editor_buffer::{buffer, editor};
+pub use focus::*;
 mod cosmic_edit;
-pub use cursor::{CursorPluginDisabled, HoverCursor, TextHoverIn, TextHoverOut};
-mod cursor;
-pub use events::CosmicTextChanged;
-mod events;
-pub use focus::FocusedWidget;
-mod focus;
-pub use input::InputSet;
-mod input;
-pub use password::Password;
-mod password;
-pub use placeholder::Placeholder;
-mod placeholder;
+mod double_click;
+mod editor_buffer;
+pub mod focus;
 mod render;
-pub use user_select::UserSelectNone;
-mod user_select;
+
+// pub required
+pub use input::hover::HoverCursor;
+pub mod input;
+pub mod render_implementations;
 pub mod utils;
-mod widget;
-pub(crate) use render_targets::{ChangedCosmicWidgetSize, CosmicWidgetSize};
-pub mod render_targets;
+
+// extra modules
+pub mod password;
+pub mod placeholder;
+pub mod user_select;
+
+#[cfg(feature = "internal-debugging")]
+mod debug;
diff --git a/src/password.rs b/src/password.rs
index 5cd1966..7aec85d 100644
--- a/src/password.rs
+++ b/src/password.rs
@@ -15,13 +15,13 @@ impl Plugin for PasswordPlugin {
     fn build(&self, app: &mut App) {
         app.add_systems(
             PreUpdate,
-            (hide_password_text.before(crate::input::input_mouse),),
+            (hide_password_text.before(crate::input::InputSet),),
         )
         .add_systems(
             Update,
             (restore_password_text
-                .before(crate::input::kb_input_text)
-                .after(crate::input::kb_move_cursor),),
+                .before(crate::input::keyboard::kb_input_text)
+                .after(crate::input::keyboard::kb_move_cursor),),
         )
         .add_systems(
             PostUpdate,
@@ -38,11 +38,14 @@ impl Plugin for PasswordPlugin {
 ///
 /// ```
 /// # use bevy::prelude::*;
-/// # use bevy_cosmic_edit::*;
+/// # use bevy_cosmic_edit::prelude::*;
+/// use bevy_cosmic_edit::password::Password;
+///
 /// #
 /// # fn setup(mut commands: Commands) {
 /// // Create a new cosmic bundle
 /// commands.spawn((
+///     TextEdit2d,
 ///     CosmicEditBuffer::default(),
 ///     Sprite {
 ///         custom_size: Some(Vec2::new(300.0, 40.0)),
@@ -84,79 +87,79 @@ impl Password {
 fn hide_password_text(
     mut q: Query<(
         &mut Password,
-        &mut CosmicEditBuffer,
+        EditorBuffer,
         &DefaultAttrs,
-        Option<&mut CosmicEditor>,
         Option<&Placeholder>,
     )>,
     mut font_system: ResMut<CosmicFontSystem>,
 ) {
-    for (mut password, mut buffer, attrs, editor_opt, placeholder_opt) in q.iter_mut() {
+    for (mut password, mut editor, attrs, placeholder_opt) in q.iter_mut() {
         if let Some(placeholder) = placeholder_opt {
             if placeholder.is_active() {
                 // doesn't override placeholder
                 continue;
             }
         }
-        if let Some(mut editor) = editor_opt {
-            let mut cursor = editor.cursor();
-            let mut selection = editor.selection();
-
-            editor.with_buffer_mut(|buffer| {
-                let text = buffer.get_text();
-
-                // Translate cursor to correct position for blocker glyphs
-                let translate_cursor = |c: &mut Cursor| {
-                    let (pre, _post) = text.split_at(c.index);
-                    let graphemes = pre.graphemes(true).count();
-                    c.index = graphemes * password.glyph.len_utf8();
-                };
-
-                translate_cursor(&mut cursor);
-
-                // Translate selection cursor
-                match selection {
-                    Selection::None => {}
-                    Selection::Line(ref mut c) => {
-                        translate_cursor(c);
-                    }
-                    Selection::Word(ref mut c) => {
-                        translate_cursor(c);
+        match editor.editor() {
+            Some(editor) => {
+                let mut cursor = editor.cursor();
+                let mut selection = editor.selection();
+
+                editor.with_buffer_mut(|buffer| {
+                    let text = buffer.get_text();
+
+                    // Translate cursor to correct position for blocker glyphs
+                    let translate_cursor = |c: &mut Cursor| {
+                        let (pre, _post) = text.split_at(c.index);
+                        let graphemes = pre.graphemes(true).count();
+                        c.index = graphemes * password.glyph.len_utf8();
+                    };
+
+                    translate_cursor(&mut cursor);
+
+                    // Translate selection cursor
+                    match selection {
+                        Selection::None => {}
+                        Selection::Line(ref mut c) => {
+                            translate_cursor(c);
+                        }
+                        Selection::Word(ref mut c) => {
+                            translate_cursor(c);
+                        }
+                        Selection::Normal(ref mut c) => {
+                            translate_cursor(c);
+                        }
                     }
-                    Selection::Normal(ref mut c) => {
-                        translate_cursor(c);
-                    }
-                }
 
-                // Update text to blockers
-                buffer.set_text(
+                    // Update text to blockers
+                    buffer.set_text(
+                        &mut font_system,
+                        password
+                            .glyph
+                            .to_string()
+                            .repeat(text.graphemes(true).count())
+                            .as_str(),
+                        attrs.as_attrs(),
+                        Shaping::Advanced,
+                    );
+
+                    password.real_text = text;
+                });
+
+                editor.set_cursor(cursor);
+                editor.set_selection(selection);
+            }
+            None => {
+                let text = editor.get_text();
+
+                editor.set_text(
                     &mut font_system,
-                    password
-                        .glyph
-                        .to_string()
-                        .repeat(text.graphemes(true).count())
-                        .as_str(),
+                    password.glyph.to_string().repeat(text.len()).as_str(),
                     attrs.as_attrs(),
-                    Shaping::Advanced,
                 );
-
                 password.real_text = text;
-            });
-
-            editor.set_cursor(cursor);
-            editor.set_selection(selection);
-
-            continue;
+            }
         }
-
-        let text = buffer.get_text();
-
-        buffer.set_text(
-            &mut font_system,
-            password.glyph.to_string().repeat(text.len()).as_str(),
-            attrs.as_attrs(),
-        );
-        password.real_text = text;
     }
 }
 
diff --git a/src/placeholder.rs b/src/placeholder.rs
index 55d6489..2aca341 100644
--- a/src/placeholder.rs
+++ b/src/placeholder.rs
@@ -1,5 +1,5 @@
 use crate::{
-    cosmic_edit::DefaultAttrs, events::CosmicTextChanged, input::InputSet, prelude::*,
+    cosmic_edit::DefaultAttrs, input::CosmicTextChanged, input::InputSet, prelude::*,
     render::RenderSet,
 };
 use cosmic_text::{Attrs, Edit};
@@ -9,7 +9,8 @@ use cosmic_text::{Attrs, Edit};
 /// ```
 /// # use bevy::prelude::*;
 /// # use bevy_cosmic_edit::prelude::*;
-/// use bevy_cosmic_edit::Placeholder;
+/// # use bevy_cosmic_edit::cosmic_text::Attrs;
+/// use bevy_cosmic_edit::placeholder::Placeholder;
 ///
 /// # fn setup(mut commands: Commands) {
 /// commands.spawn((
@@ -71,7 +72,7 @@ impl Plugin for PlaceholderPlugin {
 }
 
 fn add_placeholder_to_buffer(
-    mut q: Query<(&mut CosmicEditBuffer, &mut Placeholder)>,
+    mut q: Query<(EditorBuffer, &mut Placeholder)>,
     mut font_system: ResMut<CosmicFontSystem>,
 ) {
     for (mut buffer, mut placeholder) in q.iter_mut() {
diff --git a/src/primary.rs b/src/primary.rs
index e2e41f1..502591f 100644
--- a/src/primary.rs
+++ b/src/primary.rs
@@ -1,5 +1,7 @@
 use std::path::PathBuf;
 
+use bevy::ecs::world::DeferredWorld;
+
 use crate::prelude::*;
 
 /// Plugin struct that adds systems and initializes resources related to cosmic edit functionality.
@@ -15,62 +17,26 @@ impl Plugin for CosmicEditPlugin {
 
         app.add_plugins((
             crate::cosmic_edit::plugin,
-            crate::buffer::BufferPlugin,
+            // crate::buffer::BufferPlugin,
+            crate::editor_buffer::EditorBufferPlugin,
             crate::render::RenderPlugin,
-            crate::widget::WidgetPlugin,
             crate::input::InputPlugin,
             crate::focus::FocusPlugin,
-            crate::cursor::CursorPlugin,
             crate::placeholder::PlaceholderPlugin,
             crate::password::PasswordPlugin,
-            crate::events::EventsPlugin,
             crate::user_select::UserSelectPlugin,
+            crate::double_click::plugin,
         ))
         // TODO: Use the builtin bevy CosmicFontSystem
         .insert_resource(crate::cosmic_edit::CosmicFontSystem(font_system));
 
         app.register_type::<CosmicRenderOutput>();
+
+        #[cfg(feature = "internal-debugging")]
+        app.add_plugins(crate::debug::plugin);
     }
 }
 
-/// Attach to primary camera, and enable the `multicam` feature to use multiple cameras.
-/// Will panic if no Camera's without this component exist and the `multicam` feature is enabled.
-///
-/// A very basic example which doesn't panic:
-/// ```rust,no_run
-/// use bevy::prelude::*;
-/// use bevy_cosmic_edit::prelude::*;
-///
-/// fn main() {
-///     App::new()
-///         .add_plugins((
-///             DefaultPlugins,
-///             CosmicEditPlugin::default(),
-///         ))
-///     .add_systems(Startup, setup)
-///     .run();
-/// }
-///
-/// fn setup(mut commands: Commands) {
-///     commands.spawn((Camera3d::default(), CosmicPrimaryCamera));
-///     commands.spawn((
-///         Camera3d::default(),
-///         Camera {
-///             order: 2,
-///             ..default()
-///         },
-///     ));
-/// }
-/// ```
-#[derive(Component, Debug, Default)]
-pub struct CosmicPrimaryCamera;
-
-#[cfg(feature = "multicam")]
-pub(crate) type CameraFilter = With<CosmicPrimaryCamera>;
-
-#[cfg(not(feature = "multicam"))]
-pub(crate) type CameraFilter = ();
-
 /// Resource struct that holds configuration options for cosmic fonts.
 #[derive(Resource, Clone)]
 pub struct CosmicFontConfig {
@@ -93,9 +59,25 @@ impl Default for CosmicFontConfig {
 }
 
 /// Used to ferry data from a [`CosmicEditBuffer`]
-#[derive(Component, Reflect, Default, Debug, Deref)]
+#[derive(Component, Default, Reflect, Debug, Deref)]
+#[component(on_add = new_image_from_default)]
 pub(crate) struct CosmicRenderOutput(pub(crate) Handle<Image>);
 
+/// Without this, multiple buffers will show the same image
+/// as the focussed editor. IDK why
+fn new_image_from_default(
+    mut world: DeferredWorld,
+    entity: Entity,
+    _: bevy::ecs::component::ComponentId,
+) {
+    let mut images = world.resource_mut::<Assets<Image>>();
+    let default_image = images.add(Image::default());
+    *world
+        .entity_mut(entity)
+        .get_mut::<CosmicRenderOutput>()
+        .unwrap() = CosmicRenderOutput(default_image);
+}
+
 fn create_cosmic_font_system(cosmic_font_config: CosmicFontConfig) -> cosmic_text::FontSystem {
     let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
     let mut db = cosmic_text::fontdb::Database::new();
@@ -131,6 +113,7 @@ mod tests {
     }
 
     #[test]
+    #[ignore] // would need to support MinimalPlugins as well as DefaultPlugins
     fn test_spawn_cosmic_edit() {
         let mut app = App::new();
         app.add_plugins(TaskPoolPlugin::default());
@@ -154,6 +137,7 @@ mod tests {
         let mut text_nodes_query = app.world_mut().query::<&CosmicEditBuffer>();
         for cosmic_editor in text_nodes_query.iter(app.world()) {
             insta::assert_debug_snapshot!(cosmic_editor
+                .inner()
                 .lines
                 .iter()
                 .map(|line| line.text())
diff --git a/src/render.rs b/src/render.rs
index ecfe228..9bb245d 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -1,9 +1,8 @@
-use crate::widget::CosmicPadding;
-use crate::{cosmic_edit::ReadOnly, prelude::*, widget::WidgetSet};
-use crate::{cosmic_edit::*, CosmicWidgetSize};
+use crate::{cosmic_edit::ReadOnly, prelude::*};
+use crate::{cosmic_edit::*, BufferMutExtras};
 use bevy::render::render_resource::Extent3d;
-use cosmic_text::{Color, Edit};
 use image::{imageops::FilterType, GenericImageView};
+use render_implementations::CosmicWidgetSize;
 
 /// System set for cosmic text rendering systems. Runs in [`PostUpdate`]
 #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
@@ -16,26 +15,41 @@ impl Plugin for RenderPlugin {
         if !app.world().contains_resource::<SwashCache>() {
             app.insert_resource(SwashCache::default());
         } else {
-            debug!("Skipping inserting `SwashCache` resource");
+            debug!(
+                "Skipping inserting `SwashCache` resource as bevy has already inserted it for us"
+            );
         }
-        app.add_systems(Update, blink_cursor).add_systems(
-            PostUpdate,
-            (render_texture,).in_set(RenderSet).after(WidgetSet),
-        );
+        app.add_systems(
+            First,
+            update_internal_target_handles.pipe(render_implementations::debug_error),
+        )
+        .add_systems(PostUpdate, (render_texture,).in_set(RenderSet));
     }
 }
 
-pub(crate) 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);
-        }
+/// Every frame updates the output (in [`CosmicRenderOutput`]) to its receiver
+/// on the same entity, e.g. [`Sprite`]
+fn update_internal_target_handles(
+    mut buffers_q: Query<
+        (&CosmicRenderOutput, render_implementations::OutputToEntity),
+        With<CosmicEditBuffer>,
+    >,
+) -> render_implementations::Result<()> {
+    for (CosmicRenderOutput(output_data), mut output_components) in buffers_q.iter_mut() {
+        output_components.write_image_data(output_data)?;
     }
+
+    Ok(())
 }
 
-fn draw_pixel(buffer: &mut [u8], width: i32, height: i32, x: i32, y: i32, color: Color) {
+fn draw_pixel(
+    buffer: &mut [u8],
+    width: i32,
+    height: i32,
+    x: i32,
+    y: i32,
+    color: cosmic_text::Color,
+) {
     let a_a = color.a() as u32;
     if a_a == 0 {
         // Do not draw if alpha is zero
@@ -69,18 +83,63 @@ fn draw_pixel(buffer: &mut [u8], width: i32, height: i32, x: i32, y: i32, color:
 
     let out = premul + (bg.to_srgba() * (1.0 - fg.alpha));
 
-    buffer[offset + 2] = (out.blue * 255.0) as u8;
-    buffer[offset + 1] = (out.green * 255.0) as u8;
     buffer[offset] = (out.red * 255.0) as u8;
+    buffer[offset + 1] = (out.green * 255.0) as u8;
+    buffer[offset + 2] = (out.blue * 255.0) as u8;
     buffer[offset + 3] = (out.alpha * 255.0) as u8;
 }
 
+pub(crate) struct WidgetBufferCoordTransformation {
+    /// Padding between the top of the render target and the
+    /// top of the buffer
+    top_padding: f32,
+
+    render_target_size: Vec2,
+}
+
+impl WidgetBufferCoordTransformation {
+    pub fn new(vertical_align: VerticalAlign, render_target_size: Vec2, buffer_size: Vec2) -> Self {
+        let top_padding = match vertical_align {
+            VerticalAlign::Top => 0.0,
+            VerticalAlign::Bottom => (render_target_size.y - buffer_size.y).max(0.0),
+            VerticalAlign::Center => ((render_target_size.y - buffer_size.y) / 2.0).max(0.0),
+        };
+        // debug!(?top_padding, ?render_target_height, ?buffer_height);
+        Self {
+            top_padding,
+            render_target_size,
+        }
+    }
+
+    /// If you have the buffer coord, used for rendering
+    // Confusing ngl, but it works
+    pub fn buffer_to_widget(&self, buffer: Vec2) -> Vec2 {
+        Vec2::new(buffer.x, buffer.y + self.top_padding)
+    }
+
+    /// If you have the relative widget coord centered (0, 0) in the middle of the widget,
+    /// returns the buffer coord starting (0, 0) top left and working downward
+    pub fn widget_origined_to_buffer_topleft(&self, widget: Vec2) -> Vec2 {
+        Vec2::new(
+            widget.x + self.render_target_size.x / 2.,
+            -widget.y + self.render_target_size.y / 2. - self.top_padding,
+        )
+    }
+
+    pub fn widget_topleft_to_buffer_topleft(&self, widget: Vec2) -> Vec2 {
+        Vec2::new(widget.x, widget.y - self.top_padding)
+    }
+
+    #[allow(dead_code)]
+    pub(crate) fn debug_top_padding(&self) {
+        debug!(?self.top_padding);
+    }
+}
+
 /// Renders to the [CosmicRenderOutput]
-#[allow(unused_mut)] // for .set_redraw(false) commented out
 fn render_texture(
     mut query: Query<(
-        Option<&mut CosmicEditor>,
-        &mut CosmicEditBuffer,
+        EditorBuffer,
         &DefaultAttrs,
         &CosmicBackgroundImage,
         &CosmicBackgroundColor,
@@ -89,18 +148,16 @@ fn render_texture(
         Option<&SelectedTextColor>,
         &CosmicRenderOutput,
         CosmicWidgetSize,
-        &CosmicPadding,
-        &XOffset,
         Option<&ReadOnly>,
         &CosmicTextAlign,
+        &CosmicWrap,
     )>,
     mut font_system: ResMut<CosmicFontSystem>,
     mut images: ResMut<Assets<Image>>,
     mut swash_cache_state: ResMut<SwashCache>,
 ) {
     for (
-        editor,
-        mut buffer,
+        mut editor,
         attrs,
         background_image,
         fill_color,
@@ -109,18 +166,18 @@ fn render_texture(
         selected_text_color_option,
         canvas,
         size,
-        padding,
-        x_offset,
         readonly_opt,
-        position,
+        text_align,
+        wrap,
     ) in query.iter_mut()
     {
-        let Ok(size) = size.logical_size() else {
+        let font_system = &mut font_system.0;
+        let Ok(render_target_size) = size.logical_size() else {
             continue;
         };
 
         // avoids a panic
-        if size.x == 0. || size.y == 0. {
+        if render_target_size.x == 0. || render_target_size.y == 0. {
             debug!(
                 message = "Size of buffer is zero, skipping",
                 // once = "This log only appears once"
@@ -129,14 +186,14 @@ fn render_texture(
         }
 
         // Draw background
-        let mut pixels = vec![0; size.x as usize * size.y as usize * 4];
+        let mut pixels = vec![0; render_target_size.x as usize * render_target_size.y as usize * 4];
         if let Some(bg_image) = background_image.0.clone() {
             if let Some(image) = images.get(&bg_image) {
                 let mut dynamic_image = image.clone().try_into_dynamic().unwrap();
-                if image.size() != size.as_uvec2() {
+                if image.size() != render_target_size.as_uvec2() {
                     dynamic_image = dynamic_image.resize_to_fill(
-                        size.x as u32,
-                        size.y as u32,
+                        render_target_size.x as u32,
+                        render_target_size.y as u32,
                         FilterType::Triangle,
                     );
                 }
@@ -164,29 +221,60 @@ fn render_texture(
             .color_opt
             .unwrap_or(cosmic_text::Color::rgb(0, 0, 0));
 
-        let min_pad = match position {
-            CosmicTextAlign::Center { padding } => *padding as f32,
-            CosmicTextAlign::TopLeft { padding } => *padding as f32,
-            CosmicTextAlign::Left { padding } => *padding as f32,
-        };
+        // compute y-offset
+        let buffer_size = editor.borrow_with(font_system).expected_size();
+        let transformation = WidgetBufferCoordTransformation::new(
+            text_align.vertical,
+            render_target_size,
+            buffer_size,
+        );
 
+        // let mut actually_rendered_max = IVec2::ZERO;
+        // let mut actually_rendered_min = IVec2::new(i32::MAX, i32::MAX);
         let draw_closure = |x, y, w, h, color| {
             for row in 0..h as i32 {
                 for col in 0..w as i32 {
+                    let buffer_coord = IVec2::new(x + col, y + row);
+                    // actually_rendered_max = actually_rendered_max.max(buffer_coord);
+                    // actually_rendered_min = actually_rendered_min.min(buffer_coord);
+
+                    // compute padding_top
+                    let widget_coord = transformation
+                        .buffer_to_widget(buffer_coord.as_vec2())
+                        .as_ivec2();
+
+                    // actually draw pixel
                     draw_pixel(
                         &mut pixels,
-                        size.x as i32,
-                        size.y as i32,
-                        x + col + padding.x.max(min_pad) as i32 - x_offset.left as i32,
-                        y + row + padding.y as i32,
+                        render_target_size.x as i32,
+                        render_target_size.y as i32,
+                        widget_coord.x,
+                        widget_coord.y,
                         color,
                     );
                 }
             }
         };
 
+        editor.set_size(
+            font_system,
+            Some(match wrap {
+                CosmicWrap::Wrap => render_target_size.x,
+                // probably high enough
+                CosmicWrap::InfiniteLine => f32::MAX / 10f32.powi(3),
+            }),
+            Some(render_target_size.y),
+        );
+        if let Some(alignment) = text_align.horizontal {
+            for line in &mut editor.lines {
+                line.set_align(Some(alignment.into()));
+            }
+        }
+
         // Draw glyphs
-        if let Some(mut editor) = editor {
+        if let Some(editor) = editor.editor() {
+            // todo: optimizations (see below comments)
+            editor.set_redraw(true);
             if !editor.redraw() {
                 continue;
             }
@@ -206,8 +294,24 @@ fn render_texture(
                 .map(|selected_text_color| selected_text_color.0.to_cosmic())
                 .unwrap_or(font_color);
 
+            // try to fix annoying scroll behaviour
+            // by only allowing vertical scrolling if the buffer is actually larger than the canvas
+            // let mut scroll = editor.with_buffer(|b| b.scroll());
+            // if buffer_size.y + 10.0 < render_target_size.y {
+            //     trace!(
+            //         message = "Ignoring vertical scroll as buffer is smaller than canvas",
+            //         ?buffer_size.y,
+            //         ?render_target_size.y
+            //     );
+            //     scroll.vertical = 0.0;
+            // }
+            // editor.with_buffer_mut(|b| b.set_scroll(scroll));
+
+            // let new_buffer_size = editor.expected_size();
+
+            let mut editor = editor.borrow_with(font_system);
+            editor.shape_as_needed(false);
             editor.draw(
-                &mut font_system.0,
                 &mut swash_cache_state.0,
                 font_color,
                 cursor_color,
@@ -215,19 +319,46 @@ fn render_texture(
                 selected_text_color,
                 draw_closure,
             );
+
+            // if coord calculations seem to be buggy, this code may help you to debug
+            // let actually_rendered_buffer_size = actually_rendered_max - actually_rendered_min;
+            // trace!(
+            //     ?buffer_size,
+            //     ?new_buffer_size,
+            //     ?actually_rendered_buffer_size
+            // );
+            // transformation.debug_top_padding();
+            // debug check only
+            // if (new_buffer_size.as_ivec2() - actually_rendered_buffer_size)
+            //     .as_vec2()
+            //     .length()
+            //     > 5.0
+            // {
+            //     warn_once!(
+            //         message = "Calculations of buffer sizes are off by a significant amount",
+            //         note = "This is likely an internal bug with bevy_cosmic_edit"
+            //     );
+            // }
+
             // TODO: Performance optimization, read all possible render-input
             // changes and only redraw if necessary
             // editor.set_redraw(false);
         } else {
-            if !buffer.redraw() {
+            // todo: performance optimizations (see comments above/below)
+            editor.set_redraw(true);
+            if !editor.redraw() {
                 continue;
             }
-            buffer.draw(
-                &mut font_system.0,
+
+            // editor.borrow_with(font_system).compute_everything();
+            editor.shape_until_scroll(font_system, false);
+            editor.draw(
+                font_system,
                 &mut swash_cache_state.0,
                 font_color,
                 draw_closure,
             );
+
             // TODO: Performance optimization, read all possible render-input
             // changes and only redraw if necessary
             // buffer.set_redraw(false);
@@ -238,8 +369,8 @@ fn render_texture(
             // Updates the stored asset image with the computed pixels
             prev_image.data.extend_from_slice(pixels.as_slice());
             prev_image.resize(Extent3d {
-                width: size.x as u32,
-                height: size.y as u32,
+                width: render_target_size.x as u32,
+                height: render_target_size.y as u32,
                 depth_or_array_layers: 1,
             });
         }
diff --git a/src/render_implementations.rs b/src/render_implementations.rs
new file mode 100644
index 0000000..76642f6
--- /dev/null
+++ b/src/render_implementations.rs
@@ -0,0 +1,97 @@
+//! Generalizes over render target implementations. All code that
+//! depends on the specific render target implementation should
+//! live in this module.
+//!
+//! All implementations should use [`bevy::picking`] for interactions,
+//! even [`SourceType::Ui`], for consistency.
+//!
+//! ## Sprite: [`TextEdit2d`]
+//! Requires [`Sprite`] component and requires [`Sprite.custom_size`] to be Some( non-zero )
+//!
+//! ## UI: [`TextEdit`]
+//! Requires [`ImageNode`] for rendering
+// TODO: Remove `CosmicWidgetSize`?
+
+mod prelude {
+    pub(super) use super::error::Result;
+    pub(super) use super::{RenderTargetError, SourceType};
+    pub(super) use super::{RenderTypeScan, RenderTypeScanItem};
+}
+
+pub use error::*;
+mod error {
+    pub type Error = crate::render_implementations::RenderTargetError;
+    pub type Result<T> = core::result::Result<T, RenderTargetError>;
+
+    #[derive(Debug)]
+    pub enum RenderTargetError {
+        /// When no recognized [`SourceType`] could be found
+        NoTargetsAvailable,
+
+        /// When more than one [`SourceType`] was detected.
+        ///
+        /// This will always be thrown if more than one target type is available,
+        /// there is no propritisation procedure as this should be considered a
+        /// logic error.
+        MoreThanOneTargetAvailable,
+
+        /// When a [`RenderTypeScan`] was successfully conducted yet the expected
+        /// [required component/s](https://docs.rs/bevy/latest/bevy/ecs/prelude/trait.Component.html#required-components)
+        /// were not found
+        RequiredComponentNotAvailable {
+            debug_name: String,
+        },
+
+        /// When using [`SourceType::Sprite`], you must set [`Sprite.custom_size`]
+        SpriteCustomSizeNotSet,
+
+        SpriteUnexpectedNormal,
+
+        SpriteExpectedHitdataPosition,
+
+        UiExpectedCursorPosition,
+    }
+
+    impl RenderTargetError {
+        pub fn required_component_missing<C: bevy::prelude::Component>() -> Self {
+            Self::RequiredComponentNotAvailable {
+                debug_name: format!("{:?}", core::any::type_name::<C>()),
+            }
+        }
+    }
+}
+
+pub(crate) use coords::*;
+mod coords;
+pub(crate) use output::*;
+mod output;
+pub(crate) use widget_size::*;
+mod widget_size;
+pub(crate) use scan::*;
+mod scan;
+
+use crate::prelude::*;
+
+/// The top level UI text edit component
+///
+/// Adding [`TextEdit`] will pull in the required components for setting up
+/// a text edit widget, notably [`CosmicEditBuffer`]
+///
+/// Hopefully this API will eventually mirror [`bevy::prelude::Text`].
+/// See [`CosmicEditBuffer`] for more information.
+#[derive(Component)]
+#[require(ImageNode, Button, bevy::ui::RelativeCursorPosition, CosmicEditBuffer)]
+pub struct TextEdit;
+
+/// The top-level 2D text edit component
+///
+/// Adding [`TextEdit2d`] will pull in the required components for setting up
+/// a 2D text editor using a [`Sprite`] with [`Sprite.custom_size`] set,
+/// to set the size of the text editor add a [`Sprite`] component with
+/// [`Sprite.custom_size`] set.
+///
+/// Hopefully this API will eventually mirror [`bevy::prelude::Text2d`].
+/// See [`CosmicEditBuffer`] for more information.
+#[derive(Component)]
+#[require(Sprite, CosmicEditBuffer)]
+pub struct TextEdit2d;
diff --git a/src/render_implementations/coords.rs b/src/render_implementations/coords.rs
new file mode 100644
index 0000000..eef85e1
--- /dev/null
+++ b/src/render_implementations/coords.rs
@@ -0,0 +1,94 @@
+use bevy::ecs::query::QueryData;
+use bevy::picking::backend::HitData;
+use bevy::ui::RelativeCursorPosition;
+use render_implementations::prelude::*;
+
+use crate::render::WidgetBufferCoordTransformation;
+use crate::render_implementations::CosmicWidgetSize;
+use crate::{prelude::*, CosmicTextAlign};
+
+/// Responsible for translating a world coordinate to a buffer coordinate
+#[derive(QueryData)]
+pub(crate) struct RelativeQuery {
+    widget_size: CosmicWidgetSize,
+    text_align: &'static CosmicTextAlign,
+
+    sprite_global_transform: &'static GlobalTransform,
+    ui_cursor_position: Option<&'static RelativeCursorPosition>,
+}
+
+impl<'s> std::ops::Deref for RelativeQueryItem<'s> {
+    type Target = render_implementations::RenderTypeScanItem<'s>;
+
+    fn deref(&self) -> &Self::Target {
+        self.widget_size.deref()
+    }
+}
+
+impl RelativeQueryItem<'_> {
+    pub fn compute_buffer_coord(&self, hit_data: &HitData, buffer_size: Vec2) -> Result<Vec2> {
+        match self.scan()? {
+            SourceType::Sprite => {
+                if hit_data.normal != Some(Vec3::Z) {
+                    warn!(?hit_data, "Normal is not out of screen, skipping");
+                    return Err(RenderTargetError::SpriteUnexpectedNormal);
+                }
+
+                let world_position = hit_data
+                    .position
+                    .ok_or(RenderTargetError::SpriteExpectedHitdataPosition)?;
+                let RelativeQueryItem {
+                    sprite_global_transform,
+                    text_align,
+                    widget_size,
+                    ..
+                } = self;
+
+                let position_transform =
+                    GlobalTransform::from(Transform::from_translation(world_position));
+                let relative_transform = position_transform.reparented_to(sprite_global_transform);
+                let relative_position = relative_transform.translation.xy();
+
+                let render_target_size = widget_size.logical_size()?;
+                let transformation = WidgetBufferCoordTransformation::new(
+                    text_align.vertical,
+                    render_target_size,
+                    buffer_size,
+                );
+                // .xy swizzle depends on normal vector being perfectly out of screen
+                let buffer_coord =
+                    transformation.widget_origined_to_buffer_topleft(relative_position);
+
+                Ok(buffer_coord)
+            }
+            SourceType::Ui => {
+                let RelativeQueryItem {
+                    widget_size,
+                    text_align,
+                    ui_cursor_position,
+                    ..
+                } = self;
+                let cursor_position_normalized = ui_cursor_position
+                    .ok_or(RenderTargetError::required_component_missing::<
+                        RelativeCursorPosition,
+                    >())?
+                    .normalized
+                    .ok_or(RenderTargetError::UiExpectedCursorPosition)?;
+
+                let widget_size = widget_size.logical_size()?;
+                let relative_position = cursor_position_normalized * widget_size;
+
+                let transformation = WidgetBufferCoordTransformation::new(
+                    text_align.vertical,
+                    widget_size,
+                    buffer_size,
+                );
+
+                let buffer_coord =
+                    transformation.widget_topleft_to_buffer_topleft(relative_position);
+
+                Ok(buffer_coord)
+            }
+        }
+    }
+}
diff --git a/src/render_implementations/output.rs b/src/render_implementations/output.rs
new file mode 100644
index 0000000..7c1d51d
--- /dev/null
+++ b/src/render_implementations/output.rs
@@ -0,0 +1,46 @@
+use bevy::ecs::query::QueryData;
+use render_implementations::prelude::*;
+
+use crate::prelude::*;
+
+/// Will attempt to find a place on the receiving entity to place
+/// a [`Handle<Image>`]
+#[derive(QueryData)]
+#[query_data(mutable)]
+pub(crate) struct OutputToEntity {
+    scan: RenderTypeScan,
+
+    sprite_target: Option<&'static mut Sprite>,
+    image_node_target: Option<&'static mut ImageNode>,
+}
+
+impl<'s> std::ops::Deref for OutputToEntityItem<'s> {
+    type Target = RenderTypeScanItem<'s>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.scan
+    }
+}
+
+impl OutputToEntityItem<'_> {
+    pub fn write_image_data(&mut self, image: &Handle<Image>) -> Result<()> {
+        match self.scan()? {
+            SourceType::Sprite => {
+                let sprite = self
+                    .sprite_target
+                    .as_mut()
+                    .ok_or(RenderTargetError::required_component_missing::<Sprite>())?;
+                sprite.image = image.clone_weak();
+                Ok(())
+            }
+            SourceType::Ui => {
+                let image_node = self
+                    .image_node_target
+                    .as_mut()
+                    .ok_or(RenderTargetError::required_component_missing::<ImageNode>())?;
+                image_node.image = image.clone_weak();
+                Ok(())
+            }
+        }
+    }
+}
diff --git a/src/render_implementations/scan.rs b/src/render_implementations/scan.rs
new file mode 100644
index 0000000..e5c0887
--- /dev/null
+++ b/src/render_implementations/scan.rs
@@ -0,0 +1,43 @@
+use bevy::ecs::query::QueryData;
+
+use crate::prelude::*;
+use crate::render_implementations::prelude::*;
+
+/// TODO: Generalize implementations depending on this
+/// and add 3D
+#[non_exhaustive]
+pub(in crate::render_implementations) enum SourceType {
+    Ui,
+    Sprite,
+}
+
+#[derive(QueryData)]
+pub struct RenderTypeScan {
+    is_sprite: Has<TextEdit2d>,
+    is_ui: Has<TextEdit>,
+}
+
+impl RenderTypeScanItem<'_> {
+    pub fn confirm_conformance(&self) -> Result<()> {
+        match self.scan() {
+            Ok(_) => Ok(()),
+            Err(err) => Err(err),
+        }
+    }
+
+    pub(in crate::render_implementations) fn scan(&self) -> Result<SourceType> {
+        match (self.is_sprite, self.is_ui) {
+            (true, false) => Ok(SourceType::Sprite),
+            (false, true) => Ok(SourceType::Ui),
+            (true, true) => Err(RenderTargetError::MoreThanOneTargetAvailable),
+            (false, false) => Err(RenderTargetError::NoTargetsAvailable),
+        }
+    }
+}
+
+pub(crate) fn debug_error<T>(In(result): In<Result<T>>) {
+    match result {
+        Ok(_) => {}
+        Err(err) => debug!(message = "Error in render target", ?err),
+    }
+}
diff --git a/src/render_implementations/widget_size.rs b/src/render_implementations/widget_size.rs
new file mode 100644
index 0000000..8cf0f2e
--- /dev/null
+++ b/src/render_implementations/widget_size.rs
@@ -0,0 +1,71 @@
+use bevy::ecs::query::{QueryData, QueryFilter};
+use render_implementations::{RenderTypeScan, RenderTypeScanItem};
+
+use crate::prelude::*;
+use render_implementations::prelude::*;
+
+/// Query the (logical) size of a widget
+#[derive(QueryData)]
+pub struct CosmicWidgetSize {
+    scan: RenderTypeScan,
+
+    sprite: Option<&'static Sprite>,
+    ui: Option<&'static ComputedNode>,
+}
+
+/// Allows `.scan()` to be called on a [`CosmicWidgetSize`] through deref
+impl<'s> std::ops::Deref for CosmicWidgetSizeItem<'s> {
+    type Target = RenderTypeScanItem<'s>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.scan
+    }
+}
+
+/// An optimization [`QueryFilter`](bevy::ecs::query::QueryFilter)
+#[derive(QueryFilter)]
+pub(crate) struct ChangedCosmicWidgetSize {
+    sprite: Changed<Sprite>,
+    ui: Changed<ComputedNode>,
+}
+
+trait NodeSizeExt {
+    fn logical_size(&self) -> Vec2;
+}
+
+impl NodeSizeExt for ComputedNode {
+    fn logical_size(&self) -> Vec2 {
+        self.size() * self.inverse_scale_factor()
+    }
+}
+
+impl CosmicWidgetSizeItem<'_> {
+    /// Automatically logs any errors
+    pub fn logical_size(&self) -> Result<Vec2> {
+        let ret = self._logical_size();
+        if let Err(err) = &ret {
+            debug!(message = "Finding the size of a widget failed", ?err);
+        }
+        ret
+    }
+
+    fn _logical_size(&self) -> Result<Vec2> {
+        let source_type = self.scan.scan()?;
+        match source_type {
+            SourceType::Ui => {
+                let ui = self
+                    .ui
+                    .ok_or(RenderTargetError::required_component_missing::<ComputedNode>())?;
+                Ok(ui.logical_size())
+            }
+            SourceType::Sprite => {
+                let sprite = self
+                    .sprite
+                    .ok_or(RenderTargetError::required_component_missing::<Sprite>())?;
+                Ok(sprite
+                    .custom_size
+                    .ok_or(RenderTargetError::SpriteCustomSizeNotSet)?)
+            }
+        }
+    }
+}
diff --git a/src/render_targets.rs b/src/render_targets.rs
deleted file mode 100644
index d7ddca2..0000000
--- a/src/render_targets.rs
+++ /dev/null
@@ -1,242 +0,0 @@
-//! Generalizes over render target implementations.
-//!
-//! ## Sprite:
-//! Requires [`Sprite`] component and requires [`Sprite.custom_size`] to be Some( non-zero )
-//!
-//! ## UI:
-//! Requires [`ImageNode`] for rendering and [`Button`] for [`Interaction`]s
-// TODO: Remove `CosmicWidgetSize`?
-
-use bevy::{
-    ecs::query::{QueryData, QueryFilter},
-    window::SystemCursorIcon,
-    winit::cursor::CursorIcon,
-};
-
-use crate::{prelude::*, primary::CameraFilter, HoverCursor, TextHoverIn, TextHoverOut};
-
-/// The top level UI text edit component
-///
-/// Adding [`TextEdit`] will pull in the required components for setting up
-/// a text edit widget, notably [`CosmicEditBuffer`]
-///
-/// Hopefully this API will eventually mirror [`bevy::prelude::Text`].
-/// See [`CosmicEditBuffer`] for more information.
-#[derive(Component)]
-#[require(ImageNode, Button, CosmicEditBuffer)]
-pub struct TextEdit;
-
-/// The top-level 2D text edit component
-///
-/// Adding [`TextEdit2d`] will pull in the required components for setting up
-/// a 2D text editor using a [`Sprite`] with [`Sprite.custom_size`] set,
-/// to set the size of the text editor add a [`Sprite`] component with
-/// [`Sprite.custom_size`] set.
-///
-/// Hopefully this API will eventually mirror [`bevy::prelude::Text2d`].
-/// See [`CosmicEditBuffer`] for more information.
-#[derive(Component)]
-#[require(Sprite, CosmicEditBuffer)]
-pub struct TextEdit2d;
-
-/// TODO: Generalize implementations depending on this
-/// and add 3D
-pub(crate) enum SourceType {
-    Ui,
-    Sprite,
-}
-
-#[derive(Debug)]
-pub(crate) enum RenderTargetError {
-    /// When no recognized [`SourceType`] could be found
-    NoTargetsAvailable,
-
-    /// When more than one [`SourceType`] was detected.
-    ///
-    /// This will always be thrown if more than one target type is available,
-    /// there is no propritisation procedure as this should be considered a
-    /// logic error.
-    MoreThanOneTargetAvailable,
-
-    /// When a [`RenderTypeScan`] was successfully conducted yet the expected
-    /// [required component/s](https://docs.rs/bevy/latest/bevy/ecs/prelude/trait.Component.html#required-components)
-    /// were not found
-    RequiredComponentNotAvailable,
-
-    /// When using [`SourceType::Sprite`], you must set [`Sprite.custom_size`]
-    SpriteCustomSizeNotSet,
-}
-
-type Result<T> = core::result::Result<T, RenderTargetError>;
-
-#[derive(QueryData)]
-pub(crate) struct RenderTypeScan {
-    is_sprite: Has<TextEdit2d>,
-    is_ui: Has<TextEdit>,
-}
-
-impl RenderTypeScanItem<'_> {
-    pub fn scan(&self) -> Result<SourceType> {
-        match (self.is_sprite, self.is_ui) {
-            (true, false) => Ok(SourceType::Sprite),
-            (false, true) => Ok(SourceType::Ui),
-            (true, true) => Err(RenderTargetError::MoreThanOneTargetAvailable),
-            (false, false) => Err(RenderTargetError::NoTargetsAvailable),
-        }
-    }
-}
-
-/// Query the size of a widget using any [`SourceType`]
-#[derive(QueryData)]
-pub(crate) struct CosmicWidgetSize {
-    scan: RenderTypeScan,
-    sprite: Option<&'static Sprite>,
-    ui: Option<&'static ComputedNode>,
-}
-
-/// Allows `.scan()` to be called on a [`CosmicWidgetSize`] through deref
-impl<'s> std::ops::Deref for CosmicWidgetSizeItem<'s> {
-    type Target = RenderTypeScanItem<'s>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.scan
-    }
-}
-
-/// An optimization [`QueryFilter`](bevy::ecs::query::QueryFilter)
-#[derive(QueryFilter)]
-pub(crate) struct ChangedCosmicWidgetSize {
-    sprite: Changed<Sprite>,
-    ui: Changed<ComputedNode>,
-}
-
-pub(crate) trait NodeSizeExt {
-    fn logical_size(&self) -> Vec2;
-}
-
-impl NodeSizeExt for ComputedNode {
-    fn logical_size(&self) -> Vec2 {
-        self.size() * self.inverse_scale_factor()
-    }
-}
-
-impl CosmicWidgetSizeItem<'_> {
-    pub fn logical_size(&self) -> Result<Vec2> {
-        let source_type = self.scan.scan()?;
-        match source_type {
-            SourceType::Ui => {
-                let ui = self
-                    .ui
-                    .ok_or(RenderTargetError::RequiredComponentNotAvailable)?;
-                Ok(ui.logical_size())
-            }
-            SourceType::Sprite => {
-                let sprite = self
-                    .sprite
-                    .ok_or(RenderTargetError::RequiredComponentNotAvailable)?;
-                Ok(sprite
-                    .custom_size
-                    .ok_or(RenderTargetError::SpriteCustomSizeNotSet)?)
-            }
-        }
-    }
-}
-
-/// Function to find the location of the mouse cursor in a cosmic widget.
-/// Returns in logical pixels
-// TODO: Change this to use builtin `bevy::picking` instead
-pub(crate) fn get_node_cursor_pos(
-    window: &Window,
-    node_transform: &GlobalTransform,
-    size: Vec2,
-    source_type: SourceType,
-    camera: &Camera,
-    camera_transform: &GlobalTransform,
-) -> Option<Vec2> {
-    let node_translation = node_transform.affine().translation;
-    let node_bounds = Rect::new(
-        node_translation.x - size.x / 2.,
-        node_translation.y - size.y / 2.,
-        node_translation.x + size.x / 2.,
-        node_translation.y + size.y / 2.,
-    );
-
-    window.cursor_position().and_then(|pos| match source_type {
-        SourceType::Ui => {
-            if node_bounds.contains(pos) {
-                Some(Vec2::new(
-                    pos.x - node_bounds.min.x,
-                    pos.y - node_bounds.min.y,
-                ))
-            } else {
-                None
-            }
-        }
-        SourceType::Sprite => camera
-            .viewport_to_world_2d(camera_transform, pos)
-            .ok()
-            .and_then(|pos| {
-                if node_bounds.contains(pos) {
-                    Some(Vec2::new(
-                        pos.x - node_bounds.min.x,
-                        node_bounds.max.y - pos.y,
-                    ))
-                } else {
-                    None
-                }
-            }),
-    })
-}
-
-pub(crate) fn hover_sprites(
-    windows: Query<&Window, With<bevy::window::PrimaryWindow>>,
-    mut cosmic_edit_query: Query<
-        (&mut Sprite, &Visibility, &GlobalTransform, &HoverCursor),
-        With<CosmicEditBuffer>,
-    >,
-    camera_q: Query<(&Camera, &GlobalTransform), CameraFilter>,
-    mut hovered: Local<bool>,
-    mut last_hovered: Local<bool>,
-    mut evw_hover_in: EventWriter<TextHoverIn>,
-    mut evw_hover_out: EventWriter<TextHoverOut>,
-) {
-    *hovered = false;
-    if windows.iter().len() == 0 {
-        return;
-    }
-    let window = windows.single();
-    let (camera, camera_transform) = camera_q.single();
-
-    let mut icon = CursorIcon::System(SystemCursorIcon::Default);
-
-    for (sprite, visibility, node_transform, hover) in &mut cosmic_edit_query.iter_mut() {
-        if visibility == Visibility::Hidden {
-            continue;
-        }
-
-        let size = sprite.custom_size.unwrap_or(Vec2::ONE);
-        if crate::render_targets::get_node_cursor_pos(
-            window,
-            node_transform,
-            size,
-            SourceType::Sprite,
-            camera,
-            camera_transform,
-        )
-        .is_some()
-        {
-            *hovered = true;
-            icon = hover.0.clone();
-        }
-    }
-
-    if *last_hovered != *hovered {
-        if *hovered {
-            evw_hover_in.send(TextHoverIn(icon));
-        } else {
-            evw_hover_out.send(TextHoverOut);
-        }
-    }
-
-    *last_hovered = *hovered;
-}
diff --git a/src/utils.rs b/src/utils.rs
index 12e9a4b..440407c 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,9 +1,9 @@
 // Common functions for examples
 use crate::{
-    cosmic_edit::ReadOnly, prelude::*, primary::CameraFilter, ChangedCosmicWidgetSize,
-    CosmicWidgetSize,
+    prelude::*,
+    render_implementations::{ChangedCosmicWidgetSize, CosmicWidgetSize},
 };
-use bevy::{ecs::query::QueryData, window::PrimaryWindow};
+use bevy::ecs::query::QueryData;
 use cosmic_text::Edit;
 
 /// Trait for adding color conversion from [`bevy::prelude::Color`] to [`cosmic_text::Color`]
@@ -35,59 +35,6 @@ pub fn deselect_editor_on_esc(i: Res<ButtonInput<KeyCode>>, mut focus: ResMut<Fo
     }
 }
 
-/// System to allow focus on click for sprite widgets
-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<CosmicEditBuffer>, Without<ReadOnly>),
-    >,
-    camera_q: Query<(&Camera, &GlobalTransform), CameraFilter>,
-) {
-    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 Ok(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)))
-                    };
-                }
-            };
-        }
-    }
-}
-
-/// System to allow focus on click for UI widgets
-pub fn change_active_editor_ui(
-    mut interaction_query: Query<
-        (&Interaction, Entity),
-        (
-            Changed<Interaction>,
-            Without<ReadOnly>,
-            With<CosmicEditBuffer>,
-        ),
-    >,
-    mut focussed_widget: ResMut<FocusedWidget>,
-) {
-    for (interaction, entity) in interaction_query.iter_mut() {
-        if let Interaction::Pressed = interaction {
-            *focussed_widget = FocusedWidget(Some(entity));
-        }
-    }
-}
-
 /// System to print editor text content on change
 pub fn print_editor_text(
     text_inputs_q: Query<&CosmicEditor>,
@@ -137,6 +84,7 @@ pub fn print_editor_sizes(
 }
 
 /// Calls javascript to get the current timestamp
+#[allow(dead_code)] // idk why this isn't used
 #[cfg(target_arch = "wasm32")]
 pub(crate) fn get_timestamp() -> f64 {
     js_sys::Date::now()
diff --git a/src/widget.rs b/src/widget.rs
deleted file mode 100644
index 1e2d1d3..0000000
--- a/src/widget.rs
+++ /dev/null
@@ -1,215 +0,0 @@
-use crate::buffer::get_x_offset_center;
-use crate::buffer::get_y_offset_center;
-use crate::cosmic_edit::CosmicTextAlign;
-use crate::cosmic_edit::CosmicWrap;
-use crate::cosmic_edit::XOffset;
-use crate::input::InputSet;
-use crate::prelude::*;
-use crate::ChangedCosmicWidgetSize;
-use crate::CosmicWidgetSize;
-use cosmic_text::Affinity;
-use cosmic_text::Edit;
-
-/// System set for cosmic text layout systems. Runs in [`PostUpdate`]
-#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
-pub(crate) struct WidgetSet;
-
-pub(crate) struct WidgetPlugin;
-
-impl Plugin for WidgetPlugin {
-    fn build(&self, app: &mut App) {
-        app.add_systems(Update, reshape.in_set(WidgetSet).after(InputSet))
-            .add_systems(
-                PostUpdate,
-                (
-                    new_image_from_default,
-                    set_buffer_size,
-                    set_padding,
-                    set_x_offset,
-                )
-                    .chain()
-                    .in_set(WidgetSet)
-                    .after(TransformSystem::TransformPropagate),
-            )
-            .register_type::<CosmicPadding>();
-    }
-}
-
-/// Wrapper for a [`Vec2`] describing the horizontal and vertical padding of a widget.
-/// This is set programatically, not for user modification.
-/// To set a widget's padding, use [`CosmicTextAlign`]
-#[derive(Component, Reflect, Default, Deref, DerefMut, Debug)]
-pub(crate) struct CosmicPadding(pub(crate) Vec2);
-
-// /// Wrapper for a [`Vec2`] describing the horizontal and vertical size of a widget.
-// /// This is set programatically, not for user modification.
-// /// To set a widget's size, use either it's [`Sprite`] dimensions or modify the target UI element's
-// /// size.
-// #[derive(Component, Reflect, Default, Deref, DerefMut)]
-// pub(crate) struct CosmicWidgetSize(pub(crate) Vec2);
-
-/// Reshapes text in a [`CosmicEditor`]
-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);
-    }
-}
-
-/// Programatically sets the [`CosmicPadding`] of a widget based on it's [`CosmicTextAlign`]
-fn set_padding(
-    mut query: Query<
-        (
-            &mut CosmicPadding,
-            &CosmicTextAlign,
-            &CosmicEditBuffer,
-            CosmicWidgetSize,
-            Option<&CosmicEditor>,
-        ),
-        Or<(
-            With<CosmicEditor>,
-            Changed<CosmicTextAlign>,
-            Changed<CosmicEditBuffer>,
-            ChangedCosmicWidgetSize,
-        )>,
-    >,
-) {
-    for (mut padding, position, buffer, size, editor_opt) in query.iter_mut() {
-        let Ok(size) = size.logical_size() else {
-            continue;
-        };
-
-        // 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 {
-            CosmicTextAlign::Center { padding: _ } => Vec2::new(
-                get_x_offset_center(size.x, &buffer) as f32,
-                get_y_offset_center(size.y, &buffer) as f32,
-            ),
-            CosmicTextAlign::TopLeft { padding } => Vec2::new(*padding as f32, *padding as f32),
-            CosmicTextAlign::Left { padding } => {
-                Vec2::new(*padding as f32, get_y_offset_center(size.y, &buffer) as f32)
-            }
-        }
-    }
-}
-
-/// Sets the internal [`Buffer`]'s size according to the [`CosmicWidgetSize`] and [`CosmicTextAlign`]
-fn set_buffer_size(
-    mut query: Query<
-        (
-            &mut CosmicEditBuffer,
-            &CosmicWrap,
-            CosmicWidgetSize,
-            &CosmicTextAlign,
-        ),
-        Or<(
-            Changed<CosmicWrap>,
-            ChangedCosmicWidgetSize,
-            Changed<CosmicTextAlign>,
-        )>,
-    >,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    for (mut buffer, mode, size, position) in query.iter_mut() {
-        let Ok(size) = size.logical_size() else {
-            continue;
-        };
-        let padding_x = match position {
-            CosmicTextAlign::Center { padding: _ } => 0.,
-            CosmicTextAlign::TopLeft { padding } => *padding as f32,
-            CosmicTextAlign::Left { padding } => *padding as f32,
-        };
-
-        let (buffer_width, buffer_height) = match mode {
-            CosmicWrap::InfiniteLine => (f32::MAX, size.y),
-            CosmicWrap::Wrap => (size.x - padding_x, size.y),
-        };
-
-        buffer.set_size(&mut font_system.0, Some(buffer_width), Some(buffer_height));
-    }
-}
-
-/// Instantiates a new image for a [`CosmicEditBuffer`]
-fn new_image_from_default(
-    mut query: Query<&mut CosmicRenderOutput, Added<CosmicEditBuffer>>,
-    mut images: ResMut<Assets<Image>>,
-) {
-    for mut canvas in query.iter_mut() {
-        debug!(message = "Initializing a new canvas");
-        *canvas = CosmicRenderOutput(images.add(Image::default()));
-    }
-}
-
-fn set_x_offset(
-    mut query: Query<(
-        &mut XOffset,
-        &CosmicWrap,
-        &CosmicEditor,
-        CosmicWidgetSize,
-        &CosmicTextAlign,
-    )>,
-) {
-    for (mut x_offset, mode, editor, size, position) in query.iter_mut() {
-        let Ok(size) = size.logical_size() else {
-            continue;
-        };
-        if mode != &CosmicWrap::InfiniteLine {
-            continue; // this used to be `return` though I feel like it should be continue
-        }
-
-        let mut cursor_x = 0.;
-        let cursor = editor.cursor();
-
-        // counts the width of the glyphs up to the cursor in `cursor_x`
-        if let Some(line) = editor.with_buffer(|b| b.clone()).layout_runs().next() {
-            for (idx, glyph) in line.glyphs.iter().enumerate() {
-                match cursor.affinity {
-                    Affinity::Before => {
-                        if idx <= cursor.index {
-                            cursor_x += glyph.w;
-                        } else {
-                            break;
-                        }
-                    }
-                    Affinity::After => {
-                        if idx < cursor.index {
-                            cursor_x += glyph.w;
-                        } else {
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-
-        let padding_x = match position {
-            CosmicTextAlign::Center { padding } => *padding as f32,
-            CosmicTextAlign::TopLeft { padding } => *padding as f32,
-            CosmicTextAlign::Left { padding } => *padding as f32,
-        };
-
-        if x_offset.width.is_none() {
-            x_offset.width = Some(size.x - padding_x * 2.);
-        }
-
-        let right = x_offset.width.unwrap() + x_offset.left;
-
-        if cursor_x > right {
-            let diff = cursor_x - right;
-            x_offset.left += diff;
-        }
-        if cursor_x < x_offset.left {
-            let diff = x_offset.left - cursor_x;
-            x_offset.left -= diff;
-        }
-    }
-}
-- 
GitLab