egui/widgets/text_edit/
builder.rs

1use std::sync::Arc;
2
3use epaint::text::{cursor::*, Galley, LayoutJob};
4
5use crate::{
6    os::OperatingSystem,
7    output::OutputEvent,
8    text_selection::{
9        text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange, CursorRange,
10    },
11    *,
12};
13
14use super::{TextEditOutput, TextEditState};
15
16/// A text region that the user can edit the contents of.
17///
18/// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`].
19///
20/// Example:
21///
22/// ```
23/// # egui::__run_test_ui(|ui| {
24/// # let mut my_string = String::new();
25/// let response = ui.add(egui::TextEdit::singleline(&mut my_string));
26/// if response.changed() {
27///     // …
28/// }
29/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
30///     // …
31/// }
32/// # });
33/// ```
34///
35/// To fill an [`Ui`] with a [`TextEdit`] use [`Ui::add_sized`]:
36///
37/// ```
38/// # egui::__run_test_ui(|ui| {
39/// # let mut my_string = String::new();
40/// ui.add_sized(ui.available_size(), egui::TextEdit::multiline(&mut my_string));
41/// # });
42/// ```
43///
44///
45/// You can also use [`TextEdit`] to show text that can be selected, but not edited.
46/// To do so, pass in a `&mut` reference to a `&str`, for instance:
47///
48/// ```
49/// fn selectable_text(ui: &mut egui::Ui, mut text: &str) {
50///     ui.add(egui::TextEdit::multiline(&mut text));
51/// }
52/// ```
53///
54/// ## Advanced usage
55/// See [`TextEdit::show`].
56///
57/// ## Other
58/// The background color of a [`TextEdit`] is [`Visuals::extreme_bg_color`].
59#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
60pub struct TextEdit<'t> {
61    text: &'t mut dyn TextBuffer,
62    hint_text: WidgetText,
63    hint_text_font: Option<FontSelection>,
64    id: Option<Id>,
65    id_source: Option<Id>,
66    font_selection: FontSelection,
67    text_color: Option<Color32>,
68    layouter: Option<&'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>>,
69    password: bool,
70    frame: bool,
71    margin: Margin,
72    multiline: bool,
73    interactive: bool,
74    desired_width: Option<f32>,
75    desired_height_rows: usize,
76    event_filter: EventFilter,
77    cursor_at_end: bool,
78    min_size: Vec2,
79    align: Align2,
80    clip_text: bool,
81    char_limit: usize,
82    return_key: Option<KeyboardShortcut>,
83}
84
85impl<'t> WidgetWithState for TextEdit<'t> {
86    type State = TextEditState;
87}
88
89impl<'t> TextEdit<'t> {
90    pub fn load_state(ctx: &Context, id: Id) -> Option<TextEditState> {
91        TextEditState::load(ctx, id)
92    }
93
94    pub fn store_state(ctx: &Context, id: Id, state: TextEditState) {
95        state.store(ctx, id);
96    }
97}
98
99impl<'t> TextEdit<'t> {
100    /// No newlines (`\n`) allowed. Pressing enter key will result in the [`TextEdit`] losing focus (`response.lost_focus`).
101    pub fn singleline(text: &'t mut dyn TextBuffer) -> Self {
102        Self {
103            desired_height_rows: 1,
104            multiline: false,
105            clip_text: true,
106            ..Self::multiline(text)
107        }
108    }
109
110    /// A [`TextEdit`] for multiple lines. Pressing enter key will create a new line by default (can be changed with [`return_key`](TextEdit::return_key)).
111    pub fn multiline(text: &'t mut dyn TextBuffer) -> Self {
112        Self {
113            text,
114            hint_text: Default::default(),
115            hint_text_font: None,
116            id: None,
117            id_source: None,
118            font_selection: Default::default(),
119            text_color: None,
120            layouter: None,
121            password: false,
122            frame: true,
123            margin: Margin::symmetric(4.0, 2.0),
124            multiline: true,
125            interactive: true,
126            desired_width: None,
127            desired_height_rows: 4,
128            event_filter: EventFilter {
129                // moving the cursor is really important
130                horizontal_arrows: true,
131                vertical_arrows: true,
132                tab: false, // tab is used to change focus, not to insert a tab character
133                ..Default::default()
134            },
135            cursor_at_end: true,
136            min_size: Vec2::ZERO,
137            align: Align2::LEFT_TOP,
138            clip_text: false,
139            char_limit: usize::MAX,
140            return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)),
141        }
142    }
143
144    /// Build a [`TextEdit`] focused on code editing.
145    /// By default it comes with:
146    /// - monospaced font
147    /// - focus lock (tab will insert a tab character instead of moving focus)
148    pub fn code_editor(self) -> Self {
149        self.font(TextStyle::Monospace).lock_focus(true)
150    }
151
152    /// Use if you want to set an explicit [`Id`] for this widget.
153    #[inline]
154    pub fn id(mut self, id: Id) -> Self {
155        self.id = Some(id);
156        self
157    }
158
159    /// A source for the unique [`Id`], e.g. `.id_source("second_text_edit_field")` or `.id_source(loop_index)`.
160    #[inline]
161    pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self {
162        self.id_source = Some(Id::new(id_source));
163        self
164    }
165
166    /// Show a faint hint text when the text field is empty.
167    ///
168    /// If the hint text needs to be persisted even when the text field has input,
169    /// the following workaround can be used:
170    /// ```
171    /// # egui::__run_test_ui(|ui| {
172    /// # let mut my_string = String::new();
173    /// # use egui::{ Color32, FontId };
174    /// let text_edit = egui::TextEdit::multiline(&mut my_string)
175    ///     .desired_width(f32::INFINITY);
176    /// let output = text_edit.show(ui);
177    /// let painter = ui.painter_at(output.response.rect);
178    /// let text_color = Color32::from_rgba_premultiplied(100, 100, 100, 100);
179    /// let galley = painter.layout(
180    ///     String::from("Enter text"),
181    ///     FontId::default(),
182    ///     text_color,
183    ///     f32::INFINITY
184    /// );
185    /// painter.galley(output.galley_pos, galley, text_color);
186    /// # });
187    /// ```
188    #[inline]
189    pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
190        self.hint_text = hint_text.into();
191        self
192    }
193
194    /// Set a specific style for the hint text.
195    #[inline]
196    pub fn hint_text_font(mut self, hint_text_font: impl Into<FontSelection>) -> Self {
197        self.hint_text_font = Some(hint_text_font.into());
198        self
199    }
200
201    /// If true, hide the letters from view and prevent copying from the field.
202    #[inline]
203    pub fn password(mut self, password: bool) -> Self {
204        self.password = password;
205        self
206    }
207
208    /// Pick a [`FontId`] or [`TextStyle`].
209    #[inline]
210    pub fn font(mut self, font_selection: impl Into<FontSelection>) -> Self {
211        self.font_selection = font_selection.into();
212        self
213    }
214
215    #[inline]
216    pub fn text_color(mut self, text_color: Color32) -> Self {
217        self.text_color = Some(text_color);
218        self
219    }
220
221    #[inline]
222    pub fn text_color_opt(mut self, text_color: Option<Color32>) -> Self {
223        self.text_color = text_color;
224        self
225    }
226
227    /// Override how text is being shown inside the [`TextEdit`].
228    ///
229    /// This can be used to implement things like syntax highlighting.
230    ///
231    /// This function will be called at least once per frame,
232    /// so it is strongly suggested that you cache the results of any syntax highlighter
233    /// so as not to waste CPU highlighting the same string every frame.
234    ///
235    /// The arguments is the enclosing [`Ui`] (so you can access e.g. [`Ui::fonts`]),
236    /// the text and the wrap width.
237    ///
238    /// ```
239    /// # egui::__run_test_ui(|ui| {
240    /// # let mut my_code = String::new();
241    /// # fn my_memoized_highlighter(s: &str) -> egui::text::LayoutJob { Default::default() }
242    /// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
243    ///     let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string);
244    ///     layout_job.wrap.max_width = wrap_width;
245    ///     ui.fonts(|f| f.layout_job(layout_job))
246    /// };
247    /// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
248    /// # });
249    /// ```
250    #[inline]
251    pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>) -> Self {
252        self.layouter = Some(layouter);
253
254        self
255    }
256
257    /// Default is `true`. If set to `false` then you cannot interact with the text (neither edit or select it).
258    ///
259    /// Consider using [`Ui::add_enabled`] instead to also give the [`TextEdit`] a greyed out look.
260    #[inline]
261    pub fn interactive(mut self, interactive: bool) -> Self {
262        self.interactive = interactive;
263        self
264    }
265
266    /// Default is `true`. If set to `false` there will be no frame showing that this is editable text!
267    #[inline]
268    pub fn frame(mut self, frame: bool) -> Self {
269        self.frame = frame;
270        self
271    }
272
273    /// Set margin of text. Default is `Margin::symmetric(4.0, 2.0)`
274    #[inline]
275    pub fn margin(mut self, margin: impl Into<Margin>) -> Self {
276        self.margin = margin.into();
277        self
278    }
279
280    /// Set to 0.0 to keep as small as possible.
281    /// Set to [`f32::INFINITY`] to take up all available space (i.e. disable automatic word wrap).
282    #[inline]
283    pub fn desired_width(mut self, desired_width: f32) -> Self {
284        self.desired_width = Some(desired_width);
285        self
286    }
287
288    /// Set the number of rows to show by default.
289    /// The default for singleline text is `1`.
290    /// The default for multiline text is `4`.
291    #[inline]
292    pub fn desired_rows(mut self, desired_height_rows: usize) -> Self {
293        self.desired_height_rows = desired_height_rows;
294        self
295    }
296
297    /// When `false` (default), pressing TAB will move focus
298    /// to the next widget.
299    ///
300    /// When `true`, the widget will keep the focus and pressing TAB
301    /// will insert the `'\t'` character.
302    #[inline]
303    pub fn lock_focus(mut self, tab_will_indent: bool) -> Self {
304        self.event_filter.tab = tab_will_indent;
305        self
306    }
307
308    /// When `true` (default), the cursor will initially be placed at the end of the text.
309    ///
310    /// When `false`, the cursor will initially be placed at the beginning of the text.
311    #[inline]
312    pub fn cursor_at_end(mut self, b: bool) -> Self {
313        self.cursor_at_end = b;
314        self
315    }
316
317    /// When `true` (default), overflowing text will be clipped.
318    ///
319    /// When `false`, widget width will expand to make all text visible.
320    ///
321    /// This only works for singleline [`TextEdit`].
322    #[inline]
323    pub fn clip_text(mut self, b: bool) -> Self {
324        // always show everything in multiline
325        if !self.multiline {
326            self.clip_text = b;
327        }
328        self
329    }
330
331    /// Sets the limit for the amount of characters can be entered
332    ///
333    /// This only works for singleline [`TextEdit`]
334    #[inline]
335    pub fn char_limit(mut self, limit: usize) -> Self {
336        self.char_limit = limit;
337        self
338    }
339
340    /// Set the horizontal align of the inner text.
341    #[inline]
342    pub fn horizontal_align(mut self, align: Align) -> Self {
343        self.align.0[0] = align;
344        self
345    }
346
347    /// Set the vertical align of the inner text.
348    #[inline]
349    pub fn vertical_align(mut self, align: Align) -> Self {
350        self.align.0[1] = align;
351        self
352    }
353
354    /// Set the minimum size of the [`TextEdit`].
355    #[inline]
356    pub fn min_size(mut self, min_size: Vec2) -> Self {
357        self.min_size = min_size;
358        self
359    }
360
361    /// Set the return key combination.
362    ///
363    /// This combination will cause a newline on multiline,
364    /// whereas on singleline it will cause the widget to lose focus.
365    ///
366    /// This combination is optional and can be disabled by passing [`None`] into this function.
367    #[inline]
368    pub fn return_key(mut self, return_key: impl Into<Option<KeyboardShortcut>>) -> Self {
369        self.return_key = return_key.into();
370        self
371    }
372}
373
374// ----------------------------------------------------------------------------
375
376impl<'t> Widget for TextEdit<'t> {
377    fn ui(self, ui: &mut Ui) -> Response {
378        self.show(ui).response
379    }
380}
381
382impl<'t> TextEdit<'t> {
383    /// Show the [`TextEdit`], returning a rich [`TextEditOutput`].
384    ///
385    /// ```
386    /// # egui::__run_test_ui(|ui| {
387    /// # let mut my_string = String::new();
388    /// let output = egui::TextEdit::singleline(&mut my_string).show(ui);
389    /// if let Some(text_cursor_range) = output.cursor_range {
390    ///     use egui::TextBuffer as _;
391    ///     let selected_chars = text_cursor_range.as_sorted_char_range();
392    ///     let selected_text = my_string.char_range(selected_chars);
393    ///     ui.label("Selected text: ");
394    ///     ui.monospace(selected_text);
395    /// }
396    /// # });
397    /// ```
398    pub fn show(self, ui: &mut Ui) -> TextEditOutput {
399        let is_mutable = self.text.is_mutable();
400        let frame = self.frame;
401        let where_to_put_background = ui.painter().add(Shape::Noop);
402
403        let margin = self.margin;
404        let mut output = self.show_content(ui);
405
406        // TODO(emilk): return full outer_rect in `TextEditOutput`.
407        // Can't do it now because this fix is ging into a patch release.
408        let outer_rect = output.response.rect;
409        let inner_rect = outer_rect - margin;
410        output.response.rect = inner_rect;
411
412        if frame {
413            let visuals = ui.style().interact(&output.response);
414            let frame_rect = outer_rect.expand(visuals.expansion);
415            let shape = if is_mutable {
416                if output.response.has_focus() {
417                    epaint::RectShape::new(
418                        frame_rect,
419                        visuals.rounding,
420                        ui.visuals().extreme_bg_color,
421                        ui.visuals().selection.stroke,
422                    )
423                } else {
424                    epaint::RectShape::new(
425                        frame_rect,
426                        visuals.rounding,
427                        ui.visuals().extreme_bg_color,
428                        visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
429                    )
430                }
431            } else {
432                let visuals = &ui.style().visuals.widgets.inactive;
433                epaint::RectShape::stroke(
434                    frame_rect,
435                    visuals.rounding,
436                    visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
437                )
438            };
439
440            ui.painter().set(where_to_put_background, shape);
441        }
442
443        output
444    }
445
446    fn show_content(self, ui: &mut Ui) -> TextEditOutput {
447        let TextEdit {
448            text,
449            hint_text,
450            hint_text_font,
451            id,
452            id_source,
453            font_selection,
454            text_color,
455            layouter,
456            password,
457            frame: _,
458            margin,
459            multiline,
460            interactive,
461            desired_width,
462            desired_height_rows,
463            event_filter,
464            cursor_at_end,
465            min_size,
466            align,
467            clip_text,
468            char_limit,
469            return_key,
470        } = self;
471
472        let text_color = text_color
473            .or(ui.visuals().override_text_color)
474            // .unwrap_or_else(|| ui.style().interact(&response).text_color()); // too bright
475            .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
476
477        let prev_text = text.as_str().to_owned();
478
479        let font_id = font_selection.resolve(ui.style());
480        let row_height = ui.fonts(|f| f.row_height(&font_id));
481        const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
482        let available_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH);
483        let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
484        let wrap_width = if ui.layout().horizontal_justify() {
485            available_width
486        } else {
487            desired_width.min(available_width)
488        };
489
490        let font_id_clone = font_id.clone();
491        let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| {
492            let text = mask_if_password(password, text);
493            let layout_job = if multiline {
494                LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
495            } else {
496                LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
497            };
498            ui.fonts(|f| f.layout_job(layout_job))
499        };
500
501        let layouter = layouter.unwrap_or(&mut default_layouter);
502
503        let mut galley = layouter(ui, text.as_str(), wrap_width);
504
505        let desired_width = if clip_text {
506            wrap_width // visual clipping with scroll in singleline input.
507        } else {
508            galley.size().x.max(wrap_width)
509        };
510        let desired_height = (desired_height_rows.at_least(1) as f32) * row_height;
511        let desired_inner_size = vec2(desired_width, galley.size().y.max(desired_height));
512        let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size);
513        let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size);
514        let rect = outer_rect - margin; // inner rect (excluding frame/margin).
515
516        let id = id.unwrap_or_else(|| {
517            if let Some(id_source) = id_source {
518                ui.make_persistent_id(id_source)
519            } else {
520                auto_id // Since we are only storing the cursor a persistent Id is not super important
521            }
522        });
523        let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
524
525        // On touch screens (e.g. mobile in `eframe` web), should
526        // dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
527        // Since currently copying selected text in not supported on `eframe` web,
528        // we prioritize touch-scrolling:
529        let allow_drag_to_select =
530            ui.input(|i| !i.has_touch_screen()) || ui.memory(|mem| mem.has_focus(id));
531
532        let sense = if interactive {
533            if allow_drag_to_select {
534                Sense::click_and_drag()
535            } else {
536                Sense::click()
537            }
538        } else {
539            Sense::hover()
540        };
541        let mut response = ui.interact(outer_rect, id, sense);
542
543        response.fake_primary_click = false; // Don't sent `OutputEvent::Clicked` when a user presses the space bar
544
545        let text_clip_rect = rect;
546        let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor
547
548        if interactive {
549            if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
550                if response.hovered() && text.is_mutable() {
551                    ui.output_mut(|o| o.mutable_text_under_cursor = true);
552                }
553
554                // TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
555
556                let singleline_offset = vec2(state.singleline_offset, 0.0);
557                let cursor_at_pointer =
558                    galley.cursor_from_pos(pointer_pos - rect.min + singleline_offset);
559
560                if ui.visuals().text_cursor.preview
561                    && response.hovered()
562                    && ui.input(|i| i.pointer.is_moving())
563                {
564                    // text cursor preview:
565                    let cursor_rect =
566                        cursor_rect(rect.min, &galley, &cursor_at_pointer, row_height);
567                    text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect);
568                }
569
570                let is_being_dragged = ui.ctx().is_being_dragged(response.id);
571                let did_interact = state.cursor.pointer_interaction(
572                    ui,
573                    &response,
574                    cursor_at_pointer,
575                    &galley,
576                    is_being_dragged,
577                );
578
579                if did_interact {
580                    ui.memory_mut(|mem| mem.request_focus(response.id));
581                }
582            }
583        }
584
585        if interactive && response.hovered() {
586            ui.ctx().set_cursor_icon(CursorIcon::Text);
587        }
588
589        let mut cursor_range = None;
590        let prev_cursor_range = state.cursor.range(&galley);
591        if interactive && ui.memory(|mem| mem.has_focus(id)) {
592            ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
593
594            let default_cursor_range = if cursor_at_end {
595                CursorRange::one(galley.end())
596            } else {
597                CursorRange::default()
598            };
599
600            let (changed, new_cursor_range) = events(
601                ui,
602                &mut state,
603                text,
604                &mut galley,
605                layouter,
606                id,
607                wrap_width,
608                multiline,
609                password,
610                default_cursor_range,
611                char_limit,
612                event_filter,
613                return_key,
614            );
615
616            if changed {
617                response.mark_changed();
618            }
619            cursor_range = Some(new_cursor_range);
620        }
621
622        let mut galley_pos = align
623            .align_size_within_rect(galley.size(), rect)
624            .intersect(rect) // limit pos to the response rect area
625            .min;
626        let align_offset = rect.left() - galley_pos.x;
627
628        // Visual clipping for singleline text editor with text larger than width
629        if clip_text && align_offset == 0.0 {
630            let cursor_pos = match (cursor_range, ui.memory(|mem| mem.has_focus(id))) {
631                (Some(cursor_range), true) => galley.pos_from_cursor(&cursor_range.primary).min.x,
632                _ => 0.0,
633            };
634
635            let mut offset_x = state.singleline_offset;
636            let visible_range = offset_x..=offset_x + desired_inner_size.x;
637
638            if !visible_range.contains(&cursor_pos) {
639                if cursor_pos < *visible_range.start() {
640                    offset_x = cursor_pos;
641                } else {
642                    offset_x = cursor_pos - desired_inner_size.x;
643                }
644            }
645
646            offset_x = offset_x
647                .at_most(galley.size().x - desired_inner_size.x)
648                .at_least(0.0);
649
650            state.singleline_offset = offset_x;
651            galley_pos -= vec2(offset_x, 0.0);
652        } else {
653            state.singleline_offset = align_offset;
654        }
655
656        let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) =
657            (cursor_range, prev_cursor_range)
658        {
659            prev_cursor_range.as_ccursor_range() != cursor_range.as_ccursor_range()
660        } else {
661            false
662        };
663
664        if ui.is_rect_visible(rect) {
665            painter.galley(galley_pos, galley.clone(), text_color);
666
667            if text.as_str().is_empty() && !hint_text.is_empty() {
668                let hint_text_color = ui.visuals().weak_text_color();
669                let hint_text_font_id = hint_text_font.unwrap_or(font_id.into());
670                let galley = if multiline {
671                    hint_text.into_galley(
672                        ui,
673                        Some(TextWrapMode::Wrap),
674                        desired_inner_size.x,
675                        hint_text_font_id,
676                    )
677                } else {
678                    hint_text.into_galley(
679                        ui,
680                        Some(TextWrapMode::Extend),
681                        f32::INFINITY,
682                        hint_text_font_id,
683                    )
684                };
685                painter.galley(rect.min, galley, hint_text_color);
686            }
687
688            if ui.memory(|mem| mem.has_focus(id)) {
689                if let Some(cursor_range) = state.cursor.range(&galley) {
690                    // We paint the cursor on top of the text, in case
691                    // the text galley has backgrounds (as e.g. `code` snippets in markup do).
692                    paint_text_selection(
693                        &painter,
694                        ui.visuals(),
695                        galley_pos,
696                        &galley,
697                        &cursor_range,
698                        None,
699                    );
700
701                    let primary_cursor_rect =
702                        cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height);
703
704                    let is_fully_visible = ui.clip_rect().contains_rect(rect); // TODO(emilk): remove this HACK workaround for https://github.com/emilk/egui/issues/1531
705                    if (response.changed || selection_changed) && !is_fully_visible {
706                        // Scroll to keep primary cursor in view:
707                        ui.scroll_to_rect(primary_cursor_rect, None);
708                    }
709
710                    if text.is_mutable() && interactive {
711                        let now = ui.ctx().input(|i| i.time);
712                        if response.changed || selection_changed {
713                            state.last_edit_time = now;
714                        }
715
716                        // Only show (and blink) cursor if the egui viewport has focus.
717                        // This is for two reasons:
718                        // * Don't give the impression that the user can type into a window without focus
719                        // * Don't repaint the ui because of a blinking cursor in an app that is not in focus
720                        if ui.ctx().input(|i| i.focused) {
721                            text_selection::visuals::paint_text_cursor(
722                                ui,
723                                &painter,
724                                primary_cursor_rect,
725                                now - state.last_edit_time,
726                            );
727                        }
728
729                        // Set IME output (in screen coords) when text is editable and visible
730                        let transform = ui
731                            .memory(|m| m.layer_transforms.get(&ui.layer_id()).copied())
732                            .unwrap_or_default();
733
734                        ui.ctx().output_mut(|o| {
735                            o.ime = Some(crate::output::IMEOutput {
736                                rect: transform * rect,
737                                cursor_rect: transform * primary_cursor_rect,
738                            });
739                        });
740                    }
741                }
742            }
743        }
744
745        state.clone().store(ui.ctx(), id);
746
747        if response.changed {
748            response.widget_info(|| {
749                WidgetInfo::text_edit(
750                    ui.is_enabled(),
751                    mask_if_password(password, prev_text.as_str()),
752                    mask_if_password(password, text.as_str()),
753                )
754            });
755        } else if selection_changed {
756            let cursor_range = cursor_range.unwrap();
757            let char_range =
758                cursor_range.primary.ccursor.index..=cursor_range.secondary.ccursor.index;
759            let info = WidgetInfo::text_selection_changed(
760                ui.is_enabled(),
761                char_range,
762                mask_if_password(password, text.as_str()),
763            );
764            response.output_event(OutputEvent::TextSelectionChanged(info));
765        } else {
766            response.widget_info(|| {
767                WidgetInfo::text_edit(
768                    ui.is_enabled(),
769                    mask_if_password(password, prev_text.as_str()),
770                    mask_if_password(password, text.as_str()),
771                )
772            });
773        }
774
775        #[cfg(feature = "accesskit")]
776        {
777            let role = if password {
778                accesskit::Role::PasswordInput
779            } else if multiline {
780                accesskit::Role::MultilineTextInput
781            } else {
782                accesskit::Role::TextInput
783            };
784
785            crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
786                ui.ctx(),
787                id,
788                cursor_range,
789                role,
790                galley_pos,
791                &galley,
792            );
793        }
794
795        TextEditOutput {
796            response,
797            galley,
798            galley_pos,
799            text_clip_rect,
800            state,
801            cursor_range,
802        }
803    }
804}
805
806fn mask_if_password(is_password: bool, text: &str) -> String {
807    fn mask_password(text: &str) -> String {
808        std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR)
809            .take(text.chars().count())
810            .collect::<String>()
811    }
812
813    if is_password {
814        mask_password(text)
815    } else {
816        text.to_owned()
817    }
818}
819
820// ----------------------------------------------------------------------------
821
822/// Check for (keyboard) events to edit the cursor and/or text.
823#[allow(clippy::too_many_arguments)]
824fn events(
825    ui: &crate::Ui,
826    state: &mut TextEditState,
827    text: &mut dyn TextBuffer,
828    galley: &mut Arc<Galley>,
829    layouter: &mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>,
830    id: Id,
831    wrap_width: f32,
832    multiline: bool,
833    password: bool,
834    default_cursor_range: CursorRange,
835    char_limit: usize,
836    event_filter: EventFilter,
837    return_key: Option<KeyboardShortcut>,
838) -> (bool, CursorRange) {
839    let os = ui.ctx().os();
840
841    let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
842
843    // We feed state to the undoer both before and after handling input
844    // so that the undoer creates automatic saves even when there are no events for a while.
845    state.undoer.lock().feed_state(
846        ui.input(|i| i.time),
847        &(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
848    );
849
850    let copy_if_not_password = |ui: &Ui, text: String| {
851        if !password {
852            ui.ctx().copy_text(text);
853        }
854    };
855
856    let mut any_change = false;
857
858    let events = ui.input(|i| i.filtered_events(&event_filter));
859    for event in &events {
860        let did_mutate_text = match event {
861            // First handle events that only changes the selection cursor, not the text:
862            event if cursor_range.on_event(os, event, galley, id) => None,
863
864            Event::Copy => {
865                if cursor_range.is_empty() {
866                    copy_if_not_password(ui, text.as_str().to_owned());
867                } else {
868                    copy_if_not_password(ui, cursor_range.slice_str(text.as_str()).to_owned());
869                }
870                None
871            }
872            Event::Cut => {
873                if cursor_range.is_empty() {
874                    copy_if_not_password(ui, text.take());
875                    Some(CCursorRange::default())
876                } else {
877                    copy_if_not_password(ui, cursor_range.slice_str(text.as_str()).to_owned());
878                    Some(CCursorRange::one(text.delete_selected(&cursor_range)))
879                }
880            }
881            Event::Paste(text_to_insert) => {
882                if !text_to_insert.is_empty() {
883                    let mut ccursor = text.delete_selected(&cursor_range);
884
885                    text.insert_text_at(&mut ccursor, text_to_insert, char_limit);
886
887                    Some(CCursorRange::one(ccursor))
888                } else {
889                    None
890                }
891            }
892            Event::Text(text_to_insert) => {
893                // Newlines are handled by `Key::Enter`.
894                if !text_to_insert.is_empty() && text_to_insert != "\n" && text_to_insert != "\r" {
895                    let mut ccursor = text.delete_selected(&cursor_range);
896
897                    text.insert_text_at(&mut ccursor, text_to_insert, char_limit);
898
899                    Some(CCursorRange::one(ccursor))
900                } else {
901                    None
902                }
903            }
904            Event::Key {
905                key: Key::Tab,
906                pressed: true,
907                modifiers,
908                ..
909            } if multiline => {
910                let mut ccursor = text.delete_selected(&cursor_range);
911                if modifiers.shift {
912                    // TODO(emilk): support removing indentation over a selection?
913                    text.decrease_indentation(&mut ccursor);
914                } else {
915                    text.insert_text_at(&mut ccursor, "\t", char_limit);
916                }
917                Some(CCursorRange::one(ccursor))
918            }
919            Event::Key {
920                key,
921                pressed: true,
922                modifiers,
923                ..
924            } if return_key.is_some_and(|return_key| {
925                *key == return_key.logical_key && modifiers.matches_logically(return_key.modifiers)
926            }) =>
927            {
928                if multiline {
929                    let mut ccursor = text.delete_selected(&cursor_range);
930                    text.insert_text_at(&mut ccursor, "\n", char_limit);
931                    // TODO(emilk): if code editor, auto-indent by same leading tabs, + one if the lines end on an opening bracket
932                    Some(CCursorRange::one(ccursor))
933                } else {
934                    ui.memory_mut(|mem| mem.surrender_focus(id)); // End input with enter
935                    break;
936                }
937            }
938            Event::Key {
939                key: Key::Z,
940                pressed: true,
941                modifiers,
942                ..
943            } if modifiers.matches_logically(Modifiers::COMMAND) => {
944                if let Some((undo_ccursor_range, undo_txt)) = state
945                    .undoer
946                    .lock()
947                    .undo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned()))
948                {
949                    text.replace_with(undo_txt);
950                    Some(*undo_ccursor_range)
951                } else {
952                    None
953                }
954            }
955            Event::Key {
956                key,
957                pressed: true,
958                modifiers,
959                ..
960            } if (modifiers.matches_logically(Modifiers::COMMAND) && *key == Key::Y)
961                || (modifiers.matches_logically(Modifiers::SHIFT | Modifiers::COMMAND)
962                    && *key == Key::Z) =>
963            {
964                if let Some((redo_ccursor_range, redo_txt)) = state
965                    .undoer
966                    .lock()
967                    .redo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned()))
968                {
969                    text.replace_with(redo_txt);
970                    Some(*redo_ccursor_range)
971                } else {
972                    None
973                }
974            }
975
976            Event::Key {
977                modifiers,
978                key,
979                pressed: true,
980                ..
981            } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key),
982
983            Event::Ime(ime_event) => match ime_event {
984                ImeEvent::Enabled => {
985                    state.ime_enabled = true;
986                    state.ime_cursor_range = cursor_range;
987                    None
988                }
989                ImeEvent::Preedit(text_mark) => {
990                    if text_mark == "\n" || text_mark == "\r" {
991                        None
992                    } else {
993                        // Empty prediction can be produced when user press backspace
994                        // or escape during IME, so we clear current text.
995                        let mut ccursor = text.delete_selected(&cursor_range);
996                        let start_cursor = ccursor;
997                        if !text_mark.is_empty() {
998                            text.insert_text_at(&mut ccursor, text_mark, char_limit);
999                        }
1000                        state.ime_cursor_range = cursor_range;
1001                        Some(CCursorRange::two(start_cursor, ccursor))
1002                    }
1003                }
1004                ImeEvent::Commit(prediction) => {
1005                    if prediction == "\n" || prediction == "\r" {
1006                        None
1007                    } else {
1008                        state.ime_enabled = false;
1009
1010                        if !prediction.is_empty()
1011                            && cursor_range.secondary.ccursor.index
1012                                == state.ime_cursor_range.secondary.ccursor.index
1013                        {
1014                            let mut ccursor = text.delete_selected(&cursor_range);
1015                            text.insert_text_at(&mut ccursor, prediction, char_limit);
1016                            Some(CCursorRange::one(ccursor))
1017                        } else {
1018                            let ccursor = cursor_range.primary.ccursor;
1019                            Some(CCursorRange::one(ccursor))
1020                        }
1021                    }
1022                }
1023                ImeEvent::Disabled => {
1024                    state.ime_enabled = false;
1025                    None
1026                }
1027            },
1028
1029            _ => None,
1030        };
1031
1032        if let Some(new_ccursor_range) = did_mutate_text {
1033            any_change = true;
1034
1035            // Layout again to avoid frame delay, and to keep `text` and `galley` in sync.
1036            *galley = layouter(ui, text.as_str(), wrap_width);
1037
1038            // Set cursor_range using new galley:
1039            cursor_range = CursorRange {
1040                primary: galley.from_ccursor(new_ccursor_range.primary),
1041                secondary: galley.from_ccursor(new_ccursor_range.secondary),
1042            };
1043        }
1044    }
1045
1046    state.cursor.set_range(Some(cursor_range));
1047
1048    state.undoer.lock().feed_state(
1049        ui.input(|i| i.time),
1050        &(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
1051    );
1052
1053    (any_change, cursor_range)
1054}
1055
1056// ----------------------------------------------------------------------------
1057
1058/// Returns `Some(new_cursor)` if we did mutate `text`.
1059fn check_for_mutating_key_press(
1060    os: OperatingSystem,
1061    cursor_range: &CursorRange,
1062    text: &mut dyn TextBuffer,
1063    galley: &Galley,
1064    modifiers: &Modifiers,
1065    key: Key,
1066) -> Option<CCursorRange> {
1067    match key {
1068        Key::Backspace => {
1069            let ccursor = if modifiers.mac_cmd {
1070                text.delete_paragraph_before_cursor(galley, cursor_range)
1071            } else if let Some(cursor) = cursor_range.single() {
1072                if modifiers.alt || modifiers.ctrl {
1073                    // alt on mac, ctrl on windows
1074                    text.delete_previous_word(cursor.ccursor)
1075                } else {
1076                    text.delete_previous_char(cursor.ccursor)
1077                }
1078            } else {
1079                text.delete_selected(cursor_range)
1080            };
1081            Some(CCursorRange::one(ccursor))
1082        }
1083
1084        Key::Delete if !modifiers.shift || os != OperatingSystem::Windows => {
1085            let ccursor = if modifiers.mac_cmd {
1086                text.delete_paragraph_after_cursor(galley, cursor_range)
1087            } else if let Some(cursor) = cursor_range.single() {
1088                if modifiers.alt || modifiers.ctrl {
1089                    // alt on mac, ctrl on windows
1090                    text.delete_next_word(cursor.ccursor)
1091                } else {
1092                    text.delete_next_char(cursor.ccursor)
1093                }
1094            } else {
1095                text.delete_selected(cursor_range)
1096            };
1097            let ccursor = CCursor {
1098                prefer_next_row: true,
1099                ..ccursor
1100            };
1101            Some(CCursorRange::one(ccursor))
1102        }
1103
1104        Key::H if modifiers.ctrl => {
1105            let ccursor = text.delete_previous_char(cursor_range.primary.ccursor);
1106            Some(CCursorRange::one(ccursor))
1107        }
1108
1109        Key::K if modifiers.ctrl => {
1110            let ccursor = text.delete_paragraph_after_cursor(galley, cursor_range);
1111            Some(CCursorRange::one(ccursor))
1112        }
1113
1114        Key::U if modifiers.ctrl => {
1115            let ccursor = text.delete_paragraph_before_cursor(galley, cursor_range);
1116            Some(CCursorRange::one(ccursor))
1117        }
1118
1119        Key::W if modifiers.ctrl => {
1120            let ccursor = if let Some(cursor) = cursor_range.single() {
1121                text.delete_previous_word(cursor.ccursor)
1122            } else {
1123                text.delete_selected(cursor_range)
1124            };
1125            Some(CCursorRange::one(ccursor))
1126        }
1127
1128        _ => None,
1129    }
1130}