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::{fmt::Debug, hash::Hash, 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.into());
54 self.insert_str(byte_idx.into(), 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.into());
66 let byte_end = byte_index_from_char_index(self.as_str(), char_range.end.into());
67
68 self.drain(byte_start.0..byte_end.0);
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 Hash + Debug) -> Self {
220 self.id_salt(id_source)
221 }
222
223 pub fn id_salt(mut self, id_salt: impl Hash + Debug) -> 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.set_intrinsic_size(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 should_interrupt_composition: false,
641 });
642 });
643 }
644 }
645 }
646
647 if state.ime_enabled && (response.gained_focus() || response.lost_focus()) {
649 state.ime_enabled = false;
650 if let Some(mut ccursor_range) = state.cursor.char_range() {
651 ccursor_range.secondary.index = ccursor_range.primary.index;
652 state.cursor.set_char_range(Some(ccursor_range));
653 }
654 ui.input_mut(|i| i.events.retain(|e| !matches!(e, Event::Ime(_))));
655 }
656
657 state.clone().store(ui.ctx(), id);
658
659 response.widget_info(|| {
673 WidgetInfo::text_edit(
674 ui.is_enabled(),
675 String::new(),
676 String::new(),
677 String::new(),
678 )
679 });
680
681 let output = SecureTextEditOutput {
682 response,
683 state,
684 cursor_range: cursor_range_after_events,
685 };
686
687 let visuals = self.visuals.take();
688 (output, visuals)
689 }
690}
691
692impl<'a> Widget for SecureTextEdit<'a> {
693 fn ui(self, ui: &mut Ui) -> Response {
694 self.show(ui).response
695 }
696}
697
698#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
699fn secure_text_edit_events(
700 ui: &Ui,
701 state: &mut SecureTextEditState,
702 text: &mut dyn TextBuffer,
703 initial_galley: &Arc<Galley>,
704 id: Id,
705 multiline: bool,
706 password: bool,
707 default_cursor_range: CCursorRange,
708 char_limit: usize,
709 event_filter: EventFilter,
710 return_key: Option<KeyboardShortcut>,
711 font_id: &FontId,
712 text_color: Color32,
713 wrap_width: f32,
714 text_align_horizontal: Align,
715) -> (bool, CCursorRange, Arc<Galley>) {
716 let os = ui.ctx().os();
717 let mut current_galley = initial_galley.clone();
718 let mut cursor_range = state.cursor.range(¤t_galley).unwrap_or(default_cursor_range);
719 let mut text_changed_in_total = false;
720
721 let mut events_filtered = ui.input(|i| i.filtered_events(&event_filter));
722 if state.ime_enabled {
723 events_filtered.sort_by_key(|e| !matches!(e, Event::Ime(_)));
724 }
725
726 for event in events_filtered {
727 let current_char_len_before_event = text.char_len();
728 let mut text_mutated_this_event = false;
729
730 if cursor_range.on_event(os, &event, ¤t_galley, id) {
732 state.last_interaction_time = ui.input(|i| i.time);
733 continue;
734 }
735
736 let new_ccursor_range_opt: Option<CCursorRange> = match event {
737 Event::Copy => None,
739 Event::Cut => None,
740 #[cfg_attr(not(feature = "secure-types"), allow(unused_mut))]
741 Event::Paste(mut text_to_paste) => {
742 if !text_to_paste.is_empty() {
743 let [min, max] = cursor_range.sorted_cursors();
744 let selection_char_len = max.index - min.index;
745 text.delete_text_char_range(min.index.0..max.index.0);
746
747 let space_available = char_limit.saturating_sub(
748 current_char_len_before_event.saturating_sub(selection_char_len.0),
749 );
750 let mut final_text_to_paste = if text_to_paste.chars().count() > space_available {
751 text_to_paste.chars().take(space_available).collect::<String>()
752 } else {
753 text_to_paste.clone()
754 };
755
756 let mut current_ccursor = min;
757 let chars_inserted =
758 text.insert_text_at_char_idx(current_ccursor.index.0, &final_text_to_paste);
759 current_ccursor.index += chars_inserted;
760 text_mutated_this_event = true; #[cfg(feature = "secure-types")]
763 {
764 text_to_paste.zeroize();
765 final_text_to_paste.zeroize();
766 }
767
768 Some(text::CCursorRange::one(current_ccursor))
769 } else {
770 None
771 }
772 }
773 #[cfg_attr(not(feature = "secure-types"), allow(unused_mut))]
774 Event::Text(mut text_to_insert) => {
775 if !text_to_insert.is_empty() && text_to_insert != "\n" && text_to_insert != "\r" {
776 let [min, max] = cursor_range.sorted_cursors();
777 let selection_char_len = max.index - min.index;
778 text.delete_text_char_range(min.index.0..max.index.0);
779
780 let space_available = char_limit.saturating_sub(
781 current_char_len_before_event.saturating_sub(selection_char_len.0),
782 );
783 let mut final_text_to_insert = if text_to_insert.chars().count() > space_available {
784 text_to_insert.chars().take(space_available).collect::<String>()
785 } else {
786 text_to_insert.clone()
787 };
788
789 let mut current_ccursor = min;
790 let chars_inserted =
791 text.insert_text_at_char_idx(current_ccursor.index.0, &final_text_to_insert);
792 current_ccursor.index += chars_inserted;
793 text_mutated_this_event = true;
794
795 #[cfg(feature = "secure-types")]
796 {
797 text_to_insert.zeroize();
798 final_text_to_insert.zeroize();
799 }
800 Some(text::CCursorRange::one(current_ccursor))
801 } else {
802 None
803 }
804 }
805 Event::Key {
806 key: Key::Enter,
807 pressed: true,
808 modifiers,
809 ..
810 } if return_key.is_some_and(|rk| {
811 Key::Enter == rk.logical_key && modifiers.matches_logically(rk.modifiers)
812 }) =>
813 {
814 if multiline {
815 let [min, max] = cursor_range.sorted_cursors();
816 let selection_char_len = max.index - min.index;
817 text.delete_text_char_range(min.index.0..max.index.0);
818
819 let current_len_after_delete =
820 current_char_len_before_event.saturating_sub(selection_char_len.0);
821 let space_available = char_limit.saturating_sub(current_len_after_delete);
822
823 if space_available > 0 {
824 let mut current_ccursor = min;
825 let chars_inserted = text.insert_text_at_char_idx(current_ccursor.index.0, "\n");
826 current_ccursor.index += chars_inserted;
827 text_mutated_this_event = true; Some(text::CCursorRange::one(current_ccursor))
829 } else {
830 None
831 }
832 } else {
833 ui.memory_mut(|mem| mem.surrender_focus(id));
834 None
835 }
836 }
837 Event::Key {
838 key: Key::Backspace,
839 pressed: true,
840 ..
841 } => {
842 let [min, max] = cursor_range.sorted_cursors();
844 let mut new_cursor_idx = min.index;
845 if min == max {
846 if min.index.0 > 0 {
848 text.delete_text_char_range(min.index.0 - 1..min.index.0);
849 new_cursor_idx = min.index - 1;
850 text_mutated_this_event = true;
851 }
852 } else {
853 text.delete_text_char_range(min.index.0..max.index.0);
855 text_mutated_this_event = true;
857 }
858 if text_mutated_this_event {
859 Some(text::CCursorRange::one(text::CCursor::new(
860 new_cursor_idx,
861 )))
862 } else {
863 None
864 }
865 }
866 Event::Key {
867 key: Key::Delete,
868 pressed: true,
869 ..
870 } => {
871 let [min, max] = cursor_range.sorted_cursors();
873 if min == max {
874 if min.index < current_char_len_before_event.into() {
875 text.delete_text_char_range(min.index.0..min.index.0 + 1);
877 text_mutated_this_event = true;
878 }
879 } else {
880 text.delete_text_char_range(min.index.0..max.index.0);
881 text_mutated_this_event = true;
882 }
883 if text_mutated_this_event {
884 Some(text::CCursorRange::one(min))
885 } else {
886 None
887 }
888 }
889 Event::Key {
890 key: Key::Tab,
891 pressed: true,
892 modifiers,
893 ..
894 } if multiline && event_filter.tab => {
895 let [min, _max] = cursor_range.sorted_cursors();
896 let mut current_ccursor = min;
897 if modifiers.shift {
898 } else {
899 let space_available = char_limit.saturating_sub(current_char_len_before_event);
900 if space_available > 0 {
901 let chars_inserted = text.insert_text_at_char_idx(current_ccursor.index.0, "\t");
903 current_ccursor.index += chars_inserted;
904 text_mutated_this_event = true;
905 }
906 }
907 if text_mutated_this_event {
908 Some(text::CCursorRange::one(current_ccursor))
909 } else {
910 None
911 }
912 }
913 Event::Ime(ime_event) => {
914 match ime_event {
915 #[allow(deprecated)]
916 ImeEvent::Enabled => {
917 state.ime_enabled = true;
918 state.ime_cursor_range = cursor_range;
919 None
920 }
921 #[allow(deprecated)]
922 ImeEvent::Disabled => {
923 state.ime_enabled = false;
924 None
925 }
926 #[cfg_attr(not(feature = "secure-types"), allow(unused_mut))]
927 ImeEvent::Preedit {
928 text: mut preedit_text,
929 active_range_chars: _,
930 } => {
931 let [min_ime, max_ime] = state.ime_cursor_range.sorted_cursors(); text.delete_text_char_range(min_ime.index.0..max_ime.index.0);
933 let mut c = min_ime; let inserted = text.insert_text_at_char_idx(c.index.0, &preedit_text);
935 c.index += inserted;
936 text_mutated_this_event = true;
937
938 #[cfg(feature = "secure-types")]
939 preedit_text.zeroize();
940
941 Some(text::CCursorRange::two(min_ime, c))
942 }
943 #[cfg_attr(not(feature = "secure-types"), allow(unused_mut))]
944 ImeEvent::Commit(mut commit_text) => {
945 state.ime_enabled = false; let [min_commit, max_commit] = cursor_range.sorted_cursors();
947 text.delete_text_char_range(min_commit.index.0..max_commit.index.0);
948 let mut c = min_commit;
949 let inserted = text.insert_text_at_char_idx(c.index.0, &commit_text);
950 c.index += inserted;
951 text_mutated_this_event = true;
952
953 #[cfg(feature = "secure-types")]
954 commit_text.zeroize();
955
956 Some(text::CCursorRange::one(c))
957 }
958 }
959 }
960 _ => None,
961 };
962
963 if text_mutated_this_event {
964 text_changed_in_total = true;
965
966 let display_text_for_layout = if password {
969 std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR)
970 .take(text.char_len())
971 .collect::<String>()
972 } else {
973 text.to_string() };
975
976 let mut job = if multiline {
977 LayoutJob::simple(
978 display_text_for_layout,
979 font_id.clone(),
980 text_color,
981 wrap_width,
982 )
983 } else {
984 LayoutJob::simple_singleline(
985 display_text_for_layout,
986 font_id.clone(),
987 text_color,
988 )
989 };
990
991 job.halign = text_align_horizontal;
992 current_galley = ui.fonts_mut(|f| f.layout_job(job));
993 }
994
995 state.cursor.set_char_range(new_ccursor_range_opt);
997 if let Some(new_range) = new_ccursor_range_opt {
998 state.last_interaction_time = ui.input(|i| i.time);
999 cursor_range = new_range;
1000 }
1001 }
1002
1003 (
1004 text_changed_in_total,
1005 cursor_range,
1006 current_galley,
1007 )
1008}