Skip to main content

zenthra_widgets/
input.rs

1use crate::ui::{Ui, DrawCommand, OverlayRectDraw, ScrollDrag};
2use crate::text::TextBuilder;
3use zenthra_core::{Color, EdgeInsets, Id, Role, SemanticNode, Rect};
4use zenthra_platform::event::PlatformEvent;
5use zenthra_text::prelude::{TextOptions, CosmicFontProvider, Padding, ShapedGlyph};
6use zenthra_text::traits::FontProvider;
7
8pub struct InputBuilder<'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: Option<f32>,
23    min_width: f32,
24    scrollable: bool,
25    fill_x: bool,
26    text_bg_fill_x: bool,
27    radius: [f32; 4],
28    border_color: Option<Color>,
29    border_width: f32,
30    opacity: f32,
31    shadow_color: Option<Color>,
32    shadow_offset: [f32; 2],
33    shadow_blur: f32,
34    shadow_opacity: f32,
35    render_mode: Option<zenthra_core::RenderMode>,
36}
37
38impl<'u, 'a, 'b> InputBuilder<'u, 'a, 'b> {
39    pub fn new(ui: &'u mut Ui<'a>, buffer: &'b mut String) -> Self {
40        let id = ui.id();
41        let x = ui.cursor_x;
42        let y = ui.cursor_y;
43        Self {
44            ui,
45            buffer,
46            id,
47            x,
48            y,
49            font_size: 18.0,
50            color: Color::WHITE,
51            bg: Some(Color::rgb(0.2, 0.2, 0.2)),
52            text_bg: None,
53            padding: EdgeInsets::ZERO,
54            text_padding: EdgeInsets::ZERO,
55            line_height: 1.2,
56            width: None,
57            min_width: 200.0,
58            scrollable: true,
59            fill_x: false,
60            text_bg_fill_x: false,
61            highlight: None,
62            radius: [4.0; 4],
63            border_color: None,
64            border_width: 0.0,
65            opacity: 1.0,
66            shadow_color: None,
67            shadow_offset: [0.0; 2],
68            shadow_blur: 0.0,
69            shadow_opacity: 1.0,
70            render_mode: None,
71        }
72    }
73
74    pub fn id(mut self, id: impl std::hash::Hash) -> Self {
75        let mut hasher = std::collections::hash_map::DefaultHasher::new();
76        use std::hash::Hasher;
77        id.hash(&mut hasher);
78        self.id = Id::from_u64(hasher.finish());
79        self
80    }
81
82    pub fn size(mut self, size: f32) -> Self {
83        self.font_size = size;
84        self
85    }
86
87    pub fn color(mut self, color: Color) -> Self {
88        self.color = color;
89        self
90    }
91
92    pub fn bg(mut self, bg: Color) -> Self {
93        self.bg = Some(bg);
94        self
95    }
96
97    pub fn text_bg(mut self, bg: Color) -> Self {
98        self.text_bg = Some(bg);
99        self
100    }
101
102    pub fn highlight(mut self, color: Color) -> Self {
103        self.highlight = Some(color);
104        self
105    }
106
107    pub fn padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
108        self.padding = EdgeInsets { top: t, right: r, bottom: b, left: l };
109        self
110    }
111    pub fn padding_x(mut self, x: f32) -> Self {
112        self.padding.left = x;
113        self.padding.right = x;
114        self
115    }
116    pub fn padding_y(mut self, y: f32) -> Self {
117        self.padding.top = y;
118        self.padding.bottom = y;
119        self
120    }
121    pub fn padding_top(mut self, t: f32) -> Self {
122        self.padding.top = t;
123        self
124    }
125    pub fn padding_bottom(mut self, b: f32) -> Self {
126        self.padding.bottom = b;
127        self
128    }
129    pub fn padding_left(mut self, l: f32) -> Self {
130        self.padding.left = l;
131        self
132    }
133    pub fn padding_right(mut self, r: f32) -> Self {
134        self.padding.right = r;
135        self
136    }
137
138    pub fn text_padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
139        self.text_padding = EdgeInsets { top: t, right: r, bottom: b, left: l };
140        self
141    }
142    pub fn text_padding_x(mut self, x: f32) -> Self {
143        self.text_padding.left = x;
144        self.text_padding.right = x;
145        self
146    }
147    pub fn text_padding_y(mut self, y: f32) -> Self {
148        self.text_padding.top = y;
149        self.text_padding.bottom = y;
150        self
151    }
152    pub fn text_padding_top(mut self, t: f32) -> Self {
153        self.text_padding.top = t;
154        self
155    }
156    pub fn text_padding_bottom(mut self, b: f32) -> Self {
157        self.text_padding.bottom = b;
158        self
159    }
160    pub fn text_padding_left(mut self, l: f32) -> Self {
161        self.text_padding.left = l;
162        self
163    }
164    pub fn text_padding_right(mut self, r: f32) -> Self {
165        self.text_padding.right = r;
166        self
167    }
168
169    pub fn line_height(mut self, lh: f32) -> Self {
170        self.line_height = lh;
171        self
172    }
173
174    pub fn width(mut self, w: f32) -> Self {
175        self.width = Some(w);
176        self
177    }
178
179    pub fn min_width(mut self, w: f32) -> Self {
180        self.min_width = w;
181        self
182    }
183
184    pub fn scrollable(mut self, s: bool) -> Self {
185        self.scrollable = s;
186        self
187    }
188
189    pub fn fill_x(mut self) -> Self {
190        self.fill_x = true;
191        self
192    }
193
194    pub fn radius(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
195        self.radius = [tl, tr, br, bl];
196        self
197    }
198
199    pub fn radius_all(mut self, r: f32) -> Self {
200        self.radius = [r, r, r, r];
201        self
202    }
203
204    pub fn radius_top(mut self, r: f32) -> Self {
205        self.radius[0] = r;
206        self.radius[1] = r;
207        self
208    }
209
210    pub fn radius_bottom(mut self, r: f32) -> Self {
211        self.radius[2] = r;
212        self.radius[3] = r;
213        self
214    }
215
216    pub fn radius_top_left(mut self, r: f32) -> Self {
217        self.radius[0] = r;
218        self
219    }
220
221    pub fn radius_top_right(mut self, r: f32) -> Self {
222        self.radius[1] = r;
223        self
224    }
225
226    pub fn radius_bottom_right(mut self, r: f32) -> Self {
227        self.radius[2] = r;
228        self
229    }
230
231    pub fn radius_bottom_left(mut self, r: f32) -> Self {
232        self.radius[3] = r;
233        self
234    }
235
236    pub fn radius_left(mut self, r: f32) -> Self {
237        self.radius[0] = r;
238        self.radius[3] = r;
239        self
240    }
241
242    pub fn radius_right(mut self, r: f32) -> Self {
243        self.radius[1] = r;
244        self.radius[2] = r;
245        self
246    }
247
248    pub fn border(mut self, color: Color, width: f32) -> Self {
249        self.border_color = Some(color);
250        self.border_width = width;
251        self
252    }
253
254    pub fn shadow(mut self, color: Color, ox: f32, oy: f32, blur: f32) -> Self {
255        self.shadow_color = Some(color);
256        self.shadow_offset = [ox, oy];
257        self.shadow_blur = blur;
258        self
259    }
260
261    pub fn opacity(mut self, o: f32) -> Self {
262        self.opacity = o;
263        self
264    }
265
266    pub fn text_bg_fill_x(mut self, enabled: bool) -> Self {
267        self.text_bg_fill_x = enabled;
268        self
269    }
270
271    pub fn render_mode(mut self, mode: zenthra_core::RenderMode) -> Self {
272        self.render_mode = Some(mode);
273        self
274    }
275
276    pub fn on_change<F>(self, mut f: F) -> Self
277    where
278        F: FnMut(String) + 'a,
279    {
280        self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
281            if let crate::ui::WidgetEvent::Change(crate::ui::EventValue::String(val)) = event {
282                f(val.clone());
283            }
284        });
285        self
286    }
287
288    pub fn on_hover<F>(self, mut f: F) -> Self
289    where
290        F: FnMut(bool) + 'a,
291    {
292        self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
293            if let crate::ui::WidgetEvent::Hover(hovered) = event {
294                f(*hovered);
295            }
296        });
297        self
298    }
299
300    pub fn on_event<F>(self, phase: crate::ui::EventPhase, f: F) -> Self
301    where
302        F: FnMut(&mut crate::ui::EventContext, &crate::ui::WidgetEvent) + 'a,
303    {
304        self.ui.add_listener(self.id, phase, f);
305        self
306    }
307
308    pub fn show(self) -> zenthra_core::Response {
309        if let Some(mode) = self.render_mode {
310            self.ui.render_mode_stack.push(mode);
311        }
312        let is_focused = self.ui.focused_id == Some(self.id);
313        
314        // --- 1. Initial Measure (for hit-testing and initial sizing) ---
315        let (mut w_text_raw, h_content, mut shaped_buffer) = if let Some(fs) = self.ui.font_system.as_ref() {
316            let mut adapter = CosmicFontProvider::new_with_system(fs.clone());
317            let t_padding = Padding::from(self.text_padding);
318            adapter.set_layout_size(1000000.0, 10000.0);
319            let options = TextOptions::new()
320                .font_size(self.font_size)
321                .line_height(self.line_height)
322                .wrap(zenthra_text::prelude::TextWrap::None);
323            let buffer = adapter.shape(&self.buffer, &options);
324            let (cw, _ch) = buffer.content_size();
325            let m = adapter.metrics(&options);
326            (cw + t_padding.horizontal(), m.line_height() + t_padding.vertical(), Some(buffer))
327        } else {
328            (self.min_width, 20.0, None)
329        };
330
331        let max_available_w = (self.ui.max_x - self.x).max(self.min_width);
332        let mut w_box = if self.fill_x { max_available_w } else { self.width.unwrap_or_else(|| (w_text_raw + self.padding.horizontal()).min(max_available_w)).max(self.min_width) };
333        let h_box = h_content + self.padding.vertical();
334        let mut w_view = w_box - self.padding.horizontal();
335
336        // --- 2. Hit Testing & Event Handling ---
337        let mut cursor_index = *self.ui.cursor_state.get(&self.id).unwrap_or(&self.buffer.len());
338        cursor_index = cursor_index.min(self.buffer.len());
339        if !self.buffer.is_char_boundary(cursor_index) {
340            cursor_index = self.buffer.len();
341        }
342
343        let (actual_x, actual_y) = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
344            (rect.origin.x + self.ui.offset_x, rect.origin.y + self.ui.offset_y)
345        } else {
346            (self.x + self.ui.offset_x, self.y + self.ui.offset_y)
347        };
348        let is_hovered = self.ui.is_hovered(self.id, actual_x, actual_y, w_box, h_box);
349        if is_hovered {
350            self.ui.cursor_icon = crate::text::CursorIcon::Text;
351        }
352        self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Hover(is_hovered));
353        let mut needs_auto_scroll = false;
354        let mut changed = false;
355
356        if is_focused || is_hovered || self.ui.active_drag.is_some() {
357            let events = std::mem::take(&mut self.ui.input_events);
358            for event in &events {
359                match event {
360                    PlatformEvent::CharTyped(c) if is_focused => {
361                        if *c != '\r' && *c != '\n' {
362                             self.buffer.insert(cursor_index, *c);
363                             cursor_index += c.len_utf8();
364                             needs_auto_scroll = true;
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                                        needs_auto_scroll = true;
379                                        changed = true;
380                                        self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
381                                    }
382                                }
383                            }
384                            winit::keyboard::KeyCode::ArrowLeft => {
385                                if cursor_index > 0 {
386                                    let mut chars = self.buffer[..cursor_index].chars();
387                                    if let Some(c) = chars.next_back() {
388                                        cursor_index -= c.len_utf8();
389                                        self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
390                                        self.ui.needs_redraw = true;
391                                        needs_auto_scroll = true;
392                                    }
393                                }
394                            }
395                            winit::keyboard::KeyCode::ArrowRight => {
396                                if cursor_index < self.buffer.len() {
397                                    let mut chars = self.buffer[cursor_index..].chars();
398                                    if let Some(c) = chars.next() {
399                                        cursor_index += c.len_utf8();
400                                        self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
401                                        self.ui.needs_redraw = true;
402                                        needs_auto_scroll = true;
403                                    }
404                                }
405                            }
406                            _ => {}
407                        }
408                    }
409                    PlatformEvent::MouseWheel { delta_x, delta_y } if is_hovered => {
410                        let _id = self.id;
411                        let (mut sx, sy) = *self.ui.scroll_state.get(&self.id).unwrap_or(&(0.0, 0.0));
412                        let effect = if delta_x.abs() < 0.001 { *delta_y } else { *delta_x };
413                        sx -= effect * 30.0;
414                        self.ui.scroll_state.insert(self.id, (sx, sy));
415                    }
416                    _ => {}
417                }
418            }
419            
420            if self.ui.clicked && is_hovered {
421                self.ui.focused_id = Some(self.id);
422                needs_auto_scroll = true;
423                self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Click);
424            }
425            
426            self.ui.input_events = events;
427            self.ui.cursor_state.insert(self.id, cursor_index);
428
429            if changed {
430                self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Change(crate::ui::EventValue::String(self.buffer.clone())));
431            }
432        }
433
434        // --- 3. Re-Measure if content changed ---
435        if changed {
436             if let Some(fs) = self.ui.font_system.as_ref() {
437                let mut adapter = CosmicFontProvider::new_with_system(fs.clone());
438                let t_padding = Padding::from(self.text_padding);
439                adapter.set_layout_size(1000000.0, 10000.0);
440                let options = TextOptions::new()
441                    .font_size(self.font_size)
442                    .line_height(self.line_height)
443                    .wrap(zenthra_text::prelude::TextWrap::None);
444                let buffer = adapter.shape(&self.buffer, &options);
445                let (cw, _ch) = buffer.content_size();
446                w_text_raw = cw + t_padding.horizontal();
447                shaped_buffer = Some(buffer);
448                
449                // Re-calculate boxes
450                w_box = if self.fill_x { max_available_w } else { self.width.unwrap_or_else(|| (w_text_raw + self.padding.horizontal()).min(max_available_w)).max(self.min_width) };
451                w_view = w_box - self.padding.horizontal();
452            }
453        }
454
455        // --- 4. Scroll & Auto-Scroll Calculation ---
456        let scroll_state_id = self.id;
457        let (mut scroll_x, scroll_y) = *self.ui.scroll_state.get(&scroll_state_id).unwrap_or(&(0.0, 0.0));
458        let max_scroll = (w_text_raw - w_view).max(0.0f32);
459
460        let scroll_bar_h = 4.0;
461        let scroll_bar_y = self.y + h_box - scroll_bar_h - 2.0;
462        let thumb_w = if w_text_raw > w_view { (w_view / w_text_raw) * w_view } else { w_view };
463        let thumb_w = thumb_w.max(20.0);
464        let scroll_percent = if max_scroll > 0.0 { scroll_x / max_scroll } else { 0.0 };
465        let track_width = (w_view - thumb_w).max(0.0);
466        let thumb_x = self.x + self.padding.left + scroll_percent * track_width;
467
468        let actual_thumb_x = actual_x + self.padding.left + scroll_percent * track_width;
469        let actual_scroll_bar_y = actual_y + h_box - scroll_bar_h - 2.0;
470        let is_over_thumb = self.ui.mouse_in_rect(actual_thumb_x, actual_scroll_bar_y - 2.0, thumb_w, scroll_bar_h + 4.0);
471
472        // Handle dragging
473        if let Some(drag) = self.ui.active_drag {
474            if drag.id == scroll_state_id {
475                let delta_mouse = self.ui.mouse_x - drag.start_mouse;
476                if track_width > 0.0 {
477                    let scroll_delta = (delta_mouse / track_width) * max_scroll;
478                    scroll_x = (drag.start_scroll + scroll_delta).clamp(0.0, max_scroll);
479                }
480            }
481        }
482        
483        if self.ui.clicked && is_over_thumb {
484            self.ui.active_drag = Some(ScrollDrag {
485                id: scroll_state_id,
486                start_mouse: self.ui.mouse_x,
487                start_scroll: scroll_x,
488            });
489        }
490
491        // Auto-scroll logic (Now uses FRESH buffer)
492        let is_dragging_this = self.ui.active_drag.map(|d| d.id == scroll_state_id).unwrap_or(false);
493        if is_focused && !is_dragging_this && needs_auto_scroll {
494            if let Some(sb) = &shaped_buffer {
495                let mut clx = 0.0;
496                let mut found = false;
497                for g in sb.glyphs() {
498                    if g.cluster == cursor_index {
499                        clx = g.x;
500                        found = true;
501                        break;
502                    }
503                }
504                if !found && cursor_index == self.buffer.len() {
505                    clx = sb.glyphs().last().map(|g: &ShapedGlyph| g.x + g.width).unwrap_or(0.0);
506                }
507
508                let cursor_x_v = clx + self.text_padding.left;
509                if cursor_x_v > scroll_x + w_view - 40.0 {
510                    scroll_x = cursor_x_v - w_view + 40.0;
511                } else if cursor_x_v < scroll_x + 10.0 {
512                    scroll_x = (cursor_x_v - 10.0).max(0.0);
513                }
514            }
515        }
516
517        scroll_x = scroll_x.clamp(0.0, max_scroll);
518        self.ui.scroll_state.insert(scroll_state_id, (scroll_x, scroll_y));
519
520        // --- 4. Render Background ---
521        let start_draw = self.ui.draws.len();
522        if let Some(bg) = self.bg {
523            use crate::ui::RectDraw;
524            use zenthra_render::RectInstance;
525            self.ui.draws.push(DrawCommand::Rect(RectDraw {
526                instance: RectInstance {
527                    pos: [self.x, self.y],
528                    size: [w_box, h_box],
529                    color: bg.to_array(),
530                    radius: [
531                        self.radius[3],
532                        self.radius[2],
533                        self.radius[1],
534                        self.radius[0],
535                    ],
536                    border_width: if is_focused { self.border_width.max(1.0) } else { self.border_width },
537                    border_color: if is_focused { self.border_color.unwrap_or(Color::rgb(0.4, 0.7, 1.0)).to_array() } else { self.border_color.unwrap_or(Color::TRANSPARENT).to_array() },
538                    shadow_color: self.shadow_color.map(|mut c| { c.a *= self.shadow_opacity; c.to_array() }).unwrap_or([0.0; 4]),
539                    shadow_offset: self.shadow_offset,
540                    shadow_blur: self.shadow_blur,
541                    clip_rect: [0.0, 0.0, 9999.0, 9999.0],
542                    grayscale: 0.0,
543                    brightness: 1.0,
544                    opacity: self.opacity,
545                    ..Default::default()
546                }
547            }));
548        }
549
550        // --- 5. Render Text ---
551        let mut text_builder = TextBuilder::new(self.ui, &self.buffer)
552            .size(self.font_size)
553            .line_height(self.line_height)
554            .color(self.color)
555            .fill_x(self.fill_x)
556            .padding(self.text_padding.top, self.text_padding.right, self.text_padding.bottom, self.text_padding.left)
557            .wrap(zenthra_text::prelude::TextWrap::None)
558            .max_width(1000000.0) 
559            .pos(self.x + self.padding.left - scroll_x, self.y + self.padding.top)
560            .clip_rect(self.x, self.y, w_box, h_box);
561
562        if self.text_bg_fill_x {
563            text_builder = text_builder.min_width(w_view);
564        }
565        
566        if let Some(tbg) = self.text_bg {
567            text_builder = text_builder.bg(tbg).fill_x(false);
568        }
569
570        if let Some(h) = self.highlight {
571            text_builder = text_builder.highlight(h);
572        }
573        
574        let (_, _, final_sb, _) = text_builder.draw_and_measure();
575        
576        // --- 6. Cursor Rendering ---
577        if is_focused {
578            let font_size = self.font_size;
579            let lh = self.line_height;
580            let cursor_height = font_size * lh; 
581
582            if let Some(sb) = final_sb {
583                let mut lx = 0.0;
584                let mut found = false;
585                for g in sb.glyphs() {
586                    if g.cluster == cursor_index {
587                        lx = g.x;
588                        found = true;
589                        break;
590                    }
591                }
592
593                if !found && cursor_index == self.buffer.len() {
594                    lx = sb.glyphs().last().map(|g: &ShapedGlyph| g.x + g.width).unwrap_or(0.0);
595                }
596
597                let cx = lx + self.x + self.padding.left + self.text_padding.left - scroll_x;
598                let cy = self.y + self.padding.top + self.text_padding.top;
599                
600                // Smart Blink: Solid while typing, blink when idle
601                let last_activity = *self.ui.interaction_state.get(&self.id).unwrap_or(&0.0);
602                let time_since_activity = self.ui.elapsed_time - last_activity;
603                
604                let is_blink_visible = if time_since_activity < 0.5 {
605                    true // Solid during and just after activity
606                } else {
607                    // Start blinking after 500ms of idle. 
608                    // Offset by last_activity so the cycle always starts "ON" when you stop.
609                    (self.ui.elapsed_time - last_activity).fract() < 0.5
610                };
611
612                if is_blink_visible {
613                    self.ui.draws.push(DrawCommand::OverlayRect(OverlayRectDraw {
614                        x: cx,
615                        y: cy,
616                        width: 2.0,
617                        height: cursor_height,
618                        color: Color::WHITE,
619                        clip: [self.x, self.y, w_box, h_box],
620                    }));
621                }
622            }
623        }
624
625        // --- 7. Render Horizontal Scroll Bar ---
626        if self.scrollable && w_text_raw > w_view {
627            let is_dragging = self.ui.active_drag.map(|d| d.id == scroll_state_id).unwrap_or(false);
628            
629            self.ui.draws.push(DrawCommand::OverlayRect(OverlayRectDraw {
630                x: thumb_x,
631                y: scroll_bar_y,
632                width: thumb_w,
633                height: scroll_bar_h,
634                color: if is_dragging { Color::rgba(1.0, 1.0, 1.0, 0.8) } else { Color::rgba(1.0, 1.0, 1.0, 0.4) },
635                clip: [self.x, self.y, w_box, h_box],
636            }));
637        }
638
639        // --- 8. Semantic Registration ---
640        self.ui.register_semantic(
641            SemanticNode::new(self.id, Role::TextInput, Rect::new(self.x, self.y, w_box, h_box))
642                .with_label(self.buffer.clone())
643                .with_focus(is_focused)
644        );
645
646        // --- 9. Advance UI ---
647        self.ui.record_layout(self.id, Rect::new(self.x, self.y, w_box, h_box));
648        self.ui.advance(w_box, h_box, start_draw);
649
650        if self.render_mode.is_some() {
651            self.ui.render_mode_stack.pop();
652        }
653
654        zenthra_core::Response {
655            clicked: self.ui.clicked && is_hovered,
656            hovered: is_hovered,
657            pressed: is_hovered && self.ui.mouse_down,
658        }
659    }
660}