///|
pub(all) enum TextCaretDirection {
MoveLeft
MoveRight
MoveStart
MoveEnd
}
///|
pub(all) struct TextCaretMoveResult {
caret : Int32
selection : TextRange?
}
///|
pub fn active_text_selection(selection : TextRange?) -> TextRange? {
match selection {
Some(selection) => {
let normalized = selection.normalized()
if normalized.is_collapsed() {
None
} else {
Some(normalized)
}
}
None => None
}
}
///|
fn text_selection_anchor(caret : Int32, selection : TextRange?) -> Int32 {
match selection {
Some(selection) =>
if caret == selection.start {
selection.end
} else {
selection.start
}
None => caret
}
}
///|
pub fn move_text_caret(
text : String,
caret : Int32,
selection : TextRange?,
direction : TextCaretDirection,
extend_selection~ : Bool,
) -> TextCaretMoveResult {
let length = text_length(text)
let caret = clamp_int(caret, 0, length)
if extend_selection {
let anchor = nearest_boundary_fast(
text,
text_selection_anchor(caret, selection),
)
let next = move_text_caret_index(text, caret, direction)
{
caret: next,
selection: if next == anchor {
None
} else {
Some(TextRange::new(start=anchor, end=next))
},
}
} else {
match active_text_selection(selection) {
Some(selection) => {
let selection = normalize_grapheme_range_fast(text, selection)
match direction {
MoveLeft => { caret: selection.start, selection: None, }
MoveRight => { caret: selection.end, selection: None, }
MoveStart => { caret: 0, selection: None, }
MoveEnd => { caret: length, selection: None, }
}
}
None =>
{
caret: move_text_caret_index(text, caret, direction),
selection: None,
}
}
}
}
///|
/// Returns cp(caret-2) … cp(caret+1) when every window codepoint is
/// ASCII, or None to signal the general fallback. A caret move only
/// depends on the cluster rules at these five codepoints: break
/// decisions reference the flanking pair and, for RI/Extend/Indic
/// context, runs that would place a non-ASCII codepoint inside the
/// window themselves.
fn ascii_window_around_caret(
text : String,
caret~ : Int32,
) -> (Char?, Char?, Char?, Char?)? {
let mut index = 0
let mut minus_two : Char? = None
let mut minus_one : Char? = None
let mut at : Char? = None
let mut plus_one : Char? = None
for ch in text {
if index >= caret - 2 && index <= caret + 1 && ch.to_int() >= 128 {
return None
}
if index == caret - 2 {
minus_two = Some(ch)
} else if index == caret - 1 {
minus_one = Some(ch)
} else if index == caret {
at = Some(ch)
} else if index == caret + 1 {
plus_one = Some(ch)
return Some((minus_two, minus_one, at, plus_one))
}
index = index + 1
}
Some((minus_two, minus_one, at, plus_one))
}
///|
fn local_ascii_caret_move(
text : String,
caret : Int32,
direction : TextCaretDirection,
length~ : Int32,
) -> Int32? {
match ascii_window_around_caret(text, caret~) {
None => None
Some((minus_two, minus_one, at, plus_one)) =>
match direction {
MoveLeft =>
if caret <= 0 {
Some(0)
} else {
// CR-LF is one cluster: stepping left out of the pair returns
// to the CR, not onto the glued LF.
match (minus_two, minus_one) {
(Some('\r'), Some('\n')) => Some(caret - 2)
_ => Some(caret - 1)
}
}
MoveRight =>
if caret >= length {
Some(length)
} else {
match (at, plus_one) {
(Some('\r'), Some('\n')) => Some(caret + 2)
_ => Some(caret + 1)
}
}
MoveStart => Some(0)
MoveEnd => Some(length)
}
}
}
///|
fn move_text_caret_index(
text : String,
caret : Int32,
direction : TextCaretDirection,
) -> Int32 {
let length = text_length(text)
let caret = clamp_int(caret, 0, length)
match local_ascii_caret_move(text, caret, direction, length~) {
Some(next) => next
None => {
let boundaries = TextGraphemeBoundaries::new(text~)
let caret = boundaries.clamp_index(caret)
match direction {
MoveLeft => boundaries.previous_boundary(caret)
MoveRight => boundaries.next_boundary(caret)
MoveStart => 0
MoveEnd => boundaries.length()
}
}
}
}
///|
fn text_length(text : String) -> Int32 {
text.char_length()
}
///|
pub fn surrounding_delete_range(
text : String,
caret : Int32,
before : Int32,
after : Int32,
) -> TextRange {
let length = text_length(text)
let before = max_int(0, before)
let after = max_int(0, after)
let range = delete_grapheme_range(
TextGraphemeBoundaries::new(text~),
caret,
before,
after,
)
TextRange::new(
start=clamp_int(range.start, 0, length),
end=clamp_int(range.end, 0, length),
)
}
///|
pub fn text_input_grapheme_selection(
boundaries : TextGraphemeBoundaries,
selection : TextRange?,
) -> TextRange? {
match selection {
Some(selection) => {
let start = boundaries.nearest_boundary(selection.start)
let end = boundaries.nearest_boundary(selection.end)
if start == end {
None
} else {
Some(TextRange::new(start~, end~))
}
}
None => None
}
}