use crate::view::edit_buffer::{EditBuffer, apply_edit_key};
use crate::theme::Theme;
use crate::view::widgets::{SCROLLBAR_WIDTH, render_vertical_scrollbar, row_area, rows_and_track};
use crate::view::wrap::{fit_line, text_position_in_wrap, wrap_text_char};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::widgets::Widget;
use std::rc::Rc;
use unicode_width::UnicodeWidthStr;
const COMMENT_BODY_PREFIX: &str = "│ > ";
const COMMENT_RIGHT_PADDING: usize = 3;
pub struct Draft<A> {
pub anchor: A,
pub buffer: EditBuffer,
}
pub fn wrapped_with_cursor(buffer: &EditBuffer, width: usize) -> (Vec<String>, (usize, u16)) {
(wrap_text_char(buffer.text(), width), text_position_in_wrap(&buffer.text()[..buffer.cursor()], width))
}
pub fn comment_body_width(width: u16) -> usize {
usize::from(width)
.saturating_sub(COMMENT_BODY_PREFIX.width())
.saturating_sub(COMMENT_RIGHT_PADDING)
.max(1)
}
pub fn comment_box(
title: &str,
body: &[String],
border_color: ratatui::style::Color,
width: u16,
theme: &Theme,
) -> Vec<Line<'static>> {
let surface = Style::new().bg(theme.sidebar_bg);
let border = surface.fg(border_color);
let body_style = surface.fg(theme.text_primary);
let width = usize::from(width);
let mut lines = vec![fit_line(Line::styled(title.to_string(), border), width, border)];
lines.extend(body.iter().map(|text| {
fit_line(
Line::styled(format!("{COMMENT_BODY_PREFIX}{text}"), body_style),
width,
body_style,
)
}));
lines.push(fit_line(Line::styled("└", border), width, border));
lines
}
pub fn draft_body<A>(draft: &Draft<A>, body_width: usize) -> (Vec<String>, (usize, u16)) {
let (lines, (row, column)) = wrapped_with_cursor(&draft.buffer, body_width);
(lines, (1 + row, u16::try_from(COMMENT_BODY_PREFIX.width()).unwrap_or(u16::MAX).saturating_add(column)))
}
pub struct AnnotatedRows<A> {
rows: Vec<Row<A>>,
draft_cursor: Option<(usize, u16)>,
}
#[derive(Clone)]
pub struct Row<A> {
line: Rc<Line<'static>>,
anchor: Option<A>,
selectable: bool,
}
impl<A: Copy> Row<A> {
pub fn at(line: Line<'static>, anchor: A) -> Self {
Self { line: Rc::new(line), anchor: Some(anchor), selectable: true }
}
pub fn anchored(line: Line<'static>, anchor: A) -> Self {
Self { line: Rc::new(line), anchor: Some(anchor), selectable: false }
}
pub fn inert(line: Line<'static>) -> Self {
Self { line: Rc::new(line), anchor: None, selectable: false }
}
pub fn anchor(&self) -> Option<A> {
self.anchor
}
}
impl<A> Default for AnnotatedRows<A> {
fn default() -> Self {
Self { rows: Vec::new(), draft_cursor: None }
}
}
pub struct AnnotatedRowsView<'a, A> {
rows: &'a AnnotatedRows<A>,
offset: usize,
cursor: Option<usize>,
theme: &'a Theme,
}
impl<'a, A> AnnotatedRowsView<'a, A> {
pub fn new(rows: &'a AnnotatedRows<A>, offset: usize, cursor: Option<usize>, theme: &'a Theme) -> Self {
Self { rows, offset, cursor, theme }
}
}
impl<A: Copy + PartialEq> Widget for AnnotatedRowsView<'_, A> {
fn render(self, area: Rect, buf: &mut Buffer) {
let (body, track) = rows_and_track(area, true);
for (index, row) in self.rows.rows.iter().skip(self.offset).enumerate() {
let Some(row_area) = row_area(body, index) else {
break;
};
row.line.as_ref().render(row_area, buf);
if self.cursor == Some(self.offset + index) {
paint_cursor_row(row_area, buf, self.theme);
}
}
render_vertical_scrollbar(track, buf, self.rows.rows.len(), self.offset);
}
}
impl<A: Copy + PartialEq> AnnotatedRows<A> {
pub fn content_width(area_width: u16) -> u16 {
area_width.saturating_sub(SCROLLBAR_WIDTH)
}
pub fn push(&mut self, line: Line<'static>, anchor: A) {
self.rows.push(Row::at(line, anchor));
}
pub fn push_anchored(&mut self, line: Line<'static>, anchor: A) {
self.rows.push(Row::anchored(line, anchor));
}
pub fn push_row(&mut self, row: &Row<A>) {
self.rows.push(row.clone());
}
pub fn push_annotation(&mut self, lines: impl IntoIterator<Item = Line<'static>>) {
self.rows.extend(lines.into_iter().map(Row::inert));
}
pub fn push_draft(&mut self, lines: Vec<Line<'static>>, cursor: (usize, u16)) {
self.draft_cursor = Some((self.rows.len() + cursor.0, cursor.1));
self.push_annotation(lines);
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn row_of(&self, anchor: A) -> Option<usize> {
self.rows.iter().position(|row| row.anchor == Some(anchor) && row.selectable)
}
pub fn anchor_at_or_above(&self, row: usize) -> Option<A> {
self.rows.iter().take(row.saturating_add(1)).rev().find_map(|row| row.selectable.then_some(row.anchor?))
}
pub fn draft_cursor(&self) -> Option<(usize, u16)> {
self.draft_cursor
}
}
fn paint_cursor_row(area: Rect, buf: &mut Buffer, theme: &Theme) {
for x in area.x..area.right() {
if let Some(cell) = buf.cell_mut((x, area.y)) {
cell.set_bg(theme.accent);
cell.set_fg(theme.background);
}
}
}
pub fn apply_draft_key<A: Copy>(slot: &mut Option<Draft<A>>, key: KeyEvent) -> Option<(A, String)> {
let draft = slot.as_mut()?;
let anchor = draft.anchor;
match draft.on_key(key) {
DraftOutcome::Continue => None,
DraftOutcome::Commit(body) => {
*slot = None;
Some((anchor, body))
}
DraftOutcome::Discard => {
*slot = None;
None
}
}
}
pub fn paste_into_draft<A>(slot: &mut Option<Draft<A>>, text: &str) {
if let Some(draft) = slot.as_mut() {
draft.buffer.insert_paste(text);
}
}
enum DraftOutcome {
Continue,
Discard,
Commit(String),
}
impl<A> Draft<A> {
pub fn new(anchor: A) -> Self {
Self { anchor, buffer: EditBuffer::default() }
}
fn on_key(&mut self, key: KeyEvent) -> DraftOutcome {
match key.code {
KeyCode::Esc => DraftOutcome::Discard,
KeyCode::Enter => {
let body = self.buffer.take();
if body.trim().is_empty() { DraftOutcome::Discard } else { DraftOutcome::Commit(body) }
}
_ => {
apply_edit_key(&mut self.buffer, key);
DraftOutcome::Continue
}
}
}
}