1use crate::core::alignment;
34use crate::core::clipboard;
35use crate::core::layout::{self, Layout};
36use crate::core::mouse;
37use crate::core::renderer;
38use crate::core::text::editor::{self, Editor as _};
39use crate::core::text::highlighter;
40use crate::core::text::parser;
41use crate::core::text::{self, LineHeight, Text, Wrapping};
42use crate::core::theme;
43use crate::core::widget::{self, Widget};
44use crate::core::window;
45use crate::core::{
46 Background, Border, Color, Event, Font, Length, Padding, Pixels, Rectangle, Shell, Size, Theme,
47};
48
49use std::borrow::Cow;
50use std::cell::RefCell;
51use std::fmt;
52use std::ops::DerefMut;
53
54pub use text::Highlighter;
55pub use text::editor::{
56 Action, Binding, Cursor, Edit, KeyPress, Line, LineEnding, Motion, Selection,
57};
58
59pub struct TextEditor<'a, Parser, Message, Theme = crate::Theme, Renderer = crate::Renderer>
92where
93 Parser: text::Parser,
94 Theme: Catalog,
95 Renderer: text::Renderer,
96{
97 id: Option<widget::Id>,
98 content: &'a Content<Renderer>,
99 placeholder: Option<text::Fragment<'a>>,
100 font: Option<Font>,
101 text_size: Option<Pixels>,
102 line_height: Option<LineHeight>,
103 width: Length,
104 height: Length,
105 padding: Padding,
106 wrapping: Wrapping,
107 class: Theme::Class<'a>,
108 key_binding: Option<Box<dyn Fn(KeyPress) -> Option<Binding<Message>> + 'a>>,
109 on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
110 parser_settings: Parser::Settings,
111 highlighter: Option<Box<dyn text::Highlighter<Parser::Output, Theme> + 'a>>,
112 last_status: Option<Status>,
113}
114
115impl<'a, Message, Theme, Renderer> TextEditor<'a, parser::PlainText, Message, Theme, Renderer>
116where
117 Theme: Catalog,
118 Renderer: text::Renderer,
119{
120 pub fn new(content: &'a Content<Renderer>) -> Self {
122 Self {
123 id: None,
124 content,
125 placeholder: None,
126 font: None,
127 text_size: None,
128 line_height: None,
129 width: Length::Fill,
130 height: Length::Fit,
131 padding: Padding::new(5.0),
132 wrapping: Wrapping::default(),
133 class: <Theme as Catalog>::default(),
134 key_binding: None,
135 on_edit: None,
136 parser_settings: (),
137 highlighter: None,
138 last_status: None,
139 }
140 }
141}
142
143impl<'a, Message, Renderer> TextEditor<'a, parser::PlainText, Message, crate::Theme, Renderer>
144where
145 Renderer: text::Renderer,
146{
147 #[cfg(feature = "highlighter")]
168 pub fn highlight(
169 self,
170 syntax: &str,
171 ) -> TextEditor<'a, iced_highlighter::Parser, Message, crate::Theme, Renderer>
172 where
173 Renderer: text::Renderer,
174 {
175 self.highlight_with(
176 iced_highlighter::Settings {
177 token: syntax.to_owned(),
178 },
179 crate::core::Code::highlight,
180 )
181 }
182}
183
184impl<'a, Parser, Message, Theme, Renderer> TextEditor<'a, Parser, Message, Theme, Renderer>
185where
186 Parser: text::Parser,
187 Theme: Catalog,
188 Renderer: text::Renderer,
189{
190 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
192 self.id = Some(id.into());
193 self
194 }
195
196 pub fn placeholder(mut self, placeholder: impl text::IntoFragment<'a>) -> Self {
198 self.placeholder = Some(placeholder.into_fragment());
199 self
200 }
201
202 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
204 self.width = Length::from(width.into());
205 self
206 }
207
208 pub fn height(mut self, height: impl Into<Length>) -> Self {
210 self.height = height.into();
211 self
212 }
213
214 pub fn on_action(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> Self {
219 self.on_edit = Some(Box::new(on_edit));
220 self
221 }
222
223 pub fn font(mut self, font: impl Into<Font>) -> Self {
227 self.font = Some(font.into());
228 self
229 }
230
231 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
233 self.text_size = Some(size.into());
234 self
235 }
236
237 pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
239 self.line_height = Some(line_height.into());
240 self
241 }
242
243 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
245 self.padding = padding.into();
246 self
247 }
248
249 pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
251 self.wrapping = wrapping;
252 self
253 }
254
255 pub fn highlight_with<P: text::Parser>(
258 self,
259 settings: P::Settings,
260 highlighter: impl text::Highlighter<P::Output, Theme> + 'a,
261 ) -> TextEditor<'a, P, Message, Theme, Renderer> {
262 TextEditor {
263 id: self.id,
264 content: self.content,
265 placeholder: self.placeholder,
266 font: self.font,
267 text_size: self.text_size,
268 line_height: self.line_height,
269 width: self.width,
270 height: self.height,
271 padding: self.padding,
272 wrapping: self.wrapping,
273 class: self.class,
274 key_binding: self.key_binding,
275 on_edit: self.on_edit,
276 parser_settings: settings,
277 highlighter: Some(Box::new(highlighter)),
278 last_status: self.last_status,
279 }
280 }
281
282 pub fn key_binding(
286 mut self,
287 key_binding: impl Fn(KeyPress) -> Option<Binding<Message>> + 'a,
288 ) -> Self {
289 self.key_binding = Some(Box::new(key_binding));
290 self
291 }
292
293 #[must_use]
295 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
296 where
297 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
298 {
299 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
300 self
301 }
302
303 #[cfg(feature = "advanced")]
305 #[must_use]
306 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
307 self.class = class.into();
308 self
309 }
310}
311
312struct State<Parser: text::Parser> {
313 editor: editor::State,
314 parser: RefCell<Parser>,
315 parser_settings: Parser::Settings,
316 last_theme: RefCell<Option<String>>,
317}
318
319impl<Parser, Message, Theme, Renderer> widget::Meta
320 for TextEditor<'_, Parser, Message, Theme, Renderer>
321where
322 Parser: text::Parser,
323 Theme: Catalog,
324 Renderer: text::Renderer,
325{
326}
327
328impl<Parser, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
329 for TextEditor<'_, Parser, Message, Theme, Renderer>
330where
331 Parser: text::Parser,
332 Theme: Catalog,
333 Renderer: text::Renderer,
334{
335 fn tag(&self) -> widget::tree::Tag {
336 widget::tree::Tag::of::<State<Parser>>()
337 }
338
339 fn state(&self) -> widget::tree::State {
340 widget::tree::State::new(State {
341 editor: editor::State::new(),
342 parser: RefCell::new(Parser::new(&self.parser_settings)),
343 parser_settings: self.parser_settings.clone(),
344 last_theme: RefCell::new(None),
345 })
346 }
347
348 fn size(&self) -> Size<Length> {
349 Size {
350 width: self.width,
351 height: self.height,
352 }
353 }
354
355 fn layout(&mut self, tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits) {
356 let mut internal = self.content.0.borrow_mut();
357 let state = tree.state.downcast_mut::<State<Parser>>();
358
359 if state.parser_settings != self.parser_settings {
360 state.parser.borrow_mut().update(&self.parser_settings);
361
362 state.parser_settings = self.parser_settings.clone();
363 }
364
365 let limits = limits
366 .width(self.width)
367 .height(self.height)
368 .shrink(self.padding);
369
370 internal.editor.update(
371 limits.bounds(),
372 self.font.unwrap_or_else(|| renderer.font()),
373 self.text_size.unwrap_or_else(|| renderer.text_size()),
374 self.line_height.unwrap_or_else(|| renderer.line_height()),
375 self.wrapping,
376 text::Alignment::Default,
377 renderer.hint_factor(),
378 state.parser.borrow_mut().deref_mut(),
379 );
380
381 let bounds = limits.resolve(self.width, self.height, internal.editor.min_bounds());
382
383 tree.size = bounds.expand(self.padding);
384 }
385
386 fn update(
387 &mut self,
388 tree: &mut widget::Tree,
389 event: &Event,
390 layout: Layout,
391 cursor: mouse::Cursor,
392 _renderer: &Renderer,
393 shell: &mut Shell<'_, Message>,
394 _viewport: &Rectangle,
395 ) {
396 let Some(on_edit) = self.on_edit.as_ref() else {
397 return;
398 };
399
400 let state = tree.state.downcast_mut::<State<Parser>>();
401 let is_redraw = matches!(event, Event::Window(window::Event::RedrawRequested(_now)),);
402
403 let editor = &self.content.0.borrow().editor;
404
405 fn apply_update<Message>(
406 update: editor::Update<Message>,
407 shell: &mut Shell<'_, Message>,
408 on_edit: &impl Fn(editor::Action) -> Message,
409 ) {
410 match update {
411 editor::Update::Action(action) => {
412 shell.publish(on_edit(action));
413 shell.capture_event();
414 }
415 editor::Update::Release => {}
416 editor::Update::Custom(message) => {
417 shell.publish(message);
418 shell.capture_event();
419 }
420 editor::Update::Sequence(updates) => {
421 for update in updates {
422 apply_update(update, shell, on_edit);
423 }
424 }
425 editor::Update::Copy(content) => {
426 shell.write_clipboard(clipboard::Content::Text(content));
427 shell.capture_event();
428 }
429 editor::Update::Paste => {
430 shell.read_clipboard(clipboard::Kind::Text);
431 shell.capture_event();
432 }
433 editor::Update::RedrawAt(at) => {
434 shell.request_redraw_at(at);
435 }
436 editor::Update::Focus | editor::Update::Unfocus | editor::Update::InputMethod => {
437 shell.request_redraw();
438 }
439 }
440 }
441
442 if let Some(update) = state.editor.update(
443 &self.content.0.borrow().editor,
444 event,
445 layout.bounds(),
446 self.padding,
447 cursor,
448 self.key_binding
449 .as_deref()
450 .unwrap_or(&Binding::from_key_press as _),
451 ) {
452 apply_update(update, shell, on_edit);
453 }
454
455 let status = {
456 let is_disabled = self.on_edit.is_none();
457 let is_hovered = cursor.is_over(layout.bounds());
458
459 if is_disabled {
460 Status::Disabled
461 } else if state.editor.is_focused() {
462 Status::Focused { is_hovered }
463 } else if is_hovered {
464 Status::Hovered
465 } else {
466 Status::Active
467 }
468 };
469
470 if is_redraw {
471 self.last_status = Some(status);
472
473 shell.request_input_method(
474 &state
475 .editor
476 .input_method(editor, layout.bounds().shrink(self.padding).position()),
477 );
478 } else if self
479 .last_status
480 .is_some_and(|last_status| status != last_status)
481 {
482 shell.request_redraw();
483 }
484 }
485
486 fn draw(
487 &self,
488 tree: &widget::Tree,
489 renderer: &mut Renderer,
490 theme: &Theme,
491 _defaults: &renderer::Style,
492 layout: Layout,
493 _cursor: mouse::Cursor,
494 viewport: &Rectangle,
495 ) {
496 let bounds = layout.bounds();
497
498 let mut internal = self.content.0.borrow_mut();
499 let state = tree.state.downcast_ref::<State<Parser>>();
500
501 let font = self.font.unwrap_or_else(|| renderer.font());
502
503 let theme_name = theme.name();
504
505 if state
506 .last_theme
507 .borrow()
508 .as_ref()
509 .is_none_or(|last_theme| last_theme != theme_name)
510 {
511 state.parser.borrow_mut().change_line(0);
512 let _ = state.last_theme.borrow_mut().replace(theme_name.to_owned());
513 }
514
515 internal
516 .editor
517 .highlight(font, state.parser.borrow_mut().deref_mut(), |output| {
518 let Some(highlighter) = &self.highlighter else {
519 return highlighter::Style::default();
520 };
521
522 highlighter.highlight(output, theme)
523 });
524
525 let style = theme.style(&self.class, self.last_status.unwrap_or(Status::Active));
526
527 renderer.fill_quad(
528 renderer::Quad {
529 bounds,
530 border: style.border,
531 ..renderer::Quad::default()
532 },
533 style.background,
534 );
535
536 let text_bounds = bounds.shrink(self.padding);
537
538 if internal.editor.is_empty()
539 && let Some(placeholder) = &self.placeholder
540 {
541 renderer.fill_text(
542 Text {
543 content: placeholder.clone().into_owned(),
544 bounds: text_bounds.size(),
545 size: self.text_size.unwrap_or_else(|| renderer.text_size()),
546 line_height: self.line_height.unwrap_or_else(|| renderer.line_height()),
547 font,
548 align_x: text::Alignment::Default,
549 align_y: alignment::Vertical::Top,
550 shaping: text::Shaping::Advanced,
551 wrapping: self.wrapping,
552 ellipsis: text::Ellipsis::None,
553 hint_factor: renderer.hint_factor(),
554 },
555 text_bounds.position(),
556 style.placeholder,
557 text_bounds,
558 );
559 }
560
561 state.editor.draw(
562 &internal.editor,
563 renderer,
564 text_bounds.position(),
565 *viewport,
566 editor::Style {
567 value: style.value,
568 selection: style.selection,
569 },
570 );
571 }
572
573 fn mouse_interaction(
574 &self,
575 _tree: &widget::Tree,
576 layout: Layout,
577 cursor: mouse::Cursor,
578 _viewport: &Rectangle,
579 _renderer: &Renderer,
580 ) -> mouse::Interaction {
581 let is_disabled = self.on_edit.is_none();
582
583 if cursor.is_over(layout.bounds()) {
584 if is_disabled {
585 mouse::Interaction::NotAllowed
586 } else {
587 mouse::Interaction::Text
588 }
589 } else {
590 mouse::Interaction::default()
591 }
592 }
593
594 fn operate(
595 &mut self,
596 tree: &mut widget::Tree,
597 layout: Layout,
598 _viewport: &Rectangle,
599 _renderer: &Renderer,
600 operation: &mut dyn widget::Operation,
601 ) {
602 let state = tree.state.downcast_mut::<State<Parser>>();
603
604 operation.focusable(self.id.as_ref(), layout.bounds(), &mut state.editor);
605 operation.text_input(
606 self.id.as_ref(),
607 layout.bounds(),
608 &mut self.content.0.borrow_mut().editor,
609 );
610 }
611}
612
613pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>)
615where
616 R: text::Renderer;
617
618struct Internal<R>
619where
620 R: text::Renderer,
621{
622 editor: R::Editor,
623}
624
625impl<R> Content<R>
626where
627 R: text::Renderer,
628{
629 pub fn new() -> Self {
631 Self::with_text("")
632 }
633
634 pub fn with_text(text: &str) -> Self {
636 Self(RefCell::new(Internal {
637 editor: R::Editor::with_text(text),
638 }))
639 }
640
641 pub fn perform(&mut self, action: Action) {
643 let internal = self.0.get_mut();
644
645 internal.editor.perform(action);
646 }
647
648 pub fn move_to(&mut self, cursor: Cursor) {
650 let internal = self.0.get_mut();
651
652 internal.editor.move_to(cursor);
653 }
654
655 pub fn cursor(&self) -> Cursor {
657 self.0.borrow().editor.cursor()
658 }
659
660 pub fn line_count(&self) -> usize {
662 self.0.borrow().editor.line_count()
663 }
664
665 pub fn line(&self, index: usize) -> Option<Line<'_>> {
667 let internal = self.0.borrow();
668 let line = internal.editor.line(index)?;
669
670 Some(Line {
671 text: Cow::Owned(line.text.into_owned()),
672 ending: line.ending,
673 })
674 }
675
676 pub fn lines(&self) -> impl Iterator<Item = Line<'_>> {
678 (0..)
679 .map(|i| self.line(i))
680 .take_while(Option::is_some)
681 .flatten()
682 }
683
684 pub fn text(&self) -> String {
686 self.0.borrow().editor.text()
687 }
688
689 pub fn selection(&self) -> Option<String> {
691 self.0.borrow().editor.copy()
692 }
693
694 pub fn line_ending(&self) -> Option<LineEnding> {
696 Some(self.line(0)?.ending)
697 }
698
699 pub fn is_empty(&self) -> bool {
701 self.0.borrow().editor.is_empty()
702 }
703}
704
705impl<Renderer> Clone for Content<Renderer>
706where
707 Renderer: text::Renderer,
708{
709 fn clone(&self) -> Self {
710 Self::with_text(&self.text())
711 }
712}
713
714impl<Renderer> Default for Content<Renderer>
715where
716 Renderer: text::Renderer,
717{
718 fn default() -> Self {
719 Self::new()
720 }
721}
722
723impl<Renderer> fmt::Debug for Content<Renderer>
724where
725 Renderer: text::Renderer,
726 Renderer::Editor: fmt::Debug,
727{
728 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
729 let internal = self.0.borrow();
730
731 f.debug_struct("Content")
732 .field("editor", &internal.editor)
733 .finish()
734 }
735}
736
737#[derive(Debug, Clone, Copy, PartialEq, Eq)]
739pub enum Status {
740 Active,
742 Hovered,
744 Focused {
746 is_hovered: bool,
748 },
749 Disabled,
751}
752
753#[derive(Debug, Clone, Copy, PartialEq)]
755pub struct Style {
756 pub background: Background,
758 pub border: Border,
760 pub placeholder: Color,
762 pub value: Color,
764 pub selection: Color,
766}
767
768pub trait Catalog: theme::Base {
770 type Class<'a>;
772
773 fn default<'a>() -> Self::Class<'a>;
775
776 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
778}
779
780pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
782
783impl Catalog for Theme {
784 type Class<'a> = StyleFn<'a, Self>;
785
786 fn default<'a>() -> Self::Class<'a> {
787 Box::new(default)
788 }
789
790 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
791 class(self, status)
792 }
793}
794
795pub fn default(theme: &Theme, status: Status) -> Style {
797 let palette = theme.palette();
798
799 let active = Style {
800 background: Background::Color(palette.background.weakest.color),
801 border: Border {
802 radius: 2.0.into(),
803 width: 1.0,
804 color: palette.background.strong.color,
805 },
806 placeholder: palette.secondary.base.color,
807 value: palette.background.weakest.text,
808 selection: palette.background.strongest.color,
809 };
810
811 match status {
812 Status::Active => active,
813 Status::Hovered => Style {
814 border: Border {
815 color: palette.background.base.text,
816 ..active.border
817 },
818 ..active
819 },
820 Status::Focused { .. } => Style {
821 border: Border {
822 color: palette.primary.strong.color,
823 ..active.border
824 },
825 ..active
826 },
827 Status::Disabled => Style {
828 background: Background::Color(palette.background.base.color),
829 value: active.placeholder,
830 placeholder: palette.background.strongest.color,
831 ..active
832 },
833 }
834}