zeus_widgets/
secure_text_edit.rs

1use egui::{
2   Align, Align2, Color32, CursorIcon, Event, EventFilter, FontId, FontSelection, Galley, Id,
3   ImeEvent, Key, KeyboardShortcut, Margin, Modifiers, NumExt, Response, Sense, Shape,
4   TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, epaint, output,
5   text::{self, LayoutJob},
6   text_selection::{self, CCursorRange, text_cursor_state::byte_index_from_char_index},
7   vec2,
8};
9
10use std::sync::Arc;
11use zeus_theme::TextEditVisuals;
12
13#[cfg(feature = "secure-types")]
14use secure_types::{SecureString, Zeroize};
15
16pub trait TextBuffer {
17   fn is_secure(&self) -> bool;
18   fn insert_text_at_char_idx(&mut self, char_idx: usize, text_to_insert: &str) -> usize;
19   fn delete_text_char_range(&mut self, char_range: core::ops::Range<usize>);
20   fn char_len(&self) -> usize;
21   fn to_string(&self) -> String;
22}
23
24#[cfg(feature = "secure-types")]
25impl TextBuffer for SecureString {
26   fn is_secure(&self) -> bool {
27      true
28   }
29
30   fn insert_text_at_char_idx(&mut self, char_idx: usize, text_to_insert: &str) -> usize {
31      self.insert_text_at_char_idx(char_idx, text_to_insert)
32   }
33
34   fn delete_text_char_range(&mut self, char_range: core::ops::Range<usize>) {
35      self.delete_text_char_range(char_range)
36   }
37
38   fn char_len(&self) -> usize {
39      self.char_len()
40   }
41
42   fn to_string(&self) -> String {
43      self.unlock_str(|s| s.to_string())
44   }
45}
46
47impl TextBuffer for String {
48   fn is_secure(&self) -> bool {
49      false
50   }
51
52   fn insert_text_at_char_idx(&mut self, char_idx: usize, text_to_insert: &str) -> usize {
53      let byte_idx = byte_index_from_char_index(self.as_str(), char_idx);
54      self.insert_str(byte_idx, text_to_insert);
55
56      text_to_insert.chars().count()
57   }
58
59   fn delete_text_char_range(&mut self, char_range: core::ops::Range<usize>) {
60      assert!(
61         char_range.start <= char_range.end,
62         "start must be <= end, but got {char_range:?}"
63      );
64
65      let byte_start = byte_index_from_char_index(self.as_str(), char_range.start);
66      let byte_end = byte_index_from_char_index(self.as_str(), char_range.end);
67
68      self.drain(byte_start..byte_end);
69   }
70
71   fn char_len(&self) -> usize {
72      self.chars().count()
73   }
74
75   fn to_string(&self) -> String {
76      self.to_owned()
77   }
78}
79
80#[derive(Clone, Debug, Default)]
81pub struct SecureTextEditState {
82   pub cursor: text_selection::TextCursorState,
83   pub singleline_offset: f32,
84   pub last_interaction_time: f64,
85   pub ime_enabled: bool,
86   pub ime_cursor_range: CCursorRange,
87}
88
89impl SecureTextEditState {
90   pub fn load(ctx: &egui::Context, id: egui::Id) -> Option<Self> {
91      ctx.data_mut(|d| d.get_persisted(id))
92   }
93
94   pub fn store(self, ctx: &egui::Context, id: egui::Id) {
95      ctx.data_mut(|d| d.insert_persisted(id, self));
96   }
97}
98
99pub struct SecureTextEditOutput {
100   pub response: Response,
101   pub state: SecureTextEditState,
102   pub cursor_range: Option<CCursorRange>,
103}
104
105/// A widget for editing text.
106///
107/// This widget is identical to [`egui::TextEdit`], but it uses a custom [`TextBuffer`]
108/// that can take either a [`SecureString`] or a [`String`].
109///
110/// To use it with a [`SecureString`], the feature `secure-types` must be enabled.
111///
112/// ## Notes
113///
114/// - Accessability like screen readers is disabled to avoid multiple unsecure allocations of the entered text.
115/// - If you want to make sure the text you enter doesn't stay in memory in any way you have to set [`Self::password`] to `true`.
116///
117/// Otherwise egui will make copies of that text and some of the copied allocations will stay in memory.
118#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
119pub struct SecureTextEdit<'a> {
120   text: &'a mut dyn TextBuffer,
121   visuals: Option<TextEditVisuals>,
122   hint_text: WidgetText,
123   id: Option<Id>,
124   id_salt: Option<Id>,
125   font_selection: FontSelection,
126   text_color: Option<Color32>,
127   password: bool,
128   frame: bool,
129   margin: Margin,
130   multiline: bool,
131   interactive: bool,
132   desired_width: Option<f32>,
133   desired_height_rows: usize,
134   event_filter: EventFilter,
135   cursor_at_end: bool,
136   min_size: Vec2,
137   align: Align2,
138   clip_text: bool,
139   char_limit: usize,
140   return_key: Option<KeyboardShortcut>,
141   background_color: Option<Color32>,
142}
143
144impl<'a> SecureTextEdit<'a> {
145   pub fn singleline(text: &'a mut dyn TextBuffer) -> Self {
146      Self {
147         text,
148         visuals: None,
149         hint_text: Default::default(),
150         id: None,
151         id_salt: None,
152         font_selection: FontSelection::default(),
153         text_color: None,
154         password: false,
155         frame: true,
156         margin: Margin::symmetric(4, 2),
157         multiline: false,
158         interactive: true,
159         desired_width: None,
160         desired_height_rows: 1,
161         event_filter: EventFilter {
162            horizontal_arrows: true,
163            vertical_arrows: true,
164            tab: false,
165            ..Default::default()
166         },
167         cursor_at_end: true,
168         min_size: Vec2::ZERO,
169         align: Align2::LEFT_CENTER,
170         clip_text: true,
171         char_limit: usize::MAX,
172         return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)),
173         background_color: None,
174      }
175   }
176
177   pub fn multiline(text: &'a mut dyn TextBuffer) -> Self {
178      Self {
179         text,
180         visuals: None,
181         hint_text: Default::default(),
182         id: None,
183         id_salt: None,
184         font_selection: FontSelection::default(),
185         text_color: None,
186         password: false,
187         frame: true,
188         margin: Margin::symmetric(4, 2),
189         multiline: true,
190         interactive: true,
191         desired_width: None,
192         desired_height_rows: 4,
193         event_filter: EventFilter {
194            horizontal_arrows: true,
195            vertical_arrows: true,
196            tab: false,
197            ..Default::default()
198         },
199         cursor_at_end: true,
200         min_size: Vec2::ZERO,
201         align: Align2::LEFT_TOP,
202         clip_text: false,
203         char_limit: usize::MAX,
204         return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)),
205         background_color: None,
206      }
207   }
208
209   pub fn visuals(mut self, visuals: TextEditVisuals) -> Self {
210      self.visuals = Some(visuals);
211      self
212   }
213
214   pub fn id(mut self, id: Id) -> Self {
215      self.id = Some(id);
216      self
217   }
218
219   pub fn id_source(self, id_source: impl std::hash::Hash) -> Self {
220      self.id_salt(id_source)
221   }
222
223   pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
224      self.id_salt = Some(Id::new(id_salt));
225      self
226   }
227
228   pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
229      self.hint_text = hint_text.into();
230      self
231   }
232
233   pub fn font(mut self, font_selection: impl Into<FontSelection>) -> Self {
234      self.font_selection = font_selection.into();
235      self
236   }
237
238   pub fn text_color(mut self, text_color: Color32) -> Self {
239      self.text_color = Some(text_color);
240      self
241   }
242
243   pub fn text_color_opt(mut self, text_color: Option<Color32>) -> Self {
244      self.text_color = text_color;
245      self
246   }
247
248   pub fn password(mut self, password: bool) -> Self {
249      self.password = password;
250      self
251   }
252
253   pub fn frame(mut self, frame: bool) -> Self {
254      self.frame = frame;
255      self
256   }
257
258   pub fn margin(mut self, margin: impl Into<Margin>) -> Self {
259      self.margin = margin.into();
260      self
261   }
262
263   pub fn interactive(mut self, interactive: bool) -> Self {
264      self.interactive = interactive;
265      self
266   }
267
268   pub fn desired_width(mut self, desired_width: f32) -> Self {
269      self.desired_width = Some(desired_width);
270      self
271   }
272
273   pub fn desired_rows(mut self, desired_height_rows: usize) -> Self {
274      self.desired_height_rows = desired_height_rows;
275      self
276   }
277
278   pub fn lock_focus(mut self, tab_will_indent: bool) -> Self {
279      self.event_filter.tab = tab_will_indent;
280      self
281   }
282
283   pub fn cursor_at_end(mut self, b: bool) -> Self {
284      self.cursor_at_end = b;
285      self
286   }
287
288   pub fn min_size(mut self, min_size: Vec2) -> Self {
289      self.min_size = min_size;
290      self
291   }
292
293   pub fn horizontal_align(mut self, align: Align) -> Self {
294      self.align.0[0] = align;
295      self
296   }
297
298   pub fn vertical_align(mut self, align: Align) -> Self {
299      self.align.0[1] = align;
300      self
301   }
302
303   pub fn clip_text(mut self, b: bool) -> Self {
304      if !self.multiline {
305         self.clip_text = b;
306      }
307      self
308   }
309
310   pub fn char_limit(mut self, limit: usize) -> Self {
311      self.char_limit = limit;
312      self
313   }
314
315   pub fn return_key(mut self, return_key: impl Into<Option<KeyboardShortcut>>) -> Self {
316      self.return_key = return_key.into();
317      self
318   }
319
320   pub fn background_color(mut self, color: Color32) -> Self {
321      self.background_color = Some(color);
322      self
323   }
324
325   pub fn show(self, ui: &mut Ui) -> SecureTextEditOutput {
326      let frame = self.frame;
327      let where_to_put_background = ui.painter().add(Shape::Noop);
328      let background_color =
329         self.visuals.as_ref().map(|v| v.bg).unwrap_or(ui.visuals().extreme_bg_color);
330      let is_interactive = self.interactive;
331
332      let (output, text_edit_visuals) = self.show_content(ui);
333
334      let outer_rect_for_frame = output.response.rect;
335      if frame {
336         let visuals = ui.style().interact(&output.response);
337         let frame_rect = outer_rect_for_frame.expand(visuals.expansion);
338         let corner = text_edit_visuals
339            .as_ref()
340            .map(|v| v.corner_radius)
341            .unwrap_or(visuals.corner_radius);
342
343         let shape = if is_interactive {
344            if output.response.has_focus() || output.response.hovered() {
345               let border =
346                  text_edit_visuals.as_ref().map(|v| v.border_open).unwrap_or(visuals.bg_stroke);
347               epaint::RectShape::new(
348                  frame_rect,
349                  corner,
350                  background_color,
351                  border,
352                  epaint::StrokeKind::Inside,
353               )
354            } else {
355               let border =
356                  text_edit_visuals.as_ref().map(|v| v.border).unwrap_or(visuals.bg_stroke);
357               epaint::RectShape::new(
358                  frame_rect,
359                  corner,
360                  background_color,
361                  border,
362                  epaint::StrokeKind::Inside,
363               )
364            }
365         } else {
366            // Not interactive
367            let visuals = &ui.style().visuals.widgets.inactive;
368            let border = text_edit_visuals.as_ref().map(|v| v.border).unwrap_or(visuals.bg_stroke);
369            epaint::RectShape::stroke(
370               frame_rect,
371               corner,
372               border,
373               epaint::StrokeKind::Inside,
374            )
375         };
376         ui.painter().set(where_to_put_background, shape);
377      }
378      output
379   }
380
381   #[allow(clippy::too_many_lines)]
382   fn show_content(mut self, ui: &mut Ui) -> (SecureTextEditOutput, Option<TextEditVisuals>) {
383      let font_id = self.font_selection.resolve(ui.style());
384      let text_color = ui
385         .visuals()
386         .override_text_color
387         .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
388
389      let text_color = self.visuals.map(|v| v.text).unwrap_or(text_color);
390
391      let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
392      let available_width = (ui.available_width() - self.margin.sum().x).at_least(24.0); // Min width
393      let desired_width = self.desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
394      let wrap_width = if ui.layout().horizontal_justify() {
395         available_width
396      } else {
397         desired_width.min(available_width)
398      };
399
400      // --- Layout Galley ---
401
402      let display_text_cow = if self.password {
403         // Generate '●' string based on actual char count
404         std::borrow::Cow::<'_, str>::Owned(
405            std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR)
406               .take(self.text.char_len())
407               .collect::<String>(),
408         )
409      } else {
410         std::borrow::Cow::Owned(self.text.to_string()) // !
411      };
412
413      let mut job = if self.multiline {
414         LayoutJob::simple(
415            (*display_text_cow).to_owned(),
416            font_id.clone(),
417            text_color,
418            wrap_width,
419         )
420      } else {
421         LayoutJob::simple_singleline(
422            (*display_text_cow).to_owned(),
423            font_id.clone(),
424            text_color,
425         )
426      };
427
428      job.halign = self.align.0[0];
429      let galley: Arc<Galley> = ui.fonts_mut(|f| f.layout_job(job));
430
431      // --- Size & Allocation ---
432      let desired_inner_width = if self.clip_text && !self.multiline {
433         wrap_width
434      } else {
435         galley.size().x.max(wrap_width)
436      };
437      let desired_height = (self.desired_height_rows.at_least(1) as f32) * row_height;
438      let desired_inner_size = vec2(
439         desired_inner_width,
440         galley.size().y.max(desired_height),
441      );
442      let desired_outer_size = (desired_inner_size + self.margin.sum()).at_least(self.min_size);
443
444      let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size);
445      let text_draw_rect = outer_rect - self.margin;
446
447      let id = self.id.unwrap_or_else(|| {
448         if let Some(id_salt) = self.id_salt {
449            ui.make_persistent_id(id_salt)
450         } else {
451            auto_id
452         }
453      });
454      let mut state = SecureTextEditState::load(ui.ctx(), id).unwrap_or_default();
455
456      // --- Interaction ---
457      let allow_drag_to_select =
458         ui.input(|i| !i.has_touch_screen()) || ui.memory(|mem| mem.has_focus(id));
459      let sense_behavior = if self.interactive {
460         if allow_drag_to_select {
461            Sense::click_and_drag()
462         } else {
463            Sense::click()
464         }
465      } else {
466         Sense::hover()
467      };
468      let mut response = ui.interact(outer_rect, id, sense_behavior);
469      response.intrinsic_size = Some(vec2(desired_width, desired_outer_size.y));
470
471      // Handle click to focus
472      if self.interactive {
473         if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
474            if response.hovered() {
475               ui.output_mut(|o| o.mutable_text_under_cursor = true);
476            }
477            let singleline_offset_vec = vec2(state.singleline_offset, 0.0);
478            let cursor_at_pointer =
479               galley.cursor_from_pos(pointer_pos - text_draw_rect.min + singleline_offset_vec);
480
481            let is_being_dragged = ui.ctx().is_being_dragged(response.id);
482            let did_interact_with_cursor = state.cursor.pointer_interaction(
483               ui,
484               &response,
485               cursor_at_pointer,
486               &galley,
487               is_being_dragged,
488            );
489
490            if did_interact_with_cursor || response.clicked() {
491               ui.memory_mut(|mem| mem.request_focus(response.id));
492               state.last_interaction_time = ui.input(|i| i.time);
493            }
494         }
495      }
496      if self.interactive && response.hovered() {
497         ui.ctx().set_cursor_icon(CursorIcon::Text);
498      }
499
500      // --- Event Handling ---
501      let mut cursor_range_after_events = None;
502      // Initial galley before any events in this frame
503      let current_frame_galley = galley.clone();
504
505      if self.interactive && ui.memory(|mem| mem.has_focus(id)) {
506         ui.memory_mut(|mem| mem.set_focus_lock_filter(id, self.event_filter));
507
508         let default_cursor_range = if self.cursor_at_end {
509            CCursorRange::one(current_frame_galley.end())
510         } else {
511            CCursorRange::default()
512         };
513
514         let (text_changed_by_event, new_cursor_range, _updated_galley_from_events) =
515            secure_text_edit_events(
516               ui,
517               &mut state,
518               self.text,
519               &current_frame_galley,
520               id,
521               self.multiline,
522               self.password,
523               default_cursor_range,
524               self.char_limit,
525               self.event_filter,
526               self.return_key,
527               &font_id,
528               text_color,
529               wrap_width,
530               self.align.0[0],
531            );
532
533         if text_changed_by_event {
534            response.mark_changed();
535         }
536         cursor_range_after_events = Some(new_cursor_range);
537
538         if !text_changed_by_event {
539            state.cursor.set_char_range(Some(new_cursor_range));
540         }
541      }
542
543      // --- Galley Positioning & Single-line Offset ---
544      let mut galley_pos = self.align.align_size_within_rect(galley.size(), text_draw_rect).min;
545      if self.clip_text && !self.multiline {
546         let current_cursor_primary_x =
547            match cursor_range_after_events.or_else(|| state.cursor.range(&galley)) {
548               Some(cr) => galley.pos_from_cursor(cr.primary).min.x,
549               None => 0.0,
550            };
551         let visible_width = text_draw_rect.width();
552         let mut offset_x = state.singleline_offset;
553         let visible_range_start = offset_x;
554         let visible_range_end = offset_x + visible_width;
555
556         if current_cursor_primary_x < visible_range_start {
557            offset_x = current_cursor_primary_x;
558         } else if current_cursor_primary_x > visible_range_end {
559            offset_x = current_cursor_primary_x - visible_width;
560         }
561         offset_x = offset_x.at_most(galley.size().x - visible_width).at_least(0.0);
562         state.singleline_offset = offset_x;
563         galley_pos.x -= offset_x;
564      } else {
565         // For multiline or non-clip singleline, capture any alignment offset
566         // state.singleline_offset = text_draw_rect.left() - galley_pos.x;
567         state.singleline_offset = 0.0;
568         // And ensure galley_pos respects it if it was aligned (e.g. center/right)
569         state.singleline_offset = text_draw_rect.left() - galley_pos.x;
570      }
571
572      // --- Painting ---
573      if ui.is_rect_visible(text_draw_rect) {
574         let is_text_empty = self.text.char_len() == 0;
575         if is_text_empty && !self.hint_text.is_empty() {
576            let hint_text_color = ui.visuals().weak_text_color();
577            let hint_font_id = FontSelection::default();
578            let hint_galley = self.hint_text.clone().into_galley(
579               ui,
580               Some(TextWrapMode::Wrap),
581               text_draw_rect.width(),
582               hint_font_id,
583            );
584            let hint_galley_pos =
585               self.align.align_size_within_rect(hint_galley.size(), text_draw_rect).min;
586            ui.painter_at(text_draw_rect)
587               .galley(hint_galley_pos, hint_galley, hint_text_color);
588         }
589
590         let mut galley_for_paint = galley.clone();
591         if ui.memory(|mem| mem.has_focus(id)) {
592            if let Some(cursor_range_for_sel) = state.cursor.range(&galley_for_paint) {
593               text_selection::visuals::paint_text_selection(
594                  &mut galley_for_paint,
595                  ui.visuals(),
596                  &cursor_range_for_sel,
597                  None,
598               );
599            }
600         }
601         ui.painter_at(text_draw_rect).galley(galley_pos, galley_for_paint, text_color);
602
603         // Paint cursor
604         if self.interactive && ui.memory(|mem| mem.has_focus(id)) {
605            if let Some(cursor_range_for_cursor_paint) = state.cursor.range(&galley) {
606               // Use original galley for metrics
607               let primary_cursor_rect_ui = text_selection::text_cursor_state::cursor_rect(
608                  &galley,
609                  &cursor_range_for_cursor_paint.primary,
610                  row_height,
611               )
612               .translate(galley_pos.to_vec2());
613
614               if response.changed() {
615                  // Could also check selection_changed
616                  ui.scroll_to_rect(
617                     primary_cursor_rect_ui.expand(self.margin.sum().y / 2.0),
618                     None,
619                  );
620               }
621
622               if ui.ctx().input(|i| i.focused) {
623                  // Viewport has focus
624                  let time_since_last_interaction =
625                     ui.input(|i| i.time) - state.last_interaction_time;
626                  text_selection::visuals::paint_text_cursor(
627                     ui,
628                     &ui.painter_at(text_draw_rect.expand(1.0)), // Expand for cursor
629                     primary_cursor_rect_ui,
630                     time_since_last_interaction,
631                  );
632               }
633               // IME output
634               let to_global =
635                  ui.ctx().layer_transform_to_global(ui.layer_id()).unwrap_or_default();
636               ui.ctx().output_mut(|o| {
637                  o.ime = Some(output::IMEOutput {
638                     rect: to_global * text_draw_rect,
639                     cursor_rect: to_global * primary_cursor_rect_ui,
640                  });
641               });
642            }
643         }
644      }
645
646      // IME focus state management
647      if state.ime_enabled && (response.gained_focus() || response.lost_focus()) {
648         state.ime_enabled = false;
649         if let Some(mut ccursor_range) = state.cursor.char_range() {
650            ccursor_range.secondary.index = ccursor_range.primary.index;
651            state.cursor.set_char_range(Some(ccursor_range));
652         }
653         ui.input_mut(|i| i.events.retain(|e| !matches!(e, Event::Ime(_))));
654      }
655
656      state.clone().store(ui.ctx(), id);
657
658      // !
659      // This is only for accessibility, so set them to empty is fine
660      /*
661      let _ = self.text.str_scope(|s| {
662         if self.password {
663            std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR)
664               .take(s.chars().count())
665               .collect()
666         } else {
667            s.to_string()
668         }
669      });
670      */
671      response.widget_info(|| {
672         WidgetInfo::text_edit(
673            ui.is_enabled(),
674            String::new(),
675            String::new(),
676            String::new(),
677         )
678      });
679
680      let output = SecureTextEditOutput {
681         response,
682         state,
683         cursor_range: cursor_range_after_events,
684      };
685
686      let visuals = self.visuals.take();
687      (output, visuals)
688   }
689}
690
691impl<'a> Widget for SecureTextEdit<'a> {
692   fn ui(self, ui: &mut Ui) -> Response {
693      self.show(ui).response
694   }
695}
696
697#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
698fn secure_text_edit_events(
699   ui: &Ui,
700   state: &mut SecureTextEditState,
701   text: &mut dyn TextBuffer,
702   initial_galley: &Arc<Galley>,
703   id: Id,
704   multiline: bool,
705   password: bool,
706   default_cursor_range: CCursorRange,
707   char_limit: usize,
708   event_filter: EventFilter,
709   return_key: Option<KeyboardShortcut>,
710   font_id: &FontId,
711   text_color: Color32,
712   wrap_width: f32,
713   text_align_horizontal: Align,
714) -> (bool, CCursorRange, Arc<Galley>) {
715   let os = ui.ctx().os();
716   let mut current_galley = initial_galley.clone();
717   let mut cursor_range = state.cursor.range(&current_galley).unwrap_or(default_cursor_range);
718   let mut text_changed_in_total = false;
719
720   let mut events_filtered = ui.input(|i| i.filtered_events(&event_filter));
721   if state.ime_enabled {
722      events_filtered.sort_by_key(|e| !matches!(e, Event::Ime(_)));
723   }
724
725   for event in events_filtered {
726      let current_char_len_before_event = text.char_len();
727      let mut text_mutated_this_event = false;
728
729      // Pass current_galley to on_event. If it modifies cursor_range, it uses current_galley.
730      if cursor_range.on_event(os, &event, &current_galley, id) {
731         state.last_interaction_time = ui.input(|i| i.time);
732         continue;
733      }
734
735      let new_ccursor_range_opt: Option<CCursorRange> = match event {
736         // For now don't allow copy/cut on any text
737         Event::Copy => None,
738         Event::Cut => None,
739         Event::Paste(mut text_to_paste) => {
740            if !text_to_paste.is_empty() {
741               let [min, max] = cursor_range.sorted_cursors();
742               let selection_char_len = max.index - min.index;
743               text.delete_text_char_range(min.index..max.index);
744
745               let space_available = char_limit
746                  .saturating_sub(current_char_len_before_event.saturating_sub(selection_char_len));
747               let mut final_text_to_paste = if text_to_paste.chars().count() > space_available {
748                  text_to_paste.chars().take(space_available).collect::<String>()
749               } else {
750                  text_to_paste.clone()
751               };
752
753               let mut current_ccursor = min;
754               let chars_inserted =
755                  text.insert_text_at_char_idx(current_ccursor.index, &final_text_to_paste);
756               current_ccursor.index += chars_inserted;
757               text_mutated_this_event = true; // Mark mutation
758
759               #[cfg(feature = "secure-types")]
760               {
761                  text_to_paste.zeroize();
762                  final_text_to_paste.zeroize();
763               }
764
765               Some(text::CCursorRange::one(current_ccursor))
766            } else {
767               None
768            }
769         }
770         Event::Text(mut text_to_insert) => {
771            if !text_to_insert.is_empty() && text_to_insert != "\n" && text_to_insert != "\r" {
772               let [min, max] = cursor_range.sorted_cursors();
773               let selection_char_len = max.index - min.index;
774               text.delete_text_char_range(min.index..max.index);
775
776               let space_available = char_limit
777                  .saturating_sub(current_char_len_before_event.saturating_sub(selection_char_len));
778               let mut final_text_to_insert = if text_to_insert.chars().count() > space_available {
779                  text_to_insert.chars().take(space_available).collect::<String>()
780               } else {
781                  text_to_insert.clone()
782               };
783
784               let mut current_ccursor = min;
785               let chars_inserted =
786                  text.insert_text_at_char_idx(current_ccursor.index, &final_text_to_insert);
787               current_ccursor.index += chars_inserted;
788               text_mutated_this_event = true;
789
790               #[cfg(feature = "secure-types")]
791               {
792                  text_to_insert.zeroize();
793                  final_text_to_insert.zeroize();
794               }
795               Some(text::CCursorRange::one(current_ccursor))
796            } else {
797               None
798            }
799         }
800         Event::Key {
801            key: Key::Enter,
802            pressed: true,
803            modifiers,
804            ..
805         } if return_key.is_some_and(|rk| {
806            Key::Enter == rk.logical_key && modifiers.matches_logically(rk.modifiers)
807         }) =>
808         {
809            if multiline {
810               let [min, max] = cursor_range.sorted_cursors();
811               let selection_char_len = max.index - min.index;
812               text.delete_text_char_range(min.index..max.index);
813
814               let current_len_after_delete =
815                  current_char_len_before_event.saturating_sub(selection_char_len);
816               let space_available = char_limit.saturating_sub(current_len_after_delete);
817
818               if space_available > 0 {
819                  let mut current_ccursor = min;
820                  let chars_inserted = text.insert_text_at_char_idx(current_ccursor.index, "\n");
821                  current_ccursor.index += chars_inserted;
822                  text_mutated_this_event = true; // Mark mutation
823                  Some(text::CCursorRange::one(current_ccursor))
824               } else {
825                  None
826               }
827            } else {
828               ui.memory_mut(|mem| mem.surrender_focus(id));
829               None
830            }
831         }
832         Event::Key {
833            key: Key::Backspace,
834            pressed: true,
835            ..
836         } => {
837            // Modifiers for word/para delete later
838            let [min, max] = cursor_range.sorted_cursors();
839            let mut new_cursor_idx = min.index;
840            if min == max {
841               // No selection
842               if min.index > 0 {
843                  text.delete_text_char_range(min.index - 1..min.index);
844                  new_cursor_idx = min.index - 1;
845                  text_mutated_this_event = true;
846               }
847            } else {
848               // Selection exists
849               text.delete_text_char_range(min.index..max.index);
850               // new_cursor_idx is already min.ccursor.index
851               text_mutated_this_event = true;
852            }
853            if text_mutated_this_event {
854               Some(text::CCursorRange::one(text::CCursor::new(
855                  new_cursor_idx,
856               )))
857            } else {
858               None
859            }
860         }
861         Event::Key {
862            key: Key::Delete,
863            pressed: true,
864            ..
865         } => {
866            // Modifiers for word/para delete later
867            let [min, max] = cursor_range.sorted_cursors();
868            if min == max {
869               if min.index < current_char_len_before_event {
870                  // Before deleting
871                  text.delete_text_char_range(min.index..min.index + 1);
872                  text_mutated_this_event = true;
873               }
874            } else {
875               text.delete_text_char_range(min.index..max.index);
876               text_mutated_this_event = true;
877            }
878            if text_mutated_this_event {
879               Some(text::CCursorRange::one(min))
880            } else {
881               None
882            }
883         }
884         Event::Key {
885            key: Key::Tab,
886            pressed: true,
887            modifiers,
888            ..
889         } if multiline && event_filter.tab => {
890            let [min, _max] = cursor_range.sorted_cursors();
891            let mut current_ccursor = min;
892            if modifiers.shift {
893            } else {
894               let space_available = char_limit.saturating_sub(current_char_len_before_event);
895               if space_available > 0 {
896                  // Enough for at least '\t'
897                  let chars_inserted = text.insert_text_at_char_idx(current_ccursor.index, "\t");
898                  current_ccursor.index += chars_inserted;
899                  text_mutated_this_event = true;
900               }
901            }
902            if text_mutated_this_event {
903               Some(text::CCursorRange::one(current_ccursor))
904            } else {
905               None
906            }
907         }
908         Event::Ime(ime_event) => {
909            match ime_event {
910               ImeEvent::Enabled => {
911                  state.ime_enabled = true;
912                  state.ime_cursor_range = cursor_range;
913                  None
914               }
915               ImeEvent::Preedit(mut preedit_text) => {
916                  let [min_ime, max_ime] = state.ime_cursor_range.sorted_cursors(); // Use IME's original range for delete
917                  text.delete_text_char_range(min_ime.index..max_ime.index);
918                  let mut c = min_ime; // Insert at start of IME original selection
919                  let inserted = text.insert_text_at_char_idx(c.index, &preedit_text);
920                  c.index += inserted;
921                  text_mutated_this_event = true;
922
923                  #[cfg(feature = "secure-types")]
924                  preedit_text.zeroize();
925
926                  Some(text::CCursorRange::two(min_ime, c))
927               }
928               ImeEvent::Commit(mut commit_text) => {
929                  state.ime_enabled = false; // IME done
930                  let [min_commit, max_commit] = cursor_range.sorted_cursors();
931                  text.delete_text_char_range(min_commit.index..max_commit.index);
932                  let mut c = min_commit;
933                  let inserted = text.insert_text_at_char_idx(c.index, &commit_text);
934                  c.index += inserted;
935                  text_mutated_this_event = true;
936                  
937                  #[cfg(feature = "secure-types")]
938                  commit_text.zeroize();
939
940                  Some(text::CCursorRange::one(c))
941               }
942               ImeEvent::Disabled => {
943                  state.ime_enabled = false;
944                  None
945               }
946            }
947         }
948         _ => None,
949      };
950
951      if text_mutated_this_event {
952         text_changed_in_total = true;
953
954         // --- Re-layout galley ---
955
956         let display_text_for_layout = if password {
957            std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR)
958               .take(text.char_len())
959               .collect::<String>()
960         } else {
961            text.to_string() // !
962         };
963
964         let mut job = if multiline {
965            LayoutJob::simple(
966               display_text_for_layout,
967               font_id.clone(),
968               text_color,
969               wrap_width,
970            )
971         } else {
972            LayoutJob::simple_singleline(
973               display_text_for_layout,
974               font_id.clone(),
975               text_color,
976            )
977         };
978
979         job.halign = text_align_horizontal;
980         current_galley = ui.fonts_mut(|f| f.layout_job(job));
981      }
982
983      // Set the final state.cursor using the most up-to-date cursor_range
984      state.cursor.set_char_range(new_ccursor_range_opt);
985      if let Some(new_range) = new_ccursor_range_opt {
986         state.last_interaction_time = ui.input(|i| i.time);
987         cursor_range = new_range;
988      }
989   }
990
991   (
992      text_changed_in_total,
993      cursor_range,
994      current_galley,
995   )
996}