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;
13
14pub struct TextArea {
18 pub rect: Rect,
20 pub text: String,
22 pub text_size: f32,
24 pub text_color: ColorLinPremul,
26 pub placeholder: Option<String>,
28 pub focused: bool,
30 pub bg_color: ColorLinPremul,
32 pub border_color: ColorLinPremul,
34 pub border_width: f32,
36 pub corner_radius: f32,
38 pub validation_error: Option<String>,
40 pub cursor_position: usize,
42 scroll_y: f32,
44 padding_x: f32,
46 padding_y: f32,
48 line_height_factor: f32,
50 pub focus_id: FocusId,
52}
53
54impl TextArea {
55 pub fn new(rect: Rect) -> Self {
57 Self {
58 rect,
59 text: String::new(),
60 text_size: 14.0,
61 text_color: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
62 placeholder: None,
63 focused: false,
64 bg_color: ColorLinPremul::from_srgba_u8([40, 40, 40, 255]),
65 border_color: ColorLinPremul::from_srgba_u8([80, 80, 80, 255]),
66 border_width: 1.0,
67 corner_radius: 4.0,
68 validation_error: None,
69 cursor_position: 0,
70 scroll_y: 0.0,
71 padding_x: 8.0,
72 padding_y: 8.0,
73 line_height_factor: 1.3,
74 focus_id: FocusId(0),
75 }
76 }
77
78 pub fn text(&self) -> &str {
80 &self.text
81 }
82
83 pub fn set_text(&mut self, text: impl Into<String>) {
85 self.text = text.into();
86 self.cursor_position = self.text.len();
87 }
88
89 pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
91 self.placeholder = Some(placeholder.into());
92 }
93
94 fn line_height(&self) -> f32 {
96 self.text_size * self.line_height_factor
97 }
98
99 pub fn insert_text(&mut self, s: &str) {
101 self.text.insert_str(self.cursor_position, s);
102 self.cursor_position += s.len();
103 }
104
105 pub fn delete_char_before(&mut self) {
107 if self.cursor_position == 0 {
108 return;
109 }
110 let prev = self.text[..self.cursor_position]
111 .char_indices()
112 .next_back()
113 .map(|(i, _)| i)
114 .unwrap_or(0);
115 self.text.drain(prev..self.cursor_position);
116 self.cursor_position = prev;
117 }
118
119 pub fn delete_char_after(&mut self) {
121 if self.cursor_position >= self.text.len() {
122 return;
123 }
124 let next = self.text[self.cursor_position..]
125 .char_indices()
126 .nth(1)
127 .map(|(i, _)| self.cursor_position + i)
128 .unwrap_or(self.text.len());
129 self.text.drain(self.cursor_position..next);
130 }
131
132 pub fn move_cursor_left(&mut self) {
134 if self.cursor_position > 0 {
135 self.cursor_position = self.text[..self.cursor_position]
136 .char_indices()
137 .next_back()
138 .map(|(i, _)| i)
139 .unwrap_or(0);
140 }
141 }
142
143 pub fn move_cursor_right(&mut self) {
145 if self.cursor_position < self.text.len() {
146 self.cursor_position = self.text[self.cursor_position..]
147 .char_indices()
148 .nth(1)
149 .map(|(i, _)| self.cursor_position + i)
150 .unwrap_or(self.text.len());
151 }
152 }
153
154 pub fn move_cursor_home(&mut self) {
156 let before = &self.text[..self.cursor_position];
157 if let Some(nl) = before.rfind('\n') {
158 self.cursor_position = nl + 1;
159 } else {
160 self.cursor_position = 0;
161 }
162 }
163
164 pub fn move_cursor_end(&mut self) {
166 let after = &self.text[self.cursor_position..];
167 if let Some(nl) = after.find('\n') {
168 self.cursor_position += nl;
169 } else {
170 self.cursor_position = self.text.len();
171 }
172 }
173
174 fn lines(&self) -> Vec<&str> {
176 self.text.split('\n').collect()
177 }
178
179 pub fn hit_test(&self, x: f32, y: f32) -> bool {
181 x >= self.rect.x
182 && x <= self.rect.x + self.rect.w
183 && y >= self.rect.y
184 && y <= self.rect.y + self.rect.h
185 }
186}
187
188impl Default for TextArea {
189 fn default() -> Self {
190 Self::new(Rect {
191 x: 0.0,
192 y: 0.0,
193 w: 300.0,
194 h: 120.0,
195 })
196 }
197}
198
199impl Element for TextArea {
204 fn rect(&self) -> Rect {
205 self.rect
206 }
207
208 fn set_rect(&mut self, rect: Rect) {
209 self.rect = rect;
210 }
211
212 fn render(&self, canvas: &mut Canvas, z: i32) {
213 let rrect = RoundedRect {
215 rect: self.rect,
216 radii: RoundedRadii {
217 tl: self.corner_radius,
218 tr: self.corner_radius,
219 br: self.corner_radius,
220 bl: self.corner_radius,
221 },
222 };
223
224 let has_error = self.validation_error.is_some();
225 let border_color = if has_error {
226 Color::rgba(220, 38, 38, 255)
227 } else if self.focused {
228 Color::rgba(63, 130, 246, 255)
229 } else {
230 self.border_color
231 };
232 let border_width = if has_error {
233 self.border_width.max(2.0)
234 } else if self.focused {
235 (self.border_width + 1.0).max(2.0)
236 } else {
237 self.border_width
238 };
239
240 jag_surface::shapes::draw_snapped_rounded_rectangle(
241 canvas,
242 rrect,
243 Some(Brush::Solid(self.bg_color)),
244 Some(border_width),
245 Some(Brush::Solid(border_color)),
246 z,
247 );
248
249 let content_x = self.rect.x + self.padding_x;
250 let content_y = self.rect.y + self.padding_y;
251 let lh = self.line_height();
252
253 if self.text.is_empty() {
254 if let Some(ref ph) = self.placeholder {
256 let ph_color = ColorLinPremul::from_srgba_u8([160, 160, 160, 255]);
257 let baseline = content_y + self.text_size * 0.85 - self.scroll_y;
258 canvas.draw_text_run_weighted(
259 [content_x, baseline],
260 ph.clone(),
261 self.text_size,
262 400.0,
263 ph_color,
264 z + 1,
265 );
266 }
267 } else {
268 let lines = self.lines();
270 for (i, line) in lines.iter().enumerate() {
271 let baseline = content_y + (i as f32 * lh) + self.text_size * 0.85 - self.scroll_y;
272 if baseline < self.rect.y - lh || baseline > self.rect.y + self.rect.h + lh {
273 continue; }
275 canvas.draw_text_run_weighted(
276 [content_x, baseline],
277 line.to_string(),
278 self.text_size,
279 400.0,
280 self.text_color,
281 z + 1,
282 );
283 }
284 }
285
286 if self.focused {
288 let before_cursor = &self.text[..self.cursor_position];
289 let line_idx = before_cursor.matches('\n').count();
290 let line_start = if let Some(nl) = before_cursor.rfind('\n') {
291 nl + 1
292 } else {
293 0
294 };
295 let line_text = &self.text[line_start..self.cursor_position];
296 let cursor_offset = canvas.measure_text_width(line_text, self.text_size);
297
298 let cursor_x = content_x + cursor_offset;
299 let cursor_y = content_y + (line_idx as f32 * lh) + 2.0 - self.scroll_y;
300 let cursor_h = (lh - 4.0).max(0.0);
301
302 canvas.fill_rect(
303 cursor_x,
304 cursor_y,
305 1.5,
306 cursor_h,
307 Brush::Solid(self.text_color),
308 z + 2,
309 );
310 }
311
312 if let Some(ref error_msg) = self.validation_error {
314 let error_size = (self.text_size * 0.85).max(12.0);
315 let baseline_offset = error_size * 0.8;
316 let top_gap = 3.0;
317 let error_y = self.rect.y + self.rect.h + top_gap + baseline_offset;
318 let error_color = ColorLinPremul::from_srgba_u8([220, 38, 38, 255]);
319
320 canvas.draw_text_run_weighted(
321 [self.rect.x + self.padding_x, error_y],
322 error_msg.clone(),
323 error_size,
324 400.0,
325 error_color,
326 z + 3,
327 );
328 }
329 }
330
331 fn focus_id(&self) -> Option<FocusId> {
332 Some(self.focus_id)
333 }
334}
335
336impl EventHandler for TextArea {
341 fn handle_mouse_click(&mut self, event: &MouseClickEvent) -> EventResult {
342 if event.button != MouseButton::Left || event.state != ElementState::Pressed {
343 return EventResult::Ignored;
344 }
345 if self.hit_test(event.x, event.y) {
346 EventResult::Handled
347 } else {
348 EventResult::Ignored
349 }
350 }
351
352 fn handle_keyboard(&mut self, event: &KeyboardEvent) -> EventResult {
353 if event.state != ElementState::Pressed || !self.focused {
354 return EventResult::Ignored;
355 }
356 match event.key {
357 KeyCode::Backspace => {
358 self.delete_char_before();
359 EventResult::Handled
360 }
361 KeyCode::Delete => {
362 self.delete_char_after();
363 EventResult::Handled
364 }
365 KeyCode::ArrowLeft => {
366 self.move_cursor_left();
367 EventResult::Handled
368 }
369 KeyCode::ArrowRight => {
370 self.move_cursor_right();
371 EventResult::Handled
372 }
373 KeyCode::Home => {
374 self.move_cursor_home();
375 EventResult::Handled
376 }
377 KeyCode::End => {
378 self.move_cursor_end();
379 EventResult::Handled
380 }
381 KeyCode::Enter => {
382 self.insert_text("\n");
383 EventResult::Handled
384 }
385 _ => {
386 if let Some(ref text) = event.text
387 && !text.is_empty()
388 && text.chars().all(|c| !c.is_control() || c == ' ')
389 {
390 self.insert_text(text);
391 return EventResult::Handled;
392 }
393 EventResult::Ignored
394 }
395 }
396 }
397
398 fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
399 EventResult::Ignored
400 }
401
402 fn handle_scroll(&mut self, event: &ScrollEvent) -> EventResult {
403 if self.hit_test(event.x, event.y) {
404 self.scroll_y = (self.scroll_y - event.delta_y).max(0.0);
405 EventResult::Handled
406 } else {
407 EventResult::Ignored
408 }
409 }
410
411 fn is_focused(&self) -> bool {
412 self.focused
413 }
414
415 fn set_focused(&mut self, focused: bool) {
416 self.focused = focused;
417 if focused {
418 self.cursor_position = self.text.len();
419 }
420 }
421
422 fn contains_point(&self, x: f32, y: f32) -> bool {
423 self.hit_test(x, y)
424 }
425}
426
427#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn text_area_defaults() {
437 let ta = TextArea::default();
438 assert!(ta.text.is_empty());
439 assert!(!ta.focused);
440 assert_eq!(ta.cursor_position, 0);
441 }
442
443 #[test]
444 fn text_area_set_text() {
445 let mut ta = TextArea::default();
446 ta.set_text("Line1\nLine2");
447 assert_eq!(ta.text(), "Line1\nLine2");
448 assert_eq!(ta.cursor_position, 11);
449 }
450
451 #[test]
452 fn text_area_insert_newline() {
453 let mut ta = TextArea::default();
454 ta.insert_text("hello");
455 ta.insert_text("\n");
456 ta.insert_text("world");
457 assert_eq!(ta.text(), "hello\nworld");
458 assert_eq!(ta.lines().len(), 2);
459 }
460
461 #[test]
462 fn text_area_cursor_home_end() {
463 let mut ta = TextArea::default();
464 ta.set_text("line1\nline2\nline3");
465 assert_eq!(ta.cursor_position, 17);
467
468 ta.move_cursor_home();
469 assert_eq!(ta.cursor_position, 12);
471
472 ta.move_cursor_end();
473 assert_eq!(ta.cursor_position, 17);
475 }
476
477 #[test]
478 fn text_area_delete() {
479 let mut ta = TextArea::default();
480 ta.set_text("abcd");
481 ta.delete_char_before();
482 assert_eq!(ta.text(), "abc");
483 ta.move_cursor_home();
484 ta.delete_char_after();
485 assert_eq!(ta.text(), "bc");
486 }
487
488 #[test]
489 fn text_area_keyboard_enter() {
490 let mut ta = TextArea::default();
491 ta.focused = true;
492 ta.set_text("hello");
493 ta.cursor_position = 5;
494 let evt = KeyboardEvent {
495 key: KeyCode::Enter,
496 state: ElementState::Pressed,
497 modifiers: Default::default(),
498 text: None,
499 };
500 assert_eq!(ta.handle_keyboard(&evt), EventResult::Handled);
501 assert_eq!(ta.text(), "hello\n");
502 }
503
504 #[test]
505 fn text_area_scroll() {
506 let mut ta = TextArea::default();
507 ta.rect = Rect {
508 x: 0.0,
509 y: 0.0,
510 w: 300.0,
511 h: 100.0,
512 };
513 let evt = ScrollEvent {
514 x: 50.0,
515 y: 50.0,
516 delta_x: 0.0,
517 delta_y: -10.0,
518 };
519 assert_eq!(ta.handle_scroll(&evt), EventResult::Handled);
520 assert!((ta.scroll_y - 10.0).abs() < f32::EPSILON);
521 }
522}