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