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#[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 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); 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 let display_text_cow = if self.password {
403 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()) };
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 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 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 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 let mut cursor_range_after_events = None;
502 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 ¤t_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 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 state.singleline_offset = 0.0;
568 state.singleline_offset = text_draw_rect.left() - galley_pos.x;
570 }
571
572 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 if self.interactive && ui.memory(|mem| mem.has_focus(id)) {
605 if let Some(cursor_range_for_cursor_paint) = state.cursor.range(&galley) {
606 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 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 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)), primary_cursor_rect_ui,
630 time_since_last_interaction,
631 );
632 }
633 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 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 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(¤t_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 if cursor_range.on_event(os, &event, ¤t_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 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; #[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; 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 let [min, max] = cursor_range.sorted_cursors();
839 let mut new_cursor_idx = min.index;
840 if min == max {
841 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 text.delete_text_char_range(min.index..max.index);
850 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 let [min, max] = cursor_range.sorted_cursors();
868 if min == max {
869 if min.index < current_char_len_before_event {
870 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 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(); text.delete_text_char_range(min_ime.index..max_ime.index);
918 let mut c = min_ime; 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; 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 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() };
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 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}