use crate::{
text_edit::{poll_and_apply_paste, TextEdit},
FontCx, FontHinting, LayoutCx, LineHeight, TextBrush, TextColor, TextFont, TextLayout,
};
use alloc::sync::Arc;
use bevy_clipboard::ClipboardRead;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::prelude::*;
use core::time::Duration;
use parley::{FontContext, LayoutContext, PlainEditor, SplitString};
#[derive(Component, Clone)]
#[require(
TextLayout,
TextFont,
TextColor,
LineHeight,
FontHinting,
EditableTextGeneration
)]
pub struct EditableText {
pub editor: PlainEditor<TextBrush>,
pub pending_edits: Vec<TextEdit>,
pub pending_paste: Option<ClipboardRead>,
pub cursor_width: f32,
pub cursor_blink_period: Duration,
pub max_characters: Option<usize>,
pub visible_lines: Option<f32>,
pub visible_width: Option<f32>,
pub allow_newlines: bool,
}
impl Default for EditableText {
fn default() -> Self {
Self {
editor: PlainEditor::new(100.),
pending_edits: Vec::new(),
pending_paste: None,
cursor_width: 0.2,
cursor_blink_period: Duration::from_secs(1),
max_characters: None,
visible_lines: Some(1.),
visible_width: None,
allow_newlines: false,
}
}
}
impl EditableText {
pub fn new(initial_text: impl AsRef<str>) -> Self {
let mut editable_text = Self::default();
editable_text.editor.set_text(initial_text.as_ref());
editable_text.queue_edit(TextEdit::TextEnd(false));
editable_text
}
pub fn editor(&self) -> &PlainEditor<TextBrush> {
&self.editor
}
pub fn editor_mut(&mut self) -> &mut PlainEditor<TextBrush> {
&mut self.editor
}
pub fn value(&self) -> SplitString<'_> {
self.editor.text()
}
pub fn queue_edit(&mut self, edit: TextEdit) {
self.pending_edits.push(edit);
}
pub fn apply_pending_edits(
&mut self,
font_context: &mut FontContext,
layout_context: &mut LayoutContext<TextBrush>,
clipboard: &mut bevy_clipboard::Clipboard,
char_filter: impl Fn(char) -> bool,
) {
let Self {
editor,
pending_edits,
pending_paste,
max_characters,
..
} = self;
let mut driver = editor.driver(font_context, layout_context);
if let Some(mut read) = pending_paste.take()
&& !poll_and_apply_paste(&mut read, &mut driver, *max_characters, &char_filter)
{
*pending_paste = Some(read);
return;
}
let mut edits = core::mem::take(pending_edits).into_iter();
while let Some(edit) = edits.next() {
match edit {
TextEdit::Paste => {
let mut read = clipboard.fetch_text();
if !poll_and_apply_paste(&mut read, &mut driver, *max_characters, &char_filter)
{
*pending_paste = Some(read);
pending_edits.extend(edits);
return;
}
}
other => other.apply(&mut driver, clipboard, *max_characters, &char_filter),
}
}
}
pub fn clear(&mut self) {
self.editor.set_text("");
self.pending_edits.clear();
self.pending_paste = None;
}
pub fn is_composing(&self) -> bool {
self.editor.is_composing()
}
}
#[derive(Component, PartialEq, Eq, Default, Clone, Copy, Deref, DerefMut)]
pub struct EditableTextGeneration(parley::Generation);
#[derive(Component, Clone, Default)]
pub struct EditableTextFilter(Option<Arc<dyn Fn(char) -> bool + Send + Sync + 'static>>);
impl EditableTextFilter {
pub fn new(filter: impl Fn(char) -> bool + Send + Sync + 'static) -> Self {
Self(Some(Arc::new(filter)))
}
}
pub fn apply_text_edits(
mut query: Query<(
Entity,
&mut EditableText,
Option<&EditableTextFilter>,
&EditableTextGeneration,
)>,
mut font_context: ResMut<FontCx>,
mut layout_context: ResMut<LayoutCx>,
mut clipboard: ResMut<bevy_clipboard::Clipboard>,
mut commands: Commands,
) {
for (entity, mut editable_text, filter, generation) in query.iter_mut() {
if !editable_text.pending_edits.is_empty() || editable_text.pending_paste.is_some() {
editable_text.apply_pending_edits(
&mut font_context.0,
&mut layout_context.0,
&mut clipboard,
match filter {
Some(EditableTextFilter(Some(filter))) => filter.as_ref(),
_ => &|_| true,
},
);
}
if **generation != editable_text.editor.generation() {
commands.trigger(TextEditChange { entity });
}
}
}
#[derive(EntityEvent)]
pub struct TextEditChange {
entity: Entity,
}