use std::{ops::Not, sync::{Arc, OnceLock}};
use bevy_input_focus::InputFocus;
use bevy_ecs::system::*;
use bevy_ecs::prelude::*;
use bevy_ui::prelude::*;
use bevy_utils::prelude::*;
use bevy_app::prelude::*;
use bevy_picking::prelude::*;
use bevy_text::{TextColor, TextFont, LineHeight};
use cosmic_text::{Edit, Selection};
use crate::impl_haalka_methods_futures_signals as impl_haalka_methods;
use super::{
el::El, element::{ElementWrapper, Nameable, UiRootable}, pointer_event_aware::{PointerEventAware, CursorOnHoverable}, raw::{RawElWrapper, register_system}, mouse_wheel_scrollable::MouseWheelScrollable,
utils::clone, viewport_mutable::ViewportMutable, global_event_aware::GlobalEventAware,
raw::{observe, utils::remove_system_holder_on_remove}
};
use apply::Apply;
use bevy_ui_text_input::{actions::TextInputAction, text_input_pipeline::TextInputPipeline, *};
use futures_signals::signal::{Mutable, Signal, SignalExt};
use paste::paste;
#[derive(Default)]
pub struct TextInput {
el: El<Node>,
}
impl ElementWrapper for TextInput {
type EL = El<Node>;
fn element_mut(&mut self) -> &mut Self::EL {
&mut self.el
}
}
impl GlobalEventAware for TextInput {}
impl Nameable for TextInput {}
impl PointerEventAware for TextInput {}
impl MouseWheelScrollable for TextInput {}
impl UiRootable for TextInput {}
impl ViewportMutable for TextInput {}
impl CursorOnHoverable for TextInput {}
impl TextInput {
#[allow(missing_docs, clippy::new_without_default)]
pub fn new() -> Self {
let el = El::<Node>::new().update_raw_el(|raw_el| {
raw_el
.insert((
TextInputNode {
clear_on_submit: false,
..default()
},
Pickable::default(),
LastSignalText::default()
))
});
Self { el }
}
pub fn with_buffer(
self,
f: impl FnOnce(Mut<TextInputBuffer>, ResMut<TextInputPipeline>) + Send + 'static,
) -> Self {
self.update_raw_el(|raw_el| raw_el.on_spawn(move |world, entity| {
#[allow(clippy::type_complexity)]
let mut system_state: SystemState<(
Query<&mut TextInputBuffer>,
ResMut<TextInputPipeline>,
)> = SystemState::new(world);
let (mut buffers, text_input_pipeline) = system_state.get_mut(world);
if let Ok(buffer) = buffers.get_mut(entity) {
f(buffer, text_input_pipeline)
}
}))
}
pub fn on_signal_with_buffer<T: Send + 'static>(
self,
signal: impl Signal<Item = T> + Send + 'static,
mut f: impl FnMut(Mut<TextInputBuffer>, ResMut<TextInputPipeline>, T) + Send + Sync + 'static,
) -> Self {
self.update_raw_el(move |raw_el| {
raw_el.on_signal_with_system(
signal,
move |In((entity, value)): In<(Entity, T)>,
mut buffers: Query<&mut TextInputBuffer>,
text_input_pipeline: ResMut<TextInputPipeline>| {
if let Ok(buffer) = buffers.get_mut(entity) {
f(buffer, text_input_pipeline, value)
};
},
)
})
}
pub fn text(self, text_option: impl Into<Option<String>>) -> Self {
let text = text_option.into().unwrap_or_default();
self.with_text_input_queue(move |mut text_input_queue| {
queue_set_text_actions(&mut text_input_queue, text);
})
}
pub fn text_signal<S: Signal<Item = impl Into<Option<String>>> + Send + 'static>(
mut self,
text_option_signal_option: impl Into<Option<S>>,
) -> Self {
if let Some(text_option_signal) = text_option_signal_option.into() {
self = self.update_raw_el(|raw_el| {
raw_el
.with_entity(|mut entity| {
entity.insert_if_new(TextInputContents::default());
})
.on_signal_with_system(
text_option_signal.map(|text_option| text_option.into().unwrap_or_default()),
|In((entity, text)): In<(Entity, String)>,
mut last_text_query: Query<&mut LastSignalText>,
contents_query: Query<&TextInputContents>,
mut text_input_queues: Query<&mut TextInputQueue>| {
if let Ok(mut last_text) = last_text_query.get_mut(entity) {
if last_text.0 != text {
let should_update = contents_query
.get(entity)
.ok()
.map(|contents| contents.get() != text.as_str())
.unwrap_or(true);
if should_update {
last_text.0 = text.clone();
if let Ok(mut queue) = text_input_queues.get_mut(entity) {
queue_set_text_actions(&mut queue, text);
}
}
}
}
},
)
});
}
self
}
pub fn on_focused_change_with_system<Marker>(
self,
handler: impl IntoSystem<In<(Entity, bool,)>, (), Marker> + Send + 'static,
) -> Self {
self.update_raw_el(|raw_el| {
let system_holder = Arc::new(OnceLock::new());
raw_el
.with_entity(|mut entity| { entity.insert(Focusable { is_focused: false }); })
.on_spawn(clone!((system_holder) move |world, entity| {
let system = register_system(world, handler);
let _ = system_holder.set(system);
observe(world, entity, move |event: On<FocusedChange>, mut commands: Commands| {
commands.run_system_with(system, (entity, event.event().focused))
});
}))
.apply(remove_system_holder_on_remove(system_holder.clone()))
})
}
pub fn on_focused_change(self, mut handler: impl FnMut(bool) + Send + Sync + 'static) -> Self {
self.on_focused_change_with_system(move |In((_, is_focused))| handler(is_focused))
}
pub fn focused_sync(self, focused: Mutable<bool>) -> Self {
self.on_focused_change(move |is_focused| focused.set_neq(is_focused))
}
pub fn focus_option(mut self, focus_option: impl Into<Option<bool>>) -> Self {
if Into::<Option<bool>>::into(focus_option).unwrap_or(false) {
self = self.update_raw_el(|raw_el| raw_el.on_spawn_with_system(|In(entity), mut commands: Commands| {
commands.insert_resource(InputFocus(Some(entity)));
}));
}
self
}
pub fn focus(self) -> Self {
self.focus_option(true)
}
pub fn focus_signal<S: Signal<Item = bool> + Send + 'static>(
mut self,
focus_signal_option: impl Into<Option<S>>,
) -> Self {
if let Some(focus_signal) = focus_signal_option.into() {
self = self.update_raw_el(|raw_el| {
raw_el.on_signal_with_system(focus_signal, |In((entity, focus)), mut focused_option: ResMut<InputFocus>| {
if focus {
focused_option.0 = Some(entity);
} else if let Some(focused) = focused_option.0 && focused == entity {
focused_option.0 = None;
}
})
})
}
self
}
pub fn on_change_with_system<Marker>(self, handler: impl IntoSystem<In<(Entity, String)>, (), Marker> + Send + 'static) -> Self {
self.update_raw_el(|raw_el| {
let system_holder = Arc::new(OnceLock::new());
raw_el.on_spawn(clone!((system_holder) move |world, entity| {
let system = register_system(world, handler);
let _ = system_holder.set(system);
observe(world, entity, move |change: On<TextInputChange>, mut commands: Commands| {
let TextInputChange { entity, text } = change.event();
commands.run_system_with(system, (*entity, text.clone()));
});
}))
.with_entity(|mut entity| { entity.insert_if_new((ListenToChanges, TextInputContents::default())); })
.apply(remove_system_holder_on_remove(system_holder))
})
}
pub fn on_change(self, mut handler: impl FnMut(String) + Send + Sync + 'static) -> Self {
self.on_change_with_system(move |In((_, text))| handler(text))
}
pub fn on_change_sync(self, string: Mutable<String>) -> Self {
self.on_change_with_system(
move |In((entity, text)): In<(Entity, String)>, mut last_text_query: Query<&mut LastSignalText>| {
if let Ok(mut last_text) = last_text_query.get_mut(entity) {
if last_text.0 != text {
last_text.0 = text.clone();
string.set_neq(text);
}
}
},
)
}
}
#[derive(Component, Default)]
struct LastSignalText(String);
fn queue_set_text_actions(
text_input_queue: &mut TextInputQueue,
text: String,
) {
for action in [
TextInputAction::Edit(actions::TextInputEdit::SelectAll),
TextInputAction::Edit(actions::TextInputEdit::Paste(text)),
] {
text_input_queue.add(action);
}
}
#[derive(Component)]
struct ListenToChanges;
#[derive(EntityEvent)]
struct TextInputChange { entity: Entity, text: String }
#[allow(clippy::type_complexity)]
fn on_change(contents: Query<(Entity, &TextInputContents), (Changed<TextInputContents>, With<ListenToChanges>)>, mut commands: Commands) {
for (entity, contents) in contents.iter() {
commands.trigger(TextInputChange { entity, text: contents.get().to_string() });
}
}
#[derive(EntityEvent)]
struct FocusedChange { entity: Entity, focused: bool }
#[derive(Component)]
struct Focusable {
is_focused: bool,
}
fn on_focus_changed(
focused_option: Res<InputFocus>,
mut text_inputs: Query<(Entity, &mut Focusable)>,
mut commands: Commands,
) {
for (entity, mut focusable) in text_inputs.iter_mut() {
if Some(entity) == focused_option.0 {
if focusable.is_focused.not() {
focusable.is_focused = true;
commands.trigger(FocusedChange { entity, focused: true });
}
} else if focusable.is_focused {
focusable.is_focused = false;
commands.trigger(FocusedChange { entity, focused: false });
}
}
}
impl_haalka_methods! {
TextInput {
node: Node,
text_input_node: TextInputNode,
text_input_buffer: TextInputBuffer,
text_font: TextFont,
text_input_layout_info: TextInputLayoutInfo,
text_input_style: TextInputStyle,
text_color: TextColor,
text_input_prompt: TextInputPrompt,
text_input_queue: TextInputQueue,
line_height: LineHeight,
}
}
#[derive(Resource, Default)]
pub struct ClearSelectionOnFocusChangeDisabled;
fn clear_selection_on_focus_change(
input_focus: Res<InputFocus>,
mut text_input_pipeline: ResMut<TextInputPipeline>,
mut buffers: Query<&mut TextInputBuffer>,
mut previous_input_focus: Local<Option<Entity>>,
) {
if *previous_input_focus != input_focus.0 {
if let Some(entity) = *previous_input_focus && let Ok(mut buffer) = buffers.get_mut(entity) {
buffer
.editor
.borrow_with(&mut text_input_pipeline.font_system)
.set_selection(Selection::None);
}
*previous_input_focus = input_focus.0;
}
}
pub(super) fn plugin(app: &mut App) {
if app.is_plugin_added::<TextInputPlugin>().not() {
app.add_plugins(TextInputPlugin);
}
app.add_systems(
Update,
(
on_change.run_if(any_with_component::<ListenToChanges>),
on_focus_changed.run_if(resource_changed_or_removed::<InputFocus>),
clear_selection_on_focus_change.run_if(not(resource_exists::<ClearSelectionOnFocusChangeDisabled>))
)
.run_if(any_with_component::<TextInputNode>),
);
}