1use egui::{
2 Align, Align2, Color32, CursorIcon, Event, EventFilter, FontId, FontSelection, Galley,
3 IMEPurpose, Id, ImeEvent, Key, KeyboardShortcut, Margin, Modifiers, NumExt, Response, Sense,
4 Shape, 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 crate::theme::Theme;
11use crate::visuals::TextEditVisuals;
12use std::{fmt::Debug, hash::Hash, sync::Arc};
13
14#[cfg(feature = "secure-types")]
15use secure_types::{SecureString, Zeroize};
16
17pub trait TextBuffer {
23 fn is_secure(&self) -> bool;
25 fn insert_text_at_char_idx(&mut self, char_idx: usize, text_to_insert: &str) -> usize;
27 fn delete_text_char_range(&mut self, char_range: core::ops::Range<usize>);
29 fn char_len(&self) -> usize;
31 fn to_string(&self) -> String;
33}
34
35#[cfg(feature = "secure-types")]
36impl TextBuffer for SecureString {
37 fn is_secure(&self) -> bool {
38 true
39 }
40
41 fn insert_text_at_char_idx(&mut self, char_idx: usize, text_to_insert: &str) -> usize {
42 self.insert_text_at_char_idx(char_idx, text_to_insert)
43 }
44
45 fn delete_text_char_range(&mut self, char_range: core::ops::Range<usize>) {
46 self.delete_text_char_range(char_range)
47 }
48
49 fn char_len(&self) -> usize {
50 self.char_len()
51 }
52
53 fn to_string(&self) -> String {
54 self.unlock_str(|s| s.to_string())
55 }
56}
57
58impl TextBuffer for String {
59 fn is_secure(&self) -> bool {
60 false
61 }
62
63 fn insert_text_at_char_idx(&mut self, char_idx: usize, text_to_insert: &str) -> usize {
64 let byte_idx = byte_index_from_char_index(self.as_str(), char_idx.into());
65 self.insert_str(byte_idx.into(), text_to_insert);
66
67 text_to_insert.chars().count()
68 }
69
70 fn delete_text_char_range(&mut self, char_range: core::ops::Range<usize>) {
71 assert!(
72 char_range.start <= char_range.end,
73 "start must be <= end, but got {char_range:?}"
74 );
75
76 let byte_start = byte_index_from_char_index(self.as_str(), char_range.start.into());
77 let byte_end = byte_index_from_char_index(self.as_str(), char_range.end.into());
78
79 self.drain(byte_start.0..byte_end.0);
80 }
81
82 fn char_len(&self) -> usize {
83 self.chars().count()
84 }
85
86 fn to_string(&self) -> String {
87 self.to_owned()
88 }
89}
90
91#[derive(Clone, Debug, Default)]
93pub struct SecureTextEditState {
94 pub cursor: text_selection::TextCursorState,
96 pub singleline_offset: f32,
98 pub last_interaction_time: f64,
100 pub ime_enabled: bool,
106 pub ime_cursor_range: CCursorRange,
109}
110
111impl SecureTextEditState {
112 pub fn load(ctx: &egui::Context, id: egui::Id) -> Option<Self> {
114 ctx.data_mut(|d| d.get_persisted(id))
115 }
116
117 pub fn store(self, ctx: &egui::Context, id: egui::Id) {
119 ctx.data_mut(|d| d.insert_persisted(id, self));
120 }
121}
122
123pub struct SecureTextEditOutput {
125 pub response: Response,
127 pub state: SecureTextEditState,
129 pub cursor_range: Option<CCursorRange>,
131}
132
133#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
147pub struct SecureTextEdit<'a> {
148 text: &'a mut dyn TextBuffer,
149 visuals: Option<TextEditVisuals>,
150 hint_text: WidgetText,
151 id: Option<Id>,
152 id_salt: Option<Id>,
153 font_selection: FontSelection,
154 text_color: Option<Color32>,
155 password: bool,
156 frame: bool,
157 margin: Margin,
158 multiline: bool,
159 interactive: bool,
160 desired_width: Option<f32>,
161 desired_height_rows: usize,
162 event_filter: EventFilter,
163 cursor_at_end: bool,
164 min_size: Vec2,
165 align: Align2,
166 clip_text: bool,
167 char_limit: usize,
168 return_key: Option<KeyboardShortcut>,
169 background_color: Option<Color32>,
170}
171
172impl<'a> SecureTextEdit<'a> {
173 pub fn singleline(text: &'a mut dyn TextBuffer) -> Self {
175 Self {
176 text,
177 visuals: None,
178 hint_text: Default::default(),
179 id: None,
180 id_salt: None,
181 font_selection: FontSelection::default(),
182 text_color: None,
183 password: false,
184 frame: true,
185 margin: Margin::symmetric(4, 2),
186 multiline: false,
187 interactive: true,
188 desired_width: None,
189 desired_height_rows: 1,
190 event_filter: EventFilter {
191 horizontal_arrows: true,
192 vertical_arrows: true,
193 tab: false,
194 ..Default::default()
195 },
196 cursor_at_end: true,
197 min_size: Vec2::ZERO,
198 align: Align2::LEFT_CENTER,
199 clip_text: true,
200 char_limit: usize::MAX,
201 return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)),
202 background_color: None,
203 }
204 }
205
206 pub fn multiline(text: &'a mut dyn TextBuffer) -> Self {
208 Self {
209 text,
210 visuals: None,
211 hint_text: Default::default(),
212 id: None,
213 id_salt: None,
214 font_selection: FontSelection::default(),
215 text_color: None,
216 password: false,
217 frame: true,
218 margin: Margin::symmetric(4, 2),
219 multiline: true,
220 interactive: true,
221 desired_width: None,
222 desired_height_rows: 4,
223 event_filter: EventFilter {
224 horizontal_arrows: true,
225 vertical_arrows: true,
226 tab: false,
227 ..Default::default()
228 },
229 cursor_at_end: true,
230 min_size: Vec2::ZERO,
231 align: Align2::LEFT_TOP,
232 clip_text: false,
233 char_limit: usize::MAX,
234 return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)),
235 background_color: None,
236 }
237 }
238
239 pub fn visuals(mut self, visuals: TextEditVisuals) -> Self {
241 self.visuals = Some(visuals);
242 self
243 }
244
245 pub fn id(mut self, id: Id) -> Self {
247 self.id = Some(id);
248 self
249 }
250
251 pub fn id_source(self, id_source: impl Hash + Debug) -> Self {
253 self.id_salt(id_source)
254 }
255
256 pub fn id_salt(mut self, id_salt: impl Hash + Debug) -> Self {
258 self.id_salt = Some(Id::new(id_salt));
259 self
260 }
261
262 pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
264 self.hint_text = hint_text.into();
265 self
266 }
267
268 pub fn font(mut self, font_selection: impl Into<FontSelection>) -> Self {
270 self.font_selection = font_selection.into();
271 self
272 }
273
274 pub fn text_color(mut self, text_color: Color32) -> Self {
276 self.text_color = Some(text_color);
277 self
278 }
279
280 pub fn text_color_opt(mut self, text_color: Option<Color32>) -> Self {
282 self.text_color = text_color;
283 self
284 }
285
286 pub fn password(mut self, password: bool) -> Self {
290 self.password = password;
291 self
292 }
293
294 pub fn frame(mut self, frame: bool) -> Self {
296 self.frame = frame;
297 self
298 }
299
300 pub fn margin(mut self, margin: impl Into<Margin>) -> Self {
302 self.margin = margin.into();
303 self
304 }
305
306 pub fn interactive(mut self, interactive: bool) -> Self {
308 self.interactive = interactive;
309 self
310 }
311
312 pub fn desired_width(mut self, desired_width: f32) -> Self {
314 self.desired_width = Some(desired_width);
315 self
316 }
317
318 pub fn desired_rows(mut self, desired_height_rows: usize) -> Self {
320 self.desired_height_rows = desired_height_rows;
321 self
322 }
323
324 pub fn lock_focus(mut self, tab_will_indent: bool) -> Self {
326 self.event_filter.tab = tab_will_indent;
327 self
328 }
329
330 pub fn cursor_at_end(mut self, b: bool) -> Self {
332 self.cursor_at_end = b;
333 self
334 }
335
336 pub fn min_size(mut self, min_size: Vec2) -> Self {
338 self.min_size = min_size;
339 self
340 }
341
342 pub fn horizontal_align(mut self, align: Align) -> Self {
344 self.align.0[0] = align;
345 self
346 }
347
348 pub fn vertical_align(mut self, align: Align) -> Self {
350 self.align.0[1] = align;
351 self
352 }
353
354 pub fn clip_text(mut self, b: bool) -> Self {
356 if !self.multiline {
357 self.clip_text = b;
358 }
359 self
360 }
361
362 pub fn char_limit(mut self, limit: usize) -> Self {
364 self.char_limit = limit;
365 self
366 }
367
368 pub fn return_key(mut self, return_key: impl Into<Option<KeyboardShortcut>>) -> Self {
370 self.return_key = return_key.into();
371 self
372 }
373
374 pub fn background_color(mut self, color: Color32) -> Self {
376 self.background_color = Some(color);
377 self
378 }
379
380 pub fn show(mut self, ui: &mut Ui) -> SecureTextEditOutput {
382 self.visuals = self.visuals.or_else(|| Theme::text_edit_visuals_from_ctx(ui.ctx()));
383 let frame = self.frame;
384 let where_to_put_background = ui.painter().add(Shape::Noop);
385 let background_color =
386 self.visuals.as_ref().map(|v| v.bg).unwrap_or(ui.visuals().extreme_bg_color);
387 let is_interactive = self.interactive;
388
389 let (output, text_edit_visuals) = self.show_content(ui);
390
391 let outer_rect_for_frame = output.response.rect;
392 if frame {
393 let visuals = ui.style().interact(&output.response);
394 let frame_rect = outer_rect_for_frame.expand(visuals.expansion);
395 let corner = text_edit_visuals
396 .as_ref()
397 .map(|v| v.corner_radius)
398 .unwrap_or(visuals.corner_radius);
399
400 let shape = if is_interactive {
401 if output.response.has_focus() || output.response.hovered() {
402 let border =
403 text_edit_visuals.as_ref().map(|v| v.border_open).unwrap_or(visuals.bg_stroke);
404 epaint::RectShape::new(
405 frame_rect,
406 corner,
407 background_color,
408 border,
409 epaint::StrokeKind::Inside,
410 )
411 } else {
412 let border =
413 text_edit_visuals.as_ref().map(|v| v.border).unwrap_or(visuals.bg_stroke);
414 epaint::RectShape::new(
415 frame_rect,
416 corner,
417 background_color,
418 border,
419 epaint::StrokeKind::Inside,
420 )
421 }
422 } else {
423 let visuals = &ui.style().visuals.widgets.inactive;
425 let border = text_edit_visuals.as_ref().map(|v| v.border).unwrap_or(visuals.bg_stroke);
426 epaint::RectShape::stroke(
427 frame_rect,
428 corner,
429 border,
430 epaint::StrokeKind::Inside,
431 )
432 };
433 ui.painter().set(where_to_put_background, shape);
434 }
435 output
436 }
437
438 #[allow(clippy::too_many_lines)]
439 fn show_content(mut self, ui: &mut Ui) -> (SecureTextEditOutput, Option<TextEditVisuals>) {
440 let font_id = self.font_selection.resolve(ui.style());
441 let text_color = ui
442 .visuals()
443 .override_text_color
444 .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
445
446 let text_color = self.visuals.map(|v| v.text).unwrap_or(text_color);
447
448 let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
449 let available_width = (ui.available_width() - self.margin.sum().x).at_least(24.0); let desired_width = self.desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
451 let wrap_width = if ui.layout().horizontal_justify() {
452 available_width
453 } else {
454 desired_width.min(available_width)
455 };
456
457 let display_text_cow = if self.password {
460 std::borrow::Cow::<'_, str>::Owned(
462 std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR)
463 .take(self.text.char_len())
464 .collect::<String>(),
465 )
466 } else {
467 std::borrow::Cow::Owned(self.text.to_string()) };
469
470 let mut job = if self.multiline {
471 LayoutJob::simple(
472 (*display_text_cow).to_owned(),
473 font_id.clone(),
474 text_color,
475 wrap_width,
476 )
477 } else {
478 LayoutJob::simple_singleline(
479 (*display_text_cow).to_owned(),
480 font_id.clone(),
481 text_color,
482 )
483 };
484
485 job.halign = self.align.0[0];
486 let mut galley: Arc<Galley> = ui.fonts_mut(|f| f.layout_job(job));
487
488 let desired_inner_width = if self.clip_text && !self.multiline {
490 wrap_width
491 } else {
492 galley.size().x.max(wrap_width)
493 };
494 let desired_height = (self.desired_height_rows.at_least(1) as f32) * row_height;
495 let desired_inner_size = vec2(
496 desired_inner_width,
497 galley.size().y.max(desired_height),
498 );
499 let desired_outer_size = (desired_inner_size + self.margin.sum()).at_least(self.min_size);
500
501 let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size);
502 let text_draw_rect = outer_rect - self.margin;
503
504 let id = self.id.unwrap_or_else(|| {
505 if let Some(id_salt) = self.id_salt {
506 ui.make_persistent_id(id_salt)
507 } else {
508 auto_id
509 }
510 });
511 let mut state = SecureTextEditState::load(ui.ctx(), id).unwrap_or_default();
512
513 let allow_drag_to_select =
515 ui.input(|i| !i.has_touch_screen()) || ui.memory(|mem| mem.has_focus(id));
516 let sense_behavior = if self.interactive {
517 if allow_drag_to_select {
518 Sense::click_and_drag()
519 } else {
520 Sense::click()
521 }
522 } else {
523 Sense::hover()
524 };
525 let mut response = ui.interact(outer_rect, id, sense_behavior);
526 response.set_intrinsic_size(vec2(desired_width, desired_outer_size.y));
527
528 if self.interactive {
530 if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
531 if response.hovered() {
532 ui.output_mut(|o| o.mutable_text_under_cursor = true);
533 }
534 let singleline_offset_vec = vec2(state.singleline_offset, 0.0);
535 let cursor_at_pointer =
536 galley.cursor_from_pos(pointer_pos - text_draw_rect.min + singleline_offset_vec);
537
538 let is_being_dragged = ui.ctx().is_being_dragged(response.id);
539 let did_interact_with_cursor = state.cursor.pointer_interaction(
540 ui,
541 &response,
542 cursor_at_pointer,
543 &galley,
544 is_being_dragged,
545 );
546
547 if did_interact_with_cursor || response.clicked() {
548 ui.memory_mut(|mem| mem.request_focus(response.id));
549 state.last_interaction_time = ui.input(|i| i.time);
550 }
551 }
552 }
553 if self.interactive && response.hovered() {
554 ui.ctx().set_cursor_icon(CursorIcon::Text);
555 }
556
557 let mut cursor_range_after_events = None;
559 let owns_ime_events = ui.memory(|mem| mem.owns_ime_events(id));
560 if !owns_ime_events {
561 state.ime_enabled = false;
562 if let Some(range) = state.cursor.char_range() {
563 state.cursor.set_char_range(Some(CCursorRange::one(range.primary)));
564 }
565 }
566
567 if self.interactive && ui.memory(|mem| mem.has_focus(id)) {
568 ui.memory_mut(|mem| mem.set_focus_lock_filter(id, self.event_filter));
569
570 let default_cursor_range = if self.cursor_at_end {
571 CCursorRange::one(galley.end())
572 } else {
573 CCursorRange::default()
574 };
575
576 let (text_changed_by_event, new_cursor_range, updated_galley) = secure_text_edit_events(
577 ui,
578 &mut state,
579 self.text,
580 &galley,
581 id,
582 self.multiline,
583 self.password,
584 default_cursor_range,
585 owns_ime_events,
586 self.char_limit,
587 self.event_filter,
588 self.return_key,
589 &font_id,
590 text_color,
591 wrap_width,
592 self.align.0[0],
593 );
594
595 if text_changed_by_event {
596 response.mark_changed();
597 galley = updated_galley;
598 }
599 cursor_range_after_events = Some(new_cursor_range);
600 state.cursor.set_char_range(Some(new_cursor_range));
601 }
602
603 let mut galley_pos = self.align.align_size_within_rect(galley.size(), text_draw_rect).min;
605 if self.clip_text && !self.multiline {
606 let current_cursor_primary_x =
607 match cursor_range_after_events.or_else(|| state.cursor.range(&galley)) {
608 Some(cr) => galley.pos_from_cursor(cr.primary).min.x,
609 None => 0.0,
610 };
611 let visible_width = text_draw_rect.width();
612 let mut offset_x = state.singleline_offset;
613 let visible_range_start = offset_x;
614 let visible_range_end = offset_x + visible_width;
615
616 if current_cursor_primary_x < visible_range_start {
617 offset_x = current_cursor_primary_x;
618 } else if current_cursor_primary_x > visible_range_end {
619 offset_x = current_cursor_primary_x - visible_width;
620 }
621 offset_x = offset_x.at_most(galley.size().x - visible_width).at_least(0.0);
622 state.singleline_offset = offset_x;
623 galley_pos.x -= offset_x;
624 } else {
625 state.singleline_offset = 0.0;
628 state.singleline_offset = text_draw_rect.left() - galley_pos.x;
630 }
631
632 if ui.is_rect_visible(text_draw_rect) {
634 let is_text_empty = self.text.char_len() == 0;
635 if is_text_empty && !self.hint_text.is_empty() {
636 let hint_text_color = ui.visuals().weak_text_color();
637 let hint_font_id = FontSelection::default();
638 let hint_galley = self.hint_text.clone().into_galley(
639 ui,
640 Some(TextWrapMode::Wrap),
641 text_draw_rect.width(),
642 hint_font_id,
643 );
644 let hint_galley_pos =
645 self.align.align_size_within_rect(hint_galley.size(), text_draw_rect).min;
646 ui.painter_at(text_draw_rect)
647 .galley(hint_galley_pos, hint_galley, hint_text_color);
648 }
649
650 let mut galley_for_paint = galley.clone();
651 if ui.memory(|mem| mem.has_focus(id)) {
652 if let Some(cursor_range_for_sel) = state.cursor.range(&galley_for_paint) {
653 text_selection::visuals::paint_text_selection(
654 &mut galley_for_paint,
655 ui.visuals(),
656 &cursor_range_for_sel,
657 None,
658 );
659 }
660 }
661 ui.painter_at(text_draw_rect).galley(galley_pos, galley_for_paint, text_color);
662
663 if self.interactive && ui.memory(|mem| mem.has_focus(id)) {
665 if let Some(cursor_range_for_cursor_paint) = state.cursor.range(&galley) {
666 let primary_cursor_rect_ui = text_selection::text_cursor_state::cursor_rect(
668 &galley,
669 &cursor_range_for_cursor_paint.primary,
670 row_height,
671 )
672 .translate(galley_pos.to_vec2());
673
674 if response.changed() {
675 ui.scroll_to_rect(
677 primary_cursor_rect_ui.expand(self.margin.sum().y / 2.0),
678 None,
679 );
680 }
681
682 if ui.ctx().input(|i| i.focused) {
683 let time_since_last_interaction =
685 ui.input(|i| i.time) - state.last_interaction_time;
686 text_selection::visuals::paint_text_cursor(
687 ui,
688 &ui.painter_at(text_draw_rect.expand(1.0)), primary_cursor_rect_ui,
690 time_since_last_interaction,
691 );
692 }
693 let to_global =
695 ui.ctx().layer_transform_to_global(ui.layer_id()).unwrap_or_default();
696 ui.ctx().output_mut(|o| {
697 o.ime = Some(output::IMEOutput {
698 purpose: if self.password {
699 IMEPurpose::Password
700 } else {
701 IMEPurpose::Normal
702 },
703 rect: to_global * text_draw_rect,
704 cursor_rect: to_global * primary_cursor_rect_ui,
705 should_interrupt_composition: false,
706 });
707 });
708 }
709 }
710 }
711
712 state.clone().store(ui.ctx(), id);
713
714 response.widget_info(|| {
728 WidgetInfo::text_edit(
729 ui.is_enabled(),
730 String::new(),
731 String::new(),
732 String::new(),
733 )
734 });
735
736 let output = SecureTextEditOutput {
737 response,
738 state,
739 cursor_range: cursor_range_after_events,
740 };
741
742 let visuals = self.visuals.take();
743 (output, visuals)
744 }
745}
746
747impl<'a> Widget for SecureTextEdit<'a> {
748 fn ui(self, ui: &mut Ui) -> Response {
749 self.show(ui).response
750 }
751}
752
753#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
754fn secure_text_edit_events(
755 ui: &Ui,
756 state: &mut SecureTextEditState,
757 text: &mut dyn TextBuffer,
758 initial_galley: &Arc<Galley>,
759 id: Id,
760 multiline: bool,
761 password: bool,
762 default_cursor_range: CCursorRange,
763 owns_ime_events: bool,
764 char_limit: usize,
765 event_filter: EventFilter,
766 return_key: Option<KeyboardShortcut>,
767 font_id: &FontId,
768 text_color: Color32,
769 wrap_width: f32,
770 text_align_horizontal: Align,
771) -> (bool, CCursorRange, Arc<Galley>) {
772 let os = ui.ctx().os();
773 let mut current_galley = initial_galley.clone();
774 let mut cursor_range = state.cursor.range(¤t_galley).unwrap_or(default_cursor_range);
775 let mut text_changed_in_total = false;
776
777 let events_filtered = ui.input(|i| i.filtered_events(&event_filter));
778
779 for event in events_filtered {
780 let current_char_len_before_event = text.char_len();
781 let mut text_mutated_this_event = false;
782
783 if cursor_range.on_event(os, &event, ¤t_galley, id) {
785 state.last_interaction_time = ui.input(|i| i.time);
786 continue;
787 }
788
789 let new_ccursor_range_opt: Option<CCursorRange> = match event {
790 Event::Copy => None,
792 Event::Cut => None,
793 #[cfg_attr(not(feature = "secure-types"), allow(unused_mut))]
794 Event::Paste(mut text_to_paste) => {
795 if !text_to_paste.is_empty() {
796 let [min, max] = cursor_range.sorted_cursors();
797 let selection_char_len = max.index - min.index;
798 text.delete_text_char_range(min.index.0..max.index.0);
799
800 let space_available = char_limit.saturating_sub(
801 current_char_len_before_event.saturating_sub(selection_char_len.0),
802 );
803 let mut final_text_to_paste = if text_to_paste.chars().count() > space_available {
804 text_to_paste.chars().take(space_available).collect::<String>()
805 } else {
806 text_to_paste.clone()
807 };
808
809 let mut current_ccursor = min;
810 let chars_inserted =
811 text.insert_text_at_char_idx(current_ccursor.index.0, &final_text_to_paste);
812 current_ccursor.index += chars_inserted;
813 text_mutated_this_event = true; #[cfg(feature = "secure-types")]
816 {
817 text_to_paste.zeroize();
818 final_text_to_paste.zeroize();
819 }
820
821 Some(text::CCursorRange::one(current_ccursor))
822 } else {
823 None
824 }
825 }
826 #[cfg_attr(not(feature = "secure-types"), allow(unused_mut))]
827 Event::Text(mut text_to_insert) => {
828 if !text_to_insert.is_empty() && text_to_insert != "\n" && text_to_insert != "\r" {
829 let [min, max] = cursor_range.sorted_cursors();
830 let selection_char_len = max.index - min.index;
831 text.delete_text_char_range(min.index.0..max.index.0);
832
833 let space_available = char_limit.saturating_sub(
834 current_char_len_before_event.saturating_sub(selection_char_len.0),
835 );
836 let mut final_text_to_insert = if text_to_insert.chars().count() > space_available {
837 text_to_insert.chars().take(space_available).collect::<String>()
838 } else {
839 text_to_insert.clone()
840 };
841
842 let mut current_ccursor = min;
843 let chars_inserted =
844 text.insert_text_at_char_idx(current_ccursor.index.0, &final_text_to_insert);
845 current_ccursor.index += chars_inserted;
846 text_mutated_this_event = true;
847
848 #[cfg(feature = "secure-types")]
849 {
850 text_to_insert.zeroize();
851 final_text_to_insert.zeroize();
852 }
853 Some(text::CCursorRange::one(current_ccursor))
854 } else {
855 None
856 }
857 }
858 Event::Key {
859 key: Key::Enter,
860 pressed: true,
861 modifiers,
862 ..
863 } if return_key.is_some_and(|rk| {
864 Key::Enter == rk.logical_key && modifiers.matches_logically(rk.modifiers)
865 }) =>
866 {
867 if multiline {
868 let [min, max] = cursor_range.sorted_cursors();
869 let selection_char_len = max.index - min.index;
870 text.delete_text_char_range(min.index.0..max.index.0);
871
872 let current_len_after_delete =
873 current_char_len_before_event.saturating_sub(selection_char_len.0);
874 let space_available = char_limit.saturating_sub(current_len_after_delete);
875
876 if space_available > 0 {
877 let mut current_ccursor = min;
878 let chars_inserted = text.insert_text_at_char_idx(current_ccursor.index.0, "\n");
879 current_ccursor.index += chars_inserted;
880 text_mutated_this_event = true; Some(text::CCursorRange::one(current_ccursor))
882 } else {
883 None
884 }
885 } else {
886 ui.memory_mut(|mem| mem.surrender_focus(id));
887 None
888 }
889 }
890 Event::Key {
891 key: Key::Backspace,
892 pressed: true,
893 ..
894 } => {
895 let [min, max] = cursor_range.sorted_cursors();
897 let mut new_cursor_idx = min.index;
898 if min == max {
899 if min.index.0 > 0 {
901 text.delete_text_char_range(min.index.0 - 1..min.index.0);
902 new_cursor_idx = min.index - 1;
903 text_mutated_this_event = true;
904 }
905 } else {
906 text.delete_text_char_range(min.index.0..max.index.0);
908 text_mutated_this_event = true;
910 }
911 if text_mutated_this_event {
912 Some(text::CCursorRange::one(text::CCursor::new(
913 new_cursor_idx,
914 )))
915 } else {
916 None
917 }
918 }
919 Event::Key {
920 key: Key::Delete,
921 pressed: true,
922 ..
923 } => {
924 let [min, max] = cursor_range.sorted_cursors();
926 if min == max {
927 if min.index < current_char_len_before_event.into() {
928 text.delete_text_char_range(min.index.0..min.index.0 + 1);
930 text_mutated_this_event = true;
931 }
932 } else {
933 text.delete_text_char_range(min.index.0..max.index.0);
934 text_mutated_this_event = true;
935 }
936 if text_mutated_this_event {
937 Some(text::CCursorRange::one(min))
938 } else {
939 None
940 }
941 }
942 Event::Key {
943 key: Key::Tab,
944 pressed: true,
945 modifiers,
946 ..
947 } if multiline && event_filter.tab => {
948 let [min, _max] = cursor_range.sorted_cursors();
949 let mut current_ccursor = min;
950 if modifiers.shift {
951 } else {
952 let space_available = char_limit.saturating_sub(current_char_len_before_event);
953 if space_available > 0 {
954 let chars_inserted = text.insert_text_at_char_idx(current_ccursor.index.0, "\t");
956 current_ccursor.index += chars_inserted;
957 text_mutated_this_event = true;
958 }
959 }
960 if text_mutated_this_event {
961 Some(text::CCursorRange::one(current_ccursor))
962 } else {
963 None
964 }
965 }
966 Event::Ime(ime_event) if owns_ime_events => {
967 match ime_event {
973 #[allow(deprecated)]
974 ImeEvent::Enabled | ImeEvent::Disabled => None,
975 ImeEvent::Preedit {
976 text: ref composition_text,
977 ..
978 }
979 | ImeEvent::Commit(ref composition_text)
980 if composition_text.is_empty() && !state.ime_enabled =>
981 {
982 None
983 }
984 ImeEvent::Preedit {
985 text: ref composition_text,
986 ..
987 }
988 | ImeEvent::Commit(ref composition_text)
989 if composition_text == "\n" || composition_text == "\r" =>
990 {
991 None
992 }
993 #[cfg_attr(not(feature = "secure-types"), allow(unused_mut))]
994 ImeEvent::Preedit {
995 text: mut preedit_text,
996 active_range_chars: _,
997 } => {
998 let [min, max] = cursor_range.sorted_cursors();
999 text.delete_text_char_range(min.index.0..max.index.0);
1000 let mut c = min;
1001
1002 if preedit_text.is_empty() {
1003 state.ime_enabled = false;
1004 text_mutated_this_event = true;
1005 Some(text::CCursorRange::one(c))
1006 } else {
1007 let space_available = char_limit.saturating_sub(text.char_len());
1008 let mut final_text = if preedit_text.chars().count() > space_available {
1009 preedit_text.chars().take(space_available).collect::<String>()
1010 } else {
1011 preedit_text.clone()
1012 };
1013 let inserted = text.insert_text_at_char_idx(c.index.0, &final_text);
1014 c.index += inserted;
1015 state.ime_enabled = true;
1016 state.ime_cursor_range = text::CCursorRange::two(min, c);
1017 text_mutated_this_event = true;
1018
1019 #[cfg(feature = "secure-types")]
1020 {
1021 preedit_text.zeroize();
1022 final_text.zeroize();
1023 }
1024
1025 Some(text::CCursorRange::two(min, c))
1026 }
1027 }
1028 #[cfg_attr(not(feature = "secure-types"), allow(unused_mut))]
1029 ImeEvent::Commit(mut commit_text) => {
1030 state.ime_enabled = false;
1031 let [min, max] = cursor_range.sorted_cursors();
1032 text.delete_text_char_range(min.index.0..max.index.0);
1033 let mut c = min;
1034
1035 if !commit_text.is_empty() {
1036 let space_available = char_limit.saturating_sub(text.char_len());
1037 let mut final_text = if commit_text.chars().count() > space_available {
1038 commit_text.chars().take(space_available).collect::<String>()
1039 } else {
1040 commit_text.clone()
1041 };
1042 let inserted = text.insert_text_at_char_idx(c.index.0, &final_text);
1043 c.index += inserted;
1044
1045 #[cfg(feature = "secure-types")]
1046 final_text.zeroize();
1047 }
1048 text_mutated_this_event = true;
1049
1050 #[cfg(feature = "secure-types")]
1051 commit_text.zeroize();
1052
1053 Some(text::CCursorRange::one(c))
1054 }
1055 ImeEvent::DeleteSurrounding {
1056 before_chars,
1057 after_chars,
1058 } => {
1059 let [min, max] = cursor_range.sorted_cursors();
1060 let mut new_range = cursor_range;
1061 let mut mutated = false;
1062
1063 if after_chars > 0 {
1064 let end = (max.index.0 + after_chars).min(text.char_len());
1065 if end > max.index.0 {
1066 text.delete_text_char_range(max.index.0..end);
1067 mutated = true;
1068 }
1069 }
1070 if before_chars > 0 {
1071 let start = min.index.0.saturating_sub(before_chars);
1072 if start < min.index.0 {
1073 let deleted = min.index.0 - start;
1074 text.delete_text_char_range(start..min.index.0);
1075 new_range.primary.index =
1076 (new_range.primary.index.0.saturating_sub(deleted)).into();
1077 new_range.secondary.index =
1078 (new_range.secondary.index.0.saturating_sub(deleted)).into();
1079 mutated = true;
1080 }
1081 }
1082
1083 if mutated {
1084 text_mutated_this_event = true;
1085 Some(new_range)
1086 } else {
1087 None
1088 }
1089 }
1090 }
1091 }
1092 _ => None,
1093 };
1094
1095 if text_mutated_this_event {
1096 text_changed_in_total = true;
1097
1098 let display_text_for_layout = if password {
1101 std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR)
1102 .take(text.char_len())
1103 .collect::<String>()
1104 } else {
1105 text.to_string() };
1107
1108 let mut job = if multiline {
1109 LayoutJob::simple(
1110 display_text_for_layout,
1111 font_id.clone(),
1112 text_color,
1113 wrap_width,
1114 )
1115 } else {
1116 LayoutJob::simple_singleline(
1117 display_text_for_layout,
1118 font_id.clone(),
1119 text_color,
1120 )
1121 };
1122
1123 job.halign = text_align_horizontal;
1124 current_galley = ui.fonts_mut(|f| f.layout_job(job));
1125 }
1126
1127 if let Some(new_range) = new_ccursor_range_opt {
1128 state.last_interaction_time = ui.input(|i| i.time);
1129 cursor_range = new_range;
1130 }
1131 state.cursor.set_char_range(Some(cursor_range));
1132 }
1133
1134 (
1135 text_changed_in_total,
1136 cursor_range,
1137 current_galley,
1138 )
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143 use super::*;
1144 use egui::{Context, Event, Id, ImeEvent, RawInput};
1145
1146 fn run_pass(ctx: &Context, text: &mut String, id: Id, events: Vec<Event>) {
1147 let output = ctx.run_ui(
1148 RawInput {
1149 events,
1150 ..Default::default()
1151 },
1152 |ui| {
1153 ui.ctx().memory_mut(|mem| mem.request_focus(id));
1154 ui.add(SecureTextEdit::singleline(text).id(id));
1155 },
1156 );
1157 output.drop_without_applying_deltas();
1158 }
1159
1160 fn ime_preedit(text: &str) -> Event {
1161 Event::Ime(ImeEvent::Preedit {
1162 text: text.to_owned(),
1163 active_range_chars: None,
1164 })
1165 }
1166
1167 fn ime_commit(text: &str) -> Event {
1168 Event::Ime(ImeEvent::Commit(text.to_owned()))
1169 }
1170
1171 fn gnome_wayland_keystroke(ch: char) -> Vec<Event> {
1173 let s = ch.to_string();
1174 vec![ime_preedit(&s), ime_commit(&s), ime_preedit("")]
1175 }
1176
1177 #[test]
1178 fn gnome_wayland_ime_types_forwards_not_in_reverse() {
1179 let ctx = Context::default();
1180 let id = Id::new("secure_ime_gnome");
1181 let mut text = String::new();
1182
1183 #[allow(deprecated)]
1184 run_pass(
1185 &ctx,
1186 &mut text,
1187 id,
1188 vec![Event::Ime(ImeEvent::Enabled)],
1189 );
1190
1191 for ch in ['h', 'e', 'l', 'l', 'o'] {
1192 run_pass(&ctx, &mut text, id, gnome_wayland_keystroke(ch));
1193 }
1194
1195 assert_eq!(
1196 text, "hello",
1197 "IME keystrokes must append, not insert at 0"
1198 );
1199 }
1200
1201 #[test]
1202 fn plain_text_events_type_forwards() {
1203 let ctx = Context::default();
1204 let id = Id::new("secure_text_events");
1205 let mut text = String::new();
1206
1207 for ch in ['h', 'e', 'l', 'l', 'o'] {
1208 run_pass(
1209 &ctx,
1210 &mut text,
1211 id,
1212 vec![Event::Text(ch.to_string())],
1213 );
1214 }
1215
1216 assert_eq!(text, "hello");
1217 }
1218
1219 #[test]
1220 fn empty_ime_preedit_without_composition_does_not_reset_cursor() {
1221 let ctx = Context::default();
1222 let id = Id::new("secure_empty_preedit");
1223 let mut text = String::new();
1224
1225 run_pass(
1226 &ctx,
1227 &mut text,
1228 id,
1229 vec![Event::Text("ab".to_owned())],
1230 );
1231 run_pass(&ctx, &mut text, id, vec![ime_preedit("")]);
1232 run_pass(
1233 &ctx,
1234 &mut text,
1235 id,
1236 vec![Event::Text("c".to_owned())],
1237 );
1238
1239 assert_eq!(
1240 text, "abc",
1241 "spurious empty Preedit must not move the cursor to the start"
1242 );
1243 }
1244
1245 #[test]
1246 fn ime_composition_updates_replace_preedit_then_commit() {
1247 let ctx = Context::default();
1248 let id = Id::new("secure_ime_compose");
1249 let mut text = String::new();
1250
1251 run_pass(&ctx, &mut text, id, vec![ime_preedit("n")]);
1252 assert_eq!(text, "n");
1253 run_pass(&ctx, &mut text, id, vec![ime_preedit("ni")]);
1254 assert_eq!(text, "ni");
1255 run_pass(&ctx, &mut text, id, vec![ime_preedit("に")]);
1256 assert_eq!(text, "に");
1257 run_pass(&ctx, &mut text, id, vec![ime_commit("に")]);
1258 assert_eq!(text, "に");
1259 run_pass(&ctx, &mut text, id, gnome_wayland_keystroke('!'));
1260 assert_eq!(text, "に!");
1261 }
1262}