use std::collections::VecDeque;
use cosmic_text::{
Action, Attrs, AttrsList, Buffer, Cursor, Edit as _, Editor, FontSystem, Motion, Selection,
Shaping, Wrap,
};
use rustc_hash::FxHashMap;
use crate::color::Color;
use crate::display::{Clip, ClipId, Quad, QuadKind};
use crate::geom::{Rect, Size, Vec2};
use crate::input::{EditKey, Mods};
use crate::key::Key;
use crate::resources::Resources;
use crate::spec::{TextStyle, TextWrap};
use crate::text::TextSystem;
use crate::tree::OriginId;
#[derive(Clone, Debug, Default)]
pub struct EditOptions {
pub style: TextStyle,
pub multiline: bool,
pub wrap: bool,
pub autofocus: bool,
pub keep_tab: bool,
pub placeholder: Option<String>,
pub accent: Option<Color>,
pub secure: bool,
}
pub const SECURE_BULLET: char = '\u{2022}';
const BULLET_LEN: usize = SECURE_BULLET.len_utf8();
fn bullets(n: usize) -> String {
std::iter::repeat_n(SECURE_BULLET, n).collect()
}
fn graphemes(text: &str) -> usize {
use unicode_segmentation::UnicodeSegmentation;
text.graphemes(true).count()
}
fn grapheme_start(text: &str, n: usize) -> usize {
use unicode_segmentation::UnicodeSegmentation;
text.grapheme_indices(true)
.nth(n)
.map_or(text.len(), |(at, _)| at)
}
fn graphemes_before(text: &str, byte: usize) -> usize {
use unicode_segmentation::UnicodeSegmentation;
text.grapheme_indices(true)
.take_while(|(at, _)| *at < byte)
.count()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct KeyOutcome {
pub changed: bool,
pub submit: bool,
pub boundary: Option<Edge>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Edge {
Start,
End,
}
impl Edge {
pub fn name(self) -> &'static str {
match self {
Edge::Start => "start",
Edge::End => "end",
}
}
}
pub(crate) struct EditState {
editor: Editor<'static>,
pub(crate) style: TextStyle,
pub(crate) accent: Color,
pub(crate) multiline: bool,
keep_tab: bool,
secret: Option<String>,
placeholder: Option<(String, u32, Buffer)>,
folds: bool,
pub(crate) origin: OriginId,
scale: f32,
wrap: Option<f32>,
pub version: u64,
metrics_rev: u32,
measured: Option<(u64, u32, u32, Size)>,
natural: Option<(u64, u32, Size)>,
offset_x: f32,
preedit: Option<Preedit>,
undo: VecDeque<EditOp>,
redo: VecDeque<EditOp>,
coalesce: Option<Coalesce>,
autofocus: bool,
last_declared: u64,
}
pub const MAX_UNDECLARED_EDITS: usize = 256;
const UNDO_CAP: usize = 1000;
const COALESCE_MAX: usize = 64;
struct EditOp {
at: Cursor,
deleted: String,
inserted: String,
cursor_before: Cursor,
cursor_after: Cursor,
}
#[derive(Clone, Copy, PartialEq)]
enum Coalesce {
Insert,
Backspace,
Delete,
}
fn end_cursor(at: Cursor, s: &str) -> Cursor {
match s.rsplit_once('\n') {
None => Cursor::new(at.line, at.index + s.len()),
Some((head, tail)) => Cursor::new(at.line + head.matches('\n').count() + 1, tail.len()),
}
}
fn same_pos(a: Cursor, b: Cursor) -> bool {
a.line == b.line && a.index == b.index
}
struct Preedit {
start: Cursor,
text: String,
}
fn abs_offset(b: &Buffer, c: Cursor) -> usize {
b.lines
.iter()
.take(c.line)
.map(|l| l.text().len() + 1)
.sum::<usize>()
+ c.index
}
impl EditState {
fn invalidate_measurements(&mut self) {
self.measured = None;
self.natural = None;
}
fn record(&mut self, op: EditOp, kind: Option<Coalesce>) {
self.redo.clear();
if self.secret.is_some() {
return;
}
if let (Some(k), Some(last)) = (kind, self.undo.back_mut())
&& self.coalesce == Some(k)
&& last.deleted.len() + last.inserted.len() + op.deleted.len() + op.inserted.len()
<= COALESCE_MAX
{
let merged = match k {
Coalesce::Insert => {
same_pos(op.at, end_cursor(last.at, &last.inserted)) && {
last.inserted.push_str(&op.inserted);
true
}
}
Coalesce::Backspace => {
same_pos(end_cursor(op.at, &op.deleted), last.at) && {
last.at = op.at;
last.deleted.insert_str(0, &op.deleted);
true
}
}
Coalesce::Delete => {
same_pos(op.at, last.at) && {
last.deleted.push_str(&op.deleted);
true
}
}
};
if merged {
last.cursor_after = op.cursor_after;
return;
}
}
if self.undo.len() >= UNDO_CAP {
self.undo.pop_front();
}
self.undo.push_back(op);
self.coalesce = kind;
}
fn break_coalesce(&mut self) {
self.coalesce = None;
}
fn splice(&mut self, at: Cursor, remove: &str, insert: &str) {
if remove.is_empty() {
self.editor.set_selection(Selection::None);
self.editor.set_cursor(at);
} else {
self.editor.set_selection(Selection::Normal(at));
self.editor.set_cursor(end_cursor(at, remove));
self.editor.delete_selection();
}
if !insert.is_empty() {
self.editor.insert_string(insert, None);
}
}
fn insert_recorded(&mut self, text: &str) {
if self.secret.is_some() {
self.secure_replace(text);
return;
}
let cursor_before = self.editor.cursor();
let deleted = self.editor.copy_selection().unwrap_or_default();
self.editor.delete_selection();
let at = self.editor.cursor();
self.editor.insert_string(text, None);
let kind = (deleted.is_empty() && !text.contains('\n')).then_some(Coalesce::Insert);
self.record(
EditOp {
at,
deleted,
inserted: text.to_string(),
cursor_before,
cursor_after: self.editor.cursor(),
},
kind,
);
}
fn delete_selection_recorded(&mut self) -> bool {
if self.secret.is_some() {
return self.selected().is_some() && {
self.secure_replace("");
true
};
}
let cursor_before = self.editor.cursor();
let Some(deleted) = self.editor.copy_selection() else {
return false;
};
if !self.editor.delete_selection() {
return false;
}
let at = self.editor.cursor();
self.record(
EditOp {
at,
deleted,
inserted: String::new(),
cursor_before,
cursor_after: at,
},
None,
);
true
}
fn delete_motion_recorded(
&mut self,
motion: Motion,
kind: Coalesce,
fs: &mut FontSystem,
) -> bool {
let cursor_before = self.editor.cursor();
self.editor.set_selection(Selection::Normal(cursor_before));
self.editor.action(fs, Action::Motion(motion));
let deleted = self.editor.copy_selection().unwrap_or_default();
if deleted.is_empty() {
self.editor.set_selection(Selection::None);
return false;
}
if self.secret.is_some() {
self.secure_replace("");
return true;
}
self.editor.delete_selection();
let at = self.editor.cursor();
self.record(
EditOp {
at,
deleted,
inserted: String::new(),
cursor_before,
cursor_after: at,
},
Some(kind),
);
true
}
fn selected(&self) -> Option<(Cursor, Cursor)> {
self.editor
.selection_bounds()
.filter(|(a, z)| !same_pos(*a, *z))
}
fn secure_replace(&mut self, text: &str) {
let Some(secret) = self.secret.as_mut() else {
return;
};
let text = admitted(text, false);
let caret = self.editor.cursor();
let (a, z) = self.editor.selection_bounds().unwrap_or((caret, caret));
let (a, z) = (
grapheme_start(secret, a.index / BULLET_LEN),
grapheme_start(secret, z.index / BULLET_LEN),
);
secret.replace_range(a..z, &text);
let at = graphemes_before(secret, a + text.len());
let n = graphemes(secret);
self.set_bullets(n);
self.editor
.set_cursor(Cursor::new(0, at.min(n) * BULLET_LEN));
}
fn set_bullets(&mut self, n: usize) {
let had = self
.editor
.with_buffer(|b| b.lines.first().map_or(0, |l| l.text().len()))
/ BULLET_LEN;
self.editor.set_selection(Selection::None);
if n > had {
self.editor.set_cursor(Cursor::new(0, had * BULLET_LEN));
self.editor.insert_string(&bullets(n - had), None);
} else if n < had {
self.editor
.set_selection(Selection::Normal(Cursor::new(0, n * BULLET_LEN)));
self.editor.set_cursor(Cursor::new(0, had * BULLET_LEN));
self.editor.delete_selection();
}
}
fn set_secure(&mut self, on: bool, attrs: &Attrs<'_>) {
self.abandon_preedit();
let caret = self.editor.cursor();
let (shown, at) = if on {
let (text, before) = self.editor.with_buffer(|b| {
let text = buffer_text(b);
let at = abs_offset(b, caret).min(text.len());
let before = text.get(..at).map_or(0, |t| graphemes(&admitted(t, false)));
(admitted(&text, false).into_owned(), before)
});
let shown = bullets(graphemes(&text));
self.secret = Some(text);
(shown, before * BULLET_LEN)
} else {
let text = self.secret.take().unwrap_or_default();
let at = grapheme_start(&text, caret.index / BULLET_LEN);
(text, at)
};
self.editor
.with_buffer_mut(|b| b.set_text(&shown, attrs, Shaping::Advanced, None));
self.editor.set_selection(Selection::None);
self.editor.set_cursor(Cursor::new(0, at));
self.undo.clear();
self.redo.clear();
self.coalesce = None;
self.version += 1;
self.wrap = None;
self.invalidate_measurements();
}
fn abandon_preedit(&mut self) -> bool {
let Some(pre) = self.preedit.take() else {
return false;
};
self.splice(pre.start, &pre.text, "");
self.editor.set_cursor(pre.start);
self.editor.set_selection(Selection::None);
true
}
fn undo_one(&mut self) -> bool {
let Some(op) = self.undo.pop_back() else {
return false;
};
self.splice(op.at, &op.inserted, &op.deleted);
self.editor.set_cursor(op.cursor_before);
self.editor.set_selection(Selection::None);
self.redo.push_back(op);
self.coalesce = None;
true
}
fn redo_one(&mut self) -> bool {
let Some(op) = self.redo.pop_back() else {
return false;
};
self.splice(op.at, &op.deleted, &op.inserted);
self.editor.set_cursor(op.cursor_after);
self.editor.set_selection(Selection::None);
self.undo.push_back(op);
self.coalesce = None;
true
}
}
pub(crate) enum Unclaimed {
Key(Key),
Label(String),
}
pub struct EditStore {
states: FxHashMap<Key, EditState>,
pending: FxHashMap<Key, String>,
pending_labels: FxHashMap<String, String>,
pub(crate) focused: Option<Key>,
pub(crate) dragging: Option<(Key, Vec2)>,
pub(crate) caret_moved: Option<Key>,
caret_stamp: u64,
blink_visible: bool,
frame_no: u64,
}
impl Default for EditStore {
fn default() -> Self {
Self {
states: FxHashMap::default(),
pending: FxHashMap::default(),
pending_labels: FxHashMap::default(),
focused: None,
dragging: None,
caret_moved: None,
caret_stamp: 0,
blink_visible: true,
frame_no: 0,
}
}
}
fn admitted(text: &str, multiline: bool) -> std::borrow::Cow<'_, str> {
let text: std::borrow::Cow<'_, str> = if text.contains('\u{2029}') {
text.replace('\u{2029}', "\n").into()
} else {
text.into()
};
let text = if multiline || !text.contains(['\n', '\r']) {
text
} else {
text.chars()
.filter(|c| *c != '\n' && *c != '\r')
.collect::<String>()
.into()
};
match crate::text::one_paragraph(&text) {
std::borrow::Cow::Borrowed(_) => text,
std::borrow::Cow::Owned(s) => s.into(),
}
}
fn typed(c: char, multiline: bool) -> Option<char> {
let c = if c == '\u{2029}' { '\n' } else { c };
(!c.is_control() || (c == '\n' && multiline) || c == '\t').then_some(c)
}
fn composed(
text: &str,
cursor: Option<(usize, usize)>,
multiline: bool,
) -> (String, Option<(usize, usize)>) {
let mut out = String::with_capacity(text.len());
let mut starts = Vec::with_capacity(text.len());
for (at, c) in text.char_indices() {
starts.push((at, out.len()));
if let Some(c) = typed(c, multiline) {
out.push(c);
}
}
let moved = |o: usize| {
starts
.iter()
.find(|(at, _)| *at >= o)
.map_or(out.len(), |(_, kept)| *kept)
};
let cursor = cursor.map(|(a, b)| (moved(a), moved(b)));
(out, cursor)
}
fn attrs_for<'a>(style: &TextStyle, res: &'a Resources) -> Attrs<'a> {
res.weights_of(style.family).apply(
Attrs::new()
.family(res.family_of(style.family))
.font_features(crate::text::cosmic_features(&style.features)),
style.bold,
)
}
impl EditStore {
pub(crate) fn reweigh(&mut self, res: &Resources) {
for s in self.states.values_mut() {
let a = AttrsList::new(&attrs_for(&s.style, res));
s.editor.with_buffer_mut(|b| {
for line in &mut b.lines {
line.set_attrs_list(a.clone());
line.reset_shaping();
}
});
s.editor.set_redraw(true);
s.wrap = None;
s.metrics_rev = s.metrics_rev.wrapping_add(1);
s.invalidate_measurements();
}
}
pub fn focused(&self) -> Option<Key> {
self.focused
}
pub fn set_focus(&mut self, key: Option<Key>) {
if self.focused != key {
self.caret_stamp += 1;
if let Some(old) = self.focused
&& let Some(s) = self.states.get_mut(&old)
&& s.abandon_preedit()
{
s.version += 1;
s.invalidate_measurements();
}
}
self.focused = key;
}
pub fn caret_stamp(&self) -> u64 {
self.caret_stamp
}
pub fn set_blink_visible(&mut self, visible: bool) {
self.blink_visible = visible;
}
pub fn blink_visible(&self) -> bool {
self.blink_visible
}
fn touch_caret(&mut self, key: Key) {
self.caret_moved = Some(key);
self.caret_stamp += 1;
}
pub fn len(&self) -> usize {
self.states.len()
}
pub fn is_empty(&self) -> bool {
self.states.is_empty()
}
pub(crate) fn begin_frame(&mut self, frame_no: u64) {
self.frame_no = frame_no;
if self.states.len() > MAX_UNDECLARED_EDITS {
self.evict(frame_no.saturating_sub(1));
}
}
fn evict(&mut self, declared_at: u64) {
let focused = self.focused;
let dragging = self.dragging.map(|(k, _)| k);
let caret_moved = &mut self.caret_moved;
crate::retain::evict_undeclared(
&mut self.states,
MAX_UNDECLARED_EDITS,
declared_at,
|s| s.last_declared,
|k| Some(k) == focused || Some(k) == dragging,
|k| {
if *caret_moved == Some(k) {
*caret_moved = None;
}
},
);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn declare(
&mut self,
key: Key,
initial: &str,
opts: &EditOptions,
origin: OriginId,
scale: f32,
fs: &mut FontSystem,
res: &Resources,
) -> bool {
let frame_no = self.frame_no;
let edge = self
.states
.get(&key)
.is_none_or(|s| s.last_declared + 1 != frame_no || !s.autofocus);
let seed = if self.states.contains_key(&key) {
None
} else {
self.pending.remove(&key)
};
let seeded = seed.is_some();
let initial = seed.as_deref().unwrap_or(initial);
let multiline = opts.multiline && !opts.secure;
let state = self.states.entry(key).or_insert_with(|| {
let metrics = crate::text::shaper_metrics(
opts.style.size * scale,
opts.style.line_height * scale,
);
let mut buffer = Buffer::new(fs, metrics);
buffer.set_size(None, None);
let text = admitted(initial, multiline);
let secret = opts.secure.then(|| text.to_string());
let shown = match &secret {
Some(s) => bullets(graphemes(s)).into(),
None => text,
};
buffer.set_text(
&shown,
&attrs_for(&opts.style, res),
Shaping::Advanced,
None,
);
let mut editor = Editor::new(buffer);
if seeded || !multiline {
let end = editor.with_buffer(|b| {
let line = b.lines.len().saturating_sub(1);
Cursor::new(line, b.lines.get(line).map_or(0, |l| l.text().len()))
});
editor.set_cursor(end);
}
EditState {
editor,
style: opts.style,
accent: opts.accent.unwrap_or(crate::select::TINT),
multiline,
keep_tab: opts.keep_tab,
secret,
placeholder: None,
folds: false,
origin,
scale,
wrap: None,
version: 0,
metrics_rev: 0,
measured: None,
natural: None,
offset_x: 0.0,
preedit: None,
undo: VecDeque::new(),
redo: VecDeque::new(),
coalesce: None,
autofocus: false,
last_declared: frame_no,
}
});
state.last_declared = frame_no;
state.autofocus = opts.autofocus;
state.origin = origin;
state.multiline = multiline;
state.keep_tab = opts.keep_tab;
if opts.secure != state.secret.is_some() {
state.set_secure(opts.secure, &attrs_for(&opts.style, res));
}
state.accent = opts.accent.unwrap_or(crate::select::TINT);
let folds = multiline || (opts.wrap && opts.style.wrap != TextWrap::None);
let mode = match (multiline, opts.style.wrap) {
(false, TextWrap::Glyph) => Wrap::Glyph,
_ => Wrap::WordOrGlyph,
};
if state.folds != folds || state.editor.with_buffer(|b| b.wrap()) != mode {
state.folds = folds;
state.editor.with_buffer_mut(|b| b.set_wrap(mode));
state.invalidate_measurements();
}
if state.scale != scale || state.style != opts.style {
state.style = opts.style;
state.scale = scale;
let metrics = crate::text::shaper_metrics(
opts.style.size * scale,
opts.style.line_height * scale,
);
state.editor.with_buffer_mut(|b| b.set_metrics(metrics));
state.wrap = None;
state.metrics_rev = state.metrics_rev.wrapping_add(1);
state.invalidate_measurements();
}
match opts.placeholder.as_deref().filter(|p| !p.is_empty()) {
None => state.placeholder = None,
Some(p) => {
let rev = state.metrics_rev;
if state
.placeholder
.as_ref()
.is_none_or(|(t, r, _)| t != p || *r != rev)
{
let metrics = state.editor.with_buffer(|b| b.metrics());
let mut b = Buffer::new(fs, metrics);
b.set_size(None, None);
b.set_text(
&crate::text::one_paragraph(p),
&attrs_for(&opts.style, res),
Shaping::Advanced,
None,
);
b.shape_until_scroll(fs, false);
state.placeholder = Some((p.to_string(), rev, b));
}
}
}
edge
}
pub(crate) fn shown_text(&self, key: Key) -> Option<String> {
let s = self.states.get(&key)?;
if s.secret.is_some() {
return Some(s.editor.with_buffer(buffer_text));
}
self.text(key)
}
pub fn is_secure(&self, key: Key) -> bool {
self.states.get(&key).is_some_and(|s| s.secret.is_some())
}
pub fn placeholder(&self, key: Key) -> Option<&str> {
let s = self.states.get(&key)?;
s.placeholder.as_ref().map(|(t, ..)| t.as_str())
}
pub fn contains(&self, key: Key) -> bool {
self.states.contains_key(&key)
}
pub fn origin_of(&self, key: Key) -> Option<OriginId> {
self.states.get(&key).map(|s| s.origin)
}
pub fn text(&self, key: Key) -> Option<String> {
let s = self.states.get(&key)?;
if let Some(secret) = &s.secret {
return Some(secret.clone());
}
Some(s.editor.with_buffer(|b| {
let mut text = buffer_text(b);
if let Some(pre) = &s.preedit {
let at = abs_offset(b, pre.start);
if text.is_char_boundary(at) && at + pre.text.len() <= text.len() {
text.replace_range(at..at + pre.text.len(), "");
}
}
text
}))
}
pub fn set_text(&mut self, key: Key, text: &str, fs: &mut FontSystem, res: &Resources) -> bool {
let Some(s) = self.states.get_mut(&key) else {
self.pending.insert(key, text.to_string());
return false;
};
s.preedit = None;
let a = attrs_for(&s.style, res);
let text = admitted(text, s.multiline);
let text = match &mut s.secret {
Some(secret) => {
*secret = text.into_owned();
bullets(graphemes(secret)).into()
}
None => text,
};
s.editor
.with_buffer_mut(|b| b.set_text(&text, &a, Shaping::Advanced, None));
s.editor.set_selection(Selection::None);
s.editor.action(fs, Action::Motion(Motion::BufferEnd));
s.version += 1;
s.invalidate_measurements();
s.wrap = None;
s.undo.clear();
s.redo.clear();
s.coalesce = None;
self.touch_caret(key);
true
}
pub(crate) fn hold_label(&mut self, label: &str, text: &str) {
self.pending_labels
.insert(label.to_string(), text.to_string());
}
pub(crate) fn claim_label(
&mut self,
key: Key,
label: &str,
fs: &mut FontSystem,
res: &Resources,
) {
if self.pending_labels.is_empty() {
return;
}
let Some(text) = self.pending_labels.remove(label) else {
return;
};
if self.states.contains_key(&key) {
self.set_text(key, &text, fs, res);
} else {
self.pending.insert(key, text);
}
}
pub(crate) fn take_unclaimed_seeds(&mut self) -> Vec<Unclaimed> {
let mut out: Vec<Unclaimed> = Vec::new();
if !self.pending.is_empty() {
let mut keys: Vec<Key> = self.pending.drain().map(|(k, _)| k).collect();
keys.sort_unstable();
out.extend(keys.into_iter().map(Unclaimed::Key));
}
if !self.pending_labels.is_empty() {
let mut labels: Vec<String> = self.pending_labels.drain().map(|(l, _)| l).collect();
labels.sort_unstable();
out.extend(labels.into_iter().map(Unclaimed::Label));
}
out
}
pub fn version(&self, key: Key) -> u64 {
self.states.get(&key).map_or(0, |s| s.version)
}
pub fn caret_and_selection(&self, key: Key) -> Option<(usize, Option<(usize, usize)>)> {
let s = self.states.get(&key)?;
Some(s.editor.with_buffer(|b| {
let caret = abs_offset(b, s.editor.cursor());
let selection = s
.editor
.selection_bounds()
.map(|(a, z)| (abs_offset(b, a), abs_offset(b, z)))
.filter(|(a, z)| a != z);
(caret, selection)
}))
}
pub fn selection_cursors(&self, key: Key) -> Option<((usize, usize), (usize, usize))> {
let s = self.states.get(&key)?;
let caret = s.editor.cursor();
let anchor = match s.editor.selection() {
Selection::Normal(c) | Selection::Line(c) | Selection::Word(c) => c,
Selection::None => caret,
};
Some(((anchor.line, anchor.index), (caret.line, caret.index)))
}
pub(crate) fn runs(
&self,
key: Key,
node: Key,
origin: Vec2,
scale: f32,
) -> Vec<crate::access::AccessRun> {
let Some(s) = self.states.get(&key) else {
return Vec::new();
};
let mut out = Vec::new();
let mut n = 0;
let origin = Vec2::new(origin.x - s.offset_x, origin.y);
s.editor.with_buffer(|b| {
crate::access::runs_of_buffer(
b,
crate::access::RunSource {
key: node,
line: 0,
byte_base: 0,
origin,
scale,
newline_after_last: false,
},
&mut n,
&mut out,
|_| (0.0, 0.0),
)
});
out
}
pub fn set_selection(&mut self, key: Key, anchor: (usize, usize), focus: (usize, usize)) {
let Some(s) = self.states.get_mut(&key) else {
return;
};
s.abandon_preedit();
let clamp = |b: &Buffer, (line, index): (usize, usize)| {
let line = line.min(b.lines.len().saturating_sub(1));
let text = b.lines[line].text();
let mut index = index.min(text.len());
while !text.is_char_boundary(index) {
index -= 1;
}
Cursor::new(line, index)
};
let (a, f) = s
.editor
.with_buffer(|b| (clamp(b, anchor), clamp(b, focus)));
s.editor.set_cursor(f);
s.editor.set_selection(if a == f {
Selection::None
} else {
Selection::Normal(a)
});
s.break_coalesce();
self.touch_caret(key);
}
pub fn replace_selection(&mut self, key: Key, text: &str, fs: &mut FontSystem) -> bool {
let Some(s) = self.states.get_mut(&key) else {
return false;
};
s.abandon_preedit();
if text.is_empty() && s.editor.selection_bounds().is_none() {
return false;
}
s.insert_recorded(text);
s.editor.shape_as_needed(fs, false);
s.version += 1;
s.invalidate_measurements();
self.touch_caret(key);
true
}
pub fn copy_selection(&self, key: Key) -> Option<String> {
let s = self.states.get(&key)?;
if s.secret.is_some() {
return None;
}
s.editor.copy_selection()
}
pub fn can_copy(&self, key: Key) -> bool {
self.has_selection(key) && !self.is_secure(key)
}
pub fn has_selection(&self, key: Key) -> bool {
self.states
.get(&key)
.and_then(|s| s.editor.selection_bounds())
.is_some_and(|(a, b)| a != b)
}
pub fn history(&self, key: Key) -> (bool, bool) {
self.states
.get(&key)
.map_or((false, false), |s| (!s.undo.is_empty(), !s.redo.is_empty()))
}
pub fn delete_selection(&mut self, key: Key, fs: &mut FontSystem) -> bool {
let Some(s) = self.states.get_mut(&key) else {
return false;
};
let _ = fs;
s.abandon_preedit();
if s.delete_selection_recorded() {
s.version += 1;
s.invalidate_measurements();
self.touch_caret(key);
true
} else {
false
}
}
pub(crate) fn apply_text(&mut self, key: Key, text: &str, fs: &mut FontSystem) -> bool {
let Some(s) = self.states.get_mut(&key) else {
return false;
};
s.abandon_preedit();
let filtered: String = text.chars().filter_map(|c| typed(c, s.multiline)).collect();
if filtered.is_empty() {
return false;
}
s.insert_recorded(&filtered);
s.editor.shape_as_needed(fs, false);
s.version += 1;
s.invalidate_measurements();
self.touch_caret(key);
true
}
pub(crate) fn apply_key(
&mut self,
key: Key,
ek: EditKey,
mods: Mods,
fs: &mut FontSystem,
) -> KeyOutcome {
let Some(s) = self.states.get_mut(&key) else {
return KeyOutcome::default();
};
let mut changed = s.abandon_preedit();
let mut submit = false;
let at = |e: &Editor<'static>| (e.cursor().line, e.cursor().index);
let before = (at(&s.editor), s.editor.selection());
let row = |e: &Editor<'static>| e.cursor_position().map(|(_, y)| y);
let row_before = row(&s.editor);
let composing = changed;
let mut by_paragraph = false;
match ek {
EditKey::Left
| EditKey::Right
| EditKey::Up
| EditKey::Down
| EditKey::Home
| EditKey::End
| EditKey::PageUp
| EditKey::PageDown => {
let mac = cfg!(target_os = "macos");
let word = mods.word || (!mac && mods.doc);
let (word, line) = if s.secret.is_some() {
(false, (mac && mods.doc) || word)
} else {
(word, mac && mods.doc)
};
let motion = match (ek, line, word) {
(EditKey::Left, true, _) => Motion::Home,
(EditKey::Right, true, _) => Motion::End,
(EditKey::Up, true, _) => Motion::BufferStart,
(EditKey::Down, true, _) => Motion::BufferEnd,
(EditKey::Left, _, true) => Motion::LeftWord,
(EditKey::Right, _, true) => Motion::RightWord,
(EditKey::Up, _, true) => Motion::ParagraphStart,
(EditKey::Down, _, true) => Motion::ParagraphEnd,
(EditKey::Left, ..) => Motion::Left,
(EditKey::Right, ..) => Motion::Right,
(EditKey::Up, ..) => Motion::Up,
(EditKey::Down, ..) => Motion::Down,
(EditKey::Home, ..) if mods.doc => Motion::BufferStart,
(EditKey::End, ..) if mods.doc => Motion::BufferEnd,
(EditKey::Home, ..) => Motion::Home,
(EditKey::End, ..) => Motion::End,
(EditKey::PageUp, ..) => Motion::PageUp,
(EditKey::PageDown, ..) => Motion::PageDown,
_ => unreachable!(),
};
if mods.shift {
if s.editor.selection() == Selection::None {
s.editor.set_selection(Selection::Normal(s.editor.cursor()));
}
} else {
s.editor.set_selection(Selection::None);
}
by_paragraph = matches!(motion, Motion::ParagraphStart | Motion::ParagraphEnd);
if by_paragraph {
let c = s.editor.cursor();
let len = s
.editor
.with_buffer(|b| b.lines.get(c.line).map_or(0, |l| l.text().len()));
let last = s.editor.with_buffer(|b| b.lines.len().saturating_sub(1));
if motion == Motion::ParagraphStart && c.index == 0 && c.line > 0 {
s.editor.action(fs, Action::Motion(Motion::Previous));
} else if motion == Motion::ParagraphEnd && c.index >= len && c.line < last {
s.editor.action(fs, Action::Motion(Motion::Next));
}
}
s.editor.action(fs, Action::Motion(motion));
s.break_coalesce();
}
EditKey::Backspace | EditKey::Delete => {
let (line, word) = if cfg!(target_os = "macos") {
(mods.doc, mods.word)
} else {
(false, mods.word || mods.doc)
};
let (line, word) = if s.secret.is_some() {
(line || word, false)
} else {
(line, word)
};
let back = ek == EditKey::Backspace;
let motion = match (back, line, word) {
(true, true, _) => Motion::Home,
(true, _, true) => Motion::LeftWord,
(true, ..) => Motion::Left,
(false, true, _) => Motion::End,
(false, _, true) => Motion::RightWord,
(false, ..) => Motion::Right,
};
let kind = if back {
Coalesce::Backspace
} else {
Coalesce::Delete
};
changed |=
s.delete_selection_recorded() || s.delete_motion_recorded(motion, kind, fs);
}
EditKey::Enter => {
if s.multiline {
s.insert_recorded("\n");
changed = true;
} else {
submit = true;
}
}
EditKey::Tab => {
if s.multiline {
s.insert_recorded(" ");
changed = true;
}
}
EditKey::Undo => changed |= s.undo_one(),
EditKey::Redo => changed |= s.redo_one(),
EditKey::SelectAll => {
s.editor.action(fs, Action::Motion(Motion::BufferStart));
s.editor.set_selection(Selection::Normal(s.editor.cursor()));
s.editor.action(fs, Action::Motion(Motion::BufferEnd));
s.break_coalesce();
}
EditKey::Escape => {
s.editor.action(fs, Action::Escape);
s.break_coalesce();
}
}
s.editor.shape_as_needed(fs, false);
let plain = !changed && !composing && !mods.shift && before.1 == Selection::None;
let still = plain && (at(&s.editor), s.editor.selection()) == before;
let same_row = plain && row_before.is_some() && row(&s.editor) == row_before;
let boundary = match ek {
EditKey::Up if by_paragraph && still => Some(Edge::Start),
EditKey::Down if by_paragraph && still => Some(Edge::End),
EditKey::Up | EditKey::Down if by_paragraph => None,
EditKey::Up if same_row => Some(Edge::Start),
EditKey::Down if same_row => Some(Edge::End),
EditKey::Left | EditKey::Backspace if still => Some(Edge::Start),
EditKey::Right | EditKey::Delete if still => Some(Edge::End),
_ => None,
};
if changed {
s.version += 1;
s.invalidate_measurements();
}
self.touch_caret(key);
KeyOutcome {
changed,
submit,
boundary,
}
}
pub(crate) fn click(
&mut self,
key: Key,
local: Vec2,
clicks: u8,
extend: bool,
fs: &mut FontSystem,
) {
if let Some(s) = self.states.get_mut(&key) {
if s.abandon_preedit() {
s.version += 1;
s.invalidate_measurements();
}
let (x, y) = ((local.x * s.scale) as i32, (local.y * s.scale) as i32);
let action = match (extend, clicks) {
(true, _) => Action::Drag { x, y },
(false, 0 | 1) => Action::Click { x, y },
(false, 2) if s.secret.is_none() => Action::DoubleClick { x, y },
(false, _) => Action::TripleClick { x, y },
};
s.editor.action(fs, action);
s.editor.shape_as_needed(fs, false);
s.break_coalesce();
self.caret_stamp += 1;
}
}
pub(crate) fn drag(&mut self, key: Key, local: Vec2, fs: &mut FontSystem) {
if let Some(s) = self.states.get_mut(&key) {
let (x, y) = ((local.x * s.scale) as i32, (local.y * s.scale) as i32);
let before = s.editor.cursor();
s.editor.action(fs, Action::Drag { x, y });
if s.editor.cursor() != before {
self.touch_caret(key);
} else {
self.caret_stamp += 1;
}
}
}
pub(crate) fn set_preedit(
&mut self,
key: Key,
text: &str,
cursor: Option<(usize, usize)>,
fs: &mut FontSystem,
) -> bool {
let Some(s) = self.states.get_mut(&key) else {
return false;
};
if s.secret.is_some() {
return false;
}
let (text, cursor) = composed(text, cursor, s.multiline);
let text = &*text;
if text.is_empty() {
if !s.abandon_preedit() {
return false;
}
} else {
let start = match s.preedit.take() {
Some(pre) => {
s.splice(pre.start, &pre.text, text);
pre.start
}
None => {
s.delete_selection_recorded();
let at = s.editor.cursor();
s.editor.insert_string(text, None);
at
}
};
let mut at = cursor.map_or(text.len(), |(c, _)| c.min(text.len()));
while at > 0 && !text.is_char_boundary(at) {
at -= 1;
}
s.editor.set_cursor(end_cursor(start, &text[..at]));
s.editor.set_selection(Selection::None);
s.preedit = Some(Preedit {
start,
text: text.to_string(),
});
s.break_coalesce();
}
s.editor.shape_as_needed(fs, false);
s.version += 1;
s.invalidate_measurements();
self.touch_caret(key);
true
}
pub(crate) fn collapse_selection(&mut self, key: Key) -> bool {
let Some(s) = self.states.get_mut(&key) else {
return false;
};
if matches!(s.editor.selection(), Selection::None) {
return false;
}
s.editor.set_selection(Selection::None);
self.caret_stamp += 1;
true
}
pub fn preedit(&self, key: Key) -> Option<&str> {
self.states
.get(&key)?
.preedit
.as_ref()
.map(|p| p.text.as_str())
}
pub fn is_multiline(&self, key: Key) -> bool {
self.states.get(&key).is_some_and(|s| s.multiline)
}
pub fn keeps_tab(&self, key: Key) -> bool {
self.states
.get(&key)
.is_some_and(|s| s.keep_tab && !s.multiline)
}
pub(crate) fn folds(&self, key: Key) -> bool {
self.states.get(&key).is_some_and(|s| s.folds)
}
pub(crate) fn caret_rect(&mut self, key: Key, fs: &mut FontSystem) -> Option<Rect> {
let s = self.states.get_mut(&key)?;
s.editor.shape_as_needed(fs, false);
let (x, y) = s.editor.cursor_position()?;
let line_height = s.editor.with_buffer(|b| b.metrics().line_height);
Some(Rect::new(
x as f32 - s.offset_x,
y as f32,
(2.0 * s.scale).max(2.0),
line_height,
))
}
pub(crate) fn caret_local(&mut self, key: Key, fs: &mut FontSystem) -> Option<Rect> {
let s = self.states.get_mut(&key)?;
s.editor.shape_as_needed(fs, false);
let (x, y) = s.editor.cursor_position()?;
let line_height = s.editor.with_buffer(|b| b.metrics().line_height);
Some(Rect::new(
x as f32 / s.scale,
y as f32 / s.scale,
0.0,
line_height / s.scale,
))
}
pub(crate) fn line_offset(&mut self, key: Key, inner_w: f32, fs: &mut FontSystem) -> f32 {
let focused = self.focused == Some(key);
let Some(s) = self.states.get_mut(&key) else {
return 0.0;
};
if s.folds {
return 0.0;
}
if !focused {
s.offset_x = 0.0;
return 0.0;
}
s.editor.shape_as_needed(fs, false);
let caret_w = (2.0 * s.scale).max(2.0);
let text_w = s
.editor
.with_buffer(|b| b.layout_runs().map(|r| r.line_w).fold(0.0f32, f32::max));
if let Some((x, _)) = s.editor.cursor_position() {
let x = x as f32;
if x - s.offset_x > inner_w - caret_w {
s.offset_x = x - inner_w + caret_w;
}
if x < s.offset_x {
s.offset_x = x;
}
}
s.offset_x = s.offset_x.clamp(0.0, (text_w + caret_w - inner_w).max(0.0));
s.offset_x
}
pub(crate) fn intrinsic(&mut self, key: Key, fs: &mut FontSystem) -> Size {
let Some(s) = self.states.get_mut(&key) else {
return Size::ZERO;
};
if let Some((v, m, size)) = s.natural
&& (v, m) == (s.version, s.metrics_rev)
{
return size;
}
s.editor.with_buffer_mut(|b| b.set_size(None, None));
s.wrap = None;
s.editor.shape_as_needed(fs, false);
let (w, h) = s.editor.with_buffer(|b| {
let (size, lines) = crate::text::measure_buffer(b, 0);
(size.w, lines.max(1) as f32 * b.metrics().line_height)
});
let size = Size::new((w + 2.0 * s.scale) / s.scale, h / s.scale);
s.natural = Some((s.version, s.metrics_rev, size));
size
}
pub(crate) fn baseline(&self, key: Key) -> f32 {
let Some(s) = self.states.get(&key) else {
return f32::NAN;
};
let b = s.editor.with_buffer(|b| {
b.layout_runs()
.next()
.map_or(b.metrics().line_height * 0.8, |r| r.line_y.round())
});
b / s.scale
}
pub(crate) fn wrapped(&mut self, key: Key, max_w: f32, fs: &mut FontSystem) -> Size {
let Some(s) = self.states.get_mut(&key) else {
return Size::ZERO;
};
if !s.folds {
if s.wrap.is_some() {
s.editor.with_buffer_mut(|b| b.set_size(None, None));
s.wrap = None;
s.invalidate_measurements();
}
s.editor.shape_as_needed(fs, false);
let line = s.editor.with_buffer(|b| b.metrics().line_height);
return Size::new(max_w, line / s.scale);
}
let target = (max_w * s.scale).max(1.0);
if crate::text::wrap_differs(s.wrap, Some(target)) {
s.editor.with_buffer_mut(|b| b.set_size(Some(target), None));
s.wrap = Some(target);
}
let stamp = (s.version, s.metrics_rev, target.to_bits());
if let Some((v, m, w, size)) = s.measured
&& (v, m, w) == stamp
{
return size;
}
s.editor.shape_as_needed(fs, false);
let h = s.editor.with_buffer(|b| {
let (_, lines) = crate::text::measure_buffer(b, 0);
lines.max(1) as f32 * b.metrics().line_height
});
let size = Size::new(max_w, h / s.scale);
s.measured = Some((stamp.0, stamp.1, stamp.2, size));
size
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn emit(
&mut self,
key: Key,
origin: Vec2,
focused: bool,
selected: bool,
faint: Color,
clip: Clip,
clip_id: ClipId,
fs: &mut FontSystem,
text_system: &mut TextSystem,
atlas: &mut crate::atlas::GlyphAtlas,
out: &mut Vec<Quad>,
) {
let blink_visible = self.blink_visible;
let Some(s) = self.states.get_mut(&key) else {
return;
};
let raster = text_system.raster_mut();
s.editor.shape_as_needed(fs, false);
let color = s.style.color_or_default();
let accent = s.accent;
let scale = s.scale;
let selection = s.editor.selection_bounds().filter(|_| selected);
let composing = focused && s.preedit.is_some();
let preedit = s
.preedit
.as_ref()
.map(|p| (p.start, end_cursor(p.start, &p.text)));
let cursor_pos = if focused && (blink_visible || composing) {
s.editor.cursor_position()
} else {
None
};
let origin = Vec2::new(origin.x - s.offset_x, origin.y);
let vis = clip.visible();
let empty = s.preedit.is_none()
&& s.editor
.with_buffer(|b| b.lines.len() == 1 && b.lines[0].text().is_empty());
if empty && let Some((_, _, b)) = &s.placeholder {
for run in b.layout_runs() {
for glyph in run.glyphs.iter() {
let physical = glyph.physical((0.0, 0.0), 1.0);
let Some(slot) =
crate::text::raster_glyph(physical.cache_key, fs, raster, atlas)
else {
continue;
};
out.push(Quad {
rect: Rect::new(
origin.x + physical.x as f32 + slot.left as f32,
origin.y + run.line_y.round() + physical.y as f32 - slot.top as f32,
slot.w as f32,
slot.h as f32,
),
color: faint,
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: crate::text::glyph_kind(&slot),
uv: [slot.x, slot.y, slot.w, slot.h],
clip: clip_id,
});
}
}
}
s.editor.with_buffer(|b| {
let line_height = b.metrics().line_height;
let runs = b
.layout_runs()
.filter(|run| origin.y + run.line_top + line_height >= vis.y)
.take_while(|run| origin.y + run.line_top <= vis.y + vis.h);
for run in runs {
if let Some((start, end)) = selection
&& run.line_i >= start.line
&& run.line_i <= end.line
{
let mut any = false;
for (x, w) in run.highlight(start, end) {
any = true;
out.push(Quad {
rect: Rect::new(
origin.x + x,
origin.y + run.line_top,
w.max(2.0),
line_height,
),
color: accent,
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: QuadKind::Solid,
uv: [0; 4],
clip: clip_id,
});
}
if !any && run.glyphs.is_empty() && end.line > run.line_i {
out.push(Quad {
rect: Rect::new(origin.x, origin.y + run.line_top, 2.0, line_height),
color: accent,
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: QuadKind::Solid,
uv: [0; 4],
clip: clip_id,
});
}
}
if let Some((ps, pe)) = preedit
&& run.line_i >= ps.line
&& run.line_i <= pe.line
{
let underline_h = scale.max(1.0);
for (x, w) in run.highlight(ps, pe) {
let solid = |rect: Rect, color: Color| Quad {
rect,
color,
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: QuadKind::Solid,
uv: [0; 4],
clip: clip_id,
};
out.push(solid(
Rect::new(origin.x + x, origin.y + run.line_top, w, line_height),
Color { a: 0.3, ..accent },
));
out.push(solid(
Rect::new(
origin.x + x,
origin.y + run.line_top + line_height - underline_h,
w,
underline_h,
),
color,
));
}
}
for glyph in run.glyphs.iter() {
let physical = glyph.physical((0.0, 0.0), 1.0);
let Some(slot) =
crate::text::raster_glyph(physical.cache_key, fs, raster, atlas)
else {
continue;
};
let x = origin.x + physical.x as f32 + slot.left as f32;
let y = origin.y + run.line_y.round() + physical.y as f32 - slot.top as f32;
let glyph_color = glyph
.color_opt
.map(|c| Color::rgba8(c.r(), c.g(), c.b(), c.a()))
.unwrap_or(color);
out.push(Quad {
rect: Rect::new(x, y, slot.w as f32, slot.h as f32),
color: glyph_color,
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: crate::text::glyph_kind(&slot),
uv: [slot.x, slot.y, slot.w, slot.h],
clip: clip_id,
});
}
}
if let Some((cx, cy)) = cursor_pos {
out.push(Quad {
rect: Rect::new(
origin.x + cx as f32,
origin.y + cy as f32,
(2.0 * scale).max(2.0),
line_height,
),
color,
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: QuadKind::Solid,
uv: [0; 4],
clip: clip_id,
});
}
});
}
}
fn buffer_text(b: &Buffer) -> String {
let mut out = String::new();
for (i, line) in b.lines.iter().enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(line.text());
}
out
}