use std::cell::RefCell;
use std::ops::Range;
use parley::Cluster;
use parley::StyleProperty;
use parley::editing::PlainEditor;
use parley::editing::PlainEditorDriver;
use parley::editing::Selection;
use parley::style::LineHeight;
use parley::Generation;
use smudge_core::geometry::{Color, Point, Rect, Size};
use smudge_core::paint::GlyphRun;
use smudge_core::reactive::{RwSignal, prelude::*};
use crate::glyphs::glyph_runs;
use crate::style::TextStyle;
use crate::system::TextSystem;
const DEFAULT_FONT_SIZE: f32 = 14.0;
const HISTORY_LIMIT: usize = 100;
const UNDERLINE_OFFSET: f32 = 1.0;
const UNDERLINE_THICKNESS: f32 = 1.0;
pub struct Editor {
inner: RefCell<PlainEditor<()>>,
value: RwSignal<String>,
history: RefCell<History>,
}
struct Snapshot {
text: String,
selection: Selection,
}
#[derive(Clone, Copy, PartialEq)]
enum EditKind {
Insert,
Delete,
}
#[derive(Clone, Copy, PartialEq)]
enum Grouping {
Joins,
Starts,
Alone,
}
#[derive(Default)]
struct History {
undo: Vec<Snapshot>,
redo: Vec<Snapshot>,
open: Option<(EditKind, Range<usize>)>,
}
impl Editor {
pub fn new(value: RwSignal<String>) -> Editor {
let mut inner = PlainEditor::new(DEFAULT_FONT_SIZE);
inner.set_width(None);
Editor {
inner: RefCell::new(inner),
value,
history: RefCell::new(History::default()),
}
}
pub fn value(&self) -> RwSignal<String> {
self.value.clone()
}
pub fn set_style(&self, style: &TextStyle) {
let mut editor = self.inner.borrow_mut();
let styles = editor.edit_styles();
styles.insert(StyleProperty::FontFamily(style.family.clone()));
styles.insert(StyleProperty::FontSize(style.size));
styles.insert(StyleProperty::FontWeight(style.weight));
styles.insert(StyleProperty::LineHeight(LineHeight::FontSizeRelative(
style.line_height,
)));
}
pub fn text(&self) -> String {
self.inner.borrow().text().to_string()
}
pub fn is_empty(&self) -> bool {
self.inner.borrow().text().chars().next().is_none()
}
pub fn has_text(&self, text: &str) -> bool {
self.inner.borrow().text() == text
}
pub fn raw_text(&self) -> String {
self.inner.borrow().raw_text().to_owned()
}
pub fn selection(&self) -> Range<usize> {
self.inner.borrow().raw_selection().text_range()
}
pub fn is_composing(&self) -> bool {
self.inner.borrow().is_composing()
}
pub(crate) fn generation(&self) -> Generation {
self.inner.borrow().generation()
}
pub fn ime_area(&self) -> Rect {
self.refresh();
rect(self.inner.borrow().ime_cursor_area())
}
pub fn compose_geometry(&self) -> Vec<Rect> {
self.refresh();
let editor = self.inner.borrow();
let Some(range) = editor.raw_compose().clone() else {
return Vec::new();
};
let Some(layout) = editor.try_layout() else {
return Vec::new();
};
let mut rects = Vec::new();
let mut index = range.start;
while index < range.end {
let Some(cluster) = Cluster::from_byte_index(layout, index) else {
break;
};
index = cluster.text_range().end.max(index + 1);
let Some(offset) = cluster.visual_offset() else {
continue;
};
let baseline = cluster.line().metrics().baseline;
let top = baseline + UNDERLINE_OFFSET;
rects.push(Rect::new(
offset,
top,
offset + cluster.advance(),
top + UNDERLINE_THICKNESS,
));
}
rects
}
pub fn set_text(&self, text: &str) {
self.inner.borrow_mut().set_text(text);
self.drive(|driver| driver.move_to_text_end());
*self.history.borrow_mut() = History::default();
}
pub fn insert(&self, text: &str) {
let mut chars = text.chars();
let grouping = match (chars.next(), chars.next()) {
(Some(single), None) if single.is_whitespace() => Grouping::Starts,
(Some(_), None) => Grouping::Joins,
_ => Grouping::Alone,
};
self.edit(EditKind::Insert, grouping, |driver| {
driver.insert_or_replace_selection(text);
});
}
pub fn paste(&self, text: &str) {
let single_line: String = text.chars().filter(|c| *c != '\n' && *c != '\r').collect();
if single_line.is_empty() {
return;
}
self.edit(EditKind::Insert, Grouping::Alone, |driver| {
driver.insert_or_replace_selection(&single_line);
});
}
pub fn delete_selection(&self) {
self.edit(EditKind::Delete, Grouping::Alone, |driver| {
driver.delete_selection();
});
}
pub fn selected_text(&self) -> Option<String> {
self.refresh();
self.inner.borrow().selected_text().map(str::to_owned)
}
pub fn compose(&self, text: &str, cursor: Option<(usize, usize)>) {
if text.is_empty() {
self.cancel_compose();
return;
}
if !self.is_composing() {
self.delete_selection();
}
let end = text.len();
let cursor = cursor.map(|(anchor, focus)| (anchor.min(end), focus.min(end)));
self.drive(|driver| driver.set_compose(text, cursor));
}
fn cancel_compose(&self) {
if self.is_composing() {
self.drive(|driver| driver.clear_compose());
}
}
pub fn commit(&self, text: &str) {
if text.is_empty() {
self.cancel_compose();
return;
}
self.insert(text);
}
pub fn backspace(&self) {
self.edit(EditKind::Delete, Grouping::Joins, |driver| {
driver.backdelete();
});
}
pub fn backspace_word(&self) {
self.edit(EditKind::Delete, Grouping::Alone, |driver| {
driver.backdelete_word();
});
}
pub fn delete(&self) {
self.edit(EditKind::Delete, Grouping::Joins, |driver| driver.delete());
}
pub fn delete_word(&self) {
self.edit(EditKind::Delete, Grouping::Alone, |driver| {
driver.delete_word();
});
}
pub fn undo(&self) {
if self.is_composing() {
return;
}
let Some(previous) = self.history.borrow_mut().undo.pop() else {
return;
};
let current = self.snapshot();
let mut history = self.history.borrow_mut();
history.redo.push(current);
history.open = None;
drop(history);
self.restore(previous);
}
pub fn redo(&self) {
if self.is_composing() {
return;
}
let Some(next) = self.history.borrow_mut().redo.pop() else {
return;
};
let current = self.snapshot();
let mut history = self.history.borrow_mut();
history.undo.push(current);
history.open = None;
drop(history);
self.restore(next);
}
pub fn move_left(&self) {
self.drive(|driver| driver.move_left());
}
pub fn move_right(&self) {
self.drive(|driver| driver.move_right());
}
pub fn move_word_left(&self) {
self.drive(|driver| driver.move_word_left());
}
pub fn move_word_right(&self) {
self.drive(|driver| driver.move_word_right());
}
pub fn move_to_start(&self) {
self.drive(|driver| driver.move_to_text_start());
}
pub fn move_to_end(&self) {
self.drive(|driver| driver.move_to_text_end());
}
pub fn extend_left(&self) {
self.drive(|driver| driver.select_left());
}
pub fn extend_right(&self) {
self.drive(|driver| driver.select_right());
}
pub fn extend_word_left(&self) {
self.drive(|driver| driver.select_word_left());
}
pub fn extend_word_right(&self) {
self.drive(|driver| driver.select_word_right());
}
pub fn extend_to_start(&self) {
self.drive(|driver| driver.select_to_text_start());
}
pub fn extend_to_end(&self) {
self.drive(|driver| driver.select_to_text_end());
}
pub fn select_all(&self) {
self.drive(|driver| driver.select_all());
}
pub fn move_to_point(&self, x: f32, y: f32) {
self.drive(|driver| driver.move_to_point(x, y));
}
pub fn extend_to_point(&self, x: f32, y: f32) {
self.drive(|driver| driver.extend_selection_to_point(x, y));
}
pub fn select_word_at_point(&self, x: f32, y: f32) {
self.drive(|driver| driver.select_word_at_point(x, y));
}
fn refresh(&self) {
self.drive(|driver| {
driver.refresh_layout();
});
}
pub fn cursor_geometry(&self, width: f32) -> Option<Rect> {
self.refresh();
self.inner.borrow().cursor_geometry(width).map(rect)
}
pub fn selection_geometry(&self) -> Vec<Rect> {
self.refresh();
self.inner
.borrow()
.selection_geometry()
.into_iter()
.map(|(area, _line)| rect(area))
.filter(|area| area.width() > 0.0)
.collect()
}
pub fn layout_size(&self) -> Size {
self.refresh();
let editor = self.inner.borrow();
let layout = editor.try_layout().expect("refresh made the layout clean");
Size {
width: layout.width(),
height: layout.height(),
}
}
pub fn glyph_runs_at(&self, origin: Point, color: Color) -> Vec<GlyphRun> {
self.refresh();
let borrow = self.inner.borrow();
match borrow.try_layout() {
Some(layout) => glyph_runs(layout, origin, color),
None => Vec::new(),
}
}
fn sync_text(&self) {
if !self.value.read(|current| self.has_text(current)) {
self.value.set(self.text());
}
}
fn edit(
&self,
kind: EditKind,
grouping: Grouping,
f: impl FnOnce(&mut PlainEditorDriver<'_, ()>),
) {
self.cancel_compose();
let before = self.snapshot();
self.drive(f);
if self.has_text(&before.text) {
return;
}
let mut history = self.history.borrow_mut();
let starts = before.selection.text_range();
let continues = grouping == Grouping::Joins
&& history
.open
.as_ref()
.is_some_and(|(open, ended)| *open == kind && *ended == starts);
if !continues {
if history.undo.len() == HISTORY_LIMIT {
history.undo.remove(0);
}
history.undo.push(before);
}
history.redo.clear();
history.open = (grouping != Grouping::Alone).then(|| (kind, self.selection()));
drop(history);
self.sync_text();
}
fn snapshot(&self) -> Snapshot {
let editor = self.inner.borrow();
Snapshot {
text: editor.text().to_string(),
selection: *editor.raw_selection(),
}
}
fn restore(&self, snapshot: Snapshot) {
self.inner.borrow_mut().set_text(&snapshot.text);
self.drive(|driver| driver.set_selection(snapshot.selection));
self.sync_text();
}
fn drive<R>(&self, f: impl FnOnce(&mut PlainEditorDriver<'_, ()>) -> R) -> R {
TextSystem::with(|system| {
let mut editor = self.inner.borrow_mut();
let mut driver = editor.driver(&mut system.fonts, &mut system.layouts);
f(&mut driver)
})
}
}
fn rect(area: parley::BoundingBox) -> Rect {
Rect::new(
area.x0 as f32,
area.y0 as f32,
area.x1 as f32,
area.y1 as f32,
)
}