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 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 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 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 let dt = ctx.dt();
163
164 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 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 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 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 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 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 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 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 while let Some(char) = get_char_pressed() {
277 if char == '\u{08}' || char == '\u{7f}' { 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 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 let w = if let Some((_, size)) = resolved_rects_map.get(&entity) {
305 size.x
306 } else {
307 300.0 };
309
310 let visible_width = w - (input_field.padding.x * 2.0);
311
312 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 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 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 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 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 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}