Skip to main content

tui_lipan/widgets/document_view/
mod.rs

1//! Document view widget.
2//!
3//! A read-only rich text display widget with pluggable content formatting,
4//! text selection, scroll synchronization, and optional markdown rendering.
5//!
6//! Unlike [`TextArea`](crate::widgets::TextArea), `DocumentView` can transform
7//! content (e.g. strip markdown syntax, render tables with box-drawing) rather
8//! than just applying syntax highlighting.
9
10pub mod diagram;
11mod format;
12#[cfg(feature = "markdown")]
13mod format_markdown;
14pub(crate) mod layout;
15#[cfg(feature = "markdown")]
16pub(crate) mod mermaid;
17pub(crate) mod node;
18pub(crate) mod planner;
19pub(crate) mod reconcile;
20
21pub(crate) use format::FormatCache;
22pub use format::{
23    ColumnAlign, ContentFormatter, DocumentStyles, FormatInput, FormattedBlock, FormattedCodeBlock,
24    FormattedDiagramBlock, FormattedDocument, FormattedLine, FormattedLink, FormattedTable,
25    PlainFormatter,
26};
27#[cfg(feature = "markdown")]
28pub use format_markdown::MarkdownFormatter;
29pub use layout::measure_document_view;
30pub(crate) use layout::measure_document_view_constrained;
31pub use reconcile::reconcile_document_view;
32
33use std::cell::{Cell, RefCell};
34use std::hash::{Hash, Hasher};
35use std::rc::Rc;
36use std::sync::Arc;
37
38use rustc_hash::FxHasher;
39
40use crate::animation::TransitionConfig;
41use crate::callback::{Callback, KeyHandler};
42use crate::core::element::Element;
43use crate::style::{
44    BorderStyle, Length, Padding, ScrollbarConfig, ScrollbarVariant, Style, StyleSlot,
45};
46use crate::widgets::scroll::{ScrollBehavior, ScrollEvent};
47
48/// Event emitted when the user clicks within the document.
49#[derive(Clone, Debug)]
50pub struct DocumentClickEvent {
51    /// Source line (0-indexed) that was clicked.
52    pub source_line: usize,
53    /// If a link span was clicked, its URL.
54    pub link: Option<Arc<str>>,
55}
56
57/// Event emitted when text is selected.
58#[derive(Clone, Debug)]
59pub struct DocumentSelectEvent {
60    /// Plain text of the selection.
61    pub selected_text: Arc<str>,
62}
63
64/// Scroll metrics exposed for scroll synchronization.
65#[derive(Clone, Debug, Default)]
66pub struct DocumentScrollMetrics {
67    /// Current scroll offset (visual lines from top).
68    pub offset: usize,
69    /// Total visual lines in the document.
70    pub total_lines: usize,
71    /// Number of visual lines visible in the viewport.
72    pub viewport_lines: usize,
73    /// Source line at the top of the viewport.
74    pub top_source_line: usize,
75    /// Source line at the bottom of the viewport.
76    pub bottom_source_line: usize,
77}
78
79/// Line numbering mode for the `DocumentView` gutter.
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
81pub enum DocumentLineNumberMode {
82    /// Number by currently visible visual lines (1, 2, 3...).
83    #[default]
84    Visual,
85    /// Number by source line mapping from the formatter.
86    Source,
87}
88
89/// Width strategy for markdown/formatted tables rendered by `DocumentView`.
90#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
91pub enum DocumentTableWidthMode {
92    /// Size table columns from content; do not stretch to fill viewport.
93    #[default]
94    Content,
95    /// Stretch table columns to fill the available viewport width.
96    Fill,
97}
98
99/// Controls horizontal separator lines between table rows.
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
101pub enum TableRowSeparators {
102    /// No horizontal row separators.
103    None,
104    /// Separator only between header and data rows (markdown default).
105    #[default]
106    Header,
107    /// Separator between all rows (header + between every data row).
108    All,
109}
110
111/// A read-only rich text document viewer with pluggable formatting.
112///
113/// # Example
114///
115/// ```rust,no_run
116/// use tui_lipan::prelude::*;
117///
118/// let view = DocumentView::new("# Hello\n\nWorld")
119///     .wrap(true)
120///     .line_numbers(true);
121/// ```
122#[derive(Clone)]
123pub struct DocumentView {
124    // ── Content ──────────────────────────────────────────────────────────
125    /// The raw source text.
126    pub value: Arc<str>,
127    /// Lazily computed hash of [`Self::value`] for layout caches, with the
128    /// `(ptr, len)` identity of the `Arc` allocation used when it was computed.
129    ///
130    /// [`LayoutHash`] and measurement must **not** use the raw `Arc` address
131    /// alone: `view()` often rebuilds the same body into a new `Arc`, which
132    /// would bust [`crate::layout::hash::element_layout_hash`] and the global
133    /// measure cache on every frame (e.g. theme-only updates). We still
134    /// recompute when the allocation changes (including `dv.value = ...` without
135    /// going through [`Self::value`]).
136    layout_content_fingerprint: Cell<Option<(u64, usize, usize)>>,
137    /// Optional content-type hint passed to the formatter.
138    pub content_type: Option<Arc<str>>,
139    /// Pluggable content formatter. Defaults to [`PlainFormatter`].
140    pub formatter: Option<Rc<dyn ContentFormatter>>,
141
142    // ── Layout ───────────────────────────────────────────────────────────
143    /// Requested width.
144    /// Default: `Length::Flex(1)`.
145    pub width: Length,
146    /// Requested height.
147    /// Default: `Length::Flex(1)`.
148    pub height: Length,
149    /// Word-wrap long lines.
150    /// Default: `true`.
151    pub wrap: bool,
152    /// Show line numbers in the gutter.
153    pub line_numbers: bool,
154    /// Minimum digit width reserved for line numbers.
155    pub min_line_number_width: u8,
156    /// Show separator after built-in line numbers.
157    /// Default: `true`.
158    pub line_number_separator: bool,
159    /// Empty cells between built-in line numbers and content.
160    pub line_number_content_gap: u16,
161    /// Line numbering mode.
162    pub line_number_mode: DocumentLineNumberMode,
163    /// Style override for built-in line-number gutter text.
164    pub line_number_style: Style,
165    /// Extend per-line background highlights across the full content width.
166    pub highlight_full_width: bool,
167    /// Show a border around the widget.
168    /// Default: `true`.
169    pub border: bool,
170    /// Border style.
171    /// Default: `BorderStyle::Plain`.
172    pub border_style: BorderStyle,
173    /// Padding inside the border.
174    /// Default: `Padding::default()`.
175    pub padding: Padding,
176    /// Wrap table cell text to fit current column widths.
177    pub table_wrap: bool,
178    /// Table width behavior.
179    pub table_width_mode: DocumentTableWidthMode,
180    /// Draw table outer frame.
181    pub table_outer_frame: bool,
182    /// Draw vertical column separators between cells.
183    pub table_column_separators: bool,
184    /// Controls horizontal separator lines between rows.
185    pub table_row_separators: TableRowSeparators,
186    /// Horizontal table cell padding (left + right).
187    pub table_cell_padding: u16,
188    /// Border glyph variant used for table lines.
189    /// Default: `BorderStyle::Plain`.
190    pub table_border_variant: BorderStyle,
191
192    // ── Styling ──────────────────────────────────────────────────────────
193    /// Base text style.
194    pub style: Style,
195    /// Style applied when hovered.
196    pub hover_style: StyleSlot,
197    /// Chrome/surface style applied when focused.
198    pub focus_style: StyleSlot,
199    /// Text content style applied when focused.
200    pub focus_content_style: Style,
201    /// Style for selected text regions.
202    pub selection_style: StyleSlot,
203    /// Per-element style overrides.
204    pub doc_styles: DocumentStyles,
205    /// Border style override when hovered.
206    pub hover_border_style: Option<BorderStyle>,
207
208    // ── Scrolling ────────────────────────────────────────────────────────
209    /// Explicit vertical scroll offset (visual lines from top).
210    pub scroll_offset: Option<usize>,
211    /// Scroll to this source line (for scroll sync).
212    pub scroll_to_source_line: Option<usize>,
213    /// Behavior used when applying [`Self::scroll_to_source_line`].
214    pub scroll_behavior: ScrollBehavior,
215    /// Vertical scrollbar visibility.
216    pub scrollbar: bool,
217    /// Scrollbar configuration.
218    pub scrollbar_config: ScrollbarConfig,
219    /// Show horizontal scrollbar (only effective when `wrap` is `false`).
220    pub h_scrollbar: bool,
221    /// Horizontal scrollbar rendering style.
222    pub h_scrollbar_variant: ScrollbarVariant,
223    /// Horizontal scrollbar thumb character override.
224    pub h_scrollbar_thumb: Option<char>,
225    #[cfg(feature = "diff-view")]
226    pub(crate) pin_scrollbar_focus_style: bool,
227    /// Enable mouse wheel scrolling.
228    pub scroll_wheel: bool,
229    /// Widget-local mouse wheel step multiplier, overriding the app default when set.
230    pub scroll_wheel_multiplier: Option<u16>,
231
232    // ── Interaction ──────────────────────────────────────────────────────
233    /// Whether the widget participates in focus traversal.
234    pub focusable: bool,
235    /// Whether the widget participates in tab traversal when focusable.
236    pub tab_stop: bool,
237    /// Callback fired when the widget gains focus.
238    pub on_focus: Option<Callback<()>>,
239    /// Callback fired when the widget loses focus.
240    pub on_blur: Option<Callback<()>>,
241    /// Scroll event callback.
242    pub on_scroll: Option<Callback<ScrollEvent>>,
243    /// Click event callback (with source line + link info).
244    pub on_click: Option<Callback<DocumentClickEvent>>,
245    /// Text selection callback.
246    pub on_select: Option<Callback<DocumentSelectEvent>>,
247    /// Keyboard handler (when focused).
248    pub on_key: Option<KeyHandler>,
249    /// Optional shared selection group identifier.
250    ///
251    /// When multiple `DocumentView`s under the same `ScrollView` share this id,
252    /// drag selection can extend across them and copy concatenates in visual order.
253    pub shared_selection_id: Option<Arc<str>>,
254
255    // ── Code block highlighting (reuses SyntectStrategy) ─────────────────
256    /// Syntax highlighting strategy for code blocks.
257    #[cfg(feature = "syntax-syntect")]
258    pub code_syntax_strategy: Option<Rc<dyn crate::widgets::TextAreaColorStrategy>>,
259
260    // ── Custom gutter ────────────────────────────────────────────────────
261    /// Per-logical-line custom gutter spans. When set, replaces the built-in
262    /// `line_numbers` gutter. Indexed by logical line (0-based); continuation
263    /// visual lines render an empty gutter.
264    pub gutter_lines: Option<Arc<Vec<Vec<crate::style::Span>>>>,
265    /// Fixed column width reserved for the custom gutter. When > 0, overrides
266    /// the `line_numbers` gutter width everywhere.
267    pub gutter_col_width: u16,
268    /// Fixed empty cells before the gutter / line numbers.
269    pub gutter_gap: u16,
270    /// Logical source-line indices (0-based) to exclude from clipboard copy.
271    pub copy_excluded_source_lines: Option<Arc<Vec<usize>>>,
272    /// Optional peer source lines used for split-view wrap synchronization.
273    pub peer_source_lines: Option<Arc<Vec<Arc<str>>>>,
274    /// Lazily computed content fingerprint of [`Self::peer_source_lines`].
275    ///
276    /// `LayoutHash` and measurement must use this instead of `Arc` pointer
277    /// addresses: `trim_render_common_indent` creates new `Arc<str>`
278    /// allocations every frame even when the text is identical.
279    peer_source_fingerprint: Cell<Option<u64>>,
280    /// Lazily computed base portion of [`document_measure_cache_key`](layout::document_measure_cache_key),
281    /// covering all geometry fields except the mutable split-wrap sync state.
282    /// Tuple: `(key, content_fingerprint_guard)`.
283    pub(crate) measure_base_key_cache: Cell<Option<(u64, u64)>>,
284    #[cfg(feature = "diff-view")]
285    pub(crate) split_wrap_sync: Option<crate::widgets::diff_view::SharedSplitWrapSync>,
286    #[cfg(feature = "diff-view")]
287    pub(crate) split_wrap_side: Option<crate::widgets::diff_view::SplitPaneSide>,
288    #[cfg(feature = "diff-view")]
289    pub(crate) diff_split_pane: Option<crate::widgets::DiffPane>,
290    #[cfg(feature = "diff-view")]
291    pub(crate) diff_context_separator_click:
292        Option<crate::widgets::diff_view::DiffContextSeparatorClickConfig>,
293    /// Style used for synthetic wrap-padding gutter rows inserted for peer sync.
294    pub(crate) split_wrap_padding_gutter_style: Option<Style>,
295    /// Style used for synthetic wrap-padding content rows inserted for peer sync.
296    pub(crate) split_wrap_padding_style: Option<Style>,
297    /// Enable word/line selection on double/triple click.
298    /// Default: `true`.
299    pub multi_click_select: bool,
300    /// Triple-click selection behavior.
301    pub triple_click_mode: crate::widgets::TripleClickSelectionMode,
302    /// Forward clicks to a wrapping [`MouseRegion`](crate::widgets::MouseRegion)
303    /// while keeping drag-to-select.
304    /// Default: `false`.
305    ///
306    /// When `true`, a click positions the cursor and sets up the drag anchor
307    /// as usual, but the click is also forwarded to the nearest enabled
308    /// `MouseRegion` ancestor that has an `on_click` handler - without
309    /// requiring `capture_click(true)` on the region. If `on_click` is also
310    /// set, link clicks still go to the document callback and non-link clicks
311    /// pass through to the ancestor.
312    pub passthrough_clicks: bool,
313
314    // Shared auto-height measurement cache used by both measure and reconcile
315    // passes during width-driven layout changes (e.g. terminal resize).
316    pub(crate) measure_cache:
317        RefCell<[Option<super::document_view::layout::DocumentMeasureCacheEntry>; 2]>,
318    /// Width-independent format cache for the measurement path.
319    ///
320    /// During resize, `max_w` changes every frame which invalidates
321    /// `measure_cache`, but the formatted document (markdown parse result)
322    /// doesn't depend on width.  Caching it here avoids re-parsing markdown
323    /// on every measurement when only the width changed.
324    pub(crate) measure_format_cache: RefCell<Option<(u64, std::rc::Rc<format::FormattedDocument>)>>,
325}
326
327impl Default for DocumentView {
328    fn default() -> Self {
329        Self {
330            value: "".into(),
331            layout_content_fingerprint: Cell::new(None),
332            content_type: None,
333            formatter: None,
334            width: Length::Flex(1),
335            height: Length::Flex(1),
336            wrap: true,
337            line_numbers: false,
338            min_line_number_width: 0,
339            line_number_separator: true,
340            line_number_content_gap: 0,
341            line_number_mode: DocumentLineNumberMode::default(),
342            line_number_style: Style::default(),
343            highlight_full_width: false,
344            border: true,
345            border_style: BorderStyle::Plain,
346            padding: Padding::default(),
347            table_wrap: false,
348            table_width_mode: DocumentTableWidthMode::default(),
349            table_outer_frame: true,
350            table_column_separators: true,
351            table_row_separators: TableRowSeparators::default(),
352            table_cell_padding: 0,
353            table_border_variant: BorderStyle::Plain,
354            style: Style::default(),
355            hover_style: StyleSlot::Inherit,
356            focus_style: StyleSlot::Inherit,
357            focus_content_style: Style::default(),
358            selection_style: StyleSlot::Inherit,
359            doc_styles: DocumentStyles::default(),
360            hover_border_style: None,
361            scroll_offset: None,
362            scroll_to_source_line: None,
363            scroll_behavior: ScrollBehavior::default(),
364            scrollbar: true,
365            scrollbar_config: ScrollbarConfig::default(),
366            h_scrollbar: false,
367            h_scrollbar_variant: ScrollbarVariant::default(),
368            h_scrollbar_thumb: None,
369            #[cfg(feature = "diff-view")]
370            pin_scrollbar_focus_style: false,
371            scroll_wheel: true,
372            scroll_wheel_multiplier: None,
373            focusable: true,
374            tab_stop: true,
375            on_focus: None,
376            on_blur: None,
377            on_scroll: None,
378            on_click: None,
379            on_select: None,
380            on_key: None,
381            shared_selection_id: None,
382            #[cfg(feature = "syntax-syntect")]
383            code_syntax_strategy: None,
384            gutter_lines: None,
385            gutter_col_width: 0,
386            gutter_gap: 0,
387            copy_excluded_source_lines: None,
388            peer_source_lines: None,
389            peer_source_fingerprint: Cell::new(None),
390            measure_base_key_cache: Cell::new(None),
391            #[cfg(feature = "diff-view")]
392            split_wrap_sync: None,
393            #[cfg(feature = "diff-view")]
394            split_wrap_side: None,
395            #[cfg(feature = "diff-view")]
396            diff_split_pane: None,
397            #[cfg(feature = "diff-view")]
398            diff_context_separator_click: None,
399            split_wrap_padding_gutter_style: None,
400            split_wrap_padding_style: None,
401            multi_click_select: true,
402            triple_click_mode: crate::widgets::TripleClickSelectionMode::Line,
403            passthrough_clicks: false,
404            measure_cache: RefCell::new([None, None]),
405            measure_format_cache: RefCell::new(None),
406        }
407    }
408}
409
410impl DocumentView {
411    pub(crate) fn should_use_implicit_auto_height(&self) -> bool {
412        matches!(self.height, Length::Flex(1))
413            && self.wrap
414            && !self.scrollbar
415            && !self.h_scrollbar
416            && !self.focusable
417    }
418
419    pub(crate) fn resolved_height(&self) -> Length {
420        if self.should_use_implicit_auto_height() {
421            Length::Auto
422        } else {
423            self.height
424        }
425    }
426
427    /// Create a new document view with the given source text.
428    pub fn new(value: impl Into<Arc<str>>) -> Self {
429        Self::default().value(value)
430    }
431
432    /// Set the text content.
433    pub fn value(mut self, value: impl Into<Arc<str>>) -> Self {
434        self.layout_content_fingerprint.set(None);
435        self.value = value.into();
436        self
437    }
438
439    pub(crate) fn layout_content_fingerprint(&self) -> u64 {
440        let ptr = self.value.as_ptr() as usize;
441        let len = self.value.len();
442        if let Some((fp, cached_ptr, cached_len)) = self.layout_content_fingerprint.get()
443            && cached_ptr == ptr
444            && cached_len == len
445        {
446            return fp;
447        }
448        let mut h = FxHasher::default();
449        self.value.as_ref().hash(&mut h);
450        let fp = h.finish();
451        self.layout_content_fingerprint.set(Some((fp, ptr, len)));
452        fp
453    }
454
455    /// Content fingerprint of [`Self::peer_source_lines`] for layout caching.
456    ///
457    /// Hashes peer line content instead of `Arc` pointers, which change every
458    /// frame when `trim_render_common_indent` creates new allocations.
459    pub(crate) fn peer_source_content_fingerprint(&self) -> Option<u64> {
460        let peer = self.peer_source_lines.as_ref()?;
461        if let Some(fp) = self.peer_source_fingerprint.get() {
462            return Some(fp);
463        }
464        let mut h = FxHasher::default();
465        peer.len().hash(&mut h);
466        for line in peer.iter() {
467            line.as_ref().hash(&mut h);
468        }
469        let fp = h.finish();
470        self.peer_source_fingerprint.set(Some(fp));
471        Some(fp)
472    }
473
474    /// Set the content-type hint (e.g. `"markdown"`, `"log"`).
475    pub fn content_type(mut self, ct: impl Into<Arc<str>>) -> Self {
476        self.content_type = Some(ct.into());
477        self
478    }
479
480    /// Set the content formatter.
481    pub fn formatter(mut self, f: impl ContentFormatter + 'static) -> Self {
482        self.formatter = Some(Rc::new(f));
483        self
484    }
485
486    /// Set the requested width.
487    pub fn width(mut self, width: impl Into<Length>) -> Self {
488        self.width = width.into();
489        self
490    }
491
492    /// Set the requested height.
493    pub fn height(mut self, height: impl Into<Length>) -> Self {
494        self.height = height.into();
495        self
496    }
497
498    /// Enable or disable word wrapping.
499    pub fn wrap(mut self, wrap: bool) -> Self {
500        self.wrap = wrap;
501        self
502    }
503
504    /// Show or hide line numbers.
505    pub fn line_numbers(mut self, show: bool) -> Self {
506        self.line_numbers = show;
507        self
508    }
509
510    /// Set minimum line-number gutter digits.
511    pub fn min_line_number_width(mut self, width: u8) -> Self {
512        self.min_line_number_width = width;
513        self
514    }
515
516    /// Show or hide the built-in line-number separator.
517    pub fn line_number_separator(mut self, show: bool) -> Self {
518        self.line_number_separator = show;
519        self
520    }
521
522    /// Set empty cells between built-in line numbers and content.
523    pub fn line_number_content_gap(mut self, gap: u16) -> Self {
524        self.line_number_content_gap = gap;
525        self
526    }
527
528    /// Set line numbering mode for the gutter.
529    pub fn line_number_mode(mut self, mode: DocumentLineNumberMode) -> Self {
530        self.line_number_mode = mode;
531        self
532    }
533
534    /// Set style override for built-in line-number gutter text.
535    pub fn line_number_style(mut self, style: Style) -> Self {
536        self.line_number_style = style;
537        self
538    }
539
540    /// Extend line background highlights across the full content width.
541    pub fn highlight_full_width(mut self, enabled: bool) -> Self {
542        self.highlight_full_width = enabled;
543        self
544    }
545
546    /// Show or hide a border around the widget.
547    pub fn border(mut self, border: bool) -> Self {
548        self.border = border;
549        self
550    }
551
552    /// Set the border style.
553    pub fn border_style(mut self, style: BorderStyle) -> Self {
554        self.border_style = style;
555        self
556    }
557
558    /// Set the border style when hovered.
559    pub fn hover_border_style(mut self, style: BorderStyle) -> Self {
560        self.hover_border_style = Some(style);
561        self
562    }
563
564    /// Set the padding inside the border.
565    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
566        self.padding = padding.into();
567        self
568    }
569
570    /// Enable or disable table cell wrapping.
571    pub fn table_wrap(mut self, wrap: bool) -> Self {
572        self.table_wrap = wrap;
573        self
574    }
575
576    /// Set table width mode.
577    pub fn table_width_mode(mut self, mode: DocumentTableWidthMode) -> Self {
578        self.table_width_mode = mode;
579        self
580    }
581
582    /// Show or hide table outer frame.
583    pub fn table_outer_frame(mut self, enabled: bool) -> Self {
584        self.table_outer_frame = enabled;
585        self
586    }
587
588    /// Show or hide vertical column separators between table cells.
589    pub fn table_column_separators(mut self, enabled: bool) -> Self {
590        self.table_column_separators = enabled;
591        self
592    }
593
594    /// Set horizontal row separator mode.
595    pub fn table_row_separators(mut self, mode: TableRowSeparators) -> Self {
596        self.table_row_separators = mode;
597        self
598    }
599
600    /// Set horizontal table cell padding (left and right).
601    pub fn table_cell_padding(mut self, padding: u16) -> Self {
602        self.table_cell_padding = padding;
603        self
604    }
605
606    /// Set border glyph variant used for table lines.
607    pub fn table_border_variant(mut self, variant: BorderStyle) -> Self {
608        self.table_border_variant = variant;
609        self
610    }
611
612    /// Set style applied to table borders (color/emphasis).
613    pub fn table_border_style(mut self, style: Style) -> Self {
614        self.doc_styles.table_border_style = style;
615        self
616    }
617
618    /// Set the base text style.
619    pub fn style(mut self, style: Style) -> Self {
620        self.style = style;
621        self
622    }
623
624    /// Set the hover style.
625    pub fn hover_style(mut self, style: Style) -> Self {
626        self.hover_style = StyleSlot::Replace(style);
627        self
628    }
629
630    /// Extend the active theme's hover style with additional fields.
631    pub fn extend_hover_style(mut self, style: Style) -> Self {
632        self.hover_style = StyleSlot::Extend(style);
633        self
634    }
635
636    /// Inherit hover style from the active theme.
637    pub fn inherit_hover_style(mut self) -> Self {
638        self.hover_style = StyleSlot::Inherit;
639        self
640    }
641
642    /// Set hover style slot directly for composite forwarding.
643    pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
644        self.hover_style = slot;
645        self
646    }
647
648    /// Set the focus chrome style.
649    pub fn focus_style(mut self, style: Style) -> Self {
650        self.focus_style = StyleSlot::Replace(style);
651        self
652    }
653
654    /// Extend the active theme's focus style with additional fields.
655    pub fn extend_focus_style(mut self, style: Style) -> Self {
656        self.focus_style = StyleSlot::Extend(style);
657        self
658    }
659
660    /// Inherit focus style from the active theme.
661    pub fn inherit_focus_style(mut self) -> Self {
662        self.focus_style = StyleSlot::Inherit;
663        self
664    }
665
666    /// Set focus style slot directly for composite forwarding.
667    pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
668        self.focus_style = slot;
669        self
670    }
671
672    /// Set the focused content text style.
673    pub fn focus_content_style(mut self, style: Style) -> Self {
674        self.focus_content_style = style;
675        self
676    }
677
678    /// Set the selection style.
679    pub fn selection_style(mut self, style: Style) -> Self {
680        self.selection_style = StyleSlot::Replace(style);
681        self
682    }
683
684    /// Extend the active theme's selection style with additional fields.
685    pub fn extend_selection_style(mut self, style: Style) -> Self {
686        self.selection_style = StyleSlot::Extend(style);
687        self
688    }
689
690    /// Inherit selection style from the active theme.
691    pub fn inherit_selection_style(mut self) -> Self {
692        self.selection_style = StyleSlot::Inherit;
693        self
694    }
695
696    /// Set selection style slot directly for composite forwarding.
697    pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
698        self.selection_style = slot;
699        self
700    }
701
702    /// Set the document-element style overrides.
703    pub fn doc_styles(mut self, styles: DocumentStyles) -> Self {
704        self.doc_styles = styles;
705        self
706    }
707
708    /// Set style applied to code block rows.
709    ///
710    /// Useful for setting code block background and default foreground when
711    /// syntax highlighting is disabled.
712    pub fn code_block_style(mut self, style: Style) -> Self {
713        self.doc_styles.code_block_style = style;
714        self
715    }
716
717    /// Set explicit scroll offset (controlled mode).
718    pub fn scroll_offset(mut self, offset: usize) -> Self {
719        self.scroll_offset = Some(offset);
720        self
721    }
722
723    /// Scroll to the given source line (for scroll sync).
724    pub fn scroll_to_source_line(mut self, line: usize) -> Self {
725        self.scroll_to_source_line = Some(line);
726        self
727    }
728
729    /// Set how [`Self::scroll_to_source_line`] targets are applied.
730    pub fn scroll_behavior(mut self, behavior: ScrollBehavior) -> Self {
731        self.scroll_behavior = behavior;
732        self
733    }
734
735    /// Smoothly animate [`Self::scroll_to_source_line`] targets with `transition`.
736    pub fn scroll_transition(mut self, transition: TransitionConfig) -> Self {
737        self.scroll_behavior = ScrollBehavior::smooth(transition);
738        self
739    }
740
741    /// Show or hide the vertical scrollbar.
742    pub fn scrollbar(mut self, show: bool) -> Self {
743        self.scrollbar = show;
744        self
745    }
746
747    /// Set scrollbar configuration.
748    pub fn scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
749        self.scrollbar_config = config;
750        self
751    }
752
753    /// Show or hide the horizontal scrollbar (only effective when `wrap` is `false`).
754    pub fn h_scrollbar(mut self, show: bool) -> Self {
755        self.h_scrollbar = show;
756        self
757    }
758
759    /// Set the horizontal scrollbar rendering style.
760    pub fn h_scrollbar_variant(mut self, variant: ScrollbarVariant) -> Self {
761        self.h_scrollbar_variant = variant;
762        self
763    }
764
765    /// Set the horizontal scrollbar thumb character.
766    pub fn h_scrollbar_thumb(mut self, c: char) -> Self {
767        self.h_scrollbar_thumb = Some(c);
768        self
769    }
770
771    /// Set a custom gutter column.
772    ///
773    /// `lines` is indexed by logical line (0-based). Continuation visual lines
774    /// show an empty gutter. `col_width` is the fixed column width reserved;
775    /// when > 0 it overrides the `line_numbers` gutter width everywhere.
776    pub fn gutter_lines(
777        mut self,
778        lines: Arc<Vec<Vec<crate::style::Span>>>,
779        col_width: u16,
780    ) -> Self {
781        self.gutter_lines = Some(lines);
782        self.gutter_col_width = col_width;
783        self
784    }
785
786    /// Reserve empty cells before the gutter / line numbers.
787    pub fn gutter_inset(mut self, inset: u16) -> Self {
788        self.gutter_gap = inset;
789        self
790    }
791
792    /// Set logical source-line indices (0-based) to exclude from clipboard copy.
793    pub fn copy_excluded_source_lines(mut self, indices: Arc<Vec<usize>>) -> Self {
794        self.copy_excluded_source_lines = Some(indices);
795        self
796    }
797
798    /// Enable or disable mouse wheel scrolling.
799    pub fn scroll_wheel(mut self, enabled: bool) -> Self {
800        self.scroll_wheel = enabled;
801        self
802    }
803
804    /// Override the app-wide mouse wheel step multiplier for this document view.
805    pub fn scroll_wheel_multiplier(mut self, multiplier: u16) -> Self {
806        self.scroll_wheel_multiplier = Some(multiplier.max(1));
807        self
808    }
809
810    /// Set whether the widget is focusable.
811    pub fn focusable(mut self, focusable: bool) -> Self {
812        self.focusable = focusable;
813        self
814    }
815
816    /// Control whether the widget participates in tab traversal.
817    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
818        self.tab_stop = tab_stop;
819        self
820    }
821
822    /// Set the callback fired when the widget gains focus.
823    pub fn on_focus(mut self, cb: Callback<()>) -> Self {
824        self.on_focus = Some(cb);
825        self
826    }
827
828    /// Set the callback fired when the widget loses focus.
829    pub fn on_blur(mut self, cb: Callback<()>) -> Self {
830        self.on_blur = Some(cb);
831        self
832    }
833
834    /// Enable or disable word/line selection on double/triple click.
835    ///
836    /// When `false`, double and triple clicks behave as single clicks
837    /// (no word or line selection). Drag-to-select remains unaffected.
838    pub fn multi_click_select(mut self, enabled: bool) -> Self {
839        self.multi_click_select = enabled;
840        self
841    }
842
843    /// Set how triple-click expands selection.
844    pub fn triple_click_mode(mut self, mode: crate::widgets::TripleClickSelectionMode) -> Self {
845        self.triple_click_mode = mode;
846        self
847    }
848
849    /// Forward clicks to a wrapping [`MouseRegion`](crate::widgets::MouseRegion)
850    /// while keeping drag-to-select.
851    ///
852    /// When `true`, a click positions the cursor and sets up the drag anchor
853    /// as usual, and then also fires the nearest enabled `MouseRegion`
854    /// ancestor's `on_click` handler - without requiring `capture_click(true)`
855    /// on the region. If `on_click` is also set, link clicks still go to the
856    /// document callback and non-link clicks pass through to the ancestor.
857    pub fn passthrough_clicks(mut self, passthrough: bool) -> Self {
858        self.passthrough_clicks = passthrough;
859        self
860    }
861
862    /// Set the scroll event callback.
863    pub fn on_scroll(mut self, cb: Callback<ScrollEvent>) -> Self {
864        self.on_scroll = Some(cb);
865        self
866    }
867
868    /// Set the click event callback.
869    ///
870    /// For the common "just open the URL" case when a link span was hit, see
871    /// [`crate::callbacks::open_document_link`].
872    pub fn on_click(mut self, cb: Callback<DocumentClickEvent>) -> Self {
873        self.on_click = Some(cb);
874        self
875    }
876
877    /// Set the text selection callback.
878    pub fn on_select(mut self, cb: Callback<DocumentSelectEvent>) -> Self {
879        self.on_select = Some(cb);
880        self
881    }
882
883    /// Set the keyboard handler.
884    pub fn on_key(mut self, cb: KeyHandler) -> Self {
885        self.on_key = Some(cb);
886        self
887    }
888
889    /// Set the shared selection group id.
890    ///
891    /// `DocumentView`s under the same `ScrollView` that share this value
892    /// participate in cross-widget linear selection and unified copy.
893    pub fn shared_selection_id(mut self, id: impl Into<Arc<str>>) -> Self {
894        self.shared_selection_id = Some(id.into());
895        self
896    }
897
898    /// Set the syntax highlighting strategy for code blocks.
899    #[cfg(feature = "syntax-syntect")]
900    pub fn code_syntax_strategy(
901        mut self,
902        strategy: impl crate::widgets::TextAreaColorStrategy + 'static,
903    ) -> Self {
904        self.code_syntax_strategy = Some(Rc::new(strategy));
905        self
906    }
907
908    /// Convenience: set a [`MarkdownFormatter`] with default styles.
909    #[cfg(feature = "markdown")]
910    pub fn markdown(mut self) -> Self {
911        self = self
912            .formatter(MarkdownFormatter::default())
913            .content_type("markdown");
914
915        #[cfg(feature = "syntax-syntect")]
916        if self.code_syntax_strategy.is_none() {
917            self = self.code_syntax_strategy(
918                crate::widgets::SyntectStrategy::default().default_theme("One Dark (Atom)"),
919            );
920        }
921
922        self
923    }
924
925    /// Convenience: set markdown formatter with compact block spacing.
926    ///
927    /// When `compact` is `true`, blank lines and Markdown fence spacers are collapsed.
928    #[cfg(feature = "markdown")]
929    pub fn markdown_compact(mut self, compact: bool) -> Self {
930        self = self
931            .formatter(MarkdownFormatter::default().compact_blocks(compact))
932            .content_type("markdown");
933
934        #[cfg(feature = "syntax-syntect")]
935        if self.code_syntax_strategy.is_none() {
936            self = self.code_syntax_strategy(
937                crate::widgets::SyntectStrategy::default().default_theme("One Dark (Atom)"),
938            );
939        }
940
941        self
942    }
943
944    /// Toggle Mermaid diagram rendering on the active [`MarkdownFormatter`].
945    ///
946    /// Defaults to `true` via `.markdown()` / `.markdown_compact()`. Set to
947    /// `false` to render ```mermaid fences as plain code blocks. No effect
948    /// if the current formatter is not a [`MarkdownFormatter`].
949    #[cfg(feature = "markdown")]
950    pub fn render_diagrams(mut self, enabled: bool) -> Self {
951        if let Some(formatter_rc) = self.formatter.as_mut() {
952            if Rc::get_mut(formatter_rc).is_none() {
953                *formatter_rc = Rc::from(formatter_rc.clone_box());
954            }
955            if let Some(formatter) = Rc::get_mut(formatter_rc)
956                && let Some(md) = formatter.as_any_mut().downcast_mut::<MarkdownFormatter>()
957            {
958                md.render_diagrams = enabled;
959            }
960        }
961        self
962    }
963}
964
965impl From<DocumentView> for Element {
966    fn from(value: DocumentView) -> Self {
967        Element::new(crate::core::element::ElementKind::DocumentView(Box::new(
968            value,
969        )))
970    }
971}
972
973impl crate::layout::hash::LayoutHash for DocumentView {
974    fn layout_hash(
975        &self,
976        hasher: &mut impl std::hash::Hasher,
977        _recurse: &dyn Fn(&crate::core::element::Element) -> Option<u64>,
978    ) -> Option<()> {
979        // Content identity must survive `Arc` reallocation when `view()` rebuilds
980        // the same string; use a lazily cached full-text fingerprint (computed once
981        // per `DocumentView` instance, not on every hash call after caching).
982        self.layout_content_fingerprint().hash(hasher);
983        self.content_type.hash(hasher);
984
985        // Formatter geometry hash (stable across Rc recreations). Purely visual
986        // theme changes must not invalidate layout hashing.
987        self.formatter
988            .as_ref()
989            .map(|f| f.measure_cache_key())
990            .hash(hasher);
991
992        // Layout dimensions.
993        self.width.hash(hasher);
994        self.resolved_height().hash(hasher);
995        self.wrap.hash(hasher);
996
997        // Chrome that affects measured size.
998        self.border.hash(hasher);
999        self.padding.hash(hasher);
1000        self.scrollbar.hash(hasher);
1001        self.scrollbar_config.variant.hash(hasher);
1002        self.scrollbar_config.gap.hash(hasher);
1003        self.line_numbers.hash(hasher);
1004        self.min_line_number_width.hash(hasher);
1005        self.line_number_separator.hash(hasher);
1006        self.line_number_content_gap.hash(hasher);
1007        self.gutter_col_width.hash(hasher);
1008        self.gutter_gap.hash(hasher);
1009        self.peer_source_content_fingerprint().hash(hasher);
1010
1011        #[cfg(feature = "diff-view")]
1012        if let Some(sync) = &self.split_wrap_sync {
1013            self.split_wrap_side.hash(hasher);
1014            self.split_wrap_side
1015                .and_then(|side| crate::widgets::diff_view::split_wrap_pane_widths(sync, side))
1016                .hash(hasher);
1017            crate::widgets::diff_view::split_wrap_scrollbar_cols_pair(sync).hash(hasher);
1018            crate::widgets::diff_view::split_wrap_layout_pass(sync).hash(hasher);
1019        }
1020
1021        // Table layout properties that affect height.
1022        self.table_wrap.hash(hasher);
1023        self.table_width_mode.hash(hasher);
1024        self.table_outer_frame.hash(hasher);
1025        self.table_column_separators.hash(hasher);
1026        self.table_row_separators.hash(hasher);
1027        self.table_cell_padding.hash(hasher);
1028        self.table_border_variant.hash(hasher);
1029
1030        Some(())
1031    }
1032}