use bevy_clipboard::ClipboardRead;
use bevy_math::Vec2;
use bevy_reflect::Reflect;
use parley::PlainEditorDriver;
use smol_str::SmolStr;
use crate::TextBrush;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect)]
pub struct PreeditCursor {
pub anchor: usize,
pub focus: usize,
}
#[derive(Debug, Clone, PartialEq, Reflect)]
pub enum TextEdit {
Copy,
Cut,
Paste,
Insert(SmolStr),
Backspace,
BackspaceWord,
Delete,
DeleteWord,
Left(bool),
Right(bool),
WordLeft(bool),
WordRight(bool),
Up(bool),
Down(bool),
TextStart(bool),
TextEnd(bool),
HardLineStart(bool),
HardLineEnd(bool),
LineStart(bool),
LineEnd(bool),
CollapseSelection,
SelectAll,
SelectAllIfCollapsed,
MoveToPoint(Vec2),
SelectWordAtPoint(Vec2),
SelectLineAtPoint(Vec2),
SelectedHardLineAtPoint(Vec2),
ExtendSelectionToPoint(Vec2),
ShiftClickExtension(Vec2),
ImeSetCompose {
value: SmolStr,
cursor: Option<PreeditCursor>,
},
ImeCommit {
value: SmolStr,
},
}
impl TextEdit {
pub fn clear_ime_compose() -> Self {
Self::ImeSetCompose {
value: SmolStr::new_inline(""),
cursor: None,
}
}
pub fn apply<'a>(
self,
driver: &'a mut PlainEditorDriver<TextBrush>,
clipboard: &mut bevy_clipboard::Clipboard,
max_characters: Option<usize>,
char_filter: impl Fn(char) -> bool,
) {
match self {
TextEdit::Copy => {
if let Some(text) = driver.editor.selected_text()
&& let Err(e) = clipboard.set_text(text)
{
bevy_log::warn!("Failed to write selection to clipboard: {e:?}");
}
}
TextEdit::Cut => {
if let Some(text) = driver.editor.selected_text() {
match clipboard.set_text(text) {
Ok(()) => driver.delete(),
Err(e) => bevy_log::warn!("Failed to write selection to clipboard: {e:?}"),
}
}
}
TextEdit::Paste => {
bevy_log::warn_once!("Directly applying a Paste edit is not recommended, as it cannot defer asynchronous clipboard reads.
For proper handling of async clipboard operations, use `EditableText::apply_pending_edits` instead.");
let mut read = clipboard.fetch_text();
poll_and_apply_paste(&mut read, driver, max_characters, char_filter);
}
TextEdit::Insert(text) => {
let _ = insert_filtered(driver, text.as_str(), max_characters, char_filter);
}
TextEdit::Backspace => driver.backdelete(),
TextEdit::BackspaceWord => driver.backdelete_word(),
TextEdit::Delete => driver.delete(),
TextEdit::DeleteWord => driver.delete_word(),
TextEdit::Left(false) => driver.move_left(),
TextEdit::Right(false) => driver.move_right(),
TextEdit::WordLeft(false) => driver.move_word_left(),
TextEdit::WordRight(false) => driver.move_word_right(),
TextEdit::Up(false) => driver.move_up(),
TextEdit::Down(false) => driver.move_down(),
TextEdit::TextStart(false) => driver.move_to_text_start(),
TextEdit::TextEnd(false) => driver.move_to_text_end(),
TextEdit::HardLineStart(false) => driver.move_to_hard_line_start(),
TextEdit::HardLineEnd(false) => driver.move_to_hard_line_end(),
TextEdit::LineStart(false) => driver.move_to_line_start(),
TextEdit::LineEnd(false) => driver.move_to_line_end(),
TextEdit::Left(true) => driver.select_left(),
TextEdit::Right(true) => driver.select_right(),
TextEdit::WordLeft(true) => driver.select_word_left(),
TextEdit::WordRight(true) => driver.select_word_right(),
TextEdit::Up(true) => driver.select_up(),
TextEdit::Down(true) => driver.select_down(),
TextEdit::TextStart(true) => driver.select_to_text_start(),
TextEdit::TextEnd(true) => driver.select_to_text_end(),
TextEdit::HardLineStart(true) => driver.select_to_hard_line_start(),
TextEdit::HardLineEnd(true) => driver.select_to_hard_line_end(),
TextEdit::LineStart(true) => driver.select_to_line_start(),
TextEdit::LineEnd(true) => driver.select_to_line_end(),
TextEdit::CollapseSelection => driver.collapse_selection(),
TextEdit::SelectAll => driver.select_all(),
TextEdit::SelectAllIfCollapsed => {
if driver.editor.raw_selection().is_collapsed() {
driver.select_all();
}
}
TextEdit::MoveToPoint(point) => driver.move_to_point(point.x, point.y),
TextEdit::SelectWordAtPoint(point) => driver.select_word_at_point(point.x, point.y),
TextEdit::SelectLineAtPoint(point) => driver.select_line_at_point(point.x, point.y),
TextEdit::SelectedHardLineAtPoint(point) => {
driver.select_hard_line_at_point(point.x, point.y);
}
TextEdit::ExtendSelectionToPoint(point) => {
driver.extend_selection_to_point(point.x, point.y);
}
TextEdit::ShiftClickExtension(point) => driver.shift_click_extension(point.x, point.y),
TextEdit::ImeSetCompose { value, cursor } => {
if value.is_empty() {
driver.clear_compose();
} else {
let cursor = cursor.map(|c| (c.anchor, c.focus));
driver.set_compose(&value, cursor);
}
}
TextEdit::ImeCommit { value: text } => {
driver.clear_compose();
if text.chars().all(&char_filter)
&& max_characters.is_none_or(|max| {
driver.editor.text().chars().count() + text.chars().count() <= max
})
{
driver.insert_or_replace_selection(text.as_str());
}
}
}
}
}
enum InsertRejection {
CharFilter,
MaxLength,
}
fn insert_filtered(
driver: &mut PlainEditorDriver<TextBrush>,
text: &str,
max_characters: Option<usize>,
char_filter: impl Fn(char) -> bool,
) -> Result<(), InsertRejection> {
if !text.chars().all(char_filter) {
return Err(InsertRejection::CharFilter);
}
if let Some(max) = max_characters {
let select_len = driver
.editor
.selected_text()
.map(str::chars)
.map(Iterator::count)
.unwrap_or(0);
if max < driver.editor.text().chars().count() - select_len + text.chars().count() {
return Err(InsertRejection::MaxLength);
}
}
driver.insert_or_replace_selection(text);
Ok(())
}
pub(crate) fn poll_and_apply_paste(
read: &mut ClipboardRead,
driver: &mut PlainEditorDriver<TextBrush>,
max_characters: Option<usize>,
char_filter: impl Fn(char) -> bool,
) -> bool {
match read.poll_result() {
Some(Ok(text)) => {
if matches!(
insert_filtered(driver, &text, max_characters, char_filter),
Err(InsertRejection::CharFilter)
) {
bevy_log::debug!(
"Paste rejected: clipboard contents contained characters not allowed by the char filter."
);
}
true
}
Some(Err(e)) => {
bevy_log::warn!("Failed to read clipboard for paste: {e:?}");
true
}
None => false,
}
}