1use crate::theme::Theme;
2use crate::view::edit_buffer::EditBuffer;
3use crate::view::wrap::{as_u16, fit_prefix};
4use ratatui::buffer::Buffer;
5use ratatui::layout::{Constraint, Layout, Rect};
6use ratatui::style::{Modifier, Style};
7use ratatui::text::{Line, Span};
8use ratatui::widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget};
9use std::borrow::Cow;
10use unicode_width::UnicodeWidthStr;
11
12pub type KeyHint = (&'static str, Cow<'static, str>);
15
16pub const SCROLLBAR_WIDTH: u16 = 1;
18
19pub fn rows_and_track(area: Rect, scrollbar: bool) -> (Rect, Rect) {
25 let track_width = if scrollbar { SCROLLBAR_WIDTH } else { 0 };
26 let [rows, track] = Layout::horizontal([Constraint::Min(0), Constraint::Length(track_width)]).areas(area);
27 (rows, track)
28}
29
30pub fn row_area(area: Rect, index: usize) -> Option<Rect> {
33 let offset = as_u16(index);
34 (offset < area.height).then(|| Rect { y: area.y + offset, height: 1, ..area })
35}
36
37#[derive(Clone)]
39pub struct RowsView<'a> {
40 lines: Cow<'a, [Line<'static>]>,
41}
42
43impl<'a> RowsView<'a> {
44 pub fn new(lines: &'a [Line<'static>]) -> Self {
45 Self { lines: Cow::Borrowed(lines) }
46 }
47
48 pub fn from_lines(lines: impl IntoIterator<Item = Line<'static>>) -> Self {
49 Self { lines: Cow::Owned(lines.into_iter().collect()) }
50 }
51}
52
53impl Widget for RowsView<'_> {
54 fn render(self, area: Rect, buf: &mut Buffer) {
55 for (index, line) in self.lines.iter().enumerate() {
56 let Some(row) = row_area(area, index) else {
57 return;
58 };
59 line.render(row, buf);
60 }
61 }
62}
63
64pub fn render_vertical_scrollbar(area: Rect, buf: &mut Buffer, content_rows: usize, offset: usize) {
71 let scrollable = content_rows.saturating_sub(usize::from(area.height));
72 let mut state = ScrollbarState::new(scrollable).position(offset);
73 StatefulWidget::render(Scrollbar::new(ScrollbarOrientation::VerticalRight), area, buf, &mut state);
74}
75
76pub fn key_hints(hints: &[(impl AsRef<str>, impl AsRef<str>)], theme: &Theme) -> Line<'static> {
83 let key_style = Style::from((theme.accent, Modifier::BOLD));
84 let muted = Style::from(theme.muted);
85 let mut spans = Vec::with_capacity(hints.len() * 3);
86 for (key, description) in hints {
87 if !spans.is_empty() {
88 spans.push(Span::raw(" "));
89 }
90 spans.push(Span::styled(key.as_ref().to_string(), key_style));
91 spans.push(Span::styled(format!(" {}", description.as_ref()), muted));
92 }
93 Line::from(spans)
94}
95
96pub(crate) fn visible_window(buffer: &EditBuffer, width: usize) -> (String, usize) {
97 if width == 0 {
98 return (String::new(), 0);
99 }
100 let text = buffer.text();
101 let cursor_column = text[..buffer.cursor()].width();
102 let start_column = cursor_column.saturating_sub(width.saturating_sub(1));
103 let (start, _) = fit_prefix(text, start_column);
104 let cursor_width = text[start..buffer.cursor()].width();
105 let (visible_len, _) = fit_prefix(&text[start..], width);
106 (text[start..start + visible_len].to_string(), cursor_width.min(width.saturating_sub(1)))
107}