use azul_core::selection::{CursorAffinity, GraphemeClusterId, SelectionRange, TextCursor};
use crate::text3::cache::{
is_word_char, PositionedItem, ShapedCluster, ShapedItem, UnifiedLayout,
};
#[must_use] pub fn select_word_at_cursor(
cursor: &TextCursor,
layout: &UnifiedLayout,
) -> Option<SelectionRange> {
let (item_idx, _cluster) = find_cluster_at_cursor(cursor, layout)?;
let (line_text, cluster_map) = extract_line_text_and_clusters(item_idx, layout);
let cursor_byte_offset = cluster_map
.iter()
.take_while(|(id, _)| *id != cursor.cluster_id)
.map(|(_, len)| len)
.sum::<usize>();
let (word_start, word_end) = find_word_boundaries(&line_text, cursor_byte_offset);
let start_cluster_id = byte_offset_to_cluster_id(&cluster_map, word_start)?;
let end_cluster_id = byte_offset_to_cluster_id(&cluster_map, word_end.saturating_sub(1))
.unwrap_or(start_cluster_id);
Some(SelectionRange {
start: TextCursor {
cluster_id: start_cluster_id,
affinity: CursorAffinity::Leading,
},
end: TextCursor {
cluster_id: end_cluster_id,
affinity: CursorAffinity::Trailing,
},
})
}
#[must_use] pub fn select_paragraph_at_cursor(
cursor: &TextCursor,
layout: &UnifiedLayout,
) -> Option<SelectionRange> {
let (item_idx, _) = find_cluster_at_cursor(cursor, layout)?;
let item = &layout.items[item_idx];
let line_index = item.line_index;
let line_items: Vec<(usize, &PositionedItem)> = layout
.items
.iter()
.enumerate()
.filter(|(_, item)| item.line_index == line_index)
.collect();
if line_items.is_empty() {
return None;
}
let first_cluster = line_items
.iter()
.find_map(|(_, item)| item.item.as_cluster())?;
let last_cluster = line_items
.iter()
.rev()
.find_map(|(_, item)| item.item.as_cluster())?;
Some(SelectionRange {
start: TextCursor {
cluster_id: first_cluster.source_cluster_id,
affinity: CursorAffinity::Leading,
},
end: TextCursor {
cluster_id: last_cluster.source_cluster_id,
affinity: CursorAffinity::Trailing,
},
})
}
fn find_cluster_at_cursor<'a>(
cursor: &TextCursor,
layout: &'a UnifiedLayout,
) -> Option<(usize, &'a ShapedCluster)> {
layout.items.iter().enumerate().find_map(|(idx, item)| {
if let ShapedItem::Cluster(cluster) = &item.item {
if cluster.source_cluster_id == cursor.cluster_id {
return Some((idx, cluster));
}
}
None
})
}
fn extract_line_text_and_clusters(
item_idx: usize,
layout: &UnifiedLayout,
) -> (String, Vec<(GraphemeClusterId, usize)>) {
let Some(source_run) = layout.items[item_idx]
.item
.as_cluster()
.map(|c| c.source_cluster_id.source_run)
else {
return (String::new(), Vec::new());
};
let mut clusters: Vec<&ShapedCluster> = layout
.items
.iter()
.filter_map(|item| item.item.as_cluster())
.filter(|c| c.source_cluster_id.source_run == source_run)
.collect();
clusters.sort_by_key(|c| c.source_cluster_id.start_byte_in_run);
let mut text = String::new();
let mut cluster_map = Vec::new();
for c in clusters {
let s = c.text.as_str();
cluster_map.push((c.source_cluster_id, s.len()));
text.push_str(s);
}
(text, cluster_map)
}
fn byte_offset_to_cluster_id(
cluster_map: &[(GraphemeClusterId, usize)],
byte_offset: usize,
) -> Option<GraphemeClusterId> {
let mut cumulative = 0;
for (id, len) in cluster_map {
if byte_offset < cumulative + len {
return Some(*id);
}
cumulative += len;
}
cluster_map.last().map(|(id, _)| *id)
}
fn find_word_boundaries(text: &str, cursor_offset: usize) -> (usize, usize) {
let cursor_offset = cursor_offset.min(text.len());
let mut word_start = 0;
let char_indices: Vec<(usize, char)> = text.char_indices().collect();
for (i, (byte_idx, ch)) in char_indices.iter().enumerate().rev() {
if *byte_idx >= cursor_offset {
continue;
}
if !is_word_char(*ch) {
word_start = if i + 1 < char_indices.len() {
char_indices[i + 1].0
} else {
text.len()
};
break;
}
}
let mut word_end = text.len();
for (byte_idx, ch) in &char_indices {
if *byte_idx <= cursor_offset {
continue;
}
if !is_word_char(*ch) {
word_end = *byte_idx;
break;
}
}
if let Some((_, ch)) = char_indices.iter().find(|(idx, _)| *idx == cursor_offset) {
if !is_word_char(*ch) {
let start = char_indices
.iter()
.rev()
.find(|(idx, c)| *idx < cursor_offset && is_word_char(*c))
.map_or(0, |(idx, c)| idx + c.len_utf8());
let end = char_indices
.iter()
.find(|(idx, c)| *idx > cursor_offset && is_word_char(*c))
.map_or(text.len(), |(idx, _)| *idx);
return (start, end);
}
}
(word_start, word_end)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_word_boundaries_simple() {
let text = "Hello World";
let (start, end) = find_word_boundaries(text, 2);
assert_eq!(&text[start..end], "Hello");
let (start, end) = find_word_boundaries(text, 7);
assert_eq!(&text[start..end], "World");
let (start, end) = find_word_boundaries(text, 5);
assert_eq!(&text[start..end], " ");
}
#[test]
fn test_word_boundaries_start_end() {
let text = "Hello";
let (start, end) = find_word_boundaries(text, 0);
assert_eq!(&text[start..end], "Hello");
let (start, end) = find_word_boundaries(text, 5);
assert_eq!(&text[start..end], "Hello");
}
#[test]
fn test_word_boundaries_punctuation() {
let text = "Hello, World!";
let (start, end) = find_word_boundaries(text, 2);
assert_eq!(&text[start..end], "Hello");
let (start, end) = find_word_boundaries(text, 5);
assert_eq!(&text[start..end], ", ");
let (start, end) = find_word_boundaries(text, 8);
assert_eq!(&text[start..end], "World");
}
#[test]
fn test_word_boundaries_underscore() {
let text = "hello_world";
let (start, end) = find_word_boundaries(text, 5);
assert_eq!(&text[start..end], "hello_world");
}
#[test]
fn test_is_word_char() {
assert!(is_word_char('a'));
assert!(is_word_char('Z'));
assert!(is_word_char('0'));
assert!(is_word_char('_'));
assert!(!is_word_char(' '));
assert!(!is_word_char(','));
assert!(!is_word_char('!'));
}
#[test]
fn test_word_boundaries_empty() {
let (start, end) = find_word_boundaries("", 0);
assert_eq!(start, 0);
assert_eq!(end, 0);
}
#[test]
fn test_byte_offset_to_cluster_id_basic() {
let id0 = GraphemeClusterId { source_run: 0, start_byte_in_run: 0 };
let id1 = GraphemeClusterId { source_run: 0, start_byte_in_run: 5 };
let id2 = GraphemeClusterId { source_run: 0, start_byte_in_run: 6 };
let map = vec![(id0, 5), (id1, 1), (id2, 5)];
assert_eq!(byte_offset_to_cluster_id(&map, 0), Some(id0));
assert_eq!(byte_offset_to_cluster_id(&map, 4), Some(id0));
assert_eq!(byte_offset_to_cluster_id(&map, 5), Some(id1));
assert_eq!(byte_offset_to_cluster_id(&map, 6), Some(id2));
assert_eq!(byte_offset_to_cluster_id(&map, 100), Some(id2));
}
}