use azul_core::selection::{CursorAffinity, GraphemeClusterId, SelectionRange, TextCursor};
use crate::text3::cache::{PositionedItem, ShapedCluster, ShapedItem, UnifiedLayout};
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 = extract_line_text_at_item(item_idx, layout);
let cursor_byte_offset = cursor.cluster_id.start_byte_in_run as usize;
let (word_start, word_end) = find_word_boundaries(&line_text, cursor_byte_offset);
let start_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: cursor.cluster_id.source_run,
start_byte_in_run: word_start as u32,
},
affinity: CursorAffinity::Leading,
};
let end_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: cursor.cluster_id.source_run,
start_byte_in_run: word_end as u32,
},
affinity: CursorAffinity::Trailing,
};
Some(SelectionRange {
start: start_cursor,
end: end_cursor,
})
}
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_at_item(item_idx: usize, layout: &UnifiedLayout) -> String {
let line_index = layout.items[item_idx].line_index;
let mut text = String::new();
for item in &layout.items {
if item.line_index != line_index {
continue;
}
if let ShapedItem::Cluster(cluster) = &item.item {
text.push_str(&cluster.text);
}
}
text
}
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 mut 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.iter() {
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(|(idx, c)| idx + c.len_utf8())
.unwrap_or(0);
let end = char_indices
.iter()
.find(|(idx, c)| *idx > cursor_offset && is_word_char(*c))
.map(|(idx, _)| *idx)
.unwrap_or(text.len());
return (start, end);
}
}
(word_start, word_end)
}
#[inline]
fn is_word_char(ch: char) -> bool {
ch.is_alphanumeric() || ch == '_'
}