use crate::{err, Result};
use ratatui::buffer::Cell;
use ratatui::prelude::{Buffer, Rect};
use std::fmt;
use term_rustdoc::tree::{TreeLine, TreeLines};
mod generics;
mod interaction;
mod markdown;
mod render;
pub use self::generics::{render_line, LineState, Lines};
pub use self::interaction::{ScrollOffset, Scrollable};
pub use self::markdown::{
Headings, MarkdownAndHeading, MarkdownArea, ScrollHeading, ScrollMarkdown, ScrollText,
};
pub type ScrollTreeLines = Scroll<TreeLines>;
pub struct Cursor<State> {
pub y: u16,
pub state: State,
}
impl<State: Default> Default for Cursor<State> {
fn default() -> Self {
Self {
y: 0,
state: Default::default(),
}
}
}
pub struct Scroll<Ls: Lines> {
pub lines: Ls,
pub start: usize,
pub cursor: Cursor<<Ls::Line as LineState>::State>,
pub max_width: u16,
pub area: Rect,
}
impl<Ls: Lines> Scroll<Ls> {
pub fn all_lines(&self) -> &[Ls::Line] {
&self.lines
}
pub fn visible_lines(&self) -> Option<&[Ls::Line]> {
let total_len = self.lines.len();
if total_len == 0 {
return None;
}
let end = (self.start + self.area.height as usize).min(total_len);
self.lines.get(self.start..end)
}
pub fn get_line_of_current_cursor(&self) -> Option<&Ls::Line> {
self.visible_lines().and_then(|lines| {
let cursor = self.cursor.y as usize;
let line = lines.get(cursor);
if lines.get(cursor).is_none() {
error!(
"Cursor on row {cursor} is beyond all lines length {}.",
self.total_len()
);
}
line
})
}
pub fn get_line_on_screen(&self, y: u16) -> Option<&Ls::Line> {
y.checked_sub(self.area.y)
.and_then(|offset| self.visible_lines()?.get(self.start + offset as usize))
}
pub fn force_line_on_screen(&mut self, y: u16) -> Option<&Ls::Line> {
if !self.is_empty() && y >= self.area.y && y < self.area.y + self.area.height {
let y = y - self.area.y;
if let Some(current) = self.lines.get(self.start + y as usize) {
self.cursor.y = y;
return Some(current);
}
}
None
}
pub fn highlight_current_line(&self, buf: &mut Buffer, mut f: impl FnMut(&mut Cell)) {
let current = self.start + self.cursor.y as usize;
if self.lines.get(current).is_some() {
let area = self.area;
let y = self.cursor.y + area.y;
for x in area.x..(area.x + area.width) {
f(buf.get_mut(x, y));
}
}
}
pub fn total_len(&self) -> usize {
self.lines.len()
}
pub fn is_empty(&self) -> bool {
self.total_len() == 0
}
}
impl<Ls> Default for Scroll<Ls>
where
Ls: Default + Lines,
<Ls::Line as LineState>::State: Default,
{
fn default() -> Self where {
let (lines, start, cursor, max_windth, area) = Default::default();
Scroll {
lines,
start,
cursor,
max_width: max_windth,
area,
}
}
}
impl<Ls> Scroll<Ls>
where
Ls: Default + Lines<Line = TreeLine>,
{
pub fn new_tree_lines(lines: Ls) -> Result<Self> {
let w = lines.iter().map(TreeLine::width).max();
let max_windth = w.ok_or_else(|| err!("The documentation is empty with no items."))?;
Ok(Self {
lines,
max_width: max_windth,
..Default::default()
})
}
pub fn get_id(&self) -> Option<&str> {
self.all_lines()
.get(self.cursor.y as usize + self.start)
.and_then(|l| l.id.as_deref())
}
pub fn update_maxwidth(&mut self) {
self.max_width = self.lines.iter().map(TreeLine::width).max().unwrap();
}
}
impl<Ls: Lines> fmt::Debug for Scroll<Ls> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Scrollable");
s.field("lines.len", &self.total_len())
.field("start", &self.start)
.field("cursor.y", &self.cursor.y)
.field("max_windth", &self.max_width)
.field("area", &self.area);
s.finish()
}
}