1use jag_draw::{Brush, Color, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
4use jag_surface::Canvas;
5
6use crate::event::{
7 ElementState, EventHandler, EventResult, KeyCode, KeyboardEvent, MouseButton, MouseClickEvent,
8 MouseMoveEvent, ScrollEvent,
9};
10use crate::focus::FocusId;
11
12use super::Element;
13use super::text_align::TextAlign;
14
15pub struct InputBox {
21 pub rect: Rect,
23 pub text: String,
25 pub text_size: f32,
27 pub text_color: ColorLinPremul,
29 pub placeholder: Option<String>,
31 pub text_align: TextAlign,
33 pub focused: bool,
35 pub disabled: bool,
37 pub bg_color: ColorLinPremul,
39 pub border_color: ColorLinPremul,
41 pub border_width: f32,
43 pub corner_radius: f32,
45 pub input_type: String,
47 pub validation_error: Option<String>,
49 pub cursor_position: usize,
51 scroll_x: f32,
53 padding_x: f32,
55 padding_y: f32,
57 pub focus_id: FocusId,
59}
60
61impl InputBox {
62 pub fn new(rect: Rect) -> Self {
64 Self {
65 rect,
66 text: String::new(),
67 text_size: 14.0,
68 text_color: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
69 placeholder: None,
70 text_align: TextAlign::Left,
71 focused: false,
72 disabled: false,
73 bg_color: ColorLinPremul::from_srgba_u8([40, 40, 40, 255]),
74 border_color: ColorLinPremul::from_srgba_u8([80, 80, 80, 255]),
75 border_width: 1.0,
76 corner_radius: 4.0,
77 input_type: "text".to_string(),
78 validation_error: None,
79 cursor_position: 0,
80 scroll_x: 0.0,
81 padding_x: 8.0,
82 padding_y: 4.0,
83 focus_id: FocusId(0),
84 }
85 }
86
87 pub fn text(&self) -> &str {
89 &self.text
90 }
91
92 pub fn set_text(&mut self, text: impl Into<String>) {
94 self.text = text.into();
95 self.cursor_position = self.text.len();
96 }
97
98 pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
100 self.placeholder = Some(placeholder.into());
101 }
102
103 pub fn insert_text(&mut self, s: &str) {
105 if self.disabled {
106 return;
107 }
108 self.text.insert_str(self.cursor_position, s);
109 self.cursor_position += s.len();
110 }
111
112 pub fn delete_char_before(&mut self) {
114 if self.disabled || self.cursor_position == 0 {
115 return;
116 }
117 let prev = self.text[..self.cursor_position]
119 .char_indices()
120 .next_back()
121 .map(|(i, _)| i)
122 .unwrap_or(0);
123 self.text.drain(prev..self.cursor_position);
124 self.cursor_position = prev;
125 }
126
127 pub fn delete_char_after(&mut self) {
129 if self.disabled || self.cursor_position >= self.text.len() {
130 return;
131 }
132 let next = self.text[self.cursor_position..]
133 .char_indices()
134 .nth(1)
135 .map(|(i, _)| self.cursor_position + i)
136 .unwrap_or(self.text.len());
137 self.text.drain(self.cursor_position..next);
138 }
139
140 pub fn move_cursor_left(&mut self) {
142 if self.cursor_position > 0 {
143 self.cursor_position = self.text[..self.cursor_position]
144 .char_indices()
145 .next_back()
146 .map(|(i, _)| i)
147 .unwrap_or(0);
148 }
149 }
150
151 pub fn move_cursor_right(&mut self) {
153 if self.cursor_position < self.text.len() {
154 self.cursor_position = self.text[self.cursor_position..]
155 .char_indices()
156 .nth(1)
157 .map(|(i, _)| self.cursor_position + i)
158 .unwrap_or(self.text.len());
159 }
160 }
161
162 pub fn move_cursor_home(&mut self) {
164 self.cursor_position = 0;
165 }
166
167 pub fn move_cursor_end(&mut self) {
169 self.cursor_position = self.text.len();
170 }
171
172 fn display_text(&self) -> String {
174 if self.input_type == "password" {
175 "\u{2022}".repeat(self.text.chars().count())
176 } else {
177 self.text.clone()
178 }
179 }
180
181 pub fn hit_test(&self, x: f32, y: f32) -> bool {
183 x >= self.rect.x
184 && x <= self.rect.x + self.rect.w
185 && y >= self.rect.y
186 && y <= self.rect.y + self.rect.h
187 }
188}
189
190impl Default for InputBox {
191 fn default() -> Self {
192 Self::new(Rect {
193 x: 0.0,
194 y: 0.0,
195 w: 200.0,
196 h: 32.0,
197 })
198 }
199}
200
201impl Element for InputBox {
206 fn rect(&self) -> Rect {
207 self.rect
208 }
209
210 fn set_rect(&mut self, rect: Rect) {
211 self.rect = rect;
212 }
213
214 fn render(&self, canvas: &mut Canvas, z: i32) {
215 let rrect = RoundedRect {
217 rect: self.rect,
218 radii: RoundedRadii {
219 tl: self.corner_radius,
220 tr: self.corner_radius,
221 br: self.corner_radius,
222 bl: self.corner_radius,
223 },
224 };
225
226 let has_error = self.validation_error.is_some();
227 let border_color = if has_error {
228 Color::rgba(220, 38, 38, 255)
229 } else if self.focused {
230 Color::rgba(63, 130, 246, 255)
231 } else {
232 self.border_color
233 };
234 let border_width = if has_error {
235 self.border_width.max(2.0)
236 } else if self.focused {
237 (self.border_width + 1.0).max(2.0)
238 } else {
239 self.border_width
240 };
241
242 jag_surface::shapes::draw_snapped_rounded_rectangle(
243 canvas,
244 rrect,
245 Some(Brush::Solid(self.bg_color)),
246 Some(border_width),
247 Some(Brush::Solid(border_color)),
248 z,
249 );
250
251 let content_x = self.rect.x + self.padding_x;
253 let content_h = (self.rect.h - self.padding_y * 2.0).max(0.0);
254 let baseline_y = self.rect.y + self.padding_y + content_h * 0.5 + self.text_size * 0.35;
255
256 if self.text.is_empty() {
257 if let Some(ref ph) = self.placeholder {
259 let ph_color = ColorLinPremul::from_srgba_u8([160, 160, 160, 255]);
260 canvas.draw_text_run_weighted(
261 [content_x, baseline_y],
262 ph.clone(),
263 self.text_size,
264 400.0,
265 ph_color,
266 z + 1,
267 );
268 }
269 } else {
270 let display = self.display_text();
272 let text_x = content_x - self.scroll_x;
273 canvas.draw_text_run_weighted(
274 [text_x, baseline_y],
275 display,
276 self.text_size,
277 400.0,
278 self.text_color,
279 z + 1,
280 );
281 }
282
283 if self.focused {
285 let display = self.display_text();
286 let cursor_text = if self.cursor_position <= display.len() {
287 &display[..self.cursor_position]
288 } else {
289 &display
290 };
291 let cursor_offset = canvas.measure_text_width(cursor_text, self.text_size);
292 let cursor_x = content_x + cursor_offset - self.scroll_x;
293 let cursor_y = self.rect.y + self.padding_y + 2.0;
294 let cursor_h = content_h - 4.0;
295
296 canvas.fill_rect(
297 cursor_x,
298 cursor_y,
299 1.5,
300 cursor_h.max(0.0),
301 Brush::Solid(self.text_color),
302 z + 2,
303 );
304 }
305
306 if let Some(ref error_msg) = self.validation_error {
308 let error_size = (self.text_size * 0.85).max(12.0);
309 let baseline_offset = error_size * 0.8;
310 let top_gap = 3.0;
311 let error_y = self.rect.y + self.rect.h + top_gap + baseline_offset;
312 let error_color = ColorLinPremul::from_srgba_u8([220, 38, 38, 255]);
313
314 canvas.draw_text_run_weighted(
315 [self.rect.x + self.padding_x, error_y],
316 error_msg.clone(),
317 error_size,
318 400.0,
319 error_color,
320 z + 3,
321 );
322 }
323 }
324
325 fn focus_id(&self) -> Option<FocusId> {
326 Some(self.focus_id)
327 }
328}
329
330impl EventHandler for InputBox {
335 fn handle_mouse_click(&mut self, event: &MouseClickEvent) -> EventResult {
336 if event.button != MouseButton::Left || event.state != ElementState::Pressed {
337 return EventResult::Ignored;
338 }
339 if self.hit_test(event.x, event.y) {
340 EventResult::Handled
341 } else {
342 EventResult::Ignored
343 }
344 }
345
346 fn handle_keyboard(&mut self, event: &KeyboardEvent) -> EventResult {
347 if event.state != ElementState::Pressed || !self.focused || self.disabled {
348 return EventResult::Ignored;
349 }
350 match event.key {
351 KeyCode::Backspace => {
352 self.delete_char_before();
353 EventResult::Handled
354 }
355 KeyCode::Delete => {
356 self.delete_char_after();
357 EventResult::Handled
358 }
359 KeyCode::ArrowLeft => {
360 self.move_cursor_left();
361 EventResult::Handled
362 }
363 KeyCode::ArrowRight => {
364 self.move_cursor_right();
365 EventResult::Handled
366 }
367 KeyCode::Home => {
368 self.move_cursor_home();
369 EventResult::Handled
370 }
371 KeyCode::End => {
372 self.move_cursor_end();
373 EventResult::Handled
374 }
375 _ => {
376 if let Some(ref text) = event.text
377 && !text.is_empty()
378 && text.chars().all(|c| !c.is_control() || c == ' ')
379 {
380 self.insert_text(text);
381 return EventResult::Handled;
382 }
383 EventResult::Ignored
384 }
385 }
386 }
387
388 fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
389 EventResult::Ignored
390 }
391
392 fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
393 EventResult::Ignored
394 }
395
396 fn is_focused(&self) -> bool {
397 self.focused
398 }
399
400 fn set_focused(&mut self, focused: bool) {
401 self.focused = focused;
402 if focused {
403 self.cursor_position = self.text.len();
404 }
405 }
406
407 fn contains_point(&self, x: f32, y: f32) -> bool {
408 self.hit_test(x, y)
409 }
410}
411
412#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
421 fn input_box_defaults() {
422 let ib = InputBox::default();
423 assert!(ib.text.is_empty());
424 assert!(ib.placeholder.is_none());
425 assert!(!ib.focused);
426 assert_eq!(ib.cursor_position, 0);
427 }
428
429 #[test]
430 fn input_box_set_text() {
431 let mut ib = InputBox::default();
432 ib.set_text("Hello");
433 assert_eq!(ib.text(), "Hello");
434 assert_eq!(ib.cursor_position, 5);
435 }
436
437 #[test]
438 fn input_box_insert_and_delete() {
439 let mut ib = InputBox::default();
440 ib.insert_text("abc");
441 assert_eq!(ib.text(), "abc");
442 assert_eq!(ib.cursor_position, 3);
443
444 ib.delete_char_before();
445 assert_eq!(ib.text(), "ab");
446 assert_eq!(ib.cursor_position, 2);
447
448 ib.move_cursor_left();
449 ib.delete_char_after();
451 assert_eq!(ib.text(), "a");
453 assert_eq!(ib.cursor_position, 1); }
455
456 #[test]
457 fn input_box_cursor_movement() {
458 let mut ib = InputBox::default();
459 ib.set_text("Hello");
460 assert_eq!(ib.cursor_position, 5);
461
462 ib.move_cursor_home();
463 assert_eq!(ib.cursor_position, 0);
464
465 ib.move_cursor_end();
466 assert_eq!(ib.cursor_position, 5);
467
468 ib.move_cursor_left();
469 assert_eq!(ib.cursor_position, 4);
470
471 ib.move_cursor_right();
472 assert_eq!(ib.cursor_position, 5);
473 }
474
475 #[test]
476 fn input_box_password_display() {
477 let mut ib = InputBox::default();
478 ib.input_type = "password".to_string();
479 ib.set_text("secret");
480 let display = ib.display_text();
481 assert_eq!(display.chars().count(), 6);
482 assert!(display.chars().all(|c| c == '\u{2022}'));
483 }
484
485 #[test]
486 fn input_box_hit_test() {
487 let ib = InputBox::new(Rect {
488 x: 10.0,
489 y: 10.0,
490 w: 200.0,
491 h: 32.0,
492 });
493 assert!(ib.hit_test(50.0, 25.0));
494 assert!(!ib.hit_test(0.0, 0.0));
495 }
496
497 #[test]
498 fn input_box_disabled_no_edit() {
499 let mut ib = InputBox::default();
500 ib.disabled = true;
501 ib.insert_text("nope");
502 assert!(ib.text.is_empty());
503 }
504
505 #[test]
506 fn input_box_keyboard_typing() {
507 let mut ib = InputBox::default();
508 ib.focused = true;
509 let evt = KeyboardEvent {
510 key: KeyCode::Other(65),
511 state: ElementState::Pressed,
512 modifiers: Default::default(),
513 text: Some("a".to_string()),
514 };
515 assert_eq!(ib.handle_keyboard(&evt), EventResult::Handled);
516 assert_eq!(ib.text(), "a");
517 }
518}