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