use std::borrow::Cow;
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::ops::Range;
use parley::Cluster;
use parley::editing::Cursor;
use parley::editing::Selection as ParagraphSelection;
use parley::layout::Affinity;
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::layout::TextLayout;
use crate::line_breaks::flatten_line_breaks;
use crate::style::TextStyle;
use crate::system::TextSystem;
const HISTORY_BYTES: usize = 8 << 20;
const NEWLINE_WIDTH: f32 = 0.3;
const UNDERLINE_OFFSET: f32 = 1.0;
const UNDERLINE_THICKNESS: f32 = 1.0;
const IME_INFLATE: f32 = 1.8;
pub struct Editor {
value: RwSignal<String>,
buffer: RefCell<String>,
paragraphs: RefCell<ParagraphTable>,
selection: RefCell<Selection>,
compose: RefCell<Option<Compose>>,
history: RefCell<History>,
style: RefCell<TextStyle>,
width: Cell<Option<f32>>,
builds: Cell<u64>,
single_line: bool,
}
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: VecDeque<Snapshot>,
redo: Vec<Snapshot>,
bytes: usize,
open: Option<(EditKind, Range<usize>)>,
}
impl History {
fn cost(snapshot: &Snapshot) -> usize {
snapshot.text.len() + size_of::<Snapshot>()
}
fn push_undo(&mut self, snapshot: Snapshot) {
self.bytes += Self::cost(&snapshot);
self.undo.push_back(snapshot);
while self.bytes > HISTORY_BYTES && self.undo.len() > 1 {
if let Some(oldest) = self.undo.pop_front() {
self.bytes -= Self::cost(&oldest);
}
}
}
fn pop_undo(&mut self) -> Option<Snapshot> {
let snapshot = self.undo.pop_back()?;
self.bytes -= Self::cost(&snapshot);
Some(snapshot)
}
fn push_redo(&mut self, snapshot: Snapshot) {
self.bytes += Self::cost(&snapshot);
self.redo.push(snapshot);
}
fn pop_redo(&mut self) -> Option<Snapshot> {
let snapshot = self.redo.pop()?;
self.bytes -= Self::cost(&snapshot);
Some(snapshot)
}
fn clear_redo(&mut self) {
for snapshot in self.redo.drain(..) {
self.bytes -= Self::cost(&snapshot);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct Anchor {
paragraph: usize,
index: usize,
affinity: Affinity,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum Base {
#[default]
Cluster,
Word([Anchor; 2]),
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct Selection {
anchor: Anchor,
focus: Anchor,
base: Base,
h_pos: Option<f32>,
}
struct Compose {
range: Range<usize>,
cursor: Option<(usize, usize)>,
}
struct Paragraph {
start: usize,
len: usize,
layout: Option<TextLayout>,
stamp: u64,
}
#[derive(Default)]
struct ParagraphTable {
paragraphs: Vec<Paragraph>,
}
impl ParagraphTable {
fn global(&self, anchor: Anchor) -> usize {
self.paragraphs[anchor.paragraph].start + anchor.index
}
fn locate(&self, byte: usize) -> (usize, usize) {
let paragraph = self.index_at(byte);
(paragraph, byte - self.paragraphs[paragraph].start)
}
fn index_at(&self, byte: usize) -> usize {
self.paragraphs
.partition_point(|paragraph| paragraph.start <= byte)
- 1
}
fn rebuild(&mut self, buffer: &str) {
let mut paragraphs = Vec::new();
let mut start = 0;
for (offset, byte) in buffer.bytes().enumerate() {
if byte == b'\n' {
paragraphs.push(Paragraph {
start,
len: offset - start,
layout: None,
stamp: 0,
});
start = offset + 1;
}
}
paragraphs.push(Paragraph {
start,
len: buffer.len() - start,
layout: None,
stamp: 0,
});
self.paragraphs = paragraphs;
}
fn splice(&mut self, range: Range<usize>, replacement: &str) {
let first = self.index_at(range.start);
let mut last = if range.is_empty() {
first
} else {
self.index_at(range.end - 1)
};
if !range.is_empty()
&& last + 1 < self.paragraphs.len()
&& range.end == self.paragraphs[last + 1].start
{
last += 1;
}
let first_start = self.paragraphs[first].start;
let last_end = self.paragraphs[last].start + self.paragraphs[last].len;
let head = range.start - first_start;
let tail = last_end.saturating_sub(range.end);
self.paragraphs.drain(first..=last);
let delta = replacement.len() as isize - (range.end - range.start) as isize;
for paragraph in self.paragraphs[first..].iter_mut() {
paragraph.start = (paragraph.start as isize + delta) as usize;
}
let pieces: Vec<&str> = replacement.split('\n').collect();
let last_piece = pieces.len() - 1;
let mut entries = Vec::with_capacity(pieces.len());
let mut start = first_start;
for (index, piece) in pieces.iter().enumerate() {
let len = piece.len()
+ if index == 0 { head } else { 0 }
+ if index == last_piece { tail } else { 0 };
entries.push(Paragraph {
start,
len,
layout: None,
stamp: 0,
});
start += len + 1;
}
self.paragraphs.splice(first..first, entries);
}
}
impl Editor {
pub fn new(value: RwSignal<String>) -> Editor {
Editor::with_lines(value, false)
}
pub fn single_line(value: RwSignal<String>) -> Editor {
Editor::with_lines(value, true)
}
fn with_lines(value: RwSignal<String>, single_line: bool) -> Editor {
let mut paragraphs = ParagraphTable::default();
paragraphs.rebuild("");
Editor {
value,
buffer: RefCell::new(String::new()),
paragraphs: RefCell::new(paragraphs),
selection: RefCell::new(Selection {
anchor: START,
focus: START,
base: Base::Cluster,
h_pos: None,
}),
compose: RefCell::new(None),
history: RefCell::new(History::default()),
style: RefCell::new(TextStyle::default()),
width: Cell::new(None),
builds: Cell::new(0),
single_line,
}
}
pub fn value(&self) -> RwSignal<String> {
self.value.clone()
}
pub fn set_style(&self, style: &TextStyle) {
*self.style.borrow_mut() = style.clone();
self.invalidate_layouts();
}
pub fn set_wrap_width(&self, width: Option<f32>) {
if self.width.get() == width {
return;
}
self.width.set(width);
for paragraph in self.paragraphs.borrow_mut().paragraphs.iter_mut() {
if let Some(layout) = paragraph.layout.as_mut() {
layout.rebreak(width);
self.builds.set(self.builds.get() + 1);
paragraph.stamp = self.builds.get();
}
}
}
pub fn text(&self) -> String {
let buffer = self.buffer.borrow();
let compose = self.compose.borrow();
match compose.as_ref() {
Some(compose) => {
let mut text = String::with_capacity(buffer.len() - compose.range.len());
text.push_str(&buffer[..compose.range.start]);
text.push_str(&buffer[compose.range.end..]);
text
}
None => buffer.clone(),
}
}
pub fn is_empty(&self) -> bool {
let preedit = self
.compose
.borrow()
.as_ref()
.map_or(0, |compose| compose.range.len());
self.buffer.borrow().len() == preedit
}
pub fn has_text(&self, text: &str) -> bool {
let buffer = self.buffer.borrow();
match self.compose.borrow().as_ref() {
Some(compose) => {
let (head, tail) = (&buffer[..compose.range.start], &buffer[compose.range.end..]);
text.len() == head.len() + tail.len()
&& text.starts_with(head)
&& text.ends_with(tail)
}
None => *buffer == text,
}
}
pub fn raw_text(&self) -> String {
self.buffer.borrow().clone()
}
pub fn selection(&self) -> Range<usize> {
let paragraphs = self.paragraphs.borrow();
let selection = self.selection.borrow();
let anchor = paragraphs.global(selection.anchor);
let focus = paragraphs.global(selection.focus);
anchor.min(focus)..anchor.max(focus)
}
pub fn is_composing(&self) -> bool {
self.compose.borrow().is_some()
}
pub fn ime_area(&self) -> Rect {
let preedit = self
.compose
.borrow()
.as_ref()
.map(|compose| compose.range.clone());
self.session(|session| {
session.refresh_all();
let mut area: Option<Rect> = None;
let mut add = |rect: Rect| area = Some(area.map_or(rect, |area| area.union(rect)));
match preedit {
Some(range) => {
session.range_rects(range, EVERYWHERE, &mut add);
if area.is_none() {
let focus = session.selection.focus;
let (caret, y) = session.caret_box(focus, 0.0);
area = Some(Rect::new(caret.x0, caret.y0 + y, caret.x1, caret.y1 + y));
}
}
None => {
let focus = session.selection.focus;
let (caret, y) = session.caret_box(focus, 0.0);
let caret = Rect::new(caret.x0, caret.y0 + y, caret.x1, caret.y1 + y);
let range = session.selection_range();
session.range_rects(range, EVERYWHERE, |rect| {
if rect.y0 == caret.y0 {
add(rect);
}
});
add(caret);
}
}
let area = area.expect("the caret always gives the area a rectangle");
let height: f32 = session
.table
.paragraphs
.iter()
.map(|paragraph| {
paragraph
.layout
.as_ref()
.expect("refresh built every layout")
.size()
.height
})
.sum();
let style = self.style.borrow();
let inflate = IME_INFLATE * style.size;
let width = session.width.unwrap_or(f32::INFINITY);
Rect::new(
(area.x0 - inflate).max(0.0),
area.y0.max(0.0),
(area.x1 + inflate).min(width),
area.y1.min(height),
)
})
}
pub fn compose_geometry(&self) -> Vec<Rect> {
let Some(range) = self
.compose
.borrow()
.as_ref()
.map(|compose| compose.range.clone())
else {
return Vec::new();
};
self.session(|session| {
session.refresh_all();
let mut rects = Vec::new();
let mut y = 0.0;
for index in 0..session.table.paragraphs.len() {
let paragraph = &session.table.paragraphs[index];
let start = paragraph.start;
let len = paragraph.len;
let layout = paragraph
.layout
.as_ref()
.expect("refresh built every layout");
if range.start < start + len && range.end > start {
let mut index = range.start.max(start) - start;
let end = range.end.min(start + len) - start;
while index < end {
let Some(cluster) = Cluster::from_byte_index(&layout.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 + y,
offset + cluster.advance(),
top + UNDERLINE_THICKNESS + y,
));
}
}
y += layout.size().height;
}
rects
})
}
pub fn set_text(&self, text: &str) {
let text = self.incoming(text);
let text = text.as_ref();
self.session(|session| {
session.buffer.clear();
session.buffer.push_str(text);
session.table.rebuild(session.buffer);
let end = session.document_end();
session.selection = Selection {
anchor: end,
focus: end,
base: Base::Cluster,
h_pos: None,
};
});
*self.history.borrow_mut() = History::default();
*self.compose.borrow_mut() = None;
self.sync_text();
}
pub fn insert(&self, text: &str) {
let text = self.incoming(text);
let text = text.as_ref();
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, |session| {
let range = session.selection_range();
let (byte, upstream) = Session::caret_after_insert(range.start, text);
session.replace(range, text, byte, upstream)
});
}
pub fn paste(&self, text: &str) {
if text.is_empty() {
return;
}
let text = self.incoming(text);
let text = text.as_ref();
self.edit(EditKind::Insert, Grouping::Alone, |session| {
let range = session.selection_range();
let (byte, upstream) = Session::caret_after_insert(range.start, text);
session.replace(range, text, byte, upstream)
});
}
pub fn delete_selection(&self) {
self.edit(EditKind::Delete, Grouping::Alone, |session| {
let range = session.selection_range();
if range.is_empty() {
return false;
}
let byte = range.start;
session.replace(range, "", byte, true)
});
}
pub fn selected_text(&self) -> Option<String> {
let range = self.selection();
if range.is_empty() {
return None;
}
Some(self.buffer.borrow()[range].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 cursor =
cursor.map(|(anchor, focus)| (char_boundary(text, anchor), char_boundary(text, focus)));
self.session(|session| {
let (replaced, start) = if let Some(previous) = self.compose.borrow_mut().as_ref() {
let range = previous.range.clone();
session.buffer.replace_range(range.clone(), text);
let start = range.start;
(range, start)
} else {
let start = session.selection_range().start;
session.buffer.insert_str(start, text);
(start..start, start)
};
let range = start..start + text.len();
*self.compose.borrow_mut() = Some(Compose {
range: range.clone(),
cursor,
});
session.table.splice(replaced, text);
let (anchor, focus) = match cursor {
Some((anchor, focus)) => (start + anchor, start + focus),
None => (start, start),
};
let anchor = session.cursor_anchor(anchor);
let focus = session.cursor_anchor(focus);
session.selection = Selection {
anchor,
focus,
base: Base::Cluster,
h_pos: None,
};
});
}
fn cancel_compose(&self) {
if self.compose.borrow().is_some() {
self.session(|session| {
let Some(compose) = self.compose.borrow_mut().take() else {
return;
};
let range = compose.range;
session.buffer.replace_range(range.clone(), "");
session.table.splice(range.clone(), "");
let caret = session.cursor_anchor(range.start);
session.selection = Selection {
anchor: caret,
focus: caret,
base: Base::Cluster,
h_pos: None,
};
});
}
}
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, |session| {
if session.selection.anchor != session.selection.focus {
let range = session.selection_range();
let byte = range.start;
return session.replace(range, "", byte, true);
}
let focus = session.selection.focus;
let Some(range) = session.backspace_range(focus) else {
return false;
};
let byte = range.start;
session.replace(range, "", byte, false)
});
}
pub fn backspace_word(&self) {
self.edit(EditKind::Delete, Grouping::Alone, |session| {
if session.selection.anchor != session.selection.focus {
let range = session.selection_range();
let byte = range.start;
return session.replace(range, "", byte, true);
}
let focus = session.selection.focus;
let Some(start) = session.word_before(focus) else {
return false;
};
let end = session.table.global(focus);
if start >= end {
return false;
}
let byte = start;
session.replace(byte..end, "", byte, false)
});
}
pub fn delete(&self) {
self.edit(EditKind::Delete, Grouping::Joins, |session| {
if session.selection.anchor != session.selection.focus {
let range = session.selection_range();
let byte = range.start;
return session.replace(range, "", byte, true);
}
let focus = session.selection.focus;
let Some(range) = session.forward_delete_range(focus) else {
return false;
};
let byte = range.start;
session.replace(range, "", byte, false)
});
}
pub fn delete_word(&self) {
self.edit(EditKind::Delete, Grouping::Alone, |session| {
if session.selection.anchor != session.selection.focus {
let range = session.selection_range();
let byte = range.start;
return session.replace(range, "", byte, true);
}
let focus = session.selection.focus;
let start = session.table.global(focus);
let Some(end) = session.word_after(focus) else {
return false;
};
if start >= end {
return false;
}
let byte = start;
session.replace(byte..end, "", byte, false)
});
}
pub fn undo(&self) {
if self.is_composing() {
return;
}
let Some(previous) = self.history.borrow_mut().pop_undo() else {
return;
};
let current = self.snapshot();
let mut history = self.history.borrow_mut();
history.push_redo(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().pop_redo() else {
return;
};
let current = self.snapshot();
let mut history = self.history.borrow_mut();
history.push_undo(current);
history.open = None;
drop(history);
self.restore(next);
}
pub fn move_left(&self) {
self.caret_move(|session| session.move_horizontally(false, false));
}
pub fn move_right(&self) {
self.caret_move(|session| session.move_horizontally(true, false));
}
pub fn move_word_left(&self) {
self.caret_move(|session| session.move_word(false, false));
}
pub fn move_word_right(&self) {
self.caret_move(|session| session.move_word(true, false));
}
pub fn move_up(&self) {
self.caret_move(|session| session.move_lines(-1, false));
}
pub fn move_down(&self) {
self.caret_move(|session| session.move_lines(1, false));
}
pub fn move_lines(&self, lines: isize) {
self.caret_move(|session| session.move_lines(lines, false));
}
pub fn move_to_start(&self) {
self.caret_move(|session| session.move_to_edge(false, false));
}
pub fn move_to_end(&self) {
self.caret_move(|session| session.move_to_edge(true, false));
}
pub fn move_to_line_start(&self) {
self.caret_move(|session| session.move_to_line_edge(false, false));
}
pub fn move_to_line_end(&self) {
self.caret_move(|session| session.move_to_line_edge(true, false));
}
pub fn extend_left(&self) {
self.caret_move(|session| session.move_horizontally(false, true));
}
pub fn extend_right(&self) {
self.caret_move(|session| session.move_horizontally(true, true));
}
pub fn extend_word_left(&self) {
self.caret_move(|session| session.move_word(false, true));
}
pub fn extend_word_right(&self) {
self.caret_move(|session| session.move_word(true, true));
}
pub fn extend_up(&self) {
self.caret_move(|session| session.move_lines(-1, true));
}
pub fn extend_down(&self) {
self.caret_move(|session| session.move_lines(1, true));
}
pub fn extend_lines(&self, lines: isize) {
self.caret_move(|session| session.move_lines(lines, true));
}
pub fn extend_to_start(&self) {
self.caret_move(|session| session.move_to_edge(false, true));
}
pub fn extend_to_end(&self) {
self.caret_move(|session| session.move_to_edge(true, true));
}
pub fn extend_to_line_start(&self) {
self.caret_move(|session| session.move_to_line_edge(false, true));
}
pub fn extend_to_line_end(&self) {
self.caret_move(|session| session.move_to_line_edge(true, true));
}
pub fn select_all(&self) {
self.caret_move(|session| {
let end = session.document_end();
session.selection = Selection {
anchor: START,
focus: end,
base: Base::Cluster,
h_pos: None,
};
});
}
pub fn move_to_point(&self, x: f32, y: f32) {
self.caret_move(|session| {
let target = session.point_anchor(x, y);
session.set_focus(target, false);
});
}
pub fn extend_to_point(&self, x: f32, y: f32) {
self.caret_move(|session| session.extend_to_point(x, y));
}
pub fn select_word_at_point(&self, x: f32, y: f32) {
self.caret_move(|session| session.select_word_at_point(x, y));
}
pub fn cursor_geometry(&self, width: f32) -> Option<Rect> {
if self
.compose
.borrow()
.as_ref()
.is_some_and(|compose| compose.cursor.is_none())
{
return None;
}
self.session(|session| {
let focus = session.selection.focus;
let (caret, y) = session.caret_box(focus, width);
Some(Rect::new(caret.x0, caret.y0 + y, caret.x1, caret.y1 + y))
})
}
pub fn selection_geometry(&self) -> Vec<Rect> {
let mut rects = Vec::new();
self.selection_rects(EVERYWHERE, &mut rects);
rects
}
pub(crate) fn selection_rects(&self, visible: (f32, f32), out: &mut Vec<Rect>) {
out.clear();
self.session(|session| {
let range = session.selection_range();
if !range.is_empty() {
session.range_rects(range, visible, |rect| out.push(rect));
}
});
}
pub fn layout_size(&self) -> Size {
self.session(|session| {
session.refresh_all();
let mut size = Size {
width: 0.0,
height: 0.0,
};
for paragraph in &session.table.paragraphs {
let paragraph_size = paragraph
.layout
.as_ref()
.expect("refresh built every layout")
.size();
size.width = size.width.max(paragraph_size.width);
size.height += paragraph_size.height;
}
size
})
}
pub fn glyph_runs_at(&self, origin: Point, color: Color) -> Vec<GlyphRun> {
self.session(|session| {
session.refresh_all();
let mut runs = Vec::new();
let mut y = 0.0;
for paragraph in &session.table.paragraphs {
let layout = paragraph
.layout
.as_ref()
.expect("refresh built every layout");
runs.extend(glyph_runs(
&layout.layout,
Point {
x: origin.x,
y: origin.y + y,
},
color,
));
y += layout.size().height;
}
runs
})
}
pub fn paragraphs(&self, mut f: impl FnMut(EditorParagraph<'_>)) {
self.session(|session| session.refresh_all());
let table = self.paragraphs.borrow();
let mut y = 0.0;
for paragraph in &table.paragraphs {
let layout = paragraph
.layout
.as_ref()
.expect("paragraphs built every layout");
f(EditorParagraph {
layout,
stamp: paragraph.stamp,
y,
});
y += layout.size().height;
}
}
fn incoming<'a>(&self, text: &'a str) -> Cow<'a, str> {
if self.single_line {
if text.contains(crate::line_breaks::is_line_break) {
Cow::Owned(flatten_line_breaks(text))
} else {
Cow::Borrowed(text)
}
} else {
normalize_breaks(text)
}
}
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 Session<'_>) -> bool) {
self.cancel_compose();
let starts = self.selection();
let selection = *self.selection.borrow();
let continues = grouping == Grouping::Joins
&& self
.history
.borrow()
.open
.as_ref()
.is_some_and(|(open, ended)| *open == kind && *ended == starts);
let (changed, before) = self.session_keeping(!continues, f);
if !changed {
return;
}
let mut history = self.history.borrow_mut();
if let Some(text) = before {
history.push_undo(Snapshot { text, selection });
}
history.clear_redo();
history.open = (grouping != Grouping::Alone).then(|| (kind, self.selection()));
drop(history);
self.sync_text();
}
fn snapshot(&self) -> Snapshot {
Snapshot {
text: copy_for_undo(&self.buffer.borrow()),
selection: *self.selection.borrow(),
}
}
fn restore(&self, snapshot: Snapshot) {
self.session(|session| {
let range = changed_range(session.buffer, &snapshot.text);
let kept_end = session.buffer.len() - range.end;
let replacement = &snapshot.text[range.start..snapshot.text.len() - kept_end];
if !range.is_empty() || !replacement.is_empty() {
session.buffer.replace_range(range.clone(), replacement);
session.table.splice(range, replacement);
}
session.selection = snapshot.selection;
});
self.sync_text();
}
fn session<R>(&self, f: impl FnOnce(&mut Session<'_>) -> R) -> R {
self.session_keeping(false, f).0
}
fn session_keeping<R>(
&self,
keep_before: bool,
f: impl FnOnce(&mut Session<'_>) -> R,
) -> (R, Option<String>) {
let mut buffer = self.buffer.borrow_mut();
let mut table = self.paragraphs.borrow_mut();
let style = self.style.borrow();
let mut session = Session {
buffer: &mut buffer,
table: &mut table,
style: &style,
width: self.width.get(),
builds: &self.builds,
selection: *self.selection.borrow(),
keep_before,
before: None,
};
let result = f(&mut session);
*self.selection.borrow_mut() = session.selection;
(result, session.before)
}
fn caret_move(&self, f: impl FnOnce(&mut Session<'_>)) {
self.session(f);
self.history.borrow_mut().open = None;
}
fn invalidate_layouts(&self) {
for paragraph in self.paragraphs.borrow_mut().paragraphs.iter_mut() {
paragraph.layout = None;
}
}
}
const EVERYWHERE: (f32, f32) = (f32::NEG_INFINITY, f32::INFINITY);
const START: Anchor = Anchor {
paragraph: 0,
index: 0,
affinity: Affinity::Downstream,
};
pub struct EditorParagraph<'a> {
layout: &'a TextLayout,
stamp: u64,
y: f32,
}
impl EditorParagraph<'_> {
pub fn stamp(&self) -> u64 {
self.stamp
}
pub fn offset(&self) -> Point {
Point { x: 0.0, y: self.y }
}
pub fn size(&self) -> Size {
self.layout.size()
}
pub fn glyph_runs_at(&self, origin: Point, color: Color) -> Vec<GlyphRun> {
self.layout.glyph_runs_at(origin, color)
}
}
struct Session<'a> {
buffer: &'a mut String,
table: &'a mut ParagraphTable,
style: &'a TextStyle,
width: Option<f32>,
builds: &'a Cell<u64>,
selection: Selection,
keep_before: bool,
before: Option<String>,
}
impl Session<'_> {
fn selection_range(&self) -> Range<usize> {
let anchor = self.table.global(self.selection.anchor);
let focus = self.table.global(self.selection.focus);
anchor.min(focus)..anchor.max(focus)
}
fn cursor_anchor(&self, byte: usize) -> Anchor {
self.anchor_at(byte, false)
}
fn anchor_at(&self, byte: usize, upstream: bool) -> Anchor {
let (paragraph, index) = self.table.locate(byte);
let affinity = if byte == 0 {
Affinity::Downstream
} else if byte >= self.buffer.len() || upstream {
Affinity::Upstream
} else {
Affinity::Downstream
};
Anchor {
paragraph,
index,
affinity,
}
}
fn caret_after_insert(start: usize, text: &str) -> (usize, bool) {
(
start + text.len(),
!text.ends_with(['\n', '\r', '\u{2028}', '\u{2029}']),
)
}
fn document_end(&self) -> Anchor {
let last = self.table.paragraphs.len() - 1;
Anchor {
paragraph: last,
index: self.table.paragraphs[last].len,
affinity: Affinity::Upstream,
}
}
fn ensure(&mut self, index: usize) {
if self.table.paragraphs[index].layout.is_some() {
return;
}
let paragraph = &self.table.paragraphs[index];
let text = &self.buffer[paragraph.start..paragraph.start + paragraph.len];
let layout = TextSystem::with(|system| {
TextLayout::build(
text,
self.style,
self.width,
&mut system.fonts,
&mut system.layouts,
)
});
self.builds.set(self.builds.get() + 1);
let paragraph = &mut self.table.paragraphs[index];
paragraph.layout = Some(layout);
paragraph.stamp = self.builds.get();
}
fn refresh_all(&mut self) {
if !self
.table
.paragraphs
.iter()
.any(|paragraph| paragraph.layout.is_none())
{
return;
}
for index in 0..self.table.paragraphs.len() {
self.ensure(index);
}
}
fn with_layout<R>(&mut self, index: usize, f: impl FnOnce(&TextLayout) -> R) -> R {
self.ensure(index);
let layout = self.table.paragraphs[index]
.layout
.as_ref()
.expect("ensure built the layout");
f(layout)
}
fn paragraph_at_y(&mut self, y: f32) -> (usize, f32) {
let mut offset = 0.0;
let mut index = 0;
loop {
self.ensure(index);
let height = self.table.paragraphs[index]
.layout
.as_ref()
.expect("ensure built the layout")
.size()
.height;
if y < offset + height || index + 1 >= self.table.paragraphs.len() {
return (index, offset);
}
offset += height;
index += 1;
}
}
fn offset_of(&mut self, index: usize) -> f32 {
let mut offset = 0.0;
for i in 0..index {
self.ensure(i);
offset += self.table.paragraphs[i]
.layout
.as_ref()
.expect("ensure built the layout")
.size()
.height;
}
offset
}
fn caret_box(&mut self, anchor: Anchor, width: f32) -> (Rect, f32) {
let y = self.offset_of(anchor.paragraph);
self.with_layout(anchor.paragraph, |layout| {
let cursor = Cursor::from_byte_index(&layout.layout, anchor.index, anchor.affinity);
let area = cursor.geometry(&layout.layout, width);
(
Rect::new(
area.x0 as f32,
area.y0 as f32,
area.x1 as f32,
area.y1 as f32,
),
y,
)
})
}
fn range_rects(&mut self, range: Range<usize>, visible: (f32, f32), mut f: impl FnMut(Rect)) {
self.refresh_all();
let newline_width = NEWLINE_WIDTH * self.style.size;
let count = self.table.paragraphs.len();
let meets = |rect: &Rect| rect.y1 >= visible.0 && rect.y0 <= visible.1;
let mut y = 0.0;
for index in 0..count {
let paragraph = &self.table.paragraphs[index];
let start = paragraph.start;
let end = start + paragraph.len;
let layout = paragraph
.layout
.as_ref()
.expect("refresh built every layout");
let height = layout.size().height;
if range.end < start || y > visible.1 {
break;
}
if range.start > end || y + height < visible.0 {
y += height;
continue;
}
if range.start < end && range.end > start {
let local_start = range.start.max(start) - start;
let local_end = range.end.min(end) - start;
if local_start < local_end {
let selection = ParagraphSelection::new(
Cursor::from_byte_index(&layout.layout, local_start, Affinity::Downstream),
Cursor::from_byte_index(&layout.layout, local_end, Affinity::Downstream),
);
selection.geometry_with(&layout.layout, |area, _| {
let area = rect(area);
let area = Rect::new(area.x0, area.y0 + y, area.x1, area.y1 + y);
if area.width() > 0.0 && meets(&area) {
f(area);
}
});
}
}
if index + 1 < count
&& range.start <= end
&& range.end > end
&& let Some(line) = layout.layout.get(layout.layout.len().saturating_sub(1))
{
let metrics = line.metrics();
let top = metrics.block_min_coord.min(metrics.content_block_min_coord);
let bottom = metrics.block_max_coord.max(metrics.content_block_max_coord);
let caret =
Cursor::from_byte_index(&layout.layout, end - start, Affinity::Upstream)
.geometry(&layout.layout, 0.0);
let x = caret.x0 as f32;
let (x0, x1) = if layout.layout.is_rtl() {
(x - newline_width, x)
} else {
(x, x + newline_width)
};
let area = Rect::new(x0, top + y, x1, bottom + y);
if meets(&area) {
f(area);
}
}
y += height;
}
}
fn set_focus(&mut self, focus: Anchor, extend: bool) {
if extend {
self.selection.focus = focus;
} else {
self.selection.anchor = focus;
self.selection.focus = focus;
}
self.selection.base = Base::Cluster;
self.selection.h_pos = None;
}
fn replace(
&mut self,
range: Range<usize>,
text: &str,
caret_byte: usize,
upstream: bool,
) -> bool {
let changed = self.buffer[range.clone()] != *text;
if changed {
if self.keep_before && self.before.is_none() {
self.before = Some(copy_for_undo(self.buffer));
}
if range.is_empty() {
self.buffer.insert_str(range.start, text);
} else {
self.buffer.replace_range(range.clone(), text);
}
self.table.splice(range, text);
}
let caret = self.anchor_at(caret_byte, upstream);
self.selection = Selection {
anchor: caret,
focus: caret,
base: Base::Cluster,
h_pos: None,
};
changed
}
fn move_horizontally(&mut self, forward: bool, extend: bool) {
if !extend && self.selection.anchor != self.selection.focus {
let (first, last) = self.visual_ends();
self.set_focus(if forward { last } else { first }, false);
return;
}
let focus = self.selection.focus;
if !forward && self.at_visual_left_edge(focus) {
if focus.paragraph > 0 {
let previous = focus.paragraph - 1;
let target = Anchor {
paragraph: previous,
index: self.visual_right_index(previous),
affinity: Affinity::Downstream,
};
self.set_focus(target, extend);
}
return;
}
if forward && self.at_visual_right_edge(focus) {
if focus.paragraph + 1 < self.table.paragraphs.len() {
let next = focus.paragraph + 1;
let target = Anchor {
paragraph: next,
index: self.visual_left_index(next),
affinity: Affinity::Downstream,
};
self.set_focus(target, extend);
}
return;
}
let moved = self.with_layout(focus.paragraph, |layout| {
let cursor = Cursor::from_byte_index(&layout.layout, focus.index, focus.affinity);
let moved = if forward {
cursor.next_visual(&layout.layout)
} else {
cursor.previous_visual(&layout.layout)
};
Anchor {
paragraph: focus.paragraph,
index: moved.index(),
affinity: moved.affinity(),
}
});
self.set_focus(moved, extend);
}
fn is_rtl(&mut self, paragraph: usize) -> bool {
self.with_layout(paragraph, |layout| layout.layout.is_rtl())
}
fn at_visual_left_edge(&mut self, anchor: Anchor) -> bool {
let len = self.table.paragraphs[anchor.paragraph].len;
if len == 0 {
return true;
}
if anchor.index >= len {
return self.is_rtl(anchor.paragraph);
}
anchor.index == 0 && !self.is_rtl(anchor.paragraph)
}
fn at_visual_right_edge(&mut self, anchor: Anchor) -> bool {
let len = self.table.paragraphs[anchor.paragraph].len;
if len == 0 {
return true;
}
if anchor.index >= len {
return !self.is_rtl(anchor.paragraph);
}
anchor.index == 0 && self.is_rtl(anchor.paragraph)
}
fn visual_right_index(&mut self, paragraph: usize) -> usize {
if self.is_rtl(paragraph) {
0
} else {
self.table.paragraphs[paragraph].len
}
}
fn visual_left_index(&mut self, paragraph: usize) -> usize {
if self.is_rtl(paragraph) {
self.table.paragraphs[paragraph].len
} else {
0
}
}
fn visual_ends(&mut self) -> (Anchor, Anchor) {
let anchor = self.selection.anchor;
let focus = self.selection.focus;
if anchor.paragraph == focus.paragraph {
let (first, last) = self.with_layout(anchor.paragraph, |layout| {
let anchor_box =
Cursor::from_byte_index(&layout.layout, anchor.index, anchor.affinity)
.geometry(&layout.layout, 0.0);
let focus_box =
Cursor::from_byte_index(&layout.layout, focus.index, focus.affinity)
.geometry(&layout.layout, 0.0);
if (anchor_box.y0, anchor_box.x0) > (focus_box.y0, focus_box.x0) {
(focus, anchor)
} else {
(anchor, focus)
}
});
(first, last)
} else if anchor.paragraph < focus.paragraph {
(anchor, focus)
} else {
(focus, anchor)
}
}
fn move_word(&mut self, forward: bool, extend: bool) {
let focus = self.selection.focus;
let focus = if forward && self.at_visual_right_edge(focus) {
if focus.paragraph + 1 >= self.table.paragraphs.len() {
return;
}
let next = focus.paragraph + 1;
Anchor {
paragraph: next,
index: self.visual_left_index(next),
affinity: Affinity::Downstream,
}
} else if !forward && self.at_visual_left_edge(focus) {
if focus.paragraph == 0 {
return;
}
let previous = focus.paragraph - 1;
Anchor {
paragraph: previous,
index: self.visual_right_index(previous),
affinity: Affinity::Downstream,
}
} else {
focus
};
let steps = self.table.paragraphs[focus.paragraph].len + 1;
let moved = self.with_layout(focus.paragraph, |layout| {
let cursor = Cursor::from_byte_index(&layout.layout, focus.index, focus.affinity);
let moved = visual_word(cursor, &layout.layout, forward, steps);
Anchor {
paragraph: focus.paragraph,
index: moved.index(),
affinity: moved.affinity(),
}
});
self.set_focus(moved, extend);
}
fn word_before(&mut self, focus: Anchor) -> Option<usize> {
let start = self.table.paragraphs[focus.paragraph].start;
if focus.index == 0 {
if focus.paragraph == 0 {
return None;
}
let previous = focus.paragraph - 1;
let len = self.table.paragraphs[previous].len;
let previous_start = self.table.paragraphs[previous].start;
return self.with_layout(previous, |layout| {
let cursor = Cursor::from_byte_index(&layout.layout, len, Affinity::Downstream);
Some(previous_start + cursor.previous_logical_word(&layout.layout).index())
});
}
self.with_layout(focus.paragraph, |layout| {
let cursor = Cursor::from_byte_index(&layout.layout, focus.index, focus.affinity);
Some(start + cursor.previous_logical_word(&layout.layout).index())
})
}
fn word_after(&mut self, focus: Anchor) -> Option<usize> {
let start = self.table.paragraphs[focus.paragraph].start;
if focus.index >= self.table.paragraphs[focus.paragraph].len {
if focus.paragraph + 1 >= self.table.paragraphs.len() {
return None;
}
let next = focus.paragraph + 1;
let next_start = self.table.paragraphs[next].start;
return self.with_layout(next, |layout| {
let cursor = Cursor::from_byte_index(&layout.layout, 0, Affinity::Downstream);
Some(next_start + cursor.next_logical_word(&layout.layout).index())
});
}
self.with_layout(focus.paragraph, |layout| {
let cursor = Cursor::from_byte_index(&layout.layout, focus.index, focus.affinity);
Some(start + cursor.next_logical_word(&layout.layout).index())
})
}
fn move_lines(&mut self, delta: isize, extend: bool) {
if delta == 0 {
return;
}
let focus = self.selection.focus;
let fallback_x = self.with_layout(focus.paragraph, |layout| {
Cursor::from_byte_index(&layout.layout, focus.index, focus.affinity)
.geometry(&layout.layout, 0.0)
.x0 as f32
});
let h_pos = self.selection.h_pos.unwrap_or(fallback_x);
let mut paragraph = focus.paragraph;
let mut line = self.with_layout(focus.paragraph, |layout| {
let cursor = Cursor::from_byte_index(&layout.layout, focus.index, focus.affinity);
let area = cursor.geometry(&layout.layout, 0.0);
let middle = ((area.y0 + area.y1) / 2.0) as f32;
line_at_y(&layout.layout, middle)
});
let mut remaining = delta;
let mut clamped = false;
while remaining != 0 {
if remaining < 0 {
if line > 0 {
line -= 1;
remaining += 1;
} else if paragraph > 0 {
paragraph -= 1;
line =
self.with_layout(paragraph, |layout| layout.layout.len().saturating_sub(1));
remaining += 1;
} else {
clamped = true;
break;
}
} else {
let count = self.with_layout(paragraph, |layout| layout.layout.len());
if line + 1 < count {
line += 1;
remaining -= 1;
} else if paragraph + 1 < self.table.paragraphs.len() {
paragraph += 1;
line = 0;
remaining -= 1;
} else {
clamped = true;
break;
}
}
}
if clamped {
let target = if remaining < 0 {
Anchor {
paragraph: 0,
index: 0,
affinity: Affinity::Downstream,
}
} else {
self.document_end()
};
self.set_focus(target, extend);
return;
}
let moved = self.with_layout(paragraph, |layout| {
let line = layout
.layout
.get(line)
.expect("the walk stopped within the paragraph");
let metrics = line.metrics();
let middle = (metrics.block_min_coord + metrics.block_max_coord) / 2.0;
let moved = Cursor::from_point(&layout.layout, h_pos, middle);
Anchor {
paragraph,
index: moved.index(),
affinity: moved.affinity(),
}
});
if extend {
self.selection.focus = moved;
self.selection.base = Base::Cluster;
} else {
self.selection.anchor = moved;
self.selection.focus = moved;
self.selection.base = Base::Cluster;
}
self.selection.h_pos = Some(h_pos);
}
fn move_to_edge(&mut self, end: bool, extend: bool) {
let target = if end { self.document_end() } else { START };
self.set_focus(target, extend);
}
fn move_to_line_edge(&mut self, end: bool, extend: bool) {
let focus = self.selection.focus;
let moved = self.with_layout(focus.paragraph, |layout| {
let layout = &layout.layout;
let here = ParagraphSelection::from(Cursor::from_byte_index(
layout,
focus.index,
focus.affinity,
));
let moved = if end {
here.line_end(layout, false)
} else {
here.line_start(layout, false)
}
.focus();
Anchor {
paragraph: focus.paragraph,
index: moved.index(),
affinity: moved.affinity(),
}
});
self.set_focus(moved, extend);
}
fn point_anchor(&mut self, x: f32, y: f32) -> Anchor {
let (paragraph, offset) = self.paragraph_at_y(y);
self.with_layout(paragraph, |layout| {
let moved = Cursor::from_point(&layout.layout, x, y - offset);
Anchor {
paragraph,
index: moved.index(),
affinity: moved.affinity(),
}
})
}
fn extend_to_point(&mut self, x: f32, y: f32) {
match self.selection.base {
Base::Cluster => {
let target = self.point_anchor(x, y);
self.selection.focus = target;
self.selection.h_pos = None;
}
Base::Word(base) => {
let (target_anchor, target_focus) = self.word_at_point(x, y);
let leftward = self.table.global(target_anchor) < self.table.global(base[0]);
let mut ends = [target_anchor, target_focus, base[0], base[1]];
ends.sort_by_key(|anchor| self.table.global(*anchor));
let (anchor, focus) = if leftward {
(ends[3], ends[0])
} else {
(ends[0], ends[3])
};
self.selection.anchor = anchor;
self.selection.focus = focus;
self.selection.h_pos = None;
}
}
}
fn select_word_at_point(&mut self, x: f32, y: f32) {
let (anchor, focus) = self.word_at_point(x, y);
self.selection = Selection {
anchor,
focus,
base: Base::Word([anchor, focus]),
h_pos: None,
};
}
fn word_at_point(&mut self, x: f32, y: f32) -> (Anchor, Anchor) {
let (paragraph, offset) = self.paragraph_at_y(y);
let word = self.with_layout(paragraph, |layout| {
let layout = &layout.layout;
let (mut cluster, _) = Cluster::from_point(layout, x, y - offset)?;
if !cluster.is_word_boundary()
&& let Some(previous) = cluster.previous_logical_word()
{
cluster = previous;
}
let anchor = Cursor::from_byte_index(
layout,
cluster.text_range().start,
affinity_for_dir(cluster.is_rtl(), true),
);
let focus = match cluster.next_logical_word() {
Some(end) => Cursor::from_byte_index(
layout,
end.text_range().start,
affinity_for_dir(end.is_rtl(), true),
),
None => Cursor::from_byte_index(layout, usize::MAX, Affinity::Downstream),
};
Some((
Anchor {
paragraph,
index: anchor.index(),
affinity: anchor.affinity(),
},
Anchor {
paragraph,
index: focus.index(),
affinity: focus.affinity(),
},
))
});
word.unwrap_or_else(|| {
let end = Anchor {
paragraph,
index: self.table.paragraphs[paragraph].len,
affinity: Affinity::Upstream,
};
(end, end)
})
}
fn backspace_range(&mut self, focus: Anchor) -> Option<Range<usize>> {
if focus.paragraph == 0 && focus.index == 0 {
return None;
}
let start = self.table.paragraphs[focus.paragraph].start;
if focus.index == 0 {
return Some(start - 1..start);
}
let paragraph_start = start;
let mut range = None;
let mut whole = false;
self.with_layout(focus.paragraph, |layout| {
let cursor = Cursor::from_byte_index(&layout.layout, focus.index, focus.affinity);
if let Some(cluster) = cursor.upstream_cluster(&layout.layout) {
range = Some(cluster.text_range());
whole = cluster.is_hard_line_break() || cluster.is_emoji();
}
});
let range = range?;
let end = paragraph_start + range.end;
let start = if whole {
paragraph_start + range.start
} else {
let text = &self.buffer[paragraph_start..end];
let (start, _) = text.char_indices().next_back()?;
paragraph_start + start
};
Some(start..end)
}
fn forward_delete_range(&mut self, focus: Anchor) -> Option<Range<usize>> {
let paragraph = &self.table.paragraphs[focus.paragraph];
let start = paragraph.start;
let len = paragraph.len;
if focus.index >= len {
if focus.paragraph + 1 >= self.table.paragraphs.len() {
return None;
}
return Some(start + len..start + len + 1);
}
let mut range = None;
self.with_layout(focus.paragraph, |layout| {
let cursor = Cursor::from_byte_index(&layout.layout, focus.index, focus.affinity);
if let Some(cluster) = cursor.downstream_cluster(&layout.layout) {
range = Some(cluster.text_range());
}
});
range.map(|local| start + local.start..start + local.end)
}
}
fn normalize_breaks(text: &str) -> Cow<'_, str> {
if !text.contains('\r') {
return Cow::Borrowed(text);
}
Cow::Owned(text.replace("\r\n", "\n").replace('\r', "\n"))
}
fn copy_for_undo(text: &str) -> String {
#[cfg(test)]
SNAPSHOTS.with(|snapshots| snapshots.set(snapshots.get() + 1));
text.to_owned()
}
fn changed_range(current: &str, target: &str) -> Range<usize> {
let (a, b) = (current.as_bytes(), target.as_bytes());
let mut start = a.iter().zip(b).take_while(|(x, y)| x == y).count();
while !current.is_char_boundary(start) {
start -= 1;
}
let limit = a.len().min(b.len()) - start;
let mut end = a
.iter()
.rev()
.zip(b.iter().rev())
.take(limit)
.take_while(|(x, y)| x == y)
.count();
while !current.is_char_boundary(a.len() - end) {
end -= 1;
}
start..a.len() - end
}
fn char_boundary(text: &str, byte: usize) -> usize {
let mut byte = byte.min(text.len());
while !text.is_char_boundary(byte) {
byte -= 1;
}
byte
}
fn rect(area: parley::BoundingBox) -> Rect {
Rect::new(
area.x0 as f32,
area.y0 as f32,
area.x1 as f32,
area.y1 as f32,
)
}
fn affinity_for_dir(is_rtl: bool, moving_right: bool) -> Affinity {
match (is_rtl, moving_right) {
(true, true) | (false, false) => Affinity::Downstream,
_ => Affinity::Upstream,
}
}
fn line_at_y(layout: &parley::Layout<()>, y: f32) -> usize {
for (index, line) in layout.lines().enumerate() {
if y < line.metrics().block_max_coord {
return index;
}
}
layout.len().saturating_sub(1)
}
fn visual_word<B: parley::Brush>(
cursor: Cursor,
layout: &parley::Layout<B>,
forward: bool,
steps: usize,
) -> Cursor {
let mut current = cursor;
for _ in 0..steps {
let next = if forward {
current.next_visual(layout)
} else {
current.previous_visual(layout)
};
if next == current {
break;
}
current = next;
let [Some(left), Some(right)] = current.visual_clusters(layout) else {
break;
};
let boundary = match (forward, left.is_rtl()) {
(true, true) => left.is_word_boundary() && !left.is_space_or_nbsp(),
(true, false) => right.is_word_boundary() && !left.is_space_or_nbsp(),
(false, true) => {
left.is_word_boundary()
&& (left.is_space_or_nbsp()
|| (right.is_word_boundary() && !right.is_space_or_nbsp()))
}
(false, false) => right.is_word_boundary() && !right.is_space_or_nbsp(),
};
if boundary {
break;
}
}
current
}
#[cfg(test)]
thread_local! {
pub(crate) static SNAPSHOTS: Cell<usize> = const { Cell::new(0) };
}
#[cfg(test)]
#[path = "input_tests.rs"]
mod tests;