// src/textarea/highlight/widget.rs use ratatui::{ buffer::Buffer, layout::{Alignment, Rect}, style::Style, text::{Line, Span}, widgets::{Block, BorderType, Borders, Paragraph, StatefulWidget, Widget}, }; use unicode_width::UnicodeWidthChar; use super::chunks::{ clip_chunks_window_with_indicator_padded, wrap_chunks_indented, }; use super::state::TextAreaSyntaxState; use crate::data_provider::DataProvider; use crate::textarea::state::{ compute_h_scroll_with_padding, count_wrapped_rows_indented, TextOverflowMode, }; #[derive(Debug, Clone)] pub struct TextAreaSyntax<'a> { pub block: Option>, pub style: Style, pub border_type: BorderType, } impl<'a> Default for TextAreaSyntax<'a> { fn default() -> Self { Self { block: Some( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded), ), style: Style::default(), border_type: BorderType::Rounded, } } } impl<'a> TextAreaSyntax<'a> { pub fn block(mut self, block: Block<'a>) -> Self { self.block = Some(block); self } pub fn style(mut self, style: Style) -> Self { self.style = style; self } pub fn border_type(mut self, ty: BorderType) -> Self { self.border_type = ty; if let Some(b) = &mut self.block { *b = b.clone().border_type(ty); } self } } fn display_width(s: &str) -> u16 { s.chars() .map(|c| UnicodeWidthChar::width(c).unwrap_or(0) as u16) .sum() } fn display_cols_up_to(s: &str, char_count: usize) -> u16 { let mut cols: u16 = 0; for (i, ch) in s.chars().enumerate() { if i >= char_count { break; } cols = cols.saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0) as u16); } cols } fn resolve_start_line_and_intra_indented( state: &TextAreaSyntaxState, inner: Rect, ) -> (usize, u16) { let provider = state.textarea.editor.data_provider(); let total = provider.line_count(); if total == 0 { return (0, 0); } let wrap = matches!(state.textarea.overflow_mode, TextOverflowMode::Wrap); let width = inner.width; let target_vis = state.textarea.scroll_y; if !wrap { let start = (target_vis as usize).min(total); return (start, 0); } let indent = state.textarea.wrap_indent_cols; let mut acc: u16 = 0; for i in 0..total { let s = provider.field_value(i); let rows = count_wrapped_rows_indented(s, width, indent); if acc.saturating_add(rows) > target_vis { let intra = target_vis.saturating_sub(acc); return (i, intra); } acc = acc.saturating_add(rows); } (total.saturating_sub(1), 0) } impl<'a> StatefulWidget for TextAreaSyntax<'a> { type State = TextAreaSyntaxState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { // Reuse existing scroll logic state.textarea.ensure_visible(area, self.block.as_ref()); let inner = if let Some(b) = &self.block { b.clone().render(area, buf); b.inner(area) } else { area }; let edited_now = state.textarea.take_edited_flag(); let wrap_mode = matches!(state.textarea.overflow_mode, TextOverflowMode::Wrap); let provider = state.textarea.editor.data_provider(); let total = provider.line_count(); let (start, intra) = resolve_start_line_and_intra_indented(state, inner); let mut display_lines: Vec = Vec::new(); if total == 0 || start >= total { if let Some(ph) = &state.textarea.placeholder { display_lines.push(Line::from(Span::raw(ph.clone()))); } } else if wrap_mode { let mut rows_left = inner.height; let indent = state.textarea.wrap_indent_cols; let mut i = start; while i < total && rows_left > 0 { let s = provider.field_value(i); let chunks = state .engine .highlight_line_cached(i, s, provider); let lines = wrap_chunks_indented(&chunks, inner.width, indent); let skip = if i == start { intra as usize } else { 0 }; for l in lines.into_iter().skip(skip) { display_lines.push(l); rows_left = rows_left.saturating_sub(1); if rows_left == 0 { break; } } i += 1; } } else { let end = (start.saturating_add(inner.height as usize)).min(total); for i in start..end { let s = provider.field_value(i); let chunks = state.engine.highlight_line_cached(i, s, provider); let fits = display_width(s) <= inner.width; let start_cols = if i == state.textarea.current_field() { let col_idx = state.textarea.display_cursor_position(); let cursor_cols = display_cols_up_to(s, col_idx); let (target_h, _left_cols) = compute_h_scroll_with_padding(cursor_cols, inner.width); if fits { if edited_now { target_h } else { 0 } } else { target_h.max(state.textarea.h_scroll) } } else { 0 }; if let TextOverflowMode::Indicator { ch } = state.textarea.overflow_mode { display_lines.push(clip_chunks_window_with_indicator_padded( &chunks, inner.width, ch, start_cols, )); } } } let p = Paragraph::new(display_lines) .alignment(Alignment::Left) .style(self.style); p.render(inner, buf); } }