bevy_ui_builders/text_input/
builder.rs

1//! TextInputBuilder implementation
2
3use 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/// Builder for creating text inputs with managed focus
14#[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
30/// Builder with a single marker component
31pub struct TextInputBuilderWithMarker<M: Component> {
32    builder: TextInputBuilder,
33    marker: M,
34}
35
36/// Builder with two marker components
37pub struct TextInputBuilderWithTwoMarkers<M: Component, N: Component> {
38    builder: TextInputBuilder,
39    marker1: M,
40    marker2: N,
41}
42
43// Helper function to build text input with common components
44fn build_text_input_with_extras<M>(
45    parent: &mut ChildSpawnerCommands,
46    builder: TextInputBuilder,
47    extras: impl FnOnce(&mut EntityCommands) -> M,
48) -> Entity {
49    // If we need a clear button, create a container
50    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 components for layout
72                    Node {
73                        flex_grow: 1.0, // Take remaining space
74                        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(),  // Prevent cursor clipping
80                        ..default()
81                    },
82                    BackgroundColor(colors::BACKGROUND_LIGHT),
83                    BorderColor::all(colors::BORDER_DEFAULT),
84                    BorderRadius::all(Val::Px(5.0)),
85                    // Native text input components
86                    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,  // White cursor for maximum visibility
101                        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                    // Focus management
112                    builder.focus_type.clone(),
113                    // Make it a button so it can be clicked
114                    Button,
115                    // Add hover effects for better interactivity
116                    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                // Add extras from callback
125                extras(&mut entity_commands);
126
127                // Inactive state handled in TextInputSettings
128
129                // Add filter if specified
130                if let Some(filter) = builder.filter.clone() {
131                    entity_commands.insert(filter);
132                }
133
134                text_input_id = Some(entity_commands.id());
135
136                // Add clear button
137                let clear_button = ButtonBuilder::new("×")
138                    .style(ButtonStyle::Ghost)
139                    .size(ButtonSize::Small)
140                    .build(container);
141
142                // Add component to track which text input this button clears
143                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        // No clear button, build normally
154        let mut entity_commands = parent.spawn((
155            // Node components for layout
156            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(),  // Prevent cursor clipping
164                ..default()
165            },
166            BackgroundColor(colors::BACKGROUND_LIGHT),
167            BorderColor::all(colors::BORDER_DEFAULT),
168            BorderRadius::all(Val::Px(5.0)),
169            // Native text input components
170            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,  // White cursor for maximum visibility
185                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            // Focus management
196            builder.focus_type.clone(),
197            // Make it a button so it can be clicked
198            Button,
199            // Add hover effects for better interactivity
200            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        // Add extras from callback
209        extras(&mut entity_commands);
210
211        // Inactive state handled in TextInputSettings
212
213        // Add filter if specified
214        if let Some(filter) = builder.filter.clone() {
215            entity_commands.insert(filter);
216        }
217
218        // Add validation if specified
219        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    /// Set placeholder text (currently just sets initial value)
254    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    /// Set padding
275    pub fn with_padding(mut self, padding: UiRect) -> Self {
276        self.padding = padding;
277        self
278    }
279
280    /// Make this input part of an exclusive focus group
281    pub fn with_focus_group(mut self, group: FocusGroupId) -> Self {
282        self.focus_type = TextInputFocus::ExclusiveGroup(group);
283        self
284    }
285
286    /// Make this input independent (doesn't affect other inputs)
287    pub fn independent(mut self) -> Self {
288        self.focus_type = TextInputFocus::Independent;
289        self
290    }
291
292    /// Start with the input inactive (not focused)
293    pub fn inactive(mut self) -> Self {
294        self.inactive = true;
295        self
296    }
297
298    /// Set whether to retain text on submit
299    pub fn retain_on_submit(mut self, retain: bool) -> Self {
300        self.retain_on_submit = retain;
301        self
302    }
303
304    /// Set input filter for validation
305    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    /// Set maximum length for input
315    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    /// Set text transformation
329    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    /// Convenience method for numeric-only input (0-9)
343    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    /// Convenience method for integer input (with optional negative)
353    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    /// Convenience method for decimal input
363    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    /// Convenience method for alphabetic-only input
373    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    /// Convenience method for alphanumeric-only input
383    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    /// Add validation rules to this text input
398    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    /// Build and spawn the text input entity
411    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    /// Build and spawn the text input entity with the marker
426    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    /// Build and spawn the text input entity with both markers
435    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
443/// Convenience function to create a text input builder
444pub fn text_input() -> TextInputBuilder {
445    TextInputBuilder::new()
446}