From 800db964ab2cd521b23ef47a6a7d30494b414a9a Mon Sep 17 00:00:00 2001
From: sam edelsten <samedelsten1@gmail.com>
Date: Fri, 26 Apr 2024 12:59:20 +0100
Subject: [PATCH] refactor kb input, fix widechar password blockers

---
 examples/password.rs        |   2 +-
 src/input.rs                | 224 ++++++++++++++++++++++--------------
 src/lib.rs                  |   9 +-
 src/plugins/password/mod.rs |  34 +++++-
 4 files changed, 177 insertions(+), 92 deletions(-)

diff --git a/examples/password.rs b/examples/password.rs
index 28f92dc..5dc4d50 100644
--- a/examples/password.rs
+++ b/examples/password.rs
@@ -39,7 +39,7 @@ fn main() {
             (
                 change_active_editor_sprite,
                 deselect_editor_on_esc,
-                print_editor_text,
+                print_editor_text.after(KbInput),
             ),
         )
         .run();
diff --git a/src/input.rs b/src/input.rs
index e0cc543..c686244 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -227,36 +227,20 @@ pub(crate) fn input_mouse(
     }
 }
 
-// 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>,
-    )>,
-    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)) =
-        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);
 
@@ -273,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 {
@@ -347,6 +331,80 @@ 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);
+            }
+            return;
+        }
+    }
+}
+
+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>,
+    _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);
+        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
@@ -385,48 +443,72 @@ 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>,
+    )>,
+) {
+    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"))]
         {
@@ -497,39 +579,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) {
-                    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;
         }
 
diff --git a/src/lib.rs b/src/lib.rs
index 3028487..308de2c 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::{
@@ -240,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,
diff --git a/src/plugins/password/mod.rs b/src/plugins/password/mod.rs
index 7ee6749..3c1c6df 100644
--- a/src/plugins/password/mod.rs
+++ b/src/plugins/password/mod.rs
@@ -2,7 +2,8 @@ use bevy::prelude::*;
 use cosmic_text::{Buffer, Edit, Shaping};
 
 use crate::{
-    input::input_mouse, CosmicBuffer, CosmicEditor, CosmicFontSystem, DefaultAttrs, Render,
+    input::{input_mouse, kb_input_text, kb_move_cursor},
+    CosmicBuffer, CosmicEditor, CosmicFontSystem, DefaultAttrs, Render,
 };
 
 pub struct PasswordPlugin;
@@ -16,6 +17,15 @@ impl Plugin for PasswordPlugin {
                 restore_password_text.after(input_mouse),
             ),
         )
+        .add_systems(
+            Update,
+            (
+                hide_password_text.before(kb_move_cursor),
+                restore_password_text
+                    .before(kb_input_text)
+                    .after(kb_move_cursor),
+            ),
+        )
         .add_systems(
             PostUpdate,
             (
@@ -36,7 +46,7 @@ impl Default for Password {
     fn default() -> Self {
         Self {
             real_text: Default::default(),
-            glyph: '*',
+            glyph: '●',
         }
     }
 }
@@ -75,13 +85,28 @@ fn hide_password_text(
                     attrs.as_attrs(),
                     Shaping::Advanced,
                 );
+
+                for (i, c) in text.char_indices() {
+                    if !text.is_char_boundary(i) || c.len_utf8() > 1 {
+                        panic!("Widechars (like {c}) are not yet supported in password fields.")
+                    }
+                }
+
                 password.real_text = text;
             });
 
+            let mut cursor = editor.cursor();
+            cursor.index *= password.glyph.len_utf8(); // HACK: multiply cursor position assuming no widechars are input
+                                                       // TODO: Count characters until cursor and set new position accordingly,
+                                                       // noting the previous location for restoring
+                                                       // TODO: Look into unicode graphemes
+            editor.set_cursor(cursor);
+
             continue;
         }
 
         let text = buffer.get_text();
+
         buffer.set_text(
             &mut font_system,
             password.glyph.to_string().repeat(text.len()).as_str(),
@@ -110,6 +135,11 @@ fn restore_password_text(
                     Shaping::Advanced,
                 )
             });
+
+            let mut cursor = editor.cursor();
+            cursor.index /= password.glyph.len_utf8(); // HACK: restore cursor position assuming no widechars are input
+            editor.set_cursor(cursor);
+
             continue;
         }
 
-- 
GitLab