use ferritin_common::DocRef;
use ratatui::buffer::Buffer;
use ratatui::layout::{Position, Rect};
use rustdoc_types::Item;
use super::channels::UiCommand;
use super::render_document::BASELINE_LEFT_MARGIN;
use super::theme::InteractiveTheme;
use std::borrow::Cow;
use std::fmt::{self, Display, Formatter};
use std::ops::Range;
#[derive(Debug, Clone, PartialEq)]
pub enum HistoryEntry<'a> {
Item(DocRef<'a, Item>),
Search {
query: String,
crate_name: Option<String>,
},
List {
default_crate: Option<&'a str>,
},
}
impl Display for HistoryEntry<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
HistoryEntry::Item(item) => f.write_str(item.name().unwrap_or("<unnamed>")),
HistoryEntry::Search { query, crate_name } => {
if query.is_empty() {
if let Some(crate_name) = crate_name {
f.write_fmt(format_args!("Search in {}", crate_name))
} else {
f.write_str("Search")
}
} else {
if let Some(crate_name) = crate_name {
f.write_fmt(format_args!("\"{}\" in {}", query, crate_name))
} else {
f.write_fmt(format_args!("\"{}\"", query))
}
}
}
HistoryEntry::List { .. } => f.write_str("List"),
}
}
}
impl<'a> HistoryEntry<'a> {
pub(super) fn item(&self) -> Option<DocRef<'a, Item>> {
if let Self::Item(item) = self {
Some(*item)
} else {
None
}
}
pub(super) fn display_name(&self) -> String {
self.to_string()
}
pub(super) fn crate_name(&self) -> Option<&str> {
match self {
HistoryEntry::Item(item) => Some(item.crate_docs().name()),
HistoryEntry::Search { crate_name, .. } => crate_name.as_deref(),
HistoryEntry::List { default_crate } => default_crate.as_deref(),
}
}
pub(super) fn to_command(&self) -> UiCommand<'a> {
match self {
HistoryEntry::Item(item) => UiCommand::Navigate(*item),
HistoryEntry::Search { query, crate_name } => UiCommand::Search {
query: Cow::Owned(query.clone()),
crate_name: crate_name.as_ref().map(|c| Cow::Owned(c.clone())),
limit: 20,
},
HistoryEntry::List { .. } => UiCommand::List,
}
}
}
#[derive(Debug)]
pub(super) struct History<'a> {
entries: Vec<HistoryEntry<'a>>,
current_index: usize,
clickable_areas: Vec<(usize, Range<u16>)>,
hover_pos: Option<Position>,
}
impl<'a> History<'a> {
pub(super) fn new(initial_entry: Option<HistoryEntry<'a>>) -> Self {
let mut entries = Vec::new();
if let Some(entry) = initial_entry {
entries.push(entry);
}
Self {
entries,
current_index: 0,
clickable_areas: Vec::new(),
hover_pos: None,
}
}
pub(super) fn push(&mut self, entry: HistoryEntry<'a>) {
if self.entries.is_empty() || self.current() != Some(&entry) {
self.entries.truncate(self.current_index + 1);
self.entries.push(entry);
self.current_index = self.entries.len() - 1;
}
}
pub(super) fn go_back(&mut self) -> Option<&HistoryEntry<'a>> {
if self.current_index > 0 {
self.current_index -= 1;
Some(&self.entries[self.current_index])
} else {
None
}
}
pub(super) fn go_forward(&mut self) -> Option<&HistoryEntry<'a>> {
if self.current_index + 1 < self.entries.len() {
self.current_index += 1;
Some(&self.entries[self.current_index])
} else {
None
}
}
pub(super) fn current(&self) -> Option<&HistoryEntry<'a>> {
self.entries.get(self.current_index)
}
pub(super) fn can_go_back(&self) -> bool {
self.current_index > 0
}
pub(super) fn can_go_forward(&self) -> bool {
self.current_index + 1 < self.entries.len()
}
pub(super) fn render(&mut self, buf: &mut Buffer, area: Rect, theme: &InteractiveTheme) {
self.clickable_areas.clear();
let history: &[HistoryEntry<'a>] = &self.entries;
let current_idx = self.current_index;
let clickable_areas: &mut Vec<(usize, std::ops::Range<u16>)> = &mut self.clickable_areas;
let hover_pos = self.hover_pos;
let bg_style = theme.breadcrumb_style;
for x in 0..area.width {
buf.cell_mut((x, area.y)).unwrap().reset();
buf.cell_mut((x, area.y)).unwrap().set_style(bg_style);
}
if history.is_empty() {
let text = " 🦀 <no history>";
let mut col = BASELINE_LEFT_MARGIN;
for ch in text.chars() {
if col >= area.width {
break;
}
buf.cell_mut((col, area.y))
.unwrap()
.set_char(ch)
.set_style(bg_style);
col += 1;
}
return;
}
let mut col = BASELINE_LEFT_MARGIN;
let icon = " 🦀 ";
for ch in icon.chars() {
if col >= area.width {
break;
}
buf.cell_mut((col, area.y))
.unwrap()
.set_char(ch)
.set_style(bg_style);
col += 1;
}
for (idx, item) in history.iter().enumerate() {
if col >= area.width {
break;
}
if idx > 0 {
let arrow = " → ";
for ch in arrow.chars() {
if col >= area.width {
break;
}
buf.cell_mut((col, area.y))
.unwrap()
.set_char(ch)
.set_style(bg_style);
col += 1;
}
}
let name = item.display_name();
let start_col = col;
let name_len = name.chars().count().min((area.width - start_col) as usize);
let end_col = start_col + name_len as u16;
let is_hovered = hover_pos.is_some_and(|pos| pos.x >= start_col && pos.x < end_col);
let item_style = if is_hovered {
theme.breadcrumb_hover_style
} else if idx == current_idx {
theme.breadcrumb_current_style
} else {
theme.breadcrumb_style
};
for ch in name.chars() {
if col >= area.width {
break;
}
buf.cell_mut((col, area.y))
.unwrap()
.set_char(ch)
.set_style(item_style);
col += 1;
}
if end_col > start_col {
clickable_areas.push((idx, start_col..end_col));
}
}
}
pub(super) fn handle_hover(&mut self, pos: Position) {
let hovering = self
.clickable_areas
.iter()
.any(|(_, range)| range.contains(&pos.x));
self.hover_pos = if hovering { Some(pos) } else { None };
}
pub(super) fn clear_hover(&mut self) {
self.hover_pos = None;
}
pub(super) fn handle_click(&mut self, pos: Position) -> Option<&HistoryEntry<'a>> {
if let Some((idx, _)) = self
.clickable_areas
.iter()
.find(|(_, range)| range.contains(&pos.x))
{
self.current_index = *idx;
self.current()
} else {
None
}
}
pub(super) fn is_hovering(&self) -> bool {
self.hover_pos.is_some()
}
}