egui/widgets/text_edit/
builder.rs

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