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); + } + } +}