Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,7 @@ dependencies = [
"profiling",
"ron",
"serde",
"unicode-segmentation",
]

[[package]]
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ web-time = "1.1.0" # Timekeeping for native and web
wgpu = { version = "24.0.0", default-features = false }
windows-sys = "0.59"
winit = { version = "0.30.7", default-features = false }
unicode-segmentation = "1.12.0"
Comment thread
MStarha marked this conversation as resolved.
Outdated

[workspace.lints.rust]
unsafe_code = "deny"
Expand Down
1 change: 1 addition & 0 deletions crates/egui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ ahash.workspace = true
bitflags.workspace = true
nohash-hasher.workspace = true
profiling.workspace = true
unicode-segmentation.workspace = true

#! ### Optional dependencies
accesskit = { version = "0.17.0", optional = true }
Expand Down
55 changes: 38 additions & 17 deletions crates/egui/src/text_selection/text_cursor_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use epaint::text::{
cursor::{CCursor, Cursor},
Galley,
};
use unicode_segmentation::UnicodeSegmentation;

use crate::{epaint, NumExt, Rect, Response, Ui};
use crate::{epaint, NumExt, Rect, Response, TextBuffer, Ui};

use super::{CCursorRange, CursorRange};

Expand Down Expand Up @@ -224,7 +225,7 @@ fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {

pub fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
CCursor {
index: next_word_boundary_char_index(text.chars(), ccursor.index),
index: next_word_boundary_char_index(text, ccursor.index),
prefer_next_row: false,
}
}
Expand All @@ -238,9 +239,10 @@ fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor {

pub fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
let num_chars = text.chars().count();
let reversed: String = text.graphemes(true).rev().collect();
CCursor {
index: num_chars
- next_word_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
- next_word_boundary_char_index(&reversed, num_chars - ccursor.index).min(num_chars),
prefer_next_row: true,
}
}
Expand All @@ -254,22 +256,28 @@ fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
}
}

fn next_word_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
let mut it = it.skip(index);
if let Some(_first) = it.next() {
index += 1;

if let Some(second) = it.next() {
index += 1;
for next in it {
if is_word_char(next) != is_word_char(second) {
break;
}
index += 1;
}
fn next_word_boundary_char_index(text: &str, index: usize) -> usize {
let start_byte_index = text.byte_index_from_char_index(index);
let words = text.split_word_bound_indices().collect::<Vec<_>>();

for i in 0..words.len() {
let bi = words[i].0;
// Splitting considers contiguous whitespace as one word, such words must be skipped
// such that the cursor jumps right after the next word (this is consistent with text
// editors or browsers), the naive approach would jump after the whitespace following
// the next word.
if bi > start_byte_index && i > 0 && skip_word(words[i - 1].1) {
return char_index_from_byte_index(text, bi);
}
}
index

char_index_from_byte_index(text, text.len())
}

fn skip_word(text: &str) -> bool {
// skip words that contain anything other than alphanumeric characters and underscore
// (i.e. whitespace, dashes, etc.)
!text.chars().any(|c| !(c.is_alphanumeric() || c == '_'))
}

fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
Expand Down Expand Up @@ -328,6 +336,19 @@ pub fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
s.len()
}

pub fn char_index_from_byte_index(input: &str, byte_index: usize) -> usize {
for (ci, (bi, _)) in input.char_indices().enumerate() {
if bi == byte_index {
return ci;
}
}

input
.char_indices()
.last()
.map_or(0, |(i, c)| i + c.len_utf8())
}

pub fn slice_char_range(s: &str, char_range: std::ops::Range<usize>) -> &str {
assert!(char_range.start <= char_range.end);
let start_byte = byte_index_from_char_index(s, char_range.start);
Expand Down
8 changes: 6 additions & 2 deletions crates/egui/src/widgets/text_edit/text_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use epaint::{

use crate::text_selection::{
text_cursor_state::{
byte_index_from_char_index, ccursor_next_word, ccursor_previous_word, find_line_start,
slice_char_range,
byte_index_from_char_index, ccursor_next_word, ccursor_previous_word,
char_index_from_byte_index, find_line_start, slice_char_range,
},
CursorRange,
};
Expand Down Expand Up @@ -51,6 +51,10 @@ pub trait TextBuffer {
byte_index_from_char_index(self.as_str(), char_index)
}

fn char_index_from_byte_index(&self, char_index: usize) -> usize {
char_index_from_byte_index(self.as_str(), char_index)
}

/// Clears all characters in this buffer
fn clear(&mut self) {
self.delete_char_range(0..self.as_str().len());
Expand Down