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