diff --git a/Cargo.lock b/Cargo.lock index e6eb69a527706ae4d1ff815c3a2fb86e2921f005..e70356486b16ce06a2f46cda493253c2637b18fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -416,6 +416,7 @@ dependencies = [ "insta", "js-sys", "sys-locale", + "unicode-segmentation", "util", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/Cargo.toml b/Cargo.toml index 2acddefec71c18c8ceec6a408a8d442752829a2a..2c425e21aa9fe12f9ddf1d57732b6403f91dc97c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ exclude = ["assets/*"] [features] multicam = [] -placeholder = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -33,10 +32,11 @@ bevy = { version = "0.13", default-features = false, features = [ ] } cosmic-text = { version = "0.11.2" } # TODO: move crossbeam to wasm32, once input.rs has separate wasm copy/paste fn +unicode-segmentation = { version = "1.11.0" } + crossbeam-channel = "0.5.8" image = "0.24.6" sys-locale = "0.3.0" - [target.'cfg(not(target_arch = "wasm32"))'.dependencies] arboard = "3.2.0" diff --git a/examples/password.rs b/examples/password.rs new file mode 100644 index 0000000000000000000000000000000000000000..0539f39557b5f438a56a570ade71930164928b4f --- /dev/null +++ b/examples/password.rs @@ -0,0 +1,47 @@ +use bevy::prelude::*; +use bevy_cosmic_edit::{password::Password, placeholder::Placeholder, *}; +use util::{change_active_editor_sprite, deselect_editor_on_esc, print_editor_text}; + +fn setup(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); + + // Sprite editor + commands.spawn(( + CosmicEditBundle { + max_lines: CosmicMaxLines(1), + mode: CosmicMode::InfiniteLine, + sprite_bundle: SpriteBundle { + // Sets size of text box + sprite: Sprite { + custom_size: Some(Vec2::new(300., 100.)), + ..default() + }, + // Position of text box + transform: Transform::from_xyz(0., 100., 0.), + ..default() + }, + ..default() + }, + Password::default(), + Placeholder::new("Password", Attrs::new()), + )); +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(CosmicEditPlugin { + change_cursor: CursorConfig::Default, + ..default() + }) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + change_active_editor_sprite, + deselect_editor_on_esc, + print_editor_text.after(KbInput), + ), + ) + .run(); +} diff --git a/src/buffer.rs b/src/buffer.rs index 1d32295ba20b873ad0403f7aa34f931e0f1056f6..1244e35b633c7ac39d5acee4ab042c7f576e2d05 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,6 +1,36 @@ use crate::*; use bevy::{prelude::*, window::PrimaryWindow}; +pub trait BufferExtras { + fn get_text(&self) -> String; +} + +impl BufferExtras for Buffer { + /// Retrieves the text content from a buffer. + /// + /// # Arguments + /// + /// * none, takes the rust magic ref to self + /// + /// # Returns + /// + /// A `String` containing the cosmic text content. + fn get_text(&self) -> String { + let mut text = String::new(); + let line_count = self.lines.len(); + + for (i, line) in self.lines.iter().enumerate() { + text.push_str(line.text()); + + if i < line_count - 1 { + text.push('\n'); + } + } + + text + } +} + #[derive(Component, Deref, DerefMut)] pub struct CosmicBuffer(pub Buffer); @@ -66,29 +96,6 @@ impl<'s, 'r> CosmicBuffer { self } - /// Retrieves the cosmic text content from a buffer. - /// - /// # Arguments - /// - /// * none, takes the rust magic ref to self - /// - /// # Returns - /// - /// A `String` containing the cosmic text content. - pub fn get_text(&self) -> String { - let mut text = String::new(); - let line_count = self.lines.len(); - - for (i, line) in self.lines.iter().enumerate() { - text.push_str(line.text()); - - if i < line_count - 1 { - text.push('\n'); - } - } - - text - } /// Returns texts from a MultiStyle buffer pub fn get_text_spans(&self, default_attrs: AttrsOwned) -> Vec<Vec<(String, AttrsOwned)>> { // TODO: untested! diff --git a/src/input.rs b/src/input.rs index 9bb5477d732c52e0822f1c48d120e36e0a5019b1..be89f1fe277b9f8583e8b88bb60838d7a75c2d51 100644 --- a/src/input.rs +++ b/src/input.rs @@ -20,7 +20,7 @@ use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use crate::{ - buffer::{get_x_offset_center, get_y_offset_center}, + buffer::{get_x_offset_center, get_y_offset_center, BufferExtras}, get_node_cursor_pos, CosmicBuffer, CosmicEditor, CosmicFontSystem, CosmicMaxChars, CosmicMaxLines, CosmicSource, CosmicTextChanged, CosmicTextPosition, FocusedWidget, ReadOnly, XOffset, @@ -227,40 +227,20 @@ pub(crate) fn input_mouse( } } -#[derive(Component)] -pub struct PasswordInput; // PLACEHOLDER bc this fn uses it's presence - -// TODO: split copy/paste into own fn, separate fn for wasm -pub(crate) fn input_kb( +pub fn kb_move_cursor( active_editor: Res<FocusedWidget>, keys: Res<ButtonInput<KeyCode>>, - mut char_evr: EventReader<ReceivedCharacter>, - mut cosmic_edit_query: Query<( - &mut CosmicEditor, - &mut CosmicBuffer, - &CosmicMaxLines, - &CosmicMaxChars, - Entity, - Option<&ReadOnly>, - Option<&PasswordInput>, - )>, - mut evw_changed: EventWriter<CosmicTextChanged>, + mut cosmic_edit_query: Query<(&mut CosmicEditor,)>, mut font_system: ResMut<CosmicFontSystem>, - mut is_deleting: Local<bool>, - _channel: Option<Res<WasmPasteAsyncChannel>>, ) { let Some(active_editor_entity) = active_editor.0 else { return; }; - - if let Ok((mut editor, buffer, max_lines, max_chars, entity, readonly_opt, password_opt)) = - cosmic_edit_query.get_mut(active_editor_entity) - { + if let Ok((mut editor,)) = cosmic_edit_query.get_mut(active_editor_entity) { if keys.get_just_pressed().len() != 0 { editor.cursor_visible = true; editor.cursor_timer.reset(); } - let readonly = readonly_opt.is_some(); let command = keypress_command(&keys); @@ -277,11 +257,11 @@ pub(crate) fn input_kb( command }; - let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]); - #[cfg(target_os = "macos")] let option = keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]); + let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]); + // if shift key is pressed let already_has_selection = editor.selection() != Selection::None; if shift && !already_has_selection { @@ -351,6 +331,78 @@ pub(crate) fn input_kb( } return; } + if keys.just_pressed(KeyCode::Escape) { + editor.action(&mut font_system.0, Action::Escape); + } + if command && keys.just_pressed(KeyCode::KeyA) { + editor.action(&mut font_system.0, Action::Motion(Motion::BufferEnd)); + let current_cursor = editor.cursor(); + editor.set_selection(Selection::Normal(Cursor { + line: 0, + index: 0, + affinity: current_cursor.affinity, + })); + return; + } + if keys.just_pressed(KeyCode::Home) { + editor.action(&mut font_system.0, Action::Motion(Motion::Home)); + if !shift { + editor.set_selection(Selection::None); + } + return; + } + if keys.just_pressed(KeyCode::End) { + editor.action(&mut font_system.0, Action::Motion(Motion::End)); + if !shift { + editor.set_selection(Selection::None); + } + return; + } + if keys.just_pressed(KeyCode::PageUp) { + editor.action(&mut font_system.0, Action::Motion(Motion::PageUp)); + if !shift { + editor.set_selection(Selection::None); + } + return; + } + if keys.just_pressed(KeyCode::PageDown) { + editor.action(&mut font_system.0, Action::Motion(Motion::PageDown)); + if !shift { + editor.set_selection(Selection::None); + } + } + } +} + +pub(crate) fn kb_input_text( + active_editor: Res<FocusedWidget>, + keys: Res<ButtonInput<KeyCode>>, + mut char_evr: EventReader<ReceivedCharacter>, + mut cosmic_edit_query: Query<( + &mut CosmicEditor, + &mut CosmicBuffer, + &CosmicMaxLines, + &CosmicMaxChars, + Entity, + Option<&ReadOnly>, + )>, + mut evw_changed: EventWriter<CosmicTextChanged>, + mut font_system: ResMut<CosmicFontSystem>, + mut is_deleting: Local<bool>, +) { + let Some(active_editor_entity) = active_editor.0 else { + return; + }; + + if let Ok((mut editor, buffer, max_lines, max_chars, entity, readonly_opt)) = + cosmic_edit_query.get_mut(active_editor_entity) + { + let command = keypress_command(&keys); + if keys.get_just_pressed().len() != 0 { + editor.cursor_visible = true; + editor.cursor_timer.reset(); + } + let readonly = readonly_opt.is_some(); if keys.just_pressed(KeyCode::Backspace) & !readonly { // fix for issue #8 @@ -389,65 +441,84 @@ pub(crate) fn input_kb( editor.action(&mut font_system.0, Action::Delete); editor.with_buffer_mut(|b| b.set_redraw(true)); } - if keys.just_pressed(KeyCode::Escape) { - editor.action(&mut font_system.0, Action::Escape); - } - if command && keys.just_pressed(KeyCode::KeyA) { - editor.action(&mut font_system.0, Action::Motion(Motion::BufferEnd)); - let current_cursor = editor.cursor(); - editor.set_selection(Selection::Normal(Cursor { - line: 0, - index: 0, - affinity: current_cursor.affinity, - })); - return; - } - if keys.just_pressed(KeyCode::Home) { - editor.action(&mut font_system.0, Action::Motion(Motion::Home)); - if !shift { - editor.set_selection(Selection::None); - } + + if readonly { return; } - if keys.just_pressed(KeyCode::End) { - editor.action(&mut font_system.0, Action::Motion(Motion::End)); - if !shift { - editor.set_selection(Selection::None); + + let mut is_edit = false; + let mut is_return = false; + if keys.just_pressed(KeyCode::Enter) { + is_return = true; + if (max_lines.0 == 0 || buffer.lines.len() < max_lines.0) + && (max_chars.0 == 0 || buffer.get_text().len() < max_chars.0) + { + // to have new line on wasm rather than E + is_edit = true; + editor.action(&mut font_system.0, Action::Insert('\n')); } - return; } - if keys.just_pressed(KeyCode::PageUp) { - editor.action(&mut font_system.0, Action::Motion(Motion::PageUp)); - if !shift { - editor.set_selection(Selection::None); + + if !is_return { + for char_ev in char_evr.read() { + is_edit = true; + if *is_deleting { + editor.action(&mut font_system.0, Action::Backspace); + } else if !command && (max_chars.0 == 0 || buffer.get_text().len() < max_chars.0) { + let b = char_ev.char.as_bytes(); + for c in b { + let c: char = (*c).into(); + editor.action(&mut font_system.0, Action::Insert(c)); + } + } } - return; } - if keys.just_pressed(KeyCode::PageDown) { - editor.action(&mut font_system.0, Action::Motion(Motion::PageDown)); - if !shift { - editor.set_selection(Selection::None); - } + + if !is_edit { return; } + evw_changed.send(CosmicTextChanged((entity, buffer.get_text()))); + } +} + +pub fn kb_clipboard( + active_editor: Res<FocusedWidget>, + keys: Res<ButtonInput<KeyCode>>, + mut evw_changed: EventWriter<CosmicTextChanged>, + mut font_system: ResMut<CosmicFontSystem>, + mut cosmic_edit_query: Query<( + &mut CosmicEditor, + &mut CosmicBuffer, + &CosmicMaxLines, + &CosmicMaxChars, + Entity, + Option<&ReadOnly>, + )>, + _channel: Option<Res<WasmPasteAsyncChannel>>, +) { + let Some(active_editor_entity) = active_editor.0 else { + return; + }; + + if let Ok((mut editor, buffer, max_lines, max_chars, entity, readonly_opt)) = + cosmic_edit_query.get_mut(active_editor_entity) + { + let command = keypress_command(&keys); + + let readonly = readonly_opt.is_some(); + let mut is_clipboard = false; #[cfg(not(target_arch = "wasm32"))] { if let Ok(mut clipboard) = arboard::Clipboard::new() { if command && keys.just_pressed(KeyCode::KeyC) { - if password_opt.is_some() { - return; - } if let Some(text) = editor.copy_selection() { clipboard.set_text(text).unwrap(); return; } } if command && keys.just_pressed(KeyCode::KeyX) && !readonly { - if password_opt.is_some() { - return; - } if let Some(text) = editor.copy_selection() { clipboard.set_text(text).unwrap(); editor.delete_selection(); @@ -463,10 +534,6 @@ pub(crate) fn input_kb( editor.action(&mut font_system.0, Action::Insert(c)); } } else { - if password_opt.is_some() && c.len_utf8() > 1 { - println!("Cannot input multi-byte char '{}' to password field! See https://github.com/StaffEngineer/bevy_cosmic_edit/pull/99#issuecomment-1782607486",c); - continue; - } editor.action(&mut font_system.0, Action::Insert(c)); } } @@ -480,9 +547,6 @@ pub(crate) fn input_kb( #[cfg(target_arch = "wasm32")] { if command && keys.just_pressed(KeyCode::KeyC) { - if password_opt.is_some() { - return; - } if let Some(text) = editor.copy_selection() { write_clipboard_wasm(text.as_str()); return; @@ -490,9 +554,6 @@ pub(crate) fn input_kb( } if command && keys.just_pressed(KeyCode::KeyX) && !readonly { - if password_opt.is_some() { - return; - } if let Some(text) = editor.copy_selection() { write_clipboard_wasm(text.as_str()); editor.delete_selection(); @@ -517,43 +578,7 @@ pub(crate) fn input_kb( } } - if readonly { - return; - } - - let mut is_edit = is_clipboard; - let mut is_return = false; - if keys.just_pressed(KeyCode::Enter) { - is_return = true; - if (max_lines.0 == 0 || buffer.lines.len() < max_lines.0) - && (max_chars.0 == 0 || buffer.get_text().len() < max_chars.0) - { - // to have new line on wasm rather than E - is_edit = true; - editor.action(&mut font_system.0, Action::Insert('\n')); - } - } - - if !(is_clipboard || is_return) { - for char_ev in char_evr.read() { - is_edit = true; - if *is_deleting { - editor.action(&mut font_system.0, Action::Backspace); - } else if !command && (max_chars.0 == 0 || buffer.get_text().len() < max_chars.0) { - if password_opt.is_some() && char_ev.char.len() > 1 { - println!("Cannot input multi-byte char '{}' to password field! See https://github.com/StaffEngineer/bevy_cosmic_edit/pull/99#issuecomment-1782607486",char_ev.char); - continue; - } - let b = char_ev.char.as_bytes(); - for c in b { - let c: char = (*c).into(); - editor.action(&mut font_system.0, Action::Insert(c)); - } - } - } - } - - if !is_edit { + if !is_clipboard { return; } @@ -616,7 +641,6 @@ pub fn poll_wasm_paste( &crate::DefaultAttrs, &CosmicMaxChars, &CosmicMaxChars, - Option<&PasswordInput>, ), Without<ReadOnly>, >, @@ -627,7 +651,7 @@ pub fn poll_wasm_paste( match inlet { Ok(inlet) => { let entity = inlet.entity; - if let Ok((mut editor, mut buffer, attrs, max_chars, max_lines, password_opt)) = + if let Ok((mut editor, mut buffer, attrs, max_chars, max_lines)) = editor_q.get_mut(entity) { let text = inlet.text; @@ -639,10 +663,6 @@ pub fn poll_wasm_paste( editor.action(&mut font_system.0, Action::Insert(c)); } } else { - if password_opt.is_some() && c.len_utf8() > 1 { - info!("Cannot input multi-byte char '{}' to password field! See https://github.com/StaffEngineer/bevy_cosmic_edit/pull/99#issuecomment-1782607486",c); - continue; - } editor.action(&mut font_system.0, Action::Insert(c)); } } diff --git a/src/lib.rs b/src/lib.rs index 726f5a242712ffeace3c930240f7ae0691c553c0..1cc13e040cc458929113ac5fb4116b6ef813a7af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ use cosmic_text::{Buffer, Editor, FontSystem, SwashCache}; use cursor::{change_cursor, hover_sprites, hover_ui}; pub use cursor::{TextHoverIn, TextHoverOut}; pub use focus::*; -use input::{input_kb, input_mouse, ClickTimer}; +use input::{input_mouse, kb_clipboard, kb_input_text, kb_move_cursor, ClickTimer}; #[cfg(target_arch = "wasm32")] use input::{poll_wasm_paste, WasmPaste, WasmPasteAsyncChannel}; use layout::{ @@ -203,6 +203,9 @@ impl Default for CosmicFontConfig { #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] pub struct KbInput; +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub struct Render; + /// Plugin struct that adds systems and initializes resources related to cosmic edit functionality. #[derive(Default)] pub struct CosmicEditPlugin { @@ -237,7 +240,12 @@ impl Plugin for CosmicEditPlugin { .add_systems(PreUpdate, (input_mouse,).chain()) .add_systems( Update, - (input_kb, reshape, blink_cursor).chain().in_set(KbInput), + ( + (kb_move_cursor, kb_input_text, kb_clipboard, reshape) + .chain() + .in_set(KbInput), + blink_cursor, + ), ) .add_systems( PostUpdate, @@ -248,6 +256,7 @@ impl Plugin for CosmicEditPlugin { render_texture, ) .chain() + .in_set(Render) .after(TransformSystem::TransformPropagate), ) .init_resource::<FocusedWidget>() @@ -284,8 +293,8 @@ impl Plugin for CosmicEditPlugin { } fn add_feature_plugins(app: &mut App) -> &mut App { - #[cfg(feature = "placeholder")] app.add_plugins(plugins::placeholder::PlaceholderPlugin); + app.add_plugins(plugins::password::PasswordPlugin); app } diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 2c07c75b01ca1a0c47da5a42c846fcb4bd87a1d0..08a7f609753d4e9fc21c3d47d18b1c76649df80d 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -1,2 +1,2 @@ -#[cfg(feature = "placeholder")] +pub mod password; pub mod placeholder; diff --git a/src/plugins/password/mod.rs b/src/plugins/password/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..a36109c8e5bfc303172a59f6a326a95ce4863983 --- /dev/null +++ b/src/plugins/password/mod.rs @@ -0,0 +1,200 @@ +use crate::{buffer::BufferExtras, placeholder::Placeholder}; +use bevy::prelude::*; +use cosmic_text::{Cursor, Edit, Selection, Shaping}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{ + input::{input_mouse, kb_input_text, kb_move_cursor}, + CosmicBuffer, CosmicEditor, CosmicFontSystem, DefaultAttrs, Render, +}; + +pub struct PasswordPlugin; + +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub struct PasswordSet; + +impl Plugin for PasswordPlugin { + fn build(&self, app: &mut App) { + app.add_systems(PreUpdate, (hide_password_text.before(input_mouse),)) + .add_systems( + Update, + (restore_password_text + .before(kb_input_text) + .after(kb_move_cursor),), + ) + .add_systems( + PostUpdate, + ( + hide_password_text.before(Render).in_set(PasswordSet), + restore_password_text.after(Render), + ), + ); + } +} + +#[derive(Component)] +pub struct Password { + real_text: String, + glyph: char, +} + +impl Default for Password { + fn default() -> Self { + Self { + real_text: Default::default(), + glyph: 'â—', + } + } +} + +fn hide_password_text( + mut q: Query<( + &mut Password, + &mut CosmicBuffer, + &DefaultAttrs, + Option<&mut CosmicEditor>, + Option<&Placeholder>, + )>, + mut font_system: ResMut<CosmicFontSystem>, +) { + for (mut password, mut buffer, attrs, editor_opt, placeholder_opt) in q.iter_mut() { + if let Some(placeholder) = placeholder_opt { + if placeholder.is_active() { + continue; + } + } + if let Some(mut editor) = editor_opt { + let mut cursor = editor.cursor(); + let mut selection = editor.selection(); + + editor.with_buffer_mut(|buffer| { + let text = buffer.get_text(); + + // Translate cursor to correct position for blocker glyphs + let translate_cursor = |c: &mut Cursor| { + let (pre, _post) = text.split_at(c.index); + let graphemes = pre.graphemes(true).count(); + c.index = graphemes * password.glyph.len_utf8(); + }; + + translate_cursor(&mut cursor); + + // Translate selection cursor + match selection { + Selection::None => {} + Selection::Line(ref mut c) => { + translate_cursor(c); + } + Selection::Word(ref mut c) => { + translate_cursor(c); + } + Selection::Normal(ref mut c) => { + translate_cursor(c); + } + } + + // Update text to blockers + buffer.set_text( + &mut font_system, + password + .glyph + .to_string() + .repeat(text.graphemes(true).count()) + .as_str(), + attrs.as_attrs(), + Shaping::Advanced, + ); + + password.real_text = text; + }); + + editor.set_cursor(cursor); + editor.set_selection(selection); + + continue; + } + + let text = buffer.get_text(); + + buffer.set_text( + &mut font_system, + password.glyph.to_string().repeat(text.len()).as_str(), + attrs.as_attrs(), + ); + password.real_text = text; + } +} + +fn restore_password_text( + mut q: Query<( + &Password, + &mut CosmicBuffer, + &DefaultAttrs, + Option<&mut CosmicEditor>, + Option<&Placeholder>, + )>, + mut font_system: ResMut<CosmicFontSystem>, +) { + for (password, mut buffer, attrs, editor_opt, placeholder_opt) in q.iter_mut() { + if let Some(placeholder) = placeholder_opt { + if placeholder.is_active() { + continue; + } + } + if let Some(mut editor) = editor_opt { + let mut cursor = editor.cursor(); + let mut selection = editor.selection(); + + editor.with_buffer_mut(|buffer| { + let text = buffer.get_text(); + + // Find cursor position and translate back to correct position in real text + let restore_cursor = |c: &mut Cursor| { + let (pre, _post) = text.split_at(c.index); + let graphemes = pre.graphemes(true).count(); + let mut n_i = 0; + if let Some((i, _)) = password.real_text.grapheme_indices(true).nth(graphemes) { + n_i = i; + } else if c.index > 0 { + n_i = password.real_text.len(); + } + c.index = n_i; + }; + + restore_cursor(&mut cursor); + + // Translate selection cursor + match selection { + Selection::None => {} + Selection::Line(ref mut c) => { + restore_cursor(c); + } + Selection::Word(ref mut c) => { + restore_cursor(c); + } + Selection::Normal(ref mut c) => { + restore_cursor(c); + } + } + + buffer.set_text( + &mut font_system, + password.real_text.as_str(), + attrs.as_attrs(), + Shaping::Advanced, + ); + }); + + editor.set_cursor(cursor); + editor.set_selection(selection); + + continue; + } + + buffer.set_text( + &mut font_system, + password.real_text.as_str(), + attrs.as_attrs(), + ); + } +} diff --git a/src/plugins/placeholder/mod.rs b/src/plugins/placeholder/mod.rs index 65d7b704b31b092676722bda9eb64a5c0c4d6678..a1807795af188ba9753dd358b753d691880e791c 100644 --- a/src/plugins/placeholder/mod.rs +++ b/src/plugins/placeholder/mod.rs @@ -1,3 +1,4 @@ +use crate::{buffer::BufferExtras, Render}; use bevy::prelude::*; use cosmic_text::{Attrs, Edit}; @@ -20,6 +21,10 @@ impl Placeholder { attrs, } } + + pub fn is_active(&self) -> bool { + self.active + } } pub struct PlaceholderPlugin; @@ -35,7 +40,8 @@ impl Plugin for PlaceholderPlugin { remove_placeholder_on_input, ) .chain() - .after(KbInput), + .after(KbInput) + .before(Render), ); } }