use azul_core::selection::{
CursorAffinity, GraphemeClusterId, Selection, SelectionRange, TextCursor,
};
use crate::text3::cache::{InlineContent, StyledRun};
#[derive(Debug, Clone)]
pub enum TextEdit {
Insert(String),
DeleteBackward,
DeleteForward,
}
const fn selection_start_run(selection: &Selection) -> u32 {
match selection {
Selection::Cursor(c) => c.cluster_id.source_run,
Selection::Range(r) => r.start.cluster_id.source_run,
}
}
const fn selection_start_byte(selection: &Selection) -> u32 {
match selection {
Selection::Cursor(c) => c.cluster_id.start_byte_in_run,
Selection::Range(r) => r.start.cluster_id.start_byte_in_run,
}
}
fn sort_selections_back_to_front(selections: &[Selection]) -> Vec<Selection> {
let mut sorted = selections.to_vec();
sorted.sort_by(|a, b| {
let cursor_a = match a {
Selection::Cursor(c) => c,
Selection::Range(r) => &r.start,
};
let cursor_b = match b {
Selection::Cursor(c) => c,
Selection::Range(r) => &r.start,
};
cursor_b.cluster_id.cmp(&cursor_a.cluster_id) });
sorted
}
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn adjust_cursors(
selections: &mut [Selection],
edit_run: u32,
edit_byte: u32,
byte_offset_change: i32,
) {
for sel in selections.iter_mut() {
if let Selection::Cursor(cursor) = sel {
if cursor.cluster_id.source_run == edit_run
&& cursor.cluster_id.start_byte_in_run >= edit_byte
{
cursor.cluster_id.start_byte_in_run =
(cursor.cluster_id.start_byte_in_run as i32 + byte_offset_change).max(0) as u32;
}
}
}
}
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn adjust_cursor_runs(selections: &mut [Selection], boundary_run: u32, run_count_change: i32) {
if run_count_change == 0 {
return;
}
for sel in selections.iter_mut() {
if let Selection::Cursor(cursor) = sel {
if cursor.cluster_id.source_run > boundary_run {
let shifted = (cursor.cluster_id.source_run as i32 + run_count_change)
.max(boundary_run as i32);
cursor.cluster_id.source_run = shifted as u32;
}
}
}
}
fn run_text_len(content: &[InlineContent], run_idx: u32) -> usize {
match content.get(run_idx as usize) {
Some(InlineContent::Text(run)) => run.text.len(),
_ => 0,
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[must_use] pub fn edit_text(
content: &[InlineContent],
selections: &[Selection],
edit: &TextEdit,
) -> (Vec<InlineContent>, Vec<Selection>) {
if selections.is_empty() {
return (content.to_vec(), Vec::new());
}
let mut new_content = content.to_vec();
let mut new_selections = Vec::new();
let sorted_selections = sort_selections_back_to_front(selections);
for selection in sorted_selections {
let edit_run = selection_start_run(&selection);
let edit_byte = selection_start_byte(&selection);
let old_run_len = run_text_len(&new_content, edit_run);
let old_run_count = new_content.len();
let (temp_content, new_cursor) =
apply_edit_to_selection(&new_content, &selection, edit);
let new_run_len = run_text_len(&temp_content, edit_run);
let byte_offset_change = new_run_len as i32 - old_run_len as i32;
let run_count_change = temp_content.len() as i32 - old_run_count as i32;
adjust_cursors(&mut new_selections, edit_run, edit_byte, byte_offset_change);
adjust_cursor_runs(&mut new_selections, edit_run, run_count_change);
new_content = temp_content;
new_selections.push(Selection::Cursor(new_cursor));
}
new_selections.reverse();
(new_content, new_selections)
}
#[must_use] pub fn apply_edit_to_selection(
content: &[InlineContent],
selection: &Selection,
edit: &TextEdit,
) -> (Vec<InlineContent>, TextCursor) {
let mut new_content = content.to_vec();
match selection {
Selection::Range(range) => {
let (content_after_delete, cursor_pos) = delete_range(&new_content, range);
match edit {
TextEdit::Insert(text_to_insert) => {
let mut c = content_after_delete;
insert_text(&c, &cursor_pos, text_to_insert)
}
TextEdit::DeleteBackward | TextEdit::DeleteForward => {
(content_after_delete, cursor_pos)
}
}
}
Selection::Cursor(cursor) => {
match edit {
TextEdit::Insert(text_to_insert) => {
insert_text(&new_content, cursor, text_to_insert)
}
TextEdit::DeleteBackward => delete_backward(&new_content, cursor),
TextEdit::DeleteForward => delete_forward(&new_content, cursor),
}
}
}
}
pub(crate) fn cursor_byte_offset_in_run(text: &str, cursor: &TextCursor) -> usize {
use unicode_segmentation::UnicodeSegmentation;
let csb = cursor.cluster_id.start_byte_in_run as usize;
match cursor.affinity {
CursorAffinity::Leading => csb.min(text.len()),
CursorAffinity::Trailing => {
if csb >= text.len() {
text.len()
} else {
text[csb..]
.grapheme_indices(true)
.next()
.map_or(text.len(), |(_, g)| csb + g.len())
}
}
}
}
#[allow(clippy::cast_possible_truncation)] #[must_use] pub fn delete_range(
content: &[InlineContent],
range: &SelectionRange,
) -> (Vec<InlineContent>, TextCursor) {
let mut new_content = content.to_vec();
let start_run_idx = range.start.cluster_id.source_run as usize;
let end_run_idx = range.end.cluster_id.source_run as usize;
let mut cursor_after = range.start;
if start_run_idx == end_run_idx {
if let Some(InlineContent::Text(run)) = new_content.get_mut(start_run_idx) {
let a = cursor_byte_offset_in_run(&run.text, &range.start);
let b = cursor_byte_offset_in_run(&run.text, &range.end);
let lo = a.min(b);
let hi = a.max(b);
if hi <= run.text.len() && lo < hi {
run.text.drain(lo..hi);
cursor_after = TextCursor {
cluster_id: GraphemeClusterId {
source_run: start_run_idx as u32,
start_byte_in_run: lo as u32,
},
affinity: CursorAffinity::Leading,
};
}
} else if start_run_idx < new_content.len() && range.start != range.end {
new_content.remove(start_run_idx);
cursor_after = TextCursor {
cluster_id: GraphemeClusterId {
source_run: start_run_idx as u32,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Leading,
};
}
} else {
let (lo_run, lo_cursor, hi_run, hi_cursor) = if start_run_idx <= end_run_idx {
(start_run_idx, range.start, end_run_idx, range.end)
} else {
(end_run_idx, range.end, start_run_idx, range.start)
};
let lo_byte = match new_content.get(lo_run) {
Some(InlineContent::Text(run)) => cursor_byte_offset_in_run(&run.text, &lo_cursor),
_ => 0,
};
let hi_byte = match new_content.get(hi_run) {
Some(InlineContent::Text(run)) => cursor_byte_offset_in_run(&run.text, &hi_cursor),
_ => 0,
};
let head_len = if let Some(InlineContent::Text(run)) = new_content.get_mut(lo_run) {
let cut = lo_byte.min(run.text.len());
run.text.truncate(cut);
cut
} else {
0
};
if let Some(InlineContent::Text(run)) = new_content.get_mut(hi_run) {
let cut = hi_byte.min(run.text.len());
run.text.drain(..cut);
}
let drain_end = hi_run.min(new_content.len());
if drain_end > lo_run + 1 {
new_content.drain((lo_run + 1)..drain_end);
}
let tail_idx = lo_run + 1;
let mergeable = matches!(
(new_content.get(lo_run), new_content.get(tail_idx)),
(Some(InlineContent::Text(a)), Some(InlineContent::Text(b)))
if a.style == b.style
);
if mergeable {
if let InlineContent::Text(tail) = new_content.remove(tail_idx) {
if let Some(InlineContent::Text(head)) = new_content.get_mut(lo_run) {
head.text.push_str(&tail.text);
}
}
}
cursor_after = TextCursor {
cluster_id: GraphemeClusterId {
source_run: lo_run as u32,
start_byte_in_run: head_len as u32,
},
affinity: CursorAffinity::Leading,
};
}
(new_content, cursor_after) }
#[allow(clippy::cast_possible_truncation)] #[must_use]
pub fn insert_text(
content: &[InlineContent],
cursor: &TextCursor,
text_to_insert: &str,
) -> (Vec<InlineContent>, TextCursor) {
use unicode_segmentation::UnicodeSegmentation;
let mut new_content = content.to_vec();
let run_idx = cursor.cluster_id.source_run as usize;
let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
let byte_offset = match cursor.affinity {
CursorAffinity::Leading => {
cluster_start_byte
},
CursorAffinity::Trailing => {
if cluster_start_byte >= run.text.len() {
run.text.len()
} else {
run.text[cluster_start_byte..]
.grapheme_indices(true)
.next()
.map_or(run.text.len(), |(_, grapheme)| cluster_start_byte + grapheme.len())
}
},
};
if byte_offset <= run.text.len() {
run.text.insert_str(byte_offset, text_to_insert);
let new_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: run_idx as u32,
start_byte_in_run: (byte_offset + text_to_insert.len()) as u32,
},
affinity: CursorAffinity::Leading,
};
return (new_content, new_cursor);
}
}
(content.to_vec(), *cursor)
}
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] #[must_use]
pub fn delete_backward(
content: &[InlineContent],
cursor: &TextCursor,
) -> (Vec<InlineContent>, TextCursor) {
use unicode_segmentation::UnicodeSegmentation;
let mut new_content = content.to_vec();
let run_idx = cursor.cluster_id.source_run as usize;
let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
if new_content.get(run_idx).is_some()
&& !matches!(new_content.get(run_idx), Some(InlineContent::Text(_)))
{
return match cursor.affinity {
CursorAffinity::Trailing => {
new_content.remove(run_idx);
(
new_content,
TextCursor {
cluster_id: GraphemeClusterId {
source_run: run_idx as u32,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Leading,
},
)
}
CursorAffinity::Leading if run_idx > 0 => {
let prev_byte = match content.get(run_idx - 1) {
Some(InlineContent::Text(r)) => r.text.len() as u32,
_ => 0,
};
delete_backward(
content,
&TextCursor {
cluster_id: GraphemeClusterId {
source_run: (run_idx - 1) as u32,
start_byte_in_run: prev_byte,
},
affinity: CursorAffinity::Trailing,
},
)
}
CursorAffinity::Leading => (content.to_vec(), *cursor),
};
}
if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
let byte_offset = match cursor.affinity {
CursorAffinity::Leading => cluster_start_byte,
CursorAffinity::Trailing => {
if cluster_start_byte >= run.text.len() {
run.text.len()
} else {
run.text[cluster_start_byte..]
.grapheme_indices(true)
.next()
.map_or(run.text.len(), |(_, grapheme)| cluster_start_byte + grapheme.len())
}
},
};
if byte_offset > 0 {
let prev_grapheme_start = run.text[..byte_offset]
.grapheme_indices(true)
.next_back()
.map_or(0, |(i, _)| i);
run.text.drain(prev_grapheme_start..byte_offset);
let new_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: run_idx as u32,
start_byte_in_run: prev_grapheme_start as u32,
},
affinity: CursorAffinity::Leading,
};
return (new_content, new_cursor);
} else if run_idx > 0 {
match content.get(run_idx - 1).cloned() {
Some(InlineContent::Text(prev_run)) => {
let mut merged_text = prev_run.text;
let new_cursor_byte_offset = merged_text.len();
merged_text.push_str(&run.text);
new_content[run_idx - 1] = InlineContent::Text(StyledRun {
text: merged_text,
style: prev_run.style,
logical_start_byte: prev_run.logical_start_byte,
source_node_id: prev_run.source_node_id,
});
new_content.remove(run_idx);
let new_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: (run_idx - 1) as u32,
start_byte_in_run: new_cursor_byte_offset as u32,
},
affinity: CursorAffinity::Leading,
};
return (new_content, new_cursor);
}
Some(_) => {
new_content.remove(run_idx - 1);
let new_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: (run_idx - 1) as u32,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Leading,
};
return (new_content, new_cursor);
}
None => {}
}
}
}
(content.to_vec(), *cursor)
}
#[allow(clippy::cast_possible_truncation)] #[must_use]
pub fn delete_forward(
content: &[InlineContent],
cursor: &TextCursor,
) -> (Vec<InlineContent>, TextCursor) {
use unicode_segmentation::UnicodeSegmentation;
let mut new_content = content.to_vec();
let run_idx = cursor.cluster_id.source_run as usize;
let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
if new_content.get(run_idx).is_some()
&& !matches!(new_content.get(run_idx), Some(InlineContent::Text(_)))
{
return match cursor.affinity {
CursorAffinity::Leading => {
new_content.remove(run_idx);
(
new_content,
TextCursor {
cluster_id: GraphemeClusterId {
source_run: run_idx as u32,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Leading,
},
)
}
CursorAffinity::Trailing if run_idx + 1 < content.len() => delete_forward(
content,
&TextCursor {
cluster_id: GraphemeClusterId {
source_run: (run_idx + 1) as u32,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Leading,
},
),
CursorAffinity::Trailing => (content.to_vec(), *cursor),
};
}
if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
let byte_offset = match cursor.affinity {
CursorAffinity::Leading => cluster_start_byte,
CursorAffinity::Trailing => {
if cluster_start_byte >= run.text.len() {
run.text.len()
} else {
run.text[cluster_start_byte..]
.grapheme_indices(true)
.next()
.map_or(run.text.len(), |(_, grapheme)| cluster_start_byte + grapheme.len())
}
},
};
if byte_offset < run.text.len() {
let next_grapheme_end = run.text[byte_offset..]
.grapheme_indices(true)
.nth(1)
.map_or(run.text.len(), |(i, _)| byte_offset + i);
run.text.drain(byte_offset..next_grapheme_end);
let new_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: run_idx as u32,
start_byte_in_run: byte_offset as u32,
},
affinity: CursorAffinity::Leading,
};
return (new_content, new_cursor);
} else if run_idx < content.len() - 1 {
match content.get(run_idx + 1).cloned() {
Some(InlineContent::Text(next_run)) => {
let mut merged_text = run.text.clone();
merged_text.push_str(&next_run.text);
new_content[run_idx] = InlineContent::Text(StyledRun {
text: merged_text,
style: run.style.clone(),
logical_start_byte: run.logical_start_byte,
source_node_id: run.source_node_id,
});
new_content.remove(run_idx + 1);
return (new_content, *cursor);
}
Some(_) => {
new_content.remove(run_idx + 1);
return (new_content, *cursor);
}
None => {}
}
}
}
(content.to_vec(), *cursor)
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[must_use] pub fn edit_text_multi(
content: &[InlineContent],
selections: &[Selection],
texts: &[&str],
) -> (Vec<InlineContent>, Vec<Selection>) {
assert_eq!(
selections.len(),
texts.len(),
"edit_text_multi: selections and texts must have the same length"
);
if selections.is_empty() {
return (content.to_vec(), Vec::new());
}
let mut new_content = content.to_vec();
let mut new_selections = Vec::new();
let mut pairs: Vec<(Selection, &str)> = selections
.iter()
.copied()
.zip(texts.iter().copied())
.collect();
pairs.sort_by(|a, b| {
let cursor_a = match &a.0 {
Selection::Cursor(c) => c,
Selection::Range(r) => &r.start,
};
let cursor_b = match &b.0 {
Selection::Cursor(c) => c,
Selection::Range(r) => &r.start,
};
cursor_b.cluster_id.cmp(&cursor_a.cluster_id) });
for (selection, text) in &pairs {
let edit = TextEdit::Insert((*text).to_string());
let edit_run = selection_start_run(selection);
let edit_byte = selection_start_byte(selection);
let old_run_len = run_text_len(&new_content, edit_run);
let old_run_count = new_content.len();
let (temp_content, new_cursor) =
apply_edit_to_selection(&new_content, selection, &edit);
let new_run_len = run_text_len(&temp_content, edit_run);
let byte_offset_change = new_run_len as i32 - old_run_len as i32;
let run_count_change = temp_content.len() as i32 - old_run_count as i32;
adjust_cursors(&mut new_selections, edit_run, edit_byte, byte_offset_change);
adjust_cursor_runs(&mut new_selections, edit_run, run_count_change);
new_content = temp_content;
new_selections.push(Selection::Cursor(new_cursor));
}
new_selections.reverse();
(new_content, new_selections)
}
#[must_use] pub fn inspect_delete(
content: &[InlineContent],
selection: &Selection,
forward: bool,
) -> Option<(SelectionRange, String)> {
match selection {
Selection::Range(range) => {
let deleted_text = extract_text_in_range(content, range);
Some((*range, deleted_text))
}
Selection::Cursor(cursor) => {
if forward {
inspect_delete_forward(content, cursor)
} else {
inspect_delete_backward(content, cursor)
}
}
}
}
#[allow(clippy::cast_possible_truncation)] fn inspect_delete_forward(
content: &[InlineContent],
cursor: &TextCursor,
) -> Option<(SelectionRange, String)> {
use unicode_segmentation::UnicodeSegmentation;
let run_idx = cursor.cluster_id.source_run as usize;
if let Some(InlineContent::Text(run)) = content.get(run_idx) {
let byte_offset = cursor_byte_offset_in_run(&run.text, cursor);
if byte_offset < run.text.len() {
let next_grapheme_end = run.text[byte_offset..]
.grapheme_indices(true)
.nth(1)
.map_or(run.text.len(), |(i, _)| byte_offset + i);
let deleted_text = run.text[byte_offset..next_grapheme_end].to_string();
let range = SelectionRange {
start: *cursor,
end: TextCursor {
cluster_id: GraphemeClusterId {
source_run: run_idx as u32,
start_byte_in_run: next_grapheme_end as u32,
},
affinity: CursorAffinity::Leading,
},
};
return Some((range, deleted_text));
} else if run_idx < content.len() - 1 {
if let Some(InlineContent::Text(next_run)) = content.get(run_idx + 1) {
let deleted_text = next_run.text.graphemes(true).next()?.to_string();
let next_grapheme_end = next_run
.text
.grapheme_indices(true)
.nth(1)
.map_or(next_run.text.len(), |(i, _)| i);
let range = SelectionRange {
start: *cursor,
end: TextCursor {
cluster_id: GraphemeClusterId {
source_run: (run_idx + 1) as u32,
start_byte_in_run: next_grapheme_end as u32,
},
affinity: CursorAffinity::Leading,
},
};
return Some((range, deleted_text));
}
}
}
None }
#[allow(clippy::cast_possible_truncation)] fn inspect_delete_backward(
content: &[InlineContent],
cursor: &TextCursor,
) -> Option<(SelectionRange, String)> {
use unicode_segmentation::UnicodeSegmentation;
let run_idx = cursor.cluster_id.source_run as usize;
if let Some(InlineContent::Text(run)) = content.get(run_idx) {
let byte_offset = cursor_byte_offset_in_run(&run.text, cursor);
if byte_offset > 0 {
let prev_grapheme_start = run.text[..byte_offset]
.grapheme_indices(true)
.next_back()
.map_or(0, |(i, _)| i);
let deleted_text = run.text[prev_grapheme_start..byte_offset].to_string();
let range = SelectionRange {
start: TextCursor {
cluster_id: GraphemeClusterId {
source_run: run_idx as u32,
start_byte_in_run: prev_grapheme_start as u32,
},
affinity: CursorAffinity::Leading,
},
end: *cursor,
};
return Some((range, deleted_text));
} else if run_idx > 0 {
if let Some(InlineContent::Text(prev_run)) = content.get(run_idx - 1) {
let deleted_text = prev_run.text.graphemes(true).next_back()?.to_string();
let prev_grapheme_start = prev_run.text[..]
.grapheme_indices(true)
.next_back()
.map_or(0, |(i, _)| i);
let range = SelectionRange {
start: TextCursor {
cluster_id: GraphemeClusterId {
source_run: (run_idx - 1) as u32,
start_byte_in_run: prev_grapheme_start as u32,
},
affinity: CursorAffinity::Leading,
},
end: *cursor,
};
return Some((range, deleted_text));
}
}
}
None }
fn extract_text_in_range(content: &[InlineContent], range: &SelectionRange) -> String {
let start_run = range.start.cluster_id.source_run as usize;
let end_run = range.end.cluster_id.source_run as usize;
let start_byte = range.start.cluster_id.start_byte_in_run as usize;
let end_byte = range.end.cluster_id.start_byte_in_run as usize;
if start_run == end_run {
if let Some(InlineContent::Text(run)) = content.get(start_run) {
if start_byte <= end_byte && end_byte <= run.text.len() {
return run.text[start_byte..end_byte].to_string();
}
}
} else {
let mut result = String::new();
for (idx, item) in content.iter().enumerate() {
if let InlineContent::Text(run) = item {
if idx == start_run {
if start_byte < run.text.len() {
result.push_str(&run.text[start_byte..]);
}
} else if idx > start_run && idx < end_run {
result.push_str(&run.text);
} else if idx == end_run {
if end_byte <= run.text.len() {
result.push_str(&run.text[..end_byte]);
}
break;
}
}
}
return result;
}
String::new()
}