use std::ops::Range;
use densa_core::Rect;
use crate::selection::backspace_start;
use crate::style::{is_char_range, ParagraphError, ParagraphStyle};
use crate::{Affinity, Motion, Paragraph, TextSelection, TextSystem};
#[derive(Debug)]
pub struct TextEditor {
text: String,
style: ParagraphStyle,
paragraph: Paragraph,
selection: TextSelection,
composition: Option<Composition>,
shows_caret: bool,
}
#[derive(Debug, Clone)]
struct Composition {
range: Range<usize>,
replaced: String,
}
const LINE_BREAKS: [char; 7] = [
'\n', '\r', '\u{0B}', '\u{0C}', '\u{85}', '\u{2028}', '\u{2029}',
];
impl TextEditor {
pub fn new(
system: &mut TextSystem,
text: impl Into<String>,
style: ParagraphStyle,
) -> Result<Self, ParagraphError> {
let text = text.into();
let paragraph = system.paragraph(&text, &style, &[])?;
let selection = TextSelection::caret_at(¶graph, text.len());
Ok(Self {
text,
style,
paragraph,
selection,
composition: None,
shows_caret: true,
})
}
pub fn text(&self) -> &str {
&self.text
}
pub fn style(&self) -> &ParagraphStyle {
&self.style
}
pub fn paragraph(&self) -> &Paragraph {
&self.paragraph
}
pub fn selection(&self) -> TextSelection {
self.selection
}
pub fn selected_text(&self) -> &str {
&self.text[self.selection.range()]
}
pub fn composition(&self) -> Option<Range<usize>> {
self.composition.as_ref().map(|c| c.range.clone())
}
pub fn shows_caret(&self) -> bool {
self.shows_caret
}
pub fn set_selection(&mut self, selection: TextSelection) {
self.end_composition();
self.selection = selection.refreshed(&self.paragraph);
}
pub fn move_caret(&mut self, motion: Motion, extend: bool) {
self.end_composition();
self.selection = self.selection.moved(&self.paragraph, motion, extend);
}
pub fn insert(&mut self, system: &mut TextSystem, text: &str) -> Result<(), ParagraphError> {
let range = self.composition().unwrap_or_else(|| self.selection.range());
let caret = range.start + text.len();
let affinity = if text.ends_with(LINE_BREAKS) {
Affinity::Downstream
} else {
Affinity::Upstream
};
self.replace(system, range, text, |p| {
let at = TextSelection::caret_with(p, caret, affinity);
if at.focus() < caret {
at.moved(p, Motion::Next, false)
} else {
at
}
})?;
self.composition = None;
self.shows_caret = true;
Ok(())
}
pub fn delete(
&mut self,
system: &mut TextSystem,
motion: Motion,
) -> Result<(), ParagraphError> {
if self.composition.is_some() {
return self.set_composition(system, "", None);
}
let range = self.deletion_range(motion);
if range.is_empty() {
return Ok(());
}
let caret = range.start;
self.replace(system, range, "", |p| TextSelection::caret_at(p, caret))
}
pub fn set_text(&mut self, system: &mut TextSystem, text: &str) -> Result<(), ParagraphError> {
let all = 0..self.text.len();
let caret = text.len();
self.replace(system, all, text, |p| TextSelection::caret_at(p, caret))?;
self.composition = None;
self.shows_caret = true;
Ok(())
}
pub fn set_style(
&mut self,
system: &mut TextSystem,
style: ParagraphStyle,
) -> Result<(), ParagraphError> {
let paragraph = system.paragraph(&self.text, &style, &[])?;
self.selection = self.selection.refreshed(¶graph);
self.paragraph = paragraph;
self.style = style;
Ok(())
}
pub fn set_width(&mut self, max_width: Option<f32>) -> Result<(), ParagraphError> {
self.paragraph.reflow(max_width)?;
self.style.max_width = max_width;
self.selection = self.selection.refreshed(&self.paragraph);
Ok(())
}
pub fn set_composition(
&mut self,
system: &mut TextSystem,
text: &str,
cursor: Option<Range<usize>>,
) -> Result<(), ParagraphError> {
if let Some(cursor) = cursor.as_ref().filter(|c| !is_char_range(text, c)) {
return Err(ParagraphError::InvalidRange(cursor.clone()));
}
if text.is_empty() {
return self.cancel_composition(system);
}
let (range, replaced) = match &self.composition {
Some(composition) => (composition.range.clone(), composition.replaced.clone()),
None => (self.selection.range(), self.selected_text().to_owned()),
};
let start = range.start;
let selected = cursor.clone().unwrap_or(0..0);
self.replace(system, range, text, |p| {
TextSelection::from_range(p, start + selected.start, start + selected.end)
})?;
self.composition = Some(Composition {
range: start..start + text.len(),
replaced,
});
self.shows_caret = cursor.is_some();
Ok(())
}
pub fn for_each_composition_rect(&self, f: impl FnMut(Rect)) {
if let Some(range) = self.composition() {
TextSelection::from_range(&self.paragraph, range.start, range.end)
.for_each_rect(&self.paragraph, f);
}
}
pub fn ime_area(&self) -> Rect {
let caret = self.selection.caret_rect(&self.paragraph, 0.0);
let mut area: Option<Rect> = None;
self.for_each_composition_rect(|rect| {
area = Some(area.map_or(rect, |a| union(a, rect)));
});
area.unwrap_or(caret)
}
fn deletion_range(&self, motion: Motion) -> Range<usize> {
if !self.selection.is_caret() {
return self.selection.range();
}
let caret = self.selection.focus();
if motion == Motion::Previous {
return backspace_start(&self.paragraph, &self.text, caret)..caret;
}
self.selection.moved(&self.paragraph, motion, true).range()
}
fn cancel_composition(&mut self, system: &mut TextSystem) -> Result<(), ParagraphError> {
if let Some(Composition { range, replaced }) = self.composition.clone() {
let (start, end) = (range.start, range.start + replaced.len());
self.replace(system, range, &replaced, |p| {
TextSelection::from_range(p, start, end)
})?;
}
self.end_composition();
Ok(())
}
fn end_composition(&mut self) {
self.composition = None;
self.shows_caret = true;
}
fn replace(
&mut self,
system: &mut TextSystem,
range: Range<usize>,
with: &str,
select: impl FnOnce(&Paragraph) -> TextSelection,
) -> Result<(), ParagraphError> {
let mut text = String::with_capacity(self.text.len() - range.len() + with.len());
text.push_str(&self.text[..range.start]);
text.push_str(with);
text.push_str(&self.text[range.end..]);
let paragraph = system.paragraph(&text, &self.style, &[])?;
self.selection = select(¶graph);
self.paragraph = paragraph;
self.text = text;
Ok(())
}
}
fn union(a: Rect, b: Rect) -> Rect {
Rect::from_edges(
a.left().min(b.left()),
a.top().min(b.top()),
a.right().max(b.right()),
a.bottom().max(b.bottom()),
)
}