Skip to main content

zenthra_widgets/
text_area.rs

1use crate::ui::{Ui, DrawCommand, OverlayRectDraw};
2use crate::text::TextBuilder;
3use zenthra_core::{Color, EdgeInsets, Id, Rect};
4use zenthra_platform::event::PlatformEvent;
5use zenthra_text::prelude::{TextOptions, CosmicFontProvider, Padding};
6// use zenthra_text::traits::FontProvider;
7
8pub struct TextAreaBuilder<'u, 'a, 'b> {
9    ui: &'u mut Ui<'a>,
10    buffer: &'b mut String,
11    id: Id,
12    x: f32,
13    y: f32,
14    font_size: f32,
15    color: Color,
16    bg: Option<Color>,
17    text_bg: Option<Color>,
18    highlight: Option<Color>,
19    padding: EdgeInsets,
20    text_padding: EdgeInsets,
21    line_height: f32,
22    width: f32,
23    height: Option<f32>,
24    scrollable: bool,
25    overflow_hidden: bool,
26    text_bg_fill_x: bool,
27    fill_x: bool,
28    wrap: zenthra_text::prelude::TextWrap,
29    radius: [f32; 4],
30    border_color: Option<Color>,
31    border_width: f32,
32    shadow_color: Option<Color>,
33    shadow_offset: [f32; 2],
34    shadow_blur: f32,
35    shadow_opacity: f32,
36    opacity: f32,
37    render_mode: Option<zenthra_core::RenderMode>,
38}
39
40impl<'u, 'a, 'b> TextAreaBuilder<'u, 'a, 'b> {
41    pub fn new(ui: &'u mut Ui<'a>, buffer: &'b mut String) -> Self {
42        let id = ui.id();
43        let x = ui.cursor_x;
44        let y = ui.cursor_y;
45        Self {
46            ui,
47            buffer,
48            id,
49            x,
50            y,
51            font_size: 18.0,
52            color: Color::WHITE,
53            bg: Some(Color::rgb(0.2, 0.2, 0.2)),
54            text_bg: None,
55            padding: EdgeInsets::ZERO,
56            text_padding: EdgeInsets::ZERO,
57            line_height: 1.2,
58            width: 300.0,
59            height: None,
60            scrollable: false,
61            overflow_hidden: false,
62            text_bg_fill_x: false,
63            fill_x: false,
64            highlight: None,
65            wrap: zenthra_text::prelude::TextWrap::Word,
66            radius: [4.0; 4],
67            border_color: None,
68            border_width: 0.0,
69            shadow_color: None,
70            shadow_offset: [0.0; 2],
71            shadow_blur: 0.0,
72            shadow_opacity: 1.0,
73            opacity: 1.0,
74            render_mode: None,
75        }
76    }
77
78    pub fn id(mut self, id: impl std::hash::Hash) -> Self {
79        let mut hasher = std::collections::hash_map::DefaultHasher::new();
80        use std::hash::Hasher;
81        id.hash(&mut hasher);
82        self.id = Id::from_u64(hasher.finish());
83        self
84    }
85
86    pub fn scrollable(mut self, enabled: bool) -> Self {
87        self.scrollable = enabled;
88        if enabled {
89            self.overflow_hidden = true;
90        }
91        self
92    }
93
94    pub fn overflow_hidden(mut self, enabled: bool) -> Self {
95        self.overflow_hidden = enabled;
96        self
97    }
98
99    pub fn size(mut self, size: f32) -> Self {
100        self.font_size = size;
101        self
102    }
103
104    pub fn color(mut self, color: Color) -> Self {
105        self.color = color;
106        self
107    }
108
109    pub fn bg(mut self, bg: Color) -> Self {
110        self.bg = Some(bg);
111        self
112    }
113
114    pub fn text_bg(mut self, bg: Color) -> Self {
115        self.text_bg = Some(bg);
116        self
117    }
118
119    pub fn highlight(mut self, color: Color) -> Self {
120        self.highlight = Some(color);
121        self
122    }
123
124    pub fn padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
125        self.padding = EdgeInsets { top: t, right: r, bottom: b, left: l };
126        self
127    }
128    pub fn padding_x(mut self, x: f32) -> Self {
129        self.padding.left = x;
130        self.padding.right = x;
131        self
132    }
133    pub fn padding_y(mut self, y: f32) -> Self {
134        self.padding.top = y;
135        self.padding.bottom = y;
136        self
137    }
138    pub fn padding_top(mut self, t: f32) -> Self {
139        self.padding.top = t;
140        self
141    }
142    pub fn padding_bottom(mut self, b: f32) -> Self {
143        self.padding.bottom = b;
144        self
145    }
146    pub fn padding_left(mut self, l: f32) -> Self {
147        self.padding.left = l;
148        self
149    }
150    pub fn padding_right(mut self, r: f32) -> Self {
151        self.padding.right = r;
152        self
153    }
154
155    pub fn text_padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
156        self.text_padding = EdgeInsets { top: t, right: r, bottom: b, left: l };
157        self
158    }
159    pub fn text_padding_x(mut self, x: f32) -> Self {
160        self.text_padding.left = x;
161        self.text_padding.right = x;
162        self
163    }
164    pub fn text_padding_y(mut self, y: f32) -> Self {
165        self.text_padding.top = y;
166        self.text_padding.bottom = y;
167        self
168    }
169    pub fn text_padding_top(mut self, t: f32) -> Self {
170        self.text_padding.top = t;
171        self
172    }
173    pub fn text_padding_bottom(mut self, b: f32) -> Self {
174        self.text_padding.bottom = b;
175        self
176    }
177    pub fn text_padding_left(mut self, l: f32) -> Self {
178        self.text_padding.left = l;
179        self
180    }
181    pub fn text_padding_right(mut self, r: f32) -> Self {
182        self.text_padding.right = r;
183        self
184    }
185
186    pub fn line_height(mut self, lh: f32) -> Self {
187        self.line_height = lh;
188        self
189    }
190
191    pub fn width(mut self, w: f32) -> Self {
192        self.width = w;
193        self
194    }
195
196    pub fn fill_x(mut self) -> Self {
197        self.fill_x = true;
198        self
199    }
200
201    pub fn radius(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
202        self.radius = [tl, tr, br, bl];
203        self
204    }
205
206    pub fn radius_all(mut self, r: f32) -> Self {
207        self.radius = [r; 4];
208        self
209    }
210
211    pub fn radius_top(mut self, r: f32) -> Self {
212        self.radius[0] = r;
213        self.radius[1] = r;
214        self
215    }
216
217    pub fn radius_bottom(mut self, r: f32) -> Self {
218        self.radius[2] = r;
219        self.radius[3] = r;
220        self
221    }
222
223    pub fn radius_top_left(mut self, r: f32) -> Self {
224        self.radius[0] = r;
225        self
226    }
227
228    pub fn radius_top_right(mut self, r: f32) -> Self {
229        self.radius[1] = r;
230        self
231    }
232
233    pub fn radius_bottom_right(mut self, r: f32) -> Self {
234        self.radius[2] = r;
235        self
236    }
237
238    pub fn radius_bottom_left(mut self, r: f32) -> Self {
239        self.radius[3] = r;
240        self
241    }
242
243    pub fn radius_left(mut self, r: f32) -> Self {
244        self.radius[0] = r;
245        self.radius[3] = r;
246        self
247    }
248
249    pub fn radius_right(mut self, r: f32) -> Self {
250        self.radius[1] = r;
251        self.radius[2] = r;
252        self
253    }
254
255    pub fn border(mut self, color: Color, width: f32) -> Self {
256        self.border_color = Some(color);
257        self.border_width = width;
258        self
259    }
260
261    pub fn shadow(mut self, color: Color, ox: f32, oy: f32, blur: f32) -> Self {
262        self.shadow_color = Some(color);
263        self.shadow_offset = [ox, oy];
264        self.shadow_blur = blur;
265        self
266    }
267
268    pub fn opacity(mut self, o: f32) -> Self {
269        self.opacity = o;
270        self
271    }
272
273    pub fn text_bg_fill_x(mut self, enabled: bool) -> Self {
274        self.text_bg_fill_x = enabled;
275        self
276    }
277
278    pub fn height(mut self, h: f32) -> Self {
279        self.height = Some(h);
280        self
281    }
282
283    pub fn wrap(mut self, strategy: zenthra_text::prelude::TextWrap) -> Self {
284        self.wrap = strategy;
285        self
286    }
287
288    pub fn render_mode(mut self, mode: zenthra_core::RenderMode) -> Self {
289        self.render_mode = Some(mode);
290        self
291    }
292
293    pub fn show(self) -> zenthra_core::Response {
294        if let Some(mode) = self.render_mode {
295            self.ui.render_mode_stack.push(mode);
296        }
297        let is_focused = self.ui.focused_id == Some(self.id);
298        
299        // --- 1. Handle Scroll State ---
300        let (scroll_x, mut scroll_y) = if self.scrollable {
301            *self.ui.scroll_state.get(&self.id).unwrap_or(&(0.0, 0.0))
302        } else {
303            (0.0, 0.0)
304        };
305
306        // --- 2. Initial Measure (to get content height) ---
307        let actual_width = if self.fill_x {
308            (self.ui.max_x - self.x).max(self.width)
309        } else {
310            self.width
311        };
312
313        let (_content_w, mut h_content, mut shaped_buffer) = if let Some(fs) = self.ui.font_system.as_ref() {
314            let mut adapter = CosmicFontProvider::new_with_system(fs.clone());
315            let t_padding = Padding::from(self.text_padding);
316            let layout_width = actual_width - self.padding.horizontal() - t_padding.horizontal();
317            adapter.set_layout_size(layout_width, 10000.0);
318            
319            let options = TextOptions::new()
320                .font_size(self.font_size)
321                .line_height(self.line_height)
322                .wrap(self.wrap)
323                .max_width(layout_width);
324            
325            let buffer = adapter.shape(&self.buffer, &options);
326            let (_, ch) = buffer.content_size();
327            (actual_width, ch + t_padding.vertical(), Some(buffer))
328        } else {
329            (actual_width, 20.0, None)
330        };
331
332        let mut h_box = if self.scrollable {
333            self.height.unwrap_or(200.0)
334        } else {
335            h_content + self.padding.vertical()
336        };
337        
338        if self.scrollable && self.height.unwrap_or(0.0) == 0.0 {
339            h_box = 200.0;
340        }
341
342        // --- 3. Handle Events ---
343        let mut cursor_index = *self.ui.cursor_state.get(&self.id).unwrap_or(&self.buffer.len());
344        cursor_index = cursor_index.min(self.buffer.len());
345        if !self.buffer.is_char_boundary(cursor_index) {
346            cursor_index = self.buffer.len(); // Safety
347        }
348
349        let (actual_x, actual_y) = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
350            (rect.origin.x + self.ui.offset_x, rect.origin.y + self.ui.offset_y)
351        } else {
352            (self.x + self.ui.offset_x, self.y + self.ui.offset_y)
353        };
354        let is_hovered = self.ui.is_hovered(self.id, actual_x, actual_y, actual_width, h_box);
355
356        if is_focused || is_hovered {
357            let events = std::mem::take(&mut self.ui.input_events);
358            let mut changed = false;
359            for event in &events {
360                match event {
361                    PlatformEvent::CharTyped(c) if is_focused => {
362                        if *c != '\r' && *c != '\n' {
363                             self.buffer.insert(cursor_index, *c);
364                             cursor_index += c.len_utf8();
365                             changed = true;
366                             self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
367                        }
368                    }
369                    PlatformEvent::KeyDown { key } if is_focused => {
370                        match key {
371                            winit::keyboard::KeyCode::Backspace => {
372                                if cursor_index > 0 {
373                                    let mut chars = self.buffer[..cursor_index].chars();
374                                    if let Some(c) = chars.next_back() {
375                                        let len = c.len_utf8();
376                                        self.buffer.remove(cursor_index - len);
377                                        cursor_index -= len;
378                                        changed = true;
379                                        self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
380                                    }
381                                }
382                            }
383                            winit::keyboard::KeyCode::ArrowLeft => {
384                                if cursor_index > 0 {
385                                    let mut chars = self.buffer[..cursor_index].chars();
386                                    if let Some(c) = chars.next_back() {
387                                        cursor_index -= c.len_utf8();
388                                        self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
389                                        self.ui.needs_redraw = true;
390                                    }
391                                }
392                            }
393                            winit::keyboard::KeyCode::ArrowRight => {
394                                if cursor_index < self.buffer.len() {
395                                    let mut chars = self.buffer[cursor_index..].chars();
396                                    if let Some(c) = chars.next() {
397                                        cursor_index += c.len_utf8();
398                                        self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
399                                        self.ui.needs_redraw = true;
400                                    }
401                                }
402                            }
403                            winit::keyboard::KeyCode::ArrowUp => {
404                                if let Some(sb) = &shaped_buffer {
405                                    self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
406                                    self.ui.needs_redraw = true;
407                                    let row_threshold = (self.font_size * self.line_height) * 0.5;
408
409                                    // 1. Find EXACTLY which line index the cursor is on
410                                    let mut current_line_idx = 0;
411                                    for (i, line) in sb.lines().iter().enumerate() {
412                                        if line.start_cluster <= cursor_index {
413                                            current_line_idx = i;
414                                        } else {
415                                            break;
416                                        }
417                                    }
418
419                                    if current_line_idx > 0 {
420                                        // 2. Identify target line and target X
421                                        let target_line_idx = current_line_idx - 1;
422                                        let target_line = &sb.lines()[target_line_idx];
423                                        
424                                        // Find where we are horizontally on the current line to try and match it
425                                        let mut target_x = 0.0;
426                                        if let Some(current_line) = sb.lines().get(current_line_idx) {
427                                            for g in sb.glyphs() {
428                                                if (g.y - current_line.y).abs() < row_threshold {
429                                                    if g.cluster < cursor_index {
430                                                        target_x = g.x + g.width;
431                                                    } else if g.cluster == cursor_index {
432                                                        target_x = g.x;
433                                                        break;
434                                                    }
435                                                }
436                                            }
437                                        }
438
439                                        // 3. Find closest character on target line
440                                        let mut best_idx = None;
441                                        let mut best_dist = f32::INFINITY;
442                                        for g in sb.glyphs() {
443                                            if (g.y - target_line.y).abs() < row_threshold {
444                                                let dist = (g.x - target_x).abs();
445                                                if dist < best_dist {
446                                                    best_dist = dist;
447                                                    best_idx = Some(g.cluster);
448                                                }
449                                            }
450                                        }
451
452                                        if let Some(idx) = best_idx {
453                                            cursor_index = idx;
454                                        } else {
455                                            // Fallback for empty target line
456                                            cursor_index = target_line.start_cluster;
457                                        }
458                                    }
459                                }
460                            }
461                            winit::keyboard::KeyCode::ArrowDown => {
462                                if let Some(sb) = &shaped_buffer {
463                                    self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
464                                    self.ui.needs_redraw = true;
465                                    let row_threshold = (self.font_size * self.line_height) * 0.5;
466
467                                    // 1. Find EXACTLY which line index the cursor is on
468                                    let mut current_line_idx = 0;
469                                    for (i, line) in sb.lines().iter().enumerate() {
470                                        if line.start_cluster <= cursor_index {
471                                            current_line_idx = i;
472                                        } else {
473                                            break;
474                                        }
475                                    }
476
477                                    if current_line_idx < sb.lines().len() - 1 {
478                                        // 2. Identify target line and target X
479                                        let target_line_idx = current_line_idx + 1;
480                                        let target_line = &sb.lines()[target_line_idx];
481                                        
482                                        // Find where we are horizontally on current line
483                                        let mut target_x = 0.0;
484                                        if let Some(current_line) = sb.lines().get(current_line_idx) {
485                                            for g in sb.glyphs() {
486                                                if (g.y - current_line.y).abs() < row_threshold {
487                                                    if g.cluster < cursor_index {
488                                                        target_x = g.x + g.width;
489                                                    } else if g.cluster == cursor_index {
490                                                        target_x = g.x;
491                                                        break;
492                                                    }
493                                                }
494                                            }
495                                        }
496
497                                        // 3. Find closest character on target line
498                                        let mut best_idx = None;
499                                        let mut best_dist = f32::INFINITY;
500                                        for g in sb.glyphs() {
501                                            if (g.y - target_line.y).abs() < row_threshold {
502                                                let dist = (g.x - target_x).abs();
503                                                if dist < best_dist {
504                                                    best_dist = dist;
505                                                    best_idx = Some(g.cluster);
506                                                }
507                                            }
508                                        }
509
510                                        if let Some(idx) = best_idx {
511                                            cursor_index = idx;
512                                        } else {
513                                            // Fallback for empty target line
514                                            cursor_index = target_line.start_cluster;
515                                        }
516                                    }
517                                }
518                            }
519                            winit::keyboard::KeyCode::Enter | winit::keyboard::KeyCode::NumpadEnter => {
520                                self.buffer.insert(cursor_index, '\n');
521                                cursor_index += 1;
522                                changed = true;
523                                self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
524                            }
525                            _ => {}
526                        }
527                    }
528                    PlatformEvent::MouseWheel { delta_x, delta_y } if self.scrollable && is_hovered => {
529                        let total_delta = *delta_x + *delta_y;
530                        scroll_y -= total_delta * 30.0; 
531                    }
532                    _ => {}
533                }
534            }
535
536                    if changed {
537                        // RE-SHAPE after buffer modification
538                        if let Some(fs) = self.ui.font_system.as_ref() {
539                            let mut adapter = CosmicFontProvider::new_with_system(fs.clone());
540                            let t_padding = Padding::from(self.text_padding);
541                            let layout_width = actual_width - self.padding.horizontal() - t_padding.horizontal();
542                            adapter.set_layout_size(layout_width, 10000.0);
543                            
544                            let options = TextOptions::new().font_size(self.font_size).line_height(self.line_height).wrap(self.wrap).max_width(layout_width);
545                            let buffer = adapter.shape(&self.buffer, &options);
546                            let (_cw, ch) = buffer.content_size();
547                            h_content = ch + t_padding.vertical();
548                            shaped_buffer = Some(buffer);
549                            
550                            // Re-calculate h_box immediately if not fixed
551                            if !self.scrollable {
552                                h_box = h_content + self.padding.vertical();
553                            }
554                        }
555                        
556                        self.ui.cursor_state.insert(self.id, cursor_index);
557                    }
558                    
559                    if self.scrollable {
560                        let usable_h = h_box - self.padding.vertical();
561                        let max_scroll = (h_content - usable_h).max(0.0);
562                        
563                        // Vertical Auto-scroll
564                        if let Some(sb) = &shaped_buffer {
565                            let mut ly = 0.0;
566                            let mut found = false;
567                            for line in sb.lines() {
568                                if line.start_cluster <= cursor_index {
569                                    ly = line.y;
570                                    found = true;
571                                } else { break; }
572                            }
573                            if found {
574                                let line_h = self.font_size * self.line_height;
575                                let cursor_y_v = ly + self.text_padding.top;
576                                
577                                if cursor_y_v > scroll_y + usable_h - line_h {
578                                    scroll_y = cursor_y_v - usable_h + line_h;
579                                } else if cursor_y_v < scroll_y {
580                                    scroll_y = cursor_y_v;
581                                }
582                            }
583                        }
584
585                        scroll_y = scroll_y.clamp(0.0, max_scroll);
586                    }
587                    self.ui.input_events = events;
588                    self.ui.cursor_state.insert(self.id, cursor_index);
589                    self.ui.scroll_state.insert(self.id, (scroll_x, scroll_y));
590                }
591
592        // --- 4. Render Background (FIXED) ---
593        let start_draw = self.ui.draws.len();
594        if let Some(bg) = self.bg {
595            use crate::ui::RectDraw;
596            use zenthra_render::RectInstance;
597            self.ui.draws.push(DrawCommand::Rect(RectDraw {
598                instance: RectInstance {
599                    pos: [self.x, self.y],
600                    size: [actual_width, h_box],
601                    color: bg.to_array(),
602                    radius: self.radius,
603                    border_width: self.border_width.max(if is_focused { 1.0 } else { 0.0 }),
604                    border_color: self.border_color.unwrap_or(Color::rgba(1.0, 1.0, 1.0, 0.4)).to_array(),
605                    shadow_color: self.shadow_color.map(|c| {
606                        let mut a = c.to_array();
607                        a[3] *= self.shadow_opacity;
608                        a
609                    }).unwrap_or([0.0, 0.0, 0.0, 0.0]),
610                    shadow_offset: self.shadow_offset,
611                    shadow_blur: self.shadow_blur,
612                    clip_rect: [0.0, 0.0, 9999.0, 9999.0],
613                    grayscale: 0.0,
614                    brightness: 1.0,
615                    opacity: self.opacity,
616                    ..Default::default()
617                }
618            }));
619        }
620
621        // --- 5. Render Text (CLIPPED if overflow_hidden) ---
622        let pos_y = self.y + self.padding.top - scroll_y;
623        let mut text_builder = TextBuilder::new(self.ui, &self.buffer)
624            .size(self.font_size)
625            .line_height(self.line_height)
626            .color(self.color)
627            .fill_x(false)
628            .padding(self.text_padding.top, self.text_padding.right, self.text_padding.bottom, self.text_padding.left)
629            .wrap(self.wrap)
630            .max_width(actual_width - self.padding.horizontal() - self.text_padding.horizontal())
631            .pos(self.x + self.padding.left, pos_y);
632
633        if self.text_bg_fill_x {
634            text_builder = text_builder.min_width(actual_width - self.padding.horizontal());
635        }
636
637        if let Some(tbg) = self.text_bg {
638            text_builder = text_builder.bg(tbg).fill_x(false);
639        }
640
641        if let Some(h) = self.highlight {
642            text_builder = text_builder.highlight(h);
643        }
644
645        if self.overflow_hidden {
646            // Reverted to full-box clipping so content can merge with edges during scroll
647            text_builder = text_builder.clip_rect(self.x, self.y, actual_width, h_box);
648        }
649        
650        // Final draw
651        text_builder.draw_and_measure();
652
653        // Update persistent scroll state
654        if self.scrollable {
655            self.ui.scroll_state.insert(self.id, (scroll_x, scroll_y));
656
657            // --- 5.5 Render Scroll Bar ---
658            if h_content > h_box - self.padding.vertical() {
659                let usable_h = h_box - self.padding.vertical();
660                let scroll_bar_w = 4.0;
661                let scroll_bar_x = self.x + actual_width - scroll_bar_w - 4.0;
662                
663                let thumb_h = (usable_h / h_content) * usable_h;
664                let thumb_h = thumb_h.clamp(20.0, usable_h);
665                
666                let max_scroll = (h_content - usable_h).max(1.0);
667                let scroll_percent = scroll_y / max_scroll;
668                let thumb_y = self.y + self.padding.top + scroll_percent * (usable_h - thumb_h);
669
670                self.ui.draws.push(DrawCommand::OverlayRect(OverlayRectDraw {
671                    x: scroll_bar_x,
672                    y: thumb_y,
673                    width: scroll_bar_w,
674                    height: thumb_h,
675                    color: Color::rgba(1.0, 1.0, 1.0, 0.4),
676                    clip: [self.x, self.y, actual_width, h_box], 
677                }));
678            }
679        }
680
681        // --- 6. Handle Focus ---
682        if self.ui.mouse_down {
683            if is_hovered {
684                self.ui.focused_id = Some(self.id);
685            }
686        }
687
688        // --- 7. Cursor Rendering ---
689        if is_focused {
690            let font_size = self.font_size;
691            let lh = self.line_height;
692            let visual_ascent = font_size * (0.8 + (lh - 1.0) / 2.0);
693            let cursor_height = font_size * lh;
694
695            if let Some(sb) = shaped_buffer {
696                let first_line_y = sb.lines().first().map(|l| l.y).unwrap_or(visual_ascent);
697                let v_shift = visual_ascent - first_line_y;
698
699                let _final_border_w = self.border_width;
700                let _final_border_c = self.border_color.unwrap_or(Color::rgba(1.0, 1.0, 1.0, 0.4));
701                let mut lx = 0.0;
702                let mut ly = first_line_y;
703                let mut found = false;
704
705                for g in sb.glyphs() {
706                    if g.cluster == cursor_index {
707                        lx = g.x;
708                        ly = g.y;
709                        found = true;
710                        break;
711                    }
712                }
713
714                if !found {
715                    let mut best_line = None;
716                    for line in sb.lines() {
717                        if line.start_cluster <= cursor_index {
718                            best_line = Some(line);
719                        } else {
720                            break;
721                        }
722                    }
723
724                    if let Some(line) = best_line {
725                        ly = line.y;
726                        // If we are past the start of the line and not found in glyphs,
727                        // we're likely at a newline or trailing space.
728                        if cursor_index > line.start_cluster {
729                            lx = line.width;
730                        } else {
731                            lx = 0.0;
732                        }
733                    }
734                    
735                    if cursor_index == self.buffer.len() {
736                        if let Some(lg) = sb.glyphs().last() {
737                            if (lg.y - ly).abs() < 2.0 {
738                                lx = lg.x + lg.width;
739                            } else {
740                                // Last line might be empty
741                                if let Some(last_line) = sb.lines().last() {
742                                    ly = last_line.y;
743                                    lx = 0.0;
744                                }
745                            }
746                        }
747                    }
748                }
749                
750                let cx = lx + self.x + self.padding.left + self.text_padding.left;
751                let cy = ly + self.y + self.padding.top + self.text_padding.top + v_shift - visual_ascent - scroll_y;
752                
753                 // Smart Blink: Solid while typing, blink when idle
754                 let last_activity = *self.ui.interaction_state.get(&self.id).unwrap_or(&0.0);
755                 let time_since_activity = self.ui.elapsed_time - last_activity;
756                 
757                 let is_blink_visible = if time_since_activity < 0.5 {
758                     true // Solid during and just after activity
759                 } else {
760                     // Start blinking after 500ms of idle. 
761                     // Offset by last_activity so the cycle always starts "ON" when you stop.
762                     (self.ui.elapsed_time - last_activity).fract() < 0.5
763                 };
764                 
765                 // Only draw cursor if it's within the viewport and in the visible blink phase
766                 if cy >= self.y - 2.0 && cy + cursor_height <= self.y + h_box + 2.0 && is_blink_visible {
767                    self.ui.draws.push(DrawCommand::OverlayRect(OverlayRectDraw {
768                        x: cx,
769                        y: cy,
770                        width: 2.0,
771                        height: cursor_height,
772                        color: Color::WHITE,
773                        clip: [self.x, self.y, actual_width, h_box],
774                    }));
775                }
776            }
777        }
778
779        // --- 8. Advance UI ---
780        self.ui.record_layout(self.id, Rect::new(self.x, self.y, actual_width, h_box));
781        self.ui.advance(actual_width, h_box, start_draw);
782
783        if self.render_mode.is_some() {
784            self.ui.render_mode_stack.pop();
785        }
786
787        zenthra_core::Response {
788            clicked: self.ui.clicked && is_hovered,
789            hovered: is_hovered,
790            pressed: is_hovered && self.ui.mouse_down,
791        }
792    }
793}