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