diff --git a/src/lib.rs b/src/lib.rs
index ba9ce10a6bf9b53d1e364f87189eee93c4b908f2..1c33f71c1dfa0fcdada2cc1ae22543766dfa57d6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,30 +2,29 @@
 
 mod cursor;
 mod input;
+mod render;
 
-use std::{collections::VecDeque, path::PathBuf, time::Duration};
+use std::{collections::VecDeque, path::PathBuf};
 
 use bevy::{
-    asset::HandleId,
-    prelude::*,
-    render::{render_resource::Extent3d, texture::DEFAULT_IMAGE_HANDLE},
-    transform::TransformSystem,
-    ui::FocusPolicy,
-    window::{PrimaryWindow, WindowScaleFactorChanged},
+    prelude::*, render::texture::DEFAULT_IMAGE_HANDLE, transform::TransformSystem, ui::FocusPolicy,
 };
 pub use cosmic_text::{
     Action, Attrs, AttrsOwned, Color as CosmicColor, Cursor, Edit, Family, Style as FontStyle,
     Weight as FontWeight,
 };
 use cosmic_text::{
-    Affinity, AttrsList, Buffer, BufferLine, Editor, FontSystem, Metrics, Shaping, SwashCache,
+    AttrsList, Buffer, BufferLine, Editor, FontSystem, Metrics, Shaping, SwashCache,
 };
 use cursor::{change_cursor, hover_sprites, hover_ui};
 pub use cursor::{TextHoverIn, TextHoverOut};
-use image::{imageops::FilterType, GenericImageView};
 use input::{input_kb, input_mouse, undo_redo, ClickTimer};
 #[cfg(target_arch = "wasm32")]
 use input::{poll_wasm_paste, WasmPaste, WasmPasteAsyncChannel};
+use render::{
+    blink_cursor, cosmic_edit_redraw_buffer, freeze_cursor_blink, hide_inactive_or_readonly_cursor,
+    on_scale_factor_change, CursorBlinkTimer, CursorVisibility, SwashCacheState,
+};
 
 #[derive(Clone, Component, PartialEq, Debug)]
 pub enum CosmicText {
@@ -386,17 +385,6 @@ impl Default for CosmicFontConfig {
     }
 }
 
