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