Skip to main content

tui_lipan/widgets/text_area/
mod.rs

1mod color;
2mod decorations;
3mod layout;
4mod node;
5mod reconcile;
6mod virtual_text;
7
8#[cfg(feature = "syntax-syntect")]
9mod color_syntect;
10#[cfg(feature = "syntax-syntect")]
11mod syntect_document_formatter;
12
13pub(crate) use color::TextAreaColorCache;
14pub use color::{TextAreaColorInput, TextAreaColorLines, TextAreaColorStrategy};
15pub(crate) use decorations::{
16    TEXT_AREA_LAYER_PRIORITY_CURRENT_SEARCH, TEXT_AREA_LAYER_PRIORITY_SEARCH,
17    TEXT_AREA_LAYER_PRIORITY_SELECTION, TextAreaLayerKind, TextAreaRangeLayer,
18    TextAreaStyledSegment, public_decoration_layers_for_visible_range, resolve_text_area_spans,
19    segments_from_plain, segments_from_spans,
20};
21pub(crate) use layout::{
22    TextAreaGeometry, TextAreaVisualCache, TextAreaVisualKeyArgs, TextAreaVisualLine,
23    VirtualTextLayoutCtx, hash_peer_source_lines, layout_line_with_inline_virtual_text,
24    make_text_area_visual_key, text_area_auto_height_for_width, text_area_cursor_reserve,
25    text_area_pending_vim_search_row, text_area_total_gutter_width,
26    text_area_visual_line_for_cursor,
27};
28pub use layout::{measure_text_area, measure_text_area_constrained};
29pub use node::TextAreaNode;
30pub use reconcile::reconcile_text_area;
31pub(crate) use virtual_text::{
32    eol_virtual_texts_for_visual_line, inline_virtual_insertions_for_line,
33    inline_virtual_texts_for_visual_line, text_area_virtual_text_hash, virtual_text_content_width,
34};
35
36#[cfg(feature = "syntax-syntect")]
37pub use color_syntect::{SyntectStrategy, apply_syntect_strategy_app_theme, language_from_path};
38#[cfg(feature = "syntax-syntect")]
39pub use syntect_document_formatter::SyntectDocumentFormatter;
40mod metrics;
41mod sentinel;
42mod snapshot;
43mod vim_config;
44
45use std::collections::BTreeMap;
46use std::hash::Hash;
47use std::rc::Rc;
48use std::sync::Arc;
49
50use crate::animation::TransitionConfig;
51use crate::app::TextAreaNewlineBinding;
52use crate::callback::{Callback, KeyHandler};
53use crate::clipboard::ImageContent;
54use crate::core::element::{Element, ElementKind};
55use crate::core::event::MouseEvent;
56use crate::input::KeyBindings;
57use crate::style::{
58    BorderStyle, CaretShape, Color, LayoutConstraints, Length, Padding, ScrollbarConfig,
59    ScrollbarVariant, Span, Style, StyleSlot,
60};
61use crate::text::edit::TextEditEvent;
62use crate::text::editor::TextEditor;
63use crate::utils::text::SentinelInfo;
64use crate::widgets::scroll::{ScrollBehavior, ScrollEvent};
65
66/// Public style decoration for byte ranges in a [`TextArea`].
67#[allow(missing_docs)]
68#[derive(Clone, Debug, PartialEq)]
69pub struct TextAreaDecoration {
70    pub range: std::ops::Range<usize>,
71    pub style: Style,
72    /// Resolution order relative to other decorations and to selection/search.
73    ///
74    /// Higher priority wins **only for style attributes that two overlapping
75    /// layers both set** — composition is per-attribute (`Style::patch`), not a
76    /// full replacement. Raising the priority of a decoration that only sets a
77    /// foreground color will not mask a lower layer's background or underline;
78    /// those attributes survive because the higher layer never sets them. To
79    /// hide an attribute, set it explicitly on the higher-priority decoration.
80    pub priority: u16,
81    pub kind: TextAreaDecorationKind,
82}
83
84/// Decoration rendering mode for [`TextAreaDecoration`].
85#[non_exhaustive]
86#[allow(missing_docs)]
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
88pub enum TextAreaDecorationKind {
89    Range,
90    WholeLine,
91    Underline,
92}
93
94/// A sign/adornment rendered in a composable TextArea gutter column.
95#[allow(missing_docs)]
96#[derive(Clone, Debug, PartialEq)]
97pub struct TextAreaGutterSign {
98    pub line: usize,
99    pub spans: Vec<Span>,
100}
101
102#[allow(missing_docs)]
103impl TextAreaGutterSign {
104    pub fn new(line: usize, spans: impl Into<Vec<Span>>) -> Self {
105        Self {
106            line,
107            spans: spans.into(),
108        }
109    }
110}
111
112/// One composable gutter column.
113#[allow(missing_docs)]
114#[derive(Clone, Debug, PartialEq)]
115pub struct TextAreaGutterColumn {
116    kind: TextAreaGutterColumnKind,
117    width: u16,
118}
119
120#[derive(Clone, Debug, PartialEq)]
121enum TextAreaGutterColumnKind {
122    LineNumbers(TextAreaLineNumberMode),
123    Custom(Arc<Vec<Vec<Span>>>),
124    Signs(Vec<TextAreaGutterSign>),
125}
126
127#[allow(missing_docs)]
128impl TextAreaGutterColumn {
129    pub fn line_numbers(mode: TextAreaLineNumberMode) -> Self {
130        Self {
131            kind: TextAreaGutterColumnKind::LineNumbers(mode),
132            width: 0,
133        }
134    }
135
136    pub fn custom(lines: Arc<Vec<Vec<Span>>>, width: u16) -> Self {
137        Self {
138            kind: TextAreaGutterColumnKind::Custom(lines),
139            width,
140        }
141    }
142
143    pub fn signs(signs: impl IntoIterator<Item = TextAreaGutterSign>) -> Self {
144        let signs: Vec<_> = signs.into_iter().collect();
145        let width = signs
146            .iter()
147            .flat_map(|s| s.spans.iter())
148            .map(|s| unicode_width::UnicodeWidthStr::width(s.content.as_ref()) as u16)
149            .max()
150            .unwrap_or(1)
151            .max(1);
152        Self {
153            kind: TextAreaGutterColumnKind::Signs(signs),
154            width,
155        }
156    }
157
158    pub fn width(mut self, width: u16) -> Self {
159        self.width = width;
160        self
161    }
162}
163
164/// Composable TextArea gutter configuration.
165#[allow(missing_docs)]
166#[derive(Clone, Debug, Default, PartialEq)]
167pub struct TextAreaGutter {
168    columns: Vec<TextAreaGutterColumn>,
169}
170
171#[allow(missing_docs)]
172impl TextAreaGutter {
173    pub fn new() -> Self {
174        Self::default()
175    }
176    pub fn line_numbers(mut self, mode: TextAreaLineNumberMode) -> Self {
177        self.columns.push(TextAreaGutterColumn::line_numbers(mode));
178        self
179    }
180    pub fn signs(mut self, signs: impl IntoIterator<Item = TextAreaGutterSign>) -> Self {
181        self.columns.push(TextAreaGutterColumn::signs(signs));
182        self
183    }
184    pub fn column(mut self, column: TextAreaGutterColumn) -> Self {
185        self.columns.push(column);
186        self
187    }
188}
189
190/// Reason-tagged editor state transition emitted by [`TextArea::on_editor_state_change`].
191#[allow(missing_docs)]
192#[derive(Clone, Debug, PartialEq)]
193pub struct TextAreaStateChangeEvent {
194    pub reason: TextAreaStateChangeReason,
195    pub value: Arc<str>,
196    pub cursor: usize,
197    pub anchor: Option<usize>,
198    pub edit: Option<TextEditEvent>,
199    pub vim_mode: Option<TextAreaVimMode>,
200}
201
202#[non_exhaustive]
203#[allow(missing_docs)]
204#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
205pub enum TextAreaStateChangeReason {
206    Edit,
207    SelectionChange,
208    CursorMove,
209    Scroll,
210    VimModeChange,
211}
212
213/// A multi-line text input.
214#[derive(Clone)]
215pub struct TextArea {
216    pub(crate) value: Arc<str>,
217    pub(crate) cursor: usize,         // byte index
218    pub(crate) anchor: Option<usize>, // selection anchor byte index
219    pub(crate) placeholder: Option<Arc<str>>,
220    pub(crate) style: Style,
221    pub(crate) hover_style: StyleSlot,
222    pub(crate) focus_style: StyleSlot,
223    pub(crate) focus_content_style: Style,
224    pub(crate) hover_border_style: Option<BorderStyle>,
225    pub(crate) caret_shape: Option<CaretShape>,
226    pub(crate) caret_color: Option<Color>,
227    pub(crate) selection_style: StyleSlot,
228    pub(crate) unfocused_selection_style: StyleSlot,
229    /// When true, render the active anchor/cursor range even while unfocused.
230    pub(crate) show_selection_when_unfocused: bool,
231    pub(crate) placeholder_style: Style,
232    pub(crate) focus_placeholder_style: Style,
233    pub(crate) line_numbers: bool,
234    pub(crate) line_number_mode: TextAreaLineNumberMode,
235    pub(crate) line_number_style: Style,
236    pub(crate) min_line_number_width: u8,
237    pub(crate) wrap: bool,
238    pub(crate) color_strategy: Option<Rc<dyn TextAreaColorStrategy>>,
239    pub(crate) language: Option<Arc<str>>,
240    pub(crate) theme: Option<Arc<str>>,
241    pub(crate) border: bool,
242    pub(crate) border_style: BorderStyle,
243    pub(crate) padding: Padding,
244    pub(crate) width: Length,
245    pub(crate) height: Length,
246    pub(crate) scroll_offset: Option<usize>, // Line-based visual scroll offset
247    /// Zero-based logical/source line to bring to the top of the viewport.
248    pub(crate) scroll_to_line: Option<usize>,
249    pub(crate) scroll_behavior: ScrollBehavior,
250    pub(crate) scroll_wheel: bool,
251    pub(crate) scroll_wheel_multiplier: Option<u16>,
252    pub(crate) on_change: Option<Callback<TextAreaEvent>>,
253    pub(crate) on_edit: Option<Callback<TextEditEvent>>,
254    pub(crate) on_editor_state_change: Option<Callback<TextAreaStateChangeEvent>>,
255    pub(crate) on_scroll: Option<Callback<ScrollEvent>>,
256    pub(crate) on_scroll_to: Option<Callback<usize>>,
257    pub(crate) on_click: Option<Callback<MouseEvent>>,
258    pub(crate) on_key: Option<KeyHandler>,
259    pub(crate) key_interceptor: Option<KeyHandler>,
260    pub(crate) clear_bindings: Option<KeyBindings>,
261    pub(crate) vim_motions: bool,
262    pub(crate) vim_keymap: Option<TextAreaVimKeymap>,
263    pub(crate) vim_config: TextAreaVimConfig,
264    pub(crate) on_vim_mode_change: Option<Callback<TextAreaVimMode>>,
265    pub(crate) on_image_paste: Option<Callback<ImageContent>>,
266    pub(crate) on_text_paste: Option<Callback<TextAreaPasteEvent>>,
267    /// Ordered list of images associated with this text area.
268    /// In `Inline` mode: index `i` maps to the sentinel char `IMAGE_SENTINEL_BASE + i` in the value.
269    /// In `Attachment` mode: displayed as chip labels above the text.
270    pub(crate) images: Vec<ImageContent>,
271    pub(crate) on_images_change: Option<Callback<Vec<ImageContent>>>,
272    pub(crate) image_mode: TextAreaImageMode,
273    pub(crate) image_placeholder: Arc<str>,
274    pub(crate) image_placeholder_style: Style,
275    pub(crate) image_placeholder_focus_style: Style,
276    pub(crate) image_placeholder_hover_style: Style,
277    pub(crate) disabled: bool,
278    pub(crate) disabled_style: Style,
279    pub(crate) read_only: bool,
280    pub(crate) focusable: bool,
281    pub(crate) tab_stop: bool,
282    pub(crate) on_focus: Option<Callback<()>>,
283    pub(crate) on_blur: Option<Callback<()>>,
284    pub(crate) newline_binding: Option<TextAreaNewlineBinding>,
285    pub(crate) tab_width: u8,
286    pub(crate) insert_tab: bool,
287    /// Display width of a literal `\t` character. Tab advances to the next
288    /// multiple of `tab_display_width` from the logical line start. Set to 0 to keep
289    /// the historical zero-width behavior.
290    pub(crate) tab_display_width: u8,
291    /// Vertical scrollbar visibility.
292    pub(crate) scrollbar: bool,
293    pub(crate) scrollbar_config: ScrollbarConfig,
294    pub(crate) h_scrollbar: bool,
295    pub(crate) h_scrollbar_variant: ScrollbarVariant,
296    pub(crate) h_scrollbar_thumb: Option<char>,
297    #[cfg(feature = "diff-view")]
298    pub(crate) pin_scrollbar_focus_style: bool,
299    /// Per-logical-line custom gutter spans. When set, replaces the built-in
300    /// `line_numbers` gutter. Indexed by logical line (0-based); continuation
301    /// visual lines render an empty gutter.
302    pub(crate) gutter_lines: Option<Arc<Vec<Vec<crate::style::Span>>>>,
303    /// Width reserved for the custom gutter column. When > 0, overrides the
304    /// computed `line_numbers` gutter width everywhere.
305    pub(crate) gutter_col_width: u16,
306    /// Fixed empty cells before the gutter / line numbers.
307    pub(crate) gutter_gap: u16,
308    pub(crate) gutter: Option<TextAreaGutter>,
309    /// Peer logical source lines for split-wrap synchronization padding.
310    pub(crate) peer_source_lines: Option<Arc<Vec<Arc<str>>>>,
311    #[cfg(feature = "diff-view")]
312    pub(crate) split_wrap_sync: Option<crate::widgets::diff_view::SharedSplitWrapSync>,
313    #[cfg(feature = "diff-view")]
314    pub(crate) split_wrap_side: Option<crate::widgets::diff_view::SplitPaneSide>,
315    #[cfg(feature = "diff-view")]
316    pub(crate) diff_context_separator_click:
317        Option<crate::widgets::diff_view::DiffContextSeparatorClickConfig>,
318    /// Style used for synthetic wrap-padding gutter rows inserted for peer sync.
319    pub(crate) split_wrap_padding_gutter_style: Option<Style>,
320    /// Style used for synthetic wrap-padding content rows inserted for peer sync.
321    pub(crate) split_wrap_padding_style: Option<Style>,
322    /// Byte ranges in `value` excluded from clipboard copy (sorted, non-overlapping).
323    pub(crate) copy_excluded_bytes: Option<Arc<Vec<(usize, usize)>>>,
324    /// Optional transform applied to selected text immediately before clipboard write.
325    pub(crate) clipboard_transform: Option<TextAreaClipboardTransform>,
326    /// 0-based logical line indices whose selection highlight (the newline space) is suppressed.
327    pub(crate) selection_excluded_lines: Option<Arc<Vec<usize>>>,
328    /// Enable word/line selection on double/triple click (default: `true`).
329    pub(crate) multi_click_select: bool,
330    /// Triple-click selection behavior.
331    pub(crate) triple_click_mode: crate::widgets::TripleClickSelectionMode,
332    /// Ordered list of custom inline sentinels.
333    /// Index `i` maps to the sentinel character `SENTINEL_BASE + i` in the value.
334    pub(crate) sentinels: Vec<TextAreaSentinel>,
335    /// Callback invoked when the sentinels list changes (a sentinel was deleted).
336    pub(crate) on_sentinels_change: Option<Callback<Vec<TextAreaSentinel>>>,
337    pub(crate) on_sentinel_event: Option<Callback<Vec<SentinelEvent>>>,
338    pub(crate) on_sentinel_click: Option<Callback<TextAreaSentinelClickEvent>>,
339    pub(crate) decorations: Vec<TextAreaDecoration>,
340    pub(crate) virtual_texts: Vec<TextAreaVirtualText>,
341}
342
343impl Default for TextArea {
344    fn default() -> Self {
345        Self {
346            value: "".into(),
347            cursor: 0,
348            anchor: None,
349            placeholder: None,
350            style: Style::default(),
351            hover_style: StyleSlot::Inherit,
352            focus_style: StyleSlot::Inherit,
353            focus_content_style: Style::default(),
354            hover_border_style: None,
355            caret_shape: None,
356            caret_color: None,
357            selection_style: StyleSlot::Inherit,
358            unfocused_selection_style: StyleSlot::Inherit,
359            show_selection_when_unfocused: true,
360            placeholder_style: Style::default(),
361            focus_placeholder_style: Style::default(),
362            line_numbers: false,
363            line_number_mode: TextAreaLineNumberMode::default(),
364            line_number_style: Style::default(),
365            min_line_number_width: 0,
366            wrap: true,
367            color_strategy: None,
368            language: None,
369            theme: None,
370            border: true,
371            border_style: BorderStyle::Plain,
372            padding: Padding::default(),
373            width: Length::Flex(1),
374            height: Length::Flex(1),
375            scroll_offset: None,
376            scroll_to_line: None,
377            scroll_behavior: ScrollBehavior::Instant,
378            scroll_wheel: true,
379            scroll_wheel_multiplier: None,
380            on_change: None,
381            on_edit: None,
382            on_editor_state_change: None,
383            on_scroll: None,
384            on_scroll_to: None,
385            on_click: None,
386            on_key: None,
387            key_interceptor: None,
388            clear_bindings: None,
389            vim_motions: false,
390            vim_keymap: None,
391            vim_config: TextAreaVimConfig::default(),
392            on_vim_mode_change: None,
393            on_image_paste: None,
394            on_text_paste: None,
395            images: Vec::new(),
396            on_images_change: None,
397            image_mode: TextAreaImageMode::default(),
398            image_placeholder: "[Image]".into(),
399            image_placeholder_style: Style::default(),
400            image_placeholder_focus_style: Style::default(),
401            image_placeholder_hover_style: Style::default(),
402            disabled: false,
403            disabled_style: Style::default(),
404            read_only: false,
405            focusable: true,
406            tab_stop: true,
407            on_focus: None,
408            on_blur: None,
409            newline_binding: None,
410            tab_width: 0,
411            insert_tab: false,
412            tab_display_width: 8,
413            scrollbar: true,
414            scrollbar_config: ScrollbarConfig::default(),
415            h_scrollbar: false,
416            h_scrollbar_variant: ScrollbarVariant::default(),
417            h_scrollbar_thumb: None,
418            #[cfg(feature = "diff-view")]
419            pin_scrollbar_focus_style: false,
420            gutter_lines: None,
421            gutter_col_width: 0,
422            gutter_gap: 0,
423            gutter: None,
424            peer_source_lines: None,
425            #[cfg(feature = "diff-view")]
426            split_wrap_sync: None,
427            #[cfg(feature = "diff-view")]
428            split_wrap_side: None,
429            #[cfg(feature = "diff-view")]
430            diff_context_separator_click: None,
431            split_wrap_padding_gutter_style: None,
432            split_wrap_padding_style: None,
433            copy_excluded_bytes: None,
434            clipboard_transform: None,
435            selection_excluded_lines: None,
436            multi_click_select: true,
437            triple_click_mode: crate::widgets::TripleClickSelectionMode::Line,
438            sentinels: Vec::new(),
439            on_sentinels_change: None,
440            on_sentinel_event: None,
441            on_sentinel_click: None,
442            decorations: Vec::new(),
443            virtual_texts: Vec::new(),
444        }
445    }
446}
447
448impl TextArea {
449    /// Create a new text area.
450    pub fn new(value: impl Into<Arc<str>>) -> Self {
451        Self {
452            value: value.into(),
453            ..Self::default()
454        }
455    }
456
457    /// Create a new text area bound to a [`TextEditor`] state bundle.
458    pub fn bound(state: &TextEditor) -> Self {
459        Self::new("").bind(state)
460    }
461
462    /// Set the text content.
463    pub fn value(mut self, value: impl Into<Arc<str>>) -> Self {
464        self.value = value.into();
465        self
466    }
467
468    /// Set placeholder text (shown when empty).
469    pub fn placeholder(mut self, placeholder: impl Into<Arc<str>>) -> Self {
470        self.placeholder = Some(placeholder.into());
471        self
472    }
473
474    /// Set cursor position.
475    pub fn cursor(mut self, cursor: usize) -> Self {
476        self.cursor = cursor;
477        self
478    }
479
480    /// Set selection anchor position (byte index).
481    /// When set, text between anchor and cursor is selected.
482    pub fn anchor(mut self, anchor: Option<usize>) -> Self {
483        self.anchor = anchor;
484        self
485    }
486
487    /// Bind the text area's value, cursor, and anchor from a [`TextEditor`] state bundle.
488    pub fn bind(mut self, state: &TextEditor) -> Self {
489        self.value = state.text().into();
490        self.cursor = state.cursor();
491        self.anchor = state.anchor();
492        self
493    }
494
495    /// Show line numbers.
496    pub fn line_numbers(mut self, show: bool) -> Self {
497        self.line_numbers = show;
498        self.gutter = None;
499        self
500    }
501
502    /// Set line-number display mode for the built-in gutter.
503    ///
504    /// Use [`TextAreaLineNumberMode::Relative`] for Vim-style relative numbers:
505    /// the cursor's logical line shows its absolute number, while lines above
506    /// and below show their distance from the cursor line.
507    pub fn line_number_mode(mut self, mode: TextAreaLineNumberMode) -> Self {
508        self.line_number_mode = mode;
509        self.gutter = None;
510        self
511    }
512
513    /// Set minimum line number width (number of digits to reserve).
514    pub fn min_line_number_width(mut self, width: u8) -> Self {
515        self.min_line_number_width = width;
516        self
517    }
518
519    /// Enable word wrapping.
520    pub fn wrap(mut self, wrap: bool) -> Self {
521        self.wrap = wrap;
522        self
523    }
524
525    /// Set text coloring strategy.
526    pub fn color_strategy(mut self, strategy: impl TextAreaColorStrategy + 'static) -> Self {
527        self.color_strategy = Some(Rc::new(strategy));
528        self
529    }
530
531    /// Set language identifier for coloring strategies.
532    pub fn language(mut self, language: impl Into<Arc<str>>) -> Self {
533        self.language = Some(language.into());
534        self
535    }
536
537    /// Set language identifier by resolving from a file path's extension or name.
538    ///
539    /// Uses the default syntect syntax definitions. If no syntax matches the
540    /// path, the language remains unset (plain text fallback). TypeScript/TSX
541    /// paths fall back to JavaScript/JSX-compatible syntaxes when the default
542    /// set does not provide exact grammars.
543    #[cfg(feature = "syntax-syntect")]
544    pub fn language_from_path(self, path: impl AsRef<std::path::Path>) -> Self {
545        if let Some(lang) = crate::widgets::language_from_path(path) {
546            self.language(lang)
547        } else {
548            self
549        }
550    }
551
552    /// Set theme identifier for coloring strategies.
553    pub fn theme(mut self, theme: impl Into<Arc<str>>) -> Self {
554        self.theme = Some(theme.into());
555        self
556    }
557
558    /// Enable syntect-based syntax highlighting with default strategy.
559    #[cfg(feature = "syntax-syntect")]
560    pub fn with_syntax(self, language: impl Into<Arc<str>>, theme: impl Into<Arc<str>>) -> Self {
561        self.with_syntax_strategy(SyntectStrategy::default(), language, theme)
562    }
563
564    /// Enable syntect-based syntax highlighting with theme background colors.
565    #[cfg(feature = "syntax-syntect")]
566    pub fn with_syntax_bg(self, language: impl Into<Arc<str>>, theme: impl Into<Arc<str>>) -> Self {
567        self.with_syntax_strategy(
568            SyntectStrategy::default().use_background(true),
569            language,
570            theme,
571        )
572    }
573
574    /// Enable syntect-based syntax highlighting with a custom theme string.
575    #[cfg(feature = "syntax-syntect")]
576    pub fn with_syntax_custom_theme(
577        self,
578        language: impl Into<Arc<str>>,
579        theme_name: impl Into<Arc<str>>,
580        tm_theme_xml: impl AsRef<str>,
581    ) -> crate::Result<Self> {
582        let theme_name = theme_name.into();
583        let strategy = SyntectStrategy::default().custom_theme(theme_name.clone(), tm_theme_xml)?;
584        Ok(self.with_syntax_strategy(strategy, language, theme_name))
585    }
586
587    /// Enable syntect-based syntax highlighting with custom theme bytes.
588    #[cfg(feature = "syntax-syntect")]
589    pub fn with_syntax_custom_theme_bytes(
590        self,
591        language: impl Into<Arc<str>>,
592        theme_name: impl Into<Arc<str>>,
593        bytes: impl AsRef<[u8]>,
594    ) -> crate::Result<Self> {
595        let theme_name = theme_name.into();
596        let strategy = SyntectStrategy::default().custom_theme_bytes(theme_name.clone(), bytes)?;
597        Ok(self.with_syntax_strategy(strategy, language, theme_name))
598    }
599
600    /// Enable syntect-based syntax highlighting with a custom theme file.
601    #[cfg(feature = "syntax-syntect")]
602    pub fn with_syntax_custom_theme_from_file(
603        self,
604        language: impl Into<Arc<str>>,
605        theme_name: impl Into<Arc<str>>,
606        path: impl AsRef<std::path::Path>,
607    ) -> crate::Result<Self> {
608        let theme_name = theme_name.into();
609        let strategy =
610            SyntectStrategy::default().custom_theme_from_file(theme_name.clone(), path)?;
611        Ok(self.with_syntax_strategy(strategy, language, theme_name))
612    }
613
614    /// Enable syntect-based syntax highlighting with a custom strategy.
615    #[cfg(feature = "syntax-syntect")]
616    pub fn with_syntax_strategy(
617        self,
618        strategy: SyntectStrategy,
619        language: impl Into<Arc<str>>,
620        theme: impl Into<Arc<str>>,
621    ) -> Self {
622        self.color_strategy(strategy)
623            .language(language)
624            .theme(theme)
625    }
626
627    /// Set base style.
628    pub fn style(mut self, style: Style) -> Self {
629        self.style = style;
630        self
631    }
632
633    /// Set style when hovered.
634    pub fn hover_style(mut self, style: Style) -> Self {
635        self.hover_style = StyleSlot::Replace(style);
636        self
637    }
638
639    /// Extend the active theme's hover style with additional fields.
640    pub fn extend_hover_style(mut self, style: Style) -> Self {
641        self.hover_style = StyleSlot::Extend(style);
642        self
643    }
644
645    /// Inherit hover style from the active theme.
646    pub fn inherit_hover_style(mut self) -> Self {
647        self.hover_style = StyleSlot::Inherit;
648        self
649    }
650
651    /// Set hover style slot directly for composite forwarding.
652    pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
653        self.hover_style = slot;
654        self
655    }
656
657    /// Set focus chrome style.
658    pub fn focus_style(mut self, style: Style) -> Self {
659        self.focus_style = StyleSlot::Replace(style);
660        self
661    }
662
663    /// Extend the active theme's focus style with additional fields.
664    pub fn extend_focus_style(mut self, style: Style) -> Self {
665        self.focus_style = StyleSlot::Extend(style);
666        self
667    }
668
669    /// Inherit focus style from the active theme.
670    pub fn inherit_focus_style(mut self) -> Self {
671        self.focus_style = StyleSlot::Inherit;
672        self
673    }
674
675    /// Set focus style slot directly for composite forwarding.
676    pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
677        self.focus_style = slot;
678        self
679    }
680
681    /// Set focused content text style.
682    pub fn focus_content_style(mut self, style: Style) -> Self {
683        self.focus_content_style = style;
684        self
685    }
686
687    /// Set border style when hovered.
688    pub fn hover_border_style(mut self, border_style: BorderStyle) -> Self {
689        self.hover_border_style = Some(border_style);
690        self
691    }
692
693    /// Override the active theme's caret shape.
694    pub fn caret_shape(mut self, shape: CaretShape) -> Self {
695        self.caret_shape = Some(shape);
696        self
697    }
698
699    /// Override the active theme's hardware caret color (only used for block caret rendering).
700    pub fn caret_color(mut self, color: Color) -> Self {
701        self.caret_color = Some(color);
702        self
703    }
704
705    /// Set selection highlight style.
706    pub fn selection_style(mut self, style: Style) -> Self {
707        self.selection_style = StyleSlot::Replace(style);
708        self
709    }
710
711    /// Extend the active theme's selection style with additional fields.
712    pub fn extend_selection_style(mut self, style: Style) -> Self {
713        self.selection_style = StyleSlot::Extend(style);
714        self
715    }
716
717    /// Inherit selection style from the active theme.
718    pub fn inherit_selection_style(mut self) -> Self {
719        self.selection_style = StyleSlot::Inherit;
720        self
721    }
722
723    /// Set selection style slot directly for composite forwarding.
724    pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
725        self.selection_style = slot;
726        self
727    }
728
729    /// Show the active selection range while the text area is unfocused.
730    ///
731    /// Enabled by default so keyboard/programmatic focus changes preserve the
732    /// visible selection, matching [`DocumentView`](crate::widgets::DocumentView).
733    /// Pass `false` to hide inactive selections.
734    pub fn show_selection_when_unfocused(mut self, show: bool) -> Self {
735        self.show_selection_when_unfocused = show;
736        self
737    }
738
739    /// Set selection highlight style while unfocused.
740    pub fn unfocused_selection_style(mut self, style: Style) -> Self {
741        self.unfocused_selection_style = StyleSlot::Replace(style);
742        self
743    }
744
745    /// Inherit unfocused selection style from the active theme.
746    pub fn inherit_unfocused_selection_style(mut self) -> Self {
747        self.unfocused_selection_style = StyleSlot::Inherit;
748        self
749    }
750
751    /// Set unfocused selection style slot directly for composite forwarding.
752    pub fn unfocused_selection_style_slot(mut self, slot: StyleSlot) -> Self {
753        self.unfocused_selection_style = slot;
754        self
755    }
756
757    /// Set placeholder style.
758    pub fn placeholder_style(mut self, style: Style) -> Self {
759        self.placeholder_style = style;
760        self
761    }
762
763    /// Set placeholder style when focused.
764    pub fn focus_placeholder_style(mut self, style: Style) -> Self {
765        self.focus_placeholder_style = style;
766        self
767    }
768
769    /// Set line number style.
770    pub fn line_number_style(mut self, style: Style) -> Self {
771        self.line_number_style = style;
772        self
773    }
774
775    /// Set border.
776    pub fn border(mut self, border: bool) -> Self {
777        self.border = border;
778        self
779    }
780
781    /// Set border style.
782    pub fn border_style(mut self, style: BorderStyle) -> Self {
783        self.border_style = style;
784        self
785    }
786
787    /// Set padding.
788    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
789        self.padding = padding.into();
790        self
791    }
792
793    /// Set width.
794    pub fn width(mut self, width: Length) -> Self {
795        self.width = width;
796        self
797    }
798
799    /// Set height.
800    pub fn height(mut self, height: Length) -> Self {
801        self.height = height;
802        self
803    }
804
805    /// Set on-change callback.
806    pub fn on_change(mut self, cb: Callback<TextAreaEvent>) -> Self {
807        self.on_change = Some(cb);
808        self
809    }
810
811    /// Set on-edit callback.
812    pub fn on_edit(mut self, cb: Callback<TextEditEvent>) -> Self {
813        self.on_edit = Some(cb);
814        self
815    }
816
817    /// Set a single reason-tagged editor-state callback.
818    pub fn on_editor_state_change(mut self, cb: Callback<TextAreaStateChangeEvent>) -> Self {
819        self.on_editor_state_change = Some(cb);
820        self
821    }
822
823    /// Set on-click callback.
824    pub fn on_click(mut self, cb: Callback<MouseEvent>) -> Self {
825        self.on_click = Some(cb);
826        self
827    }
828
829    /// Set on-key handler.
830    pub fn on_key(mut self, handler: KeyHandler) -> Self {
831        self.on_key = Some(handler);
832        self
833    }
834
835    /// Set a pre-insertion key interceptor.
836    ///
837    /// This handler runs after clipboard shortcuts but before newline insertion,
838    /// tab expansion, or regular text editing. If it returns `true`, the key is
839    /// consumed and neither the editor nor `on_key` will fire.
840    pub fn key_interceptor(mut self, handler: KeyHandler) -> Self {
841        self.key_interceptor = Some(handler);
842        self
843    }
844
845    /// Set widget-level single-key bindings that clear the text area.
846    ///
847    /// Multi-step chord entries in `bindings` are ignored by the per-key text area handler.
848    pub fn clear_bindings(mut self, bindings: KeyBindings) -> Self {
849        self.clear_bindings = Some(bindings);
850        self
851    }
852
853    /// Enable or disable TextArea-only Vim-style modal motions.
854    ///
855    /// Disabled by default. When enabled, the TextArea starts in normal mode.
856    pub fn vim_motions(mut self, enabled: bool) -> Self {
857        self.vim_motions = enabled;
858        self
859    }
860
861    /// Set widget-local Vim key remaps.
862    ///
863    /// Remaps are only applied while Vim motions are enabled and the TextArea is
864    /// not in insert mode. They translate matching keys to canonical Vim command
865    /// characters before command dispatch.
866    pub fn vim_keymap(mut self, keymap: TextAreaVimKeymap) -> Self {
867        self.vim_keymap = Some(keymap);
868        self
869    }
870
871    /// Set Vim-specific rendering options such as search feedback and
872    /// current-line highlighting.
873    pub fn vim_config(mut self, config: TextAreaVimConfig) -> Self {
874        self.vim_config = config;
875        self
876    }
877
878    /// Convenience builder for Vim current-line highlighting.
879    ///
880    /// Pass [`TextAreaVimCurrentLineHighlight::Full`] to include the gutter and
881    /// line numbers, or [`TextAreaVimCurrentLineHighlight::Content`] to affect
882    /// only the text content area.
883    pub fn vim_current_line_highlight(mut self, mode: TextAreaVimCurrentLineHighlight) -> Self {
884        self.vim_config.current_line_highlight = mode;
885        self
886    }
887
888    /// Toggle full-row Vim current-line highlighting.
889    pub fn highlight_vim_current_line(mut self, enabled: bool) -> Self {
890        self.vim_config = self.vim_config.highlight_current_line(enabled);
891        self
892    }
893
894    /// Observe internal Vim mode changes for status bars or mode-aware styling.
895    pub fn on_vim_mode_change(mut self, cb: Callback<TextAreaVimMode>) -> Self {
896        self.on_vim_mode_change = Some(cb);
897        self
898    }
899
900    /// Set callback invoked when an image is pasted via `Ctrl+Shift+I`.
901    pub fn on_image_paste(mut self, cb: Callback<ImageContent>) -> Self {
902        self.on_image_paste = Some(cb);
903        self
904    }
905
906    /// Set the ordered list of images associated with this text area.
907    pub fn images(mut self, images: Vec<ImageContent>) -> Self {
908        self.images = images;
909        self
910    }
911
912    /// Set callback invoked when the images list changes (e.g. image pasted, sentinel deleted).
913    pub fn on_images_change(mut self, cb: Callback<Vec<ImageContent>>) -> Self {
914        self.on_images_change = Some(cb);
915        self
916    }
917
918    /// Set the image display mode (`Inline` or `Attachment`).
919    pub fn image_mode(mut self, mode: TextAreaImageMode) -> Self {
920        self.image_mode = mode;
921        self
922    }
923
924    /// Set the placeholder label rendered for each inline image sentinel (default: `"[Image]"`).
925    pub fn image_placeholder(mut self, label: impl Into<Arc<str>>) -> Self {
926        self.image_placeholder = label.into();
927        self
928    }
929
930    /// Set the style for inline image placeholder labels.
931    pub fn image_placeholder_style(mut self, style: Style) -> Self {
932        self.image_placeholder_style = style;
933        self
934    }
935
936    /// Set the style for inline image placeholder labels when the widget is focused.
937    pub fn image_placeholder_focus_style(mut self, style: Style) -> Self {
938        self.image_placeholder_focus_style = style;
939        self
940    }
941
942    /// Set the hover style patched over inline image placeholder labels.
943    pub fn image_placeholder_hover_style(mut self, style: Style) -> Self {
944        self.image_placeholder_hover_style = style;
945        self
946    }
947
948    /// Set disabled.
949    pub fn disabled(mut self, disabled: bool) -> Self {
950        self.disabled = disabled;
951        self
952    }
953
954    /// Set disabled style.
955    pub fn disabled_style(mut self, style: Style) -> Self {
956        self.disabled_style = style;
957        self
958    }
959
960    /// Set read-only mode. Allows mouse selection but blocks keyboard input.
961    pub fn read_only(mut self, read_only: bool) -> Self {
962        self.read_only = read_only;
963        self
964    }
965
966    /// Enable or disable word/line selection on double/triple click.
967    ///
968    /// When `false`, double and triple clicks behave as single clicks
969    /// (no word or line selection). Drag-to-select remains unaffected.
970    pub fn multi_click_select(mut self, enabled: bool) -> Self {
971        self.multi_click_select = enabled;
972        self
973    }
974
975    /// Set how triple-click expands selection.
976    pub fn triple_click_mode(mut self, mode: crate::widgets::TripleClickSelectionMode) -> Self {
977        self.triple_click_mode = mode;
978        self
979    }
980
981    /// Set focusable.
982    pub fn focusable(mut self, focusable: bool) -> Self {
983        self.focusable = focusable;
984        self
985    }
986
987    /// Set the callback fired when the text area gains focus.
988    pub fn on_focus(mut self, cb: Callback<()>) -> Self {
989        self.on_focus = Some(cb);
990        self
991    }
992
993    /// Set the callback fired when the text area loses focus.
994    pub fn on_blur(mut self, cb: Callback<()>) -> Self {
995        self.on_blur = Some(cb);
996        self
997    }
998
999    /// Override app-level newline key policy for this `TextArea` only.
1000    pub fn newline_binding(mut self, binding: TextAreaNewlineBinding) -> Self {
1001        self.newline_binding = Some(binding);
1002        self
1003    }
1004
1005    /// When set to a non-zero value, pressing Tab inserts spaces up to the next tab stop
1006    /// (aligning to a multiple of `width` columns) instead of moving focus.
1007    pub fn tab_width(mut self, width: u8) -> Self {
1008        self.tab_width = width;
1009        self
1010    }
1011
1012    /// When `true`, Tab inserts a tab character instead of moving focus.
1013    pub fn insert_tab(mut self, insert_tab: bool) -> Self {
1014        self.insert_tab = insert_tab;
1015        self
1016    }
1017
1018    /// Include this text area in sequential Tab focus traversal.
1019    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
1020        self.tab_stop = tab_stop;
1021        self
1022    }
1023
1024    /// Set the display width of a literal `\t` character.
1025    pub fn tab_display_width(mut self, width: u8) -> Self {
1026        self.tab_display_width = width;
1027        self
1028    }
1029
1030    /// Set scroll offset (line index).
1031    pub fn scroll_offset(mut self, offset: usize) -> Self {
1032        self.scroll_offset = Some(offset);
1033        self
1034    }
1035
1036    /// Scroll to a zero-based logical/source line.
1037    ///
1038    /// When wrapping is enabled, this resolves to the first visual row for the
1039    /// requested logical line. If the logical line is beyond the available text,
1040    /// reconciliation clamps to the last available visual row / maximum offset.
1041    pub fn scroll_to_line(mut self, line: usize) -> Self {
1042        self.scroll_to_line = Some(line);
1043        self
1044    }
1045
1046    /// Set how explicit line scroll targets are applied.
1047    ///
1048    /// This affects [`Self::scroll_to_line`] only; controlled offsets and
1049    /// cursor auto-scroll remain immediate.
1050    pub fn scroll_behavior(mut self, behavior: ScrollBehavior) -> Self {
1051        self.scroll_behavior = behavior;
1052        self
1053    }
1054
1055    /// Animate explicit line scroll targets with `transition`.
1056    pub fn scroll_transition(mut self, transition: TransitionConfig) -> Self {
1057        self.scroll_behavior = ScrollBehavior::smooth(transition);
1058        self
1059    }
1060
1061    /// Enable mouse wheel scrolling.
1062    pub fn scroll_wheel(mut self, enabled: bool) -> Self {
1063        self.scroll_wheel = enabled;
1064        self
1065    }
1066
1067    /// Override the app-wide mouse wheel step multiplier for this text area.
1068    pub fn scroll_wheel_multiplier(mut self, multiplier: u16) -> Self {
1069        self.scroll_wheel_multiplier = Some(multiplier.max(1));
1070        self
1071    }
1072
1073    /// Set on-scroll callback.
1074    pub fn on_scroll(mut self, cb: Callback<ScrollEvent>) -> Self {
1075        self.on_scroll = Some(cb);
1076        self
1077    }
1078
1079    /// Set on-scroll-to callback (for scrollbar dragging).
1080    pub fn on_scroll_to(mut self, cb: Callback<usize>) -> Self {
1081        self.on_scroll_to = Some(cb);
1082        self
1083    }
1084
1085    /// Enable scrollbar.
1086    pub fn scrollbar(mut self, scrollbar: bool) -> Self {
1087        self.scrollbar = scrollbar;
1088        self
1089    }
1090
1091    /// Set scrollbar configuration.
1092    pub fn scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
1093        self.scrollbar_config = config;
1094        self
1095    }
1096
1097    /// Set a custom gutter column.
1098    ///
1099    /// `lines` is indexed by logical line (0-based). Continuation visual lines
1100    /// (word-wrap overflow) show an empty gutter. `col_width` is the fixed
1101    /// column width reserved for the gutter; when > 0 it overrides the
1102    /// `line_numbers` gutter width everywhere.
1103    pub fn gutter_lines(
1104        mut self,
1105        lines: Arc<Vec<Vec<crate::style::Span>>>,
1106        col_width: u16,
1107    ) -> Self {
1108        self.gutter_lines = Some(lines);
1109        self.gutter_col_width = col_width;
1110        self.gutter = None;
1111        self
1112    }
1113
1114    /// Set a composable gutter. Compatibility fields are lowered for the current renderer.
1115    pub fn gutter(mut self, gutter: TextAreaGutter) -> Self {
1116        self.apply_gutter(&gutter);
1117        self.gutter = Some(gutter);
1118        self
1119    }
1120
1121    /// Reserve empty cells before the gutter / line numbers.
1122    pub fn gutter_inset(mut self, inset: u16) -> Self {
1123        self.gutter_gap = inset;
1124        self
1125    }
1126
1127    fn apply_gutter(&mut self, gutter: &TextAreaGutter) {
1128        if gutter.columns.is_empty() {
1129            self.line_numbers = false;
1130            self.gutter_lines = None;
1131            self.gutter_col_width = 0;
1132            return;
1133        }
1134        if gutter.columns.len() == 1
1135            && let TextAreaGutterColumnKind::LineNumbers(mode) = &gutter.columns[0].kind
1136        {
1137            self.line_numbers = true;
1138            self.line_number_mode = *mode;
1139            self.gutter_lines = None;
1140            self.gutter_col_width = gutter.columns[0].width;
1141            return;
1142        }
1143
1144        let logical_lines = self
1145            .value
1146            .as_bytes()
1147            .iter()
1148            .filter(|&&b| b == b'\n')
1149            .count()
1150            + 1;
1151        let cursor = crate::utils::text::clamp_cursor(&self.value, self.cursor);
1152        let cursor_line = self.value[..cursor]
1153            .as_bytes()
1154            .iter()
1155            .filter(|&&b| b == b'\n')
1156            .count()
1157            + 1;
1158        let mut rows = vec![Vec::new(); logical_lines.max(1)];
1159        let mut total_width = 0u16;
1160        for (col_idx, column) in gutter.columns.iter().enumerate() {
1161            if col_idx > 0 {
1162                for row in &mut rows {
1163                    row.push(Span::new(" "));
1164                }
1165                total_width = total_width.saturating_add(1);
1166            }
1167            let col_width = column_width(column, logical_lines, self.min_line_number_width);
1168            total_width = total_width.saturating_add(col_width);
1169            match &column.kind {
1170                TextAreaGutterColumnKind::LineNumbers(mode) => {
1171                    for (idx, row) in rows.iter_mut().enumerate() {
1172                        let line = idx + 1;
1173                        let n = match mode {
1174                            TextAreaLineNumberMode::Absolute => line,
1175                            TextAreaLineNumberMode::Relative => {
1176                                if line == cursor_line {
1177                                    line
1178                                } else {
1179                                    line.abs_diff(cursor_line)
1180                                }
1181                            }
1182                        };
1183                        row.push(
1184                            Span::new(format!(
1185                                "{n:>width$} │",
1186                                width = col_width.saturating_sub(2) as usize
1187                            ))
1188                            .style(self.line_number_style),
1189                        );
1190                    }
1191                }
1192                TextAreaGutterColumnKind::Custom(lines) => {
1193                    for (idx, row) in rows.iter_mut().enumerate() {
1194                        if let Some(spans) = lines.get(idx) {
1195                            row.extend(spans.iter().cloned());
1196                        }
1197                    }
1198                }
1199                TextAreaGutterColumnKind::Signs(signs) => {
1200                    let mut by_line: BTreeMap<usize, Vec<Span>> = BTreeMap::new();
1201                    for sign in signs {
1202                        by_line
1203                            .entry(sign.line)
1204                            .or_default()
1205                            .extend(sign.spans.iter().cloned());
1206                    }
1207                    for (idx, row) in rows.iter_mut().enumerate() {
1208                        if let Some(spans) = by_line.get(&idx) {
1209                            row.extend(spans.iter().cloned());
1210                        }
1211                    }
1212                }
1213            }
1214        }
1215        self.line_numbers = false;
1216        self.gutter_lines = Some(Arc::new(rows));
1217        self.gutter_col_width = total_width;
1218    }
1219
1220    /// Set byte ranges in `value` to exclude from clipboard copy.
1221    pub fn copy_excluded_bytes(mut self, ranges: Arc<Vec<(usize, usize)>>) -> Self {
1222        self.copy_excluded_bytes = Some(ranges);
1223        self
1224    }
1225
1226    /// Set an opt-in transform for selected text immediately before clipboard copy/cut.
1227    ///
1228    /// By default, TextArea copies the rendered selection unchanged.
1229    pub fn clipboard_transform(mut self, transform: TextAreaClipboardTransform) -> Self {
1230        self.clipboard_transform = Some(transform);
1231        self
1232    }
1233
1234    /// Set 0-based logical line indices whose selection newline highlight is suppressed.
1235    pub fn selection_excluded_lines(mut self, lines: Arc<Vec<usize>>) -> Self {
1236        self.selection_excluded_lines = Some(lines);
1237        self
1238    }
1239
1240    /// Enable horizontal scrollbar (only effective when wrap is disabled).
1241    pub fn h_scrollbar(mut self, h_scrollbar: bool) -> Self {
1242        self.h_scrollbar = h_scrollbar;
1243        self
1244    }
1245
1246    /// Set horizontal scrollbar rendering style (integrated into border vs standalone row).
1247    pub fn h_scrollbar_variant(mut self, style: ScrollbarVariant) -> Self {
1248        self.h_scrollbar_variant = style;
1249        self
1250    }
1251
1252    /// Set custom horizontal scrollbar thumb character (default: '█').
1253    pub fn h_scrollbar_thumb(mut self, ch: char) -> Self {
1254        self.h_scrollbar_thumb = Some(ch);
1255        self
1256    }
1257
1258    /// Set the ordered list of custom inline sentinels.
1259    pub fn sentinels(mut self, sentinels: Vec<TextAreaSentinel>) -> Self {
1260        self.sentinels = sentinels;
1261        self
1262    }
1263
1264    /// Set callback invoked when the sentinels list changes (a sentinel was deleted).
1265    pub fn on_sentinels_change(mut self, cb: Callback<Vec<TextAreaSentinel>>) -> Self {
1266        self.on_sentinels_change = Some(cb);
1267        self
1268    }
1269
1270    /// Callback for sentinel lifecycle (e.g. user-deleted token with stable id).
1271    pub fn on_sentinel_event(mut self, cb: Callback<Vec<SentinelEvent>>) -> Self {
1272        self.on_sentinel_event = Some(cb);
1273        self
1274    }
1275
1276    /// Callback invoked when an inline image or custom sentinel placeholder is clicked.
1277    pub fn on_sentinel_click(mut self, cb: Callback<TextAreaSentinelClickEvent>) -> Self {
1278        self.on_sentinel_click = Some(cb);
1279        self
1280    }
1281
1282    /// Add a byte-range decoration.
1283    pub fn decoration(mut self, decoration: TextAreaDecoration) -> Self {
1284        self.decorations.push(decoration);
1285        self
1286    }
1287
1288    /// Add byte-range decorations.
1289    pub fn decorations(
1290        mut self,
1291        decorations: impl IntoIterator<Item = TextAreaDecoration>,
1292    ) -> Self {
1293        self.decorations.extend(decorations);
1294        self
1295    }
1296
1297    /// Add non-editable virtual text rendered inline or at end-of-line.
1298    pub fn virtual_text(mut self, virtual_text: TextAreaVirtualText) -> Self {
1299        self.virtual_texts.push(virtual_text);
1300        self
1301    }
1302
1303    /// Add non-editable virtual text entries.
1304    pub fn virtual_texts(
1305        mut self,
1306        virtual_texts: impl IntoIterator<Item = TextAreaVirtualText>,
1307    ) -> Self {
1308        self.virtual_texts.extend(virtual_texts);
1309        self
1310    }
1311
1312    /// Handle pasted text before the default insertion path.
1313    ///
1314    /// When set, the callback receives the pasted text plus the current cursor/selection and the
1315    /// text area does not insert the text itself.
1316    pub fn on_text_paste(mut self, cb: Callback<TextAreaPasteEvent>) -> Self {
1317        self.on_text_paste = Some(cb);
1318        self
1319    }
1320
1321    /// Build sentinel info for width calculations on this text area.
1322    pub(crate) fn sentinel_info(&self) -> Option<SentinelInfo> {
1323        sentinel_info_for(
1324            self.image_mode,
1325            self.images.len(),
1326            &self.image_placeholder,
1327            &self.sentinels,
1328        )
1329    }
1330}
1331
1332fn column_width(
1333    column: &TextAreaGutterColumn,
1334    logical_lines: usize,
1335    min_line_number_width: u8,
1336) -> u16 {
1337    use unicode_width::UnicodeWidthStr;
1338    let measured = match &column.kind {
1339        TextAreaGutterColumnKind::LineNumbers(_) => logical_lines
1340            .max(1)
1341            .to_string()
1342            .len()
1343            .max(min_line_number_width as usize)
1344            .saturating_add(2) as u16,
1345        TextAreaGutterColumnKind::Custom(lines) => lines
1346            .iter()
1347            .map(|row| {
1348                row.iter()
1349                    .map(|s| UnicodeWidthStr::width(s.content.as_ref()))
1350                    .sum::<usize>()
1351            })
1352            .max()
1353            .unwrap_or(0) as u16,
1354        TextAreaGutterColumnKind::Signs(signs) => signs
1355            .iter()
1356            .flat_map(|s| s.spans.iter())
1357            .map(|s| UnicodeWidthStr::width(s.content.as_ref()))
1358            .max()
1359            .unwrap_or(1) as u16,
1360    };
1361    column.width.max(measured)
1362}
1363
1364impl From<TextArea> for Element {
1365    fn from(value: TextArea) -> Self {
1366        let mut min_w = value.padding.horizontal();
1367        let mut min_h = 1u16.saturating_add(value.padding.vertical());
1368        if value.border {
1369            min_w = min_w.saturating_add(2);
1370            min_h = min_h.saturating_add(2);
1371        }
1372        let layout = LayoutConstraints::default()
1373            .min_width(Length::Px(min_w))
1374            .min_height(Length::Px(min_h));
1375        Element::new(ElementKind::TextArea(Box::new(value))).with_layout(layout)
1376    }
1377}
1378
1379/// A text area change event.
1380#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1381pub struct TextAreaEvent {
1382    /// Updated value.
1383    pub value: Arc<str>,
1384    /// Updated cursor position.
1385    pub cursor: usize,
1386    /// Selection anchor position (byte index), if any.
1387    pub anchor: Option<usize>,
1388}
1389
1390/// A text paste event emitted before default text insertion.
1391#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1392pub struct TextAreaPasteEvent {
1393    /// Pasted text after clipboard/router normalization and truncation.
1394    pub text: Arc<str>,
1395    /// Cursor position before paste insertion.
1396    pub cursor: usize,
1397    /// Selection anchor before paste insertion, if any.
1398    pub anchor: Option<usize>,
1399}
1400
1401impl TextAreaEvent {
1402    /// Apply this event to a [`TextEditor`] state bundle.
1403    pub fn apply_to(&self, state: &mut TextEditor) {
1404        state.core.text = self.value.to_string();
1405        state.core.cursor = crate::utils::text::clamp_cursor(&state.core.text, self.cursor);
1406        state.core.anchor = self
1407            .anchor
1408            .map(|anchor| crate::utils::text::clamp_cursor(&state.core.text, anchor));
1409    }
1410}
1411
1412impl crate::layout::hash::LayoutHash for TextArea {
1413    fn layout_hash(
1414        &self,
1415        hasher: &mut impl std::hash::Hasher,
1416        _recurse: &dyn Fn(&Element) -> Option<u64>,
1417    ) -> Option<()> {
1418        use std::hash::Hash;
1419        self.width.hash(hasher);
1420        self.height.hash(hasher);
1421        self.wrap.hash(hasher);
1422        self.line_numbers.hash(hasher);
1423        self.line_number_mode.hash(hasher);
1424        self.min_line_number_width.hash(hasher);
1425        self.border.hash(hasher);
1426        self.padding.hash(hasher);
1427        self.scrollbar.hash(hasher);
1428        self.scrollbar_config.gap.hash(hasher);
1429        self.gutter_col_width.hash(hasher);
1430        self.gutter_gap.hash(hasher);
1431        self.gutter.as_ref().map(|g| g.columns.len()).hash(hasher);
1432        if let Some(peer_lines) = &self.peer_source_lines {
1433            peer_lines.len().hash(hasher);
1434            for line in peer_lines.iter() {
1435                line.as_ref().hash(hasher);
1436            }
1437        } else {
1438            0usize.hash(hasher);
1439        }
1440        #[cfg(feature = "diff-view")]
1441        if let Some(sync) = &self.split_wrap_sync {
1442            self.split_wrap_side.hash(hasher);
1443            self.split_wrap_side
1444                .and_then(|side| crate::widgets::diff_view::split_wrap_pane_widths(sync, side))
1445                .hash(hasher);
1446            crate::widgets::diff_view::split_wrap_scrollbar_cols_pair(sync).hash(hasher);
1447            crate::widgets::diff_view::split_wrap_layout_pass(sync).hash(hasher);
1448        }
1449        self.read_only.hash(hasher);
1450        if self.wrap && !self.read_only {
1451            self.cursor.hash(hasher);
1452        }
1453
1454        for s in &self.sentinels {
1455            s.label.hash(hasher);
1456            s.sentinel_id().hash(hasher);
1457        }
1458        self.images.len().hash(hasher);
1459        self.virtual_texts.hash(hasher);
1460
1461        let needs_content =
1462            matches!(self.width, Length::Auto) || matches!(self.height, Length::Auto);
1463        if needs_content {
1464            self.value.hash(hasher);
1465        }
1466        Some(())
1467    }
1468}
1469
1470pub use metrics::*;
1471pub use sentinel::*;
1472pub use snapshot::*;
1473pub use vim_config::*;
1474
1475pub(crate) use vim_config::TextAreaVimSearchFeedback;
1476
1477#[cfg(test)]
1478mod mod_tests;