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 }
419 editor::Update::Sequence(updates) => {
420 for update in updates {
421 apply_update(update, shell, on_edit);
422 }
423 }
424 editor::Update::Copy(content) => {
425 shell.write_clipboard(clipboard::Content::Text(content));
426 }
427 editor::Update::Paste => {
428 shell.read_clipboard(clipboard::Kind::Text);
429 }
430 editor::Update::RedrawAt(at) => {
431 shell.request_redraw_at(at);
432 }
433 editor::Update::Focus | editor::Update::Unfocus | editor::Update::InputMethod => {
434 shell.request_redraw();
435 }
436 }
437 }
438
439 if let Some(update) = state.editor.update(
440 &self.content.0.borrow().editor,
441 event,
442 layout.bounds(),
443 self.padding,
444 cursor,
445 self.key_binding
446 .as_deref()
447 .unwrap_or(&Binding::from_key_press as _),
448 ) {
449 apply_update(update, shell, on_edit);
450 }
451
452 let status = {
453 let is_disabled = self.on_edit.is_none();
454 let is_hovered = cursor.is_over(layout.bounds());
455
456 if is_disabled {
457 Status::Disabled
458 } else if state.editor.is_focused() {
459 Status::Focused { is_hovered }
460 } else if is_hovered {
461 Status::Hovered
462 } else {
463 Status::Active
464 }
465 };
466
467 if is_redraw {
468 self.last_status = Some(status);
469
470 shell.request_input_method(
471 &state
472 .editor
473 .input_method(editor, layout.bounds().shrink(self.padding).position()),
474 );
475 } else if self
476 .last_status
477 .is_some_and(|last_status| status != last_status)
478 {
479 shell.request_redraw();
480 }
481 }
482
483 fn draw(
484 &self,
485 tree: &widget::Tree,
486 renderer: &mut Renderer,
487 theme: &Theme,
488 _defaults: &renderer::Style,
489 layout: Layout,
490 _cursor: mouse::Cursor,
491 viewport: &Rectangle,
492 ) {
493 let bounds = layout.bounds();
494
495 let mut internal = self.content.0.borrow_mut();
496 let state = tree.state.downcast_ref::<State<Parser>>();
497
498 let font = self.font.unwrap_or_else(|| renderer.font());
499
500 let theme_name = theme.name();
501
502 if state
503 .last_theme
504 .borrow()
505 .as_ref()
506 .is_none_or(|last_theme| last_theme != theme_name)
507 {
508 state.parser.borrow_mut().change_line(0);
509 let _ = state.last_theme.borrow_mut().replace(theme_name.to_owned());
510 }
511
512 internal
513 .editor
514 .highlight(font, state.parser.borrow_mut().deref_mut(), |output| {
515 let Some(highlighter) = &self.highlighter else {
516 return highlighter::Style::default();
517 };
518
519 highlighter.highlight(output, theme)
520 });
521
522 let style = theme.style(&self.class, self.last_status.unwrap_or(Status::Active));
523
524 renderer.fill_quad(
525 renderer::Quad {
526 bounds,
527 border: style.border,
528 ..renderer::Quad::default()
529 },
530 style.background,
531 );
532
533 let text_bounds = bounds.shrink(self.padding);
534
535 if internal.editor.is_empty()
536 && let Some(placeholder) = &self.placeholder
537 {
538 renderer.fill_text(
539 Text {
540 content: placeholder.clone().into_owned(),
541 bounds: text_bounds.size(),
542 size: self.text_size.unwrap_or_else(|| renderer.text_size()),
543 line_height: self.line_height.unwrap_or_else(|| renderer.line_height()),
544 font,
545 align_x: text::Alignment::Default,
546 align_y: alignment::Vertical::Top,
547 shaping: text::Shaping::Advanced,
548 wrapping: self.wrapping,
549 ellipsis: text::Ellipsis::None,
550 hint_factor: renderer.hint_factor(),
551 },
552 text_bounds.position(),
553 style.placeholder,
554 text_bounds,
555 );
556 }
557
558 state.editor.draw(
559 &internal.editor,
560 renderer,
561 text_bounds.position(),
562 *viewport,
563 editor::Style {
564 value: style.value,
565 selection: style.selection,
566 },
567 );
568 }
569
570 fn mouse_interaction(
571 &self,
572 _tree: &widget::Tree,
573 layout: Layout,
574 cursor: mouse::Cursor,
575 _viewport: &Rectangle,
576 _renderer: &Renderer,
577 ) -> mouse::Interaction {
578 let is_disabled = self.on_edit.is_none();
579
580 if cursor.is_over(layout.bounds()) {
581 if is_disabled {
582 mouse::Interaction::NotAllowed
583 } else {
584 mouse::Interaction::Text
585 }
586 } else {
587 mouse::Interaction::default()
588 }
589 }
590
591 fn operate(
592 &mut self,
593 tree: &mut widget::Tree,
594 layout: Layout,
595 _viewport: &Rectangle,
596 _renderer: &Renderer,
597 operation: &mut dyn widget::Operation,
598 ) {
599 let state = tree.state.downcast_mut::<State<Parser>>();
600
601 operation.focusable(self.id.as_ref(), layout.bounds(), &mut state.editor);
602 operation.text_input(
603 self.id.as_ref(),
604 layout.bounds(),
605 &mut self.content.0.borrow_mut().editor,
606 );
607 }
608}
609
610pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>)
612where
613 R: text::Renderer;
614
615struct Internal<R>
616where
617 R: text::Renderer,
618{
619 editor: R::Editor,
620}
621
622impl<R> Content<R>
623where
624 R: text::Renderer,
625{
626 pub fn new() -> Self {
628 Self::with_text("")
629 }
630
631 pub fn with_text(text: &str) -> Self {
633 Self(RefCell::new(Internal {
634 editor: R::Editor::with_text(text),
635 }))
636 }
637
638 pub fn perform(&mut self, action: Action) {
640 let internal = self.0.get_mut();
641
642 internal.editor.perform(action);
643 }
644
645 pub fn move_to(&mut self, cursor: Cursor) {
647 let internal = self.0.get_mut();
648
649 internal.editor.move_to(cursor);
650 }
651
652 pub fn cursor(&self) -> Cursor {
654 self.0.borrow().editor.cursor()
655 }
656
657 pub fn line_count(&self) -> usize {
659 self.0.borrow().editor.line_count()
660 }
661
662 pub fn line(&self, index: usize) -> Option<Line<'_>> {
664 let internal = self.0.borrow();
665 let line = internal.editor.line(index)?;
666
667 Some(Line {
668 text: Cow::Owned(line.text.into_owned()),
669 ending: line.ending,
670 })
671 }
672
673 pub fn lines(&self) -> impl Iterator<Item = Line<'_>> {
675 (0..)
676 .map(|i| self.line(i))
677 .take_while(Option::is_some)
678 .flatten()
679 }
680
681 pub fn text(&self) -> String {
683 self.0.borrow().editor.text()
684 }
685
686 pub fn selection(&self) -> Option<String> {
688 self.0.borrow().editor.copy()
689 }
690
691 pub fn line_ending(&self) -> Option<LineEnding> {
693 Some(self.line(0)?.ending)
694 }
695
696 pub fn is_empty(&self) -> bool {
698 self.0.borrow().editor.is_empty()
699 }
700}
701
702impl<Renderer> Clone for Content<Renderer>
703where
704 Renderer: text::Renderer,
705{
706 fn clone(&self) -> Self {
707 Self::with_text(&self.text())
708 }
709}
710
711impl<Renderer> Default for Content<Renderer>
712where
713 Renderer: text::Renderer,
714{
715 fn default() -> Self {
716 Self::new()
717 }
718}
719
720impl<Renderer> fmt::Debug for Content<Renderer>
721where
722 Renderer: text::Renderer,
723 Renderer::Editor: fmt::Debug,
724{
725 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
726 let internal = self.0.borrow();
727
728 f.debug_struct("Content")
729 .field("editor", &internal.editor)
730 .finish()
731 }
732}
733
734#[derive(Debug, Clone, Copy, PartialEq, Eq)]
736pub enum Status {
737 Active,
739 Hovered,
741 Focused {
743 is_hovered: bool,
745 },
746 Disabled,
748}
749
750#[derive(Debug, Clone, Copy, PartialEq)]
752pub struct Style {
753 pub background: Background,
755 pub border: Border,
757 pub placeholder: Color,
759 pub value: Color,
761 pub selection: Color,
763}
764
765pub trait Catalog: theme::Base {
767 type Class<'a>;
769
770 fn default<'a>() -> Self::Class<'a>;
772
773 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
775}
776
777pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
779
780impl Catalog for Theme {
781 type Class<'a> = StyleFn<'a, Self>;
782
783 fn default<'a>() -> Self::Class<'a> {
784 Box::new(default)
785 }
786
787 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
788 class(self, status)
789 }
790}
791
792pub fn default(theme: &Theme, status: Status) -> Style {
794 let palette = theme.palette();
795
796 let active = Style {
797 background: Background::Color(palette.background.weakest.color),
798 border: Border {
799 radius: 2.0.into(),
800 width: 1.0,
801 color: palette.background.strong.color,
802 },
803 placeholder: palette.secondary.base.color,
804 value: palette.background.weakest.text,
805 selection: palette.background.strongest.color,
806 };
807
808 match status {
809 Status::Active => active,
810 Status::Hovered => Style {
811 border: Border {
812 color: palette.background.base.text,
813 ..active.border
814 },
815 ..active
816 },
817 Status::Focused { .. } => Style {
818 border: Border {
819 color: palette.primary.strong.color,
820 ..active.border
821 },
822 ..active
823 },
824 Status::Disabled => Style {
825 background: Background::Color(palette.background.base.color),
826 value: active.placeholder,
827 placeholder: palette.background.strongest.color,
828 ..active
829 },
830 }
831}