diff --git a/src/input.rs b/src/input.rs
new file mode 100644
index 0000000000000000000000000000000000000000..27701b576d26378e8bfa67e2b4e69cb47020a404
--- /dev/null
+++ b/src/input.rs
@@ -0,0 +1,490 @@
+#![allow(clippy::too_many_arguments, clippy::type_complexity)]
+
+use std::time::Duration;
+
+use bevy::{
+    input::mouse::{MouseScrollUnit, MouseWheel},
+    prelude::*,
+    window::PrimaryWindow,
+};
+use cosmic_text::{Action, AttrsList, BufferLine, Cursor, Edit, Shaping};
+
+use crate::{
+    get_node_cursor_pos, get_timestamp, get_x_offset_center, get_y_offset_center,
+    save_edit_history, CosmicAttrs, CosmicEditHistory, CosmicEditor, CosmicFontSystem,
+    CosmicMaxChars, CosmicMaxLines, CosmicTextChanged, CosmicTextPosition, Focus, ReadOnly,
+    XOffset,
+};
+
+pub(crate) fn input_mouse(
+    windows: Query<&Window, With<PrimaryWindow>>, // Mouse
+    active_editor: Res<Focus>,                    // Both
+    keys: Res<Input<KeyCode>>,                    // Both
+    buttons: Res<Input<MouseButton>>,             // Mouse
+    mut cosmic_edit_query: Query<(
+        &mut CosmicEditor,   // Both
+        &GlobalTransform,    // Mouse
+        &CosmicTextPosition, // Mouse, to determine point
+        Entity,              // Both
+        &XOffset,            // Mouse
+        Option<&mut Node>,
+        Option<&mut Sprite>,
+    )>,
+    mut font_system: ResMut<CosmicFontSystem>,    // Both
+    mut scroll_evr: EventReader<MouseWheel>,      // Mouse
+    camera_q: Query<(&Camera, &GlobalTransform)>, // Mouse
+) {
+    if active_editor.0.is_none() {
+        return;
+    }
+
+    let primary_window = windows.single();
+    let scale_factor = primary_window.scale_factor() as f32;
+    let (camera, camera_transform) = camera_q.iter().find(|(c, _)| c.is_active).unwrap();
+    for (mut editor, node_transform, text_position, entity, x_offset, node_opt, sprite_opt) in
+        &mut cosmic_edit_query.iter_mut()
+    {
+        if active_editor.0 != Some(entity) {
+            continue;
+        }
+
+        let (width, height, is_ui_node) = match node_opt {
+            Some(node) => (node.size().x, node.size().y, true),
+            None => {
+                let sprite = sprite_opt.unwrap();
+                let size = sprite.custom_size.unwrap();
+                (size.x, size.y, false)
+            }
+        };
+
+        let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
+
+        // if shift key is pressed
+        let already_has_selection = editor.0.select_opt().is_some();
+        if shift && !already_has_selection {
+            let cursor = editor.0.cursor();
+            editor.0.set_select_opt(Some(cursor));
+        }
+
+        let (padding_x, padding_y) = match text_position {
+            CosmicTextPosition::Center => (
+                get_x_offset_center(width * scale_factor, editor.0.buffer()),
+                get_y_offset_center(height * scale_factor, editor.0.buffer()),
+            ),
+            CosmicTextPosition::TopLeft { padding } => (*padding, *padding),
+            CosmicTextPosition::Left { padding } => (
+                *padding,
+                get_y_offset_center(height * scale_factor, editor.0.buffer()),
+            ),
+        };
+        let point = |node_cursor_pos: (f32, f32)| {
+            (
+                (node_cursor_pos.0 * scale_factor) as i32 - padding_x,
+                (node_cursor_pos.1 * scale_factor) as i32 - padding_y,
+            )
+        };
+
+        if buttons.just_pressed(MouseButton::Left) {
+            if let Some(node_cursor_pos) = get_node_cursor_pos(
+                primary_window,
+                node_transform,
+                (width, height),
+                is_ui_node,
+                camera,
+                camera_transform,
+            ) {
+                let (mut x, y) = point(node_cursor_pos);
+                x += x_offset.0.unwrap_or((0., 0.)).0 as i32;
+                if shift {
+                    editor.0.action(&mut font_system.0, Action::Drag { x, y });
+                } else {
+                    editor.0.action(&mut font_system.0, Action::Click { x, y });
+                }
+            }
+            return;
+        }
+        if buttons.pressed(MouseButton::Left) {
+            if let Some(node_cursor_pos) = get_node_cursor_pos(
+                primary_window,
+                node_transform,
+                (width, height),
+                is_ui_node,
+                camera,
+                camera_transform,
+            ) {
+                let (mut x, y) = point(node_cursor_pos);
+                x += x_offset.0.unwrap_or((0., 0.)).0 as i32;
+                if active_editor.is_changed() && !shift {
+                    editor.0.action(&mut font_system.0, Action::Click { x, y });
+                } else {
+                    editor.0.action(&mut font_system.0, Action::Drag { x, y });
+                }
+            }
+            return;
+        }
+        for ev in scroll_evr.iter() {
+            match ev.unit {
+                MouseScrollUnit::Line => {
+                    editor.0.action(
+                        &mut font_system.0,
+                        Action::Scroll {
+                            lines: -ev.y as i32,
+                        },
+                    );
+                }
+                MouseScrollUnit::Pixel => {
+                    let line_height = editor.0.buffer().metrics().line_height;
+                    editor.0.action(
+                        &mut font_system.0,
+                        Action::Scroll {
+                            lines: -(ev.y / line_height) as i32,
+                        },
+                    );
+                }
+            }
+        }
+    }
+}
+
+/// Handles undo/redo, copy/paste and char input
+pub(crate) fn input_kb(
+    active_editor: Res<Focus>,                    // Both
+    keys: Res<Input<KeyCode>>,                    // Both
+    mut char_evr: EventReader<ReceivedCharacter>, // Kb
+    mut cosmic_edit_query: Query<(
+        &mut CosmicEditor,      // Both
+        &mut CosmicEditHistory, // Kb - Undo
+        &CosmicAttrs,           // Kb - Undo
+        &CosmicMaxLines,        // Kb
+        &CosmicMaxChars,        // Kb
+        Entity,                 // Both
+        Option<&ReadOnly>,
+    )>,
+    mut evw_changed: EventWriter<CosmicTextChanged>, // Kb
+    mut font_system: ResMut<CosmicFontSystem>,       // Both
+    mut is_deleting: Local<bool>,                    // Kb
+    mut edits_duration: Local<Option<Duration>>,     // Kb - Undo
+    mut undoredo_duration: Local<Option<Duration>>,  // Kb - Undo
+) {
+    for (mut editor, mut edit_history, attrs, max_lines, max_chars, entity, readonly_opt) in
+        &mut cosmic_edit_query.iter_mut()
+    {
+        if active_editor.0 != Some(entity) {
+            continue;
+        }
+
+        let readonly = readonly_opt.is_some();
+
+        let attrs = &attrs.0;
+
+        let now_ms = get_timestamp();
+
+        #[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]);
+
+        let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
+
+        #[cfg(target_os = "macos")]
+        let option = keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]);
+
+        // if shift key is pressed
+        let already_has_selection = editor.0.select_opt().is_some();
+        if shift && !already_has_selection {
+            let cursor = editor.0.cursor();
+            editor.0.set_select_opt(Some(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::Left) {
+            editor.0.action(&mut font_system.0, Action::PreviousWord);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+        if should_jump && keys.just_pressed(KeyCode::Right) {
+            editor.0.action(&mut font_system.0, Action::NextWord);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+        if should_jump && keys.just_pressed(KeyCode::Home) {
+            editor.0.action(&mut font_system.0, Action::BufferStart);
+            // there's a bug with cosmic text where it doesn't update the visual cursor for this action
+            // TODO: fix upstream
+            editor.0.buffer_mut().set_redraw(true);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+        if should_jump && keys.just_pressed(KeyCode::End) {
+            editor.0.action(&mut font_system.0, Action::BufferEnd);
+            // there's a bug with cosmic text where it doesn't update the visual cursor for this action
+            // TODO: fix upstream
+            editor.0.buffer_mut().set_redraw(true);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+
+        if keys.just_pressed(KeyCode::Left) {
+            editor.0.action(&mut font_system.0, Action::Left);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::Right) {
+            editor.0.action(&mut font_system.0, Action::Right);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::Up) {
+            editor.0.action(&mut font_system.0, Action::Up);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::Down) {
+            editor.0.action(&mut font_system.0, Action::Down);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+
+        if keys.just_pressed(KeyCode::Back) {
+            #[cfg(target_arch = "wasm32")]
+            editor.0.action(&mut font_system.0, Action::Backspace);
+            *is_deleting = true;
+        }
+        if keys.just_released(KeyCode::Back) {
+            *is_deleting = false;
+        }
+        if keys.just_pressed(KeyCode::Delete) {
+            editor.0.action(&mut font_system.0, Action::Delete);
+        }
+        if keys.just_pressed(KeyCode::Escape) {
+            editor.0.action(&mut font_system.0, Action::Escape);
+        }
+        if command && keys.just_pressed(KeyCode::A) {
+            editor.0.action(&mut font_system.0, Action::BufferEnd);
+            let current_cursor = editor.0.cursor();
+            editor.0.set_select_opt(Some(Cursor {
+                line: 0,
+                index: 0,
+                affinity: current_cursor.affinity,
+                color: current_cursor.color,
+            }));
+            return;
+        }
+        if keys.just_pressed(KeyCode::Home) {
+            editor.0.action(&mut font_system.0, Action::Home);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::End) {
+            editor.0.action(&mut font_system.0, Action::End);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::PageUp) {
+            editor.0.action(&mut font_system.0, Action::PageUp);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+        if keys.just_pressed(KeyCode::PageDown) {
+            editor.0.action(&mut font_system.0, Action::PageDown);
+            if !shift {
+                editor.0.set_select_opt(None);
+            }
+            return;
+        }
+
+        // redo
+        #[cfg(not(target_os = "windows"))]
+        let requested_redo = command && shift && keys.just_pressed(KeyCode::Z) && !readonly;
+        #[cfg(target_os = "windows")]
+        let requested_redo = command && keys.just_pressed(KeyCode::Y);
+
+        if requested_redo {
+            let edits = &edit_history.edits;
+            if edits.is_empty() {
+                return;
+            }
+            if edit_history.current_edit == edits.len() - 1 {
+                return;
+            }
+            let idx = edit_history.current_edit + 1;
+            if let Some(current_edit) = edits.get(idx) {
+                editor.0.buffer_mut().lines.clear();
+                for line in current_edit.lines.iter() {
+                    let mut line_text = String::new();
+                    let mut attrs_list = AttrsList::new(attrs.as_attrs());
+                    for (text, attrs) in line.iter() {
+                        let start = line_text.len();
+                        line_text.push_str(text);
+                        let end = line_text.len();
+                        attrs_list.add_span(start..end, attrs.as_attrs());
+                    }
+                    editor.0.buffer_mut().lines.push(BufferLine::new(
+                        line_text,
+                        attrs_list,
+                        Shaping::Advanced,
+                    ));
+                }
+                editor.0.set_cursor(current_edit.cursor);
+                editor.0.buffer_mut().set_redraw(true);
+                edit_history.current_edit += 1;
+            }
+            *undoredo_duration = Some(Duration::from_millis(now_ms as u64));
+            evw_changed.send(CosmicTextChanged((entity, editor.get_text())));
+            return;
+        }
+        // undo
+        let requested_undo = command && keys.just_pressed(KeyCode::Z) && !readonly;
+
+        if requested_undo {
+            let edits = &edit_history.edits;
+            if edits.is_empty() {
+                return;
+            }
+            if edit_history.current_edit <= 1 {
+                return;
+            }
+            let idx = edit_history.current_edit - 1;
+            if let Some(current_edit) = edits.get(idx) {
+                editor.0.buffer_mut().lines.clear();
+                for line in current_edit.lines.iter() {
+                    let mut line_text = String::new();
+                    let mut attrs_list = AttrsList::new(attrs.as_attrs());
+                    for (text, attrs) in line.iter() {
+                        let start = line_text.len();
+                        line_text.push_str(text);
+                        let end = line_text.len();
+                        attrs_list.add_span(start..end, attrs.as_attrs());
+                    }
+                    editor.0.buffer_mut().lines.push(BufferLine::new(
+                        line_text,
+                        attrs_list,
+                        Shaping::Advanced,
+                    ));
+                }
+                editor.0.set_cursor(current_edit.cursor);
+                editor.0.buffer_mut().set_redraw(true);
+                edit_history.current_edit -= 1;
+            }
+            *undoredo_duration = Some(Duration::from_millis(now_ms as u64));
+            evw_changed.send(CosmicTextChanged((entity, editor.get_text())));
+            return;
+        }
+
+        let mut is_clipboard = false;
+        #[cfg(not(target_arch = "wasm32"))]
+        {
+            if let Ok(mut clipboard) = arboard::Clipboard::new() {
+                if command && keys.just_pressed(KeyCode::C) {
+                    if let Some(text) = editor.0.copy_selection() {
+                        clipboard.set_text(text).unwrap();
+                        return;
+                    }
+                }
+                if command && keys.just_pressed(KeyCode::X) && !readonly {
+                    if let Some(text) = editor.0.copy_selection() {
+                        clipboard.set_text(text).unwrap();
+                        editor.0.delete_selection();
+                    }
+                    is_clipboard = true;
+                }
+                if command && keys.just_pressed(KeyCode::V) && !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.0.buffer().lines.len() < max_lines.0
+                                    {
+                                        editor.0.action(&mut font_system.0, Action::Insert(c));
+                                    }
+                                } else {
+                                    editor.0.action(&mut font_system.0, Action::Insert(c));
+                                }
+                            }
+                        }
+                    }
+                    is_clipboard = true;
+                }
+            }
+        }
+
+        // fix for issue #8
+        if let Some(select) = editor.0.select_opt() {
+            if editor.0.cursor().line == select.line && editor.0.cursor().index == select.index {
+                editor.0.set_select_opt(None);
+            }
+        }
+
+        let mut is_edit = is_clipboard;
+        let mut is_return = false;
+        if keys.just_pressed(KeyCode::Return) && !readonly {
+            is_return = true;
+            if (max_lines.0 == 0 || editor.0.buffer().lines.len() < max_lines.0)
+                && (max_chars.0 == 0 || editor.get_text().len() < max_chars.0)
+            {
+                // to have new line on wasm rather than E
+                is_edit = true;
+                editor.0.action(&mut font_system.0, Action::Insert('\n'));
+            }
+        }
+
+        if !(is_clipboard || is_return || readonly) {
+            for char_ev in char_evr.iter() {
+                is_edit = true;
+                if *is_deleting {
+                    editor.0.action(&mut font_system.0, Action::Backspace);
+                } else if max_chars.0 == 0 || editor.get_text().len() < max_chars.0 {
+                    editor
+                        .0
+                        .action(&mut font_system.0, Action::Insert(char_ev.char));
+                }
+            }
+        }
+
+        if !is_edit || readonly {
+            return;
+        }
+
+        evw_changed.send(CosmicTextChanged((entity, editor.get_text())));
+
+        if let Some(last_edit_duration) = *edits_duration {
+            if Duration::from_millis(now_ms as u64) - last_edit_duration
+                > Duration::from_millis(150)
+            {
+                save_edit_history(&mut editor.0, attrs, &mut edit_history);
+                *edits_duration = Some(Duration::from_millis(now_ms as u64));
+            }
+        } else {
+            save_edit_history(&mut editor.0, attrs, &mut edit_history);
+            *edits_duration = Some(Duration::from_millis(now_ms as u64));
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 1fc99f8715c626d47e395753187c4ea5d716bc50..20297bab27b33a15e6d4a8b3ac7b006dd17747ff 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,12 +1,12 @@
 #![allow(clippy::type_complexity)]
 
 mod cursor;
+mod input;
 
 use std::{collections::VecDeque, path::PathBuf, time::Duration};
 
 use bevy::{
     asset::HandleId,
-    input::mouse::{MouseScrollUnit, MouseWheel},
     prelude::*,
     render::{render_resource::Extent3d, texture::DEFAULT_IMAGE_HANDLE},
     transform::TransformSystem,
@@ -23,6 +23,7 @@ use cosmic_text::{
 use cursor::{change_cursor, hover_sprites, hover_ui};
 pub use cursor::{TextHoverIn, TextHoverOut};
 use image::{imageops::FilterType, GenericImageView};
+use input::{input_kb, input_mouse};
 
 #[derive(Clone, Component, PartialEq, Debug)]
 pub enum CosmicText {
@@ -30,6 +31,12 @@ pub enum CosmicText {
     MultiStyle(Vec<Vec<(String, AttrsOwned)>>),
 }
 
+impl Default for CosmicText {
+    fn default() -> Self {
+        Self::OneStyle(String::new())
+    }
+}
+
 #[derive(Clone, Component, PartialEq, Default)]
 pub enum CosmicMode {
     InfiniteLine,
@@ -38,10 +45,12 @@ pub enum CosmicMode {
     Wrap,
 }
 
-impl Default for CosmicText {
-    fn default() -> Self {
-        Self::OneStyle(String::new())
-    }
+#[derive(Default)]
+pub enum CursorConfig {
+    #[default]
+    Default,
+    Events,
+    None,
 }
 
 /// Enum representing the position of the cosmic text.
@@ -160,46 +169,6 @@ impl CosmicEditor {
     }
 }
 
-/// Adds the font system to each editor when added
-fn cosmic_editor_builder(
-    mut added_editors: Query<(Entity, &CosmicMetrics), Added<CosmicText>>,
-    mut font_system: ResMut<CosmicFontSystem>,
-    mut commands: Commands,
-) {
-    for (entity, metrics) in added_editors.iter_mut() {
-        let buffer = Buffer::new(
-            &mut font_system.0,
-            Metrics::new(metrics.font_size, metrics.line_height).scale(metrics.scale_factor),
-        );
-        // buffer.set_wrap(&mut font_system.0, cosmic_text::Wrap::None);
-        let editor = Editor::new(buffer);
-
-        commands.entity(entity).insert(CosmicEditor(editor));
-        commands.entity(entity).insert(CosmicEditHistory::default());
-        commands.entity(entity).insert(XOffset(None));
-    }
-}
-
-/// Updates editor buffer when text component changes
-fn update_buffer_text(
-    mut editor_q: Query<
-        (
-            &mut CosmicEditor,
-            &mut CosmicText,
-            &CosmicAttrs,
-            &CosmicMaxChars,
-            &CosmicMaxLines,
-        ),
-        Changed<CosmicText>,
-    >,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    for (mut editor, text, attrs, max_chars, max_lines) in editor_q.iter_mut() {
-        let text = trim_text(text.to_owned(), max_chars.0, max_lines.0);
-        editor.set_text(text, attrs.0.clone(), &mut font_system.0);
-    }
-}
-
 #[derive(Component)]
 pub struct CosmicAttrs(pub AttrsOwned);
 
@@ -373,14 +342,40 @@ pub struct CosmicEditHistory {
     pub current_edit: usize,
 }
 
-#[derive(Default)]
-pub enum CursorConfig {
-    #[default]
-    Default,
-    Events,
-    None,
+/// Resource struct that keeps track of the currently active editor entity.
+#[derive(Resource, Default, Deref, DerefMut)]
+pub struct Focus(pub Option<Entity>);
+
+/// Resource struct that holds configuration options for cosmic fonts.
+#[derive(Resource, Clone)]
+pub struct CosmicFontConfig {
+    pub fonts_dir_path: Option<PathBuf>,
+    pub font_bytes: Option<Vec<&'static [u8]>>,
+    pub load_system_fonts: bool, // caution: this can be relatively slow
+}
+
+impl Default for CosmicFontConfig {
+    fn default() -> Self {
+        let fallback_font = include_bytes!("./font/FiraMono-Regular-subset.ttf");
+        Self {
+            load_system_fonts: false,
+            font_bytes: Some(vec![fallback_font]),
+            fonts_dir_path: None,
+        }
+    }
+}
+
+#[derive(Resource)]
+struct SwashCacheState {
+    swash_cache: SwashCache,
 }
 
+#[derive(Resource)]
+struct CursorBlinkTimer(pub Timer);
+
+#[derive(Resource)]
+struct CursorVisibility(pub bool);
+
 /// Plugin struct that adds systems and initializes resources related to cosmic edit functionality.
 #[derive(Default)]
 pub struct CosmicEditPlugin {
@@ -397,7 +392,8 @@ impl Plugin for CosmicEditPlugin {
             .add_systems(
                 Update,
                 (
-                    cosmic_edit_bevy_events,
+                    input_kb,
+                    input_mouse,
                     blink_cursor,
                     freeze_cursor_blink,
                     hide_inactive_or_readonly_cursor,
@@ -438,32 +434,140 @@ impl Plugin for CosmicEditPlugin {
     }
 }
 
-/// Resource struct that keeps track of the currently active editor entity.
-#[derive(Resource, Default, Deref, DerefMut)]
-pub struct Focus(pub Option<Entity>);
+fn save_edit_history(
+    editor: &mut Editor,
+    attrs: &AttrsOwned,
+    edit_history: &mut CosmicEditHistory,
+) {
+    let edits = &edit_history.edits;
+    let current_lines = get_text_spans(editor.buffer(), attrs.clone());
+    let current_edit = edit_history.current_edit;
+    let mut new_edits = VecDeque::new();
+    new_edits.extend(edits.iter().take(current_edit + 1).cloned());
+    // remove old edits
+    if new_edits.len() > 1000 {
+        new_edits.drain(0..100);
+    }
+    new_edits.push_back(EditHistoryItem {
+        cursor: editor.cursor(),
+        lines: current_lines,
+    });
+    let len = new_edits.len();
+    *edit_history = CosmicEditHistory {
+        edits: new_edits,
+        current_edit: len - 1,
+    };
+}
 
-/// Resource struct that holds configuration options for cosmic fonts.
-#[derive(Resource, Clone)]
-pub struct CosmicFontConfig {
-    pub fonts_dir_path: Option<PathBuf>,
-    pub font_bytes: Option<Vec<&'static [u8]>>,
-    pub load_system_fonts: bool, // caution: this can be relatively slow
+/// Adds the font system to each editor when added
+fn cosmic_editor_builder(
+    mut added_editors: Query<(Entity, &CosmicMetrics), Added<CosmicText>>,
+    mut font_system: ResMut<CosmicFontSystem>,
+    mut commands: Commands,
+) {
+    for (entity, metrics) in added_editors.iter_mut() {
+        let buffer = Buffer::new(
+            &mut font_system.0,
+            Metrics::new(metrics.font_size, metrics.line_height).scale(metrics.scale_factor),
+        );
+        // buffer.set_wrap(&mut font_system.0, cosmic_text::Wrap::None);
+        let editor = Editor::new(buffer);
+
+        commands.entity(entity).insert(CosmicEditor(editor));
+        commands.entity(entity).insert(CosmicEditHistory::default());
+        commands.entity(entity).insert(XOffset(None));
+    }
 }
 
-impl Default for CosmicFontConfig {
-    fn default() -> Self {
-        let fallback_font = include_bytes!("./font/FiraMono-Regular-subset.ttf");
-        Self {
-            load_system_fonts: false,
-            font_bytes: Some(vec![fallback_font]),
-            fonts_dir_path: None,
+fn create_cosmic_font_system(cosmic_font_config: CosmicFontConfig) -> FontSystem {
+    let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
+    let mut db = cosmic_text::fontdb::Database::new();
+    if let Some(dir_path) = cosmic_font_config.fonts_dir_path.clone() {
+        db.load_fonts_dir(dir_path);
+    }
+    if let Some(custom_font_data) = &cosmic_font_config.font_bytes {
+        for elem in custom_font_data {
+            db.load_font_data(elem.to_vec());
         }
     }
+    if cosmic_font_config.load_system_fonts {
+        db.load_system_fonts();
+    }
+    cosmic_text::FontSystem::new_with_locale_and_db(locale, db)
 }
 
-#[derive(Resource)]
-struct SwashCacheState {
-    swash_cache: SwashCache,
+fn on_scale_factor_change(
+    mut scale_factor_changed: EventReader<WindowScaleFactorChanged>,
+    mut cosmic_query: Query<(&mut CosmicEditor, &mut CosmicMetrics)>,
+    mut font_system: ResMut<CosmicFontSystem>,
+) {
+    if !scale_factor_changed.is_empty() {
+        let new_scale_factor = scale_factor_changed.iter().last().unwrap().scale_factor as f32;
+        for (mut editor, metrics) in &mut cosmic_query.iter_mut() {
+            let font_system = &mut font_system.0;
+            let metrics =
+                Metrics::new(metrics.font_size, metrics.line_height).scale(new_scale_factor);
+
+            editor.0.buffer_mut().set_metrics(font_system, metrics);
+            editor.0.buffer_mut().set_redraw(true);
+        }
+    }
+}
+
+pub fn get_node_cursor_pos(
+    window: &Window,
+    node_transform: &GlobalTransform,
+    size: (f32, f32),
+    is_ui_node: bool,
+    camera: &Camera,
+    camera_transform: &GlobalTransform,
+) -> Option<(f32, f32)> {
+    let (x_min, y_min, x_max, y_max) = (
+        node_transform.affine().translation.x - size.0 / 2.,
+        node_transform.affine().translation.y - size.1 / 2.,
+        node_transform.affine().translation.x + size.0 / 2.,
+        node_transform.affine().translation.y + size.1 / 2.,
+    );
+
+    window.cursor_position().and_then(|pos| {
+        if is_ui_node {
+            if x_min < pos.x && pos.x < x_max && y_min < pos.y && pos.y < y_max {
+                Some((pos.x - x_min, pos.y - y_min))
+            } else {
+                None
+            }
+        } else {
+            camera
+                .viewport_to_world_2d(camera_transform, pos)
+                .and_then(|pos| {
+                    if x_min < pos.x && pos.x < x_max && y_min < pos.y && pos.y < y_max {
+                        Some((pos.x - x_min, y_max - pos.y))
+                    } else {
+                        None
+                    }
+                })
+        }
+    })
+}
+
+/// Updates editor buffer when text component changes
+fn update_buffer_text(
+    mut editor_q: Query<
+        (
+            &mut CosmicEditor,
+            &mut CosmicText,
+            &CosmicAttrs,
+            &CosmicMaxChars,
+            &CosmicMaxLines,
+        ),
+        Changed<CosmicText>,
+    >,
+    mut font_system: ResMut<CosmicFontSystem>,
+) {
+    for (mut editor, text, attrs, max_chars, max_lines) in editor_q.iter_mut() {
+        let text = trim_text(text.to_owned(), max_chars.0, max_lines.0);
+        editor.set_text(text, attrs.0.clone(), &mut font_system.0);
+    }
 }
 
 fn trim_text(text: CosmicText, max_chars: usize, max_lines: usize) -> CosmicText {
@@ -548,78 +652,6 @@ fn trim_text(text: CosmicText, max_chars: usize, max_lines: usize) -> CosmicText
         }
     }
 }
-
-fn create_cosmic_font_system(cosmic_font_config: CosmicFontConfig) -> FontSystem {
-    let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
-    let mut db = cosmic_text::fontdb::Database::new();
-    if let Some(dir_path) = cosmic_font_config.fonts_dir_path.clone() {
-        db.load_fonts_dir(dir_path);
-    }
-    if let Some(custom_font_data) = &cosmic_font_config.font_bytes {
-        for elem in custom_font_data {
-            db.load_font_data(elem.to_vec());
-        }
-    }
-    if cosmic_font_config.load_system_fonts {
-        db.load_system_fonts();
-    }
-    cosmic_text::FontSystem::new_with_locale_and_db(locale, db)
-}
-
-fn on_scale_factor_change(
-    mut scale_factor_changed: EventReader<WindowScaleFactorChanged>,
-    mut cosmic_query: Query<(&mut CosmicEditor, &mut CosmicMetrics)>,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    if !scale_factor_changed.is_empty() {
-        let new_scale_factor = scale_factor_changed.iter().last().unwrap().scale_factor as f32;
-        for (mut editor, metrics) in &mut cosmic_query.iter_mut() {
-            let font_system = &mut font_system.0;
-            let metrics =
-                Metrics::new(metrics.font_size, metrics.line_height).scale(new_scale_factor);
-
-            editor.0.buffer_mut().set_metrics(font_system, metrics);
-            editor.0.buffer_mut().set_redraw(true);
-        }
-    }
-}
-
-pub fn get_node_cursor_pos(
-    window: &Window,
-    node_transform: &GlobalTransform,
-    size: (f32, f32),
-    is_ui_node: bool,
-    camera: &Camera,
-    camera_transform: &GlobalTransform,
-) -> Option<(f32, f32)> {
-    let (x_min, y_min, x_max, y_max) = (
-        node_transform.affine().translation.x - size.0 / 2.,
-        node_transform.affine().translation.y - size.1 / 2.,
-        node_transform.affine().translation.x + size.0 / 2.,
-        node_transform.affine().translation.y + size.1 / 2.,
-    );
-
-    window.cursor_position().and_then(|pos| {
-        if is_ui_node {
-            if x_min < pos.x && pos.x < x_max && y_min < pos.y && pos.y < y_max {
-                Some((pos.x - x_min, pos.y - y_min))
-            } else {
-                None
-            }
-        } else {
-            camera
-                .viewport_to_world_2d(camera_transform, pos)
-                .and_then(|pos| {
-                    if x_min < pos.x && pos.x < x_max && y_min < pos.y && pos.y < y_max {
-                        Some((pos.x - x_min, y_max - pos.y))
-                    } else {
-                        None
-                    }
-                })
-        }
-    })
-}
-
 /// Returns texts from a MultiStyle buffer
 pub fn get_text_spans(
     buffer: &Buffer,
@@ -659,750 +691,28 @@ pub fn get_text_spans(
     spans
 }
 
-fn save_edit_history(
-    editor: &mut Editor,
-    attrs: &AttrsOwned,
-    edit_history: &mut CosmicEditHistory,
-) {
-    let edits = &edit_history.edits;
-    let current_lines = get_text_spans(editor.buffer(), attrs.clone());
-    let current_edit = edit_history.current_edit;
-    let mut new_edits = VecDeque::new();
-    new_edits.extend(edits.iter().take(current_edit + 1).cloned());
-    // remove old edits
-    if new_edits.len() > 1000 {
-        new_edits.drain(0..100);
-    }
-    new_edits.push_back(EditHistoryItem {
-        cursor: editor.cursor(),
-        lines: current_lines,
-    });
-    let len = new_edits.len();
-    *edit_history = CosmicEditHistory {
-        edits: new_edits,
-        current_edit: len - 1,
-    };
-}
-
-fn get_text_size(buffer: &Buffer) -> (f32, f32) {
-    if buffer.layout_runs().count() == 0 {
-        return (0., buffer.metrics().line_height);
-    }
-    let width = buffer
-        .layout_runs()
-        .map(|run| run.line_w)
-        .reduce(f32::max)
-        .unwrap();
-    let height = buffer.layout_runs().count() as f32 * buffer.metrics().line_height;
-    (width, height)
-}
-
-pub fn get_y_offset_center(widget_height: f32, buffer: &Buffer) -> i32 {
-    let (_, text_height) = get_text_size(buffer);
-    ((widget_height - text_height) / 2.0) as i32
-}
-
-pub fn get_x_offset_center(widget_width: f32, buffer: &Buffer) -> i32 {
-    let (text_width, _) = get_text_size(buffer);
-    ((widget_width - text_width) / 2.0) as i32
-}
-
-#[allow(clippy::too_many_arguments, clippy::type_complexity)]
-// the meat of the input management
-fn cosmic_edit_bevy_events(
-    windows: Query<&Window, With<PrimaryWindow>>,
-    active_editor: Res<Focus>,
-    keys: Res<Input<KeyCode>>,
-    mut char_evr: EventReader<ReceivedCharacter>,
-    buttons: Res<Input<MouseButton>>,
-    mut cosmic_edit_query: Query<
-        (
-            &mut CosmicEditor,
-            &mut CosmicEditHistory,
-            &GlobalTransform,
-            &CosmicAttrs,
-            &CosmicTextPosition,
-            &CosmicMaxLines,
-            &CosmicMaxChars,
-            Entity,
-            &XOffset,
-        ),
-        With<CosmicEditor>,
-    >,
-    mut evw_changed: EventWriter<CosmicTextChanged>,
-    readonly_query: Query<&ReadOnly>,
-    node_query: Query<&mut Node>,
-    sprite_query: Query<&mut Sprite>,
-    mut font_system: ResMut<CosmicFontSystem>,
-    mut is_deleting: Local<bool>,
-    mut scroll_evr: EventReader<MouseWheel>,
-    mut edits_duration: Local<Option<Duration>>,
-    mut undoredo_duration: Local<Option<Duration>>,
-    camera_q: Query<(&Camera, &GlobalTransform)>,
-) {
-    let primary_window = windows.single();
-    let scale_factor = primary_window.scale_factor() as f32;
-    let (camera, camera_transform) = camera_q.iter().find(|(c, _)| c.is_active).unwrap();
-    for (
-        mut editor,
-        mut edit_history,
-        node_transform,
-        attrs,
-        text_position,
-        max_lines,
-        max_chars,
-        entity,
-        x_offset,
-    ) in &mut cosmic_edit_query.iter_mut()
-    {
-        let readonly = readonly_query.get(entity).is_ok();
-
-        let (width, height, is_ui_node) = match node_query.get(entity) {
-            Ok(node) => (node.size().x, node.size().y, true),
-            Err(_) => {
-                let sprite = sprite_query.get(entity).unwrap();
-                let size = sprite.custom_size.unwrap();
-                (size.x, size.y, false)
-            }
-        };
-
-        let attrs = &attrs.0;
-
-        if active_editor.0 == Some(entity) {
-            let now_ms = get_timestamp();
-
-            #[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]);
-
-            let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
-
-            #[cfg(target_os = "macos")]
-            let option = keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]);
-
-            // if shift key is pressed
-            let already_has_selection = editor.0.select_opt().is_some();
-            if shift && !already_has_selection {
-                let cursor = editor.0.cursor();
-                editor.0.set_select_opt(Some(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::Left) {
-                editor.0.action(&mut font_system.0, Action::PreviousWord);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-            if should_jump && keys.just_pressed(KeyCode::Right) {
-                editor.0.action(&mut font_system.0, Action::NextWord);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-            if should_jump && keys.just_pressed(KeyCode::Home) {
-                editor.0.action(&mut font_system.0, Action::BufferStart);
-                // there's a bug with cosmic text where it doesn't update the visual cursor for this action
-                // TODO: fix upstream
-                editor.0.buffer_mut().set_redraw(true);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-            if should_jump && keys.just_pressed(KeyCode::End) {
-                editor.0.action(&mut font_system.0, Action::BufferEnd);
-                // there's a bug with cosmic text where it doesn't update the visual cursor for this action
-                // TODO: fix upstream
-                editor.0.buffer_mut().set_redraw(true);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-
-            if keys.just_pressed(KeyCode::Left) {
-                editor.0.action(&mut font_system.0, Action::Left);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-            if keys.just_pressed(KeyCode::Right) {
-                editor.0.action(&mut font_system.0, Action::Right);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-            if keys.just_pressed(KeyCode::Up) {
-                editor.0.action(&mut font_system.0, Action::Up);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-            if keys.just_pressed(KeyCode::Down) {
-                editor.0.action(&mut font_system.0, Action::Down);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-
-            if !readonly && keys.just_pressed(KeyCode::Back) {
-                #[cfg(target_arch = "wasm32")]
-                editor.0.action(&mut font_system.0, Action::Backspace);
-                *is_deleting = true;
-            }
-            if !readonly && keys.just_released(KeyCode::Back) {
-                *is_deleting = false;
-            }
-            if !readonly && keys.just_pressed(KeyCode::Delete) {
-                editor.0.action(&mut font_system.0, Action::Delete);
-            }
-            if keys.just_pressed(KeyCode::Escape) {
-                editor.0.action(&mut font_system.0, Action::Escape);
-            }
-            if command && keys.just_pressed(KeyCode::A) {
-                editor.0.action(&mut font_system.0, Action::BufferEnd);
-                let current_cursor = editor.0.cursor();
-                editor.0.set_select_opt(Some(Cursor {
-                    line: 0,
-                    index: 0,
-                    affinity: current_cursor.affinity,
-                    color: current_cursor.color,
-                }));
-                return;
-            }
-            if keys.just_pressed(KeyCode::Home) {
-                editor.0.action(&mut font_system.0, Action::Home);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-            if keys.just_pressed(KeyCode::End) {
-                editor.0.action(&mut font_system.0, Action::End);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-            if keys.just_pressed(KeyCode::PageUp) {
-                editor.0.action(&mut font_system.0, Action::PageUp);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-            if keys.just_pressed(KeyCode::PageDown) {
-                editor.0.action(&mut font_system.0, Action::PageDown);
-                if !shift {
-                    editor.0.set_select_opt(None);
-                }
-                return;
-            }
-
-            // redo
-            #[cfg(not(target_os = "windows"))]
-            let requested_redo = command && shift && keys.just_pressed(KeyCode::Z);
-            #[cfg(target_os = "windows")]
-            let requested_redo = command && keys.just_pressed(KeyCode::Y);
-
-            if !readonly && requested_redo {
-                let edits = &edit_history.edits;
-                if edits.is_empty() {
-                    return;
-                }
-                if edit_history.current_edit == edits.len() - 1 {
-                    return;
-                }
-                let idx = edit_history.current_edit + 1;
-                if let Some(current_edit) = edits.get(idx) {
-                    editor.0.buffer_mut().lines.clear();
-                    for line in current_edit.lines.iter() {
-                        let mut line_text = String::new();
-                        let mut attrs_list = AttrsList::new(attrs.as_attrs());
-                        for (text, attrs) in line.iter() {
-                            let start = line_text.len();
-                            line_text.push_str(text);
-                            let end = line_text.len();
-                            attrs_list.add_span(start..end, attrs.as_attrs());
-                        }
-                        editor.0.buffer_mut().lines.push(BufferLine::new(
-                            line_text,
-                            attrs_list,
-                            Shaping::Advanced,
-                        ));
-                    }
-                    editor.0.set_cursor(current_edit.cursor);
-                    editor.0.buffer_mut().set_redraw(true);
-                    edit_history.current_edit += 1;
-                }
-                *undoredo_duration = Some(Duration::from_millis(now_ms as u64));
-                evw_changed.send(CosmicTextChanged((entity, editor.get_text())));
-                return;
-            }
-            // undo
-            let requested_undo = command && keys.just_pressed(KeyCode::Z);
-
-            if !readonly && requested_undo {
-                let edits = &edit_history.edits;
-                if edits.is_empty() {
-                    return;
-                }
-                if edit_history.current_edit <= 1 {
-                    return;
-                }
-                let idx = edit_history.current_edit - 1;
-                if let Some(current_edit) = edits.get(idx) {
-                    editor.0.buffer_mut().lines.clear();
-                    for line in current_edit.lines.iter() {
-                        let mut line_text = String::new();
-                        let mut attrs_list = AttrsList::new(attrs.as_attrs());
-                        for (text, attrs) in line.iter() {
-                            let start = line_text.len();
-                            line_text.push_str(text);
-                            let end = line_text.len();
-                            attrs_list.add_span(start..end, attrs.as_attrs());
-                        }
-                        editor.0.buffer_mut().lines.push(BufferLine::new(
-                            line_text,
-                            attrs_list,
-                            Shaping::Advanced,
-                        ));
-                    }
-                    editor.0.set_cursor(current_edit.cursor);
-                    editor.0.buffer_mut().set_redraw(true);
-                    edit_history.current_edit -= 1;
-                }
-                *undoredo_duration = Some(Duration::from_millis(now_ms as u64));
-                evw_changed.send(CosmicTextChanged((entity, editor.get_text())));
-                return;
-            }
-
-            let mut is_clipboard = false;
-            #[cfg(not(target_arch = "wasm32"))]
-            {
-                if let Ok(mut clipboard) = arboard::Clipboard::new() {
-                    if command && keys.just_pressed(KeyCode::C) {
-                        if let Some(text) = editor.0.copy_selection() {
-                            clipboard.set_text(text).unwrap();
-                            return;
-                        }
-                    }
-                    if !readonly && command && keys.just_pressed(KeyCode::X) {
-                        if let Some(text) = editor.0.copy_selection() {
-                            clipboard.set_text(text).unwrap();
-                            editor.0.delete_selection();
-                        }
-                        is_clipboard = true;
-                    }
-                    if !readonly && command && keys.just_pressed(KeyCode::V) {
-                        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.0.buffer().lines.len() < max_lines.0
-                                        {
-                                            editor.0.action(&mut font_system.0, Action::Insert(c));
-                                        }
-                                    } else {
-                                        editor.0.action(&mut font_system.0, Action::Insert(c));
-                                    }
-                                }
-                            }
-                        }
-                        is_clipboard = true;
-                    }
-                }
-            }
-            let (padding_x, padding_y) = match text_position {
-                CosmicTextPosition::Center => (
-                    get_x_offset_center(width * scale_factor, editor.0.buffer()),
-                    get_y_offset_center(height * scale_factor, editor.0.buffer()),
-                ),
-                CosmicTextPosition::TopLeft { padding } => (*padding, *padding),
-                CosmicTextPosition::Left { padding } => (
-                    *padding,
-                    get_y_offset_center(height * scale_factor, editor.0.buffer()),
-                ),
-            };
-            let point = |node_cursor_pos: (f32, f32)| {
-                (
-                    (node_cursor_pos.0 * scale_factor) as i32 - padding_x,
-                    (node_cursor_pos.1 * scale_factor) as i32 - padding_y,
-                )
-            };
-
-            if buttons.just_pressed(MouseButton::Left) {
-                if let Some(node_cursor_pos) = get_node_cursor_pos(
-                    primary_window,
-                    node_transform,
-                    (width, height),
-                    is_ui_node,
-                    camera,
-                    camera_transform,
-                ) {
-                    let (mut x, y) = point(node_cursor_pos);
-                    x += x_offset.0.unwrap_or((0., 0.)).0 as i32;
-                    if shift {
-                        editor.0.action(&mut font_system.0, Action::Drag { x, y });
-                    } else {
-                        editor.0.action(&mut font_system.0, Action::Click { x, y });
-                    }
-                }
-                return;
-            }
-            if buttons.pressed(MouseButton::Left) {
-                if let Some(node_cursor_pos) = get_node_cursor_pos(
-                    primary_window,
-                    node_transform,
-                    (width, height),
-                    is_ui_node,
-                    camera,
-                    camera_transform,
-                ) {
-                    let (mut x, y) = point(node_cursor_pos);
-                    x += x_offset.0.unwrap_or((0., 0.)).0 as i32;
-                    if active_editor.is_changed() && !shift {
-                        editor.0.action(&mut font_system.0, Action::Click { x, y });
-                    } else {
-                        editor.0.action(&mut font_system.0, Action::Drag { x, y });
-                    }
-                }
-                return;
-            }
-            for ev in scroll_evr.iter() {
-                match ev.unit {
-                    MouseScrollUnit::Line => {
-                        editor.0.action(
-                            &mut font_system.0,
-                            Action::Scroll {
-                                lines: -ev.y as i32,
-                            },
-                        );
-                    }
-                    MouseScrollUnit::Pixel => {
-                        let line_height = editor.0.buffer().metrics().line_height;
-                        editor.0.action(
-                            &mut font_system.0,
-                            Action::Scroll {
-                                lines: -(ev.y / line_height) as i32,
-                            },
-                        );
-                    }
-                }
-            }
-
-            if readonly {
-                return;
-            }
-
-            // fix for issue #8
-            if let Some(select) = editor.0.select_opt() {
-                if editor.0.cursor().line == select.line && editor.0.cursor().index == select.index
-                {
-                    editor.0.set_select_opt(None);
-                }
-            }
-
-            let mut is_edit = is_clipboard;
-            let mut is_return = false;
-            if keys.just_pressed(KeyCode::Return) {
-                is_return = true;
-                if (max_lines.0 == 0 || editor.0.buffer().lines.len() < max_lines.0)
-                    && (max_chars.0 == 0 || editor.get_text().len() < max_chars.0)
-                {
-                    // to have new line on wasm rather than E
-                    is_edit = true;
-                    editor.0.action(&mut font_system.0, Action::Insert('\n'));
-                }
-            }
-
-            if !(is_clipboard || is_return) {
-                for char_ev in char_evr.iter() {
-                    is_edit = true;
-                    if *is_deleting {
-                        editor.0.action(&mut font_system.0, Action::Backspace);
-                    } else if max_chars.0 == 0 || editor.get_text().len() < max_chars.0 {
-                        editor
-                            .0
-                            .action(&mut font_system.0, Action::Insert(char_ev.char));
-                    }
-                }
-            }
-
-            if !is_edit {
-                return;
-            }
-
-            evw_changed.send(CosmicTextChanged((entity, editor.get_text())));
-
-            if let Some(last_edit_duration) = *edits_duration {
-                if Duration::from_millis(now_ms as u64) - last_edit_duration
-                    > Duration::from_millis(150)
-                {
-                    save_edit_history(&mut editor.0, attrs, &mut edit_history);
-                    *edits_duration = Some(Duration::from_millis(now_ms as u64));
-                }
-            } else {
-                save_edit_history(&mut editor.0, attrs, &mut edit_history);
-                *edits_duration = Some(Duration::from_millis(now_ms as u64));
-            }
-        }
-    }
-}
-
-#[allow(clippy::too_many_arguments)]
-fn redraw_buffer_common(
-    mode: &CosmicMode,
-    x_offset: &mut XOffset,
-    images: &mut ResMut<Assets<Image>>,
-    swash_cache_state: &mut ResMut<SwashCacheState>,
-    editor: &mut Editor,
-    attrs: &CosmicAttrs,
-    background_image: Option<Handle<Image>>,
-    fill_color: Color,
-    cosmic_canvas_img_handle: &mut Handle<Image>,
-    text_position: &CosmicTextPosition,
-    font_system: &mut ResMut<CosmicFontSystem>,
-    scale_factor: f32,
-    original_width: f32,
-    original_height: f32,
-) {
-    let widget_width = original_width * scale_factor;
-    let widget_height = original_height * scale_factor;
-    let swash_cache = &mut swash_cache_state.swash_cache;
-
-    let mut cursor_x = 0.;
-    if mode == &CosmicMode::InfiniteLine {
-        if let Some(line) = editor.buffer().layout_runs().next() {
-            for (idx, glyph) in line.glyphs.iter().enumerate() {
-                if editor.cursor().affinity == Affinity::Before {
-                    if idx <= editor.cursor().index {
-                        cursor_x += glyph.w;
-                    }
-                } else if idx < editor.cursor().index {
-                    cursor_x += glyph.w;
-                } else {
-                    break;
-                }
-            }
-        }
-    }
-
-    if mode == &CosmicMode::InfiniteLine && x_offset.0.is_none() {
-        let padding_x = match text_position {
-            CosmicTextPosition::Center => get_x_offset_center(widget_width, editor.buffer()),
-            CosmicTextPosition::TopLeft { padding } => *padding,
-            CosmicTextPosition::Left { padding } => *padding,
-        };
-        *x_offset = XOffset(Some((0., widget_width - 2. * padding_x as f32)));
-    }
-
-    if let Some((x_min, x_max)) = x_offset.0 {
-        if cursor_x > x_max {
-            let diff = cursor_x - x_max;
-            *x_offset = XOffset(Some((x_min + diff, cursor_x)));
-        }
-        if cursor_x < x_min {
-            let diff = x_min - cursor_x;
-            *x_offset = XOffset(Some((cursor_x, x_max - diff)));
-        }
-    }
-
-    let font_color = attrs
-        .0
-        .color_opt
-        .unwrap_or(cosmic_text::Color::rgb(0, 0, 0));
-
-    let mut pixels = vec![0; widget_width as usize * widget_height as usize * 4];
-    if let Some(bg_image) = background_image {
-        if let Some(image) = images.get(&bg_image) {
-            let mut dynamic_image = image.clone().try_into_dynamic().unwrap();
-            if image.size().x != widget_width || image.size().y != widget_height {
-                dynamic_image = dynamic_image.resize_to_fill(
-                    widget_width as u32,
-                    widget_height as u32,
-                    FilterType::Triangle,
-                );
-            }
-            for (i, (_, _, rgba)) in dynamic_image.pixels().enumerate() {
-                if let Some(p) = pixels.get_mut(i * 4..(i + 1) * 4) {
-                    p[0] = rgba[0];
-                    p[1] = rgba[1];
-                    p[2] = rgba[2];
-                    p[3] = rgba[3];
-                }
-            }
-        }
-    } else {
-        let bg = fill_color;
-        for pixel in pixels.chunks_exact_mut(4) {
-            pixel[0] = (bg.r() * 255.) as u8; // Red component
-            pixel[1] = (bg.g() * 255.) as u8; // Green component
-            pixel[2] = (bg.b() * 255.) as u8; // Blue component
-            pixel[3] = (bg.a() * 255.) as u8; // Alpha component
-        }
-    }
-    let (padding_x, padding_y) = match text_position {
-        CosmicTextPosition::Center => (
-            get_x_offset_center(widget_width, editor.buffer()),
-            get_y_offset_center(widget_height, editor.buffer()),
-        ),
-        CosmicTextPosition::TopLeft { padding } => (*padding, *padding),
-        CosmicTextPosition::Left { padding } => (
-            *padding,
-            get_y_offset_center(widget_height, editor.buffer()),
-        ),
-    };
-
-    editor.draw(
-        &mut font_system.0,
-        swash_cache,
-        font_color,
-        |x, y, w, h, color| {
-            for row in 0..h as i32 {
-                for col in 0..w as i32 {
-                    draw_pixel(
-                        &mut pixels,
-                        widget_width as i32,
-                        widget_height as i32,
-                        x + col + padding_x - x_offset.0.unwrap_or((0., 0.)).0 as i32,
-                        y + row + padding_y,
-                        color,
-                    );
-                }
-            }
-        },
-    );
-
-    if let Some(prev_image) = images.get_mut(cosmic_canvas_img_handle) {
-        if *cosmic_canvas_img_handle == bevy::render::texture::DEFAULT_IMAGE_HANDLE.typed() {
-            let mut prev_image = prev_image.clone();
-            prev_image.data.clear();
-            prev_image.data.extend_from_slice(pixels.as_slice());
-            prev_image.resize(Extent3d {
-                width: widget_width as u32,
-                height: widget_height as u32,
-                depth_or_array_layers: 1,
-            });
-            let handle_id: HandleId = HandleId::random::<Image>();
-            let new_handle: Handle<Image> = Handle::weak(handle_id);
-            let new_handle = images.set(new_handle, prev_image);
-            *cosmic_canvas_img_handle = new_handle;
-        } else {
-            prev_image.data.clear();
-            prev_image.data.extend_from_slice(pixels.as_slice());
-            prev_image.resize(Extent3d {
-                width: widget_width as u32,
-                height: widget_height as u32,
-                depth_or_array_layers: 1,
-            });
-        }
-    }
-
-    editor.buffer_mut().set_redraw(false);
-}
-
-fn cosmic_edit_redraw_buffer_ui(
-    windows: Query<&Window, With<PrimaryWindow>>,
-    mut images: ResMut<Assets<Image>>,
-    mut swash_cache_state: ResMut<SwashCacheState>,
-    mut cosmic_edit_query: Query<(
-        &mut CosmicEditor,
-        &CosmicAttrs,
-        &CosmicBackground,
-        &FillColor,
-        &CosmicTextPosition,
-        &mut UiImage,
-        &Node,
-        &mut XOffset,
-        &mut Style,
-        &CosmicMode,
-    )>,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    let primary_window = windows.single();
-    let scale = primary_window.scale_factor() as f32;
-
-    for (
-        mut editor,
-        attrs,
-        background_image,
-        fill_color,
-        text_position,
-        mut img,
-        node,
-        mut x_offset,
-        mut style,
-        mode,
-    ) in &mut cosmic_edit_query.iter_mut()
-    {
-        editor.0.shape_as_needed(&mut font_system.0);
-        if !editor.0.buffer().redraw() {
-            continue;
-        }
-
-        let width = node.size().x;
-        let mut height = node.size().y;
-        let widget_height = height * scale;
-        let widget_width = width * scale;
-
-        let (buffer_width, buffer_height) = match mode {
-            CosmicMode::InfiniteLine => (f32::MAX, widget_height),
-            CosmicMode::AutoHeight => (widget_width, (i32::MAX / 2) as f32),
-            CosmicMode::Wrap => (widget_width, widget_height),
-        };
-        editor
-            .0
-            .buffer_mut()
-            .set_size(&mut font_system.0, buffer_width, buffer_height);
-
-        if mode == &CosmicMode::AutoHeight {
-            let text_size = get_text_size(editor.0.buffer());
-            let text_height = (text_size.1 / primary_window.scale_factor() as f32) + 30.;
-            if text_height > height {
-                height = text_height;
-                style.height = Val::Px(height);
-            }
-        }
-
-        redraw_buffer_common(
-            mode,
-            &mut x_offset,
-            &mut images,
-            &mut swash_cache_state,
-            &mut editor.0,
-            attrs,
-            background_image.0.clone(),
-            fill_color.0,
-            &mut img.texture,
-            text_position,
-            &mut font_system,
-            scale,
-            width,
-            height,
-        );
+fn get_text_size(buffer: &Buffer) -> (f32, f32) {
+    if buffer.layout_runs().count() == 0 {
+        return (0., buffer.metrics().line_height);
     }
+    let width = buffer
+        .layout_runs()
+        .map(|run| run.line_w)
+        .reduce(f32::max)
+        .unwrap();
+    let height = buffer.layout_runs().count() as f32 * buffer.metrics().line_height;
+    (width, height)
 }
 
-#[derive(Resource)]
-struct CursorBlinkTimer(pub Timer);
+pub fn get_y_offset_center(widget_height: f32, buffer: &Buffer) -> i32 {
+    let (_, text_height) = get_text_size(buffer);
+    ((widget_height - text_height) / 2.0) as i32
+}
 
-#[derive(Resource)]
-struct CursorVisibility(pub bool);
+pub fn get_x_offset_center(widget_width: f32, buffer: &Buffer) -> i32 {
+    let (text_width, _) = get_text_size(buffer);
+    ((widget_width - text_width) / 2.0) as i32
+}
 
 fn blink_cursor(
     mut visibility: ResMut<CursorVisibility>,
@@ -1511,6 +821,88 @@ fn clear_inactive_selection(
     }
 }
 
+fn cosmic_edit_redraw_buffer_ui(
+    windows: Query<&Window, With<PrimaryWindow>>,
+    mut images: ResMut<Assets<Image>>,
+    mut swash_cache_state: ResMut<SwashCacheState>,
+    mut cosmic_edit_query: Query<(
+        &mut CosmicEditor,
+        &CosmicAttrs,
+        &CosmicBackground,
+        &FillColor,
+        &CosmicTextPosition,
+        &mut UiImage,
+        &Node,
+        &mut XOffset,
+        &mut Style,
+        &CosmicMode,
+    )>,
+    mut font_system: ResMut<CosmicFontSystem>,
+) {
+    let primary_window = windows.single();
+    let scale = primary_window.scale_factor() as f32;
+
+    for (
+        mut editor,
+        attrs,
+        background_image,
+        fill_color,
+        text_position,
+        mut img,
+        node,
+        mut x_offset,
+        mut style,
+        mode,
+    ) in &mut cosmic_edit_query.iter_mut()
+    {
+        editor.0.shape_as_needed(&mut font_system.0);
+        if !editor.0.buffer().redraw() {
+            continue;
+        }
+
+        let width = node.size().x;
+        let mut height = node.size().y;
+        let widget_height = height * scale;
+        let widget_width = width * scale;
+
+        let (buffer_width, buffer_height) = match mode {
+            CosmicMode::InfiniteLine => (f32::MAX, widget_height),
+            CosmicMode::AutoHeight => (widget_width, (i32::MAX / 2) as f32),
+            CosmicMode::Wrap => (widget_width, widget_height),
+        };
+        editor
+            .0
+            .buffer_mut()
+            .set_size(&mut font_system.0, buffer_width, buffer_height);
+
+        if mode == &CosmicMode::AutoHeight {
+            let text_size = get_text_size(editor.0.buffer());
+            let text_height = (text_size.1 / primary_window.scale_factor() as f32) + 30.;
+            if text_height > height {
+                height = text_height;
+                style.height = Val::Px(height);
+            }
+        }
+
+        redraw_buffer_common(
+            mode,
+            &mut x_offset,
+            &mut images,
+            &mut swash_cache_state,
+            &mut editor.0,
+            attrs,
+            background_image.0.clone(),
+            fill_color.0,
+            &mut img.texture,
+            text_position,
+            &mut font_system,
+            scale,
+            width,
+            height,
+        );
+    }
+}
+
 fn cosmic_edit_redraw_buffer(
     windows: Query<&Window, With<PrimaryWindow>>,
     mut images: ResMut<Assets<Image>>,
@@ -1590,6 +982,158 @@ fn cosmic_edit_redraw_buffer(
     }
 }
 
+#[allow(clippy::too_many_arguments)]
+fn redraw_buffer_common(
+    mode: &CosmicMode,
+    x_offset: &mut XOffset,
+    images: &mut ResMut<Assets<Image>>,
+    swash_cache_state: &mut ResMut<SwashCacheState>,
+    editor: &mut Editor,
+    attrs: &CosmicAttrs,
+    background_image: Option<Handle<Image>>,
+    fill_color: Color,
+    cosmic_canvas_img_handle: &mut Handle<Image>,
+    text_position: &CosmicTextPosition,
+    font_system: &mut ResMut<CosmicFontSystem>,
+    scale_factor: f32,
+    original_width: f32,
+    original_height: f32,
+) {
+    let widget_width = original_width * scale_factor;
+    let widget_height = original_height * scale_factor;
+    let swash_cache = &mut swash_cache_state.swash_cache;
+
+    let mut cursor_x = 0.;
+    if mode == &CosmicMode::InfiniteLine {
+        if let Some(line) = editor.buffer().layout_runs().next() {
+            for (idx, glyph) in line.glyphs.iter().enumerate() {
+                if editor.cursor().affinity == Affinity::Before {
+                    if idx <= editor.cursor().index {
+                        cursor_x += glyph.w;
+                    }
+                } else if idx < editor.cursor().index {
+                    cursor_x += glyph.w;
+                } else {
+                    break;
+                }
+            }
+        }
+    }
+
+    if mode == &CosmicMode::InfiniteLine && x_offset.0.is_none() {
+        let padding_x = match text_position {
+            CosmicTextPosition::Center => get_x_offset_center(widget_width, editor.buffer()),
+            CosmicTextPosition::TopLeft { padding } => *padding,
+            CosmicTextPosition::Left { padding } => *padding,
+        };
+        *x_offset = XOffset(Some((0., widget_width - 2. * padding_x as f32)));
+    }
+
+    if let Some((x_min, x_max)) = x_offset.0 {
+        if cursor_x > x_max {
+            let diff = cursor_x - x_max;
+            *x_offset = XOffset(Some((x_min + diff, cursor_x)));
+        }
+        if cursor_x < x_min {
+            let diff = x_min - cursor_x;
+            *x_offset = XOffset(Some((cursor_x, x_max - diff)));
+        }
+    }
+
+    let font_color = attrs
+        .0
+        .color_opt
+        .unwrap_or(cosmic_text::Color::rgb(0, 0, 0));
+
+    let mut pixels = vec![0; widget_width as usize * widget_height as usize * 4];
+    if let Some(bg_image) = background_image {
+        if let Some(image) = images.get(&bg_image) {
+            let mut dynamic_image = image.clone().try_into_dynamic().unwrap();
+            if image.size().x != widget_width || image.size().y != widget_height {
+                dynamic_image = dynamic_image.resize_to_fill(
+                    widget_width as u32,
+                    widget_height as u32,
+                    FilterType::Triangle,
+                );
+            }
+            for (i, (_, _, rgba)) in dynamic_image.pixels().enumerate() {
+                if let Some(p) = pixels.get_mut(i * 4..(i + 1) * 4) {
+                    p[0] = rgba[0];
+                    p[1] = rgba[1];
+                    p[2] = rgba[2];
+                    p[3] = rgba[3];
+                }
+            }
+        }
+    } else {
+        let bg = fill_color;
+        for pixel in pixels.chunks_exact_mut(4) {
+            pixel[0] = (bg.r() * 255.) as u8; // Red component
+            pixel[1] = (bg.g() * 255.) as u8; // Green component
+            pixel[2] = (bg.b() * 255.) as u8; // Blue component
+            pixel[3] = (bg.a() * 255.) as u8; // Alpha component
+        }
+    }
+    let (padding_x, padding_y) = match text_position {
+        CosmicTextPosition::Center => (
+            get_x_offset_center(widget_width, editor.buffer()),
+            get_y_offset_center(widget_height, editor.buffer()),
+        ),
+        CosmicTextPosition::TopLeft { padding } => (*padding, *padding),
+        CosmicTextPosition::Left { padding } => (
+            *padding,
+            get_y_offset_center(widget_height, editor.buffer()),
+        ),
+    };
+
+    editor.draw(
+        &mut font_system.0,
+        swash_cache,
+        font_color,
+        |x, y, w, h, color| {
+            for row in 0..h as i32 {
+                for col in 0..w as i32 {
+                    draw_pixel(
+                        &mut pixels,
+                        widget_width as i32,
+                        widget_height as i32,
+                        x + col + padding_x - x_offset.0.unwrap_or((0., 0.)).0 as i32,
+                        y + row + padding_y,
+                        color,
+                    );
+                }
+            }
+        },
+    );
+
+    if let Some(prev_image) = images.get_mut(cosmic_canvas_img_handle) {
+        if *cosmic_canvas_img_handle == bevy::render::texture::DEFAULT_IMAGE_HANDLE.typed() {
+            let mut prev_image = prev_image.clone();
+            prev_image.data.clear();
+            prev_image.data.extend_from_slice(pixels.as_slice());
+            prev_image.resize(Extent3d {
+                width: widget_width as u32,
+                height: widget_height as u32,
+                depth_or_array_layers: 1,
+            });
+            let handle_id: HandleId = HandleId::random::<Image>();
+            let new_handle: Handle<Image> = Handle::weak(handle_id);
+            let new_handle = images.set(new_handle, prev_image);
+            *cosmic_canvas_img_handle = new_handle;
+        } else {
+            prev_image.data.clear();
+            prev_image.data.extend_from_slice(pixels.as_slice());
+            prev_image.resize(Extent3d {
+                width: widget_width as u32,
+                height: widget_height as u32,
+                depth_or_array_layers: 1,
+            });
+        }
+    }
+
+    editor.buffer_mut().set_redraw(false);
+}
+
 fn draw_pixel(
     buffer: &mut [u8],
     width: i32,