Skip to main content

floem/views/
text_input.rs

1#![deny(missing_docs)]
2use crate::action::{exec_after, set_ime_allowed, set_ime_cursor_area};
3use crate::event::{EventListener, EventPropagation};
4use crate::reactive::{Effect, RwSignal};
5use crate::style::{FontFamily, FontProps, PaddingProp, SelectionStyle, StyleClass, TextAlignProp};
6use crate::style::{FontStyle, FontWeight, TextColor};
7use crate::unit::{PxPct, PxPctAuto};
8use crate::view::ViewId;
9use crate::views::editor::text::Preedit;
10use crate::{Clipboard, prop_extractor, style_class};
11use floem_reactive::{SignalGet, SignalUpdate, SignalWith};
12use taffy::prelude::{Layout, NodeId};
13
14use floem_renderer::Renderer;
15use ui_events::keyboard::{Key, KeyState, KeyboardEvent, Modifiers, NamedKey};
16use ui_events::pointer::{PointerButton, PointerButtonEvent, PointerEvent};
17use unicode_segmentation::UnicodeSegmentation;
18
19use crate::{peniko::color::palette, style::Style, view::View};
20
21use std::{any::Any, ops::Range};
22
23use crate::platform::{Duration, Instant};
24use crate::text::{Attrs, AttrsList, FamilyOwned, TextLayout};
25
26use peniko::Brush;
27use peniko::kurbo::{Point, Rect, Size};
28
29use crate::{
30    context::{EventCx, UpdateCx},
31    event::Event,
32};
33
34use super::Decorators;
35
36style_class!(
37    /// The style class that is applied to all `TextInput` views.
38    pub TextInputClass
39);
40style_class!(
41    /// The style class that is applied to the placeholder `TextInput` text.
42    pub PlaceholderTextClass
43);
44
45prop_extractor! {
46    Extractor {
47        color: TextColor,
48        text_align: TextAlignProp,
49    }
50}
51
52prop_extractor! {
53    PlaceholderStyle {
54        pub color: TextColor,
55        //TODO: pub font_size: FontSize,
56        pub font_weight: FontWeight,
57        pub font_style: FontStyle,
58        pub font_family: FontFamily,
59        pub text_align: TextAlignProp,
60    }
61}
62
63/// Holds text buffer of InputText view.
64struct BufferState {
65    buffer: RwSignal<String>,
66    last_buffer: String,
67}
68
69impl BufferState {
70    fn update(&mut self, update: impl FnOnce(&mut String)) {
71        self.buffer.update(|s| {
72            let last = s.clone();
73            update(s);
74            self.last_buffer = last;
75        });
76    }
77
78    fn get_untracked(&self) -> String {
79        self.buffer.get_untracked()
80    }
81
82    fn with_untracked<T>(&self, f: impl FnOnce(&String) -> T) -> T {
83        self.buffer.with_untracked(f)
84    }
85}
86
87/// Text Input View.
88pub struct TextInput {
89    id: ViewId,
90    buffer: BufferState,
91    /// Optional text shown when the text input buffer is empty.
92    placeholder_text: Option<String>,
93    on_enter: Option<Box<dyn Fn()>>,
94    placeholder_style: PlaceholderStyle,
95    selection_style: SelectionStyle,
96    preedit: Option<Preedit>,
97    // Index of where are we in the main buffer, in bytes.
98    cursor_glyph_idx: usize,
99    // This can be retrieved from the glyph, but we store it for efficiency.
100    cursor_x: f64,
101    text_buf: TextLayout,
102    text_node: Option<NodeId>,
103    // When the visible range changes, we also may need to have a small offset depending on the direction we moved.
104    // This makes sure character under the cursor is always fully visible and correctly aligned,
105    // and may cause the last character in the opposite direction to be "cut".
106    clip_start_x: f64,
107    selection: Option<Range<usize>>,
108    width: f32,
109    height: f32,
110    // Approx max size of a glyph, given the current font weight & size.
111    glyph_max_size: Size,
112    style: Extractor,
113    font: FontProps,
114    cursor_width: f64, // TODO: make this configurable
115    is_focused: bool,
116    last_pointer_down: Point,
117    last_cursor_action_on: Instant,
118    window_origin: Option<Point>,
119    last_ime_cursor_area: Option<(Point, Size)>,
120}
121
122/// Type of cursor movement in navigation.
123#[derive(Clone, Copy, Debug)]
124pub enum Movement {
125    /// Move by a glyph.
126    Glyph,
127    /// Move by a word.
128    Word,
129    /// Move by a line.
130    Line,
131}
132
133/// Type of text direction in the file.
134#[derive(Clone, Copy, Debug)]
135pub enum TextDirection {
136    /// Text direction from left to right.
137    Left,
138    /// Text direction from right to left.
139    Right,
140}
141
142/// Creates a [TextInput] view. This can be used for basic text input.
143/// ### Examples
144/// ```rust
145/// # use floem::prelude::*;
146/// # use floem::prelude::palette::css;
147/// # use floem::text::Weight;
148/// # use floem::style::SelectionCornerRadius;
149/// // Create empty `String` as a text buffer in the read-write signal
150/// let text = RwSignal::new(String::new());
151/// // Create simple text imput from it
152/// let simple = text_input(text)
153///     // Optional placeholder text
154///     .placeholder("Placeholder text")
155///     // Width of the text widget
156///     .style(|s| s
157///         .width(250.)
158///         // Enable keyboard navigation on the widget
159///         .focusable(true)
160///      );
161///
162/// // Stylized text example:
163/// let stylized = text_input(text)
164///     .placeholder("Placeholder text")
165///     .style(|s| s
166///         .border(1.5)
167///         .width(250.0)
168///         .background(css::LIGHT_GRAY)
169///         .border_radius(15.0)
170///         .border_color(css::DIM_GRAY)
171///         .padding(10.0)
172///         // Styles applied on widget pointer hover.
173///         .hover(|s| s.background(css::LIGHT_GRAY.multiply_alpha(0.5)).border_color(css::DARK_GRAY))
174///         .set(SelectionCornerRadius, 4.0)
175///         // Styles applied when widget holds the focus.
176///         .focus(|s| s
177///             .border_color(css::SKY_BLUE)
178///             // Styles applied on widget pointer hover when focused.
179///             .hover(|s| s.border_color(css::SKY_BLUE))
180///         )
181///         // Apply class and override some of its styles.
182///         .class(PlaceholderTextClass, |s| s
183///             .color(css::SKY_BLUE)
184///             .font_style(floem::text::Style::Italic)
185///             .font_weight(Weight::BOLD)
186///         )
187///         .font_family("monospace".to_owned())
188///         .focusable(true)
189///     );
190/// ```
191/// ### Reactivity
192/// The view is reactive and will track updates on buffer signal.
193/// ### Info
194/// For more advanced editing see [TextEditor](super::text_editor::TextEditor).
195pub fn text_input(buffer: RwSignal<String>) -> TextInput {
196    let id = ViewId::new();
197    let is_focused = RwSignal::new(false);
198
199    {
200        Effect::new(move |_| {
201            // subscribe to changes without cloning string
202            buffer.with(|_| {});
203            id.update_state(is_focused.get());
204        });
205    }
206
207    TextInput {
208        id,
209        cursor_glyph_idx: 0,
210        placeholder_text: None,
211        placeholder_style: Default::default(),
212        selection_style: Default::default(),
213        preedit: None,
214        buffer: BufferState {
215            buffer,
216            last_buffer: buffer.get_untracked(),
217        },
218        text_buf: TextLayout::new(),
219        text_node: None,
220        style: Default::default(),
221        font: FontProps::default(),
222        cursor_x: 0.0,
223        selection: None,
224        glyph_max_size: Size::ZERO,
225        clip_start_x: 0.0,
226        cursor_width: 1.0,
227        width: 0.0,
228        height: 0.0,
229        is_focused: false,
230        last_pointer_down: Point::ZERO,
231        last_cursor_action_on: Instant::now(),
232        on_enter: None,
233        window_origin: None,
234        last_ime_cursor_area: None,
235    }
236    .on_event_stop(EventListener::FocusGained, move |_| {
237        is_focused.set(true);
238        set_ime_allowed(true);
239    })
240    .on_event_stop(EventListener::FocusLost, move |_| {
241        is_focused.set(false);
242        set_ime_allowed(false);
243    })
244    .class(TextInputClass)
245}
246
247pub(crate) enum TextCommand {
248    SelectAll,
249    Copy,
250    Paste,
251    Cut,
252    None,
253}
254use ui_events::keyboard::Code;
255
256impl From<&KeyboardEvent> for TextCommand {
257    fn from(event: &KeyboardEvent) -> Self {
258        #[cfg(target_os = "macos")]
259        match (event.modifiers, event.code) {
260            (Modifiers::META, Code::KeyA) => Self::SelectAll,
261            (Modifiers::META, Code::KeyC) => Self::Copy,
262            (Modifiers::META, Code::KeyX) => Self::Cut,
263            (Modifiers::META, Code::KeyV) => Self::Paste,
264            _ => Self::None,
265        }
266        #[cfg(not(target_os = "macos"))]
267        match (event.modifiers, event.code) {
268            (Modifiers::CONTROL, Code::KeyA) => Self::SelectAll,
269            (Modifiers::CONTROL, Code::KeyC) => Self::Copy,
270            (Modifiers::CONTROL, Code::KeyX) => Self::Cut,
271            (Modifiers::CONTROL, Code::KeyV) => Self::Paste,
272            _ => Self::None,
273        }
274    }
275}
276
277/// Determines if motion should be word based.
278fn get_word_based_motion(event: &KeyboardEvent) -> Option<Movement> {
279    #[cfg(not(target_os = "macos"))]
280    return event
281        .modifiers
282        .contains(Modifiers::CONTROL)
283        .then_some(Movement::Word);
284
285    #[cfg(target_os = "macos")]
286    return event
287        .modifiers
288        .contains(Modifiers::ALT)
289        .then_some(Movement::Word)
290        .or(event
291            .modifiers
292            .contains(Modifiers::META)
293            .then_some(Movement::Line));
294}
295
296const DEFAULT_FONT_SIZE: f32 = 14.0;
297const CURSOR_BLINK_INTERVAL_MS: u64 = 500;
298
299impl TextInput {
300    /// Add placeholder text visible when buffer is empty.
301    /// ```
302    /// # use floem::views::text_input;
303    /// # use floem_reactive::RwSignal;
304    /// let text = RwSignal::new(String::new());
305    /// let simple = text_input(text)
306    ///     // Optional placeholder text
307    ///     .placeholder("Placeholder text");
308    /// ```
309    /// ### Reactivity
310    /// This method is not reactive.
311    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
312        self.placeholder_text = Some(text.into());
313        self
314    }
315
316    /// Add action that will run on `Enter` key press.
317    ///
318    /// Useful for submitting forms using a keyboard.
319    /// ```
320    /// # use floem::views::text_input;
321    /// # use floem_reactive::RwSignal;
322    /// # use floem_reactive::SignalGet;
323    /// let form = RwSignal::new(String::new());
324    /// text_input(form)
325    ///     .placeholder("fill the form")
326    ///     .on_enter(move || { format!("Form {} submitted!", form.get_untracked()); });
327    /// ``````
328    /// ### Reactivity
329    /// This method is not reactive, but will always run provided function
330    /// when pressed `Enter`.
331    pub fn on_enter(mut self, action: impl Fn() + 'static) -> Self {
332        self.on_enter = Some(Box::new(action));
333        self
334    }
335}
336
337impl TextInput {
338    fn move_cursor(&mut self, move_kind: Movement, direction: TextDirection) -> bool {
339        match (move_kind, direction) {
340            (Movement::Glyph, TextDirection::Left) => {
341                let untracked_buffer = self.buffer.get_untracked();
342                let mut grapheme_iter = untracked_buffer[..self.cursor_glyph_idx].graphemes(true);
343                match grapheme_iter.next_back() {
344                    None => false,
345                    Some(prev_character) => {
346                        self.cursor_glyph_idx -= prev_character.len();
347                        true
348                    }
349                }
350            }
351            (Movement::Glyph, TextDirection::Right) => {
352                let untracked_buffer = self.buffer.get_untracked();
353                let mut grapheme_iter = untracked_buffer[self.cursor_glyph_idx..].graphemes(true);
354                match grapheme_iter.next() {
355                    None => false,
356                    Some(next_character) => {
357                        self.cursor_glyph_idx += next_character.len();
358                        true
359                    }
360                }
361            }
362            (Movement::Line, TextDirection::Right) => {
363                if self.cursor_glyph_idx < self.buffer.with_untracked(|buff| buff.len()) {
364                    self.cursor_glyph_idx = self.buffer.with_untracked(|buff| buff.len());
365                    return true;
366                }
367                false
368            }
369            (Movement::Line, TextDirection::Left) => {
370                if self.cursor_glyph_idx > 0 {
371                    self.cursor_glyph_idx = 0;
372                    return true;
373                }
374                false
375            }
376            (Movement::Word, TextDirection::Right) => self.buffer.with_untracked(|buff| {
377                for (idx, word) in buff.unicode_word_indices() {
378                    let word_end_idx = idx + word.len();
379                    if word_end_idx > self.cursor_glyph_idx {
380                        self.cursor_glyph_idx = word_end_idx;
381                        return true;
382                    }
383                }
384                false
385            }),
386            (Movement::Word, TextDirection::Left) if self.cursor_glyph_idx > 0 => {
387                self.buffer.with_untracked(|buff| {
388                    let mut prev_word_idx = 0;
389                    for (idx, _) in buff.unicode_word_indices() {
390                        if idx < self.cursor_glyph_idx {
391                            prev_word_idx = idx;
392                        } else {
393                            break;
394                        }
395                    }
396                    self.cursor_glyph_idx = prev_word_idx;
397                    true
398                })
399            }
400            (_movement, _dir) => false,
401        }
402    }
403
404    fn cursor_visual_idx(&self) -> usize {
405        let Some(preedit) = &self.preedit else {
406            return self.cursor_glyph_idx;
407        };
408
409        let Some(cursor) = preedit.cursor else {
410            return self.cursor_glyph_idx + preedit.text.len();
411        };
412
413        self.cursor_glyph_idx + cursor.0
414    }
415
416    fn calculate_clip_offset(&mut self, node_layout: &Layout) {
417        let node_width = node_layout.size.width as f64;
418        let cursor_glyph_pos = self.text_buf.hit_position(self.cursor_visual_idx());
419        let cursor_x = cursor_glyph_pos.point.x;
420
421        let mut clip_start_x = self.clip_start_x;
422
423        if cursor_x < clip_start_x {
424            clip_start_x = cursor_x;
425        } else if cursor_x > clip_start_x + node_width {
426            clip_start_x = cursor_x - node_width;
427        }
428
429        self.cursor_x = cursor_x;
430        self.clip_start_x = clip_start_x;
431
432        self.update_text_layout();
433    }
434
435    fn get_cursor_rect(&self, node_layout: &Layout) -> Rect {
436        let node_location = node_layout.location;
437
438        let text_height = self.height as f64;
439
440        let cursor_start = Point::new(
441            self.cursor_x - self.clip_start_x + node_location.x as f64,
442            node_location.y as f64,
443        );
444
445        Rect::from_points(
446            cursor_start,
447            Point::new(
448                cursor_start.x + self.cursor_width,
449                node_location.y as f64 + text_height,
450            ),
451        )
452    }
453
454    fn scroll(&mut self, offset: f64) {
455        self.clip_start_x += offset;
456        self.clip_start_x = self
457            .clip_start_x
458            .min(self.text_buf.size().width - self.width as f64)
459            .max(0.0);
460    }
461
462    fn handle_double_click(&mut self, pos_x: f64) {
463        let clicked_glyph_idx = self.get_box_position(pos_x);
464
465        self.buffer.with_untracked(|buff| {
466            let selection = get_dbl_click_selection(clicked_glyph_idx, buff);
467            if selection.start != selection.end {
468                self.cursor_glyph_idx = selection.end;
469                self.selection = Some(selection);
470            }
471        })
472    }
473
474    fn handle_triple_click(&mut self) {
475        self.select_all();
476    }
477
478    fn get_box_position(&self, pos_x: f64) -> usize {
479        let layout = self.id.get_layout().unwrap_or_default();
480        let view_state = self.id.state();
481        let view_state = view_state.borrow();
482        let style = view_state.combined_style.builtin();
483
484        let padding_left = match style.padding_left() {
485            PxPct::Px(padding) => padding as f32,
486            PxPct::Pct(pct) => pct as f32 * layout.size.width,
487        };
488        self.text_buf
489            .hit_point(Point::new(
490                pos_x + self.clip_start_x - padding_left as f64,
491                0.0,
492            ))
493            .index
494    }
495
496    fn get_selection_rect(&self, node_layout: &Layout, left_padding: f64) -> Rect {
497        let selection = if let Some(curr_selection) = &self.selection {
498            curr_selection.clone()
499        } else if let Some(cursor) = self.preedit.as_ref().and_then(|p| p.cursor) {
500            self.cursor_glyph_idx + cursor.0..self.cursor_glyph_idx + cursor.1
501        } else {
502            return Rect::ZERO;
503        };
504
505        let text_height = self.height;
506
507        let selection_start_x =
508            self.text_buf.hit_position(selection.start).point.x - self.clip_start_x;
509        let selection_start_x = selection_start_x.max(node_layout.location.x as f64 - left_padding);
510
511        let selection_end_x =
512            self.text_buf.hit_position(selection.end).point.x + left_padding - self.clip_start_x;
513        let selection_end_x =
514            selection_end_x.min(selection_start_x + self.width as f64 + left_padding);
515
516        let node_location = node_layout.location;
517
518        let selection_start = Point::new(
519            selection_start_x + node_location.x as f64,
520            node_location.y as f64,
521        );
522
523        Rect::from_points(
524            selection_start,
525            Point::new(selection_end_x, selection_start.y + text_height as f64),
526        )
527    }
528
529    /// Determine approximate max size of a single glyph, given the current font weight & size
530    fn get_font_glyph_max_size(&self) -> Size {
531        let mut tmp = TextLayout::new();
532        let attrs_list = self.get_text_attrs();
533        let align = self.style.text_align();
534        tmp.set_text("W", attrs_list, align);
535        tmp.size() + Size::new(0., tmp.hit_position(0).glyph_descent)
536    }
537
538    fn update_selection(&mut self, selection_start: usize, selection_end: usize) {
539        self.selection = match selection_start.cmp(&selection_end) {
540            std::cmp::Ordering::Less => Some(Range {
541                start: selection_start,
542                end: selection_end,
543            }),
544            std::cmp::Ordering::Greater => Some(Range {
545                start: selection_end,
546                end: selection_start,
547            }),
548            std::cmp::Ordering::Equal => None,
549        };
550    }
551
552    fn update_ime_cursor_area(&mut self) {
553        if !self.is_focused {
554            return;
555        }
556
557        let (Some(layout), Some(origin)) = (self.id.get_layout(), self.window_origin) else {
558            return;
559        };
560
561        let left_padding = layout.border.left + layout.padding.left;
562        let top_padding = layout.border.top + layout.padding.top;
563
564        let pos = Point::new(
565            origin.x + self.cursor_x - self.clip_start_x + left_padding as f64,
566            origin.y + top_padding as f64,
567        );
568
569        let width = self
570            .preedit
571            .as_ref()
572            .map(|preedit| {
573                let start_idx = preedit.offset;
574                let end_idx = start_idx + preedit.text.len();
575
576                let start_x = self.text_buf.hit_position(start_idx).point.x;
577                let end_x = self.text_buf.hit_position(end_idx).point.x;
578
579                (end_x - start_x).abs()
580            })
581            .unwrap_or_default();
582
583        let size = Size::new(width, layout.content_box_height() as f64);
584
585        if self.last_ime_cursor_area != Some((pos, size)) {
586            set_ime_cursor_area(pos, size);
587            self.last_ime_cursor_area = Some((pos, size));
588        }
589    }
590
591    fn commit_preedit(&mut self) -> bool {
592        if let Some(preedit) = self.preedit.take() {
593            self.buffer
594                .update(|buf| buf.insert_str(self.cursor_glyph_idx, &preedit.text));
595
596            if self.is_focused {
597                // toggle IME to flush external preedit state
598                set_ime_allowed(false);
599                set_ime_allowed(true);
600                // ime area will be set in compute_layout
601            }
602
603            self.update_text_layout();
604            true
605        } else {
606            false
607        }
608    }
609
610    fn update_text_layout(&mut self) {
611        let glyph_max_size = self.get_font_glyph_max_size();
612        self.height = glyph_max_size.height as f32;
613        self.glyph_max_size = glyph_max_size;
614
615        let buffer_is_empty = self.buffer.with_untracked(|buff| {
616            buff.is_empty() && self.preedit.as_ref().is_none_or(|p| p.text.is_empty())
617        });
618
619        if let (Some(placeholder_text), true) = (&self.placeholder_text, buffer_is_empty) {
620            let attrs_list = self.get_placeholder_text_attrs();
621            self.text_buf.set_text(
622                placeholder_text,
623                attrs_list,
624                self.placeholder_style.text_align(),
625            );
626        } else {
627            let attrs_list = self.get_text_attrs();
628            let align = self.style.text_align();
629            self.buffer.with_untracked(|buff| {
630                let preedited;
631                let display_text = if let Some(preedit) = &self.preedit {
632                    let preedit_offset = self.cursor_glyph_idx.min(buff.len());
633
634                    preedited = [
635                        &buff[..preedit_offset],
636                        &preedit.text,
637                        &buff[preedit_offset..],
638                    ]
639                    .concat();
640
641                    &preedited
642                } else {
643                    buff
644                };
645
646                self.text_buf
647                    .set_text(display_text, attrs_list.clone(), align);
648            });
649        }
650    }
651
652    fn font_size(&self) -> f32 {
653        self.font.size().unwrap_or(DEFAULT_FONT_SIZE)
654    }
655
656    /// Retrieve attributes for the placeholder text.
657    pub fn get_placeholder_text_attrs(&self) -> AttrsList {
658        let mut attrs = Attrs::new().color(
659            self.placeholder_style
660                .color()
661                .unwrap_or(palette::css::BLACK),
662        );
663
664        //TODO:
665        // self.placeholder_style
666        //     .font_size()
667        //     .unwrap_or(self.font_size())
668        attrs = attrs.font_size(self.font_size());
669
670        if let Some(font_style) = self.placeholder_style.font_style() {
671            attrs = attrs.style(font_style);
672        } else if let Some(font_style) = self.font.style() {
673            attrs = attrs.style(font_style);
674        }
675
676        if let Some(font_weight) = self.placeholder_style.font_weight() {
677            attrs = attrs.weight(font_weight);
678        } else if let Some(font_weight) = self.font.weight() {
679            attrs = attrs.weight(font_weight);
680        }
681
682        let input_font_family = self.font.family().as_ref().map(|font_family| {
683            let family: Vec<FamilyOwned> = FamilyOwned::parse_list(font_family).collect();
684            family
685        });
686
687        let placeholder_font_family =
688            self.placeholder_style
689                .font_family()
690                .as_ref()
691                .map(|font_family| {
692                    let family: Vec<FamilyOwned> = FamilyOwned::parse_list(font_family).collect();
693                    family
694                });
695
696        // Inherit the font family of the text input unless overridden by the placeholder
697        if let Some(font_family) = placeholder_font_family.as_ref() {
698            attrs = attrs.family(font_family);
699        } else if let Some(font_family) = input_font_family.as_ref() {
700            attrs = attrs.family(font_family);
701        }
702
703        AttrsList::new(attrs)
704    }
705
706    /// Retrieve attributes for the text.
707    pub fn get_text_attrs(&self) -> AttrsList {
708        let mut attrs = Attrs::new().color(self.style.color().unwrap_or(palette::css::BLACK));
709
710        attrs = attrs.font_size(self.font_size());
711
712        if let Some(font_style) = self.font.style() {
713            attrs = attrs.style(font_style);
714        }
715        let font_family = self.font.family().as_ref().map(|font_family| {
716            let family: Vec<FamilyOwned> = FamilyOwned::parse_list(font_family).collect();
717            family
718        });
719        if let Some(font_family) = font_family.as_ref() {
720            attrs = attrs.family(font_family);
721        }
722        if let Some(font_weight) = self.font.weight() {
723            attrs = attrs.weight(font_weight);
724        }
725        AttrsList::new(attrs)
726    }
727
728    /// Select all text in the buffer.
729    fn select_all(&mut self) {
730        let len = self.buffer.with_untracked(|val| val.len());
731
732        if len == 0 {
733            return;
734        }
735
736        let text_node = self.text_node.unwrap();
737        let node_layout = self
738            .id
739            .taffy()
740            .borrow()
741            .layout(text_node)
742            .cloned()
743            .unwrap_or_default();
744        self.cursor_glyph_idx = len;
745
746        let buf_width = self.text_buf.size().width;
747        let node_width = node_layout.size.width as f64;
748
749        if buf_width > node_width {
750            self.calculate_clip_offset(&node_layout);
751        }
752
753        self.selection = Some(0..len);
754    }
755
756    fn handle_modifier_cmd(&mut self, event: &KeyboardEvent) -> bool {
757        if event.modifiers.is_empty() || event.modifiers == Modifiers::SHIFT {
758            return false;
759        }
760
761        let command = event.into();
762
763        match command {
764            TextCommand::SelectAll => {
765                self.select_all();
766                true
767            }
768            TextCommand::Copy => {
769                if let Some(selection) = &self.selection {
770                    let selection_txt = self
771                        .buffer
772                        .get_untracked()
773                        .chars()
774                        .skip(selection.start)
775                        .take(selection.end - selection.start)
776                        .collect();
777                    let _ = Clipboard::set_contents(selection_txt);
778                }
779                true
780            }
781            TextCommand::Cut => {
782                if let Some(selection) = &self.selection {
783                    let selection_txt = self
784                        .buffer
785                        .get_untracked()
786                        .chars()
787                        .skip(selection.start)
788                        .take(selection.end - selection.start)
789                        .collect();
790                    let _ = Clipboard::set_contents(selection_txt);
791
792                    self.buffer
793                        .update(|buf| replace_range(buf, selection.clone(), None));
794
795                    self.cursor_glyph_idx = selection.start;
796                    self.selection = None;
797                }
798
799                true
800            }
801            TextCommand::Paste => {
802                let mut clipboard_content = match Clipboard::get_contents() {
803                    Ok(content) => content,
804                    Err(_) => return false,
805                };
806
807                clipboard_content.retain(|c| c != '\r' && c != '\n');
808
809                if clipboard_content.is_empty() {
810                    return false;
811                }
812
813                if let Some(selection) = self.selection.take() {
814                    self.buffer.update(|buf| {
815                        replace_range(buf, selection.clone(), Some(&clipboard_content))
816                    });
817
818                    self.cursor_glyph_idx = selection.start + clipboard_content.len();
819                } else {
820                    self.buffer
821                        .update(|buf| buf.insert_str(self.cursor_glyph_idx, &clipboard_content));
822                    self.cursor_glyph_idx += clipboard_content.len();
823                }
824
825                true
826            }
827            TextCommand::None => {
828                self.selection = None;
829                false
830            }
831        }
832    }
833
834    fn handle_key_down(&mut self, cx: &mut EventCx, event: &KeyboardEvent) -> bool {
835        let handled = match event.key {
836            Key::Character(ref c) if c == " " => {
837                if let Some(selection) = &self.selection {
838                    self.buffer
839                        .update(|buf| replace_range(buf, selection.clone(), None));
840                    self.cursor_glyph_idx = selection.start;
841                    self.selection = None;
842                } else {
843                    self.buffer
844                        .update(|buf| buf.insert(self.cursor_glyph_idx, ' '));
845                }
846                self.move_cursor(Movement::Glyph, TextDirection::Right)
847            }
848            Key::Named(NamedKey::Backspace) => {
849                let selection = self.selection.clone();
850                if let Some(selection) = selection {
851                    self.cursor_glyph_idx = selection.start;
852                    self.buffer
853                        .update(|buf| replace_range(buf, selection, None));
854                    self.selection = None;
855                    true
856                } else {
857                    let prev_cursor_idx = self.cursor_glyph_idx;
858
859                    self.move_cursor(
860                        get_word_based_motion(event).unwrap_or(Movement::Glyph),
861                        TextDirection::Left,
862                    );
863
864                    if self.cursor_glyph_idx == prev_cursor_idx {
865                        return false;
866                    }
867
868                    self.buffer.update(|buf| {
869                        replace_range(buf, self.cursor_glyph_idx..prev_cursor_idx, None);
870                    });
871                    true
872                }
873            }
874            Key::Named(NamedKey::Delete) => {
875                let selection = self.selection.clone();
876                if let Some(selection) = selection {
877                    self.cursor_glyph_idx = selection.start;
878                    self.buffer
879                        .update(|buf| replace_range(buf, selection, None));
880                    self.selection = None;
881                    return true;
882                }
883
884                let prev_cursor_idx = self.cursor_glyph_idx;
885
886                self.move_cursor(
887                    get_word_based_motion(event).unwrap_or(Movement::Glyph),
888                    TextDirection::Right,
889                );
890
891                if self.cursor_glyph_idx == prev_cursor_idx {
892                    return false;
893                }
894
895                self.buffer.update(|buf| {
896                    replace_range(buf, prev_cursor_idx..self.cursor_glyph_idx, None);
897                });
898
899                self.cursor_glyph_idx = prev_cursor_idx;
900                true
901            }
902            Key::Named(NamedKey::Escape) => {
903                cx.window_state.clear_focus();
904                true
905            }
906            Key::Named(NamedKey::Enter) => {
907                if let Some(action) = &self.on_enter {
908                    action();
909                }
910                true
911            }
912            Key::Named(NamedKey::End) => {
913                if event.modifiers.contains(Modifiers::SHIFT) {
914                    match &self.selection {
915                        Some(selection_value) => self.update_selection(
916                            selection_value.start,
917                            self.buffer.get_untracked().len(),
918                        ),
919                        None => self.update_selection(
920                            self.cursor_glyph_idx,
921                            self.buffer.get_untracked().len(),
922                        ),
923                    }
924                } else {
925                    self.selection = None;
926                }
927                self.move_cursor(Movement::Line, TextDirection::Right)
928            }
929            Key::Named(NamedKey::Home) => {
930                if event.modifiers.contains(Modifiers::SHIFT) {
931                    match &self.selection {
932                        Some(selection_value) => self.update_selection(0, selection_value.end),
933                        None => self.update_selection(0, self.cursor_glyph_idx),
934                    }
935                } else {
936                    self.selection = None;
937                }
938                self.move_cursor(Movement::Line, TextDirection::Left)
939            }
940            Key::Named(NamedKey::ArrowLeft) => {
941                let old_glyph_idx = self.cursor_glyph_idx;
942
943                let move_kind = get_word_based_motion(event).unwrap_or(Movement::Glyph);
944                let cursor_moved = self.move_cursor(move_kind, TextDirection::Left);
945
946                if event.modifiers.contains(Modifiers::SHIFT) {
947                    self.move_selection(old_glyph_idx, self.cursor_glyph_idx);
948                } else if let Some(selection) = self.selection.take() {
949                    // clear and jump to the start of the selection
950                    if matches!(move_kind, Movement::Glyph) {
951                        self.cursor_glyph_idx = selection.start;
952                    }
953                }
954
955                cursor_moved
956            }
957            Key::Named(NamedKey::ArrowRight) => {
958                let old_glyph_idx = self.cursor_glyph_idx;
959
960                let move_kind = get_word_based_motion(event).unwrap_or(Movement::Glyph);
961                let cursor_moved = self.move_cursor(move_kind, TextDirection::Right);
962
963                if event.modifiers.contains(Modifiers::SHIFT) {
964                    self.move_selection(old_glyph_idx, self.cursor_glyph_idx);
965                } else if let Some(selection) = self.selection.take() {
966                    // clear and jump to the end of the selection
967                    if matches!(move_kind, Movement::Glyph) {
968                        self.cursor_glyph_idx = selection.end;
969                    }
970                }
971
972                cursor_moved
973            }
974            _ => false,
975        };
976        if handled {
977            return true;
978        }
979
980        match event.key {
981            Key::Character(ref ch) => {
982                let handled_modifier_cmd = self.handle_modifier_cmd(event);
983                if handled_modifier_cmd {
984                    return true;
985                }
986                let non_shift_mask = Modifiers::all().difference(Modifiers::SHIFT);
987                if event.modifiers.intersects(non_shift_mask) {
988                    return false;
989                }
990                self.insert_text(ch)
991            }
992            _ => false,
993        }
994    }
995
996    fn insert_text(&mut self, ch: &str) -> bool {
997        let selection = self.selection.clone();
998        if let Some(selection) = selection {
999            self.buffer
1000                .update(|buf| replace_range(buf, selection.clone(), None));
1001            self.cursor_glyph_idx = selection.start;
1002            self.selection = None;
1003        }
1004
1005        self.buffer
1006            .update(|buf| buf.insert_str(self.cursor_glyph_idx, ch));
1007        self.move_cursor(Movement::Glyph, TextDirection::Right)
1008    }
1009
1010    fn move_selection(&mut self, old_glyph_idx: usize, curr_glyph_idx: usize) {
1011        let new_selection = if let Some(selection) = &self.selection {
1012            // we're making an assumption that the caret is at the selection's edge
1013            // the opposite edge will be our anchor
1014            let anchor = if selection.start == old_glyph_idx {
1015                selection.end
1016            } else {
1017                selection.start
1018            };
1019
1020            if anchor < curr_glyph_idx {
1021                anchor..curr_glyph_idx
1022            } else {
1023                curr_glyph_idx..anchor
1024            }
1025        } else if old_glyph_idx < curr_glyph_idx {
1026            old_glyph_idx..curr_glyph_idx
1027        } else {
1028            curr_glyph_idx..old_glyph_idx
1029        };
1030
1031        // avoid empty selection
1032        self.selection = if new_selection.is_empty() {
1033            None
1034        } else {
1035            Some(new_selection)
1036        };
1037    }
1038
1039    fn paint_selection_rect(&self, &node_layout: &Layout, cx: &mut crate::context::PaintCx<'_>) {
1040        let view_state = self.id.state();
1041        let view_state = view_state.borrow();
1042        let style = &view_state.combined_style;
1043
1044        let cursor_color = self.selection_style.selection_color();
1045
1046        let padding_left = match style.get(PaddingProp).left.unwrap_or(PxPct::Px(0.)) {
1047            PxPct::Px(padding) => padding,
1048            PxPct::Pct(pct) => pct / 100.0 * node_layout.size.width as f64,
1049        };
1050
1051        let border_radius = self.selection_style.corner_radius();
1052        let selection_rect = self
1053            .get_selection_rect(&node_layout, padding_left)
1054            .to_rounded_rect(border_radius);
1055        cx.save();
1056        cx.clip(&self.id.get_content_rect());
1057        cx.fill(&selection_rect, &cursor_color, 0.0);
1058        cx.restore();
1059    }
1060}
1061
1062fn replace_range(buff: &mut String, del_range: Range<usize>, replacement: Option<&str>) {
1063    assert!(del_range.start <= del_range.end);
1064    if !buff.is_char_boundary(del_range.end) {
1065        eprintln!("[Floem] Tried to delete range with invalid end: {del_range:?}");
1066        return;
1067    }
1068
1069    if !buff.is_char_boundary(del_range.start) {
1070        eprintln!("[Floem] Tried to delete range with invalid start: {del_range:?}");
1071        return;
1072    }
1073
1074    // Get text after range to delete
1075    let after_del_range = buff.split_off(del_range.end);
1076
1077    // Truncate up to range's start to delete it
1078    buff.truncate(del_range.start);
1079
1080    if let Some(repl) = replacement {
1081        buff.push_str(repl);
1082    }
1083
1084    buff.push_str(&after_del_range);
1085}
1086
1087fn get_dbl_click_selection(glyph_idx: usize, buffer: &str) -> Range<usize> {
1088    let mut selectable_ranges: Vec<Range<usize>> = Vec::new();
1089    let glyph_idx = usize::min(glyph_idx, buffer.len().saturating_sub(1));
1090
1091    for (idx, word) in buffer.unicode_word_indices() {
1092        let word_range = idx..idx + word.len();
1093
1094        if let Some(prev) = selectable_ranges.last() {
1095            if prev.end != idx {
1096                // non-alphanumeric char sequence between previous word and current word
1097                selectable_ranges.push(prev.end..idx);
1098            }
1099        } else if idx > 0 {
1100            // non-alphanumeric char sequence at the beginning of the buffer(before the first word)
1101            selectable_ranges.push(0..idx);
1102        }
1103
1104        selectable_ranges.push(word_range);
1105    }
1106
1107    // left-over non-alphanumeric char sequence at the end of the buffer(after the last word)
1108    if let Some(last) = selectable_ranges.last() {
1109        if last.end != buffer.len() {
1110            selectable_ranges.push(last.end..buffer.len());
1111        }
1112    }
1113
1114    for range in selectable_ranges {
1115        if range.contains(&glyph_idx) {
1116            return range;
1117        }
1118    }
1119
1120    // should reach here only if buffer does not contain any words(only non-alphanumeric characters)
1121    0..buffer.len()
1122}
1123
1124impl View for TextInput {
1125    fn id(&self) -> ViewId {
1126        self.id
1127    }
1128
1129    fn debug_name(&self) -> std::borrow::Cow<'static, str> {
1130        format!("TextInput: {:?}", self.buffer.get_untracked()).into()
1131    }
1132
1133    fn update(&mut self, cx: &mut UpdateCx, state: Box<dyn Any>) {
1134        if let Ok(state) = state.downcast::<bool>() {
1135            let is_focused = *state;
1136
1137            if self.is_focused != is_focused {
1138                self.is_focused = is_focused;
1139                self.last_ime_cursor_area = None;
1140
1141                self.commit_preedit();
1142                self.update_ime_cursor_area();
1143
1144                if is_focused && !cx.window_state.is_active(&self.id) {
1145                    self.selection = None;
1146                    self.cursor_glyph_idx = self.buffer.with_untracked(|buf| buf.len());
1147                }
1148            }
1149
1150            // Only update recomputation if the state has actually changed
1151            let text_updated = self.buffer.buffer.with_untracked(|buf| {
1152                let updated = *buf != self.buffer.last_buffer;
1153
1154                if updated {
1155                    self.buffer.last_buffer.clone_from(buf);
1156                }
1157
1158                updated
1159            });
1160
1161            if text_updated {
1162                self.update_text_layout();
1163                self.id.request_layout();
1164            }
1165        } else {
1166            eprintln!("downcast failed");
1167        }
1168    }
1169
1170    fn event_before_children(&mut self, cx: &mut EventCx, event: &Event) -> EventPropagation {
1171        let buff_len = self.buffer.with_untracked(|buff| buff.len());
1172        // Workaround for cursor going out of bounds when text buffer is modified externally
1173        // TODO: find a better way to handle this
1174        if self.cursor_glyph_idx > buff_len {
1175            self.cursor_glyph_idx = buff_len;
1176        }
1177
1178        let is_handled = match &event {
1179            // match on pointer primary button press
1180            Event::Pointer(PointerEvent::Down(PointerButtonEvent {
1181                button: Some(PointerButton::Primary),
1182                state,
1183                ..
1184            })) => {
1185                cx.update_active(self.id);
1186                let point = state.logical_point();
1187                self.last_pointer_down = point;
1188
1189                self.commit_preedit();
1190
1191                if self.buffer.with_untracked(|buff| !buff.is_empty()) {
1192                    if state.count == 2 {
1193                        self.handle_double_click(point.x);
1194                    } else if state.count == 3 {
1195                        self.handle_triple_click();
1196                    } else {
1197                        self.cursor_glyph_idx = self.get_box_position(point.x);
1198                        self.selection = None;
1199                    }
1200                }
1201                true
1202            }
1203            Event::Pointer(PointerEvent::Move(pu)) => {
1204                if cx.is_active(self.id) && self.buffer.with_untracked(|buff| !buff.is_empty()) {
1205                    if self.commit_preedit() {
1206                        self.id.request_layout();
1207                    }
1208                    let pos = pu.current.logical_point();
1209
1210                    if pos.x < 0. && pos.x < self.last_pointer_down.x {
1211                        self.scroll(pos.x);
1212                    } else if pos.x > self.width as f64 && pos.x > self.last_pointer_down.x {
1213                        self.scroll(pos.x - self.width as f64);
1214                    }
1215
1216                    let selection_stop = self.get_box_position(pos.x);
1217                    self.update_selection(self.cursor_glyph_idx, selection_stop);
1218
1219                    self.id.request_paint();
1220                }
1221                false
1222            }
1223            Event::Key(
1224                ke @ KeyboardEvent {
1225                    state: KeyState::Down,
1226                    ..
1227                },
1228            ) => self.handle_key_down(cx, ke),
1229            Event::ImePreedit { text, cursor } => {
1230                if self.is_focused && !text.is_empty() {
1231                    if let Some(selection) = self.selection.take() {
1232                        self.cursor_glyph_idx = selection.start;
1233                        self.buffer
1234                            .update(|buf| replace_range(buf, selection.clone(), None));
1235                    }
1236
1237                    let mut preedit = self.preedit.take().unwrap_or_else(|| Preedit {
1238                        text: Default::default(),
1239                        cursor: None,
1240                        offset: 0,
1241                    });
1242                    preedit.text.clone_from(text);
1243                    preedit.cursor = *cursor;
1244                    self.preedit = Some(preedit);
1245
1246                    true
1247                } else {
1248                    // clear preedit and queue UI update
1249                    self.preedit.take().is_some()
1250                }
1251            }
1252            Event::ImeDeleteSurrounding {
1253                before_bytes,
1254                after_bytes,
1255            } => {
1256                if self.is_focused {
1257                    self.buffer.update(|buf| {
1258                        if let Some(selection) = self.selection.take() {
1259                            self.cursor_glyph_idx = selection.start;
1260                            buf.replace_range(selection, "");
1261                        }
1262                        // If the index falls inside a character, delete that character too.
1263                        // This only happens on desynchronized input:
1264                        // 1. IME sends a request with index on code point boundary
1265                        // 2. Another source shifts text around
1266                        // 3. Request arrives.
1267                        // This situation is expected to be rare, so not trying to be too clever at handling it.
1268                        let before_start = buf[..self.cursor_glyph_idx]
1269                            .char_indices()
1270                            .rev()
1271                            .find(|(index, _)| self.cursor_glyph_idx - index >= *before_bytes)
1272                            .map(|(i, _)| i)
1273                            .unwrap_or(0);
1274                        let after_end = buf[self.cursor_glyph_idx..]
1275                            .char_indices()
1276                            .map(|(index, _)| index)
1277                            .find(|index| index >= after_bytes)
1278                            .map(|i| i + self.cursor_glyph_idx)
1279                            .unwrap_or(buf.len());
1280                        buf.replace_range(before_start..after_end, "");
1281                        self.cursor_glyph_idx = before_start;
1282                    });
1283                    true
1284                } else {
1285                    false
1286                }
1287            }
1288            Event::ImeCommit(text) => {
1289                if self.is_focused {
1290                    self.buffer
1291                        .update(|buf| buf.insert_str(self.cursor_glyph_idx, text));
1292                    self.cursor_glyph_idx += text.len();
1293                    self.preedit = None;
1294
1295                    true
1296                } else {
1297                    false
1298                }
1299            }
1300            _ => false,
1301        };
1302
1303        if is_handled {
1304            self.update_text_layout();
1305            self.id.request_layout();
1306            self.last_cursor_action_on = Instant::now();
1307        }
1308
1309        if is_handled {
1310            EventPropagation::Stop
1311        } else {
1312            EventPropagation::Continue
1313        }
1314    }
1315
1316    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
1317        let style = cx.style();
1318
1319        let placeholder_style = cx.resolve_nested_maps(
1320            style.clone(),
1321            &[PlaceholderTextClass::class_ref()],
1322            false,
1323            false,
1324            false,
1325        );
1326        self.placeholder_style.read_style(cx, &placeholder_style);
1327
1328        if self.font.read(cx) {
1329            self.update_text_layout();
1330            self.id.request_layout();
1331        }
1332        if self.style.read(cx) {
1333            cx.window_state.request_paint(self.id);
1334
1335            // necessary to update the text layout attrs
1336            self.update_text_layout();
1337        }
1338
1339        self.selection_style.read_style(cx, &style);
1340    }
1341
1342    fn layout(&mut self, cx: &mut crate::context::LayoutCx) -> taffy::tree::NodeId {
1343        cx.layout_node(self.id(), true, |cx| {
1344            let was_focused = self.is_focused;
1345            self.is_focused = cx.window_state.is_focused(&self.id);
1346
1347            if was_focused && !self.is_focused {
1348                self.selection = None;
1349            }
1350
1351            if self.text_node.is_none() {
1352                self.text_node = Some(
1353                    self.id
1354                        .taffy()
1355                        .borrow_mut()
1356                        .new_leaf(taffy::style::Style::DEFAULT)
1357                        .unwrap(),
1358                );
1359            }
1360
1361            let text_node = self.text_node.unwrap();
1362
1363            let style = Style::new()
1364                .width(PxPctAuto::Pct(100.))
1365                .height(self.height)
1366                .to_taffy_style();
1367            let _ = self.id.taffy().borrow_mut().set_style(text_node, style);
1368
1369            vec![text_node]
1370        })
1371    }
1372
1373    fn compute_layout(&mut self, cx: &mut crate::context::ComputeLayoutCx) -> Option<Rect> {
1374        self.width = self.id.get_content_rect().width() as f32;
1375        let buf_width = self.text_buf.size().width;
1376        let text_node = self.text_node.unwrap();
1377        let node_layout = self
1378            .id
1379            .taffy()
1380            .borrow()
1381            .layout(text_node)
1382            .cloned()
1383            .unwrap_or_default();
1384        let node_width = node_layout.size.width as f64;
1385
1386        if buf_width > node_width {
1387            self.calculate_clip_offset(&node_layout);
1388        } else {
1389            self.clip_start_x = 0.0;
1390            let hit_pos = self.text_buf.hit_position(self.cursor_visual_idx());
1391            self.cursor_x = hit_pos.point.x;
1392        }
1393
1394        self.window_origin = Some(cx.window_origin);
1395        self.update_ime_cursor_area();
1396
1397        None
1398    }
1399
1400    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
1401        let text_node = self.text_node.unwrap();
1402        let node_layout = self
1403            .id
1404            .taffy()
1405            .borrow()
1406            .layout(text_node)
1407            .cloned()
1408            .unwrap_or_default();
1409
1410        let location = node_layout.location;
1411        let text_start_point = Point::new(location.x as f64, location.y as f64);
1412
1413        cx.save();
1414        cx.clip(&self.id.get_content_rect());
1415        cx.draw_text(
1416            &self.text_buf,
1417            Point::new(text_start_point.x - self.clip_start_x, text_start_point.y),
1418        );
1419
1420        // underline the preedit text
1421        if let Some(preedit) = &self.preedit {
1422            let start_idx = self.cursor_glyph_idx;
1423            let end_idx = start_idx + preedit.text.len();
1424
1425            let start_hit = self.text_buf.hit_position(start_idx);
1426            let start_x = location.x as f64 + start_hit.point.x - self.clip_start_x;
1427            let end_x =
1428                location.x as f64 + self.text_buf.hit_position(end_idx).point.x - self.clip_start_x;
1429
1430            let color = self.style.color().unwrap_or(palette::css::BLACK);
1431            let y = location.y as f64 + start_hit.glyph_ascent;
1432
1433            cx.fill(
1434                &Rect::new(start_x, y, end_x, y + 1.0),
1435                &Brush::Solid(color),
1436                0.0,
1437            );
1438        }
1439
1440        cx.restore();
1441
1442        // skip rendering selection / cursor if we don't have focus
1443        if !cx.window_state.is_focused(&self.id()) {
1444            return;
1445        }
1446
1447        // see if we have a selection range
1448        let has_selection = self.selection.is_some()
1449            || self
1450                .preedit
1451                .as_ref()
1452                .is_some_and(|p| p.cursor.is_some_and(|c| c.0 != c.1));
1453
1454        if has_selection {
1455            self.paint_selection_rect(&node_layout, cx);
1456            // we can skip drawing a cursor and handling blink
1457            return;
1458        }
1459
1460        // see if we should render the cursor
1461        let is_cursor_visible = (self.last_cursor_action_on.elapsed().as_millis()
1462            / CURSOR_BLINK_INTERVAL_MS as u128)
1463            .is_multiple_of(2);
1464
1465        if is_cursor_visible {
1466            let cursor_color = self
1467                .id
1468                .state()
1469                .borrow()
1470                .combined_style
1471                .builtin()
1472                .cursor_color();
1473            let cursor_rect = self.get_cursor_rect(&node_layout);
1474            cx.fill(&cursor_rect, &cursor_color, 0.0);
1475        }
1476
1477        // request paint either way if we're attempting draw a cursor
1478        let id = self.id();
1479        exec_after(Duration::from_millis(CURSOR_BLINK_INTERVAL_MS), move |_| {
1480            id.request_paint();
1481        });
1482    }
1483}
1484
1485#[cfg(test)]
1486mod tests {
1487    use crate::views::text_input::get_dbl_click_selection;
1488
1489    use super::replace_range;
1490
1491    #[test]
1492    fn replace_range_start() {
1493        let mut s = "Sample text".to_owned();
1494        replace_range(&mut s, 0..7, Some("Replaced___"));
1495        assert_eq!("Replaced___text", s);
1496    }
1497
1498    #[test]
1499    fn delete_range_start() {
1500        let mut s = "Sample text".to_owned();
1501        replace_range(&mut s, 0..7, None);
1502        assert_eq!("text", s);
1503    }
1504
1505    #[test]
1506    fn replace_range_end() {
1507        let mut s = "Sample text".to_owned();
1508        let len = s.len();
1509        replace_range(&mut s, 6..len, Some("++Replaced"));
1510        assert_eq!("Sample++Replaced", s);
1511    }
1512
1513    #[test]
1514    fn delete_range_full() {
1515        let mut s = "Sample text".to_owned();
1516        let len = s.len();
1517        replace_range(&mut s, 0..len, None);
1518        assert_eq!("", s);
1519    }
1520
1521    #[test]
1522    fn replace_range_full() {
1523        let mut s = "Sample text".to_owned();
1524        let len = s.len();
1525        replace_range(&mut s, 0..len, Some("Hello world"));
1526        assert_eq!("Hello world", s);
1527    }
1528
1529    #[test]
1530    fn delete_range_end() {
1531        let mut s = "Sample text".to_owned();
1532        let len = s.len();
1533        replace_range(&mut s, 6..len, None);
1534        assert_eq!("Sample", s);
1535    }
1536
1537    #[test]
1538    fn dbl_click_whitespace_before_word() {
1539        let s = "  select  ".to_owned();
1540
1541        let range = get_dbl_click_selection(0, &s);
1542        assert_eq!(range, 0..2);
1543
1544        let range = get_dbl_click_selection(1, &s);
1545        assert_eq!(range, 0..2);
1546    }
1547
1548    #[test]
1549    fn dbl_click_word_surrounded_by_whitespace() {
1550        let s = "  select  ".to_owned();
1551
1552        let range = get_dbl_click_selection(2, &s);
1553        assert_eq!(range, 2..8);
1554
1555        let range = get_dbl_click_selection(6, &s);
1556        assert_eq!(range, 2..8);
1557    }
1558
1559    #[test]
1560    fn dbl_click_whitespace_bween_words() {
1561        let s = "select   select".to_owned();
1562
1563        let range = get_dbl_click_selection(6, &s);
1564        assert_eq!(range, 6..9);
1565
1566        let range = get_dbl_click_selection(7, &s);
1567        assert_eq!(range, 6..9);
1568
1569        let range = get_dbl_click_selection(8, &s);
1570        assert_eq!(range, 6..9);
1571    }
1572
1573    #[test]
1574    fn dbl_click_whitespace_after_word() {
1575        let s = "  select  ".to_owned();
1576
1577        let range = get_dbl_click_selection(8, &s);
1578        assert_eq!(range, 8..10);
1579
1580        let range = get_dbl_click_selection(9, &s);
1581        assert_eq!(range, 8..10);
1582    }
1583
1584    #[test]
1585    fn dbl_click_letter_after_whitespace() {
1586        let s = "     s".to_owned();
1587        let range = get_dbl_click_selection(5, &s);
1588
1589        assert_eq!(range, 5..6);
1590    }
1591
1592    #[test]
1593    fn dbl_click_single_letter() {
1594        let s = "s".to_owned();
1595        let range = get_dbl_click_selection(0, &s);
1596
1597        assert_eq!(range, 0..1);
1598    }
1599
1600    #[test]
1601    fn dbl_click_outside_boundaries_selects_all() {
1602        let s = "     ".to_owned();
1603        let range = get_dbl_click_selection(100, &s);
1604
1605        assert_eq!(range, 0..5);
1606    }
1607
1608    #[test]
1609    fn dbl_click_letters_with_whitespace() {
1610        let s = " s  s  ".to_owned();
1611        let range = get_dbl_click_selection(1, &s);
1612        assert_eq!(range, 1..2);
1613
1614        let range = get_dbl_click_selection(4, &s);
1615        assert_eq!(range, 4..5);
1616    }
1617
1618    #[test]
1619    fn dbl_click_single_word() {
1620        let s = "123testttttttttttttttttttt123".to_owned();
1621        let range = get_dbl_click_selection(1, &s);
1622        let len = s.len();
1623        assert_eq!(range, 0..len);
1624
1625        let range = get_dbl_click_selection(5, &s);
1626        assert_eq!(range, 0..len);
1627
1628        let range = get_dbl_click_selection(len - 1, &s);
1629        assert_eq!(range, 0..len);
1630    }
1631
1632    #[test]
1633    fn dbl_click_two_words_and_whitespace() {
1634        let s = "  word1  word2 ".to_owned();
1635
1636        let range = get_dbl_click_selection(2, &s);
1637        assert_eq!(range, 2..7);
1638
1639        let range = get_dbl_click_selection(6, &s);
1640        assert_eq!(range, 2..7);
1641    }
1642
1643    #[test]
1644    fn dbl_click_empty_string() {
1645        let s = "".to_owned();
1646
1647        let range = get_dbl_click_selection(0, &s);
1648        assert_eq!(range, 0..0);
1649
1650        let range = get_dbl_click_selection(1, &s);
1651        assert_eq!(range, 0..0);
1652    }
1653
1654    #[test]
1655    fn dbl_click_whitespace_only() {
1656        let s = "       ".to_owned();
1657        let range = get_dbl_click_selection(2, &s);
1658
1659        assert_eq!(range, 0..s.len());
1660    }
1661}