use std::cell::RefCell;
use std::rc::Rc;
use crate::text::measure_advance;
use crate::text::Font;
use crate::undo::UndoRedoCommand;
pub fn prev_char_boundary(s: &str, byte_pos: usize) -> usize {
let mut pos = byte_pos;
loop {
if pos == 0 {
return 0;
}
pos -= 1;
if s.is_char_boundary(pos) {
return pos;
}
}
}
pub fn next_char_boundary(s: &str, byte_pos: usize) -> usize {
let mut pos = byte_pos + 1;
while pos <= s.len() {
if s.is_char_boundary(pos) {
return pos;
}
pos += 1;
}
s.len()
}
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
pub fn next_word_boundary(s: &str, pos: usize) -> usize {
let mut chars = s[pos..].char_indices().peekable();
let mut advanced = 0usize;
while let Some(&(i, c)) = chars.peek() {
if !c.is_whitespace() {
break;
}
advanced = i + c.len_utf8();
chars.next();
}
while let Some(&(i, c)) = chars.peek() {
if c.is_whitespace() {
break;
}
advanced = i + c.len_utf8();
chars.next();
}
pos + advanced
}
pub fn prev_word_boundary(s: &str, pos: usize) -> usize {
if pos == 0 {
return 0;
}
let chars: Vec<(usize, char)> = s[..pos].char_indices().collect();
let mut i = chars.len();
while i > 0 && chars[i - 1].1.is_whitespace() {
i -= 1;
}
while i > 0 && !chars[i - 1].1.is_whitespace() {
i -= 1;
}
if i < chars.len() {
chars[i].0
} else {
0
}
}
pub fn word_range_at(s: &str, byte_pos: usize) -> (usize, usize) {
let anchor_class = is_word_char(s[byte_pos..].chars().next().unwrap_or(' '));
let start = {
let mut p = byte_pos;
while p > 0 {
let prev = prev_char_boundary(s, p);
let c = s[prev..p].chars().next().unwrap_or(' ');
if is_word_char(c) != anchor_class {
break;
}
p = prev;
}
p
};
let end = {
let mut p = byte_pos;
for (_, c) in s[byte_pos..].char_indices() {
if is_word_char(c) != anchor_class {
break;
}
p += c.len_utf8();
}
p
};
(start, end)
}
pub fn paragraph_range_at(s: &str, byte_pos: usize) -> (usize, usize) {
let pos = byte_pos.min(s.len());
let start = s[..pos].rfind('\n').map(|i| i + 1).unwrap_or(0);
let end = s[pos..].find('\n').map(|i| pos + i).unwrap_or(s.len());
(start, end)
}
pub fn byte_at_x(font: &Font, text: &str, font_size: f64, target_x: f64) -> usize {
if target_x <= 0.0 {
return 0;
}
let mut prev_x = 0.0f64;
let mut prev_pos = 0usize;
for (i, c) in text.char_indices() {
let x = measure_advance(font, &text[..i], font_size);
let mid = (prev_x + x) * 0.5;
if target_x < mid {
return prev_pos;
}
prev_x = x;
prev_pos = i;
let _ = c;
}
let total = measure_advance(font, text, font_size);
let mid = (prev_x + total) * 0.5;
if target_x < mid {
prev_pos
} else {
text.len()
}
}
#[derive(Clone, Default)]
pub struct TextEditState {
pub text: String,
pub cursor: usize,
pub anchor: usize,
pub epoch: u64,
}
impl TextEditState {
#[inline]
pub fn note_text_change(&mut self) {
self.epoch = self.epoch.wrapping_add(1);
}
#[inline]
pub fn selection_range(&self) -> Option<(usize, usize)> {
let lo = self.cursor.min(self.anchor);
let hi = self.cursor.max(self.anchor);
if hi > lo {
Some((lo, hi))
} else {
None
}
}
}
pub struct TextEditCommand {
pub name: &'static str,
pub before: TextEditState,
pub after: TextEditState,
pub target: Rc<RefCell<TextEditState>>,
}
impl UndoRedoCommand for TextEditCommand {
fn name(&self) -> &str {
self.name
}
fn do_it(&mut self) {
*self.target.borrow_mut() = self.after.clone();
}
fn undo_it(&mut self) {
*self.target.borrow_mut() = self.before.clone();
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
#[cfg(test)]
mod word_line_tests {
use super::{paragraph_range_at, word_range_at};
#[test]
fn word_range_selects_alnum_run() {
let s = "hello world";
assert_eq!(word_range_at(s, 2), (0, 5));
assert_eq!(word_range_at(s, 8), (6, 11));
}
#[test]
fn word_range_stops_at_punctuation() {
let s = "foo.bar";
assert_eq!(word_range_at(s, 1), (0, 3));
assert_eq!(word_range_at(s, 5), (4, 7));
assert_eq!(word_range_at(s, 3), (3, 4));
}
#[test]
fn word_range_at_word_edge() {
let s = "cat dog";
assert_eq!(word_range_at(s, 3), (3, 4));
assert_eq!(word_range_at(s, 2), (0, 3));
}
#[test]
fn word_range_includes_underscore() {
let s = "snake_case here";
assert_eq!(word_range_at(s, 3), (0, 10));
}
#[test]
fn paragraph_range_single_line() {
let s = "just one line";
assert_eq!(paragraph_range_at(s, 4), (0, s.len()));
}
#[test]
fn paragraph_range_middle_line() {
let s = "first\nsecond\nthird";
assert_eq!(paragraph_range_at(s, 8), (6, 12));
}
#[test]
fn paragraph_range_last_line_no_trailing_newline() {
let s = "a\nbb\nccc";
assert_eq!(paragraph_range_at(s, 7), (5, 8));
}
#[test]
fn paragraph_range_on_blank_line() {
let s = "a\n\nb";
assert_eq!(paragraph_range_at(s, 2), (2, 2));
}
}