Skip to main content

fantasy_craft/gui/
gui_input_field.rs

1use hecs::Entity;
2use macroquad::prelude::*;
3use serde::Deserialize;
4use crate::{gui::{font_component::FontComponent, gui_box::GuiBox, resources::UiResolvedRects}, prelude::{ColorData, ComponentLoader, Context, Vec2Data, Visible}};
5
6#[derive(Debug, Clone)]
7pub struct GuiInputField {
8    pub text: String,
9    pub is_focused: bool,
10    pub caret_blink_timer: f32,
11    pub caret_visible: bool,
12    pub max_chars: Option<usize>,
13    pub font_size: f32,
14    pub color: Color,
15    pub backspace_repeat_timer: f32,
16    pub padding: Vec2,
17    pub caret_position: usize,
18    pub scroll_offset: f32,
19    pub left_key_repeat_timer: f32,
20    pub right_key_repeat_timer: f32
21}
22
23impl Default for GuiInputField {
24    fn default() -> Self {
25        Self {
26            text: String::new(),
27            is_focused: false,
28            caret_blink_timer: 0.0,
29            caret_visible: true,
30            max_chars: None,
31            font_size: 30.0,
32            color: BLACK,
33            backspace_repeat_timer: 0.0,
34            padding: vec2(0.0, 0.0),
35            caret_position: 0,
36            scroll_offset: 0.0,
37            left_key_repeat_timer: 0.0,
38            right_key_repeat_timer: 0.0
39        }
40    }
41}
42
43#[derive(Deserialize, Debug, Default)]
44pub struct GuiInputFieldLoaderData {
45    pub text: String,
46    pub is_focused: bool,
47    pub caret_blink_timer: f32,
48    pub caret_visible: bool,
49    pub max_chars: Option<usize>,
50    pub font_size: f32,
51    pub color: ColorData,
52    pub backspace_repeat_timer: f32,
53    pub padding: Vec2Data,
54    pub caret_position: usize,
55    pub scroll_offset: f32,
56    pub left_key_repeat_timer: f32,
57    pub right_key_repeat_timer: f32
58}
59
60pub struct GuiInputFieldLoader;
61
62impl ComponentLoader for GuiInputFieldLoader {
63    fn load(&self, ctx: &mut crate::prelude::Context, entity: hecs::Entity, data: &serde_json::Value) {
64        let loader_data: GuiInputFieldLoaderData = serde_json::from_value(data.clone())
65            .unwrap_or_default();
66
67        let component = GuiInputField {
68            text: loader_data.text,
69            is_focused: loader_data.is_focused,
70            caret_blink_timer: loader_data.caret_blink_timer,
71            caret_visible: loader_data.caret_visible,
72            max_chars: loader_data.max_chars,
73            font_size: loader_data.font_size,
74            color: Color::new(
75                loader_data.color.r,
76                loader_data.color.g,
77                loader_data.color.b,
78                loader_data.color.a
79            ),
80            backspace_repeat_timer: loader_data.backspace_repeat_timer,
81            padding: vec2(loader_data.padding.x, loader_data.padding.y),
82            caret_position: loader_data.caret_position,
83            scroll_offset: loader_data.scroll_offset,
84            left_key_repeat_timer: loader_data.left_key_repeat_timer,
85            right_key_repeat_timer: loader_data.right_key_repeat_timer
86        };
87
88        ctx.world.insert_one(entity, component).expect("Failed to insert GuiInputField");
89    }
90}
91
92pub fn input_field_focus_system(ctx: &mut Context) {
93    let (mouse_x, mouse_y) = mouse_position();
94    let is_pressed = is_mouse_button_pressed(MouseButton::Left);
95
96    if !is_pressed {
97        return;
98    }
99
100    let mut clicked_entity: Option<Entity> = None;
101    
102    // --- MODIFIED: Get map once ---
103    let resolved_rects_map = &ctx.resource::<UiResolvedRects>().0;
104
105    let mut query = ctx.world.query::<(&GuiBox, Option<&Visible>)>();
106
107    for (entity, (gui_box, visibility)) in query.iter() {
108        if ctx.world.get::<&GuiInputField>(entity).is_err() {
109            continue;
110        }
111
112        let is_visible = visibility.map_or(true, |v| v.0);
113        if !is_visible || !gui_box.screen_space {
114            continue;
115        }
116
117        // --- MODIFIED ---
118        let (resolved_pos, resolved_size) = 
119            if let Some(rect) = resolved_rects_map.get(&entity) {
120                *rect
121            } else {
122                continue;
123            };
124
125        let x = resolved_pos.x;
126        let y = resolved_pos.y;
127        let w = resolved_size.x;
128        let h = resolved_size.y;
129
130        let is_hovered = mouse_x >= x && mouse_x <= (x + w) && mouse_y >= y && mouse_y <= (y + h);
131
132        if is_hovered {
133            clicked_entity = Some(entity);
134            break;
135        }
136    }
137
138    // (Logic for setting focus is correct)
139    let mut query = ctx.world.query::<&mut GuiInputField>();
140    for (e, input_field) in query.iter() {
141        if Some(e) == clicked_entity {
142            if !input_field.is_focused {
143                while get_char_pressed().is_some() {}
144                input_field.caret_position = input_field.text.chars().count()
145            }
146
147            input_field.is_focused = true;
148            input_field.caret_visible = true;
149            input_field.caret_blink_timer = 0.0;
150        }
151        else {
152            input_field.is_focused = false;
153        }
154    }
155}
156
157pub fn input_field_typing_system(ctx: &mut Context) {
158    const KEY_REPEAT_INITIAL_DELAY: f32 = 0.4;
159    const KEY_REPEAT_RATE: f32 = 0.05;
160
161    // --- MODIFIED: Get dt once ---
162    let dt = ctx.dt();
163    
164    // --- MODIFIED: Get map once ---
165    let resolved_rects_map = &ctx.resource::<UiResolvedRects>().0;
166
167    let mut query = ctx.world.query::<(&mut GuiInputField, &GuiBox, Option<&FontComponent>)>();
168
169    for (entity, (input_field, _gui_box, font_opt)) in query.iter() {
170        if !input_field.is_focused {
171            input_field.backspace_repeat_timer = 0.0;
172            input_field.left_key_repeat_timer = 0.0;
173            input_field.right_key_repeat_timer = 0.0;
174            continue;
175        }
176        
177        // --- Left Arrow ---
178        let left_pressed = is_key_pressed(KeyCode::Left);
179        let left_down = is_key_down(KeyCode::Left);
180        let mut move_left = false;
181
182        if left_pressed {
183            move_left = true;
184            input_field.left_key_repeat_timer = KEY_REPEAT_INITIAL_DELAY;
185        }
186        else if left_down {
187            // --- MODIFIED ---
188            input_field.left_key_repeat_timer -= dt;
189            if input_field.left_key_repeat_timer <= 0.0 {
190                move_left = true;
191                input_field.left_key_repeat_timer = KEY_REPEAT_RATE;
192            }
193        }
194        else {
195            input_field.left_key_repeat_timer = 0.0;
196        }
197
198        if move_left && input_field.caret_position > 0 {
199            input_field.caret_position -= 1;
200            input_field.caret_visible = true;
201            input_field.caret_blink_timer = 0.0;
202        }
203
204        // --- Right Arrow ---
205        let right_pressed = is_key_pressed(KeyCode::Right);
206        let right_down = is_key_down(KeyCode::Right);
207        let mut move_right = false;
208
209        if right_pressed {
210            move_right = true;
211            input_field.right_key_repeat_timer = KEY_REPEAT_INITIAL_DELAY;
212        }
213        else if right_down {
214            // --- MODIFIED ---
215            input_field.right_key_repeat_timer -= dt;
216            if input_field.right_key_repeat_timer <= 0.0 {
217                move_right = true;
218                input_field.right_key_repeat_timer = KEY_REPEAT_RATE;
219            }
220        }
221        else {
222            input_field.right_key_repeat_timer = 0.0;
223        }
224
225        if move_right {
226            let text_len = input_field.text.chars().count();
227            if input_field.caret_position < text_len {
228                input_field.caret_position += 1;
229                input_field.caret_visible = true;
230                input_field.caret_blink_timer = 0.0;
231            }
232        }
233
234        // --- Backspace ---
235        let backspace_pressed = is_key_pressed(KeyCode::Backspace);
236        let backspace_down = is_key_down(KeyCode::Backspace);
237        
238        let mut should_delete = false;
239        if backspace_pressed {
240            should_delete = true;
241            input_field.backspace_repeat_timer = KEY_REPEAT_INITIAL_DELAY;
242        } else if backspace_down {
243            // --- MODIFIED ---
244            input_field.backspace_repeat_timer -= dt;
245            if input_field.backspace_repeat_timer <= 0.0 {
246                should_delete = true;
247                input_field.backspace_repeat_timer = KEY_REPEAT_RATE;
248            }
249        } else {
250            input_field.backspace_repeat_timer = 0.0;
251        }
252
253        if should_delete && input_field.caret_position > 0 {
254            let mut chars: Vec<char> = input_field.text.chars().collect();
255            if input_field.caret_position <= chars.len() {
256                chars.remove(input_field.caret_position - 1);
257                input_field.text = chars.into_iter().collect();
258                input_field.caret_position -= 1;
259                input_field.caret_visible = true;
260                input_field.caret_blink_timer = 0.0;
261            }
262        }
263        
264        // --- Delete Key ---
265        if is_key_pressed(KeyCode::Delete) {
266             let mut chars: Vec<char> = input_field.text.chars().collect();
267             if input_field.caret_position < chars.len() {
268                chars.remove(input_field.caret_position);
269                input_field.text = chars.into_iter().collect();
270                input_field.caret_visible = true;
271                input_field.caret_blink_timer = 0.0;
272             }
273        }
274
275        // --- Typing ---
276        while let Some(char) = get_char_pressed() {
277            if char == '\u{08}' || char == '\u{7f}' { // Backspace or Delete
278                continue; 
279            }
280
281            let char_count = input_field.text.chars().count();
282            let at_limit = input_field.max_chars.map_or(false, |max| char_count >= max);
283        
284            if !at_limit {
285                let mut chars: Vec<char> = input_field.text.chars().collect();
286                let insert_pos = input_field.caret_position.min(chars.len());
287                chars.insert(insert_pos, char);
288                input_field.text = chars.into_iter().collect();
289                
290                input_field.caret_position += 1;
291                input_field.caret_visible = true;
292                input_field.caret_blink_timer = 0.0;
293            }
294        }
295
296
297        // --- Scroll Logic ---
298        let font_to_use: Option<&Font> = font_opt.and_then(|f| ctx.asset_server.get_font(&f.0));
299
300        let text_before_caret: String = input_field.text.chars().take(input_field.caret_position).collect();
301        let caret_x_absolute = measure_text(&text_before_caret, font_to_use, input_field.font_size as u16, 1.0).width;
302
303        // --- MODIFIED ---
304        let w = if let Some((_, size)) = resolved_rects_map.get(&entity) {
305            size.x
306        } else {
307            300.0 // Fallback
308        };
309
310        let visible_width = w - (input_field.padding.x * 2.0);
311
312        // (Scroll logic is correct)
313        if caret_x_absolute < input_field.scroll_offset {
314            input_field.scroll_offset = caret_x_absolute;
315        }
316        if caret_x_absolute > input_field.scroll_offset + visible_width {
317            input_field.scroll_offset = caret_x_absolute - visible_width;
318        }
319        let total_text_width = measure_text(&input_field.text, font_to_use, input_field.font_size as u16, 1.0).width;
320        if total_text_width < visible_width {
321             input_field.scroll_offset = 0.0;
322        } else if total_text_width - input_field.scroll_offset < visible_width {
323             input_field.scroll_offset = (total_text_width - visible_width).max(0.0);
324        }
325
326        // --- Caret Blink ---
327        // --- MODIFIED ---
328        input_field.caret_blink_timer += dt;
329        if input_field.caret_blink_timer >= 0.5 {
330            input_field.caret_visible = !input_field.caret_visible;
331            input_field.caret_blink_timer = 0.0;
332        }
333    }
334}
335
336pub fn input_field_render_system(ctx: &mut Context) {
337    // --- MODIFIED: Get map once ---
338    let resolved_rects_map = &ctx.resource::<UiResolvedRects>().0;
339
340    let mut query = ctx.world.query::<(&GuiInputField, &GuiBox, Option<&Visible>, Option<&FontComponent>)>();
341
342    for (entity, (input_field, gui_box, visibility, font_opt)) in query.iter() {
343        let is_visible = visibility.map_or(true, |v| v.0);
344        if !is_visible { continue; }
345
346        if !gui_box.screen_space {
347            continue;
348        }
349
350        // --- MODIFIED ---
351        let (resolved_pos, resolved_size) = 
352            if let Some(rect) = resolved_rects_map.get(&entity) {
353                *rect
354            } else {
355                continue;
356            };
357
358        let x = resolved_pos.x;
359        let y = resolved_pos.y;
360        let w = resolved_size.x;
361        let h = resolved_size.y;
362
363        let rt_w = (w.max(1.0)) as u32;
364        let rt_h = (h.max(1.0)) as u32;
365        let rt = render_target(rt_w, rt_h);
366
367        let camera = Camera2D {
368            render_target: Some(rt.clone()),
369            viewport: None,
370            zoom: vec2(2.0 / rt_w as f32, 2.0 / rt_h as f32),
371            target: vec2(rt_w as f32 / 2.0, rt_h as f32 / 2.0),
372            ..Default::default()
373        };
374
375        set_camera(&camera);
376        clear_background(Color::new(0.0, 0.0, 0.0, 0.0));
377
378        let content_x = input_field.padding.x;
379        let text_y_top = (rt_h as f32 - input_field.font_size) / 2.0;
380        let baseline_y = text_y_top + input_field.font_size * 0.8; 
381        let draw_x = content_x - input_field.scroll_offset;
382
383        let font_to_use: Option<&Font> = font_opt.and_then(|f| ctx.asset_server.get_font(&f.0));
384
385        // (Text drawing logic is correct)
386        if let Some(font) = font_to_use {
387            draw_text_ex(
388                &input_field.text,
389                draw_x,
390                baseline_y,
391                TextParams {
392                    font: Some(font),
393                    font_size: input_field.font_size as u16,
394                    color: input_field.color,
395                    ..Default::default()
396                }
397            );
398        } else {
399            draw_text(
400                &input_field.text,
401                draw_x,
402                baseline_y,
403                input_field.font_size,
404                input_field.color
405            );
406        }
407
408        // (Caret drawing logic is correct)
409        if input_field.is_focused && input_field.caret_visible {
410            let text_before_caret: String = input_field.text.chars().take(input_field.caret_position).collect();
411            let caret_offset = measure_text(&text_before_caret, font_to_use, input_field.font_size as u16, 1.0).width;
412            let caret_x = draw_x + caret_offset;
413
414            draw_rectangle(
415                caret_x,
416                text_y_top,
417                2.0, 
418                input_field.font_size,
419                input_field.color
420            );
421        }
422
423        set_default_camera();
424
425        let draw_params = DrawTextureParams {
426            dest_size: Some(vec2(w, h)),
427            ..Default::default()
428        };
429
430        draw_texture_ex(&rt.texture, x, y, WHITE, draw_params);
431    }
432}