1use bevy::prelude::*;
4use crate::button::{ButtonBuilder, ButtonSize};
5use crate::styles::{colors, ButtonStyle};
6use crate::systems::hover::HoverColors;
7use super::types::*;
8use super::native_input::{
9 NativeTextInput, TextBuffer, TextInputVisual,
10 TextInputSettings, TabBehavior,
11};
12
13#[derive(Clone)]
15pub struct TextInputBuilder {
16 value: String,
17 placeholder: Option<String>,
18 font_size: f32,
19 width: Val,
20 height: Val,
21 padding: UiRect,
22 focus_type: TextInputFocus,
23 inactive: bool,
24 retain_on_submit: bool,
25 filter: Option<TextInputFilter>,
26 show_clear_button: bool,
27}
28
29pub struct TextInputBuilderWithMarker<M: Component> {
31 builder: TextInputBuilder,
32 marker: M,
33}
34
35pub struct TextInputBuilderWithTwoMarkers<M: Component, N: Component> {
37 builder: TextInputBuilder,
38 marker1: M,
39 marker2: N,
40}
41
42fn build_text_input_with_extras<M>(
44 parent: &mut ChildSpawnerCommands,
45 builder: TextInputBuilder,
46 extras: impl FnOnce(&mut EntityCommands) -> M,
47) -> Entity {
48 if builder.show_clear_button {
50 let container_id = parent
51 .spawn((
52 Node {
53 width: builder.width,
54 height: builder.height,
55 flex_direction: FlexDirection::Row,
56 column_gap: Val::Px(5.0),
57 ..default()
58 },
59 BackgroundColor(Color::NONE),
60 ))
61 .id();
62
63 let mut text_input_id = None;
64
65 parent
66 .commands()
67 .entity(container_id)
68 .with_children(|container| {
69 let mut entity_commands = container.spawn((
70 Node {
72 flex_grow: 1.0, height: Val::Percent(100.0),
74 padding: builder.padding,
75 border: UiRect::all(Val::Px(2.0)),
76 justify_content: JustifyContent::Start,
77 align_items: AlignItems::Center,
78 overflow: Overflow::visible(), ..default()
80 },
81 BackgroundColor(colors::BACKGROUND_LIGHT),
82 BorderColor(colors::BORDER_DEFAULT),
83 BorderRadius::all(Val::Px(5.0)),
84 NativeTextInput,
86 TextBuffer {
87 content: builder.value.clone(),
88 cursor_pos: builder.value.chars().count(),
89 is_focused: false,
90 },
91 TextInputVisual {
92 font: TextFont {
93 font_size: builder.font_size,
94 ..default()
95 },
96 text_color: colors::TEXT_PRIMARY,
97 placeholder: builder.placeholder.clone().unwrap_or_default(),
98 placeholder_color: colors::TEXT_MUTED,
99 cursor_color: Color::WHITE, selection_color: colors::PRIMARY.with_alpha(0.3),
101 mask_char: None,
102 },
103 TextInputSettings {
104 multiline: false,
105 max_length: builder.filter.as_ref().and_then(|f| f.max_length),
106 retain_on_submit: builder.retain_on_submit,
107 read_only: builder.inactive,
108 tab_behavior: TabBehavior::NextField,
109 },
110 builder.focus_type.clone(),
112 Button,
114 HoverColors {
116 normal_bg: colors::BACKGROUND_LIGHT,
117 hover_bg: colors::BACKGROUND_MEDIUM,
118 normal_border: colors::BORDER_DEFAULT,
119 hover_border: colors::BORDER_FOCUS,
120 },
121 ));
122
123 extras(&mut entity_commands);
125
126 if let Some(filter) = builder.filter.clone() {
130 entity_commands.insert(filter);
131 }
132
133 text_input_id = Some(entity_commands.id());
134
135 let clear_button = ButtonBuilder::new("×")
137 .style(ButtonStyle::Ghost)
138 .size(ButtonSize::Small)
139 .build(container);
140
141 if let Some(input_id) = text_input_id {
143 container
144 .commands()
145 .entity(clear_button)
146 .insert(ClearButtonTarget(input_id));
147 }
148 });
149
150 container_id
151 } else {
152 let mut entity_commands = parent.spawn((
154 Node {
156 width: builder.width,
157 height: builder.height,
158 padding: builder.padding,
159 border: UiRect::all(Val::Px(2.0)),
160 justify_content: JustifyContent::Start,
161 align_items: AlignItems::Center,
162 overflow: Overflow::visible(), ..default()
164 },
165 BackgroundColor(colors::BACKGROUND_LIGHT),
166 BorderColor(colors::BORDER_DEFAULT),
167 BorderRadius::all(Val::Px(5.0)),
168 NativeTextInput,
170 TextBuffer {
171 content: builder.value.clone(),
172 cursor_pos: builder.value.chars().count(),
173 is_focused: false,
174 },
175 TextInputVisual {
176 font: TextFont {
177 font_size: builder.font_size,
178 ..default()
179 },
180 text_color: colors::TEXT_PRIMARY,
181 placeholder: builder.placeholder.unwrap_or_default(),
182 placeholder_color: colors::TEXT_MUTED,
183 cursor_color: Color::WHITE, selection_color: colors::PRIMARY.with_alpha(0.3),
185 mask_char: None,
186 },
187 TextInputSettings {
188 multiline: false,
189 max_length: builder.filter.as_ref().and_then(|f| f.max_length),
190 retain_on_submit: builder.retain_on_submit,
191 read_only: builder.inactive,
192 tab_behavior: TabBehavior::NextField,
193 },
194 builder.focus_type.clone(),
196 Button,
198 HoverColors {
200 normal_bg: colors::BACKGROUND_LIGHT,
201 hover_bg: colors::BACKGROUND_MEDIUM,
202 normal_border: colors::BORDER_DEFAULT,
203 hover_border: colors::BORDER_FOCUS,
204 },
205 ));
206
207 extras(&mut entity_commands);
209
210 if let Some(filter) = builder.filter.clone() {
214 entity_commands.insert(filter);
215 }
216
217 entity_commands.id()
218 }
219}
220
221impl TextInputBuilder {
222 pub fn new() -> Self {
223 Self {
224 value: String::new(),
225 placeholder: None,
226 font_size: 16.0,
227 width: Val::Px(300.0),
228 height: Val::Px(40.0),
229 padding: UiRect::all(Val::Px(10.0)),
230 focus_type: TextInputFocus::Independent,
231 inactive: false,
232 retain_on_submit: true,
233 filter: None,
234 show_clear_button: false,
235 }
236 }
237
238 pub fn with_value(mut self, value: impl Into<String>) -> Self {
239 self.value = value.into();
240 self
241 }
242
243 pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
245 self.placeholder = Some(placeholder.into());
246 self
247 }
248
249 pub fn with_font_size(mut self, size: f32) -> Self {
250 self.font_size = size;
251 self
252 }
253
254 pub fn with_width(mut self, width: Val) -> Self {
255 self.width = width;
256 self
257 }
258
259 pub fn with_height(mut self, height: Val) -> Self {
260 self.height = height;
261 self
262 }
263
264 pub fn with_padding(mut self, padding: UiRect) -> Self {
266 self.padding = padding;
267 self
268 }
269
270 pub fn with_focus_group(mut self, group: FocusGroupId) -> Self {
272 self.focus_type = TextInputFocus::ExclusiveGroup(group);
273 self
274 }
275
276 pub fn independent(mut self) -> Self {
278 self.focus_type = TextInputFocus::Independent;
279 self
280 }
281
282 pub fn inactive(mut self) -> Self {
284 self.inactive = true;
285 self
286 }
287
288 pub fn retain_on_submit(mut self, retain: bool) -> Self {
290 self.retain_on_submit = retain;
291 self
292 }
293
294 pub fn with_filter(mut self, filter_type: InputFilter) -> Self {
296 self.filter = Some(TextInputFilter {
297 filter_type,
298 max_length: None,
299 transform: InputTransform::None,
300 });
301 self
302 }
303
304 pub fn with_max_length(mut self, max_length: usize) -> Self {
306 if let Some(ref mut filter) = self.filter {
307 filter.max_length = Some(max_length);
308 } else {
309 self.filter = Some(TextInputFilter {
310 filter_type: InputFilter::None,
311 max_length: Some(max_length),
312 transform: InputTransform::None,
313 });
314 }
315 self
316 }
317
318 pub fn with_transform(mut self, transform: InputTransform) -> Self {
320 if let Some(ref mut filter) = self.filter {
321 filter.transform = transform;
322 } else {
323 self.filter = Some(TextInputFilter {
324 filter_type: InputFilter::None,
325 max_length: None,
326 transform,
327 });
328 }
329 self
330 }
331
332 pub fn numeric_only(mut self) -> Self {
334 self.filter = Some(TextInputFilter {
335 filter_type: InputFilter::Numeric,
336 max_length: None,
337 transform: InputTransform::None,
338 });
339 self
340 }
341
342 pub fn integer_only(mut self) -> Self {
344 self.filter = Some(TextInputFilter {
345 filter_type: InputFilter::Integer,
346 max_length: None,
347 transform: InputTransform::None,
348 });
349 self
350 }
351
352 pub fn decimal_only(mut self) -> Self {
354 self.filter = Some(TextInputFilter {
355 filter_type: InputFilter::Decimal,
356 max_length: None,
357 transform: InputTransform::None,
358 });
359 self
360 }
361
362 pub fn alphabetic_only(mut self) -> Self {
364 self.filter = Some(TextInputFilter {
365 filter_type: InputFilter::Alphabetic,
366 max_length: None,
367 transform: InputTransform::None,
368 });
369 self
370 }
371
372 pub fn alphanumeric_only(mut self) -> Self {
374 self.filter = Some(TextInputFilter {
375 filter_type: InputFilter::Alphanumeric,
376 max_length: None,
377 transform: InputTransform::None,
378 });
379 self
380 }
381
382 pub fn with_clear_button(mut self) -> Self {
383 self.show_clear_button = true;
384 self
385 }
386
387 pub fn with_marker<M: Component>(self, marker: M) -> TextInputBuilderWithMarker<M> {
388 TextInputBuilderWithMarker {
389 builder: self,
390 marker,
391 }
392 }
393
394 pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
396 build_text_input_with_extras(parent, self, |_entity| {})
397 }
398}
399
400impl<M: Component> TextInputBuilderWithMarker<M> {
401 pub fn and_marker<N: Component>(self, marker2: N) -> TextInputBuilderWithTwoMarkers<M, N> {
402 TextInputBuilderWithTwoMarkers {
403 builder: self.builder,
404 marker1: self.marker,
405 marker2,
406 }
407 }
408
409 pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
411 build_text_input_with_extras(parent, self.builder, |entity| {
412 entity.insert(self.marker);
413 })
414 }
415}
416
417impl<M: Component, N: Component> TextInputBuilderWithTwoMarkers<M, N> {
418 pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
420 build_text_input_with_extras(parent, self.builder, |entity| {
421 entity.insert(self.marker1);
422 entity.insert(self.marker2);
423 })
424 }
425}
426
427pub fn text_input() -> TextInputBuilder {
429 TextInputBuilder::new()
430}