1use std::{ops::Not, sync::{Arc, OnceLock}};
4
5use bevy_input_focus::InputFocus;
6use bevy_ecs::system::*;
7use bevy_ecs::prelude::*;
8use bevy_ui::prelude::*;
9use bevy_utils::prelude::*;
10use bevy_app::prelude::*;
11use bevy_picking::prelude::*;
12use bevy_text::{cosmic_text::{Edit, Selection}, TextColor, TextFont};
13
14use crate::impl_haalka_methods;
15
16use super::{
17 el::El, element::{ElementWrapper, Nameable, UiRootable}, pointer_event_aware::{PointerEventAware, CursorOnHoverable}, raw::{RawElWrapper, register_system}, mouse_wheel_scrollable::MouseWheelScrollable,
18 utils::clone, viewport_mutable::ViewportMutable, global_event_aware::GlobalEventAware,
19 raw::{observe, utils::remove_system_holder_on_remove}
20};
21use apply::Apply;
22use bevy_ui_text_input::{actions::TextInputAction, text_input_pipeline::TextInputPipeline, *};
23use futures_signals::signal::{Mutable, Signal, SignalExt};
24use paste::paste;
25
26#[derive(Default)]
28pub struct TextInput {
29 el: El<Node>,
30}
31
32impl ElementWrapper for TextInput {
33 type EL = El<Node>;
34 fn element_mut(&mut self) -> &mut Self::EL {
35 &mut self.el
36 }
37}
38
39impl GlobalEventAware for TextInput {}
40impl Nameable for TextInput {}
41impl PointerEventAware for TextInput {}
42impl MouseWheelScrollable for TextInput {}
43impl UiRootable for TextInput {}
44impl ViewportMutable for TextInput {}
45impl CursorOnHoverable for TextInput {}
46
47impl TextInput {
49 #[allow(missing_docs, clippy::new_without_default)]
50 pub fn new() -> Self {
51 let el = El::<Node>::new().update_raw_el(|raw_el| {
52 raw_el
53 .insert((
54 TextInputNode {
55 clear_on_submit: false,
56 ..default()
57 },
58 Pickable::default(),
59 LastSignalText::default()
60 ))
61 });
62 Self { el }
63 }
64
65 pub fn with_buffer(
67 self,
68 f: impl FnOnce(Mut<TextInputBuffer>, ResMut<TextInputPipeline>) + Send + 'static,
69 ) -> Self {
70 self.update_raw_el(|raw_el| raw_el.on_spawn(move |world, entity| {
72 #[allow(clippy::type_complexity)]
74 let mut system_state: SystemState<(
75 Query<&mut TextInputBuffer>,
76 ResMut<TextInputPipeline>,
77 )> = SystemState::new(world);
78 let (mut buffers, text_input_pipeline) = system_state.get_mut(world);
79 if let Ok(buffer) = buffers.get_mut(entity) {
80 f(buffer, text_input_pipeline)
81 }
82 }))
83 }
84
85 pub fn on_signal_with_buffer<T: Send + 'static>(
87 self,
88 signal: impl Signal<Item = T> + Send + 'static,
89 mut f: impl FnMut(Mut<TextInputBuffer>, ResMut<TextInputPipeline>, T) + Send + Sync + 'static,
90 ) -> Self {
91 self.update_raw_el(move |raw_el| {
92 raw_el.on_signal_with_system(
93 signal,
94 move |In((entity, value)): In<(Entity, T)>,
95 mut buffers: Query<&mut TextInputBuffer>,
96 text_input_pipeline: ResMut<TextInputPipeline>| {
97 if let Ok(buffer) = buffers.get_mut(entity) {
98 f(buffer, text_input_pipeline, value)
99 };
100 },
101 )
102 })
103 }
104
105 pub fn text(self, text_option: impl Into<Option<String>>) -> Self {
107 let text = text_option.into().unwrap_or_default();
108 self.with_text_input_queue(move |mut text_input_queue| {
109 queue_set_text_actions(&mut text_input_queue, text);
110 })
111 }
112
113 pub fn text_signal<S: Signal<Item = impl Into<Option<String>>> + Send + 'static>(
115 mut self,
116 text_option_signal_option: impl Into<Option<S>>,
117 ) -> Self {
118 if let Some(text_option_signal) = text_option_signal_option.into() {
119 self = self.update_raw_el(|raw_el| {
120 raw_el.on_signal_with_system(
121 text_option_signal.map(|text_option| text_option.into().unwrap_or_default()),
122 |In((entity, text)): In<(Entity, String)>,
123 mut last_text_query: Query<&mut LastSignalText>,
124 mut text_input_queues: Query<&mut TextInputQueue>| {
125 if let Ok(mut last_text) = last_text_query.get_mut(entity) {
126 if last_text.0 != text {
129 last_text.0 = text.clone();
130 if let Ok(mut queue) = text_input_queues.get_mut(entity) {
131 queue_set_text_actions(&mut queue, text);
132 }
133 }
134 }
135 },
136 )
137 });
138 }
139 self
140 }
141
142 pub fn on_focused_change_with_system<Marker>(
145 self,
146 handler: impl IntoSystem<In<(Entity, bool,)>, (), Marker> + Send + 'static,
147 ) -> Self {
148 self.update_raw_el(|raw_el| {
149 let system_holder = Arc::new(OnceLock::new());
150 raw_el
151 .with_entity(|mut entity| { entity.insert(Focusable { is_focused: false }); })
152 .on_spawn(clone!((system_holder) move |world, entity| {
153 let system = register_system(world, handler);
154 let _ = system_holder.set(system);
155 observe(world, entity, move |event: Trigger<FocusedChange>, mut commands: Commands| {
156 commands.run_system_with(system, (entity, event.event().0))
157 });
158 }))
159 .apply(remove_system_holder_on_remove(system_holder.clone()))
160 })
161 }
162
163 pub fn on_focused_change(self, mut handler: impl FnMut(bool) + Send + Sync + 'static) -> Self {
165 self.on_focused_change_with_system(move |In((_, is_focused))| handler(is_focused))
166 }
167
168 pub fn focused_sync(self, focused: Mutable<bool>) -> Self {
170 self.on_focused_change(move |is_focused| focused.set_neq(is_focused))
171 }
172
173 pub fn focus_option(mut self, focus_option: impl Into<Option<bool>>) -> Self {
175 if Into::<Option<bool>>::into(focus_option).unwrap_or(false) {
176 self = self.update_raw_el(|raw_el| raw_el.on_spawn_with_system(|In(entity), mut commands: Commands| {
177 commands.insert_resource(InputFocus(Some(entity)));
178 }));
179 }
180 self
181 }
182
183 pub fn focus(self) -> Self {
185 self.focus_option(true)
186 }
187
188 pub fn focus_signal<S: Signal<Item = bool> + Send + 'static>(
190 mut self,
191 focus_signal_option: impl Into<Option<S>>,
192 ) -> Self {
193 if let Some(focus_signal) = focus_signal_option.into() {
194 self = self.update_raw_el(|raw_el| {
195 raw_el.on_signal_with_system(focus_signal, |In((entity, focus)), mut focused_option: ResMut<InputFocus>| {
196 if focus {
197 focused_option.0 = Some(entity);
198 } else if let Some(focused) = focused_option.0 && focused == entity {
199 focused_option.0 = None;
200 }
201 })
202 })
203 }
204 self
205 }
206
207 pub fn on_change_with_system<Marker>(self, handler: impl IntoSystem<In<(Entity, String)>, (), Marker> + Send + 'static) -> Self {
209 self.update_raw_el(|raw_el| {
210 let system_holder = Arc::new(OnceLock::new());
211 raw_el.on_spawn(clone!((system_holder) move |world, entity| {
212 let system = register_system(world, handler);
213 let _ = system_holder.set(system);
214 observe(world, entity, move |change: Trigger<TextInputChange>, mut commands: Commands| {
215 commands.run_system_with(system, (change.target(), change.event().0.clone()));
216 });
217 }))
218 .with_entity(|mut entity| { entity.insert_if_new((ListenToChanges, TextInputContents::default())); })
219 .apply(remove_system_holder_on_remove(system_holder))
220 })
221 }
222
223 pub fn on_change(self, mut handler: impl FnMut(String) + Send + Sync + 'static) -> Self {
225 self.on_change_with_system(move |In((_, text))| handler(text))
226 }
227
228 pub fn on_change_sync(self, string: Mutable<String>) -> Self {
230 self.on_change_with_system(
231 move |In((entity, text)): In<(Entity, String)>, mut last_text_query: Query<&mut LastSignalText>| {
232 if let Ok(mut last_text) = last_text_query.get_mut(entity) {
233 if last_text.0 != text {
235 last_text.0 = text.clone();
236 string.set_neq(text);
237 }
238 }
239 },
240 )
241 }
242}
243
244#[derive(Component, Default)]
247struct LastSignalText(String);
248
249fn queue_set_text_actions(
250 text_input_queue: &mut TextInputQueue,
251 text: String,
252) {
253 for action in [
254 TextInputAction::Edit(actions::TextInputEdit::SelectAll),
255 TextInputAction::Edit(actions::TextInputEdit::Paste(text)),
256 ] {
257 text_input_queue.add(action);
258 }
259}
260
261#[derive(Component)]
262struct ListenToChanges;
263
264#[derive(Event)]
265struct TextInputChange(String);
266
267#[allow(clippy::type_complexity)]
268fn on_change(contents: Query<(Entity, &TextInputContents), (Changed<TextInputContents>, With<ListenToChanges>)>, mut commands: Commands) {
269 for (entity, contents) in contents.iter() {
270 commands.trigger_targets(TextInputChange(contents.get().to_string()), entity);
271 }
272}
273
274#[derive(Event)]
275struct FocusedChange(bool);
276
277#[derive(Component)]
278struct Focusable {
279 is_focused: bool,
280}
281
282fn on_focus_changed(
283 focused_option: Res<InputFocus>,
284 mut text_inputs: Query<(Entity, &mut Focusable)>,
285 mut commands: Commands,
286) {
287 for (entity, mut focusable) in text_inputs.iter_mut() {
288 if Some(entity) == focused_option.0 {
289 if focusable.is_focused.not() {
291 focusable.is_focused = true;
292 commands.trigger_targets(FocusedChange(true), entity);
293 }
294 } else if focusable.is_focused {
295 focusable.is_focused = false;
296 commands.trigger_targets(FocusedChange(false), entity);
297 }
298 }
299}
300
301impl_haalka_methods! {
302 TextInput {
303 node: Node,
304 text_input_node: TextInputNode,
305 text_input_buffer: TextInputBuffer,
306 text_font: TextFont,
307 text_input_layout_info: TextInputLayoutInfo,
308 text_input_style: TextInputStyle,
309 text_color: TextColor,
310 text_input_prompt: TextInputPrompt,
311 text_input_queue: TextInputQueue,
312 }
313}
314
315#[derive(Resource, Default)]
317pub struct ClearSelectionOnFocusChangeDisabled;
318
319fn clear_selection_on_focus_change(
320 input_focus: Res<InputFocus>,
321 mut text_input_pipeline: ResMut<TextInputPipeline>,
322 mut buffers: Query<&mut TextInputBuffer>,
323 mut previous_input_focus: Local<Option<Entity>>,
324) {
325 if *previous_input_focus != input_focus.0 {
326 if let Some(entity) = *previous_input_focus && let Ok(mut buffer) = buffers.get_mut(entity) {
327 buffer
328 .editor
329 .borrow_with(&mut text_input_pipeline.font_system)
330 .set_selection(Selection::None);
331 }
332 *previous_input_focus = input_focus.0;
333 }
334}
335
336pub(super) fn plugin(app: &mut App) {
337 app
338 .add_plugins(TextInputPlugin)
339 .add_systems(
340 Update,
341 (
342 on_change.run_if(any_with_component::<ListenToChanges>),
343 on_focus_changed.run_if(resource_changed_or_removed::<InputFocus>),
344 clear_selection_on_focus_change.run_if(not(resource_exists::<ClearSelectionOnFocusChangeDisabled>))
345 )
346 .run_if(any_with_component::<TextInputNode>),
347 );
348}