use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, ListState},
Frame,
};
use std::collections::HashSet;
use crate::core::pane::PaneShared;
use crate::core::search::SearchMatch;
pub const BORDER_FOCUSED: Color = Color::Cyan;
pub const BORDER_UNFOCUSED: Color = Color::DarkGray;
pub const SEARCH_CURRENT_FG: Color = Color::Black;
pub const SEARCH_CURRENT_BG: Color = Color::Rgb(200, 120, 0);
pub const SEARCH_MATCH_BG: Color = Color::Rgb(60, 60, 0);
pub const LIST_SELECTION_BG: Color = Color::DarkGray;
pub const EMPTY_TEXT_FG: Color = Color::DarkGray;
pub const MODAL_BG: Color = Color::Rgb(30, 30, 30);
pub const SELECTION_BG: Color = Color::Rgb(60, 60, 100);
pub const CURSOR_FG: Color = Color::Black;
pub const CURSOR_BG: Color = Color::White;
pub struct SearchHighlight {
pub bg: Option<Color>,
pub fg_override: Option<Color>,
}
impl SearchHighlight {
pub fn none() -> Self {
Self {
bg: None,
fg_override: None,
}
}
pub fn is_active(&self) -> bool {
self.bg.is_some()
}
pub fn apply(&self, mut style: Style) -> Style {
if let Some(bg) = self.bg {
style = style.bg(bg);
}
if let Some(fg) = self.fg_override {
style = style.fg(fg);
}
style
}
pub fn style_with_fg(&self, default_fg: Color) -> Style {
let mut s = Style::default().fg(self.fg_override.unwrap_or(default_fg));
if let Some(bg) = self.bg {
s = s.bg(bg);
}
s
}
}
pub fn search_highlight_for(
match_set: &HashSet<usize>,
current_match_idx: Option<usize>,
idx: usize,
) -> SearchHighlight {
let is_current = current_match_idx == Some(idx);
let is_match = match_set.contains(&idx);
if is_current {
SearchHighlight {
bg: Some(SEARCH_CURRENT_BG),
fg_override: Some(SEARCH_CURRENT_FG),
}
} else if is_match {
SearchHighlight {
bg: Some(SEARCH_MATCH_BG),
fg_override: None,
}
} else {
SearchHighlight::none()
}
}
pub fn pane_block(title: &str, is_focused: bool) -> Block<'_> {
Block::default()
.title(format!(" {title} "))
.borders(Borders::ALL)
.border_style(Style::default().fg(if is_focused {
BORDER_FOCUSED
} else {
BORDER_UNFOCUSED
}))
}
pub fn render_empty_list(f: &mut Frame, area: Rect, block: Block, message: &str) {
let items = vec![ListItem::new(Line::from(Span::styled(
format!(" {message}"),
Style::default().fg(EMPTY_TEXT_FG),
)))];
let list = List::new(items).block(block);
f.render_widget(list, area);
}
pub fn list_highlight_style(selected_is_match: bool) -> Style {
if selected_is_match {
Style::default().add_modifier(Modifier::BOLD)
} else {
Style::default()
.bg(LIST_SELECTION_BG)
.add_modifier(Modifier::BOLD)
}
}
pub fn render_search_list(
f: &mut Frame,
area: Rect,
items: Vec<ListItem>,
block: Block,
selected_idx: Option<usize>,
match_set: &HashSet<usize>,
) {
let highlight_style =
list_highlight_style(selected_idx.is_some_and(|idx| match_set.contains(&idx)));
let list = List::new(items)
.block(block)
.highlight_style(highlight_style);
let mut list_state = ListState::default();
list_state.select(selected_idx);
f.render_stateful_widget(list, area, &mut list_state);
}
#[allow(clippy::too_many_arguments)]
pub fn render_list_pane(
f: &mut Frame,
area: Rect,
shared: &PaneShared,
pane_id: usize,
title: &str,
selected: Option<usize>,
empty: Option<&str>,
build_items: impl FnOnce(&HashSet<usize>, Option<usize>) -> Vec<ListItem<'static>>,
) {
let block = pane_block(title, shared.focused_pane == pane_id);
if let Some(message) = empty {
render_empty_list(f, area, block, message);
return;
}
let (match_set, current_match_idx) = list_search_highlights(shared, pane_id);
let items = build_items(&match_set, current_match_idx);
render_search_list(f, area, items, block, selected, &match_set);
}
pub fn list_search_highlights(
shared: &PaneShared,
pane_id: usize,
) -> (HashSet<usize>, Option<usize>) {
if shared.search.origin != pane_id {
return (HashSet::new(), None);
}
let set: HashSet<usize> = shared
.search
.matches
.iter()
.filter_map(|m| match m {
SearchMatch::ListEntry(idx) => Some(*idx),
_ => None,
})
.collect();
let current =
shared
.search
.current_match_idx
.and_then(|ci| match shared.search.matches.get(ci) {
Some(SearchMatch::ListEntry(idx)) => Some(*idx),
_ => None,
});
(set, current)
}