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,