1use std::sync::Arc;
2
3use emath::{Rect, TSTransform};
4use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
5
6use crate::{
7 Align, Align2, AsIdSalt, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context,
8 CursorIcon, Event, EventFilter, FontSelection, Frame, IMEPurpose, Id, IdSalt, ImeEvent,
9 IntoAtoms, IntoSizedResult, Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response,
10 Sense, SizedAtomKind, TextBuffer, TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState,
11 epaint,
12 os::OperatingSystem,
13 output::OutputEvent,
14 response,
15 text_edit::state::TextEditCursorPurpose,
16 text_selection::{
17 self, CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection,
18 },
19 vec2,
20};
21
22use super::{TextEditOutput, TextEditState};
23
24type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>;
25
26#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
70pub struct TextEdit<'t> {
71 text: &'t mut dyn TextBuffer,
72 prefix: Atoms<'static>,
73 suffix: Atoms<'static>,
74 hint_text: Atoms<'static>,
75 id: Option<Id>,
76 id_salt: Option<IdSalt>,
77 font_selection: FontSelection,
78 text_color: Option<Color32>,
79 layouter: Option<LayouterFn<'t>>,
80 password: bool,
81 frame: Option<Frame>,
82 margin: Margin,
83 multiline: bool,
84 interactive: bool,
85 desired_width: Option<f32>,
86 desired_height_rows: usize,
87 event_filter: EventFilter,
88 cursor_at_end: bool,
89 min_size: Vec2,
90 align: Align2,
91 clip_text: bool,
92 char_limit: usize,
93 return_key: Option<KeyboardShortcut>,
94 background_color: Option<Color32>,
95}
96
97impl WidgetWithState for TextEdit<'_> {
98 type State = TextEditState;
99}
100
101impl TextEdit<'_> {
102 pub fn load_state(ctx: &Context, id: Id) -> Option<TextEditState> {
103 TextEditState::load(ctx, id)
104 }
105
106 pub fn store_state(ctx: &Context, id: Id, state: TextEditState) {
107 state.store(ctx, id);
108 }
109}
110
111impl<'t> TextEdit<'t> {
112 pub fn singleline(text: &'t mut dyn TextBuffer) -> Self {
114 Self {
115 desired_height_rows: 1,
116 multiline: false,
117 clip_text: true,
118 ..Self::multiline(text)
119 }
120 }
121
122 pub fn multiline(text: &'t mut dyn TextBuffer) -> Self {
124 Self {
125 text,
126 prefix: Default::default(),
127 suffix: Default::default(),
128 hint_text: Default::default(),
129 id: None,
130 id_salt: None,
131 font_selection: Default::default(),
132 text_color: None,
133 layouter: None,
134 password: false,
135 frame: None,
136 margin: Margin::symmetric(4, 2),
137 multiline: true,
138 interactive: true,
139 desired_width: None,
140 desired_height_rows: 4,
141 event_filter: EventFilter {
142 horizontal_arrows: true,
144 vertical_arrows: true,
145 tab: false, ..Default::default()
147 },
148 cursor_at_end: true,
149 min_size: Vec2::ZERO,
150 align: Align2::LEFT_TOP,
151 clip_text: false,
152 char_limit: usize::MAX,
153 return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)),
154 background_color: None,
155 }
156 }
157
158 pub fn code_editor(self) -> Self {
163 self.font(TextStyle::Monospace).lock_focus(true)
164 }
165
166 #[inline]
168 pub fn id(mut self, id: Id) -> Self {
169 self.id = Some(id);
170 self
171 }
172
173 #[inline]
175 pub fn id_source(self, id_salt: impl AsIdSalt) -> Self {
176 self.id_salt(id_salt)
177 }
178
179 #[inline]
181 pub fn id_salt(mut self, id_salt: impl AsIdSalt) -> Self {
182 self.id_salt = Some(IdSalt::new(id_salt));
183 self
184 }
185
186 #[inline]
209 pub fn hint_text(mut self, hint_text: impl IntoAtoms<'static>) -> Self {
210 self.hint_text = hint_text.into_atoms();
211 self
212 }
213
214 #[inline]
216 pub fn prefix(mut self, prefix: impl IntoAtoms<'static>) -> Self {
217 self.prefix = prefix.into_atoms();
218 self
219 }
220
221 #[inline]
223 pub fn suffix(mut self, suffix: impl IntoAtoms<'static>) -> Self {
224 self.suffix = suffix.into_atoms();
225 self
226 }
227
228 #[inline]
231 pub fn background_color(mut self, color: Color32) -> Self {
232 self.background_color = Some(color);
233 self
234 }
235
236 #[inline]
238 pub fn password(mut self, password: bool) -> Self {
239 self.password = password;
240 self
241 }
242
243 #[inline]
245 pub fn font(mut self, font_selection: impl Into<FontSelection>) -> Self {
246 self.font_selection = font_selection.into();
247 self
248 }
249
250 #[inline]
251 pub fn text_color(mut self, text_color: Color32) -> Self {
252 self.text_color = Some(text_color);
253 self
254 }
255
256 #[inline]
257 pub fn text_color_opt(mut self, text_color: Option<Color32>) -> Self {
258 self.text_color = text_color;
259 self
260 }
261
262 #[inline]
286 pub fn layouter(
287 mut self,
288 layouter: &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>,
289 ) -> Self {
290 self.layouter = Some(layouter);
291
292 self
293 }
294
295 #[inline]
299 pub fn interactive(mut self, interactive: bool) -> Self {
300 self.interactive = interactive;
301 self
302 }
303
304 #[inline]
306 pub fn frame(mut self, frame: Frame) -> Self {
307 self.frame = Some(frame);
308 self
309 }
310
311 #[inline]
313 pub fn margin(mut self, margin: impl Into<Margin>) -> Self {
314 self.margin = margin.into();
315 self
316 }
317
318 #[inline]
321 pub fn desired_width(mut self, desired_width: f32) -> Self {
322 self.desired_width = Some(desired_width);
323 self
324 }
325
326 #[inline]
330 pub fn desired_rows(mut self, desired_height_rows: usize) -> Self {
331 self.desired_height_rows = desired_height_rows;
332 self
333 }
334
335 #[inline]
341 pub fn lock_focus(mut self, tab_will_indent: bool) -> Self {
342 self.event_filter.tab = tab_will_indent;
343 self
344 }
345
346 #[inline]
357 pub fn event_filter(mut self, event_filter: EventFilter) -> Self {
358 self.event_filter = event_filter;
359 self
360 }
361
362 #[inline]
366 pub fn cursor_at_end(mut self, b: bool) -> Self {
367 self.cursor_at_end = b;
368 self
369 }
370
371 #[inline]
377 pub fn clip_text(mut self, b: bool) -> Self {
378 if !self.multiline {
380 self.clip_text = b;
381 }
382 self
383 }
384
385 #[inline]
389 pub fn char_limit(mut self, limit: usize) -> Self {
390 self.char_limit = limit;
391 self
392 }
393
394 #[inline]
396 pub fn horizontal_align(mut self, align: Align) -> Self {
397 self.align.0[0] = align;
398 self
399 }
400
401 #[inline]
403 pub fn vertical_align(mut self, align: Align) -> Self {
404 self.align.0[1] = align;
405 self
406 }
407
408 #[inline]
410 pub fn min_size(mut self, min_size: Vec2) -> Self {
411 self.min_size = min_size;
412 self
413 }
414
415 #[inline]
422 pub fn return_key(mut self, return_key: impl Into<Option<KeyboardShortcut>>) -> Self {
423 self.return_key = return_key.into();
424 self
425 }
426}
427
428impl Widget for TextEdit<'_> {
431 fn ui(self, ui: &mut Ui) -> Response {
432 self.show(ui).response.response
433 }
434}
435
436impl TextEdit<'_> {
437 pub fn show(self, ui: &mut Ui) -> TextEditOutput {
453 let TextEdit {
454 text,
455 prefix,
456 suffix,
457 mut hint_text,
458 id,
459 id_salt,
460 font_selection,
461 text_color,
462 layouter,
463 password,
464 frame,
465 margin,
466 multiline,
467 interactive,
468 desired_width,
469 desired_height_rows,
470 event_filter,
471 cursor_at_end,
472 min_size,
473 align,
474 clip_text,
475 char_limit,
476 return_key,
477 background_color,
478 } = self;
479
480 let text_color = text_color
481 .or_else(|| ui.visuals().override_text_color)
482 .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
484
485 let prev_text = text.as_str().to_owned();
486 let hint_text_str = hint_text.text().unwrap_or_default().to_string();
487
488 let font_id = font_selection.resolve(ui.style());
489 let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
490 let line_height = row_height + ui.spacing().extra_text_line_spacing;
491
492 const MIN_WIDTH: f32 = 24.0; let available_width = ui.available_width().at_least(MIN_WIDTH);
494 let desired_width = desired_width
495 .unwrap_or_else(|| ui.spacing().text_edit_width)
496 .at_least(min_size.x);
497 let allocate_width = desired_width.at_most(available_width).at_least(min_size.x);
499
500 let font_id_clone = font_id.clone();
501 let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
502 let text = mask_if_password(password, text.as_str());
503 let mut layout_job = if multiline {
504 LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
505 } else {
506 LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
507 };
508 layout_job.halign = align.x();
509 layout_job.keep_trailing_whitespace = true;
511
512 for section in &mut layout_job.sections {
513 section.format.line_height = Some(line_height);
514 }
515
516 ui.fonts_mut(|f| f.layout_job(layout_job))
517 };
518
519 let layouter = layouter.unwrap_or(&mut default_layouter);
520
521 let min_inner_height = (desired_height_rows.at_least(1) as f32) * line_height;
522
523 let id = id.unwrap_or_else(|| {
524 if let Some(id_salt) = id_salt {
525 ui.make_persistent_id(id_salt)
526 } else {
527 let id = ui.next_auto_id();
529 ui.skip_ahead_auto_ids(1);
530 id
531 }
532 });
533
534 let allow_drag_to_select =
539 ui.input(|i| !i.has_touch_screen()) || ui.memory(|mem| mem.has_focus(id));
540
541 let sense = if interactive {
542 if allow_drag_to_select {
543 Sense::click_and_drag()
544 } else {
545 Sense::click()
546 }
547 } else {
548 Sense::hover()
549 };
550
551 let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
552 let mut cursor_range = None;
553 let mut prev_cursor_range = None;
554
555 let owns_ime_events = ui.memory(|mem| mem.owns_ime_events(id));
556 if !owns_ime_events {
557 state.cursor_purpose = TextEditCursorPurpose::Selection;
558 if !state.cursor.is_empty() {
559 state.cursor.set_char_range(
560 state
561 .cursor
562 .char_range()
563 .map(|r| CCursorRange::one(r.primary)),
564 );
565 }
566 }
567
568 let mut text_changed = false;
569 let text_mutable = text.is_mutable();
570
571 let mut handle_events = |ui: &Ui, galley: &mut Arc<Galley>, layouter, wrap_width, text| {
572 if interactive && ui.memory(|mem| mem.has_focus(id)) {
573 ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
574
575 let default_cursor_range = if cursor_at_end {
576 CCursorRange::one(galley.end())
577 } else {
578 CCursorRange::default()
579 };
580 prev_cursor_range = state.cursor.range(galley);
581
582 let (changed, new_cursor_range) = events(
583 ui,
584 &mut state,
585 text,
586 galley,
587 layouter,
588 &EventsOptions {
589 id,
590 wrap_width,
591 multiline,
592 password,
593 default_cursor_range,
594 owns_ime_events,
595 char_limit,
596 event_filter,
597 return_key,
598 },
599 );
600
601 if changed {
602 text_changed = true;
603 }
604 cursor_range = Some(new_cursor_range);
605 }
606 };
607
608 let mut get_galley = None;
612 let inner_rect_id = Id::new("text_edit_rect");
613 let mut response = {
614 let any_shrink = hint_text.any_shrink();
615 let mut atoms: Atoms<'_> = Atoms::new(());
618
619 for atom in prefix {
622 atoms.push_right(atom);
623 }
624
625 if text.as_str().is_empty() && !hint_text.is_empty() {
626 let mut shrunk = any_shrink;
628 let mut first = true;
629
630 hint_text.map_texts(|t| t.color(ui.style().visuals.weak_text_color()));
633
634 for mut atom in hint_text {
635 if !shrunk && matches!(atom.kind, AtomKind::Text(_)) {
636 atom = atom.atom_shrink(true);
638 atom = atom.atom_grow(true);
639 shrunk = true;
640 }
641
642 if first {
643 atom = atom.atom_id(inner_rect_id);
646 first = false;
647 }
648
649 atoms.push_right(atom.atom_align(align));
653 }
654
655 let available_width = allocate_width - margin.sum().x;
658 let galley = layouter(ui, text, available_width);
659
660 let mut galley_clone = Arc::clone(&galley);
664 handle_events(ui, &mut galley_clone, layouter, available_width, text);
665
666 get_galley = Some(galley);
667 } else {
668 let should_shrink = clip_text || multiline;
672
673 atoms.push_right(
677 AtomKind::closure(|ui, args| {
678 let mut galley = layouter(ui, text, args.available_size.x);
679
680 handle_events(ui, &mut galley, layouter, args.available_size.x, text);
684
685 let intrinsic_size = galley.intrinsic_size();
686 let mut size = galley.size();
687 size.y = size.y.at_least(min_inner_height);
688 if clip_text {
689 size.x = size.x.at_most(args.available_size.x);
690 }
691
692 get_galley = Some(galley);
694 IntoSizedResult {
695 intrinsic_size,
696 sized: SizedAtomKind::Empty { size: Some(size) },
697 }
698 })
699 .atom_grow(true)
700 .atom_align(self.align)
701 .atom_id(inner_rect_id)
702 .atom_shrink(should_shrink),
703 );
704 }
705
706 for atom in suffix {
709 atoms.push_right(atom);
710 }
711
712 let custom_frame = frame.is_some();
713 let frame = frame.unwrap_or_else(|| Frame::new().inner_margin(margin));
714
715 let min_height = (min_inner_height + frame.total_margin().sum().y).at_least(min_size.y);
716
717 let wrap_mode = if multiline {
719 TextWrapMode::Wrap
720 } else {
721 TextWrapMode::Truncate
722 };
723
724 let mut allocated = AtomLayout::new(atoms)
725 .id(id)
726 .min_size(Vec2::new(allocate_width, min_height))
727 .max_width(allocate_width)
728 .sense(sense)
729 .frame(frame)
730 .align2(align)
731 .wrap_mode(wrap_mode)
732 .allocate(ui);
733
734 allocated.frame = if custom_frame {
735 allocated.frame
736 } else {
737 let visuals = ui.style().interact(&allocated.response);
738 let background_color =
739 background_color.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
740
741 let (corner_radius, background_color, stroke) = if text_mutable {
742 if allocated.response.has_focus() {
743 (
744 visuals.corner_radius,
745 background_color,
746 ui.visuals().selection.stroke,
747 )
748 } else {
749 (visuals.corner_radius, background_color, visuals.bg_stroke)
750 }
751 } else {
752 let visuals = &ui.style().visuals.widgets.inactive;
753 (
754 visuals.corner_radius,
755 Color32::TRANSPARENT,
756 visuals.bg_stroke,
757 )
758 };
759 allocated
760 .frame
761 .fill(background_color)
762 .corner_radius(corner_radius)
763 .inner_margin(
764 allocated.frame.inner_margin
765 + Margin::same((visuals.expansion - stroke.width).round() as i8),
766 )
767 .outer_margin(Margin::same(-(visuals.expansion as i8)))
768 .stroke(stroke)
769 };
770
771 allocated.paint(ui)
772 };
773
774 let inner_rect = response.rect(inner_rect_id).unwrap_or(Rect::ZERO);
775
776 let mut galley = get_galley.expect("Galley should be available here");
778
779 response.flags -= response::Flags::FAKE_PRIMARY_CLICKED;
781 let text_clip_rect = inner_rect;
782 let painter = ui.painter_at(text_clip_rect.expand(1.0)); if interactive && let Some(pointer_pos) = response.interact_pointer_pos() {
785 if response.hovered() && text.is_mutable() {
786 ui.output_mut(|o| o.mutable_text_under_cursor = true);
787 }
788
789 let cursor_at_pointer = galley.cursor_from_pos(
792 pointer_pos - inner_rect.min + state.text_offset + vec2(galley.rect.left(), 0.0),
793 );
794
795 if ui.visuals().text_cursor.preview
796 && response.hovered()
797 && ui.input(|i| i.pointer.is_moving())
798 {
799 let cursor_rect = TSTransform::from_translation(
801 inner_rect.min.to_vec2() - vec2(galley.rect.left(), 0.0),
802 ) * cursor_rect(&galley, &cursor_at_pointer, row_height);
803 text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect);
804 }
805
806 let is_being_dragged = ui.is_being_dragged(response.id);
807 let did_interact = state.cursor.pointer_interaction(
808 ui,
809 &response,
810 cursor_at_pointer,
811 &galley,
812 is_being_dragged,
813 );
814
815 if did_interact || response.clicked() {
816 ui.memory_mut(|mem| mem.request_focus(response.id));
817 state.cursor_purpose = TextEditCursorPurpose::Selection;
818
819 state.last_interaction_time = ui.input(|i| i.time);
820 }
821 }
822
823 if interactive && response.hovered() {
824 ui.set_cursor_icon(CursorIcon::Text);
825 }
826
827 if text_changed {
828 response.mark_changed();
829 }
830
831 let mut galley_pos = align
832 .align_size_within_rect(galley.size(), inner_rect)
833 .intersect(inner_rect) .min;
835 let align_offset = inner_rect.left_top() - galley_pos;
836
837 if clip_text && align_offset.x == 0.0 {
839 let cursor_pos = match (cursor_range, ui.memory(|mem| mem.has_focus(id))) {
840 (Some(cursor_range), true) => galley.pos_from_cursor(cursor_range.primary).min.x,
841 _ => 0.0,
842 };
843
844 let mut offset_x = state.text_offset.x;
845 let visible_range = offset_x..=offset_x + inner_rect.width();
846
847 if !visible_range.contains(&cursor_pos) {
848 if cursor_pos < *visible_range.start() {
849 offset_x = cursor_pos;
850 } else {
851 offset_x = cursor_pos - inner_rect.width();
852 }
853 }
854
855 offset_x = offset_x
856 .at_most(galley.size().x - inner_rect.width())
857 .at_least(0.0);
858
859 state.text_offset = vec2(offset_x, align_offset.y);
860 galley_pos -= vec2(offset_x, 0.0);
861 } else {
862 state.text_offset = align_offset;
863 }
864
865 let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) =
866 (cursor_range, prev_cursor_range)
867 {
868 prev_cursor_range != cursor_range
869 } else {
870 false
871 };
872
873 let should_paint_ime_visuals_the_legacy_way = ui.visuals().ime_composition.legacy_visuals;
874
875 if ui.is_rect_visible(inner_rect) {
876 let has_focus = ui.memory(|mem| mem.has_focus(id));
877
878 if has_focus
879 && (state.cursor_purpose.is_selection() || should_paint_ime_visuals_the_legacy_way)
880 && let Some(cursor_range) = state.cursor.range(&galley)
881 {
882 paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None);
884 }
885
886 painter.galley(
887 galley_pos - vec2(galley.rect.left(), 0.0),
888 Arc::clone(&galley),
889 text_color,
890 );
891
892 if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
893 let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height)
894 .translate(galley_pos.to_vec2() - vec2(galley.rect.left(), 0.0));
895
896 if response.changed() || selection_changed {
897 ui.scroll_to_rect(primary_cursor_rect, None);
899 }
900
901 if text.is_mutable() && interactive {
902 let now = ui.input(|i| i.time);
903 if response.changed() || selection_changed {
904 state.last_interaction_time = now;
905 }
906
907 let viewport_has_focus = ui.input(|i| i.focused);
912 if viewport_has_focus {
913 let time_since_last_interaction = now - state.last_interaction_time;
914 let cursor_purpose = if should_paint_ime_visuals_the_legacy_way {
915 &TextEditCursorPurpose::Selection
916 } else {
917 &state.cursor_purpose
918 };
919 match cursor_purpose {
920 TextEditCursorPurpose::Selection => {
921 text_selection::visuals::paint_text_cursor(
922 ui,
923 &painter,
924 primary_cursor_rect,
925 time_since_last_interaction,
926 );
927 }
928 TextEditCursorPurpose::ImeComposition { active_range } => {
929 text_selection::visuals::paint_ime_preedit_text_visuals(
930 galley_pos,
931 ui,
932 &painter,
933 &galley,
934 row_height,
935 {
936 let [start, end] = cursor_range.sorted_cursors();
937 start..end
938 },
939 active_range.clone(),
940 time_since_last_interaction,
941 );
942 }
943 }
944 }
945 if ui.memory(|mem| mem.owns_ime_events(id)) {
946 let to_global = ui
948 .ctx()
949 .layer_transform_to_global(ui.layer_id())
950 .unwrap_or_default();
951 ui.output_mut(|o| {
952 o.ime = Some(crate::output::IMEOutput {
953 purpose: if password {
954 IMEPurpose::Password
955 } else {
956 IMEPurpose::Normal
957 },
958 rect: to_global * inner_rect,
959 cursor_rect: to_global * primary_cursor_rect,
960 should_interrupt_composition: false,
961 });
962 });
963 }
964 }
965 }
966 }
967
968 state.clone().store(ui.ctx(), id);
969
970 if response.changed() {
971 response.widget_info(|| {
972 WidgetInfo::text_edit(
973 ui.is_enabled(),
974 mask_if_password(password, prev_text.as_str()),
975 mask_if_password(password, text.as_str()),
976 hint_text_str.as_str(),
977 )
978 });
979 } else if selection_changed && let Some(cursor_range) = cursor_range {
980 let char_range = cursor_range.as_sorted_char_range();
981 let info = WidgetInfo::text_selection_changed(
982 ui.is_enabled(),
983 char_range,
984 mask_if_password(password, text.as_str()),
985 );
986 response.output_event(OutputEvent::TextSelectionChanged(info));
987 } else {
988 response.widget_info(|| {
989 WidgetInfo::text_edit(
990 ui.is_enabled(),
991 mask_if_password(password, prev_text.as_str()),
992 mask_if_password(password, text.as_str()),
993 hint_text_str.as_str(),
994 )
995 });
996 }
997
998 let role = if password {
999 accesskit::Role::PasswordInput
1000 } else if multiline {
1001 accesskit::Role::MultilineTextInput
1002 } else {
1003 accesskit::Role::TextInput
1004 };
1005
1006 crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
1007 ui.ctx(),
1008 id,
1009 cursor_range,
1010 role,
1011 TSTransform::from_translation(galley_pos.to_vec2()),
1012 &galley,
1013 );
1014
1015 TextEditOutput {
1016 response,
1017 galley,
1018 galley_pos,
1019 text_clip_rect,
1020 state,
1021 cursor_range,
1022 }
1023 }
1024}
1025
1026fn mask_if_password(is_password: bool, text: &str) -> String {
1027 fn mask_password(text: &str) -> String {
1028 core::iter::repeat_n(
1029 epaint::text::PASSWORD_REPLACEMENT_CHAR,
1030 text.chars().count(),
1031 )
1032 .collect::<String>()
1033 }
1034
1035 if is_password {
1036 mask_password(text)
1037 } else {
1038 text.to_owned()
1039 }
1040}
1041
1042struct EventsOptions {
1047 id: Id,
1048 wrap_width: f32,
1049 multiline: bool,
1050 password: bool,
1051 default_cursor_range: CCursorRange,
1052 owns_ime_events: bool,
1053 char_limit: usize,
1054 event_filter: EventFilter,
1055 return_key: Option<KeyboardShortcut>,
1056}
1057
1058fn events(
1060 ui: &crate::Ui,
1061 state: &mut TextEditState,
1062 text: &mut dyn TextBuffer,
1063 galley: &mut Arc<Galley>,
1064 layouter: &mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>,
1065 opts: &EventsOptions,
1066) -> (bool, CCursorRange) {
1067 let EventsOptions {
1068 id,
1069 wrap_width,
1070 multiline,
1071 password,
1072 default_cursor_range,
1073 owns_ime_events,
1074 char_limit,
1075 event_filter,
1076 return_key,
1077 } = *opts;
1078
1079 let os = ui.os();
1080
1081 let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
1082
1083 state.undoer.lock().feed_state(
1086 ui.input(|i| i.time),
1087 &(cursor_range, text.as_str().to_owned()),
1088 );
1089
1090 let copy_if_not_password = |ui: &Ui, text: String| {
1091 if !password {
1092 ui.copy_text(text);
1093 }
1094 };
1095
1096 let mut any_change = false;
1097
1098 let events = ui.input(|i| i.filtered_events(&event_filter));
1099
1100 enum CursorMutation {
1101 Selection(CCursorRange),
1102 ImeComposition {
1103 cursor_range: CCursorRange,
1104 active_range: Option<core::ops::Range<CCursor>>,
1105 },
1106 ImeCompositionCursorRange(CCursorRange),
1107 }
1108
1109 for event in &events {
1110 let did_mutate_text = match event {
1111 event if cursor_range.on_event(os, event, galley, id) => None,
1113
1114 Event::Copy => {
1115 if !cursor_range.is_empty() {
1116 copy_if_not_password(ui, cursor_range.slice_str(text.as_str()).to_owned());
1117 }
1118 None
1119 }
1120 Event::Cut => {
1121 if cursor_range.is_empty() {
1122 None
1123 } else {
1124 copy_if_not_password(ui, cursor_range.slice_str(text.as_str()).to_owned());
1125 Some(CursorMutation::Selection(CCursorRange::one(
1126 text.delete_selected(&cursor_range),
1127 )))
1128 }
1129 }
1130 Event::Paste(text_to_insert) => {
1131 if text_to_insert.is_empty() {
1132 None
1133 } else {
1134 let mut ccursor = text.delete_selected(&cursor_range);
1135 if multiline {
1136 text.insert_text_at(&mut ccursor, text_to_insert, char_limit);
1137 } else {
1138 let single_line = text_to_insert.replace(['\r', '\n'], " ");
1139 text.insert_text_at(&mut ccursor, &single_line, char_limit);
1140 }
1141
1142 Some(CursorMutation::Selection(CCursorRange::one(ccursor)))
1143 }
1144 }
1145 Event::Text(text_to_insert) => {
1146 if !text_to_insert.is_empty() && text_to_insert != "\n" && text_to_insert != "\r" {
1148 let mut ccursor = text.delete_selected(&cursor_range);
1149
1150 text.insert_text_at(&mut ccursor, text_to_insert, char_limit);
1151
1152 Some(CursorMutation::Selection(CCursorRange::one(ccursor)))
1153 } else {
1154 None
1155 }
1156 }
1157 Event::Key {
1158 key: Key::Tab,
1159 pressed: true,
1160 modifiers,
1161 ..
1162 } if multiline => {
1163 let mut ccursor = text.delete_selected(&cursor_range);
1164 if modifiers.shift {
1165 text.decrease_indentation(&mut ccursor);
1167 } else {
1168 text.insert_text_at(&mut ccursor, "\t", char_limit);
1169 }
1170 Some(CursorMutation::Selection(CCursorRange::one(ccursor)))
1171 }
1172 Event::Key {
1173 key,
1174 pressed: true,
1175 modifiers,
1176 ..
1177 } if return_key.is_some_and(|return_key| {
1178 *key == return_key.logical_key && modifiers.matches_logically(return_key.modifiers)
1179 }) =>
1180 {
1181 if multiline {
1182 let mut ccursor = text.delete_selected(&cursor_range);
1183 text.insert_text_at(&mut ccursor, "\n", char_limit);
1184 Some(CursorMutation::Selection(CCursorRange::one(ccursor)))
1186 } else {
1187 ui.memory_mut(|mem| mem.surrender_focus(id)); break;
1189 }
1190 }
1191
1192 Event::Key {
1193 key,
1194 pressed: true,
1195 modifiers,
1196 ..
1197 } if (modifiers.matches_logically(Modifiers::COMMAND) && *key == Key::Y)
1198 || (modifiers.matches_logically(Modifiers::SHIFT | Modifiers::COMMAND)
1199 && *key == Key::Z) =>
1200 {
1201 if let Some((redo_ccursor_range, redo_txt)) = state
1202 .undoer
1203 .lock()
1204 .redo(&(cursor_range, text.as_str().to_owned()))
1205 {
1206 text.replace_with(redo_txt);
1207 Some(CursorMutation::Selection(*redo_ccursor_range))
1208 } else {
1209 None
1210 }
1211 }
1212
1213 Event::Key {
1214 key: Key::Z,
1215 pressed: true,
1216 modifiers,
1217 ..
1218 } if modifiers.matches_logically(Modifiers::COMMAND) => {
1219 if let Some((undo_ccursor_range, undo_txt)) = state
1220 .undoer
1221 .lock()
1222 .undo(&(cursor_range, text.as_str().to_owned()))
1223 {
1224 text.replace_with(undo_txt);
1225 Some(CursorMutation::Selection(*undo_ccursor_range))
1226 } else {
1227 None
1228 }
1229 }
1230
1231 Event::Key {
1232 modifiers,
1233 key,
1234 pressed: true,
1235 ..
1236 } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key)
1237 .map(CursorMutation::Selection),
1238 Event::Ime(ime_event) if owns_ime_events => {
1239 fn clear_preedit_text(
1265 text: &mut dyn TextBuffer,
1266 preedit_range: &CCursorRange,
1267 ) -> CCursor {
1268 text.delete_selected(preedit_range)
1269 }
1270
1271 match ime_event {
1272 #[expect(deprecated)]
1273 ImeEvent::Enabled | ImeEvent::Disabled => None,
1274 ImeEvent::Preedit {
1288 text: composition_text,
1289 ..
1290 }
1291 | ImeEvent::Commit(composition_text)
1292 if composition_text.is_empty()
1293 && !state.cursor_purpose.is_ime_composition() =>
1294 {
1295 None
1296 }
1297 ImeEvent::Preedit {
1298 text: composition_text,
1299 ..
1300 }
1301 | ImeEvent::Commit(composition_text)
1302 if composition_text == "\n" || composition_text == "\r" =>
1303 {
1304 None
1305 }
1306 ImeEvent::Preedit {
1307 text: preedit_text,
1308 active_range_chars,
1309 } => {
1310 let mut ccursor = clear_preedit_text(text, &cursor_range);
1311
1312 if preedit_text.is_empty() {
1313 Some(CursorMutation::Selection(CCursorRange::one(ccursor)))
1314 } else {
1315 let start_cursor = ccursor;
1316 text.insert_text_at(&mut ccursor, preedit_text, char_limit);
1317 Some(CursorMutation::ImeComposition {
1318 cursor_range: CCursorRange::two(start_cursor, ccursor),
1319 active_range: active_range_chars.clone().map(|range| {
1320 CCursor::new(range.start)..CCursor::new(range.end)
1321 }),
1322 })
1323 }
1324 }
1325 ImeEvent::Commit(commit_text) => {
1326 state.cursor_purpose = TextEditCursorPurpose::Selection;
1327 let mut ccursor = clear_preedit_text(text, &cursor_range);
1328
1329 if !commit_text.is_empty() {
1330 text.insert_text_at(&mut ccursor, commit_text, char_limit);
1331 }
1332
1333 Some(CursorMutation::Selection(CCursorRange::one(ccursor)))
1334 }
1335 ImeEvent::DeleteSurrounding {
1336 before_chars,
1337 after_chars,
1338 } => Some(CursorMutation::ImeCompositionCursorRange(
1339 text.delete_surrounding_chars(cursor_range, *before_chars, *after_chars),
1340 )),
1341 }
1342 }
1343
1344 _ => None,
1345 };
1346
1347 if let Some(cursor_mutation) = did_mutate_text {
1348 any_change = true;
1349
1350 *galley = layouter(ui, text, wrap_width);
1352
1353 match cursor_mutation {
1355 CursorMutation::Selection(new_cursor_range) => {
1356 cursor_range = new_cursor_range;
1357 state.cursor_purpose = TextEditCursorPurpose::Selection;
1358 }
1359 CursorMutation::ImeComposition {
1360 cursor_range: new_cursor_range,
1361 active_range,
1362 } => {
1363 cursor_range = new_cursor_range;
1364 state.cursor_purpose = TextEditCursorPurpose::ImeComposition { active_range };
1365 }
1366 CursorMutation::ImeCompositionCursorRange(new_cursor_range) => {
1367 cursor_range = new_cursor_range;
1368 }
1369 }
1370 }
1371 }
1372
1373 state.cursor.set_char_range(Some(cursor_range));
1374
1375 state.undoer.lock().feed_state(
1376 ui.input(|i| i.time),
1377 &(cursor_range, text.as_str().to_owned()),
1378 );
1379
1380 (any_change, cursor_range)
1381}
1382
1383fn check_for_mutating_key_press(
1387 os: OperatingSystem,
1388 cursor_range: &CCursorRange,
1389 text: &mut dyn TextBuffer,
1390 galley: &Galley,
1391 modifiers: &Modifiers,
1392 key: Key,
1393) -> Option<CCursorRange> {
1394 match key {
1395 Key::Backspace => {
1396 let ccursor = if modifiers.mac_cmd {
1397 text.delete_paragraph_before_cursor(galley, cursor_range)
1398 } else if let Some(cursor) = cursor_range.single() {
1399 if modifiers.alt || modifiers.ctrl {
1400 text.delete_previous_word(cursor)
1402 } else {
1403 text.delete_previous_char(cursor)
1404 }
1405 } else {
1406 text.delete_selected(cursor_range)
1407 };
1408 Some(CCursorRange::one(ccursor))
1409 }
1410
1411 Key::Delete if !modifiers.shift || os != OperatingSystem::Windows => {
1412 let ccursor = if modifiers.mac_cmd {
1413 text.delete_paragraph_after_cursor(galley, cursor_range)
1414 } else if let Some(cursor) = cursor_range.single() {
1415 if modifiers.alt || modifiers.ctrl {
1416 text.delete_next_word(cursor)
1418 } else {
1419 text.delete_next_char(cursor)
1420 }
1421 } else {
1422 text.delete_selected(cursor_range)
1423 };
1424 let ccursor = CCursor {
1425 prefer_next_row: true,
1426 ..ccursor
1427 };
1428 Some(CCursorRange::one(ccursor))
1429 }
1430
1431 Key::H if modifiers.ctrl => {
1432 let ccursor = text.delete_previous_char(cursor_range.primary);
1433 Some(CCursorRange::one(ccursor))
1434 }
1435
1436 Key::K if modifiers.ctrl => {
1437 let ccursor = text.delete_paragraph_after_cursor(galley, cursor_range);
1438 Some(CCursorRange::one(ccursor))
1439 }
1440
1441 Key::U if modifiers.ctrl => {
1442 let ccursor = text.delete_paragraph_before_cursor(galley, cursor_range);
1443 Some(CCursorRange::one(ccursor))
1444 }
1445
1446 Key::W if modifiers.ctrl => {
1447 let ccursor = if let Some(cursor) = cursor_range.single() {
1448 text.delete_previous_word(cursor)
1449 } else {
1450 text.delete_selected(cursor_range)
1451 };
1452 Some(CCursorRange::one(ccursor))
1453 }
1454
1455 _ => None,
1456 }
1457}