mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
236 lines
8.7 KiB
Rust
236 lines
8.7 KiB
Rust
use super::*;
|
|
|
|
/// Byte range in `s` of the characters `r` (IME ranges count characters; carets count bytes).
|
|
/// `None` when `r` runs past the end of `s` or backwards.
|
|
/// The `type.step` an Alt+arrow asks for, by one step: ←/→ kerning at a `caret`, else tracking;
|
|
/// ↑/↓ leading (down opens it up); Shift+↑/↓ baseline shift.
|
|
pub fn step_key(key: ToolKey, shift: bool, caret: bool) -> Option<(&'static str, f64)> {
|
|
let sign = |up: bool| if up { 1.0 } else { -1.0 };
|
|
Some(match (key, shift) {
|
|
(ToolKey::Left | ToolKey::Right, false) => (if caret { "kerning" } else { "tracking" }, sign(key == ToolKey::Right)),
|
|
(ToolKey::Up | ToolKey::Down, false) => ("leading", sign(key == ToolKey::Down)),
|
|
(ToolKey::Up | ToolKey::Down, true) => ("baselineShift", sign(key == ToolKey::Up)),
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
pub(super) fn char_range_to_bytes(s: &str, r: Range<usize>) -> Option<Range<usize>> {
|
|
let byte = |c: usize| if c == s.chars().count() { Some(s.len()) } else { s.char_indices().nth(c).map(|(i, _)| i) };
|
|
let (a, b) = (byte(r.start)?, byte(r.end)?);
|
|
(a <= b).then_some(a..b)
|
|
}
|
|
|
|
/// Lengths of the common prefix (at most `max_prefix`) and suffix of `a` and `b` (on char
|
|
/// boundaries, not overlapping).
|
|
pub(super) fn common_affixes(a: &str, b: &str, max_prefix: usize) -> (usize, usize) {
|
|
let mut p = 0;
|
|
for ((i, x), y) in a.char_indices().zip(b.chars()) {
|
|
if x != y || i + x.len_utf8() > max_prefix {
|
|
break;
|
|
}
|
|
p = i + x.len_utf8();
|
|
}
|
|
let max_s = (a.len() - p).min(b.len() - p);
|
|
let mut s = 0;
|
|
for (x, y) in a[p..].chars().rev().zip(b[p..].chars().rev()) {
|
|
if x != y || s + x.len_utf8() > max_s {
|
|
break;
|
|
}
|
|
s += x.len_utf8();
|
|
}
|
|
(p, s)
|
|
}
|
|
|
|
/// Byte length of the borrowed plain-text segments.
|
|
pub(super) fn plain_len(segments: &[&str]) -> usize {
|
|
segments.iter().map(|s| s.len()).sum()
|
|
}
|
|
|
|
/// Is `byte` a UTF-8 boundary in the concatenation of `segments`? Run edges are boundaries.
|
|
pub(super) fn plain_is_boundary(segments: &[&str], byte: usize) -> bool {
|
|
let mut at = 0usize;
|
|
for segment in segments {
|
|
if byte == at {
|
|
return true;
|
|
}
|
|
if byte < at + segment.len() {
|
|
return segment.is_char_boundary(byte - at);
|
|
}
|
|
at += segment.len();
|
|
}
|
|
byte == at
|
|
}
|
|
|
|
/// Append the bytes `start..end` of the concatenation of `segments` to `out`.
|
|
fn copy_plain_range(segments: &[&str], start: usize, end: usize, out: &mut String) {
|
|
let mut at = 0usize;
|
|
for segment in segments {
|
|
let segment_end = at + segment.len();
|
|
let from = start.max(at);
|
|
let to = end.min(segment_end);
|
|
if from < to {
|
|
let (a, b) = (from - at, to - at);
|
|
if segment.is_char_boundary(a) && segment.is_char_boundary(b) {
|
|
out.push_str(&segment[a..b]);
|
|
}
|
|
}
|
|
if segment_end >= end {
|
|
break;
|
|
}
|
|
at = segment_end;
|
|
}
|
|
}
|
|
|
|
/// Move `i` back to a character boundary in the concatenation of `segments` (deleting fewer
|
|
/// bytes rather than splitting UTF-8).
|
|
pub(super) fn plain_floor_boundary(segments: &[&str], mut i: usize) -> usize {
|
|
i = i.min(plain_len(segments));
|
|
while i > 0 && !plain_is_boundary(segments, i) {
|
|
i -= 1;
|
|
}
|
|
i
|
|
}
|
|
|
|
/// Move `i` forward to a character boundary in the concatenation of `segments`.
|
|
pub(super) fn plain_ceil_boundary(segments: &[&str], mut i: usize) -> usize {
|
|
let len = plain_len(segments);
|
|
i = i.min(len);
|
|
while i < len && !plain_is_boundary(segments, i) {
|
|
i += 1;
|
|
}
|
|
i
|
|
}
|
|
|
|
/// The byte offset `n` characters before `at` (a boundary) in `segments`, stopping at the start of
|
|
/// the text. Walks the runs, so a large IME deletion count never copies the whole text.
|
|
pub(super) fn plain_chars_before(segments: &[&str], at: usize, n: usize) -> usize {
|
|
if n == 0 {
|
|
return at;
|
|
}
|
|
let mut remaining = n;
|
|
let mut end = plain_len(segments);
|
|
for segment in segments.iter().rev() {
|
|
let start = end - segment.len();
|
|
let local_end = at.min(end).saturating_sub(start);
|
|
if local_end > 0 && segment.is_char_boundary(local_end) {
|
|
let before = &segment[..local_end];
|
|
let count = before.chars().count();
|
|
if count >= remaining {
|
|
return start + before.char_indices().rev().nth(remaining.saturating_sub(1)).map_or(0, |(i, _)| i);
|
|
}
|
|
remaining -= count;
|
|
}
|
|
end = start;
|
|
}
|
|
0
|
|
}
|
|
|
|
/// The byte offset `n` characters after `at` (a boundary) in `segments`, stopping at the end of
|
|
/// the text.
|
|
pub(super) fn plain_chars_after(segments: &[&str], at: usize, n: usize) -> usize {
|
|
if n == 0 {
|
|
return at;
|
|
}
|
|
let len = plain_len(segments);
|
|
let mut remaining = n;
|
|
let mut acc = 0usize;
|
|
for segment in segments {
|
|
let end = acc + segment.len();
|
|
let local_start = at.saturating_sub(acc);
|
|
if end > at && segment.is_char_boundary(local_start) {
|
|
let after = &segment[local_start..];
|
|
let count = after.chars().count();
|
|
if count >= remaining {
|
|
return acc + local_start + after.char_indices().nth(remaining).map_or(after.len(), |(i, _)| i);
|
|
}
|
|
remaining -= count;
|
|
}
|
|
acc = end;
|
|
}
|
|
len
|
|
}
|
|
|
|
/// The logical IME surrounding window: the pre-edit is removed and the caret/anchor offsets
|
|
/// are remapped around it. `preedit` has already been validated against `segments`.
|
|
pub(super) fn ime_surrounding_window(
|
|
segments: &[&str],
|
|
preedit: Option<Range<usize>>,
|
|
len: usize,
|
|
caret: usize,
|
|
anchor: usize,
|
|
max_bytes: usize,
|
|
) -> ImeSurrounding {
|
|
let removed = preedit.as_ref().map_or(0, |r| r.end - r.start);
|
|
let logical_len = len - removed;
|
|
let to_logical = |byte: usize| -> usize {
|
|
match &preedit {
|
|
Some(r) if byte > r.start && byte < r.end => r.start,
|
|
Some(r) if byte >= r.end => byte - removed,
|
|
_ => byte,
|
|
}
|
|
};
|
|
let cursor = to_logical(caret);
|
|
let anchor = to_logical(anchor);
|
|
let selection_start = cursor.min(anchor);
|
|
let selection_end = cursor.max(anchor);
|
|
let selection_len = selection_end - selection_start;
|
|
if selection_len > max_bytes {
|
|
return ImeSurrounding::Unavailable("the selection is wider than the IME surrounding-text limit");
|
|
}
|
|
let budget = max_bytes - selection_len;
|
|
let before_available = selection_start;
|
|
let after_available = logical_len - selection_end;
|
|
let mut before = (budget / 2).min(before_available);
|
|
let mut after = (budget - before).min(after_available);
|
|
before = (budget - after).min(before_available);
|
|
after = (budget - before).min(after_available);
|
|
let mut window_start = selection_start - before;
|
|
let mut window_end = selection_end + after;
|
|
let logical_boundary = |byte: usize| -> bool {
|
|
match &preedit {
|
|
Some(r) if byte == r.start => true,
|
|
Some(r) if byte < r.start => plain_is_boundary(segments, byte),
|
|
Some(_) => plain_is_boundary(segments, byte + removed),
|
|
None => plain_is_boundary(segments, byte),
|
|
}
|
|
};
|
|
while window_start < selection_start && !logical_boundary(window_start) {
|
|
window_start += 1;
|
|
}
|
|
while selection_end < window_end && !logical_boundary(window_end) {
|
|
window_end -= 1;
|
|
}
|
|
let to_original = |byte: usize| -> usize {
|
|
match &preedit {
|
|
Some(r) if byte > r.start => byte + removed,
|
|
_ => byte,
|
|
}
|
|
};
|
|
let original_start = to_original(window_start);
|
|
let original_end = to_original(window_end);
|
|
let mut text = String::with_capacity(window_end - window_start);
|
|
match &preedit {
|
|
Some(r) => {
|
|
if window_start < r.start {
|
|
let first_end = original_end.min(r.start);
|
|
if original_start < first_end {
|
|
copy_plain_range(segments, original_start, first_end, &mut text);
|
|
}
|
|
}
|
|
if r.start < window_end {
|
|
let second_start = r.end.max(original_start);
|
|
if second_start < original_end {
|
|
copy_plain_range(segments, second_start, original_end, &mut text);
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
if original_start < original_end {
|
|
copy_plain_range(segments, original_start, original_end, &mut text);
|
|
}
|
|
}
|
|
}
|
|
debug_assert_eq!(text.len(), window_end - window_start, "removing the pre-edit must preserve the logical window length");
|
|
ImeSurrounding::Available { text, cursor: cursor - window_start, anchor: anchor - window_start }
|
|
}
|