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;
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#[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 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); 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 let galley: Arc<Galley> = self.text.unlock_str(|text_slice| {
310 let display_text_cow = if self.password {
311 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()) };
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 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 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 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 let mut cursor_range_after_events = None;
410 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 ¤t_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 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 state.singleline_offset = 0.0;
476 state.singleline_offset = text_draw_rect.left() - galley_pos.x;
478 }
479
480 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 if self.interactive && ui.memory(|mem| mem.has_focus(id)) {
513 if let Some(cursor_range_for_cursor_paint) = state.cursor.range(&galley) {
514 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 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 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)), primary_cursor_rect_ui,
538 time_since_last_interaction,
539 );
540 }
541 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 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 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(¤t_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 if cursor_range.on_event(os, event, ¤t_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 Event::Copy => None,
643 Event::Cut => None,
644 Event::Paste(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 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; Some(text::CCursorRange::one(current_ccursor))
664 } else {
665 None
666 }
667 }
668 Event::Text(text_to_insert) => {
669 if !text_to_insert.is_empty() && text_to_insert != "\n" && text_to_insert != "\r" {
670 let [min, max] = cursor_range.sorted_cursors();
671 let selection_char_len = max.index - min.index;
672 secure_text.delete_text_char_range(min.index..max.index);
673
674 let space_available = char_limit
675 .saturating_sub(current_char_len_before_event.saturating_sub(selection_char_len));
676 let final_text_to_insert = if text_to_insert.chars().count() > space_available {
677 text_to_insert.chars().take(space_available).collect::<String>()
678 } else {
679 text_to_insert.clone()
680 };
681
682 let mut current_ccursor = min;
683 let chars_inserted =
684 secure_text.insert_text_at_char_idx(current_ccursor.index, &final_text_to_insert);
685 current_ccursor.index += chars_inserted;
686 text_mutated_this_event = true;
687 Some(text::CCursorRange::one(current_ccursor))
688 } else {
689 None
690 }
691 }
692 Event::Key {
693 key: Key::Enter,
694 pressed: true,
695 modifiers,
696 ..
697 } if return_key.is_some_and(|rk| {
698 Key::Enter == rk.logical_key && modifiers.matches_logically(rk.modifiers)
699 }) =>
700 {
701 if multiline {
702 let [min, max] = cursor_range.sorted_cursors();
703 let selection_char_len = max.index - min.index;
704 secure_text.delete_text_char_range(min.index..max.index);
705
706 let current_len_after_delete =
707 current_char_len_before_event.saturating_sub(selection_char_len);
708 let space_available = char_limit.saturating_sub(current_len_after_delete);
709
710 if space_available > 0 {
711 let mut current_ccursor = min;
712 let chars_inserted =
713 secure_text.insert_text_at_char_idx(current_ccursor.index, "\n");
714 current_ccursor.index += chars_inserted;
715 text_mutated_this_event = true; Some(text::CCursorRange::one(current_ccursor))
717 } else {
718 None
719 }
720 } else {
721 ui.memory_mut(|mem| mem.surrender_focus(id));
722 None
723 }
724 }
725 Event::Key {
726 key: Key::Backspace,
727 pressed: true,
728 ..
729 } => {
730 let [min, max] = cursor_range.sorted_cursors();
732 let mut new_cursor_idx = min.index;
733 if min == max {
734 if min.index > 0 {
736 secure_text.delete_text_char_range(min.index - 1..min.index);
737 new_cursor_idx = min.index - 1;
738 text_mutated_this_event = true;
739 }
740 } else {
741 secure_text.delete_text_char_range(min.index..max.index);
743 text_mutated_this_event = true;
745 }
746 if text_mutated_this_event {
747 Some(text::CCursorRange::one(text::CCursor::new(
748 new_cursor_idx,
749 )))
750 } else {
751 None
752 }
753 }
754 Event::Key {
755 key: Key::Delete,
756 pressed: true,
757 ..
758 } => {
759 let [min, max] = cursor_range.sorted_cursors();
761 if min == max {
762 if min.index < current_char_len_before_event {
763 secure_text.delete_text_char_range(min.index..min.index + 1);
765 text_mutated_this_event = true;
766 }
767 } else {
768 secure_text.delete_text_char_range(min.index..max.index);
769 text_mutated_this_event = true;
770 }
771 if text_mutated_this_event {
772 Some(text::CCursorRange::one(min))
773 } else {
774 None
775 }
776 }
777 Event::Key {
778 key: Key::Tab,
779 pressed: true,
780 modifiers,
781 ..
782 } if multiline && event_filter.tab => {
783 let [min, _max] = cursor_range.sorted_cursors();
784 let mut current_ccursor = min;
785 if modifiers.shift {
786 } else {
787 let space_available = char_limit.saturating_sub(current_char_len_before_event);
788 if space_available > 0 {
789 let chars_inserted =
791 secure_text.insert_text_at_char_idx(current_ccursor.index, "\t");
792 current_ccursor.index += chars_inserted;
793 text_mutated_this_event = true;
794 }
795 }
796 if text_mutated_this_event {
797 Some(text::CCursorRange::one(current_ccursor))
798 } else {
799 None
800 }
801 }
802 Event::Ime(ime_event) => {
803 match ime_event {
804 ImeEvent::Enabled => {
805 state.ime_enabled = true;
806 state.ime_cursor_range = cursor_range;
807 None
808 }
809 ImeEvent::Preedit(preedit_text) => {
810 let [min_ime, max_ime] = state.ime_cursor_range.sorted_cursors(); secure_text.delete_text_char_range(min_ime.index..max_ime.index);
812 let mut c = min_ime; let inserted = secure_text.insert_text_at_char_idx(c.index, preedit_text);
814 c.index += inserted;
815 text_mutated_this_event = true;
816 Some(text::CCursorRange::two(min_ime, c))
817 }
818 ImeEvent::Commit(commit_text) => {
819 state.ime_enabled = false; let [min_commit, max_commit] = cursor_range.sorted_cursors();
821 secure_text.delete_text_char_range(min_commit.index..max_commit.index);
822 let mut c = min_commit;
823 let inserted = secure_text.insert_text_at_char_idx(c.index, commit_text);
824 c.index += inserted;
825 text_mutated_this_event = true;
826 Some(text::CCursorRange::one(c))
827 }
828 ImeEvent::Disabled => {
829 state.ime_enabled = false;
830 None
831 }
832 }
833 }
834 _ => None,
835 };
836
837 if text_mutated_this_event {
838 text_changed_in_total = true;
839
840 current_galley = secure_text.unlock_str(|text_slice| {
842 let display_text_for_layout = if password {
843 std::iter::repeat(epaint::text::PASSWORD_REPLACEMENT_CHAR)
844 .take(text_slice.chars().count())
845 .collect::<String>()
846 } else {
847 text_slice.to_owned() };
849 let mut job = if multiline {
850 LayoutJob::simple(
851 display_text_for_layout,
852 font_id.clone(),
853 text_color,
854 wrap_width,
855 )
856 } else {
857 LayoutJob::simple_singleline(
858 display_text_for_layout,
859 font_id.clone(),
860 text_color,
861 )
862 };
863 job.halign = text_align_horizontal;
864 ui.fonts_mut(|f| f.layout_job(job))
865 });
866 }
867
868 state.cursor.set_char_range(new_ccursor_range_opt);
870 if let Some(new_range) = new_ccursor_range_opt {
871 state.last_interaction_time = ui.input(|i| i.time);
872 cursor_range = new_range;
873 }
874 }
875
876 (
877 text_changed_in_total,
878 cursor_range,
879 current_galley,
880 )
881}