use std::ops::Range;
use densa_core::{Color, Point, Rect};
use parley::{BoundingBox, Cursor, Layout, Selection};
use crate::Paragraph;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Motion {
Left,
Right,
WordLeft,
WordRight,
Previous,
Next,
PreviousWord,
NextWord,
Up,
Down,
LineStart,
LineEnd,
TextStart,
TextEnd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Affinity {
#[default]
Downstream,
Upstream,
}
impl From<Affinity> for parley::Affinity {
fn from(affinity: Affinity) -> Self {
match affinity {
Affinity::Downstream => parley::Affinity::Downstream,
Affinity::Upstream => parley::Affinity::Upstream,
}
}
}
fn from_parley(affinity: parley::Affinity) -> Affinity {
match affinity {
parley::Affinity::Upstream => Affinity::Upstream,
parley::Affinity::Downstream => Affinity::Downstream,
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TextSelection {
inner: Selection,
}
impl TextSelection {
pub fn caret_at(paragraph: &Paragraph, index: usize) -> Self {
Self::caret_with(paragraph, index, Affinity::Downstream)
}
pub fn caret_with(paragraph: &Paragraph, index: usize, affinity: Affinity) -> Self {
let cursor = cursor(paragraph, index, affinity);
Self {
inner: Selection::new(cursor, cursor),
}
}
pub fn from_range(paragraph: &Paragraph, anchor: usize, focus: usize) -> Self {
let anchor = cursor(paragraph, anchor, Affinity::Downstream);
let focus = cursor(paragraph, focus, Affinity::Upstream);
Self {
inner: Selection::new(anchor, focus),
}
}
pub fn select_all(paragraph: &Paragraph) -> Self {
Self::from_range(paragraph, 0, paragraph.text_len())
}
pub fn at_point(paragraph: &Paragraph, point: Point) -> Self {
Self {
inner: Selection::from_point(paragraph.layout(), point.x, point.y),
}
}
pub fn word_at_point(paragraph: &Paragraph, point: Point) -> Self {
Self {
inner: Selection::word_from_point(paragraph.layout(), point.x, point.y),
}
}
pub fn line_at_point(paragraph: &Paragraph, point: Point) -> Self {
Self {
inner: Selection::line_from_point(paragraph.layout(), point.x, point.y),
}
}
#[must_use]
pub fn extend_to_point(self, paragraph: &Paragraph, point: Point) -> Self {
Self {
inner: self
.inner
.extend_to_point(paragraph.layout(), point.x, point.y),
}
}
#[must_use]
pub fn shift_click(self, paragraph: &Paragraph, point: Point) -> Self {
Self {
inner: self
.inner
.shift_click_extension(paragraph.layout(), point.x, point.y),
}
}
#[must_use]
pub fn moved(self, paragraph: &Paragraph, motion: Motion, extend: bool) -> Self {
let layout = paragraph.layout();
let s = self.inner;
let inner = match motion {
Motion::Left => s.previous_visual(layout, extend),
Motion::Right => s.next_visual(layout, extend),
Motion::WordLeft => s.previous_visual_word(layout, extend),
Motion::WordRight => s.next_visual_word(layout, extend),
Motion::Up => s.previous_line(layout, extend),
Motion::Down => s.next_line(layout, extend),
Motion::LineStart => s.line_start(layout, extend),
Motion::LineEnd => s.line_end(layout, extend),
Motion::Previous => to(s, previous_logical(layout, s.focus()), extend),
Motion::Next => to(s, next_logical(layout, s.focus()), extend),
Motion::PreviousWord => to(s, s.focus().previous_logical_word(layout), extend),
Motion::NextWord => to(s, s.focus().next_logical_word(layout), extend),
Motion::TextStart => to(s, cursor(paragraph, 0, Affinity::Downstream), extend),
Motion::TextEnd => {
let end = cursor(paragraph, paragraph.text_len(), Affinity::Downstream);
to(s, end, extend)
}
};
Self { inner }
}
#[must_use]
pub fn refreshed(self, paragraph: &Paragraph) -> Self {
let clamp = |c: Cursor| cursor(paragraph, c.index(), from_parley(c.affinity()));
let anchor = clamp(self.inner.anchor());
let focus = clamp(self.inner.focus());
Self {
inner: Selection::new(anchor, focus).refresh(paragraph.layout()),
}
}
#[must_use]
pub fn collapsed(self) -> Self {
Self {
inner: self.inner.collapse(),
}
}
pub fn is_caret(&self) -> bool {
self.inner.is_collapsed()
}
pub fn range(&self) -> Range<usize> {
self.inner.text_range()
}
pub fn anchor(&self) -> usize {
self.inner.anchor().index()
}
pub fn focus(&self) -> usize {
self.inner.focus().index()
}
pub fn focus_affinity(&self) -> Affinity {
from_parley(self.inner.focus().affinity())
}
pub fn caret_rect(&self, paragraph: &Paragraph, width: f32) -> Rect {
to_rect(self.inner.focus().geometry(paragraph.layout(), width))
}
pub fn for_each_rect(&self, paragraph: &Paragraph, mut f: impl FnMut(Rect)) {
self.inner
.geometry_with(paragraph.layout(), |bounds, _line| f(to_rect(bounds)));
}
}
pub(crate) fn backspace_start(paragraph: &Paragraph, text: &str, index: usize) -> usize {
let at = cursor(paragraph, index, Affinity::Downstream);
let [upstream, _] = at.logical_clusters(paragraph.layout());
let Some(cluster) = upstream else {
return index;
};
if cluster.is_hard_line_break() || cluster.is_emoji() {
return cluster.text_range().start;
}
text.get(..index)
.and_then(|before| before.char_indices().next_back())
.map_or(index, |(start, _)| start)
}
fn cursor(paragraph: &Paragraph, index: usize, affinity: Affinity) -> Cursor {
let index = index.min(paragraph.text_len());
Cursor::from_byte_index(paragraph.layout(), index, affinity.into())
}
fn to(selection: Selection, target: Cursor, extend: bool) -> Selection {
if extend {
selection.extend(target)
} else {
Selection::new(target, target)
}
}
fn previous_logical(layout: &Layout<Color>, at: Cursor) -> Cursor {
let [upstream, _] = at.logical_clusters(layout);
upstream.map_or(at, |cluster| {
let start = cluster.text_range().start;
Cursor::from_byte_index(layout, start, parley::Affinity::Downstream)
})
}
fn next_logical(layout: &Layout<Color>, at: Cursor) -> Cursor {
let [_, downstream] = at.logical_clusters(layout);
downstream.map_or(at, |cluster| {
let end = cluster.text_range().end;
Cursor::from_byte_index(layout, end, parley::Affinity::Upstream)
})
}
fn to_rect(b: BoundingBox) -> Rect {
Rect::from_edges(b.x0 as f32, b.y0 as f32, b.x1 as f32, b.y1 as f32)
}