egui/widgets/text_edit/
builder.rs

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