Skip to main content

vizia_core/views/
textbox.rs

1use std::cell::RefCell;
2use std::ops::Range;
3use std::rc::Rc;
4
5use crate::prelude::*;
6
7use crate::text::{
8    Direction, EditableText, Movement, PreeditBackup, Selection, VerticalMovement, apply_movement,
9    enforce_text_bounds, ensure_visible, offset_for_delete_backwards, resolved_text_direction,
10};
11use accesskit::{ActionData, ActionRequest, TextDirection, TextPosition, TextSelection};
12use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
13use skia_safe::{ClipOp, Paint, PaintStyle, Rect};
14use unicode_segmentation::UnicodeSegmentation;
15
16/// Events for modifying a textbox.
17pub enum TextEvent {
18    /// Insert a string of text into the textbox.
19    InsertText(String),
20    /// Update the preedit text of the textbox (for IME input).
21    UpdatePreedit(String, Option<(usize, usize)>),
22    /// Clear the preedit text of the textbox.
23    ClearPreedit,
24    /// Reset the text of the textbox to the bound data.
25    Clear,
26    /// Delete a section of text, determined by the `Movement`.
27    DeleteText(Movement),
28    /// Move the cursor and selection.
29    MoveCursor(Movement, bool),
30    /// Select all text.
31    SelectAll,
32    /// Select the word at the current cursor position.
33    SelectWord,
34    /// Select the paragraph at the current cursor position.
35    SelectParagraph,
36    /// Toggle the textbox to allow text input.
37    StartEdit,
38    /// Toggle the textbox to *not* allow text input.
39    EndEdit,
40    /// Trigger the `on_submit` callback with the current text.
41    Submit(bool),
42    /// Specify the 'hit' position of the mouse cursor.
43    Hit(f32, f32, bool),
44    /// Specify the 'drag' position of the mouse cursor.
45    Drag(f32, f32),
46    /// Specify the scroll offset of the textbox.
47    Scroll(f32, f32),
48    /// Copy the textbox buffer to the clipboard.
49    Copy,
50    /// Paste the clipboard buffer into the textbox.
51    Paste,
52    /// Cut the textbox text and place it in the clipboard.
53    Cut,
54    /// Set the placeholder text of the textbox.
55    SetPlaceholder(String),
56    /// Trigger the `on_blur` callback.
57    Blur,
58    /// Set whether masked text should be visible.
59    SetMaskVisible(bool),
60    /// Toggle whether masked text should be visible.
61    ToggleMaskVisible,
62    /// Toggle the visibility of the text Caret.
63    ToggleCaret,
64}
65
66/// The `Textbox` view provides an input control for editing a value as a string.
67///
68/// The textbox takes a lens to some value, which must be a type which can convert to and from a `String`,
69/// as determined by the `ToString` and `FromStr` traits. The value type is used for validation and returned by
70/// the `on_submit` callback, which is triggered when the textbox is submitted with the enter key or when the textbox
71/// loses keyboard focus.
72pub struct Textbox<R, T> {
73    value: R,
74    kind: TextboxKind,
75    edit: bool,
76    transform: Rc<RefCell<(f32, f32)>>,
77    on_edit: Option<Box<dyn Fn(&mut EventContext, String) + Send + Sync>>,
78    on_submit: Option<Box<dyn Fn(&mut EventContext, T, bool) + Send + Sync>>,
79    on_blur: Option<Box<dyn Fn(&mut EventContext) + Send + Sync>>,
80    on_cancel: Option<Box<dyn Fn(&mut EventContext) + Send + Sync>>,
81    validate: Option<Box<dyn Fn(&T) -> bool>>,
82    placeholder: Signal<String>,
83    show_placeholder: Signal<bool>,
84    show_caret: Signal<bool>,
85    mask_char: Signal<Option<char>>,
86    max_length: Signal<Option<usize>>,
87    can_copy: Signal<bool>,
88    can_paste: Signal<bool>,
89    mask_visible: bool,
90    real_text: String,
91    caret_timer: Timer,
92    selection: Selection,
93    preedit_backup: Option<PreeditBackup>,
94    text_overflow: Option<TextOverflow>,
95    edited_since_focus: bool,
96    edited_once: bool,
97}
98
99// Determines whether the enter key submits the text or inserts a new line.
100#[derive(Copy, Clone, PartialEq, Eq)]
101enum TextboxKind {
102    SingleLine,
103    MultiLineUnwrapped,
104    MultiLineWrapped,
105}
106
107impl<R, T> Textbox<R, T>
108where
109    R: Res<T> + 'static,
110    T: Clone + ToStringLocalized + std::str::FromStr + 'static,
111{
112    /// Creates a new single-line textbox.
113    ///
114    /// # Example
115    /// ```rust
116    /// # use vizia_core::prelude::*;
117    /// #
118    /// # #[derive(Lens)]
119    /// # struct AppData {
120    /// #     text: String,
121    /// # }
122    /// #
123    /// # impl Model for AppData {}
124    /// #
125    /// # let cx = &mut Context::default();
126    /// #
127    /// # AppData { text: String::from("Hello World") }.build(cx);
128    /// #
129    /// Textbox::new(cx, AppData::text);
130    /// ```
131    pub fn new(cx: &mut Context, value: R) -> Handle<Self>
132    where
133        R: Clone,
134    {
135        Self::new_core(cx, value, TextboxKind::SingleLine)
136    }
137
138    /// Creates a new multi-line textbox.
139    ///
140    /// The `wrap` parameter determines whether text which is too long for the textbox
141    /// should soft-wrap onto multiple lines. If false, then only hard-wraps from line breaks
142    /// will cause the text to span multiple lines.
143    ///
144    /// # Example
145    /// ```rust
146    /// # use vizia_core::prelude::*;
147    /// #
148    /// # #[derive(Lens)]
149    /// # struct AppData {
150    /// #     text: String,
151    /// # }
152    /// #
153    /// # impl Model for AppData {}
154    /// #
155    /// # let cx = &mut Context::default();
156    /// #
157    /// # AppData { text: String::from("Hello World") }.build(cx);
158    /// #
159    /// Textbox::new_multiline(cx, AppData::text, true);
160    /// ```
161    pub fn new_multiline(cx: &mut Context, value: R, wrap: bool) -> Handle<Self>
162    where
163        R: Clone,
164    {
165        Self::new_core(
166            cx,
167            value,
168            if wrap { TextboxKind::MultiLineWrapped } else { TextboxKind::MultiLineUnwrapped },
169        )
170    }
171
172    fn new_core(cx: &mut Context, value: R, kind: TextboxKind) -> Handle<Self>
173    where
174        R: Clone,
175    {
176        let value_text = value.clone().to_signal(cx);
177        let caret_timer = cx.environment().caret_timer;
178        let initial_text = value.get_value(cx).to_string_local(cx);
179        let show_caret = Signal::new(false);
180        let mask_char = Signal::new(None);
181        let max_length = Signal::new(None);
182        let can_copy = Signal::new(true);
183        let can_paste = Signal::new(true);
184        let placeholder = Signal::new(String::from(""));
185        let show_placeholder = Signal::new(initial_text.is_empty());
186
187        Self {
188            value: value.clone(),
189            kind,
190            edit: false,
191            transform: Rc::new(RefCell::new((0.0, 0.0))),
192            on_edit: None,
193            on_submit: None,
194            on_blur: None,
195            on_cancel: None,
196            validate: None,
197            placeholder,
198            show_placeholder,
199            show_caret,
200            mask_char,
201            max_length,
202            can_copy,
203            can_paste,
204            mask_visible: false,
205            real_text: initial_text.clone(),
206            caret_timer,
207            selection: Selection::new(0, 0),
208            preedit_backup: None,
209            text_overflow: None,
210            edited_since_focus: false,
211            edited_once: false,
212        }
213        .build(cx, move |cx| {
214            cx.add_listener(move |textbox: &mut Self, cx, event| {
215                let flag: bool = textbox.edit;
216                event.map(|window_event, meta| match window_event {
217                    WindowEvent::MouseDown(_) => {
218                        if flag && meta.origin != cx.current() && cx.hovered() != cx.current() {
219                            cx.emit(TextEvent::Blur);
220                        }
221                    }
222
223                    _ => {}
224                });
225            });
226        })
227        .toggle_class("multiline", kind == TextboxKind::MultiLineWrapped)
228        .text_wrap(kind == TextboxKind::MultiLineWrapped)
229        .navigable(true)
230        .role(if kind == TextboxKind::SingleLine {
231            Role::TextInput
232        } else {
233            Role::MultilineTextInput
234        })
235        .text_value(value.clone())
236        .toggle_class("caret", show_caret)
237        .placeholder_shown(show_placeholder)
238        .bind(value_text, move |handle| {
239            handle.bind(placeholder, move |handle| {
240                let text = value_text.get();
241                let txt = text.to_string_local(&handle);
242                let mut display_text = String::new();
243                let handle = handle.modify(|textbox| {
244                    textbox.real_text = txt.clone();
245                    textbox.show_placeholder.set_if_changed(txt.is_empty());
246                    display_text = textbox.display_text_from_real();
247                });
248                handle.text(display_text);
249            });
250        })
251    }
252
253    fn display_text_from_real(&self) -> String {
254        if self.show_placeholder.get() {
255            return self.placeholder.get().clone();
256        }
257
258        if self.mask_visible {
259            return self.real_text.clone();
260        }
261
262        let Some(mask) = self.mask_char.get() else {
263            return self.real_text.clone();
264        };
265
266        let mut masked = String::with_capacity(self.real_text.len());
267        for grapheme in self.real_text.graphemes(true) {
268            if grapheme == "\n" {
269                masked.push('\n');
270            } else {
271                masked.push(mask);
272            }
273        }
274
275        masked
276    }
277
278    fn sync_display_text(&self, cx: &mut EventContext) {
279        cx.style.text.insert(cx.current, self.display_text_from_real());
280        cx.style.needs_text_update(cx.current);
281    }
282
283    fn grapheme_index_to_byte(text: &str, idx: usize) -> usize {
284        if idx == 0 {
285            return 0;
286        }
287
288        for (i, (byte, _)) in text.grapheme_indices(true).enumerate() {
289            if i == idx {
290                return byte;
291            }
292        }
293
294        text.len()
295    }
296
297    fn byte_to_grapheme_index(text: &str, byte_offset: usize) -> usize {
298        let mut idx = 0;
299        for (byte, _) in text.grapheme_indices(true) {
300            if byte >= byte_offset {
301                break;
302            }
303            idx += 1;
304        }
305        idx
306    }
307
308    fn selection_display_to_real(
309        display_text: &str,
310        real_text: &str,
311        selection: Selection,
312    ) -> Selection {
313        let anchor_g = Self::byte_to_grapheme_index(display_text, selection.anchor);
314        let active_g = Self::byte_to_grapheme_index(display_text, selection.active);
315        let anchor = Self::grapheme_index_to_byte(real_text, anchor_g);
316        let active = Self::grapheme_index_to_byte(real_text, active_g);
317        Selection::new(anchor, active).with_h_pos(selection.h_pos)
318    }
319
320    fn remap_selection_for_display_change(&mut self, old_display: &str, new_display: &str) {
321        let anchor_g = Self::byte_to_grapheme_index(old_display, self.selection.anchor);
322        let active_g = Self::byte_to_grapheme_index(old_display, self.selection.active);
323        self.selection.anchor = Self::grapheme_index_to_byte(new_display, anchor_g);
324        self.selection.active = Self::grapheme_index_to_byte(new_display, active_g);
325    }
326
327    fn insert_text(&mut self, cx: &mut EventContext, txt: &str) {
328        if let Some(text) = cx.style.text.get_mut(cx.current) {
329            let old_display = text.clone();
330            if self.show_placeholder.get() && !txt.is_empty() {
331                text.clear();
332                self.show_placeholder.set(false);
333            }
334
335            let real_selection =
336                Self::selection_display_to_real(&old_display, &self.real_text, self.selection);
337
338            let clamped = self.clamp_insert_text(txt, real_selection.range());
339
340            text.edit(self.selection.range(), &clamped);
341            self.real_text.edit(real_selection.range(), &clamped);
342
343            // Track the caret in grapheme space so we can remap it safely after
344            // display text is regenerated (for example when masking is enabled).
345            let new_caret_grapheme =
346                Self::byte_to_grapheme_index(text, self.selection.min() + clamped.len());
347
348            self.show_placeholder.set(self.real_text.is_empty());
349            self.sync_display_text(cx);
350            if let Some(new_display) = cx.style.text.get(cx.current) {
351                let new_caret = Self::grapheme_index_to_byte(new_display, new_caret_grapheme);
352                self.selection = Selection::caret(new_caret);
353            }
354            cx.style.needs_access_update(cx.current);
355        }
356    }
357
358    fn update_preedit(
359        &mut self,
360        cx: &mut EventContext,
361        preedit_txt: &str,
362        cursor: Option<(usize, usize)>,
363    ) {
364        if preedit_txt.is_empty() || cursor.is_none() {
365            return;
366        }
367
368        if let Some(text) = cx.style.text.get_mut(cx.current) {
369            let old_display = text.clone();
370            if self.show_placeholder.get() {
371                text.clear();
372                self.show_placeholder.set(false);
373            }
374
375            if !self.selection.is_caret() {
376                let start = self.selection.min();
377                let end = self.selection.max();
378                let real_selection =
379                    Self::selection_display_to_real(&old_display, &self.real_text, self.selection);
380
381                if end > start && end <= text.len() {
382                    text.replace_range(start..end, "");
383                }
384                if real_selection.max() <= self.real_text.len() {
385                    self.real_text.replace_range(real_selection.range(), "");
386                }
387                self.selection = Selection::caret(start);
388            }
389
390            let (original_selection, prev_preedit_text) = {
391                let preedit_backup = self
392                    .preedit_backup
393                    .get_or_insert_with(|| PreeditBackup::new(String::new(), self.selection));
394                (preedit_backup.original_selection, preedit_backup.prev_preedit.clone())
395            };
396
397            let new_caret_grapheme;
398
399            if prev_preedit_text == preedit_txt {
400                // Move the cursor only
401                let caret = original_selection.min() + cursor.unwrap().0;
402                new_caret_grapheme = Self::byte_to_grapheme_index(text, caret);
403            } else {
404                // Bytes index
405                let start = original_selection.min();
406                let end = start + prev_preedit_text.chars().map(|c| c.len_utf8()).sum::<usize>();
407
408                // Delete old preedit text
409                if end > start && end <= text.len() {
410                    text.replace_range(start..end, "");
411                }
412
413                let original_real_selection = Self::selection_display_to_real(
414                    &old_display,
415                    &self.real_text,
416                    original_selection,
417                );
418                let real_start = original_real_selection.min();
419                let real_end =
420                    real_start + prev_preedit_text.chars().map(|c| c.len_utf8()).sum::<usize>();
421                let clamped_preedit = self.clamp_insert_text(preedit_txt, real_start..real_end);
422                if real_end > real_start && real_end <= self.real_text.len() {
423                    self.real_text.replace_range(real_start..real_end, "");
424                }
425                self.real_text.insert_str(real_start, &clamped_preedit);
426
427                text.insert_str(start, &clamped_preedit);
428
429                if let Some((cursor_index, _)) = cursor {
430                    let new_caret =
431                        original_selection.min() + cursor_index.min(clamped_preedit.len());
432                    new_caret_grapheme = Self::byte_to_grapheme_index(text, new_caret);
433                } else {
434                    // If there is no valid cursor, the default behavior is to move to the end of the text.
435                    let new_caret = original_selection.min() + clamped_preedit.chars().count();
436                    new_caret_grapheme = Self::byte_to_grapheme_index(text, new_caret);
437                }
438
439                self.preedit_backup.as_mut().unwrap().set_prev_preedit(clamped_preedit);
440            }
441
442            self.show_placeholder.set(self.real_text.is_empty());
443            self.sync_display_text(cx);
444            if let Some(new_display) = cx.style.text.get(cx.current) {
445                let new_caret = Self::grapheme_index_to_byte(new_display, new_caret_grapheme);
446                self.selection = Selection::caret(new_caret);
447            }
448        }
449    }
450
451    fn clear_preedit(&mut self, cx: &mut EventContext) {
452        if let Some(text) = cx.style.text.get_mut(cx.current) {
453            if let Some(preedit_backup) = self.preedit_backup.as_ref() {
454                let old_display = text.clone();
455                let original_selection = preedit_backup.original_selection;
456                let prev_preedit_text = preedit_backup.prev_preedit.clone();
457
458                let start = original_selection.min();
459                let end = start + prev_preedit_text.chars().map(|c| c.len_utf8()).sum::<usize>();
460
461                text.replace_range(start..end, "");
462
463                let original_real_selection = Self::selection_display_to_real(
464                    &old_display,
465                    &self.real_text,
466                    original_selection,
467                );
468                let real_start = original_real_selection.min();
469                let real_end =
470                    real_start + prev_preedit_text.chars().map(|c| c.len_utf8()).sum::<usize>();
471                if real_end <= self.real_text.len() {
472                    self.real_text.replace_range(real_start..real_end, "");
473                }
474
475                self.selection = original_selection;
476
477                self.preedit_backup = None;
478                self.show_placeholder.set(self.real_text.is_empty());
479                self.sync_display_text(cx);
480            }
481        }
482    }
483
484    fn delete_text(&mut self, cx: &mut EventContext, movement: Movement) {
485        if self.show_placeholder.get() {
486            return;
487        }
488
489        if self.preedit_backup.is_some() {
490            return;
491        }
492
493        if self.selection.is_caret() {
494            if movement == Movement::Grapheme(Direction::Upstream) {
495                if self.selection.active == 0 {
496                    return;
497                }
498                if let Some(text) = cx.style.text.get_mut(cx.current) {
499                    let old_display = text.clone();
500                    let del_offset = offset_for_delete_backwards(&self.selection, text);
501                    let del_range = del_offset..self.selection.active;
502                    let real_del_range = Self::selection_display_to_real(
503                        &old_display,
504                        &self.real_text,
505                        Selection::new(del_range.start, del_range.end),
506                    )
507                    .range();
508
509                    self.selection = Selection::caret(del_range.start);
510
511                    text.edit(del_range, "");
512                    self.real_text.edit(real_del_range, "");
513
514                    self.sync_display_text(cx);
515                    cx.style.needs_access_update(cx.current);
516                }
517            } else if let Some(text) = cx.style.text.get_mut(cx.current) {
518                if let Some(paragraph) = cx.text_context.text_paragraphs.get(cx.current) {
519                    let old_display = text.clone();
520                    let to_delete = apply_movement(movement, self.selection, text, paragraph, true);
521                    let real_to_delete =
522                        Self::selection_display_to_real(&old_display, &self.real_text, to_delete);
523                    self.selection = to_delete;
524                    let new_cursor_pos = self.selection.min();
525
526                    text.edit(to_delete.range(), "");
527                    self.real_text.edit(real_to_delete.range(), "");
528                    self.selection = Selection::caret(new_cursor_pos);
529
530                    self.sync_display_text(cx);
531                    cx.style.needs_access_update(cx.current);
532                }
533            }
534        } else if let Some(text) = cx.style.text.get_mut(cx.current) {
535            let old_display = text.clone();
536            let del_range = self.selection.range();
537            let real_del_range =
538                Self::selection_display_to_real(&old_display, &self.real_text, self.selection)
539                    .range();
540
541            self.selection = Selection::caret(del_range.start);
542
543            text.edit(del_range, "");
544            self.real_text.edit(real_del_range, "");
545
546            self.sync_display_text(cx);
547            cx.style.needs_access_update(cx.current);
548        }
549
550        self.show_placeholder.set(self.real_text.is_empty());
551    }
552
553    fn reset_text(&mut self, cx: &mut EventContext) {
554        if let Some(text) = cx.style.text.get_mut(cx.current) {
555            text.clear();
556            self.real_text.clear();
557            self.selection = Selection::caret(0);
558            self.show_placeholder.set(true);
559            self.sync_display_text(cx);
560            cx.style.needs_access_update(cx.current);
561        }
562    }
563
564    /// When IME is enabled, the cursor movement logic will be controlled by [`update_preedit`].
565    ///
566    /// [`update_preedit`]: Textbox::update_preedit
567    fn move_cursor(&mut self, cx: &mut EventContext, movement: Movement, selection: bool) {
568        if let Some(text) = cx.style.text.get_mut(cx.current) {
569            if let Some(paragraph) = cx.text_context.text_paragraphs.get(cx.current) {
570                let new_selection =
571                    apply_movement(movement, self.selection, text, paragraph, selection);
572                self.selection = new_selection;
573                cx.needs_redraw();
574                cx.style.needs_access_update(cx.current);
575            }
576        }
577    }
578
579    fn select_all(&mut self, cx: &mut EventContext) {
580        if self.show_placeholder.get() {
581            return;
582        }
583        if let Some(text) = cx.style.text.get(cx.current) {
584            self.selection.anchor = 0;
585            self.selection.active = text.len();
586            cx.needs_redraw();
587            cx.style.needs_access_update(cx.current);
588        }
589    }
590
591    fn select_word(&mut self, cx: &mut EventContext) {
592        if self.show_placeholder.get() {
593            return;
594        }
595        self.move_cursor(cx, Movement::Word(Direction::Upstream), false);
596        self.move_cursor(cx, Movement::Word(Direction::Downstream), true);
597    }
598
599    fn select_paragraph(&mut self, cx: &mut EventContext) {
600        if self.show_placeholder.get() {
601            return;
602        }
603        self.move_cursor(cx, Movement::ParagraphStart, false);
604        self.move_cursor(cx, Movement::ParagraphEnd, true);
605    }
606
607    fn deselect(&mut self) {
608        self.selection = Selection::caret(self.selection.active);
609    }
610
611    /// These input coordinates should be physical coordinates, i.e. what the mouse events provide.
612    /// The output text coordinates will also be physical, but relative to the top of the text
613    /// glyphs, appropriate for passage to cosmic.
614    fn coordinates_global_to_text(&self, cx: &EventContext, x: f32, y: f32) -> (f32, f32) {
615        let bounds = cx.bounds();
616
617        if let Some(paragraph) = cx.text_context.text_paragraphs.get(cx.current) {
618            let padding_left = cx
619                .style
620                .padding_left
621                .get_resolved(cx.current, &cx.style.custom_units_props)
622                .unwrap_or_default();
623            let padding_top = cx
624                .style
625                .padding_top
626                .get_resolved(cx.current, &cx.style.custom_units_props)
627                .unwrap_or_default();
628            let padding_right = cx
629                .style
630                .padding_right
631                .get_resolved(cx.current, &cx.style.custom_units_props)
632                .unwrap_or_default();
633            let padding_bottom = cx
634                .style
635                .padding_bottom
636                .get_resolved(cx.current, &cx.style.custom_units_props)
637                .unwrap_or_default();
638
639            let logical_parent_width = cx.physical_to_logical(bounds.w);
640            let logical_parent_height = cx.physical_to_logical(bounds.h);
641
642            let mut padding_left =
643                padding_left.to_px(logical_parent_width, 0.0) * cx.scale_factor();
644            let mut padding_right =
645                padding_right.to_px(logical_parent_width, 0.0) * cx.scale_factor();
646            let padding_top = padding_top.to_px(logical_parent_height, 0.0) * cx.scale_factor();
647            let padding_bottom =
648                padding_bottom.to_px(logical_parent_height, 0.0) * cx.scale_factor();
649
650            if resolved_text_direction(cx.style, cx.current) == crate::style::Direction::RightToLeft
651            {
652                std::mem::swap(&mut padding_left, &mut padding_right);
653            }
654
655            let (mut top, _) = match cx.style.alignment.get(cx.current).copied().unwrap_or_default()
656            {
657                Alignment::TopLeft => (0.0, 0.0),
658                Alignment::TopCenter => (0.0, 0.5),
659                Alignment::TopRight => (0.0, 1.0),
660                Alignment::Left => (0.5, 0.0),
661                Alignment::Center => (0.5, 0.5),
662                Alignment::Right => (0.5, 1.0),
663                Alignment::BottomLeft => (1.0, 0.0),
664                Alignment::BottomCenter => (1.0, 0.5),
665                Alignment::BottomRight => (1.0, 1.0),
666            };
667
668            top *= bounds.height() - padding_top - padding_bottom - paragraph.height();
669
670            let x = x - bounds.x - padding_left;
671            let y = y - bounds.y - padding_top - top;
672
673            (x, y)
674        } else {
675            (x, y)
676        }
677    }
678
679    /// This function takes window-global physical coordinates.
680    fn hit(&mut self, cx: &mut EventContext, x: f32, y: f32, selection: bool) {
681        if let Some(text) = cx.style.text.get(cx.current) {
682            if let Some(paragraph) = cx.text_context.text_paragraphs.get(cx.current) {
683                let x = x - self.transform.borrow().0;
684                let y = y - self.transform.borrow().1;
685                let gp = paragraph
686                    .get_glyph_position_at_coordinate(self.coordinates_global_to_text(cx, x, y));
687                let num_graphemes = text.graphemes(true).count();
688                let pos = (gp.position as usize).min(num_graphemes);
689                let mut cursor = text.len();
690                for (i, (j, _)) in text.grapheme_indices(true).enumerate() {
691                    if pos == i {
692                        cursor = j;
693                        break;
694                    }
695                }
696
697                if selection {
698                    self.selection.active = cursor;
699                } else {
700                    self.selection = Selection::caret(cursor);
701                }
702
703                cx.needs_redraw();
704                cx.style.needs_access_update(cx.current);
705            }
706        }
707    }
708
709    /// This function takes window-global physical coordinates.
710    fn drag(&mut self, cx: &mut EventContext, x: f32, y: f32) {
711        if let Some(text) = cx.style.text.get(cx.current) {
712            if let Some(paragraph) = cx.text_context.text_paragraphs.get(cx.current) {
713                let x = x - self.transform.borrow().0;
714                let y = y - self.transform.borrow().1;
715                let gp = paragraph
716                    .get_glyph_position_at_coordinate(self.coordinates_global_to_text(cx, x, y));
717                let num_graphemes = text.graphemes(true).count();
718                let pos = (gp.position as usize).min(num_graphemes);
719
720                let mut cursor = text.len();
721                for (i, (j, _)) in text.grapheme_indices(true).enumerate() {
722                    if pos == i {
723                        cursor = j;
724                        break;
725                    }
726                }
727
728                self.selection.active = cursor;
729
730                cx.needs_redraw();
731                cx.style.needs_access_update(cx.current);
732            }
733        }
734    }
735
736    // /// This function takes window-global physical dimensions.
737    // fn scroll(&mut self, cx: &mut EventContext, x: f32, y: f32) {}
738
739    #[cfg(feature = "clipboard")]
740    fn clone_selected(&self, cx: &mut EventContext) -> Option<String> {
741        if let Some(text) = cx.style.text.get(cx.current) {
742            let real_selection =
743                Self::selection_display_to_real(text, &self.real_text, self.selection);
744            let substring = &self.real_text[real_selection.range()];
745            return Some(substring.to_string());
746        }
747
748        None
749    }
750
751    fn clone_text(&self, _cx: &mut EventContext) -> String {
752        if self.show_placeholder.get() {
753            return String::new();
754        }
755
756        self.real_text.clone()
757    }
758
759    fn clamp_insert_text(&self, txt: &str, replace_range: Range<usize>) -> String {
760        let Some(max_length) = self.max_length.get() else {
761            return txt.to_string();
762        };
763
764        let current_len = self.real_text.graphemes(true).count();
765        let replaced_len = self.real_text[replace_range].graphemes(true).count();
766        let preserved_len = current_len.saturating_sub(replaced_len);
767        let remaining = max_length.saturating_sub(preserved_len);
768
769        if remaining == 0 {
770            return String::new();
771        }
772
773        txt.graphemes(true).take(remaining).collect()
774    }
775
776    fn is_text_valid(&self, text: &str) -> bool {
777        if let Ok(value) = text.parse::<T>() {
778            if let Some(validate) = &self.validate { validate(&value) } else { true }
779        } else {
780            false
781        }
782    }
783
784    fn reset_caret_timer(&mut self, cx: &mut EventContext) {
785        cx.stop_timer(self.caret_timer);
786        if !cx.is_read_only() {
787            self.show_caret.set(true);
788            cx.start_timer(self.caret_timer);
789        }
790    }
791
792    fn reset_ime_position(&mut self, cx: &mut EventContext) {
793        // TODO: Make the position of IME follow the cursor.
794        cx.event_queue.push_back(
795            Event::new(WindowEvent::SetImeCursorArea(
796                (cx.bounds().x as u32, cx.bounds().y as u32),
797                ((cx.bounds().width()) as u32, cx.bounds().height() as u32),
798            ))
799            .target(cx.current),
800        );
801    }
802
803    fn draw_selection(&self, cx: &mut DrawContext, canvas: &Canvas) {
804        if !self.selection.is_caret() {
805            if let Some(paragraph) = cx.text_context.text_paragraphs.get(cx.current) {
806                if let Some(text) = cx.style.text.get(cx.current) {
807                    let min = text.current_grapheme_offset(self.selection.min());
808                    let max = text.current_grapheme_offset(self.selection.max());
809
810                    let cursor_rects = paragraph.get_rects_for_range(
811                        min..max,
812                        RectHeightStyle::Tight,
813                        RectWidthStyle::Tight,
814                    );
815
816                    for cursor_rect in cursor_rects {
817                        let bounds = cx.bounds();
818
819                        let alignment = cx.alignment();
820
821                        let (mut top, left) = match alignment {
822                            Alignment::TopLeft => (0.0, 0.0),
823                            Alignment::TopCenter => (0.0, 0.5),
824                            Alignment::TopRight => (0.0, 1.0),
825                            Alignment::Left => (0.5, 0.0),
826                            Alignment::Center => (0.5, 0.5),
827                            Alignment::Right => (0.5, 1.0),
828                            Alignment::BottomLeft => (1.0, 0.0),
829                            Alignment::BottomCenter => (1.0, 0.5),
830                            Alignment::BottomRight => (1.0, 1.0),
831                        };
832
833                        let padding_top = match cx.padding_top() {
834                            Units::Pixels(val) => val,
835                            _ => 0.0,
836                        };
837
838                        let padding_bottom = match cx.padding_bottom() {
839                            Units::Pixels(val) => val,
840                            _ => 0.0,
841                        };
842
843                        top *= bounds.height() - padding_top - padding_bottom - paragraph.height();
844
845                        let mut padding_left = match cx.padding_left() {
846                            Units::Pixels(val) => val,
847                            _ => 0.0,
848                        };
849
850                        let mut padding_right = match cx.padding_right() {
851                            Units::Pixels(val) => val,
852                            _ => 0.0,
853                        };
854
855                        if resolved_text_direction(cx.style, cx.current)
856                            == crate::style::Direction::RightToLeft
857                        {
858                            std::mem::swap(&mut padding_left, &mut padding_right);
859                        }
860
861                        let x = bounds.x + padding_left + cursor_rect.rect.left + left;
862                        let y = bounds.y + padding_top + cursor_rect.rect.top + top;
863
864                        let x2 = x + (cursor_rect.rect.right - cursor_rect.rect.left);
865                        let y2 = y + (cursor_rect.rect.bottom - cursor_rect.rect.top);
866
867                        let mut paint = Paint::default();
868                        paint.set_anti_alias(true);
869                        paint.set_style(PaintStyle::Fill);
870                        paint.set_color(cx.selection_color());
871
872                        canvas.draw_rect(Rect::new(x, y, x2, y2), &paint);
873                    }
874                }
875            }
876        }
877    }
878
879    /// Draw text caret for the current view.
880    pub fn draw_text_caret(&self, cx: &mut DrawContext, canvas: &Canvas) {
881        if let Some(paragraph) = cx.text_context.text_paragraphs.get(cx.current) {
882            if let Some(text) = cx.style.text.get(cx.current) {
883                let bounds = cx.bounds();
884
885                let current = text.current_grapheme_offset(self.selection.active);
886
887                let grapheme_count = text.graphemes(true).count();
888                let (range_start, range_end, use_trailing_edge) = if current < grapheme_count {
889                    (current, current + 1, false)
890                } else if current > 0 {
891                    // At end-of-text, use the previous grapheme box and place the caret on its trailing edge.
892                    (current - 1, current, true)
893                } else {
894                    // Empty text: anchor caret to the paragraph's first position.
895                    (0, 1, false)
896                };
897
898                let rects = paragraph.get_rects_for_range(
899                    range_start..range_end,
900                    RectHeightStyle::Tight,
901                    RectWidthStyle::Tight,
902                );
903
904                let Some(cursor_rect) = rects.first() else {
905                    return;
906                };
907
908                let alignment = cx.alignment();
909
910                let (mut top, _) = match alignment {
911                    Alignment::TopLeft => (0.0, 0.0),
912                    Alignment::TopCenter => (0.0, 0.5),
913                    Alignment::TopRight => (0.0, 1.0),
914                    Alignment::Left => (0.5, 0.0),
915                    Alignment::Center => (0.5, 0.5),
916                    Alignment::Right => (0.5, 1.0),
917                    Alignment::BottomLeft => (1.0, 0.0),
918                    Alignment::BottomCenter => (1.0, 0.5),
919                    Alignment::BottomRight => (1.0, 1.0),
920                };
921
922                let padding_top = match cx.padding_top() {
923                    Units::Pixels(val) => val,
924                    _ => 0.0,
925                };
926
927                let padding_bottom = match cx.padding_bottom() {
928                    Units::Pixels(val) => val,
929                    _ => 0.0,
930                };
931
932                top *= bounds.height() - padding_top - padding_bottom - paragraph.height();
933
934                let mut padding_left = match cx.padding_left() {
935                    Units::Pixels(val) => val,
936                    _ => 0.0,
937                };
938
939                let mut padding_right = match cx.padding_right() {
940                    Units::Pixels(val) => val,
941                    _ => 0.0,
942                };
943
944                if resolved_text_direction(cx.style, cx.current)
945                    == crate::style::Direction::RightToLeft
946                {
947                    std::mem::swap(&mut padding_left, &mut padding_right);
948                }
949
950                let caret_x =
951                    if use_trailing_edge { cursor_rect.rect.right } else { cursor_rect.rect.left };
952
953                let x = (bounds.x + padding_left + caret_x).round();
954                let y = (bounds.y + padding_top + cursor_rect.rect.top + top).round();
955
956                let x2 = x + 1.0;
957                let y2 = y + (cursor_rect.rect.bottom - cursor_rect.rect.top);
958
959                let mut paint = Paint::default();
960                paint.set_anti_alias(true);
961                paint.set_style(PaintStyle::Fill);
962                paint.set_color(cx.caret_color());
963
964                canvas.draw_rect(Rect::new(x, y, x2, y2), &paint);
965
966                let mut transform = self.transform.borrow_mut();
967
968                let text_bounds = BoundingBox::from_min_max(
969                    bounds.x + padding_left,
970                    bounds.y + padding_top + top,
971                    bounds.x + padding_left + paragraph.max_intrinsic_width(),
972                    bounds.y + padding_top + top + paragraph.height(),
973                );
974
975                let mut bounds = bounds;
976
977                bounds =
978                    bounds.shrink_sides(padding_left, padding_top, padding_right, padding_bottom);
979
980                let (tx, ty) =
981                    enforce_text_bounds(&text_bounds, &bounds, (transform.0, transform.1));
982
983                let caret_box = BoundingBox::from_min_max(x, y, x2, y2);
984
985                let (new_tx, new_ty) = ensure_visible(&caret_box, &bounds, (tx, ty));
986
987                if new_tx != transform.0 || new_ty != transform.1 {
988                    *transform = (new_tx, new_ty);
989                    cx.needs_redraw();
990                }
991            }
992        }
993    }
994}
995
996impl<R, T> Handle<'_, Textbox<R, T>>
997where
998    R: Res<T> + 'static,
999    T: Clone + ToStringLocalized + std::str::FromStr + 'static,
1000{
1001    /// Sets the callback triggered when a textbox is edited, i.e. text is inserted/deleted.
1002    ///
1003    /// Callback provides the current text of the textbox.
1004    pub fn on_edit<F>(self, callback: F) -> Self
1005    where
1006        F: 'static + Fn(&mut EventContext, String) + Send + Sync,
1007    {
1008        self.modify(|textbox| textbox.on_edit = Some(Box::new(callback)))
1009    }
1010
1011    /// Sets the callback triggered when a textbox is submitted,
1012    /// i.e. when the enter key is pressed with a single-line textbox or the textbox loses focus.
1013    ///
1014    /// Callback provides the text of the textbox and a flag to indicate if the submit was due to a key press or a loss of focus.
1015    pub fn on_submit<F>(self, callback: F) -> Self
1016    where
1017        F: 'static + Fn(&mut EventContext, T, bool) + Send + Sync,
1018    {
1019        self.modify(|textbox| textbox.on_submit = Some(Box::new(callback)))
1020    }
1021
1022    /// Sets the callback triggered when a textbox is blurred, i.e. the mouse is pressed outside of the textbox.
1023    pub fn on_blur<F>(self, callback: F) -> Self
1024    where
1025        F: 'static + Fn(&mut EventContext) + Send + Sync,
1026    {
1027        self.modify(|textbox| textbox.on_blur = Some(Box::new(callback)))
1028    }
1029
1030    /// Sets the callback triggered when a textbox edit is cancelled, i.e. the escape key is pressed while editing.
1031    pub fn on_cancel<F>(self, callback: F) -> Self
1032    where
1033        F: 'static + Fn(&mut EventContext) + Send + Sync,
1034    {
1035        self.modify(|textbox| textbox.on_cancel = Some(Box::new(callback)))
1036    }
1037
1038    /// Sets a validation closure which is called when the textbox is edited and sets the validity attribute to the output of the closure.
1039    ///
1040    /// If a textbox is modified with the validate modifier then the `on_submit` will not be called if the text is invalid.
1041    pub fn validate<F>(self, is_valid: F) -> Self
1042    where
1043        F: 'static + Fn(&T) -> bool + Send + Sync,
1044    {
1045        self.modify(|textbox| textbox.validate = Some(Box::new(is_valid)))
1046    }
1047
1048    /// Sets the placeholder text that appears when the textbox has no value.
1049    pub fn placeholder<P: ToStringLocalized + Clone + 'static>(
1050        self,
1051        text: impl Res<P> + 'static,
1052    ) -> Self {
1053        let text = text.to_signal(self.cx);
1054        self.bind(text, move |mut handle| {
1055            let text = text.get();
1056            let txt = text.to_string_local(&handle);
1057            let entity = handle.entity();
1058            handle = handle.modify(|textbox| textbox.placeholder.set(txt));
1059            handle.context().style.needs_access_update(entity);
1060        })
1061    }
1062
1063    /// Sets an optional character used to visually mask textbox text.
1064    ///
1065    /// Use `Some('*')` (or any character) to enable masking and `None` to disable it.
1066    pub fn mask_char<U: Into<Option<char>> + Clone + 'static>(
1067        self,
1068        mask: impl Res<U> + 'static,
1069    ) -> Self {
1070        let mask = mask.to_signal(self.cx);
1071        self.bind(mask, move |mut handle| {
1072            let entity = handle.entity();
1073            let new_mask = mask.get().into();
1074            let mut display_text = String::new();
1075            handle = handle.modify(|textbox| {
1076                let old_display = textbox.display_text_from_real();
1077                textbox.mask_char.set_if_changed(new_mask);
1078                let new_display = textbox.display_text_from_real();
1079                textbox.remap_selection_for_display_change(&old_display, &new_display);
1080                display_text = new_display;
1081            });
1082            handle = handle.text(display_text);
1083            handle.context().style.needs_text_update(entity);
1084            handle.context().needs_redraw(entity);
1085        })
1086    }
1087
1088    /// Sets whether masked text should be visible.
1089    pub fn mask_visible<U: Into<bool> + Clone + 'static>(
1090        self,
1091        visible: impl Res<U> + 'static,
1092    ) -> Self {
1093        let visible = visible.to_signal(self.cx);
1094        self.bind(visible, move |mut handle| {
1095            let entity = handle.entity();
1096            let new_visible = visible.get().into();
1097            let mut display_text = String::new();
1098            let mut changed = false;
1099
1100            handle = handle.modify(|textbox| {
1101                if textbox.mask_visible != new_visible {
1102                    let old_display = textbox.display_text_from_real();
1103                    textbox.mask_visible = new_visible;
1104                    let new_display = textbox.display_text_from_real();
1105                    textbox.remap_selection_for_display_change(&old_display, &new_display);
1106                    display_text = new_display;
1107                    changed = true;
1108                }
1109            });
1110
1111            if changed {
1112                handle = handle.text(display_text);
1113                handle.context().style.needs_text_update(entity);
1114                handle.context().needs_redraw(entity);
1115            }
1116        })
1117    }
1118
1119    /// Sets an optional maximum number of graphemes for textbox input.
1120    ///
1121    /// Use `Some(n)` to limit input length and `None` to remove the limit.
1122    pub fn max_length<U: Into<Option<usize>> + Clone + 'static>(
1123        self,
1124        max_length: impl Res<U> + 'static,
1125    ) -> Self {
1126        let max_length = max_length.to_signal(self.cx);
1127        self.bind(max_length, move |handle| {
1128            let value = max_length.get().into();
1129            handle.modify(|textbox| textbox.max_length.set_if_changed(value));
1130        })
1131    }
1132
1133    /// Sets whether text in this textbox can be copied to the clipboard.
1134    pub fn can_copy<U: Into<bool> + Clone + 'static>(self, state: impl Res<U> + 'static) -> Self {
1135        let state = state.to_signal(self.cx);
1136        self.bind(state, move |handle| {
1137            let value = state.get().into();
1138            handle.modify(|textbox| textbox.can_copy.set_if_changed(value));
1139        })
1140    }
1141
1142    /// Sets whether text can be pasted into this textbox from the clipboard.
1143    pub fn can_paste<U: Into<bool> + Clone + 'static>(self, state: impl Res<U> + 'static) -> Self {
1144        let state = state.to_signal(self.cx);
1145        self.bind(state, move |handle| {
1146            let value = state.get().into();
1147            handle.modify(|textbox| textbox.can_paste.set_if_changed(value));
1148        })
1149    }
1150}
1151
1152/// Converts a byte offset (relative to line start) into a character index
1153/// within the `character_lengths` array for AccessKit text positioning.
1154fn byte_offset_to_char_index(character_lengths: &[u8], byte_offset: usize) -> usize {
1155    let mut cumulative = 0;
1156    for (i, &len) in character_lengths.iter().enumerate() {
1157        cumulative += len as usize;
1158        if byte_offset < cumulative {
1159            return i;
1160        }
1161    }
1162    character_lengths.len()
1163}
1164
1165impl<R, T> View for Textbox<R, T>
1166where
1167    R: Res<T> + 'static,
1168    T: Clone + ToStringLocalized + std::str::FromStr + 'static,
1169{
1170    fn element(&self) -> Option<&'static str> {
1171        Some("textbox")
1172    }
1173
1174    fn accessibility(&self, cx: &mut AccessContext, node: &mut AccessNode) {
1175        if !self.placeholder.get().is_empty() {
1176            node.set_placeholder(self.placeholder.get().clone());
1177        }
1178
1179        let node_id = node.node_id();
1180
1181        let selection = self.selection;
1182
1183        let mut selection_active_line = None;
1184        let mut selection_anchor_line = None;
1185        let mut selection_active_cursor = 0;
1186        let mut selection_anchor_cursor = 0;
1187        let mut first_line_node_id = None;
1188
1189        let text = if self.show_placeholder.get() {
1190            ""
1191        } else {
1192            cx.style.text.get(cx.current).map(|t| t.as_str()).unwrap_or("")
1193        };
1194        // build_paragraph() appends a zero-width space (\u{200B}, 3 UTF-8 bytes)
1195        // to every paragraph, so skia's line metrics include indices beyond the
1196        // actual text. We use text.len() as the upper bound for all slicing.
1197        let text_len = text.len();
1198
1199        if let Some(paragraph) = cx.text_context.text_paragraphs.get(cx.current) {
1200            let text_direction = if resolved_text_direction(cx.style, cx.current)
1201                == crate::style::Direction::RightToLeft
1202            {
1203                TextDirection::RightToLeft
1204            } else {
1205                TextDirection::LeftToRight
1206            };
1207
1208            let line_metrics = paragraph.get_line_metrics();
1209            for line in line_metrics.iter() {
1210                // Skip lines that start beyond the actual text (i.e., the ZWS-only line)
1211                if line.start_index >= text_len && text_len > 0 {
1212                    continue;
1213                }
1214
1215                // We need a child node per line
1216                let mut line_node = AccessNode::new_from_parent(node_id, line.line_number);
1217                line_node.set_role(Role::TextRun);
1218                line_node.set_text_direction(text_direction);
1219                line_node.set_bounds(BoundingBox {
1220                    x: line.left as f32,
1221                    y: (line.baseline - line.ascent) as f32,
1222                    w: line.width as f32,
1223                    h: line.height as f32,
1224                });
1225
1226                // Only iterate over glyphs within the actual text range
1227                let glyph_end = line.end_index.min(text_len);
1228                if line.start_index > glyph_end {
1229                    continue;
1230                }
1231                let estimated_chars = glyph_end.saturating_sub(line.start_index);
1232                let mut character_lengths: Vec<u8> = Vec::with_capacity(estimated_chars);
1233                let mut character_positions: Vec<f32> = Vec::with_capacity(estimated_chars);
1234                let mut character_widths: Vec<f32> = Vec::with_capacity(estimated_chars);
1235                let mut glyph_pos = line.start_index;
1236
1237                while glyph_pos < glyph_end {
1238                    if let Some(cluster_info) = paragraph.get_glyph_cluster_at(glyph_pos) {
1239                        let length = cluster_info.text_range.end - cluster_info.text_range.start;
1240                        if length == 0 {
1241                            break;
1242                        }
1243
1244                        character_lengths.push(length as u8);
1245                        character_positions.push(cluster_info.bounds.left());
1246                        character_widths.push(cluster_info.bounds.width());
1247
1248                        glyph_pos += length;
1249                    } else {
1250                        break;
1251                    }
1252                }
1253
1254                // Include the newline character for hard breaks, as AccessKit needs it
1255                let mut line_end = if line.hard_break {
1256                    line.end_including_newline.min(text_len)
1257                } else {
1258                    glyph_end
1259                };
1260                if line_end < line.start_index {
1261                    line_end = line.start_index;
1262                }
1263                let line_text = text.get(line.start_index..line_end).unwrap_or("").to_owned();
1264
1265                if line.hard_break && line.end_including_newline <= text_len {
1266                    character_lengths.push(1);
1267                    character_positions.push(line.width as f32);
1268                    character_widths.push(0.0);
1269                }
1270
1271                let mut word_starts = Vec::new();
1272                let mut previous_is_alphanumeric = text
1273                    .get(..line.start_index)
1274                    .and_then(|prefix| prefix.graphemes(true).next_back())
1275                    .and_then(|grapheme| grapheme.chars().next())
1276                    .is_some_and(|ch| ch.is_alphanumeric());
1277
1278                for (character_index, grapheme) in line_text.graphemes(true).enumerate() {
1279                    let current_is_alphanumeric =
1280                        grapheme.chars().next().is_some_and(|ch| ch.is_alphanumeric());
1281
1282                    if current_is_alphanumeric
1283                        && !previous_is_alphanumeric
1284                        && let Ok(character_index) = u8::try_from(character_index)
1285                    {
1286                        word_starts.push(character_index);
1287                    }
1288
1289                    previous_is_alphanumeric = current_is_alphanumeric;
1290                }
1291
1292                if first_line_node_id.is_none() {
1293                    first_line_node_id = Some(line_node.node_id());
1294                }
1295
1296                // Check if this line contains the selection active (focus) position
1297                if selection.active >= line.start_index && selection.active <= line_end {
1298                    selection_active_line = Some(line_node.node_id());
1299                    selection_active_cursor = byte_offset_to_char_index(
1300                        &character_lengths,
1301                        selection.active - line.start_index,
1302                    );
1303                }
1304
1305                // Check if this line contains the selection anchor position
1306                if selection.anchor >= line.start_index && selection.anchor <= line_end {
1307                    selection_anchor_line = Some(line_node.node_id());
1308                    selection_anchor_cursor = byte_offset_to_char_index(
1309                        &character_lengths,
1310                        selection.anchor - line.start_index,
1311                    );
1312                }
1313
1314                line_node.set_value(line_text.into_boxed_str());
1315                line_node.set_character_lengths(character_lengths.into_boxed_slice());
1316                line_node.set_character_positions(character_positions.into_boxed_slice());
1317                line_node.set_character_widths(character_widths.into_boxed_slice());
1318                line_node.set_word_starts(word_starts.into_boxed_slice());
1319
1320                node.add_child(line_node);
1321            }
1322        }
1323
1324        if let Some(fallback) = first_line_node_id {
1325            node.set_text_selection(TextSelection {
1326                anchor: TextPosition {
1327                    node: selection_anchor_line.unwrap_or(fallback),
1328                    character_index: selection_anchor_cursor,
1329                },
1330                focus: TextPosition {
1331                    node: selection_active_line.unwrap_or(fallback),
1332                    character_index: selection_active_cursor,
1333                },
1334            });
1335        }
1336    }
1337
1338    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
1339        // Window Events
1340        event.map(|window_event, meta| match window_event {
1341            WindowEvent::MouseDown(MouseButton::Left) => {
1342                if meta.origin == cx.current {
1343                    return;
1344                }
1345
1346                if cx.is_over() {
1347                    if !cx.is_disabled() {
1348                        cx.focus_with_visibility(true);
1349                        cx.capture();
1350                        cx.lock_cursor_icon();
1351
1352                        if !self.edit {
1353                            cx.emit(TextEvent::StartEdit);
1354                        }
1355                        self.reset_caret_timer(cx);
1356                        cx.emit(TextEvent::Hit(
1357                            cx.mouse.cursor_x,
1358                            cx.mouse.cursor_y,
1359                            cx.modifiers.shift(),
1360                        ));
1361                    }
1362                } else {
1363                    cx.release();
1364
1365                    // Forward event to hovered
1366                    cx.event_queue.push_back(
1367                        Event::new(WindowEvent::MouseDown(MouseButton::Left)).target(cx.hovered()),
1368                    );
1369                    cx.event_queue.push_back(
1370                        Event::new(WindowEvent::PressDown { mouse: true }).target(cx.hovered()),
1371                    );
1372                }
1373            }
1374
1375            WindowEvent::FocusIn => {
1376                if cx.mouse.left.pressed != cx.current()
1377                    || cx.mouse.left.state == MouseButtonState::Released
1378                {
1379                    cx.emit(TextEvent::StartEdit);
1380                }
1381            }
1382
1383            WindowEvent::FocusOut => {
1384                cx.emit(TextEvent::EndEdit);
1385            }
1386
1387            WindowEvent::MouseDoubleClick(MouseButton::Left) => {
1388                cx.emit(TextEvent::SelectWord);
1389            }
1390
1391            WindowEvent::MouseTripleClick(MouseButton::Left) => {
1392                cx.emit(TextEvent::SelectParagraph);
1393            }
1394
1395            WindowEvent::MouseUp(MouseButton::Left) => {
1396                self.reset_caret_timer(cx);
1397                cx.unlock_cursor_icon();
1398                cx.release();
1399            }
1400
1401            WindowEvent::MouseMove(x, y) => {
1402                if cx.mouse.left.state == MouseButtonState::Pressed
1403                    && cx.mouse.left.pressed == cx.current
1404                {
1405                    if self.edit {
1406                        self.reset_caret_timer(cx);
1407                    }
1408                    if cx.mouse.left.pos_down.0 != *x || cx.mouse.left.pos_down.1 != *y {
1409                        cx.emit(TextEvent::Drag(*x, *y));
1410                    }
1411                }
1412            }
1413
1414            WindowEvent::MouseScroll(x, y) => {
1415                cx.emit(TextEvent::Scroll(*x, *y));
1416            }
1417
1418            WindowEvent::CharInput(c) => {
1419                if *c != '\u{1b}' && // Escape
1420                    *c != '\u{8}' && // Backspace
1421                    *c != '\u{9}' && // Tab
1422                    *c != '\u{7f}' && // Delete
1423                    *c != '\u{0d}' && // Carriage return
1424                    !cx.modifiers.ctrl() &&
1425                    !cx.modifiers.logo() &&
1426                    self.edit &&
1427                    !cx.is_read_only()
1428                {
1429                    self.reset_caret_timer(cx);
1430                    cx.emit(TextEvent::InsertText(String::from(*c)));
1431                }
1432            }
1433
1434            WindowEvent::ImeCommit(text) => {
1435                if !cx.modifiers.ctrl() && !cx.modifiers.logo() && self.edit && !cx.is_read_only() {
1436                    self.reset_caret_timer(cx);
1437                    cx.emit(TextEvent::ClearPreedit);
1438                    cx.emit(TextEvent::InsertText(text.to_string()));
1439
1440                    self.reset_ime_position(cx);
1441                }
1442            }
1443
1444            WindowEvent::ImePreedit(text, cursor) => {
1445                if !cx.modifiers.ctrl() && !cx.modifiers.logo() && self.edit && !cx.is_read_only() {
1446                    self.reset_caret_timer(cx);
1447                    cx.emit(TextEvent::UpdatePreedit(text.to_string(), *cursor));
1448                }
1449            }
1450
1451            WindowEvent::KeyDown(code, _) => match code {
1452                Code::Enter => {
1453                    if matches!(self.kind, TextboxKind::SingleLine) {
1454                        cx.emit(TextEvent::Submit(true));
1455                    } else if !cx.is_read_only() {
1456                        self.reset_caret_timer(cx);
1457                        cx.emit(TextEvent::InsertText("\n".to_owned()));
1458                    }
1459                }
1460
1461                // Note: no `Code::Space` arm — the space character arrives
1462                // through `WindowEvent::CharInput(' ')` above, which already
1463                // inserts it (and correctly suppresses insertion when Ctrl
1464                // or Cmd is held). Handling it here as well produced double
1465                // insertion on platforms that emit both events for a plain
1466                // spacebar press.
1467                Code::ArrowLeft => {
1468                    self.reset_caret_timer(cx);
1469                    // macOS convention: Option (alt) for word movement,
1470                    // Cmd (logo) for line-boundary movement.
1471                    // Other platforms: Ctrl for word movement.
1472                    #[cfg(target_os = "macos")]
1473                    let movement = if cx.modifiers.logo() {
1474                        Movement::LineStart
1475                    } else if cx.modifiers.alt() {
1476                        Movement::Word(Direction::Left)
1477                    } else {
1478                        Movement::Grapheme(Direction::Left)
1479                    };
1480                    #[cfg(not(target_os = "macos"))]
1481                    let movement = if cx.modifiers.ctrl() {
1482                        Movement::Word(Direction::Left)
1483                    } else {
1484                        Movement::Grapheme(Direction::Left)
1485                    };
1486
1487                    cx.emit(TextEvent::MoveCursor(movement, cx.modifiers.shift()));
1488                }
1489
1490                Code::ArrowRight => {
1491                    self.reset_caret_timer(cx);
1492
1493                    #[cfg(target_os = "macos")]
1494                    let movement = if cx.modifiers.logo() {
1495                        Movement::LineEnd
1496                    } else if cx.modifiers.alt() {
1497                        Movement::Word(Direction::Right)
1498                    } else {
1499                        Movement::Grapheme(Direction::Right)
1500                    };
1501                    #[cfg(not(target_os = "macos"))]
1502                    let movement = if cx.modifiers.ctrl() {
1503                        Movement::Word(Direction::Right)
1504                    } else {
1505                        Movement::Grapheme(Direction::Right)
1506                    };
1507
1508                    cx.emit(TextEvent::MoveCursor(movement, cx.modifiers.shift()));
1509                }
1510
1511                Code::ArrowUp => {
1512                    self.reset_caret_timer(cx);
1513                    if self.kind != TextboxKind::SingleLine {
1514                        cx.emit(TextEvent::MoveCursor(
1515                            Movement::Vertical(VerticalMovement::LineUp),
1516                            cx.modifiers.shift(),
1517                        ));
1518                    }
1519                }
1520
1521                Code::ArrowDown => {
1522                    self.reset_caret_timer(cx);
1523                    if self.kind != TextboxKind::SingleLine {
1524                        cx.emit(TextEvent::MoveCursor(
1525                            Movement::Vertical(VerticalMovement::LineDown),
1526                            cx.modifiers.shift(),
1527                        ));
1528                    }
1529                }
1530
1531                Code::Backspace => {
1532                    self.reset_caret_timer(cx);
1533                    if !cx.is_read_only() {
1534                        #[cfg(target_os = "macos")]
1535                        let movement = if cx.modifiers.logo() {
1536                            // Cmd+Backspace deletes from caret to the visual
1537                            // line start on macOS, matching Cmd+Left cursor
1538                            // movement (which uses `Movement::LineStart`).
1539                            Movement::LineStart
1540                        } else if cx.modifiers.alt() {
1541                            Movement::Word(Direction::Upstream)
1542                        } else {
1543                            Movement::Grapheme(Direction::Upstream)
1544                        };
1545                        #[cfg(not(target_os = "macos"))]
1546                        let movement = if cx.modifiers.ctrl() {
1547                            Movement::Word(Direction::Upstream)
1548                        } else {
1549                            Movement::Grapheme(Direction::Upstream)
1550                        };
1551
1552                        cx.emit(TextEvent::DeleteText(movement));
1553                    }
1554                }
1555
1556                Code::Delete => {
1557                    self.reset_caret_timer(cx);
1558                    if !cx.is_read_only() {
1559                        #[cfg(target_os = "macos")]
1560                        let movement = if cx.modifiers.alt() {
1561                            Movement::Word(Direction::Downstream)
1562                        } else {
1563                            Movement::Grapheme(Direction::Downstream)
1564                        };
1565                        #[cfg(not(target_os = "macos"))]
1566                        let movement = if cx.modifiers.ctrl() {
1567                            Movement::Word(Direction::Downstream)
1568                        } else {
1569                            Movement::Grapheme(Direction::Downstream)
1570                        };
1571
1572                        cx.emit(TextEvent::DeleteText(movement));
1573                    }
1574                }
1575
1576                Code::Escape => {
1577                    if let Some(callback) = &self.on_cancel {
1578                        (callback)(cx);
1579                    } else {
1580                        cx.emit(TextEvent::EndEdit);
1581                    }
1582                }
1583
1584                Code::Home => {
1585                    self.reset_caret_timer(cx);
1586                    cx.emit(TextEvent::MoveCursor(Movement::LineStart, cx.modifiers.shift()));
1587                }
1588
1589                Code::End => {
1590                    self.reset_caret_timer(cx);
1591                    cx.emit(TextEvent::MoveCursor(Movement::LineEnd, cx.modifiers.shift()));
1592                }
1593
1594                Code::PageUp | Code::PageDown => {
1595                    self.reset_caret_timer(cx);
1596                    let direction = if *code == Code::PageUp {
1597                        Direction::Upstream
1598                    } else {
1599                        Direction::Downstream
1600                    };
1601                    cx.emit(TextEvent::MoveCursor(
1602                        if cx.modifiers.ctrl() {
1603                            Movement::Body(direction)
1604                        } else {
1605                            Movement::Page(direction)
1606                        },
1607                        cx.modifiers.shift(),
1608                    ));
1609                }
1610
1611                Code::KeyA => {
1612                    #[cfg(target_os = "macos")]
1613                    let modifier = Modifiers::SUPER;
1614                    #[cfg(not(target_os = "macos"))]
1615                    let modifier = Modifiers::CTRL;
1616
1617                    if cx.modifiers == &modifier {
1618                        cx.emit(TextEvent::SelectAll);
1619                    }
1620                }
1621
1622                Code::KeyC => {
1623                    #[cfg(target_os = "macos")]
1624                    let modifier = Modifiers::SUPER;
1625                    #[cfg(not(target_os = "macos"))]
1626                    let modifier = Modifiers::CTRL;
1627
1628                    if cx.modifiers == &modifier && self.can_copy.get() {
1629                        cx.emit(TextEvent::Copy);
1630                    }
1631                }
1632
1633                Code::KeyV => {
1634                    #[cfg(target_os = "macos")]
1635                    let modifier = Modifiers::SUPER;
1636                    #[cfg(not(target_os = "macos"))]
1637                    let modifier = Modifiers::CTRL;
1638
1639                    if cx.modifiers == &modifier && self.can_paste.get() {
1640                        cx.emit(TextEvent::Paste);
1641                    }
1642                }
1643
1644                Code::KeyX => {
1645                    #[cfg(target_os = "macos")]
1646                    let modifier = Modifiers::SUPER;
1647                    #[cfg(not(target_os = "macos"))]
1648                    let modifier = Modifiers::CTRL;
1649
1650                    if cx.modifiers == &modifier && !cx.is_read_only() {
1651                        cx.emit(TextEvent::Cut);
1652                    }
1653                }
1654
1655                _ => {}
1656            },
1657
1658            WindowEvent::ActionRequest(ActionRequest {
1659                action: accesskit::Action::SetTextSelection,
1660                target_tree: _,
1661                target_node: _,
1662                data: Some(ActionData::SetTextSelection(_selection)),
1663            }) => {
1664                // TODO: Implement SetTextSelection action for screen reader support.
1665            }
1666
1667            _ => {}
1668        });
1669
1670        // Textbox Events
1671        event.map(|text_event, _| match text_event {
1672            TextEvent::InsertText(text) => {
1673                if self.preedit_backup.is_some() {
1674                    return;
1675                }
1676
1677                self.edited_since_focus = true;
1678                self.edited_once = true;
1679
1680                if self.show_placeholder.get() {
1681                    self.reset_text(cx);
1682                }
1683
1684                self.insert_text(cx, text);
1685
1686                let text = self.clone_text(cx);
1687
1688                cx.set_valid(self.is_text_valid(&text));
1689
1690                if self.edit {
1691                    if let Some(callback) = &self.on_edit {
1692                        (callback)(cx, text);
1693                    }
1694                }
1695            }
1696
1697            TextEvent::UpdatePreedit(preedit, cursor) => {
1698                self.update_preedit(cx, preedit, *cursor);
1699            }
1700
1701            TextEvent::ClearPreedit => {
1702                self.clear_preedit(cx);
1703            }
1704
1705            TextEvent::Clear => {
1706                self.reset_text(cx);
1707                // self.scroll(cx, 0.0, 0.0); // ensure_visible
1708                cx.needs_relayout();
1709                cx.needs_redraw();
1710            }
1711
1712            TextEvent::DeleteText(movement) => {
1713                if self.edit {
1714                    self.edited_since_focus = true;
1715                    self.edited_once = true;
1716                    self.delete_text(cx, *movement);
1717
1718                    let text = self.clone_text(cx);
1719
1720                    cx.set_valid(self.is_text_valid(&text));
1721
1722                    if let Some(callback) = &self.on_edit {
1723                        (callback)(cx, text);
1724                    }
1725                }
1726            }
1727
1728            TextEvent::MoveCursor(movement, selection) => {
1729                if self.edit && !self.show_placeholder.get() && self.preedit_backup.is_none() {
1730                    self.move_cursor(cx, *movement, *selection);
1731                }
1732            }
1733
1734            TextEvent::SetPlaceholder(text) => {
1735                self.placeholder.set(text.clone());
1736                cx.style.needs_access_update(cx.current);
1737            }
1738
1739            TextEvent::StartEdit => {
1740                if !cx.is_disabled() && !self.edit {
1741                    self.edit = true;
1742                    self.edited_since_focus = false;
1743                    cx.focus_with_visibility(true);
1744                    cx.capture();
1745                    self.reset_caret_timer(cx);
1746                    self.reset_ime_position(cx);
1747
1748                    self.text_overflow = cx.style.text_overflow.get_inline(cx.current).copied();
1749                    cx.style.text_overflow.remove(cx.current);
1750
1751                    let text = self.value.get_value(cx);
1752                    let text = text.to_string_local(cx);
1753                    self.real_text = text.clone();
1754
1755                    if text.is_empty() {
1756                        self.show_placeholder.set(true);
1757                        self.selection = Selection::caret(0);
1758                        cx.style.needs_access_update(cx.current);
1759                    } else {
1760                        self.show_placeholder.set(false);
1761                        self.select_all(cx);
1762                    }
1763
1764                    // Keep textbox pristine only until first user edit; once edited,
1765                    // preserve validation across blur/focus cycles.
1766                    if self.edited_once || !text.is_empty() {
1767                        cx.set_valid(self.is_text_valid(&text));
1768                    } else {
1769                        cx.set_valid(true);
1770                    }
1771                }
1772
1773                self.sync_display_text(cx);
1774            }
1775
1776            TextEvent::EndEdit => {
1777                self.deselect();
1778                self.edit = false;
1779                cx.release();
1780                cx.stop_timer(self.caret_timer);
1781
1782                let text = self.clone_text(cx);
1783                self.show_placeholder.set(text.is_empty());
1784
1785                if let Some(text_overflow) = self.text_overflow {
1786                    cx.style.text_overflow.insert(cx.current, text_overflow);
1787                } else {
1788                    cx.style.text_overflow.remove(cx.current);
1789                }
1790
1791                self.select_all(cx);
1792
1793                if self.edited_since_focus {
1794                    cx.set_valid(self.is_text_valid(&text));
1795                }
1796
1797                // Reset transform to 0,0
1798                let mut transform = self.transform.borrow_mut();
1799                *transform = (0.0, 0.0);
1800
1801                // Reset cursor position
1802                self.selection = Selection::caret(0);
1803
1804                self.sync_display_text(cx);
1805                cx.style.needs_access_update(cx.current);
1806            }
1807
1808            TextEvent::Blur => {
1809                // Clicking outside a textbox can end editing while retaining keyboard focus
1810                // (for example when clicking non-focusable chrome). Keep focus but remove
1811                // the visible focus indicator for pointer-driven blur.
1812                if cx.focused() == cx.current() {
1813                    cx.focus_with_visibility(false);
1814                }
1815
1816                if let Some(callback) = &self.on_blur {
1817                    (callback)(cx);
1818                } else {
1819                    cx.emit(TextEvent::Submit(false));
1820                    cx.emit(TextEvent::EndEdit);
1821                }
1822            }
1823
1824            TextEvent::SetMaskVisible(visible) => {
1825                if self.mask_visible != *visible {
1826                    let old_display = self.display_text_from_real();
1827                    self.mask_visible = *visible;
1828                    let new_display = self.display_text_from_real();
1829                    self.remap_selection_for_display_change(&old_display, &new_display);
1830                    self.sync_display_text(cx);
1831                    cx.needs_redraw();
1832                }
1833            }
1834
1835            TextEvent::ToggleMaskVisible => {
1836                let old_display = self.display_text_from_real();
1837                self.mask_visible = !self.mask_visible;
1838                let new_display = self.display_text_from_real();
1839                self.remap_selection_for_display_change(&old_display, &new_display);
1840                self.sync_display_text(cx);
1841                cx.needs_redraw();
1842            }
1843
1844            TextEvent::Submit(reason) => {
1845                if let Some(callback) = &self.on_submit {
1846                    let text = self.clone_text(cx);
1847                    let is_valid = self.is_text_valid(&text);
1848                    cx.set_valid(is_valid);
1849                    if is_valid && let Ok(value) = text.parse::<T>() {
1850                        (callback)(cx, value, *reason);
1851                    }
1852                }
1853            }
1854
1855            TextEvent::SelectAll => {
1856                self.select_all(cx);
1857            }
1858
1859            TextEvent::SelectWord => {
1860                self.select_word(cx);
1861            }
1862
1863            TextEvent::SelectParagraph => {
1864                self.select_paragraph(cx);
1865            }
1866
1867            TextEvent::Hit(posx, posy, selection) => {
1868                if !self.show_placeholder.get() {
1869                    self.hit(cx, *posx, *posy, *selection);
1870                }
1871            }
1872
1873            TextEvent::Drag(posx, posy) => {
1874                if !self.show_placeholder.get() {
1875                    self.drag(cx, *posx, *posy);
1876                }
1877            }
1878
1879            TextEvent::Scroll(_x, _y) => {
1880                //self.scroll(cx, *x, *y);
1881            }
1882
1883            TextEvent::Copy =>
1884            {
1885                #[cfg(feature = "clipboard")]
1886                if self.edit && self.can_copy.get() {
1887                    if let Some(selected_text) = self.clone_selected(cx) {
1888                        if !selected_text.is_empty() {
1889                            cx.set_clipboard(selected_text)
1890                                .expect("Failed to add text to clipboard");
1891                        }
1892                    }
1893                }
1894            }
1895
1896            TextEvent::Paste =>
1897            {
1898                #[cfg(feature = "clipboard")]
1899                if self.edit && self.can_paste.get() {
1900                    if let Ok(text) = cx.get_clipboard() {
1901                        cx.emit(TextEvent::InsertText(text));
1902                    }
1903                }
1904            }
1905
1906            TextEvent::Cut =>
1907            {
1908                #[cfg(feature = "clipboard")]
1909                if self.edit {
1910                    if let Some(selected_text) = self.clone_selected(cx) {
1911                        if !selected_text.is_empty() {
1912                            self.edited_since_focus = true;
1913                            self.edited_once = true;
1914                            cx.set_clipboard(selected_text)
1915                                .expect("Failed to add text to clipboard");
1916                            self.delete_text(cx, Movement::Grapheme(Direction::Upstream));
1917
1918                            let text = self.clone_text(cx);
1919
1920                            cx.set_valid(self.is_text_valid(&text));
1921
1922                            if let Some(callback) = &self.on_edit {
1923                                (callback)(cx, text);
1924                            }
1925                        }
1926                    }
1927                }
1928            }
1929
1930            TextEvent::ToggleCaret => {
1931                self.show_caret ^= true;
1932            }
1933        });
1934    }
1935
1936    // Use custom drawing for the textbox so a transform can be applied to just the text.
1937    fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
1938        cx.draw_shadows(canvas);
1939        cx.draw_background(canvas);
1940        cx.draw_border(canvas);
1941        cx.draw_outline(canvas);
1942
1943        // Clip only the text content to the textbox shape so long text is contained
1944        // without clipping outside effects such as outlines.
1945        canvas.save();
1946        let path = cx.path();
1947        canvas.clip_path(&path, ClipOp::Intersect, true);
1948
1949        let bounds = cx.bounds();
1950        let padding_left = match cx.padding_left() {
1951            Units::Pixels(val) => val,
1952            _ => 0.0,
1953        };
1954        let padding_right = match cx.padding_right() {
1955            Units::Pixels(val) => val,
1956            _ => 0.0,
1957        };
1958        let padding_top = match cx.padding_top() {
1959            Units::Pixels(val) => val,
1960            _ => 0.0,
1961        };
1962        let padding_bottom = match cx.padding_bottom() {
1963            Units::Pixels(val) => val,
1964            _ => 0.0,
1965        };
1966
1967        let content_left = bounds.x + padding_left;
1968        let content_top = bounds.y + padding_top;
1969        let content_right = (bounds.x + bounds.w - padding_right).max(content_left);
1970        let content_bottom = (bounds.y + bounds.h - padding_bottom).max(content_top);
1971        canvas.clip_rect(
1972            Rect::new(content_left, content_top, content_right, content_bottom),
1973            ClipOp::Intersect,
1974            true,
1975        );
1976
1977        let transform = *self.transform.borrow();
1978        canvas.translate((transform.0, transform.1));
1979        cx.draw_text(canvas);
1980
1981        if self.edit {
1982            self.draw_selection(cx, canvas);
1983            self.draw_text_caret(cx, canvas);
1984        }
1985
1986        canvas.restore();
1987    }
1988}