use std::collections::HashMap;
use std::sync::Arc;
use egui::output::IMEOutput;
use egui::text::{CCursor, CCursorRange, CharIndex};
use egui::text_selection::text_cursor_state::{
ccursor_next_word, ccursor_previous_word, is_word_char,
};
use egui::text_selection::visuals::{paint_text_cursor, paint_text_selection};
use egui::{
CursorIcon, Event, EventFilter, Galley, IMEPurpose, Id, ImeEvent, Key, Modifiers, Pos2,
Response, Sense, Ui, vec2,
};
use crate::{Edit, Mark, Model, OffsetMap, Position, Selection};
const FILTER: EventFilter = EventFilter {
tab: true,
horizontal_arrows: true,
vertical_arrows: true,
escape: false,
};
pub struct Laid {
pub galley: Arc<Galley>,
pub map: OffsetMap,
pub origin: Pos2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Group {
Typing,
Deleting,
Other,
}
pub struct RichEdit<P> {
id: Id,
selection: Option<Selection<P>>,
next_row: bool,
column: Option<f32>,
group: Option<Group>,
placed: HashMap<P, Placed>,
placing: Placing<P>,
owed: Owed,
page_height: f32,
dragging: bool,
last_interaction: f64,
pending: Option<Pending<P>>,
}
struct Pending<P> {
at: Position<P>,
marks: Vec<(Mark, bool)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Owed {
Nothing,
Reveal,
FocusAndReveal,
}
struct Placed {
galley: Arc<Galley>,
map: OffsetMap,
origin: Pos2,
index: usize,
}
struct Placing<P> {
placed: HashMap<P, Placed>,
inside: bool,
}
impl<P> Default for Placing<P> {
fn default() -> Self {
Self {
placed: HashMap::new(),
inside: false,
}
}
}
impl<P: Clone + Eq + std::hash::Hash + std::fmt::Debug> RichEdit<P> {
pub fn new(id: Id) -> Self {
Self {
id,
selection: None,
next_row: false,
column: None,
group: None,
placed: HashMap::new(),
placing: Placing::default(),
owed: Owed::Nothing,
page_height: 0.0,
dragging: false,
last_interaction: 0.0,
pending: None,
}
}
pub fn selection(&self) -> Option<&Selection<P>> {
self.selection.as_ref()
}
pub fn select(&mut self, selection: Selection<P>) {
self.selection = Some(selection);
self.group = None;
self.column = None;
self.pending = None;
self.owed = Owed::FocusAndReveal;
}
pub fn clear(&mut self) {
self.selection = None;
self.group = None;
self.column = None;
self.dragging = false;
self.pending = None;
}
pub fn document_replaced(&mut self) {
self.group = None;
self.column = None;
self.pending = None;
}
pub fn marked<M: Model<Paragraph = P>>(&self, model: &M, mark: Mark) -> Option<bool> {
let selection = self.selection.as_ref()?;
if let Some(on) = self.pending_mark(mark) {
return Some(on);
}
let (from, to) = self.ordered(selection);
model.marked(&from, &to, mark)
}
pub fn toggle<M: Model<Paragraph = P>>(&mut self, model: &mut M, mark: Mark) -> bool {
let Some(selection) = self.selection.clone() else {
return false;
};
self.owed = Owed::FocusAndReveal;
let on = self.marked(model, mark) != Some(true);
if selection.is_caret() {
let at = selection.focus;
let mut marks = match self.pending.take() {
Some(pending) if pending.at == at => pending.marks,
_ => Vec::new(),
};
marks.retain(|(m, _)| *m != mark);
if model.marked(&at, &at, mark) != Some(on) {
marks.push((mark, on));
}
self.pending = (!marks.is_empty()).then_some(Pending { at, marks });
return false;
}
let (from, to) = self.ordered(&selection);
self.group = None;
let changed = self.format(model, from, to, mark, on, Group::Other);
self.group = None;
changed
}
fn pending_mark(&self, mark: Mark) -> Option<bool> {
let selection = self.selection.as_ref().filter(|s| s.is_caret())?;
let pending = self.pending.as_ref().filter(|p| p.at == selection.focus)?;
pending
.marks
.iter()
.find(|(m, _)| *m == mark)
.map(|(_, on)| *on)
}
pub fn input<M: Model<Paragraph = P>>(&mut self, ui: &mut Ui, model: &mut M) -> bool {
self.placed = std::mem::take(&mut self.placing).placed;
ui.interact(ui.clip_rect(), self.id, Sense::focusable_noninteractive());
self.page_height = ui.clip_rect().height();
if self.owed == Owed::FocusAndReveal {
self.owed = Owed::Reveal;
ui.memory_mut(|memory| memory.request_focus(self.id));
}
if !ui.memory(|memory| memory.has_focus(self.id)) {
self.dragging = false;
return false;
}
ui.memory_mut(|memory| memory.set_focus_lock_filter(self.id, FILTER));
if self.dragging {
scroll_toward_pointer(ui);
}
self.settle(model);
let events = ui.input(|input| input.filtered_events(&FILTER));
let mut changed = false;
for event in &events {
let (moved, edited) = self.event(ui, model, event);
if moved || edited {
self.last_interaction = ui.input(|input| input.time);
self.owed = Owed::Reveal;
}
changed |= edited;
}
changed
}
pub fn paragraph(&mut self, ui: &Ui, response: &Response, paragraph: &P, laid: Laid) {
let Laid {
mut galley,
map,
origin,
} = laid;
let focused = ui.memory(|memory| memory.has_focus(self.id));
if response.hovered() {
ui.ctx().set_cursor_icon(CursorIcon::Text);
}
self.pointer(ui, response, paragraph, &galley, &map, origin);
if let Some(range) = self.selected_here(paragraph, &map)
&& focused
{
paint_text_selection(&mut galley, ui.visuals(), &range, None);
}
ui.painter()
.galley(origin, galley.clone(), ui.visuals().text_color());
if focused
&& let Some(selection) = &self.selection
&& selection.focus.paragraph == *paragraph
{
let cursor = CCursor {
index: CharIndex(map.to_galley(selection.focus.offset)),
prefer_next_row: self.next_row,
};
let caret = galley.pos_from_cursor(cursor).translate(origin.to_vec2());
let since = ui.input(|input| input.time) - self.last_interaction;
paint_text_cursor(ui, ui.painter(), caret, since);
if self.owed == Owed::Reveal {
self.owed = Owed::Nothing;
ui.scroll_to_rect(caret.expand(4.0), None);
}
let to_global = ui
.ctx()
.layer_transform_to_global(ui.layer_id())
.unwrap_or_default();
ui.output_mut(|output| {
output.ime = Some(IMEOutput {
purpose: IMEPurpose::Normal,
rect: to_global * response.rect,
cursor_rect: to_global * caret,
should_interrupt_composition: false,
});
});
}
let index = self.placing.placed.len();
self.placing.placed.insert(
paragraph.clone(),
Placed {
galley,
map,
origin,
index,
},
);
}
fn settle<M: Model<Paragraph = P>>(&mut self, model: &M) {
let Some(selection) = &mut self.selection else {
return;
};
let kept = [&mut selection.anchor, &mut selection.focus]
.into_iter()
.all(|position| {
len(model, &position.paragraph)
.map(|len| position.offset = position.offset.min(len))
.is_some()
});
if !kept {
self.selection = None;
}
}
fn event<M: Model<Paragraph = P>>(
&mut self,
ui: &Ui,
model: &mut M,
event: &Event,
) -> (bool, bool) {
match event {
Event::Text(text) | Event::Ime(ImeEvent::Commit(text))
if !text.is_empty() && text != "\n" && text != "\r" =>
{
(false, self.type_text(model, text))
}
Event::Paste(text) => (false, self.paste(model, text)),
Event::Copy => {
if let Some(text) = self.selected_text(model) {
ui.ctx().copy_text(text);
}
(false, false)
}
Event::Cut => {
let Some(text) = self.selected_text(model) else {
return (false, false);
};
ui.ctx().copy_text(text);
self.group = None;
let cut = self.delete_selection(model, Group::Other);
self.group = None;
(false, cut)
}
Event::Key {
key,
pressed: true,
modifiers,
..
} => match shortcut(*key).filter(|_| modifiers.command) {
Some(mark) => (false, self.toggle(model, mark)),
None => self.key(model, *key, *modifiers),
},
_ => (false, false),
}
}
fn key<M: Model<Paragraph = P>>(
&mut self,
model: &mut M,
key: Key,
modifiers: Modifiers,
) -> (bool, bool) {
let Some(selection) = self.selection.clone() else {
return (false, false);
};
let word = modifiers.alt || modifiers.ctrl;
let extend = modifiers.shift;
match key {
Key::ArrowLeft | Key::ArrowRight => {
let right = key == Key::ArrowRight;
let to = if !selection.is_caret() && !extend {
let (from, to) = self.ordered(&selection);
if right { to } else { from }
} else if modifiers.mac_cmd {
self.row_edge(model, &selection.focus, right)
} else if right {
step_right(model, &selection.focus, word)
} else {
step_left(model, &selection.focus, word)
};
self.next_row = false;
self.move_to(to, extend);
(true, false)
}
Key::A if modifiers.command => {
let (Some(first), Some(last)) = (model.first(), model.last()) else {
return (false, false);
};
self.move_to(Position::new(first, 0), false);
self.move_to(edge(model, last, true), true);
(true, false)
}
Key::Home | Key::End if modifiers.command => {
let end = key == Key::End;
let paragraph = if end { model.last() } else { model.first() };
let Some(paragraph) = paragraph else {
return (false, false);
};
self.next_row = false;
self.move_to(edge(model, paragraph, end), extend);
(true, false)
}
Key::Home | Key::End => {
let to = self.row_edge(model, &selection.focus, key == Key::End);
self.move_to(to, extend);
(true, false)
}
Key::PageUp | Key::PageDown => {
let Some(to) = self.page(&selection.focus, key == Key::PageDown) else {
return (false, false);
};
let column = self.column;
self.move_to(to, extend);
self.column = column;
(true, false)
}
Key::ArrowUp | Key::ArrowDown => {
let to = self.vertical(model, &selection.focus, key == Key::ArrowDown);
let column = self.column;
self.move_to(to, extend);
self.column = column;
(true, false)
}
Key::Backspace | Key::Delete => {
if !selection.is_caret() {
return (false, self.delete_selection(model, Group::Deleting));
}
let at = selection.focus;
let (from, to) = if key == Key::Backspace {
(step_left(model, &at, word), at)
} else {
let to = step_right(model, &at, word);
(at, to)
};
if from == to {
return (false, false);
}
let edit = Edit::Replace { from, to, text: "" };
(false, self.apply(model, edit, Group::Deleting))
}
Key::Enter if modifiers.shift => (false, self.type_text(model, "\n")),
Key::Enter => {
self.group = None;
let changed = self.delete_selection(model, Group::Other);
if !changed && !selection.is_caret() {
return (false, false);
}
let mut changed = changed;
let at = self.selection.clone().map(|s| s.focus);
if let Some(at) = at {
changed |= self.apply(model, Edit::Split { at }, Group::Other);
}
self.group = None;
(false, changed)
}
Key::Tab if modifiers.is_none() => (false, self.type_text(model, "\t")),
_ => (false, false),
}
}
fn move_to(&mut self, to: Position<P>, extend: bool) {
self.group = None;
self.column = None;
match (&mut self.selection, extend) {
(Some(selection), true) => selection.focus = to,
_ => self.selection = Some(Selection::caret(to)),
}
}
fn type_text<M: Model<Paragraph = P>>(&mut self, model: &mut M, text: &str) -> bool {
let Some(selection) = &self.selection else {
return false;
};
let (from, to) = self.ordered(selection);
let marks: Vec<(Mark, bool)> = Mark::ALL
.into_iter()
.filter_map(|mark| self.pending_mark(mark).map(|on| (mark, on)))
.collect();
let edit = Edit::Replace {
from: from.clone(),
to,
text,
};
if !self.apply(model, edit, Group::Typing) {
return false;
}
if let Some(end) = self.selection.as_ref().map(|s| s.focus.clone()) {
for (mark, on) in marks {
self.format(model, from.clone(), end.clone(), mark, on, Group::Typing);
}
}
true
}
fn paste<M: Model<Paragraph = P>>(&mut self, model: &mut M, text: &str) -> bool {
let text = text.replace("\r\n", "\n").replace('\r', "\n");
self.group = None;
let mut changed = false;
for (index, line) in text.split('\n').enumerate() {
if index > 0 {
let Some(at) = self.selection.clone().map(|s| s.focus) else {
break;
};
if !self.apply(model, Edit::Split { at }, Group::Other) {
break;
}
changed = true;
}
if index == 0 || !line.is_empty() {
let Some(selection) = &self.selection else {
break;
};
let (from, to) = self.ordered(selection);
let edit = Edit::Replace {
from,
to,
text: line,
};
if !self.apply(model, edit, Group::Other) {
break;
}
changed = true;
}
}
self.group = None;
changed
}
fn delete_selection<M: Model<Paragraph = P>>(&mut self, model: &mut M, group: Group) -> bool {
let Some(selection) = self.selection.as_ref().filter(|s| !s.is_caret()) else {
return false;
};
let (from, to) = self.ordered(selection);
self.apply(model, Edit::Replace { from, to, text: "" }, group)
}
fn apply<M: Model<Paragraph = P>>(
&mut self,
model: &mut M,
edit: Edit<'_, P>,
group: Group,
) -> bool {
let new_step = self.group != Some(group);
let Some(at) = model.apply(edit, new_step) else {
return false;
};
self.group = Some(group);
self.selection = Some(Selection::caret(at));
self.next_row = false;
self.column = None;
self.pending = None;
true
}
fn format<M: Model<Paragraph = P>>(
&mut self,
model: &mut M,
from: Position<P>,
to: Position<P>,
mark: Mark,
on: bool,
group: Group,
) -> bool {
let new_step = self.group != Some(group);
let edit = Edit::Format { from, to, mark, on };
if model.apply(edit, new_step).is_none() {
return false;
}
self.group = Some(group);
true
}
fn ordered(&self, selection: &Selection<P>) -> (Position<P>, Position<P>) {
let Selection { anchor, focus } = selection;
let anchor_first = if anchor.paragraph == focus.paragraph {
anchor.offset <= focus.offset
} else {
match (
self.placed.get(&anchor.paragraph),
self.placed.get(&focus.paragraph),
) {
(Some(a), Some(f)) => a.index < f.index,
_ => true,
}
};
if anchor_first {
(anchor.clone(), focus.clone())
} else {
(focus.clone(), anchor.clone())
}
}
fn selected_text<M: Model<Paragraph = P>>(&self, model: &M) -> Option<String> {
let selection = self.selection.as_ref().filter(|s| !s.is_caret())?;
let (from, to) = self.ordered(selection);
let mut out = String::new();
let mut paragraph = from.paragraph;
let mut start = from.offset;
loop {
let text = model.text(¶graph)?;
if paragraph == to.paragraph {
out.extend(
text.chars()
.skip(start)
.take(to.offset.saturating_sub(start)),
);
return Some(out);
}
out.extend(text.chars().skip(start));
out.push('\n');
paragraph = model.next(¶graph)?;
start = 0;
}
}
fn row_edge<M: Model<Paragraph = P>>(
&mut self,
model: &M,
at: &Position<P>,
end: bool,
) -> Position<P> {
let Some(placed) = self.placed.get(&at.paragraph) else {
return edge(model, at.paragraph.clone(), end);
};
let cursor = CCursor {
index: CharIndex(placed.map.to_galley(at.offset)),
prefer_next_row: self.next_row,
};
let edge = if end {
placed.galley.cursor_end_of_row(&cursor)
} else {
placed.galley.cursor_begin_of_row(&cursor)
};
self.next_row = !end;
Position::new(at.paragraph.clone(), placed.map.to_model(edge.index.0))
}
fn page(&mut self, at: &Position<P>, down: bool) -> Option<Position<P>> {
let placed = self.placed.get(&at.paragraph)?;
let cursor = CCursor {
index: CharIndex(placed.map.to_galley(at.offset)),
prefer_next_row: self.next_row,
};
let caret = placed
.galley
.pos_from_cursor(cursor)
.translate(placed.origin.to_vec2());
let x = self.column.unwrap_or_else(|| caret.center().x);
let y = caret.center().y
+ if down {
self.page_height
} else {
-self.page_height
};
let distance = |placed: &Placed| {
let top = placed.origin.y + placed.galley.rect.top();
let bottom = placed.origin.y + placed.galley.rect.bottom();
(top - y).max(y - bottom).max(0.0)
};
let (paragraph, there) = self
.placed
.iter()
.min_by(|(_, a), (_, b)| distance(a).total_cmp(&distance(b)))?;
let landed = there
.galley
.cursor_from_pos(vec2(x - there.origin.x, y - there.origin.y));
let to = Position::new(paragraph.clone(), there.map.to_model(landed.index.0));
self.column = Some(x);
self.next_row = landed.prefer_next_row;
Some(to)
}
fn vertical<M: Model<Paragraph = P>>(
&mut self,
model: &M,
at: &Position<P>,
down: bool,
) -> Position<P> {
let neighbour = if down {
model.next(&at.paragraph)
} else {
model.previous(&at.paragraph)
};
let edge_of_this = || edge(model, at.paragraph.clone(), down);
let Some(placed) = self.placed.get(&at.paragraph) else {
return neighbour.map_or_else(edge_of_this, |p| Position::new(p, 0));
};
let galley = &placed.galley;
let cursor = CCursor {
index: CharIndex(placed.map.to_galley(at.offset)),
prefer_next_row: self.next_row,
};
let x = self
.column
.unwrap_or_else(|| placed.origin.x + galley.pos_from_cursor(cursor).center().x);
self.column = Some(x);
let row = galley.layout_from_cursor(cursor).row;
let last_row = galley.rows.len().saturating_sub(1);
if (down && row < last_row) || (!down && row > 0) {
let h = Some(x - placed.origin.x);
let (moved, _) = if down {
galley.cursor_down_one_row(&cursor, h)
} else {
galley.cursor_up_one_row(&cursor, h)
};
self.next_row = moved.prefer_next_row;
return Position::new(at.paragraph.clone(), placed.map.to_model(moved.index.0));
}
let Some(neighbour) = neighbour else {
return edge_of_this();
};
let Some(there) = self.placed.get(&neighbour) else {
return edge(model, neighbour, !down);
};
let target_row = if down {
there.galley.rows.first()
} else {
there.galley.rows.last()
};
let y = target_row.map_or(0.0, |row| row.rect().center().y);
let landed = there.galley.cursor_from_pos(vec2(x - there.origin.x, y));
self.next_row = landed.prefer_next_row;
Position::new(neighbour.clone(), there.map.to_model(landed.index.0))
}
fn pointer(
&mut self,
ui: &Ui,
response: &Response,
paragraph: &P,
galley: &Galley,
map: &OffsetMap,
origin: Pos2,
) {
let at_pointer = |pos: Pos2| galley.cursor_from_pos(pos - origin);
let (pressed, down, shift, pos) = ui.input(|input| {
(
input.pointer.primary_pressed(),
input.pointer.primary_down(),
input.modifiers.shift,
input.pointer.interact_pos(),
)
});
let placed = if response.sense.senses_drag() {
pressed && (response.is_pointer_button_down_on() || response.clicked())
} else {
response.clicked()
};
if placed && let Some(pos) = pos {
let cursor = at_pointer(pos);
let to = Position::new(paragraph.clone(), map.to_model(cursor.index.0));
match &mut self.selection {
Some(selection) if shift => selection.focus = to,
_ => self.selection = Some(Selection::caret(to)),
}
self.next_row = cursor.prefer_next_row;
self.group = None;
self.column = None;
self.dragging = response.sense.senses_drag();
self.last_interaction = ui.input(|input| input.time);
ui.memory_mut(|memory| memory.request_focus(self.id));
} else if self.dragging && !down {
self.dragging = false;
} else if self.dragging
&& let Some(pos) = pos
&& response.rect.contains(pos)
&& let Some(selection) = &mut self.selection
{
let cursor = at_pointer(pos);
selection.focus = Position::new(paragraph.clone(), map.to_model(cursor.index.0));
self.next_row = cursor.prefer_next_row;
self.last_interaction = ui.input(|input| input.time);
}
if response.clicked() {
ui.memory_mut(|memory| memory.request_focus(self.id));
}
if (response.double_clicked() || response.triple_clicked())
&& let Some(pos) = pos
{
let (start, end) = if response.triple_clicked() {
(0, map.model_len())
} else {
let (start, end) = word_around(&galley.job.text, at_pointer(pos).index.0);
(map.to_model(start), map.to_model(end))
};
self.selection = Some(Selection {
anchor: Position::new(paragraph.clone(), start),
focus: Position::new(paragraph.clone(), end),
});
self.next_row = false;
}
}
fn selected_here(&mut self, paragraph: &P, map: &OffsetMap) -> Option<CCursorRange> {
let selection = self.selection.as_ref().filter(|s| !s.is_caret())?;
let (anchor, focus) = (&selection.anchor, &selection.focus);
let (start, end) = match (
anchor.paragraph == *paragraph,
focus.paragraph == *paragraph,
) {
(true, true) => (
anchor.offset.min(focus.offset),
anchor.offset.max(focus.offset),
),
(true, false) | (false, true) => {
let at = if anchor.paragraph == *paragraph {
anchor.offset
} else {
focus.offset
};
self.placing.inside = !self.placing.inside;
if self.placing.inside {
(at, map.model_len())
} else {
(0, at)
}
}
(false, false) if self.placing.inside => (0, map.model_len()),
(false, false) => return None,
};
Some(CCursorRange::two(
CCursor::new(map.to_galley(start)),
CCursor::new(map.to_galley(end)),
))
}
}
fn shortcut(key: Key) -> Option<Mark> {
match key {
Key::B => Some(Mark::Bold),
Key::I => Some(Mark::Italic),
Key::U => Some(Mark::Underline),
_ => None,
}
}
fn len<M: Model>(model: &M, paragraph: &M::Paragraph) -> Option<usize> {
model.text(paragraph).map(|text| text.chars().count())
}
fn edge<M: Model>(model: &M, paragraph: M::Paragraph, end: bool) -> Position<M::Paragraph> {
let offset = if end {
len(model, ¶graph).unwrap_or(0)
} else {
0
};
Position::new(paragraph, offset)
}
fn step_left<M: Model>(
model: &M,
at: &Position<M::Paragraph>,
word: bool,
) -> Position<M::Paragraph> {
if at.offset == 0 {
return model
.previous(&at.paragraph)
.map_or_else(|| at.clone(), |p| edge(model, p, true));
}
let offset = if word {
let text = model.text(&at.paragraph).unwrap_or_default();
ccursor_previous_word(&text, CCursor::new(at.offset))
.index
.0
} else {
at.offset - 1
};
Position::new(at.paragraph.clone(), offset)
}
fn step_right<M: Model>(
model: &M,
at: &Position<M::Paragraph>,
word: bool,
) -> Position<M::Paragraph> {
let text = model.text(&at.paragraph).unwrap_or_default();
let len = text.chars().count();
if at.offset >= len {
return model
.next(&at.paragraph)
.map_or_else(|| at.clone(), |p| Position::new(p, 0));
}
let offset = if word {
ccursor_next_word(&text, CCursor::new(at.offset)).index.0
} else {
at.offset + 1
};
Position::new(at.paragraph.clone(), offset.min(len))
}
fn word_around(text: &str, index: usize) -> (usize, usize) {
let chars: Vec<char> = text.chars().collect();
let index = index.min(chars.len());
let mut start = index;
while start > 0 && is_word_char(chars[start - 1]) {
start -= 1;
}
let mut end = index;
while end < chars.len() && is_word_char(chars[end]) {
end += 1;
}
(start, end)
}
fn scroll_toward_pointer(ui: &Ui) {
const SPEED: f32 = 0.2;
const SLOWEST: f32 = 2.0;
const FASTEST: f32 = 30.0;
let Some(pointer) = ui.input(|input| input.pointer.latest_pos()) else {
return;
};
let view = ui.clip_rect();
let past = (pointer.y - view.bottom()).max(0.0) - (view.top() - pointer.y).max(0.0);
if past == 0.0 {
return;
}
let step = (past.abs() * SPEED).clamp(SLOWEST, FASTEST);
ui.scroll_with_delta(vec2(0.0, -step.copysign(past)));
ui.ctx().request_repaint();
}