613 lines
20 KiB
Rust
613 lines
20 KiB
Rust
use std::ops::{Deref, DerefMut};
|
|
|
|
use crate::prelude::*;
|
|
use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, LayoutRun, Motion};
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
use winit::{
|
|
event::KeyEvent,
|
|
keyboard::{Key, NamedKey},
|
|
};
|
|
|
|
pub struct TextEdit {
|
|
view: TextView,
|
|
selection: TextSelection,
|
|
history: Vec<(String, TextSelection)>,
|
|
double_hit: Option<Cursor>,
|
|
pub single_line: bool,
|
|
}
|
|
|
|
impl TextEdit {
|
|
pub fn new(view: TextView, single_line: bool) -> Self {
|
|
Self {
|
|
view,
|
|
selection: Default::default(),
|
|
history: Default::default(),
|
|
double_hit: None,
|
|
single_line,
|
|
}
|
|
}
|
|
pub fn select_content(&self, start: Cursor, end: Cursor) -> String {
|
|
let (start, end) = sort_cursors(start, end);
|
|
let mut iter = self.buf.lines.iter().skip(start.line);
|
|
let first = iter.next().unwrap();
|
|
if start.line == end.line {
|
|
first.text()[start.index..end.index].to_string()
|
|
} else {
|
|
let mut str = first.text()[start.index..].to_string();
|
|
for _ in (start.line + 1)..end.line {
|
|
str = str + "\n" + iter.next().unwrap().text();
|
|
}
|
|
let last = iter.next().unwrap();
|
|
str = str + "\n" + &last.text()[..end.index];
|
|
str
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Widget for TextEdit {
|
|
fn draw(&mut self, painter: &mut Painter) {
|
|
let base = painter.layer;
|
|
painter.child_layer();
|
|
self.view.draw(painter);
|
|
painter.layer = base;
|
|
let region = self.region();
|
|
|
|
let size = vec2(1, self.attrs.line_height);
|
|
match self.selection {
|
|
TextSelection::None => (),
|
|
TextSelection::Pos(cursor) => {
|
|
if let Some(offset) = cursor_pos(cursor, &self.buf) {
|
|
painter.primitive_within(
|
|
RectPrimitive::color(Color::WHITE),
|
|
size.align(Align::TOP_LEFT).offset(offset).within(®ion),
|
|
);
|
|
}
|
|
}
|
|
TextSelection::Span { start, end } => {
|
|
let (start, end) = sort_cursors(start, end);
|
|
for (l, x, width) in iter_layout_lines(start, end, &self.buf) {
|
|
let top_left = vec2(x, self.attrs.line_height * l as f32);
|
|
painter.primitive_within(
|
|
RectPrimitive::color(Color::SKY),
|
|
size.with_x(width)
|
|
.align(Align::TOP_LEFT)
|
|
.offset(top_left)
|
|
.within(®ion),
|
|
);
|
|
}
|
|
if let Some(end_offset) = cursor_pos(end, &self.buf) {
|
|
painter.primitive_within(
|
|
RectPrimitive::color(Color::WHITE),
|
|
size.align(Align::TOP_LEFT)
|
|
.offset(end_offset)
|
|
.within(®ion),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
self.view.desired_width(ctx)
|
|
}
|
|
|
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
self.view.desired_height(ctx)
|
|
}
|
|
}
|
|
|
|
/// provides top left + width
|
|
fn iter_layout_lines(
|
|
start: Cursor,
|
|
end: Cursor,
|
|
buf: &TextBuffer,
|
|
) -> impl Iterator<Item = (usize, f32, f32)> {
|
|
gen move {
|
|
let mut iter = buf.layout_runs().enumerate();
|
|
for (i, line) in iter.by_ref() {
|
|
if line.line_i == start.line
|
|
&& let Some(start_x) = index_x(&line, start.index)
|
|
{
|
|
if start.line == end.line
|
|
&& let Some(end_x) = index_x(&line, end.index)
|
|
{
|
|
yield (i, start_x, end_x - start_x);
|
|
return;
|
|
}
|
|
yield (i, start_x, line.line_w - start_x);
|
|
break;
|
|
}
|
|
}
|
|
for (i, line) in iter {
|
|
if line.line_i > end.line {
|
|
return;
|
|
}
|
|
if line.line_i == end.line
|
|
&& let Some(end_x) = index_x(&line, end.index)
|
|
{
|
|
yield (i, 0.0, end_x);
|
|
return;
|
|
}
|
|
yield (i, 0.0, line.line_w);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// copied & modified from fn found in Editor in cosmic_text
|
|
/// returns x pos of a (non layout) index within an layout run
|
|
fn index_x(run: &LayoutRun, index: usize) -> Option<f32> {
|
|
for glyph in run.glyphs.iter() {
|
|
if index == glyph.start {
|
|
return Some(glyph.x);
|
|
} else if index > glyph.start && index < glyph.end {
|
|
// Guess x offset based on characters
|
|
let mut before = 0;
|
|
let mut total = 0;
|
|
|
|
let cluster = &run.text[glyph.start..glyph.end];
|
|
for (i, _) in cluster.grapheme_indices(true) {
|
|
if glyph.start + i < index {
|
|
before += 1;
|
|
}
|
|
total += 1;
|
|
}
|
|
|
|
let offset = glyph.w * (before as f32) / (total as f32);
|
|
return Some(glyph.x + offset);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// returns top of line segment where cursor should visually select
|
|
fn cursor_pos(cursor: Cursor, buf: &TextBuffer) -> Option<Vec2> {
|
|
let mut prev = None;
|
|
for run in buf
|
|
.layout_runs()
|
|
.skip_while(|r| r.line_i < cursor.line)
|
|
.take_while(|r| r.line_i == cursor.line)
|
|
{
|
|
prev = Some(vec2(run.line_w, run.line_top));
|
|
if let Some(pos) = index_x(&run, cursor.index) {
|
|
return Some(vec2(pos, run.line_top));
|
|
}
|
|
}
|
|
prev
|
|
}
|
|
|
|
pub struct TextEditCtx<'a> {
|
|
pub text: &'a mut TextEdit,
|
|
pub font_system: &'a mut FontSystem,
|
|
}
|
|
|
|
impl<'a> TextEditCtx<'a> {
|
|
pub fn take(&mut self) -> String {
|
|
let text = self
|
|
.text
|
|
.buf
|
|
.lines
|
|
.drain(..)
|
|
.map(|l| l.into_text())
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
self.text
|
|
.buf
|
|
.set_text(self.font_system, "", &Attrs::new(), SHAPING, None);
|
|
self.text.selection.clear();
|
|
text
|
|
}
|
|
|
|
pub fn set(&mut self, text: &str) {
|
|
let text = self.string(text);
|
|
self.text
|
|
.buf
|
|
.set_text(self.font_system, &text, &Attrs::new(), SHAPING, None);
|
|
self.text.selection.clear();
|
|
}
|
|
|
|
pub fn motion(&mut self, motion: Motion, select: bool) {
|
|
if let TextSelection::Pos(cursor) = self.text.selection
|
|
&& let Some(new) = self.buf_motion(cursor, motion)
|
|
{
|
|
if select {
|
|
self.text.selection = TextSelection::Span {
|
|
start: cursor,
|
|
end: new,
|
|
};
|
|
} else {
|
|
self.text.selection = TextSelection::Pos(new);
|
|
}
|
|
} else if let TextSelection::Span { start, end } = self.text.selection {
|
|
if select {
|
|
if let Some(cursor) = self.buf_motion(end, motion) {
|
|
self.text.selection = TextSelection::Span { start, end: cursor };
|
|
}
|
|
} else {
|
|
let (start, end) = sort_cursors(start, end);
|
|
let sel = &mut self.text.selection;
|
|
match motion {
|
|
Motion::Left | Motion::LeftWord => *sel = TextSelection::Pos(start),
|
|
Motion::Right | Motion::RightWord => *sel = TextSelection::Pos(end),
|
|
_ => {
|
|
if let Some(cursor) = self.buf_motion(end, motion) {
|
|
self.text.selection = TextSelection::Pos(cursor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn replace(&mut self, len: usize, text: &str) {
|
|
let text = self.string(text);
|
|
for _ in 0..len {
|
|
self.delete(false);
|
|
}
|
|
self.insert_inner(&text, false);
|
|
}
|
|
|
|
fn string(&self, text: &str) -> String {
|
|
if self.text.single_line {
|
|
text.replace('\n', "")
|
|
} else {
|
|
text.to_string()
|
|
}
|
|
}
|
|
|
|
pub fn insert(&mut self, text: &str) {
|
|
let text = self.string(text);
|
|
let mut lines = text.split('\n');
|
|
let Some(first) = lines.next() else {
|
|
return;
|
|
};
|
|
self.insert_inner(first, true);
|
|
for line in lines {
|
|
self.newline();
|
|
self.insert_inner(line, true);
|
|
}
|
|
}
|
|
|
|
pub fn clear_span(&mut self) -> bool {
|
|
if let TextSelection::Span { start, end } = self.text.selection {
|
|
self.delete_between(start, end);
|
|
let (start, _) = sort_cursors(start, end);
|
|
self.text.selection = TextSelection::Pos(start);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn delete_between(&mut self, start: Cursor, end: Cursor) {
|
|
let lines = &mut self.text.view.buf.lines;
|
|
let (start, end) = sort_cursors(start, end);
|
|
if start.line == end.line {
|
|
let line = &mut lines[start.line];
|
|
let text = line.text();
|
|
let text = text[..start.index].to_string() + &text[end.index..];
|
|
edit_line(line, text);
|
|
} else {
|
|
// start
|
|
let start_text = lines[start.line].text()[..start.index].to_string();
|
|
let end_text = &lines[end.line].text()[end.index..];
|
|
let text = start_text + end_text;
|
|
edit_line(&mut lines[start.line], text);
|
|
}
|
|
// between
|
|
let range = (start.line + 1)..=end.line;
|
|
if !range.is_empty() {
|
|
lines.splice(range, None);
|
|
}
|
|
}
|
|
|
|
fn insert_inner(&mut self, text: &str, mov: bool) {
|
|
self.clear_span();
|
|
if let TextSelection::Pos(cursor) = &mut self.text.selection {
|
|
let line = &mut self.text.view.buf.lines[cursor.line];
|
|
let mut line_text = line.text().to_string();
|
|
line_text.insert_str(cursor.index, text);
|
|
edit_line(line, line_text);
|
|
if mov {
|
|
for _ in 0..text.chars().count() {
|
|
self.motion(Motion::Right, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn newline(&mut self) {
|
|
if self.text.single_line {
|
|
return;
|
|
}
|
|
self.clear_span();
|
|
if let TextSelection::Pos(cursor) = &mut self.text.selection {
|
|
let lines = &mut self.text.view.buf.lines;
|
|
let line = &mut lines[cursor.line];
|
|
let new = line.split_off(cursor.index);
|
|
cursor.line += 1;
|
|
lines.insert(cursor.line, new);
|
|
cursor.index = 0;
|
|
}
|
|
}
|
|
|
|
pub fn backspace(&mut self, word: bool) {
|
|
if !self.clear_span()
|
|
&& let TextSelection::Pos(cursor) = &mut self.text.selection
|
|
&& (cursor.index != 0 || cursor.line != 0)
|
|
{
|
|
self.motion(if word { Motion::LeftWord } else { Motion::Left }, false);
|
|
self.delete(word);
|
|
}
|
|
}
|
|
|
|
pub fn delete(&mut self, word: bool) {
|
|
if !self.clear_span()
|
|
&& let TextSelection::Pos(cursor) = &mut self.text.selection
|
|
{
|
|
if word {
|
|
let start = *cursor;
|
|
if let Some(end) = self.buf_motion(start, Motion::RightWord) {
|
|
self.delete_between(start, end);
|
|
}
|
|
} else {
|
|
let lines = &mut self.text.view.buf.lines;
|
|
let line = &mut lines[cursor.line];
|
|
if cursor.index == line.text().len() {
|
|
if cursor.line == lines.len() - 1 {
|
|
return;
|
|
}
|
|
let add = lines.remove(cursor.line + 1).into_text();
|
|
let line = &mut lines[cursor.line];
|
|
let mut cur = line.text().to_string();
|
|
cur.push_str(&add);
|
|
edit_line(line, cur);
|
|
} else {
|
|
let mut text = line.text().to_string();
|
|
text.remove(cursor.index);
|
|
edit_line(line, text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn buf_motion(&mut self, cursor: Cursor, motion: Motion) -> Option<Cursor> {
|
|
self.text
|
|
.buf
|
|
.cursor_motion(self.font_system, cursor, None, motion)
|
|
.map(|r| r.0)
|
|
}
|
|
|
|
pub fn select_word_at(&mut self, cursor: Cursor) {
|
|
if let (Some(start), Some(end)) = (
|
|
self.buf_motion(cursor, Motion::LeftWord),
|
|
self.buf_motion(cursor, Motion::RightWord),
|
|
) {
|
|
self.text.selection = TextSelection::Span { start, end };
|
|
}
|
|
}
|
|
|
|
pub fn select_line_at(&mut self, cursor: Cursor) {
|
|
let end = self.text.buf.lines[cursor.line].text().len();
|
|
self.text.selection = TextSelection::Span {
|
|
start: Cursor::new(cursor.line, 0),
|
|
end: Cursor::new(cursor.line, end),
|
|
}
|
|
}
|
|
|
|
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
|
let pos = pos - self.text.region().top_left().to_abs(size);
|
|
let hit = self.text.buf.hit(pos.x, pos.y);
|
|
let sel = &mut self.text.selection;
|
|
match sel {
|
|
TextSelection::None => {
|
|
if !drag && let Some(hit) = hit {
|
|
*sel = TextSelection::Pos(hit)
|
|
}
|
|
}
|
|
TextSelection::Pos(pos) => match (hit, drag) {
|
|
(None, false) => *sel = TextSelection::None,
|
|
(None, true) => (),
|
|
(Some(hit), false) => {
|
|
if recent && hit == *pos {
|
|
self.text.double_hit = Some(hit);
|
|
return self.select_word_at(hit);
|
|
} else {
|
|
*pos = hit
|
|
}
|
|
}
|
|
(Some(end), true) => *sel = TextSelection::Span { start: *pos, end },
|
|
},
|
|
TextSelection::Span { start, end } => match (hit, drag) {
|
|
(None, false) => *sel = TextSelection::None,
|
|
(None, true) => *sel = TextSelection::Pos(*start),
|
|
(Some(hit), false) => {
|
|
if recent
|
|
&& let Some(double) = self.text.double_hit
|
|
&& double == hit
|
|
{
|
|
return self.select_line_at(hit);
|
|
} else {
|
|
*sel = TextSelection::Pos(hit)
|
|
}
|
|
}
|
|
(Some(hit), true) => *end = hit,
|
|
},
|
|
}
|
|
if let TextSelection::Span { start, end } = sel
|
|
&& start == end
|
|
{
|
|
*sel = TextSelection::Pos(*start);
|
|
}
|
|
}
|
|
|
|
pub fn deselect(&mut self) {
|
|
self.text.selection = TextSelection::None;
|
|
}
|
|
|
|
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
|
|
let old = (self.text.content(), self.text.selection);
|
|
let mut undo = false;
|
|
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
|
if undo && let Some((old, selection)) = self.text.history.pop() {
|
|
self.set(&old);
|
|
self.text.selection = selection;
|
|
} else if self.text.content() != old.0 {
|
|
self.text.history.push(old);
|
|
}
|
|
res
|
|
}
|
|
|
|
fn apply_event_inner(
|
|
&mut self,
|
|
event: &KeyEvent,
|
|
modifiers: &Modifiers,
|
|
undo: &mut bool,
|
|
) -> TextInputResult {
|
|
match &event.logical_key {
|
|
Key::Named(named) => match named {
|
|
NamedKey::Backspace => self.backspace(modifiers.control),
|
|
NamedKey::Delete => self.delete(modifiers.control),
|
|
NamedKey::Space => self.insert(" "),
|
|
NamedKey::Enter => {
|
|
if modifiers.shift {
|
|
self.newline();
|
|
} else {
|
|
return TextInputResult::Submit;
|
|
}
|
|
}
|
|
NamedKey::ArrowRight => {
|
|
if modifiers.control {
|
|
self.motion(Motion::RightWord, modifiers.shift)
|
|
} else {
|
|
self.motion(Motion::Right, modifiers.shift)
|
|
}
|
|
}
|
|
NamedKey::ArrowLeft => {
|
|
if modifiers.control {
|
|
self.motion(Motion::LeftWord, modifiers.shift)
|
|
} else {
|
|
self.motion(Motion::Left, modifiers.shift)
|
|
}
|
|
}
|
|
NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift),
|
|
NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift),
|
|
NamedKey::Escape => {
|
|
self.deselect();
|
|
return TextInputResult::Unfocus;
|
|
}
|
|
_ => return TextInputResult::Unused,
|
|
},
|
|
Key::Character(text) => {
|
|
if modifiers.control {
|
|
match text.as_str() {
|
|
"v" => return TextInputResult::Paste,
|
|
"c" => {
|
|
if let TextSelection::Span { start, end } = self.text.selection {
|
|
let content = self.text.select_content(start, end);
|
|
return TextInputResult::Copy(content);
|
|
}
|
|
}
|
|
"x" => {
|
|
if let TextSelection::Span { start, end } = self.text.selection {
|
|
let content = self.text.select_content(start, end);
|
|
self.clear_span();
|
|
return TextInputResult::Copy(content);
|
|
}
|
|
}
|
|
"a" => {
|
|
if !self.text.buf.lines[0].text().is_empty()
|
|
|| self.text.buf.lines.len() > 1
|
|
{
|
|
let lines = &self.text.buf.lines;
|
|
let last_line = lines.len() - 1;
|
|
let last_idx = lines[last_line].text().len();
|
|
self.text.selection = TextSelection::Span {
|
|
start: Cursor::new(0, 0),
|
|
end: Cursor::new(last_line, last_idx),
|
|
};
|
|
}
|
|
}
|
|
"z" => {
|
|
*undo = true;
|
|
}
|
|
_ => self.insert(text),
|
|
}
|
|
} else {
|
|
self.insert(text);
|
|
}
|
|
}
|
|
_ => return TextInputResult::Unused,
|
|
}
|
|
TextInputResult::Used
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct Modifiers {
|
|
pub shift: bool,
|
|
pub control: bool,
|
|
}
|
|
|
|
impl Modifiers {
|
|
pub fn clear(&mut self) {
|
|
self.shift = false;
|
|
self.control = false;
|
|
}
|
|
}
|
|
|
|
pub enum TextInputResult {
|
|
Used,
|
|
Unused,
|
|
Unfocus,
|
|
Submit,
|
|
Copy(String),
|
|
Paste,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Copy)]
|
|
pub enum TextSelection {
|
|
#[default]
|
|
None,
|
|
Pos(Cursor),
|
|
Span {
|
|
start: Cursor,
|
|
end: Cursor,
|
|
},
|
|
}
|
|
|
|
impl TextSelection {
|
|
pub fn clear(&mut self) {
|
|
match self {
|
|
TextSelection::None => (),
|
|
TextSelection::Pos(cursor) => {
|
|
cursor.line = 0;
|
|
cursor.index = 0;
|
|
cursor.affinity = Affinity::default();
|
|
}
|
|
TextSelection::Span { start: _, end: _ } => {
|
|
*self = TextSelection::None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TextInputResult {
|
|
pub fn unfocus(&self) -> bool {
|
|
matches!(self, TextInputResult::Unfocus)
|
|
}
|
|
}
|
|
|
|
impl Deref for TextEdit {
|
|
type Target = TextView;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.view
|
|
}
|
|
}
|
|
|
|
impl DerefMut for TextEdit {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.view
|
|
}
|
|
}
|