Skip to main content

bexa_ui_core/widgets/
text_input.rs

1use std::cell::Cell;
2use std::time::Instant;
3
4use arboard::Clipboard;
5use glyphon::Metrics;
6use glyphon::cosmic_text::Align;
7use taffy::prelude::*;
8use winit::event::{ElementState, MouseButton, WindowEvent, KeyEvent};
9use winit::keyboard::{Key, ModifiersState, NamedKey};
10
11use crate::framework::{DrawContext, EventContext, Widget};
12use crate::signal::SetSignal;
13
14pub struct TextInput {
15    text: String,
16    cursor_pos: usize,
17    selection: Option<(usize, usize)>,
18    on_change: Option<SetSignal<String>>,
19    metrics: Metrics,
20    text_color: [u8; 3],
21    placeholder_color: [u8; 3],
22    placeholder: String,
23    background: [f32; 4],
24    focus_border_color: [f32; 4],
25    border_radius: f32,
26    padding: f32,
27    focused: bool,
28    last_input_time: Instant,
29    /// Cached pixel width of text before cursor, updated by render layer
30    pub(crate) cursor_pixel_x: f32,
31    /// Cached pixel positions for selection highlight
32    selection_lo_px: f32,
33    selection_hi_px: f32,
34    /// Pixel x-positions of each character edge (0..=char_count), for click-to-position
35    char_edges: Vec<f32>,
36    /// Whether mouse is currently dragging a selection
37    mouse_dragging: bool,
38    /// Last known cursor position (window coords)
39    last_mouse_x: f32,
40    /// Index of the text command emitted during draw (for measure feedback)
41    text_cmd_index: Cell<Option<usize>>,
42}
43
44impl TextInput {
45    pub fn new(on_change: SetSignal<String>) -> Self {
46        Self {
47            text: String::new(),
48            cursor_pos: 0,
49            selection: None,
50            on_change: Some(on_change),
51            metrics: Metrics::new(16.0, 22.0),
52            text_color: [230, 230, 230],
53            placeholder_color: [120, 120, 140],
54            placeholder: String::new(),
55            background: [0.12, 0.16, 0.22, 1.0],
56            focus_border_color: [0.3, 0.6, 0.9, 1.0],
57            border_radius: 6.0,
58            padding: 10.0,
59            focused: false,
60            last_input_time: Instant::now(),
61            cursor_pixel_x: 0.0,
62            selection_lo_px: 0.0,
63            selection_hi_px: 0.0,
64            char_edges: Vec::new(),
65            mouse_dragging: false,
66            last_mouse_x: 0.0,
67            text_cmd_index: Cell::new(None),
68        }
69    }
70
71    pub fn with_placeholder(mut self, text: impl Into<String>) -> Self {
72        self.placeholder = text.into();
73        self
74    }
75
76    pub fn with_metrics(mut self, metrics: Metrics) -> Self {
77        self.metrics = metrics;
78        self
79    }
80
81    pub fn with_text_color(mut self, color: [u8; 3]) -> Self {
82        self.text_color = color;
83        self
84    }
85
86    pub fn with_background(mut self, color: [f32; 4]) -> Self {
87        self.background = color;
88        self
89    }
90
91    pub fn with_border_radius(mut self, radius: f32) -> Self {
92        self.border_radius = radius;
93        self
94    }
95
96    pub fn with_padding(mut self, padding: f32) -> Self {
97        self.padding = padding;
98        self
99    }
100
101    pub fn with_initial_value(mut self, text: impl Into<String>) -> Self {
102        self.text = text.into();
103        self.cursor_pos = self.text.len();
104        self
105    }
106
107    fn notify_change(&self) {
108        if let Some(ref sig) = self.on_change {
109            sig.set(self.text.clone());
110        }
111    }
112
113    fn insert_text(&mut self, s: &str) {
114        self.delete_selection();
115        let byte_pos = self.cursor_byte_pos();
116        self.text.insert_str(byte_pos, s);
117        self.cursor_pos += s.chars().count();
118        self.last_input_time = Instant::now();
119        self.notify_change();
120    }
121
122    fn delete_back(&mut self) {
123        if self.delete_selection() {
124            return;
125        }
126        if self.cursor_pos > 0 {
127            self.cursor_pos -= 1;
128            let byte_pos = self.cursor_byte_pos();
129            self.text.remove(byte_pos);
130            self.last_input_time = Instant::now();
131            self.notify_change();
132        }
133    }
134
135    fn delete_forward(&mut self) {
136        if self.delete_selection() {
137            return;
138        }
139        let char_count = self.text.chars().count();
140        if self.cursor_pos < char_count {
141            let byte_pos = self.cursor_byte_pos();
142            self.text.remove(byte_pos);
143            self.last_input_time = Instant::now();
144            self.notify_change();
145        }
146    }
147
148    fn delete_selection(&mut self) -> bool {
149        if let Some((start, end)) = self.selection.take() {
150            let (lo, hi) = if start < end { (start, end) } else { (end, start) };
151            let lo_byte = self.char_to_byte(lo);
152            let hi_byte = self.char_to_byte(hi);
153            self.text.replace_range(lo_byte..hi_byte, "");
154            self.cursor_pos = lo;
155            self.last_input_time = Instant::now();
156            self.notify_change();
157            true
158        } else {
159            false
160        }
161    }
162
163    fn move_cursor(&mut self, delta: i32, shift: bool) {
164        let char_count = self.text.chars().count();
165        let old_pos = self.cursor_pos;
166
167        if delta < 0 {
168            self.cursor_pos = self.cursor_pos.saturating_sub((-delta) as usize);
169        } else {
170            self.cursor_pos = (self.cursor_pos + delta as usize).min(char_count);
171        }
172
173        if shift {
174            match self.selection {
175                None => self.selection = Some((old_pos, self.cursor_pos)),
176                Some((anchor, _)) => self.selection = Some((anchor, self.cursor_pos)),
177            }
178        } else {
179            self.selection = None;
180        }
181
182        self.last_input_time = Instant::now();
183    }
184
185    fn select_all(&mut self) {
186        let char_count = self.text.chars().count();
187        self.selection = Some((0, char_count));
188        self.cursor_pos = char_count;
189    }
190
191    fn selected_text(&self) -> Option<String> {
192        let (start, end) = self.selection?;
193        let (lo, hi) = if start < end { (start, end) } else { (end, start) };
194        let lo_byte = self.char_to_byte(lo);
195        let hi_byte = self.char_to_byte(hi);
196        Some(self.text[lo_byte..hi_byte].to_string())
197    }
198
199    fn copy_selection(&self) {
200        if let Some(text) = self.selected_text() {
201            if let Ok(mut cb) = Clipboard::new() {
202                let _ = cb.set_text(text);
203            }
204        }
205    }
206
207    fn cut_selection(&mut self) {
208        self.copy_selection();
209        self.delete_selection();
210    }
211
212    fn paste(&mut self) {
213        if let Ok(mut cb) = Clipboard::new() {
214            if let Ok(text) = cb.get_text() {
215                self.insert_text(&text);
216            }
217        }
218    }
219
220    fn cursor_byte_pos(&self) -> usize {
221        self.char_to_byte(self.cursor_pos)
222    }
223
224    fn char_to_byte(&self, char_pos: usize) -> usize {
225        self.text
226            .char_indices()
227            .nth(char_pos)
228            .map(|(i, _)| i)
229            .unwrap_or(self.text.len())
230    }
231
232    fn cursor_visible(&self) -> bool {
233        let elapsed = self.last_input_time.elapsed().as_millis();
234        (elapsed % 1060) < 530
235    }
236
237    /// Returns the text substring before the cursor position.
238    pub fn text_before_cursor(&self) -> &str {
239        let byte_pos = self.cursor_byte_pos();
240        &self.text[..byte_pos]
241    }
242
243    /// Returns the full text content.
244    pub fn text(&self) -> &str {
245        &self.text
246    }
247
248    /// Returns cursor position in chars.
249    pub fn cursor(&self) -> usize {
250        self.cursor_pos
251    }
252
253    /// Returns whether the widget has focus.
254    pub fn is_focused(&self) -> bool {
255        self.focused
256    }
257
258    /// Given an absolute x pixel position, find the closest char position using glyph edges.
259    fn char_pos_from_x(&self, layout: &Layout, x: f32) -> usize {
260        let text_x = layout.location.x + self.padding;
261        let rel_x = x - text_x;
262        if self.char_edges.is_empty() {
263            return 0;
264        }
265        // Find the edge closest to rel_x
266        let mut best = 0;
267        let mut best_dist = f32::MAX;
268        for (i, &edge) in self.char_edges.iter().enumerate() {
269            let dist = (edge - rel_x).abs();
270            if dist < best_dist {
271                best_dist = dist;
272                best = i;
273            }
274        }
275        best
276    }
277
278    fn hit_test(&self, layout: &Layout, x: f32, y: f32) -> bool {
279        x >= layout.location.x
280            && x <= layout.location.x + layout.size.width
281            && y >= layout.location.y
282            && y <= layout.location.y + layout.size.height
283    }
284}
285
286impl Widget for TextInput {
287    fn style(&self) -> Style {
288        let height = self.metrics.line_height + self.padding * 2.0;
289        Style {
290            size: Size {
291                width: Dimension::Percent(1.0),
292                height: Dimension::Length(height),
293            },
294            flex_shrink: 0.0,
295            ..Default::default()
296        }
297    }
298
299    fn draw(&self, ctx: &mut DrawContext) {
300        let layout = ctx.layout;
301        let x = layout.location.x;
302        let y = layout.location.y;
303        let w = layout.size.width;
304        let h = layout.size.height;
305
306        // Background
307        let border_w = if self.focused { 1.5 } else { 0.0 };
308        let border_c = if self.focused {
309            self.focus_border_color
310        } else {
311            [0.0; 4]
312        };
313        ctx.renderer.fill_rect_styled(
314            (x, y, w, h),
315            self.background,
316            self.border_radius,
317            border_w,
318            border_c,
319        );
320
321        let text_x = x + self.padding;
322        let text_y = y + self.padding;
323        let text_w = (w - self.padding * 2.0).max(0.0);
324        let text_h = (h - self.padding * 2.0).max(0.0);
325
326        // Selection highlight
327        if let Some((start, end)) = self.selection {
328            let (lo, hi) = if start < end { (start, end) } else { (end, start) };
329            if lo != hi {
330                let sel_x0 = text_x + self.selection_lo_px;
331                let sel_x1 = text_x + self.selection_hi_px;
332                let sel_w = (sel_x1 - sel_x0).max(0.0);
333                ctx.renderer.fill_rect_rounded(
334                    (sel_x0, text_y, sel_w, text_h),
335                    [0.2, 0.4, 0.7, 0.5],
336                    2.0,
337                );
338            }
339        }
340
341        // Text or placeholder
342        if self.text.is_empty() && !self.placeholder.is_empty() {
343            self.text_cmd_index.set(None);
344            ctx.renderer.draw_text(
345                &self.placeholder,
346                (text_x, text_y),
347                self.placeholder_color,
348                (text_w, text_h),
349                self.metrics,
350                Align::Left,
351            );
352        } else {
353            // Measure all char edges [0, 1, 2, ..., char_count] for mouse positioning
354            let char_count = self.text.chars().count();
355            let measure: Vec<usize> = (0..=char_count).collect();
356            let idx = ctx.renderer.draw_text_measured(
357                &self.text,
358                (text_x, text_y),
359                self.text_color,
360                (text_w, text_h),
361                self.metrics,
362                Align::Left,
363                measure,
364            );
365            self.text_cmd_index.set(Some(idx));
366        }
367
368        // Cursor (caret) — positioned using real pixel width from render layer
369        if self.focused && self.cursor_visible() {
370            let cursor_x = text_x + self.cursor_pixel_x;
371            let cursor_h = self.metrics.font_size;
372            let cursor_y = text_y + (text_h - cursor_h) * 0.5;
373            ctx.renderer.fill_rect_rounded(
374                (cursor_x, cursor_y, 1.5, cursor_h),
375                [0.4, 0.7, 1.0, 1.0],
376                0.0,
377            );
378        }
379    }
380
381    fn handle_event(&mut self, ctx: &mut EventContext) -> bool {
382        let layout = ctx.layout;
383        match ctx.event {
384            WindowEvent::MouseInput {
385                state: ElementState::Pressed,
386                button: MouseButton::Left,
387                ..
388            } => {
389                if self.hit_test(layout, self.last_mouse_x, layout.location.y + 1.0) {
390                    let pos = self.char_pos_from_x(layout, self.last_mouse_x);
391                    self.cursor_pos = pos;
392                    self.selection = None;
393                    self.mouse_dragging = true;
394                    self.last_input_time = Instant::now();
395                    true
396                } else {
397                    false
398                }
399            }
400            WindowEvent::MouseInput {
401                state: ElementState::Released,
402                button: MouseButton::Left,
403                ..
404            } => {
405                self.mouse_dragging = false;
406                false
407            }
408            WindowEvent::CursorMoved { position, .. } => {
409                self.last_mouse_x = position.x as f32;
410                if self.mouse_dragging && self.focused {
411                    let pos = self.char_pos_from_x(layout, position.x as f32);
412                    if pos != self.cursor_pos {
413                        let anchor = match self.selection {
414                            Some((anchor, _)) => anchor,
415                            None => self.cursor_pos,
416                        };
417                        self.cursor_pos = pos;
418                        if anchor != pos {
419                            self.selection = Some((anchor, pos));
420                        } else {
421                            self.selection = None;
422                        }
423                        self.last_input_time = Instant::now();
424                    }
425                    true
426                } else {
427                    false
428                }
429            }
430            _ => false,
431        }
432    }
433
434    fn handle_key_event(&mut self, event: &KeyEvent, modifiers: ModifiersState) -> bool {
435        let ctrl = modifiers.control_key();
436        let shift = modifiers.shift_key();
437
438        if ctrl {
439            match &event.logical_key {
440                Key::Character(c) if c.as_str() == "a" => {
441                    self.select_all();
442                    return true;
443                }
444                Key::Character(c) if c.as_str() == "c" => {
445                    self.copy_selection();
446                    return true;
447                }
448                Key::Character(c) if c.as_str() == "v" => {
449                    self.paste();
450                    return true;
451                }
452                Key::Character(c) if c.as_str() == "x" => {
453                    self.cut_selection();
454                    return true;
455                }
456                _ => {}
457            }
458        }
459
460        match &event.logical_key {
461            Key::Character(c) => {
462                if !ctrl {
463                    self.insert_text(c.as_str());
464                    return true;
465                }
466                false
467            }
468            Key::Named(NamedKey::Backspace) => {
469                self.delete_back();
470                true
471            }
472            Key::Named(NamedKey::Delete) => {
473                self.delete_forward();
474                true
475            }
476            Key::Named(NamedKey::ArrowLeft) => {
477                self.move_cursor(-1, shift);
478                true
479            }
480            Key::Named(NamedKey::ArrowRight) => {
481                self.move_cursor(1, shift);
482                true
483            }
484            Key::Named(NamedKey::Home) => {
485                let old = self.cursor_pos;
486                self.cursor_pos = 0;
487                if shift {
488                    match self.selection {
489                        None => self.selection = Some((old, 0)),
490                        Some((anchor, _)) => self.selection = Some((anchor, 0)),
491                    }
492                } else {
493                    self.selection = None;
494                }
495                self.last_input_time = Instant::now();
496                true
497            }
498            Key::Named(NamedKey::End) => {
499                let old = self.cursor_pos;
500                let end = self.text.chars().count();
501                self.cursor_pos = end;
502                if shift {
503                    match self.selection {
504                        None => self.selection = Some((old, end)),
505                        Some((anchor, _)) => self.selection = Some((anchor, end)),
506                    }
507                } else {
508                    self.selection = None;
509                }
510                self.last_input_time = Instant::now();
511                true
512            }
513            Key::Named(NamedKey::Tab) => false,
514            Key::Named(NamedKey::Enter) => false,
515            _ => false,
516        }
517    }
518
519    fn update_measures(&mut self, measures: &[Vec<f32>]) {
520        if let Some(idx) = self.text_cmd_index.get() {
521            if let Some(edges) = measures.get(idx) {
522                self.char_edges = edges.clone();
523                // cursor_pixel_x = edge at cursor_pos
524                if let Some(&w) = edges.get(self.cursor_pos) {
525                    self.cursor_pixel_x = w;
526                }
527                // selection highlight positions
528                if let Some((start, end)) = self.selection {
529                    let (lo, hi) = if start < end { (start, end) } else { (end, start) };
530                    self.selection_lo_px = edges.get(lo).copied().unwrap_or(0.0);
531                    self.selection_hi_px = edges.get(hi).copied().unwrap_or(0.0);
532                }
533            }
534        }
535    }
536
537    fn is_focusable(&self) -> bool {
538        true
539    }
540
541    fn set_focus(&mut self, focused: bool) {
542        self.focused = focused;
543        if focused {
544            self.last_input_time = Instant::now();
545        }
546    }
547}