1use crate::core::alignment;
35use crate::core::clipboard::{self, Clipboard};
36use crate::core::input_method;
37use crate::core::keyboard;
38use crate::core::keyboard::key;
39use crate::core::layout::{self, Layout};
40use crate::core::mouse;
41use crate::core::renderer;
42use crate::core::text::editor::{Cursor, Editor as _};
43use crate::core::text::highlighter::{self, Highlighter};
44use crate::core::text::{self, LineHeight, Text, Wrapping};
45use crate::core::time::{Duration, Instant};
46use crate::core::widget::operation;
47use crate::core::widget::{self, Widget};
48use crate::core::window;
49use crate::core::{
50 Background, Border, Color, Element, Event, InputMethod, Length, Padding,
51 Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector,
52};
53
54use std::borrow::Cow;
55use std::cell::RefCell;
56use std::fmt;
57use std::ops::DerefMut;
58use std::ops::Range;
59use std::sync::Arc;
60
61pub use text::editor::{Action, Edit, Line, LineEnding, Motion};
62
63#[allow(missing_debug_implementations)]
97pub struct TextEditor<
98 'a,
99 Highlighter,
100 Message,
101 Theme = crate::Theme,
102 Renderer = crate::Renderer,
103> where
104 Highlighter: text::Highlighter,
105 Theme: Catalog,
106 Renderer: text::Renderer,
107{
108 content: &'a Content<Renderer>,
109 placeholder: Option<text::Fragment<'a>>,
110 font: Option<Renderer::Font>,
111 text_size: Option<Pixels>,
112 line_height: LineHeight,
113 width: Length,
114 height: Length,
115 min_height: f32,
116 max_height: f32,
117 padding: Padding,
118 wrapping: Wrapping,
119 class: Theme::Class<'a>,
120 key_binding: Option<Box<dyn Fn(KeyPress) -> Option<Binding<Message>> + 'a>>,
121 on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
122 highlighter_settings: Highlighter::Settings,
123 highlighter_format: fn(
124 &Highlighter::Highlight,
125 &Theme,
126 ) -> highlighter::Format<Renderer::Font>,
127 last_status: Option<Status>,
128}
129
130impl<'a, Message, Theme, Renderer>
131 TextEditor<'a, highlighter::PlainText, Message, Theme, Renderer>
132where
133 Theme: Catalog,
134 Renderer: text::Renderer,
135{
136 pub fn new(content: &'a Content<Renderer>) -> Self {
138 Self {
139 content,
140 placeholder: None,
141 font: None,
142 text_size: None,
143 line_height: LineHeight::default(),
144 width: Length::Fill,
145 height: Length::Shrink,
146 min_height: 0.0,
147 max_height: f32::INFINITY,
148 padding: Padding::new(5.0),
149 wrapping: Wrapping::default(),
150 class: Theme::default(),
151 key_binding: None,
152 on_edit: None,
153 highlighter_settings: (),
154 highlighter_format: |_highlight, _theme| {
155 highlighter::Format::default()
156 },
157 last_status: None,
158 }
159 }
160}
161
162impl<'a, Highlighter, Message, Theme, Renderer>
163 TextEditor<'a, Highlighter, Message, Theme, Renderer>
164where
165 Highlighter: text::Highlighter,
166 Theme: Catalog,
167 Renderer: text::Renderer,
168{
169 pub fn placeholder(
171 mut self,
172 placeholder: impl text::IntoFragment<'a>,
173 ) -> Self {
174 self.placeholder = Some(placeholder.into_fragment());
175 self
176 }
177
178 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
180 self.width = Length::from(width.into());
181 self
182 }
183
184 pub fn height(mut self, height: impl Into<Length>) -> Self {
186 self.height = height.into();
187 self
188 }
189
190 pub fn min_height(mut self, min_height: impl Into<Pixels>) -> Self {
192 self.min_height = min_height.into().0;
193 self
194 }
195
196 pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self {
198 self.max_height = max_height.into().0;
199 self
200 }
201
202 pub fn on_action(
207 mut self,
208 on_edit: impl Fn(Action) -> Message + 'a,
209 ) -> Self {
210 self.on_edit = Some(Box::new(on_edit));
211 self
212 }
213
214 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
218 self.font = Some(font.into());
219 self
220 }
221
222 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
224 self.text_size = Some(size.into());
225 self
226 }
227
228 pub fn line_height(
230 mut self,
231 line_height: impl Into<text::LineHeight>,
232 ) -> Self {
233 self.line_height = line_height.into();
234 self
235 }
236
237 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
239 self.padding = padding.into();
240 self
241 }
242
243 pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
245 self.wrapping = wrapping;
246 self
247 }
248
249 #[cfg(feature = "highlighter")]
251 pub fn highlight(
252 self,
253 syntax: &str,
254 theme: iced_highlighter::Theme,
255 ) -> TextEditor<'a, iced_highlighter::Highlighter, Message, Theme, Renderer>
256 where
257 Renderer: text::Renderer<Font = crate::core::Font>,
258 {
259 self.highlight_with::<iced_highlighter::Highlighter>(
260 iced_highlighter::Settings {
261 theme,
262 token: syntax.to_owned(),
263 },
264 |highlight, _theme| highlight.to_format(),
265 )
266 }
267
268 pub fn highlight_with<H: text::Highlighter>(
271 self,
272 settings: H::Settings,
273 to_format: fn(
274 &H::Highlight,
275 &Theme,
276 ) -> highlighter::Format<Renderer::Font>,
277 ) -> TextEditor<'a, H, Message, Theme, Renderer> {
278 TextEditor {
279 content: self.content,
280 placeholder: self.placeholder,
281 font: self.font,
282 text_size: self.text_size,
283 line_height: self.line_height,
284 width: self.width,
285 height: self.height,
286 min_height: self.min_height,
287 max_height: self.max_height,
288 padding: self.padding,
289 wrapping: self.wrapping,
290 class: self.class,
291 key_binding: self.key_binding,
292 on_edit: self.on_edit,
293 highlighter_settings: settings,
294 highlighter_format: to_format,
295 last_status: self.last_status,
296 }
297 }
298
299 pub fn key_binding(
303 mut self,
304 key_binding: impl Fn(KeyPress) -> Option<Binding<Message>> + 'a,
305 ) -> Self {
306 self.key_binding = Some(Box::new(key_binding));
307 self
308 }
309
310 #[must_use]
312 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
313 where
314 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
315 {
316 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
317 self
318 }
319
320 #[cfg(feature = "advanced")]
322 #[must_use]
323 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
324 self.class = class.into();
325 self
326 }
327
328 fn input_method<'b>(
329 &self,
330 state: &'b State<Highlighter>,
331 renderer: &Renderer,
332 layout: Layout<'_>,
333 ) -> InputMethod<&'b str> {
334 let Some(Focus {
335 is_window_focused: true,
336 ..
337 }) = &state.focus
338 else {
339 return InputMethod::Disabled;
340 };
341
342 let bounds = layout.bounds();
343 let internal = self.content.0.borrow_mut();
344
345 let text_bounds = bounds.shrink(self.padding);
346 let translation = text_bounds.position() - Point::ORIGIN;
347
348 let cursor = match internal.editor.cursor() {
349 Cursor::Caret(position) => position,
350 Cursor::Selection(ranges) => {
351 ranges.first().cloned().unwrap_or_default().position()
352 }
353 };
354
355 let line_height = self.line_height.to_absolute(
356 self.text_size.unwrap_or_else(|| renderer.default_size()),
357 );
358
359 let position =
360 cursor + translation + Vector::new(0.0, f32::from(line_height));
361
362 InputMethod::Enabled {
363 position,
364 purpose: input_method::Purpose::Normal,
365 preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),
366 }
367 }
368}
369
370pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>)
372where
373 R: text::Renderer;
374
375struct Internal<R>
376where
377 R: text::Renderer,
378{
379 editor: R::Editor,
380 is_dirty: bool,
381}
382
383impl<R> Content<R>
384where
385 R: text::Renderer,
386{
387 pub fn new() -> Self {
389 Self::with_text("")
390 }
391
392 pub fn with_text(text: &str) -> Self {
394 Self(RefCell::new(Internal {
395 editor: R::Editor::with_text(text),
396 is_dirty: true,
397 }))
398 }
399
400 pub fn perform(&mut self, action: Action) {
402 let internal = self.0.get_mut();
403
404 internal.editor.perform(action);
405 internal.is_dirty = true;
406 }
407
408 pub fn line_count(&self) -> usize {
410 self.0.borrow().editor.line_count()
411 }
412
413 pub fn line(&self, index: usize) -> Option<Line<'_>> {
415 let internal = self.0.borrow();
416 let line = internal.editor.line(index)?;
417
418 Some(Line {
419 text: Cow::Owned(line.text.into_owned()),
420 ending: line.ending,
421 })
422 }
423
424 pub fn lines(&self) -> impl Iterator<Item = Line<'_>> {
426 (0..)
427 .map(|i| self.line(i))
428 .take_while(Option::is_some)
429 .flatten()
430 }
431
432 pub fn text(&self) -> String {
434 let mut contents = String::new();
435 let mut lines = self.lines().peekable();
436
437 while let Some(line) = lines.next() {
438 contents.push_str(&line.text);
439
440 if lines.peek().is_some() {
441 contents.push_str(if line.ending == LineEnding::None {
442 LineEnding::default().as_str()
443 } else {
444 line.ending.as_str()
445 });
446 }
447 }
448
449 contents
450 }
451
452 pub fn line_ending(&self) -> Option<LineEnding> {
454 Some(self.line(0)?.ending)
455 }
456
457 pub fn selection(&self) -> Option<String> {
459 self.0.borrow().editor.selection()
460 }
461
462 pub fn cursor_position(&self) -> (usize, usize) {
464 self.0.borrow().editor.cursor_position()
465 }
466}
467
468impl<Renderer> Default for Content<Renderer>
469where
470 Renderer: text::Renderer,
471{
472 fn default() -> Self {
473 Self::new()
474 }
475}
476
477impl<Renderer> fmt::Debug for Content<Renderer>
478where
479 Renderer: text::Renderer,
480 Renderer::Editor: fmt::Debug,
481{
482 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483 let internal = self.0.borrow();
484
485 f.debug_struct("Content")
486 .field("editor", &internal.editor)
487 .field("is_dirty", &internal.is_dirty)
488 .finish()
489 }
490}
491
492#[derive(Debug)]
494pub struct State<Highlighter: text::Highlighter> {
495 focus: Option<Focus>,
496 preedit: Option<input_method::Preedit>,
497 last_click: Option<mouse::Click>,
498 drag_click: Option<mouse::click::Kind>,
499 partial_scroll: f32,
500 highlighter: RefCell<Highlighter>,
501 highlighter_settings: Highlighter::Settings,
502 highlighter_format_address: usize,
503}
504
505#[derive(Debug, Clone)]
506struct Focus {
507 updated_at: Instant,
508 now: Instant,
509 is_window_focused: bool,
510}
511
512impl Focus {
513 const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
514
515 fn now() -> Self {
516 let now = Instant::now();
517
518 Self {
519 updated_at: now,
520 now,
521 is_window_focused: true,
522 }
523 }
524
525 fn is_cursor_visible(&self) -> bool {
526 self.is_window_focused
527 && ((self.now - self.updated_at).as_millis()
528 / Self::CURSOR_BLINK_INTERVAL_MILLIS)
529 % 2
530 == 0
531 }
532}
533
534impl<Highlighter: text::Highlighter> State<Highlighter> {
535 pub fn is_focused(&self) -> bool {
537 self.focus.is_some()
538 }
539}
540
541impl<Highlighter: text::Highlighter> operation::Focusable
542 for State<Highlighter>
543{
544 fn is_focused(&self) -> bool {
545 self.focus.is_some()
546 }
547
548 fn focus(&mut self) {
549 self.focus = Some(Focus::now());
550 }
551
552 fn unfocus(&mut self) {
553 self.focus = None;
554 }
555}
556
557impl<Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
558 for TextEditor<'_, Highlighter, Message, Theme, Renderer>
559where
560 Highlighter: text::Highlighter,
561 Theme: Catalog,
562 Renderer: text::Renderer,
563{
564 fn tag(&self) -> widget::tree::Tag {
565 widget::tree::Tag::of::<State<Highlighter>>()
566 }
567
568 fn state(&self) -> widget::tree::State {
569 widget::tree::State::new(State {
570 focus: None,
571 preedit: None,
572 last_click: None,
573 drag_click: None,
574 partial_scroll: 0.0,
575 highlighter: RefCell::new(Highlighter::new(
576 &self.highlighter_settings,
577 )),
578 highlighter_settings: self.highlighter_settings.clone(),
579 highlighter_format_address: self.highlighter_format as usize,
580 })
581 }
582
583 fn size(&self) -> Size<Length> {
584 Size {
585 width: self.width,
586 height: self.height,
587 }
588 }
589
590 fn layout(
591 &self,
592 tree: &mut widget::Tree,
593 renderer: &Renderer,
594 limits: &layout::Limits,
595 ) -> iced_renderer::core::layout::Node {
596 let mut internal = self.content.0.borrow_mut();
597 let state = tree.state.downcast_mut::<State<Highlighter>>();
598
599 if state.highlighter_format_address != self.highlighter_format as usize
600 {
601 state.highlighter.borrow_mut().change_line(0);
602
603 state.highlighter_format_address = self.highlighter_format as usize;
604 }
605
606 if state.highlighter_settings != self.highlighter_settings {
607 state
608 .highlighter
609 .borrow_mut()
610 .update(&self.highlighter_settings);
611
612 state.highlighter_settings = self.highlighter_settings.clone();
613 }
614
615 let limits = limits
616 .width(self.width)
617 .height(self.height)
618 .min_height(self.min_height)
619 .max_height(self.max_height);
620
621 internal.editor.update(
622 limits.shrink(self.padding).max(),
623 self.font.unwrap_or_else(|| renderer.default_font()),
624 self.text_size.unwrap_or_else(|| renderer.default_size()),
625 self.line_height,
626 self.wrapping,
627 state.highlighter.borrow_mut().deref_mut(),
628 );
629
630 match self.height {
631 Length::Fill | Length::FillPortion(_) | Length::Fixed(_) => {
632 layout::Node::new(limits.max())
633 }
634 Length::Shrink => {
635 let min_bounds = internal.editor.min_bounds();
636
637 layout::Node::new(
638 limits
639 .height(min_bounds.height)
640 .max()
641 .expand(Size::new(0.0, self.padding.vertical())),
642 )
643 }
644 }
645 }
646
647 fn update(
648 &mut self,
649 tree: &mut widget::Tree,
650 event: &Event,
651 layout: Layout<'_>,
652 cursor: mouse::Cursor,
653 renderer: &Renderer,
654 clipboard: &mut dyn Clipboard,
655 shell: &mut Shell<'_, Message>,
656 _viewport: &Rectangle,
657 ) {
658 let Some(on_edit) = self.on_edit.as_ref() else {
659 return;
660 };
661
662 let state = tree.state.downcast_mut::<State<Highlighter>>();
663 let is_redraw = matches!(
664 event,
665 Event::Window(window::Event::RedrawRequested(_now)),
666 );
667
668 match event {
669 Event::Window(window::Event::Unfocused) => {
670 if let Some(focus) = &mut state.focus {
671 focus.is_window_focused = false;
672 }
673 }
674 Event::Window(window::Event::Focused) => {
675 if let Some(focus) = &mut state.focus {
676 focus.is_window_focused = true;
677 focus.updated_at = Instant::now();
678
679 shell.request_redraw();
680 }
681 }
682 Event::Window(window::Event::RedrawRequested(now)) => {
683 if let Some(focus) = &mut state.focus {
684 if focus.is_window_focused {
685 focus.now = *now;
686
687 let millis_until_redraw =
688 Focus::CURSOR_BLINK_INTERVAL_MILLIS
689 - (focus.now - focus.updated_at).as_millis()
690 % Focus::CURSOR_BLINK_INTERVAL_MILLIS;
691
692 shell.request_redraw_at(
693 focus.now
694 + Duration::from_millis(
695 millis_until_redraw as u64,
696 ),
697 );
698 }
699 }
700 }
701 _ => {}
702 }
703
704 if let Some(update) = Update::from_event(
705 event,
706 state,
707 layout.bounds(),
708 self.padding,
709 cursor,
710 self.key_binding.as_deref(),
711 ) {
712 match update {
713 Update::Click(click) => {
714 let action = match click.kind() {
715 mouse::click::Kind::Single => {
716 Action::Click(click.position())
717 }
718 mouse::click::Kind::Double => Action::SelectWord,
719 mouse::click::Kind::Triple => Action::SelectLine,
720 };
721
722 state.focus = Some(Focus::now());
723 state.last_click = Some(click);
724 state.drag_click = Some(click.kind());
725
726 shell.publish(on_edit(action));
727 shell.capture_event();
728 }
729 Update::Drag(position) => {
730 shell.publish(on_edit(Action::Drag(position)));
731 }
732 Update::Release => {
733 state.drag_click = None;
734 }
735 Update::Scroll(lines) => {
736 let bounds = self.content.0.borrow().editor.bounds();
737
738 if bounds.height >= i32::MAX as f32 {
739 return;
740 }
741
742 let lines = lines + state.partial_scroll;
743 state.partial_scroll = lines.fract();
744
745 shell.publish(on_edit(Action::Scroll {
746 lines: lines as i32,
747 }));
748 shell.capture_event();
749 }
750 Update::InputMethod(update) => match update {
751 Ime::Toggle(is_open) => {
752 state.preedit =
753 is_open.then(input_method::Preedit::new);
754
755 shell.request_redraw();
756 }
757 Ime::Preedit { content, selection } => {
758 state.preedit = Some(input_method::Preedit {
759 content,
760 selection,
761 text_size: self.text_size,
762 });
763
764 shell.request_redraw();
765 }
766 Ime::Commit(text) => {
767 shell.publish(on_edit(Action::Edit(Edit::Paste(
768 Arc::new(text),
769 ))));
770 }
771 },
772 Update::Binding(binding) => {
773 fn apply_binding<
774 H: text::Highlighter,
775 R: text::Renderer,
776 Message,
777 >(
778 binding: Binding<Message>,
779 content: &Content<R>,
780 state: &mut State<H>,
781 on_edit: &dyn Fn(Action) -> Message,
782 clipboard: &mut dyn Clipboard,
783 shell: &mut Shell<'_, Message>,
784 ) {
785 let mut publish =
786 |action| shell.publish(on_edit(action));
787
788 match binding {
789 Binding::Unfocus => {
790 state.focus = None;
791 state.drag_click = None;
792 }
793 Binding::Copy => {
794 if let Some(selection) = content.selection() {
795 clipboard.write(
796 clipboard::Kind::Standard,
797 selection,
798 );
799 }
800 }
801 Binding::Cut => {
802 if let Some(selection) = content.selection() {
803 clipboard.write(
804 clipboard::Kind::Standard,
805 selection,
806 );
807
808 publish(Action::Edit(Edit::Delete));
809 }
810 }
811 Binding::Paste => {
812 if let Some(contents) =
813 clipboard.read(clipboard::Kind::Standard)
814 {
815 publish(Action::Edit(Edit::Paste(
816 Arc::new(contents),
817 )));
818 }
819 }
820 Binding::Move(motion) => {
821 publish(Action::Move(motion));
822 }
823 Binding::Select(motion) => {
824 publish(Action::Select(motion));
825 }
826 Binding::SelectWord => {
827 publish(Action::SelectWord);
828 }
829 Binding::SelectLine => {
830 publish(Action::SelectLine);
831 }
832 Binding::SelectAll => {
833 publish(Action::SelectAll);
834 }
835 Binding::Insert(c) => {
836 publish(Action::Edit(Edit::Insert(c)));
837 }
838 Binding::Enter => {
839 publish(Action::Edit(Edit::Enter));
840 }
841 Binding::Backspace => {
842 publish(Action::Edit(Edit::Backspace));
843 }
844 Binding::Delete => {
845 publish(Action::Edit(Edit::Delete));
846 }
847 Binding::Sequence(sequence) => {
848 for binding in sequence {
849 apply_binding(
850 binding, content, state, on_edit,
851 clipboard, shell,
852 );
853 }
854 }
855 Binding::Custom(message) => {
856 shell.publish(message);
857 }
858 }
859 }
860
861 if !matches!(binding, Binding::Unfocus) {
862 shell.capture_event();
863 }
864
865 apply_binding(
866 binding,
867 self.content,
868 state,
869 on_edit,
870 clipboard,
871 shell,
872 );
873
874 if let Some(focus) = &mut state.focus {
875 focus.updated_at = Instant::now();
876 }
877 }
878 }
879 }
880
881 let status = {
882 let is_disabled = self.on_edit.is_none();
883 let is_hovered = cursor.is_over(layout.bounds());
884
885 if is_disabled {
886 Status::Disabled
887 } else if state.focus.is_some() {
888 Status::Focused { is_hovered }
889 } else if is_hovered {
890 Status::Hovered
891 } else {
892 Status::Active
893 }
894 };
895
896 if is_redraw {
897 self.last_status = Some(status);
898
899 shell.request_input_method(
900 &self.input_method(state, renderer, layout),
901 );
902 } else if self
903 .last_status
904 .is_some_and(|last_status| status != last_status)
905 {
906 shell.request_redraw();
907 }
908 }
909
910 fn draw(
911 &self,
912 tree: &widget::Tree,
913 renderer: &mut Renderer,
914 theme: &Theme,
915 _defaults: &renderer::Style,
916 layout: Layout<'_>,
917 _cursor: mouse::Cursor,
918 _viewport: &Rectangle,
919 ) {
920 let bounds = layout.bounds();
921
922 let mut internal = self.content.0.borrow_mut();
923 let state = tree.state.downcast_ref::<State<Highlighter>>();
924
925 let font = self.font.unwrap_or_else(|| renderer.default_font());
926
927 internal.editor.highlight(
928 font,
929 state.highlighter.borrow_mut().deref_mut(),
930 |highlight| (self.highlighter_format)(highlight, theme),
931 );
932
933 let style = theme
934 .style(&self.class, self.last_status.unwrap_or(Status::Active));
935
936 renderer.fill_quad(
937 renderer::Quad {
938 bounds,
939 border: style.border,
940 ..renderer::Quad::default()
941 },
942 style.background,
943 );
944
945 let text_bounds = bounds.shrink(self.padding);
946
947 if internal.editor.is_empty() {
948 if let Some(placeholder) = self.placeholder.clone() {
949 renderer.fill_text(
950 Text {
951 content: placeholder.into_owned(),
952 bounds: text_bounds.size(),
953 size: self
954 .text_size
955 .unwrap_or_else(|| renderer.default_size()),
956 line_height: self.line_height,
957 font,
958 align_x: text::Alignment::Default,
959 align_y: alignment::Vertical::Top,
960 shaping: text::Shaping::Advanced,
961 wrapping: self.wrapping,
962 },
963 text_bounds.position(),
964 style.placeholder,
965 text_bounds,
966 );
967 }
968 } else {
969 renderer.fill_editor(
970 &internal.editor,
971 text_bounds.position(),
972 style.value,
973 text_bounds,
974 );
975 }
976
977 let translation = text_bounds.position() - Point::ORIGIN;
978
979 if let Some(focus) = state.focus.as_ref() {
980 match internal.editor.cursor() {
981 Cursor::Caret(position) if focus.is_cursor_visible() => {
982 let cursor =
983 Rectangle::new(
984 position + translation,
985 Size::new(
986 1.0,
987 self.line_height
988 .to_absolute(self.text_size.unwrap_or_else(
989 || renderer.default_size(),
990 ))
991 .into(),
992 ),
993 );
994
995 if let Some(clipped_cursor) =
996 text_bounds.intersection(&cursor)
997 {
998 renderer.fill_quad(
999 renderer::Quad {
1000 bounds: clipped_cursor,
1001 ..renderer::Quad::default()
1002 },
1003 style.value,
1004 );
1005 }
1006 }
1007 Cursor::Selection(ranges) => {
1008 for range in ranges.into_iter().filter_map(|range| {
1009 text_bounds.intersection(&(range + translation))
1010 }) {
1011 renderer.fill_quad(
1012 renderer::Quad {
1013 bounds: range,
1014 ..renderer::Quad::default()
1015 },
1016 style.selection,
1017 );
1018 }
1019 }
1020 Cursor::Caret(_) => {}
1021 }
1022 }
1023 }
1024
1025 fn mouse_interaction(
1026 &self,
1027 _state: &widget::Tree,
1028 layout: Layout<'_>,
1029 cursor: mouse::Cursor,
1030 _viewport: &Rectangle,
1031 _renderer: &Renderer,
1032 ) -> mouse::Interaction {
1033 let is_disabled = self.on_edit.is_none();
1034
1035 if cursor.is_over(layout.bounds()) {
1036 if is_disabled {
1037 mouse::Interaction::NotAllowed
1038 } else {
1039 mouse::Interaction::Text
1040 }
1041 } else {
1042 mouse::Interaction::default()
1043 }
1044 }
1045
1046 fn operate(
1047 &self,
1048 tree: &mut widget::Tree,
1049 layout: Layout<'_>,
1050 _renderer: &Renderer,
1051 operation: &mut dyn widget::Operation,
1052 ) {
1053 let state = tree.state.downcast_mut::<State<Highlighter>>();
1054
1055 operation.focusable(None, layout.bounds(), state);
1056 }
1057}
1058
1059impl<'a, Highlighter, Message, Theme, Renderer>
1060 From<TextEditor<'a, Highlighter, Message, Theme, Renderer>>
1061 for Element<'a, Message, Theme, Renderer>
1062where
1063 Highlighter: text::Highlighter,
1064 Message: 'a,
1065 Theme: Catalog + 'a,
1066 Renderer: text::Renderer,
1067{
1068 fn from(
1069 text_editor: TextEditor<'a, Highlighter, Message, Theme, Renderer>,
1070 ) -> Self {
1071 Self::new(text_editor)
1072 }
1073}
1074
1075#[derive(Debug, Clone, PartialEq)]
1077pub enum Binding<Message> {
1078 Unfocus,
1080 Copy,
1082 Cut,
1084 Paste,
1086 Move(Motion),
1088 Select(Motion),
1090 SelectWord,
1092 SelectLine,
1094 SelectAll,
1096 Insert(char),
1098 Enter,
1100 Backspace,
1102 Delete,
1104 Sequence(Vec<Self>),
1106 Custom(Message),
1108}
1109
1110#[derive(Debug, Clone, PartialEq, Eq)]
1112pub struct KeyPress {
1113 pub key: keyboard::Key,
1115 pub modifiers: keyboard::Modifiers,
1117 pub text: Option<SmolStr>,
1119 pub status: Status,
1121}
1122
1123impl<Message> Binding<Message> {
1124 pub fn from_key_press(event: KeyPress) -> Option<Self> {
1126 let KeyPress {
1127 key,
1128 modifiers,
1129 text,
1130 status,
1131 } = event;
1132
1133 if !matches!(status, Status::Focused { .. }) {
1134 return None;
1135 }
1136
1137 match key.as_ref() {
1138 keyboard::Key::Named(key::Named::Enter) => Some(Self::Enter),
1139 keyboard::Key::Named(key::Named::Backspace) => {
1140 Some(Self::Backspace)
1141 }
1142 keyboard::Key::Named(key::Named::Delete)
1143 if text.is_none() || text.as_deref() == Some("\u{7f}") =>
1144 {
1145 Some(Self::Delete)
1146 }
1147 keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus),
1148 keyboard::Key::Character("c") if modifiers.command() => {
1149 Some(Self::Copy)
1150 }
1151 keyboard::Key::Character("x") if modifiers.command() => {
1152 Some(Self::Cut)
1153 }
1154 keyboard::Key::Character("v")
1155 if modifiers.command() && !modifiers.alt() =>
1156 {
1157 Some(Self::Paste)
1158 }
1159 keyboard::Key::Character("a") if modifiers.command() => {
1160 Some(Self::SelectAll)
1161 }
1162 _ => {
1163 if let Some(text) = text {
1164 let c = text.chars().find(|c| !c.is_control())?;
1165
1166 Some(Self::Insert(c))
1167 } else if let keyboard::Key::Named(named_key) = key.as_ref() {
1168 let motion = motion(named_key)?;
1169
1170 let motion = if modifiers.macos_command() {
1171 match motion {
1172 Motion::Left => Motion::Home,
1173 Motion::Right => Motion::End,
1174 _ => motion,
1175 }
1176 } else {
1177 motion
1178 };
1179
1180 let motion = if modifiers.jump() {
1181 motion.widen()
1182 } else {
1183 motion
1184 };
1185
1186 Some(if modifiers.shift() {
1187 Self::Select(motion)
1188 } else {
1189 Self::Move(motion)
1190 })
1191 } else {
1192 None
1193 }
1194 }
1195 }
1196 }
1197}
1198
1199enum Update<Message> {
1200 Click(mouse::Click),
1201 Drag(Point),
1202 Release,
1203 Scroll(f32),
1204 InputMethod(Ime),
1205 Binding(Binding<Message>),
1206}
1207
1208enum Ime {
1209 Toggle(bool),
1210 Preedit {
1211 content: String,
1212 selection: Option<Range<usize>>,
1213 },
1214 Commit(String),
1215}
1216
1217impl<Message> Update<Message> {
1218 fn from_event<H: Highlighter>(
1219 event: &Event,
1220 state: &State<H>,
1221 bounds: Rectangle,
1222 padding: Padding,
1223 cursor: mouse::Cursor,
1224 key_binding: Option<&dyn Fn(KeyPress) -> Option<Binding<Message>>>,
1225 ) -> Option<Self> {
1226 let binding = |binding| Some(Update::Binding(binding));
1227
1228 match event {
1229 Event::Mouse(event) => match event {
1230 mouse::Event::ButtonPressed(mouse::Button::Left) => {
1231 if let Some(cursor_position) = cursor.position_in(bounds) {
1232 let cursor_position = cursor_position
1233 - Vector::new(padding.top, padding.left);
1234
1235 let click = mouse::Click::new(
1236 cursor_position,
1237 mouse::Button::Left,
1238 state.last_click,
1239 );
1240
1241 Some(Update::Click(click))
1242 } else if state.focus.is_some() {
1243 binding(Binding::Unfocus)
1244 } else {
1245 None
1246 }
1247 }
1248 mouse::Event::ButtonReleased(mouse::Button::Left) => {
1249 Some(Update::Release)
1250 }
1251 mouse::Event::CursorMoved { .. } => match state.drag_click {
1252 Some(mouse::click::Kind::Single) => {
1253 let cursor_position = cursor.position_in(bounds)?
1254 - Vector::new(padding.top, padding.left);
1255
1256 Some(Update::Drag(cursor_position))
1257 }
1258 _ => None,
1259 },
1260 mouse::Event::WheelScrolled { delta }
1261 if cursor.is_over(bounds) =>
1262 {
1263 Some(Update::Scroll(match delta {
1264 mouse::ScrollDelta::Lines { y, .. } => {
1265 if y.abs() > 0.0 {
1266 y.signum() * -(y.abs() * 4.0).max(1.0)
1267 } else {
1268 0.0
1269 }
1270 }
1271 mouse::ScrollDelta::Pixels { y, .. } => -y / 4.0,
1272 }))
1273 }
1274 _ => None,
1275 },
1276 Event::InputMethod(event) => match event {
1277 input_method::Event::Opened | input_method::Event::Closed => {
1278 Some(Update::InputMethod(Ime::Toggle(matches!(
1279 event,
1280 input_method::Event::Opened
1281 ))))
1282 }
1283 input_method::Event::Preedit(content, selection)
1284 if state.focus.is_some() =>
1285 {
1286 Some(Update::InputMethod(Ime::Preedit {
1287 content: content.clone(),
1288 selection: selection.clone(),
1289 }))
1290 }
1291 input_method::Event::Commit(content)
1292 if state.focus.is_some() =>
1293 {
1294 Some(Update::InputMethod(Ime::Commit(content.clone())))
1295 }
1296 _ => None,
1297 },
1298 Event::Keyboard(keyboard::Event::KeyPressed {
1299 key,
1300 modifiers,
1301 text,
1302 ..
1303 }) => {
1304 let status = if state.focus.is_some() {
1305 Status::Focused {
1306 is_hovered: cursor.is_over(bounds),
1307 }
1308 } else {
1309 Status::Active
1310 };
1311
1312 let key_press = KeyPress {
1313 key: key.clone(),
1314 modifiers: *modifiers,
1315 text: text.clone(),
1316 status,
1317 };
1318
1319 if let Some(key_binding) = key_binding {
1320 key_binding(key_press)
1321 } else {
1322 Binding::from_key_press(key_press)
1323 }
1324 .map(Self::Binding)
1325 }
1326 _ => None,
1327 }
1328 }
1329}
1330
1331fn motion(key: key::Named) -> Option<Motion> {
1332 match key {
1333 key::Named::ArrowLeft => Some(Motion::Left),
1334 key::Named::ArrowRight => Some(Motion::Right),
1335 key::Named::ArrowUp => Some(Motion::Up),
1336 key::Named::ArrowDown => Some(Motion::Down),
1337 key::Named::Home => Some(Motion::Home),
1338 key::Named::End => Some(Motion::End),
1339 key::Named::PageUp => Some(Motion::PageUp),
1340 key::Named::PageDown => Some(Motion::PageDown),
1341 _ => None,
1342 }
1343}
1344
1345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1347pub enum Status {
1348 Active,
1350 Hovered,
1352 Focused {
1354 is_hovered: bool,
1356 },
1357 Disabled,
1359}
1360
1361#[derive(Debug, Clone, Copy, PartialEq)]
1363pub struct Style {
1364 pub background: Background,
1366 pub border: Border,
1368 pub icon: Color,
1370 pub placeholder: Color,
1372 pub value: Color,
1374 pub selection: Color,
1376}
1377
1378pub trait Catalog {
1380 type Class<'a>;
1382
1383 fn default<'a>() -> Self::Class<'a>;
1385
1386 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
1388}
1389
1390pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
1392
1393impl Catalog for Theme {
1394 type Class<'a> = StyleFn<'a, Self>;
1395
1396 fn default<'a>() -> Self::Class<'a> {
1397 Box::new(default)
1398 }
1399
1400 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
1401 class(self, status)
1402 }
1403}
1404
1405pub fn default(theme: &Theme, status: Status) -> Style {
1407 let palette = theme.extended_palette();
1408
1409 let active = Style {
1410 background: Background::Color(palette.background.base.color),
1411 border: Border {
1412 radius: 2.0.into(),
1413 width: 1.0,
1414 color: palette.background.strong.color,
1415 },
1416 icon: palette.background.weak.text,
1417 placeholder: palette.background.strong.color,
1418 value: palette.background.base.text,
1419 selection: palette.primary.weak.color,
1420 };
1421
1422 match status {
1423 Status::Active => active,
1424 Status::Hovered => Style {
1425 border: Border {
1426 color: palette.background.base.text,
1427 ..active.border
1428 },
1429 ..active
1430 },
1431 Status::Focused { .. } => Style {
1432 border: Border {
1433 color: palette.primary.strong.color,
1434 ..active.border
1435 },
1436 ..active
1437 },
1438 Status::Disabled => Style {
1439 background: Background::Color(palette.background.weak.color),
1440 value: active.placeholder,
1441 ..active
1442 },
1443 }
1444}