-#[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 {
@@ -432,8 +420,7 @@ impl Plugin for CosmicEditPlugin {
         )
         .add_systems(
             PostUpdate,
-            (cosmic_edit_redraw_buffer_ui, cosmic_edit_redraw_buffer)
-                .after(TransformSystem::TransformPropagate),
+            (cosmic_edit_redraw_buffer).after(TransformSystem::TransformPropagate),
         )
         .init_resource::<Focus>()
         .insert_resource(CursorBlinkTimer(Timer::from_seconds(
@@ -558,26 +545,6 @@ fn create_cosmic_font_system(cosmic_font_config: CosmicFontConfig) -> FontSystem
     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, &CosmicMetrics, &mut XOffset)>,
-    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, mut x_offset) in &mut cosmic_query.iter_mut() {
-            let font_system = &mut font_system.0;
-            let metrics =
-                Metrics::new(metrics.font_size, metrics.line_height).scale(new_scale_factor);
-
-            editor.0.buffer_mut().set_metrics(font_system, metrics);
-            editor.0.buffer_mut().set_redraw(true);
-
-            *x_offset = XOffset(None);
-        }
-    }
-}
-
 pub fn get_node_cursor_pos(
     window: &Window,
     node_transform: &GlobalTransform,
@@ -793,120 +760,6 @@ pub fn get_x_offset_center(widget_width: f32, buffer: &Buffer) -> i32 {
     ((widget_width - text_width) / 2.0) as i32
 }
 
-fn blink_cursor(
-    mut visibility: ResMut<CursorVisibility>,
-    mut timer: ResMut<CursorBlinkTimer>,
-    time: Res<Time>,
-    active_editor: ResMut<Focus>,
-    mut cosmic_editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>,
-    mut placeholder_editor_q: Query<&mut Placeholder, Without<ReadOnly>>,
-) {
-    if let Some(e) = active_editor.0 {
-        timer.0.tick(time.delta());
-        if !timer.0.just_finished() && !active_editor.is_changed() {
-            return;
-        }
-        visibility.0 = !visibility.0;
-
-        // always start cursor visible on focus
-        if active_editor.is_changed() {
-            visibility.0 = true;
-            timer.0.set_elapsed(Duration::ZERO);
-        }
-
-        let new_color = if visibility.0 {
-            None
-        } else {
-            Some(cosmic_text::Color::rgba(0, 0, 0, 0))
-        };
-
-        if let Ok(mut editor) = cosmic_editor_q.get_mut(e) {
-            let editor = &mut editor.0;
-            let mut cursor = editor.cursor();
-            cursor.color = new_color;
-            editor.set_cursor(cursor);
-            editor.buffer_mut().set_redraw(true);
-        }
-
-        if let Ok(mut placeholder) = placeholder_editor_q.get_mut(e) {
-            let placeholder = &mut placeholder.0 .0;
-            let mut cursor_p = placeholder.cursor();
-            cursor_p.color = new_color;
-            placeholder.set_cursor(cursor_p);
-            placeholder.buffer_mut().set_redraw(true);
-        }
-    }
-}
-
-fn freeze_cursor_blink(
-    mut visibility: ResMut<CursorVisibility>,
-    mut timer: ResMut<CursorBlinkTimer>,
-    active_editor: Res<Focus>,
-    keys: Res<Input<KeyCode>>,
-    char_evr: EventReader<ReceivedCharacter>,
-    mut editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>,
-) {
-    let inputs = [
-        KeyCode::Left,
-        KeyCode::Right,
-        KeyCode::Up,
-        KeyCode::Down,
-        KeyCode::Back,
-        KeyCode::Return,
-    ];
-    if !keys.any_pressed(inputs) && char_evr.is_empty() {
-        return;
-    }
-
-    if let Some(e) = active_editor.0 {
-        if let Ok(mut editor) = editor_q.get_mut(e) {
-            timer.0.set_elapsed(Duration::ZERO);
-            visibility.0 = true;
-            let mut cursor = editor.0.cursor();
-            cursor.color = None;
-            editor.0.set_cursor(cursor);
-            editor.0.buffer_mut().set_redraw(true);
-        }
-    }
-}
-
-fn hide_inactive_or_readonly_cursor(
-    mut cosmic_editor_q_readonly: Query<&mut CosmicEditor, With<ReadOnly>>,
-    mut cosmic_editor_q_placeholder: Query<(Entity, &mut Placeholder, Option<&ReadOnly>)>,
-    mut cosmic_editor_q_editable: Query<(Entity, &mut CosmicEditor), Without<ReadOnly>>,
-    active_editor: Res<Focus>,
-) {
-    for mut editor in &mut cosmic_editor_q_readonly.iter_mut() {
-        let mut cursor = editor.0.cursor();
-        cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
-        editor.0.set_cursor(cursor);
-        editor.0.buffer_mut().set_redraw(true);
-    }
-
-    if active_editor.is_changed() || active_editor.0.is_none() {
-        return;
-    }
-
-    for (e, mut editor, readonly_opt) in &mut cosmic_editor_q_placeholder.iter_mut() {
-        if e != active_editor.0.unwrap() || readonly_opt.is_some() {
-            let editor = &mut editor.0;
-            let mut cursor = editor.0.cursor();
-            cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
-            editor.0.set_cursor(cursor);
-            editor.0.buffer_mut().set_redraw(true);
-        }
-    }
-
-    for (e, mut editor) in &mut cosmic_editor_q_editable.iter_mut() {
-        if e != active_editor.0.unwrap() {
-            let mut cursor = editor.0.cursor();
-            cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
-            editor.0.set_cursor(cursor);
-            editor.0.buffer_mut().set_redraw(true);
-        }
-    }
-}
-
 fn clear_inactive_selection(
     mut cosmic_editor_q: Query<(Entity, &mut CosmicEditor)>,
     active_editor: Res<Focus>,
@@ -922,386 +775,6 @@ 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,
-        Option<&mut Placeholder>,
-        &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,
-        mut placeholder_opt,
-        attrs,
-        background_image,
-        fill_color,
-        text_position,
-        mut img,
-        node,
-        mut x_offset,
-        mut style,
-        mode,
-    ) in &mut cosmic_edit_query.iter_mut()
-    {
-        let editor = if editor.get_text().is_empty() && placeholder_opt.is_some() {
-            let placeholder = &mut placeholder_opt.as_mut().unwrap().0 .0;
-            let mut cursor = placeholder.cursor();
-            cursor.index = 0;
-            placeholder.set_cursor(cursor);
-            placeholder.buffer_mut().set_redraw(true);
-            *x_offset = XOffset(None);
-            placeholder
-        } else {
-            &mut editor.0
-        };
-
-        editor.shape_as_needed(&mut font_system.0);
-        if !editor.buffer().redraw() {
-            continue;
-        }
-
-        let width = node.size().x.ceil();
-        let mut height = node.size().y.ceil();
-        let widget_height = height * scale;
-        let widget_width = width * scale;
-
-        let padding_x = match text_position {
-            CosmicTextPosition::Center => 0.,
-            CosmicTextPosition::TopLeft { padding } => *padding as f32,
-            CosmicTextPosition::Left { padding } => *padding as f32,
-        };
-
-        let (buffer_width, buffer_height) = match mode {
-            CosmicMode::InfiniteLine => (f32::MAX, widget_height),
-            CosmicMode::AutoHeight => (widget_width - padding_x, (i32::MAX / 2) as f32),
-            CosmicMode::Wrap => (widget_width - padding_x, widget_height),
-        };
-        editor
-            .buffer_mut()
-            .set_size(&mut font_system.0, buffer_width, buffer_height);
-
-        if mode == &CosmicMode::AutoHeight {
-            let text_size = get_text_size(editor.buffer());
-            let text_height = (text_size.1 + 30.) / primary_window.scale_factor() as f32;
-            if text_height > height {
-                height = text_height.ceil();
-                style.height = Val::Px(height);
-            }
-        }
-
-        redraw_buffer_common(
-            mode,
-            &mut x_offset,
-            &mut images,
-            &mut swash_cache_state,
-            editor,
-            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>>,
-    mut swash_cache_state: ResMut<SwashCacheState>,
-    mut cosmic_edit_query: Query<(
-        &mut CosmicEditor,
-        &CosmicAttrs,
-        &mut Sprite,
-        &CosmicBackground,
-        &FillColor,
-        &CosmicTextPosition,
-        &mut Handle<Image>,
-        &mut XOffset,
-        &CosmicMode,
-    )>,
-    mut font_system: ResMut<CosmicFontSystem>,
-) {
-    let primary_window = windows.single();
-    let scale = primary_window.scale_factor() as f32;
-
-    for (
-        mut editor,
-        attrs,
-        sprite,
-        background_image,
-        fill_color,
-        text_position,
-        mut handle,
-        mut x_offset,
-        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 = sprite.custom_size.unwrap().x.ceil();
-        let mut height = sprite.custom_size.unwrap().y.ceil();
-        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), // TODO: workaround
-            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 + 30.) / primary_window.scale_factor() as f32;
-            if text_height > height {
-                height = text_height.ceil();
-                sprite.custom_size.unwrap().y = 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 handle,
-            text_position,
-            &mut font_system,
-            scale,
-            width,
-            height,
-        );
-    }
-}
-
-#[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,
-    height: i32,
-    x: i32,
-    y: i32,
-    color: cosmic_text::Color,
-) {
-    // TODO: perftest this fn against previous iteration
-    let a_a = color.a() as u32;
-    if a_a == 0 {
-        // Do not draw if alpha is zero
-        return;
-    }
-
-    if y < 0 || y >= height {
-        // Skip if y out of bounds
-        return;
-    }
-
-    if x < 0 || x >= width {
-        // Skip if x out of bounds
-        return;
-    }
-
-    let offset = (y as usize * width as usize + x as usize) * 4;
-
-    let bg = Color::rgba_u8(
-        buffer[offset],
-        buffer[offset + 1],
-        buffer[offset + 2],
-        buffer[offset + 3],
-    );
-
-    // TODO: if alpha is 100% or bg is empty skip blending
-
-    let fg = Color::rgba_u8(color.r(), color.g(), color.b(), color.a());
-
-    let premul = fg * Vec3::splat(color.a() as f32 / 255.0);
-
-    let out = premul + bg * (1.0 - fg.a());
-
-    buffer[offset + 2] = (out.b() * 255.0) as u8;
-    buffer[offset + 1] = (out.g() * 255.0) as u8;
-    buffer[offset] = (out.r() * 255.0) as u8;
-    buffer[offset + 3] = (bg.a() * 255.0) as u8;
-}
-
 #[cfg(target_arch = "wasm32")]
 pub fn get_timestamp() -> f64 {
     js_sys::Date::now()
diff --git a/src/render.rs b/src/render.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f6d8bf07f7dcbc61eeaf4dc72bab92034351350e
--- /dev/null
+++ b/src/render.rs
@@ -0,0 +1,478 @@
+use std::time::Duration;
+
+use bevy::{
+    asset::HandleId,
+    prelude::*,
+    render::render_resource::Extent3d,
+    window::{PrimaryWindow, WindowScaleFactorChanged},
+};
+use cosmic_text::{Affinity, Edit, Metrics, SwashCache};
+use image::{imageops::FilterType, GenericImageView};
+
+use crate::{
+    get_text_size, get_x_offset_center, get_y_offset_center, CosmicAttrs, CosmicBackground,
+    CosmicEditor, CosmicFontSystem, CosmicMetrics, CosmicMode, CosmicTextPosition, FillColor,
+    Focus, Placeholder, ReadOnly, XOffset,
+};
+
+#[derive(Resource)]
+pub(crate) struct SwashCacheState {
+    pub swash_cache: SwashCache,
+}
+
+#[derive(Resource)]
+pub(crate) struct CursorBlinkTimer(pub Timer);
+
+#[derive(Resource)]
+pub(crate) struct CursorVisibility(pub bool);
+
+pub(crate) fn cosmic_edit_redraw_buffer(
+    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,
+        Option<&mut UiImage>,
+        Option<&Node>,
+        Option<&mut Style>,
+        Option<&mut Sprite>,
+        Option<&mut Handle<Image>>,
+        &mut XOffset,
+        &CosmicMode,
+        Option<&mut Placeholder>,
+    )>,
+    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,
+        ui_image_opt,
+        node_opt,
+        style_opt,
+        sprite_opt,
+        handle_opt,
+        mut x_offset,
+        mode,
+        mut placeholder_opt,
+    ) in &mut cosmic_edit_query.iter_mut()
+    {
+        if !editor.0.buffer().redraw() {
+            continue;
+        }
+
+        // Check for placeholder, replace editor if found and buffer is empty
+        let editor = if editor.get_text().is_empty() && placeholder_opt.is_some() {
+            let placeholder = &mut placeholder_opt.as_mut().unwrap().0 .0;
+            placeholder.buffer_mut().set_redraw(true);
+
+            editor.0.buffer_mut().set_redraw(true);
+
+            let mut cursor = placeholder.cursor();
+            cursor.index = 0;
+            placeholder.set_cursor(cursor);
+            *x_offset = XOffset(None);
+            placeholder
+        } else {
+            &mut editor.0
+        };
+
+        editor.shape_as_needed(&mut font_system.0);
+
+        // Get numbers, do maths to find and set cursor
+
+        let (base_width, mut base_height) = if node_opt.is_some() {
+            (
+                node_opt.unwrap().size().x.ceil(),
+                node_opt.unwrap().size().y.ceil(),
+            )
+        } else {
+            (
+                sprite_opt.as_ref().unwrap().custom_size.unwrap().x.ceil(),
+                sprite_opt.as_ref().unwrap().custom_size.unwrap().y.ceil(),
+            )
+        };
+
+        let widget_width = base_width * scale;
+        let widget_height = base_height * scale;
+
+        let padding_x = match text_position {
+            CosmicTextPosition::Center => 0.,
+            CosmicTextPosition::TopLeft { padding } => *padding as f32,
+            CosmicTextPosition::Left { padding } => *padding as f32,
+        };
+
+        let (buffer_width, buffer_height) = match mode {
+            CosmicMode::InfiniteLine => (f32::MAX, widget_height),
+            CosmicMode::AutoHeight => (widget_width - padding_x, (i32::MAX / 2) as f32),
+            CosmicMode::Wrap => (widget_width - padding_x, widget_height),
+        };
+
+        editor
+            .buffer_mut()
+            .set_size(&mut font_system.0, buffer_width, buffer_height);
+
+        if mode == &CosmicMode::AutoHeight {
+            let text_size = get_text_size(editor.buffer());
+            let text_height = (text_size.1 + 30.) / primary_window.scale_factor() as f32;
+            if text_height > base_height {
+                base_height = text_height.ceil();
+                match style_opt {
+                    Some(mut style) => style.height = Val::Px(base_height),
+                    None => sprite_opt.unwrap().custom_size.unwrap().y = base_height,
+                }
+            }
+        }
+
+        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)));
+            }
+        }
+
+        // Draw background
+        let mut pixels = vec![0; widget_width as usize * widget_height 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().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.0;
+            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
+            }
+        }
+
+        // Get values for glyph draw step
+        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()),
+            ),
+        };
+
+        let font_color = attrs
+            .0
+            .color_opt
+            .unwrap_or(cosmic_text::Color::rgb(0, 0, 0));
+
+        // Draw glyphs
+        editor.draw(
+            &mut font_system.0,
+            &mut swash_cache_state.swash_cache,
+            font_color,
+            |x, y, w, h, color| {
+                for row in 0..h as i32 {
+                    for col in 0..w as i32 {
+                        draw_pixel(
+                            &mut pixels,
+                            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,
+                        );
+                    }
+                }
+            },
+        );
+
+        // replace target image with modified pixel buffer
+        match handle_opt {
+            Some(mut handle) => replace_target_image(
+                &mut handle,
+                &mut images,
+                widget_width,
+                widget_height,
+                pixels,
+            ),
+            None => replace_target_image(
+                &mut ui_image_opt.unwrap().texture,
+                &mut images,
+                widget_width,
+                widget_height,
+                pixels,
+            ),
+        }
+
+        editor.buffer_mut().set_redraw(false);
+    }
+}
+
+fn replace_target_image(
+    cosmic_canvas_img_handle: &mut Handle<Image>,
+    images: &mut ResMut<Assets<Image>>,
+    width: f32,
+    height: f32,
+    pixels: Vec<u8>,
+) {
+    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: width as u32,
+                height: 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: width as u32,
+                height: height as u32,
+                depth_or_array_layers: 1,
+            });
+        }
+    }
+}
+
+fn draw_pixel(
+    buffer: &mut [u8],
+    width: i32,
+    height: i32,
+    x: i32,
+    y: i32,
+    color: cosmic_text::Color,
+) {
+    // TODO: perftest this fn against previous iteration
+    let a_a = color.a() as u32;
+    if a_a == 0 {
+        // Do not draw if alpha is zero
+        return;
+    }
+
+    if y < 0 || y >= height {
+        // Skip if y out of bounds
+        return;
+    }
+
+    if x < 0 || x >= width {
+        // Skip if x out of bounds
+        return;
+    }
+
+    let offset = (y as usize * width as usize + x as usize) * 4;
+
+    let bg = Color::rgba_u8(
+        buffer[offset],
+        buffer[offset + 1],
+        buffer[offset + 2],
+        buffer[offset + 3],
+    );
+
+    // TODO: if alpha is 100% or bg is empty skip blending
+
+    let fg = Color::rgba_u8(color.r(), color.g(), color.b(), color.a());
+
+    let premul = fg * Vec3::splat(color.a() as f32 / 255.0);
+
+    let out = premul + bg * (1.0 - fg.a());
+
+    buffer[offset + 2] = (out.b() * 255.0) as u8;
+    buffer[offset + 1] = (out.g() * 255.0) as u8;
+    buffer[offset] = (out.r() * 255.0) as u8;
+    buffer[offset + 3] = (bg.a() * 255.0) as u8;
+}
+
+pub(crate) fn blink_cursor(
+    mut visibility: ResMut<CursorVisibility>,
+    mut timer: ResMut<CursorBlinkTimer>,
+    time: Res<Time>,
+    active_editor: ResMut<Focus>,
+    mut cosmic_editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>,
+    mut placeholder_editor_q: Query<&mut Placeholder, Without<ReadOnly>>,
+) {
+    if let Some(e) = active_editor.0 {
+        timer.0.tick(time.delta());
+        if !timer.0.just_finished() && !active_editor.is_changed() {
+            return;
+        }
+        visibility.0 = !visibility.0;
+
+        // always start cursor visible on focus
+        if active_editor.is_changed() {
+            visibility.0 = true;
+            timer.0.set_elapsed(Duration::ZERO);
+        }
+
+        let new_color = if visibility.0 {
+            None
+        } else {
+            Some(cosmic_text::Color::rgba(0, 0, 0, 0))
+        };
+
+        if let Ok(mut editor) = cosmic_editor_q.get_mut(e) {
+            let editor = &mut editor.0;
+            let mut cursor = editor.cursor();
+            cursor.color = new_color;
+            editor.set_cursor(cursor);
+            editor.buffer_mut().set_redraw(true);
+        }
+
+        if let Ok(mut placeholder) = placeholder_editor_q.get_mut(e) {
+            let placeholder = &mut placeholder.0 .0;
+            let mut cursor_p = placeholder.cursor();
+            cursor_p.color = new_color;
+            placeholder.set_cursor(cursor_p);
+            placeholder.buffer_mut().set_redraw(true);
+        }
+    }
+}
+
+pub(crate) fn freeze_cursor_blink(
+    mut visibility: ResMut<CursorVisibility>,
+    mut timer: ResMut<CursorBlinkTimer>,
+    active_editor: Res<Focus>,
+    keys: Res<Input<KeyCode>>,
+    char_evr: EventReader<ReceivedCharacter>,
+    mut editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>,
+) {
+    let inputs = [
+        KeyCode::Left,
+        KeyCode::Right,
+        KeyCode::Up,
+        KeyCode::Down,
+        KeyCode::Back,
+        KeyCode::Return,
+    ];
+    if !keys.any_pressed(inputs) && char_evr.is_empty() {
+        return;
+    }
+
+    if let Some(e) = active_editor.0 {
+        if let Ok(mut editor) = editor_q.get_mut(e) {
+            timer.0.set_elapsed(Duration::ZERO);
+            visibility.0 = true;
+            let mut cursor = editor.0.cursor();
+            cursor.color = None;
+            editor.0.set_cursor(cursor);
+            editor.0.buffer_mut().set_redraw(true);
+        }
+    }
+}
+
+pub(crate) fn hide_inactive_or_readonly_cursor(
+    mut cosmic_editor_q_readonly: Query<&mut CosmicEditor, With<ReadOnly>>,
+    mut cosmic_editor_q_placeholder: Query<(Entity, &mut Placeholder, Option<&ReadOnly>)>,
+    mut cosmic_editor_q_editable: Query<(Entity, &mut CosmicEditor), Without<ReadOnly>>,
+    active_editor: Res<Focus>,
+) {
+    for mut editor in &mut cosmic_editor_q_readonly.iter_mut() {
+        let mut cursor = editor.0.cursor();
+        cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
+        editor.0.set_cursor(cursor);
+        editor.0.buffer_mut().set_redraw(true);
+    }
+
+    if active_editor.is_changed() || active_editor.0.is_none() {
+        return;
+    }
+
+    for (e, mut editor, readonly_opt) in &mut cosmic_editor_q_placeholder.iter_mut() {
+        if e != active_editor.0.unwrap() || readonly_opt.is_some() {
+            let editor = &mut editor.0;
+            let mut cursor = editor.0.cursor();
+            cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
+            editor.0.set_cursor(cursor);
+            editor.0.buffer_mut().set_redraw(true);
+        }
+    }
+
+    for (e, mut editor) in &mut cosmic_editor_q_editable.iter_mut() {
+        if e != active_editor.0.unwrap() {
+            let mut cursor = editor.0.cursor();
+            cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0));
+            editor.0.set_cursor(cursor);
+            editor.0.buffer_mut().set_redraw(true);
+        }
+    }
+}
+
+pub(crate) fn on_scale_factor_change(
+    mut scale_factor_changed: EventReader<WindowScaleFactorChanged>,
+    mut cosmic_query: Query<(&mut CosmicEditor, &CosmicMetrics, &mut XOffset)>,
+    mut font_system: ResMut<CosmicFontSystem>,
+) {
+    if !scale_factor_changed.is_empty() {
+        let new_scale_factor = scale_factor_changed.iter().last().unwrap().scale_factor as f32;
+        for (mut editor, metrics, mut x_offset) in &mut cosmic_query.iter_mut() {
+            let font_system = &mut font_system.0;
+            let metrics =
+                Metrics::new(metrics.font_size, metrics.line_height).scale(new_scale_factor);
+
+            editor.0.buffer_mut().set_metrics(font_system, metrics);
+            editor.0.buffer_mut().set_redraw(true);
+
+            *x_offset = XOffset(None);
+        }
+    }
+}