From 1c0c81130ef32d5caaad221a04a910702a37950e Mon Sep 17 00:00:00 2001
From: sam edelsten <samedelsten1@gmail.com>
Date: Mon, 29 Apr 2024 10:11:08 +0100
Subject: [PATCH] fix selection panics, add `get_text` to `Buffer`

---
 Cargo.toml                     |   2 +-
 src/buffer.rs                  |  53 +++++++------
 src/input.rs                   |   3 +-
 src/plugins/password/mod.rs    | 133 +++++++++++++++++----------------
 src/plugins/placeholder/mod.rs |   1 +
 5 files changed, 103 insertions(+), 89 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 03eb4da..b4dc8d0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,7 +37,7 @@ cosmic-text = { version = "0.11.2" }
 crossbeam-channel = "0.5.8"
 image = "0.24.6"
 sys-locale = "0.3.0"
-unicode-segmentation = { optional = true }
+unicode-segmentation = { version = "1.11.0", optional = true }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 arboard = "3.2.0"
diff --git a/src/buffer.rs b/src/buffer.rs
index 1d32295..1244e35 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 c686244..aed352f 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,
@@ -370,7 +370,6 @@ pub fn kb_move_cursor(
             if !shift {
                 editor.set_selection(Selection::None);
             }
-            return;
         }
     }
 }
diff --git a/src/plugins/password/mod.rs b/src/plugins/password/mod.rs
index d890369..7a7d045 100644
--- a/src/plugins/password/mod.rs
+++ b/src/plugins/password/mod.rs
@@ -1,5 +1,6 @@
+use crate::buffer::BufferExtras;
 use bevy::prelude::*;
-use cosmic_text::{Buffer, Edit, Shaping};
+use cosmic_text::{Cursor, Edit, Selection, Shaping};
 use unicode_segmentation::UnicodeSegmentation;
 
 use crate::{
@@ -11,29 +12,20 @@ pub struct PasswordPlugin;
 
 impl Plugin for PasswordPlugin {
     fn build(&self, app: &mut App) {
-        app.add_systems(
-            PreUpdate,
-            (
-                hide_password_text.before(input_mouse),
-                restore_password_text.after(input_mouse),
-            ),
-        )
-        .add_systems(
-            Update,
-            (
-                hide_password_text.before(kb_move_cursor),
-                restore_password_text
+        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),
-                restore_password_text.after(Render),
-            ),
-        );
+                    .after(kb_move_cursor),),
+            )
+            .add_systems(
+                PostUpdate,
+                (
+                    hide_password_text.before(Render),
+                    restore_password_text.after(Render),
+                ),
+            );
     }
 }
 
@@ -52,22 +44,6 @@ impl Default for Password {
     }
 }
 
-// TODO: impl this on buffer
-fn get_text(buffer: &mut Buffer) -> String {
-    let mut text = String::new();
-    let line_count = buffer.lines.len();
-
-    for (i, line) in buffer.lines.iter().enumerate() {
-        text.push_str(line.text());
-
-        if i < line_count - 1 {
-            text.push('\n');
-        }
-    }
-
-    text
-}
-
 fn hide_password_text(
     mut q: Query<(
         &mut Password,
@@ -80,16 +56,35 @@ fn hide_password_text(
     for (mut password, mut buffer, attrs, editor_opt) in q.iter_mut() {
         if let Some(mut editor) = editor_opt {
             let mut cursor = editor.cursor();
+            let mut selection = editor.selection();
 
             editor.with_buffer_mut(|buffer| {
-                let text = get_text(buffer);
-
-                let (pre, _post) = text.split_at(cursor.index);
-
-                let graphemes = pre.graphemes(true).count();
-
-                cursor.index = graphemes * password.glyph.len_utf8();
+                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
@@ -105,6 +100,7 @@ fn hide_password_text(
             });
 
             editor.set_cursor(cursor);
+            editor.set_selection(selection);
 
             continue;
         }
@@ -132,26 +128,38 @@ fn restore_password_text(
     for (password, mut buffer, attrs, editor_opt) in q.iter_mut() {
         if let Some(mut editor) = editor_opt {
             let mut cursor = editor.cursor();
-            let mut index = 0;
+            let mut selection = editor.selection();
 
             editor.with_buffer_mut(|buffer| {
-                let text = get_text(buffer);
-                let (pre, _post) = text.split_at(cursor.index);
-
-                let grapheme_count = pre.graphemes(true).count();
-
-                let mut g_idx = 0;
-                for (i, _c) in password.real_text.grapheme_indices(true) {
-                    if g_idx == grapheme_count {
-                        index = i;
+                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();
                     }
-                    g_idx += 1;
-                }
+                    c.index = n_i;
+                };
 
-                // TODO: save/restore with selection bounds
+                restore_cursor(&mut cursor);
 
-                if cursor.index > 0 && index == 0 {
-                    index = password.real_text.len();
+                // 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(
@@ -162,9 +170,8 @@ fn restore_password_text(
                 );
             });
 
-            cursor.index = index;
-
             editor.set_cursor(cursor);
+            editor.set_selection(selection);
 
             continue;
         }
diff --git a/src/plugins/placeholder/mod.rs b/src/plugins/placeholder/mod.rs
index 65d7b70..7fa3d94 100644
--- a/src/plugins/placeholder/mod.rs
+++ b/src/plugins/placeholder/mod.rs
@@ -1,3 +1,4 @@
+use crate::buffer::BufferExtras;
 use bevy::prelude::*;
 use cosmic_text::{Attrs, Edit};
 
-- 
GitLab