Skip to main content

egui/widgets/text_edit/
builder.rs

1use std::sync::Arc;
2
3use emath::{Rect, TSTransform};
4use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
5
6use crate::{
7    Align, Align2, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon, Event,
8    EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key,
9    KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer,
10    TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
11    os::OperatingSystem,
12    output::OutputEvent,
13    response, text_selection,
14    text_selection::{CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection},
15    vec2,
16};
17
18use super::{TextEditOutput, TextEditState};
19
20type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>;
21
22/// A text region that the user can edit the contents of.
23///
24/// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`].
25///
26/// Example:
27///
28/// ```
29/// # egui::__run_test_ui(|ui| {
30/// # let mut my_string = String::new();
31/// let response = ui.add(egui::TextEdit::singleline(&mut my_string));
32/// if response.changed() {
33///     // …
34/// }
35/// if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
36///     // …
37/// }
38/// # });
39/// ```
40///
41/// To fill an [`Ui`] with a [`TextEdit`] use [`Ui::add_sized`]:
42///
43/// ```
44/// # egui::__run_test_ui(|ui| {
45/// # let mut my_string = String::new();
46/// ui.add_sized(ui.available_size(), egui::TextEdit::multiline(&mut my_string));
47/// # });
48/// ```
49///
50///
51/// You can also use [`TextEdit`] to show text that can be selected, but not edited.
52/// To do so, pass in a `&mut` reference to a `&str`, for instance:
53///
54/// ```
55/// fn selectable_text(ui: &mut egui::Ui, mut text: &str) {
56///     ui.add(egui::TextEdit::multiline(&mut text));
57/// }
58/// ```
59///
60/// ## Advanced usage
61/// See [`TextEdit::show`].
62///
63/// ## Other
64/// The background color of a [`crate::TextEdit`] is [`crate::Visuals::text_edit_bg_color`] or can be set with [`crate::TextEdit::background_color`].
65#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
66pub struct TextEdit<'t> {
67    text: &'t mut dyn TextBuffer,
68    prefix: Atoms<'static>,
69    suffix: Atoms<'static>,
70    hint_text: Atoms<'static>,
71    id: Option<Id>,
72    id_salt: Option<Id>,
73    font_selection: FontSelection,
74    text_color: Option<Color32>,
75    layouter: Option<LayouterFn<'t>>,
76    password: bool,
77    frame: Option<Frame>,
78    margin: Margin,
79    multiline: bool,
80    interactive: bool,
81    desired_width: Option<f32>,
82    desired_height_rows: usize,
83    event_filter: EventFilter,
84    cursor_at_end: bool,
85    min_size: Vec2,
86    align: Align2,
87    clip_text: bool,
88    char_limit: usize,
89    return_key: Option<KeyboardShortcut>,
90    background_color: Option<Color32>,
91}
92
93impl WidgetWithState for TextEdit<'_> {
94    type State = TextEditState;
95}
96
97impl TextEdit<'_> {
98    pub fn load_state(ctx: &Context, id: Id) -> Option<TextEditState> {
99        TextEditState::load(ctx, id)
100    }
101
102    pub fn store_state(ctx: &Context, id: Id, state: TextEditState) {
103        state.store(ctx, id);
104    }
105}
106
107impl<'t> TextEdit<'t> {
108    /// No newlines (`\n`) allowed. Pressing enter key will result in the [`TextEdit`] losing focus (`response.lost_focus`).
109    pub fn singleline(text: &'t mut dyn TextBuffer) -> Self {
110        Self {
111            desired_height_rows: 1,
112            multiline: false,
113            clip_text: true,
114            ..Self::multiline(text)
115        }
116    }
117
118    /// A [`TextEdit`] for multiple lines. Pressing enter key will create a new line by default (can be changed with [`return_key`](TextEdit::return_key)).
119    pub fn multiline(text: &'t mut dyn TextBuffer) -> Self {
120        Self {
121            text,
122            prefix: Default::default(),
123            suffix: Default::default(),
124            hint_text: Default::default(),
125            id: None,
126            id_salt: None,
127            font_selection: Default::default(),
128            text_color: None,
129            layouter: None,
130            password: false,
131            frame: None,
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 IntoAtoms<'static>) -> Self {
206        self.hint_text = hint_text.into_atoms();
207        self
208    }
209
210    /// Add a prefix to the text edit. This will always be shown before the editable text.
211    #[inline]
212    pub fn prefix(mut self, prefix: impl IntoAtoms<'static>) -> Self {
213        self.prefix = prefix.into_atoms();
214        self
215    }
216
217    /// Add a suffix to the text edit. This will always be shown after the editable text.
218    #[inline]
219    pub fn suffix(mut self, suffix: impl IntoAtoms<'static>) -> Self {
220        self.suffix = suffix.into_atoms();
221        self
222    }
223
224    /// Set the background color of the [`TextEdit`]. The default is [`crate::Visuals::text_edit_bg_color`].
225    // TODO(bircni): remove this once #3284 is implemented
226    #[inline]
227    pub fn background_color(mut self, color: Color32) -> Self {
228        self.background_color = Some(color);
229        self
230    }
231
232    /// If true, hide the letters from view and prevent copying from the field.
233    #[inline]
234    pub fn password(mut self, password: bool) -> Self {
235        self.password = password;
236        self
237    }
238
239    /// Pick a [`crate::FontId`] or [`TextStyle`].
240    #[inline]
241    pub fn font(mut self, font_selection: impl Into<FontSelection>) -> Self {
242        self.font_selection = font_selection.into();
243        self
244    }
245
246    #[inline]
247    pub fn text_color(mut self, text_color: Color32) -> Self {
248        self.text_color = Some(text_color);
249        self
250    }
251
252    #[inline]
253    pub fn text_color_opt(mut self, text_color: Option<Color32>) -> Self {
254        self.text_color = text_color;
255        self
256    }
257
258    /// Override how text is being shown inside the [`TextEdit`].
259    ///
260    /// This can be used to implement things like syntax highlighting.
261    ///
262    /// This function will be called at least once per frame,
263    /// so it is strongly suggested that you cache the results of any syntax highlighter
264    /// so as not to waste CPU highlighting the same string every frame.
265    ///
266    /// The arguments is the enclosing [`Ui`] (so you can access e.g. [`Context::fonts`]),
267    /// the text and the wrap width.
268    ///
269    /// ```
270    /// # egui::__run_test_ui(|ui| {
271    /// # let mut my_code = String::new();
272    /// # fn my_memoized_highlighter(s: &str) -> egui::text::LayoutJob { Default::default() }
273    /// let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
274    ///     let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(buf.as_str());
275    ///     layout_job.wrap.max_width = wrap_width;
276    ///     ui.fonts_mut(|f| f.layout_job(layout_job))
277    /// };
278    /// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
279    /// # });
280    /// ```
281    #[inline]
282    pub fn layouter(
283        mut self,
284        layouter: &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>,
285    ) -> Self {
286        self.layouter = Some(layouter);
287
288        self
289    }
290
291    /// Default is `true`. If set to `false` then you cannot interact with the text (neither edit or select it).
292    ///
293    /// Consider using [`Ui::add_enabled`] instead to also give the [`TextEdit`] a greyed out look.
294    #[inline]
295    pub fn interactive(mut self, interactive: bool) -> Self {
296        self.interactive = interactive;
297        self
298    }
299
300    /// Customize the [`Frame`] around the text edit.
301    #[inline]
302    pub fn frame(mut self, frame: Frame) -> Self {
303        self.frame = Some(frame);
304        self
305    }
306
307    /// Set margin of text. Default is `Margin::symmetric(4.0, 2.0)`
308    #[inline]
309    pub fn margin(mut self, margin: impl Into<Margin>) -> Self {
310        self.margin = margin.into();
311        self
312    }
313
314    /// Set to 0.0 to keep as small as possible.
315    /// Set to [`f32::INFINITY`] to take up all available space (i.e. disable automatic word wrap).
316    #[inline]
317    pub fn desired_width(mut self, desired_width: f32) -> Self {
318        self.desired_width = Some(desired_width);
319        self
320    }
321
322    /// Set the number of rows to show by default.
323    /// The default for singleline text is `1`.
324    /// The default for multiline text is `4`.
325    #[inline]
326    pub fn desired_rows(mut self, desired_height_rows: usize) -> Self {
327        self.desired_height_rows = desired_height_rows;
328        self
329    }
330
331    /// When `false` (default), pressing TAB will move focus
332    /// to the next widget.
333    ///
334    /// When `true`, the widget will keep the focus and pressing TAB
335    /// will insert the `'\t'` character.
336    #[inline]
337    pub fn lock_focus(mut self, tab_will_indent: bool) -> Self {
338        self.event_filter.tab = tab_will_indent;
339        self
340    }
341
342    /// When `true` (default), the cursor will initially be placed at the end of the text.
343    ///
344    /// When `false`, the cursor will initially be placed at the beginning of the text.
345    #[inline]
346    pub fn cursor_at_end(mut self, b: bool) -> Self {
347        self.cursor_at_end = b;
348        self
349    }
350
351    /// When `true` (default), overflowing text will be clipped.
352    ///
353    /// When `false`, widget width will expand to make all text visible.
354    ///
355    /// This only works for singleline [`TextEdit`].
356    #[inline]
357    pub fn clip_text(mut self, b: bool) -> Self {
358        // always show everything in multiline
359        if !self.multiline {
360            self.clip_text = b;
361        }
362        self
363    }
364
365    /// Sets the limit for the amount of characters can be entered
366    ///
367    /// This only works for singleline [`TextEdit`]
368    #[inline]
369    pub fn char_limit(mut self, limit: usize) -> Self {
370        self.char_limit = limit;
371        self
372    }
373
374    /// Set the horizontal align of the inner text.
375    #[inline]
376    pub fn horizontal_align(mut self, align: Align) -> Self {
377        self.align.0[0] = align;
378        self
379    }
380
381    /// Set the vertical align of the inner text.
382    #[inline]
383    pub fn vertical_align(mut self, align: Align) -> Self {
384        self.align.0[1] = align;
385        self
386    }
387
388    /// Set the minimum size of the [`TextEdit`].
389    #[inline]
390    pub fn min_size(mut self, min_size: Vec2) -> Self {
391        self.min_size = min_size;
392        self
393    }
394
395    /// Set the return key combination.
396    ///
397    /// This combination will cause a newline on multiline,
398    /// whereas on singleline it will cause the widget to lose focus.
399    ///
400    /// This combination is optional and can be disabled by passing [`None`] into this function.
401    #[inline]
402    pub fn return_key(mut self, return_key: impl Into<Option<KeyboardShortcut>>) -> Self {
403        self.return_key = return_key.into();
404        self
405    }
406}
407
408// ----------------------------------------------------------------------------
409
410impl Widget for TextEdit<'_> {
411    fn ui(self, ui: &mut Ui) -> Response {
412        self.show(ui).response.response
413    }
414}
415
416impl TextEdit<'_> {
417    /// Show the [`TextEdit`], returning a rich [`TextEditOutput`].
418    ///
419    /// ```
420    /// # egui::__run_test_ui(|ui| {
421    /// # let mut my_string = String::new();
422    /// let output = egui::TextEdit::singleline(&mut my_string).show(ui);
423    /// if let Some(text_cursor_range) = output.cursor_range {
424    ///     use egui::TextBuffer as _;
425    ///     let selected_chars = text_cursor_range.as_sorted_char_range();
426    ///     let selected_text = my_string.char_range(selected_chars);
427    ///     ui.label("Selected text: ");
428    ///     ui.monospace(selected_text);
429    /// }
430    /// # });
431    /// ```
432    pub fn show(self, ui: &mut Ui) -> TextEditOutput {
433        let TextEdit {
434            text,
435            prefix,
436            suffix,
437            mut hint_text,
438            id,
439            id_salt,
440            font_selection,
441            text_color,
442            layouter,
443            password,
444            frame,
445            margin,
446            multiline,
447            interactive,
448            desired_width,
449            desired_height_rows,
450            event_filter,
451            cursor_at_end,
452            min_size,
453            align,
454            clip_text,
455            char_limit,
456            return_key,
457            background_color,
458        } = self;
459
460        let text_color = text_color
461            .or_else(|| ui.visuals().override_text_color)
462            // .unwrap_or_else(|| ui.style().interact(&response).text_color()); // too bright
463            .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
464
465        let prev_text = text.as_str().to_owned();
466        let hint_text_str = hint_text.text().unwrap_or_default().to_string();
467
468        let font_id = font_selection.resolve(ui.style());
469        let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
470        const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
471        let available_width = ui.available_width().at_least(MIN_WIDTH);
472        let desired_width = desired_width
473            .unwrap_or_else(|| ui.spacing().text_edit_width)
474            .at_least(min_size.x);
475        let allocate_width = desired_width.at_most(available_width);
476
477        let font_id_clone = font_id.clone();
478        let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
479            let text = mask_if_password(password, text.as_str());
480            let mut layout_job = if multiline {
481                LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
482            } else {
483                LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
484            };
485            layout_job.halign = align.x();
486            ui.fonts_mut(|f| f.layout_job(layout_job))
487        };
488
489        let layouter = layouter.unwrap_or(&mut default_layouter);
490
491        let min_inner_height = (desired_height_rows.at_least(1) as f32) * row_height;
492
493        let id = id.unwrap_or_else(|| {
494            if let Some(id_salt) = id_salt {
495                ui.make_persistent_id(id_salt)
496            } else {
497                // Since we are only storing the cursor a persistent Id is not super important
498                let id = ui.next_auto_id();
499                ui.skip_ahead_auto_ids(1);
500                id
501            }
502        });
503
504        // On touch screens (e.g. mobile in `eframe` web), should
505        // dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
506        // Since currently copying selected text in not supported on `eframe` web,
507        // we prioritize touch-scrolling:
508        let allow_drag_to_select =
509            ui.input(|i| !i.has_touch_screen()) || ui.memory(|mem| mem.has_focus(id));
510
511        let sense = if interactive {
512            if allow_drag_to_select {
513                Sense::click_and_drag()
514            } else {
515                Sense::click()
516            }
517        } else {
518            Sense::hover()
519        };
520
521        let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
522        let mut cursor_range = None;
523        let mut prev_cursor_range = None;
524
525        let mut text_changed = false;
526        let text_mutable = text.is_mutable();
527
528        let mut handle_events = |ui: &Ui, galley: &mut Arc<Galley>, layouter, wrap_width, text| {
529            if interactive && ui.memory(|mem| mem.has_focus(id)) {
530                ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
531
532                let default_cursor_range = if cursor_at_end {
533                    CCursorRange::one(galley.end())
534                } else {
535                    CCursorRange::default()
536                };
537                prev_cursor_range = state.cursor.range(galley);
538
539                let (changed, new_cursor_range) = events(
540                    ui,
541                    &mut state,
542                    text,
543                    galley,
544                    layouter,
545                    id,
546                    wrap_width,
547                    multiline,
548                    password,
549                    default_cursor_range,
550                    char_limit,
551                    event_filter,
552                    return_key,
553                );
554
555                if changed {
556                    text_changed = true;
557                }
558                cursor_range = Some(new_cursor_range);
559            }
560        };
561
562        // We need to calculate the galley within the atom closure, so we can calculate it based on
563        // the available width (in case of wrapping multiline text edits). But we show it later,
564        // so we can clip it to the available size. Thus, extract it from the atom closure here.
565        let mut get_galley = None;
566        let inner_rect_id = Id::new("text_edit_rect");
567        let mut response = {
568            let any_shrink = hint_text.any_shrink();
569            // Ideally we could just do `let mut atoms = prefix` here, but that won't compile
570            // but due to servo/rust-smallvec#146 (also see the comment below).
571            let mut atoms: Atoms<'_> = Atoms::new(());
572
573            // TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have
574            // smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
575            for atom in prefix {
576                atoms.push_right(atom);
577            }
578
579            if text.as_str().is_empty() && !hint_text.is_empty() {
580                // Add hint_text (if any):
581                let mut shrunk = any_shrink;
582                let mut first = true;
583
584                // Since we can't set a fallback color per atom, we have to override it here.
585                // Sucks, since it means users won't be able to override it.
586                hint_text.map_texts(|t| t.color(ui.style().visuals.weak_text_color()));
587
588                for mut atom in hint_text {
589                    if !shrunk && matches!(atom.kind, AtomKind::Text(_)) {
590                        // elide the hint_text if needed
591                        atom = atom.atom_shrink(true);
592                        atom = atom.atom_grow(true);
593                        shrunk = true;
594                    }
595
596                    if first {
597                        // The first atom in the hint text gets inner_rect_id, so we can know
598                        // where to paint the cursor
599                        atom = atom.atom_id(inner_rect_id);
600                        first = false;
601                    }
602
603                    // The hint text should be shown left top instead of centered (important for
604                    // multi line text edits)
605                    atoms.push_right(atom.atom_align(Align2::LEFT_TOP));
606                }
607
608                // Calculate the empty galley, so it can be read later. The available width is
609                // technically wrong, but doesn't matter since the galley is empty
610                let available_width = allocate_width - margin.sum().x;
611                let galley = layouter(ui, text, available_width);
612
613                // We can't update the galley immediately here, since it would show both hint text
614                // and the newly typed letter. So we pass a clone instead, and accept having a frame
615                // delay on the very first keystroke.
616                let mut galley_clone = Arc::clone(&galley);
617                handle_events(ui, &mut galley_clone, layouter, available_width, text);
618
619                get_galley = Some(galley);
620            } else {
621                // We need to shrink when clip_text, so that we don't exceed the available size
622                // and thus clip. We also need to shrink in multi line text edits, so text can
623                // wrap appropriately.
624                let should_shrink = clip_text || multiline;
625
626                // We need a closure here, so we can calculate the galley based on the available
627                // width (after adding suffix and prefix), for correct wrapping in multi line text
628                // edits
629                atoms.push_right(
630                    AtomKind::closure(|ui, args| {
631                        let mut galley = layouter(ui, text, args.available_size.x);
632
633                        // Handling events here allows us to update the galley immediately on
634                        // keystrokes, avoiding frame delays, and ensuring the scroll_to within
635                        // ScrollAreas works correctly.
636                        handle_events(ui, &mut galley, layouter, args.available_size.x, text);
637
638                        let intrinsic_size = galley.intrinsic_size();
639                        let mut size = galley.size();
640                        size.y = size.y.at_least(min_inner_height);
641                        if clip_text {
642                            size.x = size.x.at_most(args.available_size.x);
643                        }
644
645                        // We paint the galley later, so we can do clipping and offsetting
646                        get_galley = Some(galley);
647                        IntoSizedResult {
648                            intrinsic_size,
649                            sized: SizedAtomKind::Empty { size: Some(size) },
650                        }
651                    })
652                    .atom_grow(true)
653                    .atom_align(self.align)
654                    .atom_id(inner_rect_id)
655                    .atom_shrink(should_shrink),
656                );
657            }
658
659            // TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have
660            // smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
661            for atom in suffix {
662                atoms.push_right(atom);
663            }
664
665            let custom_frame = frame.is_some();
666            let frame = frame.unwrap_or_else(|| Frame::new().inner_margin(margin));
667
668            let min_height = min_inner_height + frame.total_margin().sum().y;
669
670            // This wrap mode only affects the hint_text
671            let wrap_mode = if multiline {
672                TextWrapMode::Wrap
673            } else {
674                TextWrapMode::Truncate
675            };
676
677            let mut allocated = AtomLayout::new(atoms)
678                .id(id)
679                .min_size(Vec2::new(allocate_width, min_height))
680                .max_width(allocate_width)
681                .sense(sense)
682                .frame(frame)
683                .align2(align)
684                .wrap_mode(wrap_mode)
685                .allocate(ui);
686
687            allocated.frame = if !custom_frame {
688                let visuals = ui.style().interact(&allocated.response);
689                let background_color =
690                    background_color.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
691
692                let (corner_radius, background_color, stroke) = if text_mutable {
693                    if allocated.response.has_focus() {
694                        (
695                            visuals.corner_radius,
696                            background_color,
697                            ui.visuals().selection.stroke,
698                        )
699                    } else {
700                        (visuals.corner_radius, background_color, visuals.bg_stroke)
701                    }
702                } else {
703                    let visuals = &ui.style().visuals.widgets.inactive;
704                    (
705                        visuals.corner_radius,
706                        Color32::TRANSPARENT,
707                        visuals.bg_stroke,
708                    )
709                };
710                allocated
711                    .frame
712                    .fill(background_color)
713                    .corner_radius(corner_radius)
714                    .inner_margin(
715                        allocated.frame.inner_margin
716                            + Margin::same((visuals.expansion - stroke.width).round() as i8),
717                    )
718                    .outer_margin(Margin::same(-(visuals.expansion as i8)))
719                    .stroke(stroke)
720            } else {
721                allocated.frame
722            };
723
724            allocated.paint(ui)
725        };
726
727        let inner_rect = response.rect(inner_rect_id).unwrap_or(Rect::ZERO);
728
729        // Our atom closure was now called, so the galley should always be available here
730        let mut galley = get_galley.expect("Galley should be available here");
731
732        // Don't send `OutputEvent::Clicked` when a user presses the space bar
733        response.flags -= response::Flags::FAKE_PRIMARY_CLICKED;
734        let text_clip_rect = inner_rect;
735        let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor
736
737        if interactive && let Some(pointer_pos) = response.interact_pointer_pos() {
738            if response.hovered() && text.is_mutable() {
739                ui.output_mut(|o| o.mutable_text_under_cursor = true);
740            }
741
742            // TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
743
744            let cursor_at_pointer = galley.cursor_from_pos(
745                pointer_pos - inner_rect.min + state.text_offset + vec2(galley.rect.left(), 0.0),
746            );
747
748            if ui.visuals().text_cursor.preview
749                && response.hovered()
750                && ui.input(|i| i.pointer.is_moving())
751            {
752                // text cursor preview:
753                let cursor_rect = TSTransform::from_translation(
754                    inner_rect.min.to_vec2() - vec2(galley.rect.left(), 0.0),
755                ) * cursor_rect(&galley, &cursor_at_pointer, row_height);
756                text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect);
757            }
758
759            let is_being_dragged = ui.is_being_dragged(response.id);
760            let did_interact = state.cursor.pointer_interaction(
761                ui,
762                &response,
763                cursor_at_pointer,
764                &galley,
765                is_being_dragged,
766            );
767
768            if did_interact || response.clicked() {
769                ui.memory_mut(|mem| mem.request_focus(response.id));
770
771                state.last_interaction_time = ui.input(|i| i.time);
772            }
773        }
774
775        if interactive && response.hovered() {
776            ui.set_cursor_icon(CursorIcon::Text);
777        }
778
779        if text_changed {
780            response.mark_changed();
781        }
782
783        let mut galley_pos = align
784            .align_size_within_rect(galley.size(), inner_rect)
785            .intersect(inner_rect) // limit pos to the response rect area
786            .min;
787        let align_offset = inner_rect.left_top() - galley_pos;
788
789        // Visual clipping for singleline text editor with text larger than width
790        if clip_text && align_offset.x == 0.0 {
791            let cursor_pos = match (cursor_range, ui.memory(|mem| mem.has_focus(id))) {
792                (Some(cursor_range), true) => galley.pos_from_cursor(cursor_range.primary).min.x,
793                _ => 0.0,
794            };
795
796            let mut offset_x = state.text_offset.x;
797            let visible_range = offset_x..=offset_x + inner_rect.width();
798
799            if !visible_range.contains(&cursor_pos) {
800                if cursor_pos < *visible_range.start() {
801                    offset_x = cursor_pos;
802                } else {
803                    offset_x = cursor_pos - inner_rect.width();
804                }
805            }
806
807            offset_x = offset_x
808                .at_most(galley.size().x - inner_rect.width())
809                .at_least(0.0);
810
811            state.text_offset = vec2(offset_x, align_offset.y);
812            galley_pos -= vec2(offset_x, 0.0);
813        } else {
814            state.text_offset = align_offset;
815        }
816
817        let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) =
818            (cursor_range, prev_cursor_range)
819        {
820            prev_cursor_range != cursor_range
821        } else {
822            false
823        };
824
825        if ui.is_rect_visible(inner_rect) {
826            let has_focus = ui.memory(|mem| mem.has_focus(id));
827
828            if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
829                // Add text selection rectangles to the galley:
830                paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None);
831            }
832
833            painter.galley(
834                galley_pos - vec2(galley.rect.left(), 0.0),
835                Arc::clone(&galley),
836                text_color,
837            );
838
839            if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
840                let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height)
841                    .translate(galley_pos.to_vec2() - vec2(galley.rect.left(), 0.0));
842
843                if response.changed() || selection_changed {
844                    // Scroll to keep primary cursor in view:
845                    ui.scroll_to_rect(primary_cursor_rect, None);
846                }
847
848                if text.is_mutable() && interactive {
849                    let now = ui.input(|i| i.time);
850                    if response.changed() || selection_changed {
851                        state.last_interaction_time = now;
852                    }
853
854                    // Only show (and blink) cursor if the egui viewport has focus.
855                    // This is for two reasons:
856                    // * Don't give the impression that the user can type into a window without focus
857                    // * Don't repaint the ui because of a blinking cursor in an app that is not in focus
858                    let viewport_has_focus = ui.input(|i| i.focused);
859                    if viewport_has_focus {
860                        text_selection::visuals::paint_text_cursor(
861                            ui,
862                            &painter,
863                            primary_cursor_rect,
864                            now - state.last_interaction_time,
865                        );
866                    }
867
868                    // Set IME output (in screen coords) when text is editable and visible
869                    let to_global = ui
870                        .ctx()
871                        .layer_transform_to_global(ui.layer_id())
872                        .unwrap_or_default();
873
874                    ui.output_mut(|o| {
875                        o.ime = Some(crate::output::IMEOutput {
876                            rect: to_global * inner_rect,
877                            cursor_rect: to_global * primary_cursor_rect,
878                        });
879                    });
880                }
881            }
882        }
883
884        // Ensures correct IME behavior when the text input area gains or loses focus.
885        if state.ime_enabled && (response.gained_focus() || response.lost_focus()) {
886            state.ime_enabled = false;
887            if let Some(mut ccursor_range) = state.cursor.char_range() {
888                ccursor_range.secondary.index = ccursor_range.primary.index;
889                state.cursor.set_char_range(Some(ccursor_range));
890            }
891            ui.input_mut(|i| i.events.retain(|e| !matches!(e, Event::Ime(_))));
892        }
893
894        state.clone().store(ui.ctx(), id);
895
896        if response.changed() {
897            response.widget_info(|| {
898                WidgetInfo::text_edit(
899                    ui.is_enabled(),
900                    mask_if_password(password, prev_text.as_str()),
901                    mask_if_password(password, text.as_str()),
902                    hint_text_str.as_str(),
903                )
904            });
905        } else if selection_changed && let Some(cursor_range) = cursor_range {
906            let char_range = cursor_range.primary.index..=cursor_range.secondary.index;
907            let info = WidgetInfo::text_selection_changed(
908                ui.is_enabled(),
909                char_range,
910                mask_if_password(password, text.as_str()),
911            );
912            response.output_event(OutputEvent::TextSelectionChanged(info));
913        } else {
914            response.widget_info(|| {
915                WidgetInfo::text_edit(
916                    ui.is_enabled(),
917                    mask_if_password(password, prev_text.as_str()),
918                    mask_if_password(password, text.as_str()),
919                    hint_text_str.as_str(),
920                )
921            });
922        }
923
924        let role = if password {
925            accesskit::Role::PasswordInput
926        } else if multiline {
927            accesskit::Role::MultilineTextInput
928        } else {
929            accesskit::Role::TextInput
930        };
931
932        crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
933            ui.ctx(),
934            id,
935            cursor_range,
936            role,
937            TSTransform::from_translation(galley_pos.to_vec2()),
938            &galley,
939        );
940
941        TextEditOutput {
942            response,
943            galley,
944            galley_pos,
945            text_clip_rect,
946            state,
947            cursor_range,
948        }
949    }
950}
951
952fn mask_if_password(is_password: bool, text: &str) -> String {
953    fn mask_password(text: &str) -> String {
954        std::iter::repeat_n(
955            epaint::text::PASSWORD_REPLACEMENT_CHAR,
956            text.chars().count(),
957        )
958        .collect::<String>()
959    }
960
961    if is_password {
962        mask_password(text)
963    } else {
964        text.to_owned()
965    }
966}
967
968// ----------------------------------------------------------------------------
969
970/// Check for (keyboard) events to edit the cursor and/or text.
971#[expect(clippy::too_many_arguments)]
972fn events(
973    ui: &crate::Ui,
974    state: &mut TextEditState,
975    text: &mut dyn TextBuffer,
976    galley: &mut Arc<Galley>,
977    layouter: &mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>,
978    id: Id,
979    wrap_width: f32,
980    multiline: bool,
981    password: bool,
982    default_cursor_range: CCursorRange,
983    char_limit: usize,
984    event_filter: EventFilter,
985    return_key: Option<KeyboardShortcut>,
986) -> (bool, CCursorRange) {
987    let os = ui.os();
988
989    let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
990
991    // We feed state to the undoer both before and after handling input
992    // so that the undoer creates automatic saves even when there are no events for a while.
993    state.undoer.lock().feed_state(
994        ui.input(|i| i.time),
995        &(cursor_range, text.as_str().to_owned()),
996    );
997
998    let copy_if_not_password = |ui: &Ui, text: String| {
999        if !password {
1000            ui.copy_text(text);
1001        }
1002    };
1003
1004    let mut any_change = false;
1005
1006    let events = ui.input(|i| i.filtered_events(&event_filter));
1007
1008    for event in &events {
1009        let did_mutate_text = match event {
1010            // First handle events that only changes the selection cursor, not the text:
1011            event if cursor_range.on_event(os, event, galley, id) => None,
1012
1013            Event::Copy => {
1014                if !cursor_range.is_empty() {
1015                    copy_if_not_password(ui, cursor_range.slice_str(text.as_str()).to_owned());
1016                }
1017                None
1018            }
1019            Event::Cut => {
1020                if cursor_range.is_empty() {
1021                    None
1022                } else {
1023                    copy_if_not_password(ui, cursor_range.slice_str(text.as_str()).to_owned());
1024                    Some(CCursorRange::one(text.delete_selected(&cursor_range)))
1025                }
1026            }
1027            Event::Paste(text_to_insert) => {
1028                if !text_to_insert.is_empty() {
1029                    let mut ccursor = text.delete_selected(&cursor_range);
1030                    if multiline {
1031                        text.insert_text_at(&mut ccursor, text_to_insert, char_limit);
1032                    } else {
1033                        let single_line = text_to_insert.replace(['\r', '\n'], " ");
1034                        text.insert_text_at(&mut ccursor, &single_line, char_limit);
1035                    }
1036
1037                    Some(CCursorRange::one(ccursor))
1038                } else {
1039                    None
1040                }
1041            }
1042            Event::Text(text_to_insert) => {
1043                // Newlines are handled by `Key::Enter`.
1044                if !text_to_insert.is_empty() && text_to_insert != "\n" && text_to_insert != "\r" {
1045                    let mut ccursor = text.delete_selected(&cursor_range);
1046
1047                    text.insert_text_at(&mut ccursor, text_to_insert, char_limit);
1048
1049                    Some(CCursorRange::one(ccursor))
1050                } else {
1051                    None
1052                }
1053            }
1054            Event::Key {
1055                key: Key::Tab,
1056                pressed: true,
1057                modifiers,
1058                ..
1059            } if multiline => {
1060                let mut ccursor = text.delete_selected(&cursor_range);
1061                if modifiers.shift {
1062                    // TODO(emilk): support removing indentation over a selection?
1063                    text.decrease_indentation(&mut ccursor);
1064                } else {
1065                    text.insert_text_at(&mut ccursor, "\t", char_limit);
1066                }
1067                Some(CCursorRange::one(ccursor))
1068            }
1069            Event::Key {
1070                key,
1071                pressed: true,
1072                modifiers,
1073                ..
1074            } if return_key.is_some_and(|return_key| {
1075                *key == return_key.logical_key && modifiers.matches_logically(return_key.modifiers)
1076            }) =>
1077            {
1078                if multiline {
1079                    let mut ccursor = text.delete_selected(&cursor_range);
1080                    text.insert_text_at(&mut ccursor, "\n", char_limit);
1081                    // TODO(emilk): if code editor, auto-indent by same leading tabs, + one if the lines end on an opening bracket
1082                    Some(CCursorRange::one(ccursor))
1083                } else {
1084                    ui.memory_mut(|mem| mem.surrender_focus(id)); // End input with enter
1085                    break;
1086                }
1087            }
1088
1089            Event::Key {
1090                key,
1091                pressed: true,
1092                modifiers,
1093                ..
1094            } if (modifiers.matches_logically(Modifiers::COMMAND) && *key == Key::Y)
1095                || (modifiers.matches_logically(Modifiers::SHIFT | Modifiers::COMMAND)
1096                    && *key == Key::Z) =>
1097            {
1098                if let Some((redo_ccursor_range, redo_txt)) = state
1099                    .undoer
1100                    .lock()
1101                    .redo(&(cursor_range, text.as_str().to_owned()))
1102                {
1103                    text.replace_with(redo_txt);
1104                    Some(*redo_ccursor_range)
1105                } else {
1106                    None
1107                }
1108            }
1109
1110            Event::Key {
1111                key: Key::Z,
1112                pressed: true,
1113                modifiers,
1114                ..
1115            } if modifiers.matches_logically(Modifiers::COMMAND) => {
1116                if let Some((undo_ccursor_range, undo_txt)) = state
1117                    .undoer
1118                    .lock()
1119                    .undo(&(cursor_range, text.as_str().to_owned()))
1120                {
1121                    text.replace_with(undo_txt);
1122                    Some(*undo_ccursor_range)
1123                } else {
1124                    None
1125                }
1126            }
1127
1128            Event::Key {
1129                modifiers,
1130                key,
1131                pressed: true,
1132                ..
1133            } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key),
1134
1135            Event::Ime(ime_event) => {
1136                /// Both `ImeEvent::Preedit("")` and `ImeEvent::Commit("")`
1137                /// might be emitted from different integrations to signify that
1138                /// the current IME composition should be cleared.
1139                ///
1140                /// Example integrations where only `ImeEvent::Preedit("")` of
1141                /// those two events is emitted when the last character is
1142                /// deleted with a backspace:
1143                /// - `egui-winit` on macOS 15.7.3.
1144                /// - `egui-winit` on Debian13 with gnome48 and wayland.
1145                ///
1146                /// An example integration where only `ImeEvent::Commit("")` of
1147                /// those two events is emitted when the last character is
1148                /// deleted with a backspace:
1149                /// - `eframe`'s web integration on Safari 26.2 (on macOS
1150                ///   15.7.3).
1151                ///
1152                /// ## Note
1153                ///
1154                /// The term “pre-edit string” is used by X11 and Wayland, and
1155                /// we use “pre-edit text” and “pre-edit range” here in the
1156                /// same manner.
1157                /// See: <https://wayland.app/protocols/input-method-unstable-v2>
1158                ///
1159                /// We previously referred to “pre-edit text” as “prediction”,
1160                /// which is not standard and can mean different things.
1161                fn clear_preedit_text(
1162                    text: &mut dyn TextBuffer,
1163                    preedit_range: &CCursorRange,
1164                ) -> CCursor {
1165                    text.delete_selected(preedit_range)
1166                }
1167
1168                match ime_event {
1169                    ImeEvent::Enabled => {
1170                        state.ime_enabled = true;
1171                        state.ime_cursor_range = cursor_range;
1172                        None
1173                    }
1174                    ImeEvent::Preedit(preedit_text) => {
1175                        if preedit_text == "\n" || preedit_text == "\r" {
1176                            None
1177                        } else {
1178                            let mut ccursor = clear_preedit_text(text, &cursor_range);
1179
1180                            let start_cursor = ccursor;
1181                            if !preedit_text.is_empty() {
1182                                text.insert_text_at(&mut ccursor, preedit_text, char_limit);
1183                            }
1184                            state.ime_cursor_range = cursor_range;
1185                            Some(CCursorRange::two(start_cursor, ccursor))
1186                        }
1187                    }
1188                    ImeEvent::Commit(commit_text) => {
1189                        if commit_text == "\n" || commit_text == "\r" {
1190                            None
1191                        } else {
1192                            state.ime_enabled = false;
1193
1194                            let mut ccursor = clear_preedit_text(text, &cursor_range);
1195
1196                            if !commit_text.is_empty()
1197                                && cursor_range.secondary.index
1198                                    == state.ime_cursor_range.secondary.index
1199                            {
1200                                text.insert_text_at(&mut ccursor, commit_text, char_limit);
1201                            }
1202
1203                            Some(CCursorRange::one(ccursor))
1204                        }
1205                    }
1206                    ImeEvent::Disabled => {
1207                        state.ime_enabled = false;
1208                        None
1209                    }
1210                }
1211            }
1212
1213            _ => None,
1214        };
1215
1216        if let Some(new_ccursor_range) = did_mutate_text {
1217            any_change = true;
1218
1219            // Layout again to avoid frame delay, and to keep `text` and `galley` in sync.
1220            *galley = layouter(ui, text, wrap_width);
1221
1222            // Set cursor_range using new galley:
1223            cursor_range = new_ccursor_range;
1224        }
1225    }
1226
1227    state.cursor.set_char_range(Some(cursor_range));
1228
1229    state.undoer.lock().feed_state(
1230        ui.input(|i| i.time),
1231        &(cursor_range, text.as_str().to_owned()),
1232    );
1233
1234    (any_change, cursor_range)
1235}
1236
1237// ----------------------------------------------------------------------------
1238
1239/// Returns `Some(new_cursor)` if we did mutate `text`.
1240fn check_for_mutating_key_press(
1241    os: OperatingSystem,
1242    cursor_range: &CCursorRange,
1243    text: &mut dyn TextBuffer,
1244    galley: &Galley,
1245    modifiers: &Modifiers,
1246    key: Key,
1247) -> Option<CCursorRange> {
1248    match key {
1249        Key::Backspace => {
1250            let ccursor = if modifiers.mac_cmd {
1251                text.delete_paragraph_before_cursor(galley, cursor_range)
1252            } else if let Some(cursor) = cursor_range.single() {
1253                if modifiers.alt || modifiers.ctrl {
1254                    // alt on mac, ctrl on windows
1255                    text.delete_previous_word(cursor)
1256                } else {
1257                    text.delete_previous_char(cursor)
1258                }
1259            } else {
1260                text.delete_selected(cursor_range)
1261            };
1262            Some(CCursorRange::one(ccursor))
1263        }
1264
1265        Key::Delete if !modifiers.shift || os != OperatingSystem::Windows => {
1266            let ccursor = if modifiers.mac_cmd {
1267                text.delete_paragraph_after_cursor(galley, cursor_range)
1268            } else if let Some(cursor) = cursor_range.single() {
1269                if modifiers.alt || modifiers.ctrl {
1270                    // alt on mac, ctrl on windows
1271                    text.delete_next_word(cursor)
1272                } else {
1273                    text.delete_next_char(cursor)
1274                }
1275            } else {
1276                text.delete_selected(cursor_range)
1277            };
1278            let ccursor = CCursor {
1279                prefer_next_row: true,
1280                ..ccursor
1281            };
1282            Some(CCursorRange::one(ccursor))
1283        }
1284
1285        Key::H if modifiers.ctrl => {
1286            let ccursor = text.delete_previous_char(cursor_range.primary);
1287            Some(CCursorRange::one(ccursor))
1288        }
1289
1290        Key::K if modifiers.ctrl => {
1291            let ccursor = text.delete_paragraph_after_cursor(galley, cursor_range);
1292            Some(CCursorRange::one(ccursor))
1293        }
1294
1295        Key::U if modifiers.ctrl => {
1296            let ccursor = text.delete_paragraph_before_cursor(galley, cursor_range);
1297            Some(CCursorRange::one(ccursor))
1298        }
1299
1300        Key::W if modifiers.ctrl => {
1301            let ccursor = if let Some(cursor) = cursor_range.single() {
1302                text.delete_previous_word(cursor)
1303            } else {
1304                text.delete_selected(cursor_range)
1305            };
1306            Some(CCursorRange::one(ccursor))
1307        }
1308
1309        _ => None,
1310    }
1311}