use epaint::text::{Galley, cursor::CCursor};
use unicode_segmentation::UnicodeSegmentation as _;
use crate::{NumExt as _, Rect, Response, Ui, epaint};
use super::CCursorRange;
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct TextCursorState {
ccursor_range: Option<CCursorRange>,
}
impl From<CCursorRange> for TextCursorState {
fn from(ccursor_range: CCursorRange) -> Self {
Self {
ccursor_range: Some(ccursor_range),
}
}
}
impl TextCursorState {
pub fn is_empty(&self) -> bool {
self.ccursor_range.is_none()
}
pub fn char_range(&self) -> Option<CCursorRange> {
self.ccursor_range
}
pub fn range(&self, galley: &Galley) -> Option<CCursorRange> {
self.ccursor_range.map(|mut range| {
range.primary = galley.clamp_cursor(&range.primary);
range.secondary = galley.clamp_cursor(&range.secondary);
range
})
}
pub fn set_char_range(&mut self, ccursor_range: Option<CCursorRange>) {
self.ccursor_range = ccursor_range;
}
}
impl TextCursorState {
pub fn pointer_interaction(
&mut self,
ui: &Ui,
response: &Response,
cursor_at_pointer: CCursor,
galley: &Galley,
is_being_dragged: bool,
) -> bool {
let text = galley.text();
if response.double_clicked() {
let ccursor_range = select_word_at(text, cursor_at_pointer);
self.set_char_range(Some(ccursor_range));
true
} else if response.triple_clicked() {
let ccursor_range = select_line_at(text, cursor_at_pointer);
self.set_char_range(Some(ccursor_range));
true
} else if response.sense.senses_drag() {
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
if ui.input(|i| i.modifiers.shift) {
if let Some(mut cursor_range) = self.range(galley) {
cursor_range.primary = cursor_at_pointer;
self.set_char_range(Some(cursor_range));
} else {
self.set_char_range(Some(CCursorRange::one(cursor_at_pointer)));
}
} else {
self.set_char_range(Some(CCursorRange::one(cursor_at_pointer)));
}
true
} else if is_being_dragged {
if let Some(mut cursor_range) = self.range(galley) {
cursor_range.primary = cursor_at_pointer;
self.set_char_range(Some(cursor_range));
}
true
} else {
false
}
} else {
false
}
}
}
fn select_word_at(text: &str, ccursor: CCursor) -> CCursorRange {
if ccursor.index == 0 {
CCursorRange::two(ccursor, ccursor_next_word(text, ccursor))
} else {
let it = text.chars();
let mut it = it.skip(ccursor.index - 1);
if let Some(char_before_cursor) = it.next() {
if let Some(char_after_cursor) = it.next() {
if is_word_char(char_before_cursor) && is_word_char(char_after_cursor) {
let min = ccursor_previous_word(text, ccursor + 1);
let max = ccursor_next_word(text, min);
CCursorRange::two(min, max)
} else if is_word_char(char_before_cursor) {
let min = ccursor_previous_word(text, ccursor);
let max = ccursor_next_word(text, min);
CCursorRange::two(min, max)
} else if is_word_char(char_after_cursor) {
let max = ccursor_next_word(text, ccursor);
CCursorRange::two(ccursor, max)
} else {
let min = ccursor_previous_word(text, ccursor);
let max = ccursor_next_word(text, ccursor);
CCursorRange::two(min, max)
}
} else {
let min = ccursor_previous_word(text, ccursor);
CCursorRange::two(min, ccursor)
}
} else {
let max = ccursor_next_word(text, ccursor);
CCursorRange::two(ccursor, max)
}
}
}
fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
if ccursor.index == 0 {
CCursorRange::two(ccursor, ccursor_next_line(text, ccursor))
} else {
let it = text.chars();
let mut it = it.skip(ccursor.index - 1);
if let Some(char_before_cursor) = it.next() {
if let Some(char_after_cursor) = it.next() {
if (!is_linebreak(char_before_cursor)) && (!is_linebreak(char_after_cursor)) {
let min = ccursor_previous_line(text, ccursor + 1);
let max = ccursor_next_line(text, min);
CCursorRange::two(min, max)
} else if !is_linebreak(char_before_cursor) {
let min = ccursor_previous_line(text, ccursor);
let max = ccursor_next_line(text, min);
CCursorRange::two(min, max)
} else if !is_linebreak(char_after_cursor) {
let max = ccursor_next_line(text, ccursor);
CCursorRange::two(ccursor, max)
} else {
let min = ccursor_previous_line(text, ccursor);
let max = ccursor_next_line(text, ccursor);
CCursorRange::two(min, max)
}
} else {
let min = ccursor_previous_line(text, ccursor);
CCursorRange::two(min, ccursor)
}
} else {
let max = ccursor_next_line(text, ccursor);
CCursorRange::two(ccursor, max)
}
}
}
pub fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
CCursor {
index: next_word_boundary_char_index(text, ccursor.index),
prefer_next_row: false,
}
}
fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor {
CCursor {
index: next_line_boundary_char_index(text.chars(), ccursor.index),
prefer_next_row: false,
}
}
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(&reversed, num_chars - ccursor.index).min(num_chars),
prefer_next_row: true,
}
}
fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
let num_chars = text.chars().count();
CCursor {
index: num_chars
- next_line_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
prefer_next_row: true,
}
}
fn next_word_boundary_char_index(text: &str, index: usize) -> usize {
for word in text.split_word_bound_indices() {
let ci = char_index_from_byte_index(text, word.0);
if ci > index && !skip_word(word.1) {
return ci;
}
}
char_index_from_byte_index(text, text.len())
}
fn skip_word(text: &str) -> bool {
!text.chars().any(|c| !is_word_char(c))
}
fn next_line_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_linebreak(next) != is_linebreak(second) {
break;
}
index += 1;
}
}
}
index
}
pub fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn is_linebreak(c: char) -> bool {
c == '\r' || c == '\n'
}
pub fn find_line_start(text: &str, current_index: CCursor) -> CCursor {
let chars_count = text.chars().count();
let position = text
.chars()
.rev()
.skip(chars_count - current_index.index)
.position(|x| x == '\n');
match position {
Some(pos) => CCursor::new(current_index.index - pos),
None => CCursor::new(0),
}
}
pub fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
for (ci, (bi, _)) in s.char_indices().enumerate() {
if ci == char_index {
return bi;
}
}
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, _)| i + 1)
}
pub fn slice_char_range(s: &str, char_range: std::ops::Range<usize>) -> &str {
assert!(
char_range.start <= char_range.end,
"Invalid range, start must be less than end, but start = {}, end = {}",
char_range.start,
char_range.end
);
let start_byte = byte_index_from_char_index(s, char_range.start);
let end_byte = byte_index_from_char_index(s, char_range.end);
&s[start_byte..end_byte]
}
pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect {
let mut cursor_pos = galley.pos_from_cursor(*cursor);
cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height);
cursor_pos = cursor_pos.expand(1.5);
cursor_pos
}
#[cfg(test)]
mod test {
use crate::text_selection::text_cursor_state::next_word_boundary_char_index;
#[test]
fn test_next_word_boundary_char_index() {
let text = "abc d3f g_h i-j";
assert_eq!(next_word_boundary_char_index(text, 1), 3);
assert_eq!(next_word_boundary_char_index(text, 3), 7);
assert_eq!(next_word_boundary_char_index(text, 9), 11);
assert_eq!(next_word_boundary_char_index(text, 12), 13);
assert_eq!(next_word_boundary_char_index(text, 13), 15);
assert_eq!(next_word_boundary_char_index(text, 15), 15);
assert_eq!(next_word_boundary_char_index("", 0), 0);
assert_eq!(next_word_boundary_char_index("", 1), 0);
let text = "❤️👍 skvělá knihovna 👍❤️";
assert_eq!(next_word_boundary_char_index(text, 0), 2);
assert_eq!(next_word_boundary_char_index(text, 2), 3); assert_eq!(next_word_boundary_char_index(text, 6), 10);
assert_eq!(next_word_boundary_char_index(text, 9), 10);
assert_eq!(next_word_boundary_char_index(text, 12), 19);
assert_eq!(next_word_boundary_char_index(text, 15), 19);
assert_eq!(next_word_boundary_char_index(text, 19), 20);
assert_eq!(next_word_boundary_char_index(text, 20), 21);
}
}