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}
28
29/// Builder with a single marker component
30pub struct TextInputBuilderWithMarker<M: Component> {
31    builder: TextInputBuilder,
32    marker: M,
33}
34
35/// Builder with two marker components
36pub struct TextInputBuilderWithTwoMarkers<M: Component, N: Component> {
37    builder: TextInputBuilder,
38    marker1: M,
39    marker2: N,
40}
41
42// Helper function to build text input with common components
43fn build_text_input_with_extras<M>(
44    parent: &mut ChildSpawnerCommands,
45    builder: TextInputBuilder,
46    extras: impl FnOnce(&mut EntityCommands) -> M,
47) -> Entity {
48    // If we need a clear button, create a container
49    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 components for layout
71                    Node {
72                        flex_grow: 1.0, // Take remaining space
73                        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(),  // Prevent cursor clipping
79                        ..default()
80                    },
81                    BackgroundColor(colors::BACKGROUND_LIGHT),
82                    BorderColor(colors::BORDER_DEFAULT),
83                    BorderRadius::all(Val::Px(5.0)),
84                    // Native text input components
85                    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,  // White cursor for maximum visibility
100                        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                    // Focus management
111                    builder.focus_type.clone(),
112                    // Make it a button so it can be clicked
113                    Button,
114                    // Add hover effects for better interactivity
115                    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                // Add extras from callback
124                extras(&mut entity_commands);
125
126                // Inactive state handled in TextInputSettings
127
128                // Add filter if specified
129                if let Some(filter) = builder.filter.clone() {
130                    entity_commands.insert(filter);
131                }
132
133                text_input_id = Some(entity_commands.id());
134
135                // Add clear button
136                let clear_button = ButtonBuilder::new("×")
137                    .style(ButtonStyle::Ghost)
138                    .size(ButtonSize::Small)
139                    .build(container);
140
141                // Add component to track which text input this button clears
142                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        // No clear button, build normally
153        let mut entity_commands = parent.spawn((
154            // Node components for layout
155            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(),  // Prevent cursor clipping
163                ..default()
164            },
165            BackgroundColor(colors::BACKGROUND_LIGHT),
166            BorderColor(colors::BORDER_DEFAULT),
167            BorderRadius::all(Val::Px(5.0)),
168            // Native text input components
169            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,  // White cursor for maximum visibility
184                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            // Focus management
195            builder.focus_type.clone(),
196            // Make it a button so it can be clicked
197            Button,
198            // Add hover effects for better interactivity
199            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        // Add extras from callback
208        extras(&mut entity_commands);
209
210        // Inactive state handled in TextInputSettings
211
212        // Add filter if specified
213        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    /// Set placeholder text (currently just sets initial value)
244    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    /// Set padding
265    pub fn with_padding(mut self, padding: UiRect) -> Self {
266        self.padding = padding;
267        self
268    }
269
270    /// Make this input part of an exclusive focus group
271    pub fn with_focus_group(mut self, group: FocusGroupId) -> Self {
272        self.focus_type = TextInputFocus::ExclusiveGroup(group);
273        self
274    }
275
276    /// Make this input independent (doesn't affect other inputs)
277    pub fn independent(mut self) -> Self {
278        self.focus_type = TextInputFocus::Independent;
279        self
280    }
281
282    /// Start with the input inactive (not focused)
283    pub fn inactive(mut self) -> Self {
284        self.inactive = true;
285        self
286    }
287
288    /// Set whether to retain text on submit
289    pub fn retain_on_submit(mut self, retain: bool) -> Self {
290        self.retain_on_submit = retain;
291        self
292    }
293
294    /// Set input filter for validation
295    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    /// Set maximum length for input
305    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    /// Set text transformation
319    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    /// Convenience method for numeric-only input (0-9)
333    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    /// Convenience method for integer input (with optional negative)
343    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    /// Convenience method for decimal input
353    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    /// Convenience method for alphabetic-only input
363    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    /// Convenience method for alphanumeric-only input
373    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    /// Build and spawn the text input entity
395    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    /// Build and spawn the text input entity with the marker
410    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    /// Build and spawn the text input entity with both markers
419    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
427/// Convenience function to create a text input builder
428pub fn text_input() -> TextInputBuilder {
429    TextInputBuilder::new()
430}