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 validation_rules: Option<Vec<crate::ValidationRule>>,
28}
29
30pub struct TextInputBuilderWithMarker<M: Component> {
32 builder: TextInputBuilder,
33 marker: M,
34}
35
36pub struct TextInputBuilderWithTwoMarkers<M: Component, N: Component> {
38 builder: TextInputBuilder,
39 marker1: M,
40 marker2: N,
41}
42
43fn build_text_input_with_extras<M>(
45 parent: &mut ChildSpawnerCommands,
46 builder: TextInputBuilder,
47 extras: impl FnOnce(&mut EntityCommands) -> M,
48) -> Entity {
49 if builder.show_clear_button {
51 let container_id = parent
52 .spawn((
53 Node {
54 width: builder.width,
55 height: builder.height,
56 flex_direction: FlexDirection::Row,
57 column_gap: Val::Px(5.0),
58 ..default()
59 },
60 BackgroundColor(Color::NONE),
61 ))
62 .id();
63
64 let mut text_input_id = None;
65
66 parent
67 .commands()
68 .entity(container_id)
69 .with_children(|container| {
70 let mut entity_commands = container.spawn((
71 Node {
73 flex_grow: 1.0, height: Val::Percent(100.0),
75 padding: builder.padding,
76 border: UiRect::all(Val::Px(2.0)),
77 justify_content: JustifyContent::Start,
78 align_items: AlignItems::Center,
79 overflow: Overflow::visible(), ..default()
81 },
82 BackgroundColor(colors::BACKGROUND_LIGHT),
83 BorderColor::all(colors::BORDER_DEFAULT),
84 BorderRadius::all(Val::Px(5.0)),
85 NativeTextInput,
87 TextBuffer {
88 content: builder.value.clone(),
89 cursor_pos: builder.value.chars().count(),
90 is_focused: false,
91 },
92 TextInputVisual {
93 font: TextFont {
94 font_size: builder.font_size,
95 ..default()
96 },
97 text_color: colors::TEXT_PRIMARY,
98 placeholder: builder.placeholder.clone().unwrap_or_default(),
99 placeholder_color: colors::TEXT_MUTED,
100 cursor_color: Color::WHITE, selection_color: colors::PRIMARY.with_alpha(0.3),
102 mask_char: None,
103 },
104 TextInputSettings {
105 multiline: false,
106 max_length: builder.filter.as_ref().and_then(|f| f.max_length),
107 retain_on_submit: builder.retain_on_submit,
108 read_only: builder.inactive,
109 tab_behavior: TabBehavior::NextField,
110 },
111 builder.focus_type.clone(),
113 Button,
115 HoverColors {
117 normal_bg: colors::BACKGROUND_LIGHT,
118 hover_bg: colors::BACKGROUND_MEDIUM,
119 normal_border: colors::BORDER_DEFAULT,
120 hover_border: colors::BORDER_FOCUS,
121 },
122 ));
123
124 extras(&mut entity_commands);
126
127 if let Some(filter) = builder.filter.clone() {
131 entity_commands.insert(filter);
132 }
133
134 text_input_id = Some(entity_commands.id());
135
136 let clear_button = ButtonBuilder::new("×")
138 .style(ButtonStyle::Ghost)
139 .size(ButtonSize::Small)
140 .build(container);
141
142 if let Some(input_id) = text_input_id {
144 container
145 .commands()
146 .entity(clear_button)
147 .insert(ClearButtonTarget(input_id));
148 }
149 });
150
151 container_id
152 } else {
153 let mut entity_commands = parent.spawn((
155 Node {
157 width: builder.width,
158 height: builder.height,
159 padding: builder.padding,
160 border: UiRect::all(Val::Px(2.0)),
161 justify_content: JustifyContent::Start,
162 align_items: AlignItems::Center,
163 overflow: Overflow::visible(), ..default()
165 },
166 BackgroundColor(colors::BACKGROUND_LIGHT),
167 BorderColor::all(colors::BORDER_DEFAULT),
168 BorderRadius::all(Val::Px(5.0)),
169 NativeTextInput,
171 TextBuffer {
172 content: builder.value.clone(),
173 cursor_pos: builder.value.chars().count(),
174 is_focused: false,
175 },
176 TextInputVisual {
177 font: TextFont {
178 font_size: builder.font_size,
179 ..default()
180 },
181 text_color: colors::TEXT_PRIMARY,
182 placeholder: builder.placeholder.unwrap_or_default(),
183 placeholder_color: colors::TEXT_MUTED,
184 cursor_color: Color::WHITE, selection_color: colors::PRIMARY.with_alpha(0.3),
186 mask_char: None,
187 },
188 TextInputSettings {
189 multiline: false,
190 max_length: builder.filter.as_ref().and_then(|f| f.max_length),
191 retain_on_submit: builder.retain_on_submit,
192 read_only: builder.inactive,
193 tab_behavior: TabBehavior::NextField,
194 },
195 builder.focus_type.clone(),
197 Button,
199 HoverColors {
201 normal_bg: colors::BACKGROUND_LIGHT,
202 hover_bg: colors::BACKGROUND_MEDIUM,
203 normal_border: colors::BORDER_DEFAULT,
204 hover_border: colors::BORDER_FOCUS,
205 },
206 ));
207
208 extras(&mut entity_commands);
210
211 if let Some(filter) = builder.filter.clone() {
215 entity_commands.insert(filter);
216 }
217
218 if let Some(rules) = builder.validation_rules {
220 entity_commands.insert((
221 crate::validation::Validated::new(rules),
222 crate::validation::ValidationState::default(),
223 ));
224 }
225
226 entity_commands.id()
227 }
228}
229
230impl TextInputBuilder {
231 pub fn new() -> Self {
232 Self {
233 value: String::new(),
234 placeholder: None,
235 font_size: 16.0,
236 width: Val::Px(300.0),
237 height: Val::Px(40.0),
238 padding: UiRect::all(Val::Px(10.0)),
239 focus_type: TextInputFocus::Independent,
240 inactive: false,
241 retain_on_submit: true,
242 filter: None,
243 show_clear_button: false,
244 validation_rules: None,
245 }
246 }
247
248 pub fn with_value(mut self, value: impl Into<String>) -> Self {
249 self.value = value.into();
250 self
251 }
252
253 pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
255 self.placeholder = Some(placeholder.into());
256 self
257 }
258
259 pub fn with_font_size(mut self, size: f32) -> Self {
260 self.font_size = size;
261 self
262 }
263
264 pub fn with_width(mut self, width: Val) -> Self {
265 self.width = width;
266 self
267 }
268
269 pub fn with_height(mut self, height: Val) -> Self {
270 self.height = height;
271 self
272 }
273
274 pub fn with_padding(mut self, padding: UiRect) -> Self {
276 self.padding = padding;
277 self
278 }
279
280 pub fn with_focus_group(mut self, group: FocusGroupId) -> Self {
282 self.focus_type = TextInputFocus::ExclusiveGroup(group);
283 self
284 }
285
286 pub fn independent(mut self) -> Self {
288 self.focus_type = TextInputFocus::Independent;
289 self
290 }
291
292 pub fn inactive(mut self) -> Self {
294 self.inactive = true;
295 self
296 }
297
298 pub fn retain_on_submit(mut self, retain: bool) -> Self {
300 self.retain_on_submit = retain;
301 self
302 }
303
304 pub fn with_filter(mut self, filter_type: InputFilter) -> Self {
306 self.filter = Some(TextInputFilter {
307 filter_type,
308 max_length: None,
309 transform: InputTransform::None,
310 });
311 self
312 }
313
314 pub fn with_max_length(mut self, max_length: usize) -> Self {
316 if let Some(ref mut filter) = self.filter {
317 filter.max_length = Some(max_length);
318 } else {
319 self.filter = Some(TextInputFilter {
320 filter_type: InputFilter::None,
321 max_length: Some(max_length),
322 transform: InputTransform::None,
323 });
324 }
325 self
326 }
327
328 pub fn with_transform(mut self, transform: InputTransform) -> Self {
330 if let Some(ref mut filter) = self.filter {
331 filter.transform = transform;
332 } else {
333 self.filter = Some(TextInputFilter {
334 filter_type: InputFilter::None,
335 max_length: None,
336 transform,
337 });
338 }
339 self
340 }
341
342 pub fn numeric_only(mut self) -> Self {
344 self.filter = Some(TextInputFilter {
345 filter_type: InputFilter::Numeric,
346 max_length: None,
347 transform: InputTransform::None,
348 });
349 self
350 }
351
352 pub fn integer_only(mut self) -> Self {
354 self.filter = Some(TextInputFilter {
355 filter_type: InputFilter::Integer,
356 max_length: None,
357 transform: InputTransform::None,
358 });
359 self
360 }
361
362 pub fn decimal_only(mut self) -> Self {
364 self.filter = Some(TextInputFilter {
365 filter_type: InputFilter::Decimal,
366 max_length: None,
367 transform: InputTransform::None,
368 });
369 self
370 }
371
372 pub fn alphabetic_only(mut self) -> Self {
374 self.filter = Some(TextInputFilter {
375 filter_type: InputFilter::Alphabetic,
376 max_length: None,
377 transform: InputTransform::None,
378 });
379 self
380 }
381
382 pub fn alphanumeric_only(mut self) -> Self {
384 self.filter = Some(TextInputFilter {
385 filter_type: InputFilter::Alphanumeric,
386 max_length: None,
387 transform: InputTransform::None,
388 });
389 self
390 }
391
392 pub fn with_clear_button(mut self) -> Self {
393 self.show_clear_button = true;
394 self
395 }
396
397 pub fn with_validation(mut self, rules: Vec<crate::ValidationRule>) -> Self {
399 self.validation_rules = Some(rules);
400 self
401 }
402
403 pub fn with_marker<M: Component>(self, marker: M) -> TextInputBuilderWithMarker<M> {
404 TextInputBuilderWithMarker {
405 builder: self,
406 marker,
407 }
408 }
409
410 pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
412 build_text_input_with_extras(parent, self, |_entity| {})
413 }
414}
415
416impl<M: Component> TextInputBuilderWithMarker<M> {
417 pub fn and_marker<N: Component>(self, marker2: N) -> TextInputBuilderWithTwoMarkers<M, N> {
418 TextInputBuilderWithTwoMarkers {
419 builder: self.builder,
420 marker1: self.marker,
421 marker2,
422 }
423 }
424
425 pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
427 build_text_input_with_extras(parent, self.builder, |entity| {
428 entity.insert(self.marker);
429 })
430 }
431}
432
433impl<M: Component, N: Component> TextInputBuilderWithTwoMarkers<M, N> {
434 pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
436 build_text_input_with_extras(parent, self.builder, |entity| {
437 entity.insert(self.marker1);
438 entity.insert(self.marker2);
439 })
440 }
441}
442
443pub fn text_input() -> TextInputBuilder {
445 TextInputBuilder::new()
446}