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