Skip to main content

kas_widgets/edit/
editor.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Text editor component
7
8use super::*;
9use kas::event::components::TextInput;
10use kas::event::{FocusSource, ImeSurroundingText, Scroll};
11use kas::geom::Vec2;
12use kas::prelude::*;
13use kas::text::{CursorRange, NotReady, SelectionHelper};
14use kas::theme::{Text, TextClass};
15use kas::util::UndoStack;
16use std::borrow::Cow;
17use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
18
19/// Editor component
20#[autoimpl(Debug)]
21pub struct Editor {
22    // TODO(opt): id, pos are duplicated here since macros don't let us put the core here
23    pub(super) id: Id,
24    pub(super) pos: Coord,
25    pub(super) editable: bool,
26    pub(super) text: Text<String>,
27    pub(super) selection: SelectionHelper,
28    pub(super) edit_x_coord: Option<f32>,
29    last_edit: Option<EditOp>,
30    undo_stack: UndoStack<(String, CursorRange)>,
31    pub(super) has_key_focus: bool,
32    pub(super) current: CurrentAction,
33    error_state: bool,
34    error_message: Option<Cow<'static, str>>,
35    pub(super) input_handler: TextInput,
36}
37
38/// API for use by `EditField`
39impl Editor {
40    /// Construct a default instance (empty string)
41    #[inline]
42    pub(super) fn new() -> Self {
43        Editor {
44            id: Id::default(),
45            pos: Coord::ZERO,
46            editable: true,
47            text: Text::new(String::new(), TextClass::Editor, false),
48            selection: Default::default(),
49            edit_x_coord: None,
50            last_edit: Some(EditOp::Initial),
51            undo_stack: UndoStack::new(),
52            has_key_focus: false,
53            current: CurrentAction::None,
54            error_state: false,
55            error_message: None,
56            input_handler: Default::default(),
57        }
58    }
59
60    /// Construct from a string
61    #[inline]
62    pub(super) fn from<S: ToString>(text: S) -> Self {
63        let text = text.to_string();
64        let len = text.len();
65        Editor {
66            text: Text::new(text, TextClass::Editor, false),
67            selection: SelectionHelper::from(len),
68            ..Editor::new()
69        }
70    }
71
72    /// Cancel on-going selection and IME actions
73    ///
74    /// This should be called if e.g. key-input interrupts the current
75    /// action.
76    pub(super) fn cancel_selection_and_ime(&mut self, cx: &mut EventState) {
77        if self.current == CurrentAction::Selection {
78            self.input_handler.stop_selecting();
79            self.current = CurrentAction::None;
80        } else if self.current.is_ime_enabled() {
81            self.clear_ime();
82            cx.cancel_ime_focus(&self.id);
83        }
84    }
85
86    /// Clean up IME state
87    ///
88    /// One should also call [`EventCx::cancel_ime_focus`] unless this is
89    /// implied.
90    pub(super) fn clear_ime(&mut self) {
91        if self.current.is_ime_enabled() {
92            let action = std::mem::replace(&mut self.current, CurrentAction::None);
93            if let CurrentAction::ImePreedit { edit_range } = action {
94                self.selection.set_cursor(edit_range.start.cast());
95                self.text.replace_range(edit_range.cast(), "");
96            }
97        }
98    }
99
100    pub(super) fn ime_surrounding_text(&self) -> Option<ImeSurroundingText> {
101        const MAX_TEXT_BYTES: usize = ImeSurroundingText::MAX_TEXT_BYTES;
102
103        let sel_range = self.selection.range();
104        let edit_range = match self.current.clone() {
105            CurrentAction::ImePreedit { edit_range } => Some(edit_range.cast()),
106            _ => None,
107        };
108        let mut range = edit_range.clone().unwrap_or(sel_range);
109        let initial_range = range.clone();
110        let edit_len = edit_range.clone().map(|r| r.len()).unwrap_or(0);
111
112        if let Ok(Some((_, line_range))) = self.text.find_line(range.start) {
113            range.start = line_range.start;
114        }
115        if let Ok(Some((_, line_range))) = self.text.find_line(range.end) {
116            range.end = line_range.end;
117        }
118
119        if range.len() - edit_len > MAX_TEXT_BYTES {
120            range.end = range.end.min(initial_range.end + MAX_TEXT_BYTES / 2);
121            while !self.text.as_str().is_char_boundary(range.end) {
122                range.end -= 1;
123            }
124
125            if range.len() - edit_len > MAX_TEXT_BYTES {
126                range.start = range.start.max(initial_range.start - MAX_TEXT_BYTES / 2);
127                while !self.text.as_str().is_char_boundary(range.start) {
128                    range.start += 1;
129                }
130            }
131        }
132
133        let start = range.start;
134        let mut text = String::with_capacity(range.len() - edit_len);
135        if let Some(er) = edit_range {
136            text.push_str(&self.text.as_str()[range.start..er.start]);
137            text.push_str(&self.text.as_str()[er.end..range.end]);
138        } else {
139            text = self.text.as_str()[range].to_string();
140        }
141
142        let cursor = self.selection.edit_index().saturating_sub(start);
143        // Terminology difference: our sel_index is called 'anchor'
144        // SelectionHelper::anchor is not the same thing.
145        let sel_index = self.selection.sel_index().saturating_sub(start);
146        ImeSurroundingText::new(text, cursor, sel_index)
147            .inspect_err(|err| {
148                // TODO: use Display for err not Debug
149                log::warn!("Editor::ime_surrounding_text failed: {err:?}")
150            })
151            .ok()
152    }
153
154    /// Call to set IME position only while IME is active
155    pub(super) fn set_ime_cursor_area(&self, cx: &mut EventState) {
156        if let Ok(text) = self.text.display() {
157            let range = match self.current.clone() {
158                CurrentAction::ImeStart => self.selection.range(),
159                CurrentAction::ImePreedit { edit_range } => edit_range.cast(),
160                _ => return,
161            };
162
163            let (m1, m2);
164            if range.is_empty() {
165                let mut iter = text.text_glyph_pos(range.start);
166                m1 = iter.next();
167                m2 = iter.next();
168            } else {
169                m1 = text.text_glyph_pos(range.start).next_back();
170                m2 = text.text_glyph_pos(range.end).next();
171            }
172
173            let rect = if let Some((c1, c2)) = m1.zip(m2) {
174                let left = c1.pos.0.min(c2.pos.0);
175                let right = c1.pos.0.max(c2.pos.0);
176                let top = (c1.pos.1 - c1.ascent).min(c2.pos.1 - c2.ascent);
177                let bottom = (c1.pos.1 - c1.descent).max(c2.pos.1 - c2.ascent);
178                let p1 = Vec2(left, top).cast_floor();
179                let p2 = Vec2(right, bottom).cast_ceil();
180                Rect::from_coords(p1, p2)
181            } else if let Some(c) = m1.or(m2) {
182                let p1 = Vec2(c.pos.0, c.pos.1 - c.ascent).cast_floor();
183                let p2 = Vec2(c.pos.0, c.pos.1 - c.descent).cast_ceil();
184                Rect::from_coords(p1, p2)
185            } else {
186                return;
187            };
188
189            cx.set_ime_cursor_area(&self.id, rect + Offset::conv(self.pos));
190        }
191    }
192
193    pub(super) fn clear_error(&mut self) {
194        self.error_state = false;
195        self.error_message = None;
196    }
197
198    pub(super) fn tooltip(&self) -> Option<&str> {
199        self.error_message.as_deref()
200    }
201
202    /// Call before an edit to (potentially) commit current state based on last_edit
203    ///
204    /// Call with [`None`] to force commit of any uncommitted changes.
205    pub(super) fn save_undo_state(&mut self, edit: Option<EditOp>) {
206        if let Some(op) = edit
207            && op.try_merge(&mut self.last_edit)
208        {
209            return;
210        }
211
212        self.last_edit = edit;
213        self.undo_stack
214            .try_push((self.clone_string(), self.cursor_range()));
215    }
216
217    /// Prepare text
218    ///
219    /// Updates the view offset (scroll position) if the content size changes or
220    /// `force_set_offset`. Requests redraw and resize as appropriate.
221    pub(super) fn prepare_and_scroll(&mut self, cx: &mut EventCx, force_set_offset: bool) {
222        let bb = self.text.bounding_box();
223        if self.text.prepare() {
224            self.text.ensure_no_left_overhang();
225            cx.redraw();
226        }
227
228        let mut set_offset = force_set_offset;
229        if bb != self.text.bounding_box() {
230            cx.resize();
231            set_offset = true;
232        }
233        if set_offset {
234            self.set_view_offset_from_cursor(cx);
235        }
236    }
237
238    /// Insert `text` at the cursor position
239    ///
240    /// Committing undo state is the responsibility of the caller.
241    pub(super) fn received_text(&mut self, cx: &mut EventCx, text: &str) -> IsUsed {
242        if !self.editable {
243            return Unused;
244        }
245        self.cancel_selection_and_ime(cx);
246
247        let index = self.selection.edit_index();
248        let selection = self.selection.range();
249        let have_sel = selection.start < selection.end;
250        if have_sel {
251            self.text.replace_range(selection.clone(), text);
252            self.selection.set_cursor(selection.start + text.len());
253        } else {
254            self.text.insert_str(index, text);
255            self.selection.set_cursor(index + text.len());
256        }
257        self.edit_x_coord = None;
258
259        self.prepare_and_scroll(cx, false);
260        Used
261    }
262
263    /// Request key focus, if we don't have it or IME
264    pub(super) fn request_key_focus(&self, cx: &mut EventCx, source: FocusSource) {
265        if !self.has_key_focus && !self.current.is_ime_enabled() {
266            cx.request_key_focus(self.id(), source);
267        }
268    }
269
270    pub(super) fn trim_paste(&self, text: &str) -> Range<usize> {
271        let mut end = text.len();
272        if !self.multi_line() {
273            // We cut the content short on control characters and
274            // ignore them (preventing line-breaks and ignoring any
275            // actions such as recursive-paste).
276            for (i, c) in text.char_indices() {
277                if c < '\u{20}' || ('\u{7f}'..='\u{9f}').contains(&c) {
278                    end = i;
279                    break;
280                }
281            }
282        }
283        0..end
284    }
285
286    /// Drive action of a [`Command`]
287    pub(super) fn cmd_action(
288        &mut self,
289        cx: &mut EventCx,
290        cmd: Command,
291    ) -> Result<CmdAction, NotReady> {
292        let editable = self.editable;
293        let mut shift = cx.modifiers().shift_key();
294        let mut buf = [0u8; 4];
295        let cursor = self.selection.edit_index();
296        let len = self.text.str_len();
297        let multi_line = self.multi_line();
298        let selection = self.selection.range();
299        let have_sel = selection.end > selection.start;
300        let string;
301
302        enum Action<'a> {
303            None,
304            Deselect,
305            Activate,
306            Insert(&'a str, EditOp),
307            Delete(Range<usize>, EditOp),
308            Move(usize, Option<f32>),
309            UndoRedo(bool),
310        }
311
312        let action = match cmd {
313            Command::Escape | Command::Deselect if !selection.is_empty() => Action::Deselect,
314            Command::Activate => Action::Activate,
315            Command::Enter if shift || !multi_line => Action::Activate,
316            Command::Enter if editable && multi_line => {
317                Action::Insert('\n'.encode_utf8(&mut buf), EditOp::KeyInput)
318            }
319            // NOTE: we might choose to optionally handle Tab in the future,
320            // but without some workaround it prevents keyboard navigation.
321            // Command::Tab => Action::Insert('\t'.encode_utf8(&mut buf), EditOp::Insert),
322            Command::Left | Command::Home if !shift && have_sel => {
323                Action::Move(selection.start, None)
324            }
325            Command::Left if cursor > 0 => GraphemeCursor::new(cursor, len, true)
326                .prev_boundary(self.text.text(), 0)
327                .unwrap()
328                .map(|index| Action::Move(index, None))
329                .unwrap_or(Action::None),
330            Command::Right | Command::End if !shift && have_sel => {
331                Action::Move(selection.end, None)
332            }
333            Command::Right if cursor < len => GraphemeCursor::new(cursor, len, true)
334                .next_boundary(self.text.text(), 0)
335                .unwrap()
336                .map(|index| Action::Move(index, None))
337                .unwrap_or(Action::None),
338            Command::WordLeft if cursor > 0 => {
339                let mut iter = self.text.text()[0..cursor].split_word_bound_indices();
340                let mut p = iter.next_back().map(|(index, _)| index).unwrap_or(0);
341                while self.text.text()[p..]
342                    .chars()
343                    .next()
344                    .map(|c| c.is_whitespace())
345                    .unwrap_or(false)
346                {
347                    if let Some((index, _)) = iter.next_back() {
348                        p = index;
349                    } else {
350                        break;
351                    }
352                }
353                Action::Move(p, None)
354            }
355            Command::WordRight if cursor < len => {
356                let mut iter = self.text.text()[cursor..]
357                    .split_word_bound_indices()
358                    .skip(1);
359                let mut p = iter.next().map(|(index, _)| cursor + index).unwrap_or(len);
360                while self.text.text()[p..]
361                    .chars()
362                    .next()
363                    .map(|c| c.is_whitespace())
364                    .unwrap_or(false)
365                {
366                    if let Some((index, _)) = iter.next() {
367                        p = cursor + index;
368                    } else {
369                        break;
370                    }
371                }
372                Action::Move(p, None)
373            }
374            // Avoid use of unused navigation keys (e.g. by ScrollComponent):
375            Command::Left | Command::Right | Command::WordLeft | Command::WordRight => Action::None,
376            Command::Up | Command::Down if multi_line => {
377                let x = match self.edit_x_coord {
378                    Some(x) => x,
379                    None => self
380                        .text
381                        .text_glyph_pos(cursor)?
382                        .next_back()
383                        .map(|r| r.pos.0)
384                        .unwrap_or(0.0),
385                };
386                let mut line = self.text.find_line(cursor)?.map(|r| r.0).unwrap_or(0);
387                // We can tolerate invalid line numbers here!
388                line = match cmd {
389                    Command::Up => line.wrapping_sub(1),
390                    Command::Down => line.wrapping_add(1),
391                    _ => unreachable!(),
392                };
393                const HALF: usize = usize::MAX / 2;
394                let nearest_end = match line {
395                    0..=HALF => len,
396                    _ => 0,
397                };
398                self.text
399                    .line_index_nearest(line, x)?
400                    .map(|index| Action::Move(index, Some(x)))
401                    .unwrap_or(Action::Move(nearest_end, None))
402            }
403            Command::Home if cursor > 0 => {
404                let index = self.text.find_line(cursor)?.map(|r| r.1.start).unwrap_or(0);
405                Action::Move(index, None)
406            }
407            Command::End if cursor < len => {
408                let index = self.text.find_line(cursor)?.map(|r| r.1.end).unwrap_or(len);
409                Action::Move(index, None)
410            }
411            Command::DocHome if cursor > 0 => Action::Move(0, None),
412            Command::DocEnd if cursor < len => Action::Move(len, None),
413            // Avoid use of unused navigation keys (e.g. by ScrollComponent):
414            Command::Home | Command::End | Command::DocHome | Command::DocEnd => Action::None,
415            Command::PageUp | Command::PageDown if multi_line => {
416                let mut v = self
417                    .text
418                    .text_glyph_pos(cursor)?
419                    .next_back()
420                    .map(|r| r.pos.into())
421                    .unwrap_or(Vec2::ZERO);
422                if let Some(x) = self.edit_x_coord {
423                    v.0 = x;
424                }
425                const FACTOR: f32 = 2.0 / 3.0;
426                let mut h_dist = f32::conv(self.text.rect().size.1) * FACTOR;
427                if cmd == Command::PageUp {
428                    h_dist *= -1.0;
429                }
430                v.1 += h_dist;
431                Action::Move(self.text.text_index_nearest(v)?, Some(v.0))
432            }
433            Command::Delete | Command::DelBack if editable && have_sel => {
434                Action::Delete(selection.clone(), EditOp::Delete)
435            }
436            Command::Delete if editable => GraphemeCursor::new(cursor, len, true)
437                .next_boundary(self.text.text(), 0)
438                .unwrap()
439                .map(|next| Action::Delete(cursor..next, EditOp::Delete))
440                .unwrap_or(Action::None),
441            Command::DelBack if editable => GraphemeCursor::new(cursor, len, true)
442                .prev_boundary(self.text.text(), 0)
443                .unwrap()
444                .map(|prev| Action::Delete(prev..cursor, EditOp::Delete))
445                .unwrap_or(Action::None),
446            Command::DelWord if editable => {
447                let next = self.text.text()[cursor..]
448                    .split_word_bound_indices()
449                    .nth(1)
450                    .map(|(index, _)| cursor + index)
451                    .unwrap_or(len);
452                Action::Delete(cursor..next, EditOp::Delete)
453            }
454            Command::DelWordBack if editable => {
455                let prev = self.text.text()[0..cursor]
456                    .split_word_bound_indices()
457                    .next_back()
458                    .map(|(index, _)| index)
459                    .unwrap_or(0);
460                Action::Delete(prev..cursor, EditOp::Delete)
461            }
462            Command::SelectAll => {
463                self.selection.set_sel_index(0);
464                shift = true; // hack
465                Action::Move(len, None)
466            }
467            Command::Cut if editable && have_sel => {
468                cx.set_clipboard((self.text.text()[selection.clone()]).into());
469                Action::Delete(selection.clone(), EditOp::Clipboard)
470            }
471            Command::Copy if have_sel => {
472                cx.set_clipboard((self.text.text()[selection.clone()]).into());
473                Action::None
474            }
475            Command::Paste if editable => {
476                if let Some(content) = cx.get_clipboard() {
477                    let range = self.trim_paste(&content);
478                    string = content;
479                    Action::Insert(&string[range], EditOp::Clipboard)
480                } else {
481                    Action::None
482                }
483            }
484            Command::Undo | Command::Redo if editable => Action::UndoRedo(cmd == Command::Redo),
485            _ => return Ok(CmdAction::Unused),
486        };
487
488        // We can receive some commands without key focus as a result of
489        // selection focus. Request focus on edit actions (like Command::Cut).
490        if !matches!(action, Action::None | Action::Deselect) {
491            self.request_key_focus(cx, FocusSource::Synthetic);
492        }
493
494        if !matches!(action, Action::None) {
495            self.cancel_selection_and_ime(cx);
496        }
497
498        let edit_op = match action {
499            Action::None => return Ok(CmdAction::Used),
500            Action::Deselect | Action::Move(_, _) => Some(EditOp::Cursor),
501            Action::Activate | Action::UndoRedo(_) => None,
502            Action::Insert(_, edit) | Action::Delete(_, edit) => Some(edit),
503        };
504        self.save_undo_state(edit_op);
505
506        Ok(match action {
507            Action::None => unreachable!(),
508            Action::Deselect => {
509                self.selection.set_empty();
510                cx.redraw();
511                CmdAction::Cursor
512            }
513            Action::Activate => CmdAction::Activate,
514            Action::Insert(s, _) => {
515                let mut index = cursor;
516                let range = if have_sel {
517                    index = selection.start;
518                    selection.clone()
519                } else {
520                    index..index
521                };
522                self.text.replace_range(range, s);
523                self.selection.set_cursor(index + s.len());
524                self.edit_x_coord = None;
525                CmdAction::Edit
526            }
527            Action::Delete(sel, _) => {
528                self.text.replace_range(sel.clone(), "");
529                self.selection.set_cursor(sel.start);
530                self.edit_x_coord = None;
531                CmdAction::Edit
532            }
533            Action::Move(index, x_coord) => {
534                self.selection.set_edit_index(index);
535                if !shift {
536                    self.selection.set_empty();
537                } else {
538                    self.set_primary(cx);
539                }
540                self.edit_x_coord = x_coord;
541                cx.redraw();
542                CmdAction::Cursor
543            }
544            Action::UndoRedo(redo) => {
545                if let Some((text, cursor)) = self.undo_stack.undo_or_redo(redo) {
546                    if self.text.set_str(text) {
547                        self.edit_x_coord = None;
548                    }
549                    self.selection = (*cursor).into();
550                    CmdAction::Edit
551                } else {
552                    CmdAction::Used
553                }
554            }
555        })
556    }
557
558    /// Set cursor position. It is assumed that the text has not changed.
559    ///
560    /// Committing undo state is the responsibility of the caller.
561    pub(super) fn set_cursor_from_coord(&mut self, cx: &mut EventCx, coord: Coord) {
562        let rel_pos = (coord - self.pos).cast();
563        if let Ok(index) = self.text.text_index_nearest(rel_pos) {
564            if index != self.selection.edit_index() {
565                self.selection.set_edit_index(index);
566                self.set_view_offset_from_cursor(cx);
567                self.edit_x_coord = None;
568                cx.redraw();
569            }
570        }
571    }
572
573    /// Set primary clipboard (mouse buffer) contents from selection
574    pub(super) fn set_primary(&self, cx: &mut EventCx) {
575        if self.has_key_focus && !self.selection.is_empty() && cx.has_primary() {
576            let range = self.selection.range();
577            cx.set_primary(String::from(&self.text.as_str()[range]));
578        }
579    }
580
581    /// Update view_offset after the cursor index changes
582    ///
583    /// It is assumed that the text has not changed.
584    ///
585    /// A redraw is assumed since the cursor moved.
586    pub(super) fn set_view_offset_from_cursor(&mut self, cx: &mut EventCx) {
587        let cursor = self.selection.edit_index();
588        if let Some(marker) = self
589            .text
590            .text_glyph_pos(cursor)
591            .ok()
592            .and_then(|mut m| m.next_back())
593        {
594            let y0 = (marker.pos.1 - marker.ascent).cast_floor();
595            let pos = self.pos + Offset(marker.pos.0.cast_nearest(), y0);
596            let size = Size(0, i32::conv_ceil(marker.pos.1 - marker.descent) - y0);
597            cx.set_scroll(Scroll::Rect(Rect { pos, size }));
598        }
599    }
600}
601
602/// API for use by `EditGuard` implementations
603impl Editor {
604    /// Get a reference to the widget's identifier
605    #[inline]
606    pub fn id_ref(&self) -> &Id {
607        &self.id
608    }
609
610    /// Get the widget's identifier
611    #[inline]
612    pub fn id(&self) -> Id {
613        self.id.clone()
614    }
615
616    /// Get text contents
617    #[inline]
618    pub fn as_str(&self) -> &str {
619        self.text.as_str()
620    }
621
622    /// Get the text contents as a `String`
623    #[inline]
624    pub fn clone_string(&self) -> String {
625        self.text.clone_string()
626    }
627
628    /// Commit outstanding changes to the undo history
629    ///
630    /// Call this *before* changing the text with `set_str` or `set_string`
631    /// to commit changes to the undo history.
632    #[inline]
633    pub fn pre_commit(&mut self) {
634        self.save_undo_state(Some(EditOp::Synthetic));
635    }
636
637    /// Clear text contents and undo history
638    ///
639    /// This method does not call any [`EditGuard`] actions; consider also
640    /// calling [`EditField::call_guard_edit`].
641    #[inline]
642    pub fn clear(&mut self, cx: &mut EventState) {
643        self.last_edit = Some(EditOp::Initial);
644        self.undo_stack.clear();
645        self.set_string(cx, String::new());
646    }
647
648    /// Set text contents from a `str`
649    ///
650    /// This does not interact with undo history; see also [`Self::clear`],
651    /// [`Self::pre_commit`].
652    ///
653    /// This method does not call any [`EditGuard`] actions; consider also
654    /// calling [`EditField::call_guard_edit`].
655    ///
656    /// Returns `true` if the text may have changed.
657    #[inline]
658    pub fn set_str(&mut self, cx: &mut EventState, text: &str) -> bool {
659        if self.text.as_str() != text {
660            self.set_string(cx, text.to_string());
661            true
662        } else {
663            false
664        }
665    }
666
667    /// Set text contents from a `String`
668    ///
669    /// This does not interact with undo history or call action handlers on the
670    /// guard.
671    ///
672    /// This method clears the error state but does not call any [`EditGuard`]
673    /// actions; consider also calling [`EditField::call_guard_edit`].
674    ///
675    /// Returns `true` if the text is ready and may have changed.
676    pub fn set_string(&mut self, cx: &mut EventState, string: String) -> bool {
677        self.cancel_selection_and_ime(cx);
678
679        if !self.text.set_string(string) {
680            return false;
681        }
682
683        let len = self.text.str_len();
684        self.selection.set_max_len(len);
685        self.edit_x_coord = None;
686        self.clear_error();
687        self.text.prepare()
688    }
689
690    /// Replace selected text
691    ///
692    /// This does not interact with undo history or call action handlers on the
693    /// guard.
694    ///
695    /// This method clears the error state but does not call any [`EditGuard`]
696    /// actions; consider also calling [`EditField::call_guard_edit`].
697    ///
698    /// Returns `true` if the text is ready and may have changed.
699    #[inline]
700    pub fn replace_selected_text(&mut self, cx: &mut EventState, text: &str) -> bool {
701        self.cancel_selection_and_ime(cx);
702
703        let index = self.selection.edit_index();
704        let selection = self.selection.range();
705        let have_sel = selection.start < selection.end;
706        if have_sel {
707            self.text.replace_range(selection.clone(), text);
708            self.selection.set_cursor(selection.start + text.len());
709        } else {
710            self.text.insert_str(index, text);
711            self.selection.set_cursor(index + text.len());
712        }
713        self.edit_x_coord = None;
714        self.clear_error();
715        self.text.prepare()
716    }
717
718    /// Access the cursor index / selection range
719    #[inline]
720    pub fn cursor_range(&self) -> CursorRange {
721        *self.selection
722    }
723
724    /// Set the cursor index / range
725    ///
726    /// This does not interact with undo history or call action handlers on the
727    /// guard.
728    #[inline]
729    pub fn set_cursor_range(&mut self, range: impl Into<CursorRange>) {
730        self.edit_x_coord = None;
731        self.selection = range.into().into();
732    }
733
734    /// Get whether this `EditField` is editable
735    #[inline]
736    pub fn is_editable(&self) -> bool {
737        self.editable
738    }
739
740    /// Set whether this `EditField` is editable
741    #[inline]
742    pub fn set_editable(&mut self, editable: bool) {
743        self.editable = editable;
744    }
745
746    /// True if the editor uses multi-line mode
747    #[inline]
748    pub fn multi_line(&self) -> bool {
749        self.text.wrap()
750    }
751
752    /// Get the text class used
753    #[inline]
754    pub fn class(&self) -> TextClass {
755        self.text.class()
756    }
757
758    /// Get whether the widget has input focus
759    ///
760    /// This is true when the widget is has keyboard or IME focus.
761    #[inline]
762    pub fn has_input_focus(&self) -> bool {
763        self.has_key_focus || self.current.is_ime_enabled()
764    }
765
766    /// Get whether the input state is erroneous
767    #[inline]
768    pub fn has_error(&self) -> bool {
769        self.error_state
770    }
771
772    /// Mark the input as erroneous with an optional message
773    ///
774    /// This state should be set from [`EditGuard::edit`] when appropriate. The
775    /// state is cleared immediately before calling [`EditGuard::edit`] and also
776    /// in case a text is directly assigned (e.g. using [`Self::set_string`]).
777    ///
778    /// When set, the input field's background is drawn red. If a message is
779    /// supplied, then a tooltip will be available on mouse-hover.
780    pub fn set_error(&mut self, cx: &mut EventState, message: Option<Cow<'static, str>>) {
781        self.error_state = true;
782        self.error_message = message;
783        cx.redraw(&self.id);
784    }
785}