use std::ops::{Deref, DerefMut};
use crossterm::event::{KeyCode, KeyEvent, MouseEvent};
use ratatui::{buffer::Buffer, layout::Rect};
use nucleo_matcher::{Config, Matcher};
use ratatui_interact::components::InputState;
use crate::app::{fuzzy_match_score, MatchScores};
use crate::theme::UiColors;
use super::{Component, EventResult, OverlayRequest, RenderableComponent};
pub struct FilterableItem {
pub key: String,
pub name_texts: Vec<String>,
pub help: Option<String>,
}
pub fn compute_match_scores(
items: &[FilterableItem],
pattern: &str,
) -> std::collections::HashMap<String, MatchScores> {
let mut scores = std::collections::HashMap::new();
let mut matcher = Matcher::new(Config::DEFAULT);
for item in items {
let name_score = item
.name_texts
.iter()
.map(|t| fuzzy_match_score(t, pattern, &mut matcher))
.max()
.unwrap_or(0);
let help_score = item
.help
.as_ref()
.map(|h| fuzzy_match_score(h, pattern, &mut matcher))
.unwrap_or(0);
scores.insert(item.key.clone(), MatchScores { name_score, help_score });
}
scores
}
pub trait Filterable: Component {
fn apply_filter(&mut self, text: &str) -> Option<<Self as Component>::Action>;
fn clear_filter(&mut self) -> Option<<Self as Component>::Action>;
fn set_filter_active(&mut self, active: bool);
fn has_active_filter(&self) -> bool;
#[allow(dead_code)] fn filter_text(&self) -> &str;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FilterAction<A> {
Inner(A),
FocusNext,
FocusPrev,
}
pub struct FilterableComponent<C> {
inner: C,
filtering: bool,
filter_input: InputState,
}
impl<C: Filterable> FilterableComponent<C> {
pub fn new(inner: C) -> Self {
Self {
inner,
filtering: false,
filter_input: InputState::empty(),
}
}
pub fn is_filtering(&self) -> bool {
self.filtering
}
pub fn has_active_filter(&self) -> bool {
self.inner.has_active_filter()
}
pub fn filter_text(&self) -> &str {
self.inner.filter_text()
}
pub fn start_filtering(&mut self) {
self.filtering = true;
self.filter_input.clear();
self.inner.set_filter_active(true);
}
#[allow(dead_code)]
pub fn set_filter_text(&mut self, text: &str) {
self.filter_input.set_text(text.to_string());
self.inner.apply_filter(text);
}
#[allow(dead_code)]
pub fn filter_input(&self) -> &InputState {
&self.filter_input
}
#[allow(dead_code)]
pub fn filter_input_mut(&mut self) -> &mut InputState {
&mut self.filter_input
}
fn sync_filter(&mut self) -> EventResult<FilterAction<<C as Component>::Action>> {
let text = self.filter_input.text().to_string();
match self.inner.apply_filter(&text) {
Some(action) => EventResult::Action(FilterAction::Inner(action)),
None => EventResult::Consumed,
}
}
fn stop_and_clear(&mut self) {
self.filtering = false;
self.filter_input.clear();
self.inner.clear_filter();
self.inner.set_filter_active(false);
}
}
impl<C: Filterable> Deref for FilterableComponent<C> {
type Target = C;
fn deref(&self) -> &C {
&self.inner
}
}
impl<C: Filterable> DerefMut for FilterableComponent<C> {
fn deref_mut(&mut self) -> &mut C {
&mut self.inner
}
}
impl<C: Filterable> Component for FilterableComponent<C> {
type Action = FilterAction<C::Action>;
fn handle_focus_gained(&mut self) -> EventResult<Self::Action> {
self.inner.handle_focus_gained().map(FilterAction::Inner)
}
fn handle_focus_lost(&mut self) -> EventResult<Self::Action> {
let result = self.inner.handle_focus_lost().map(FilterAction::Inner);
if self.filtering || self.inner.has_active_filter() {
self.stop_and_clear();
match result {
EventResult::NotHandled => EventResult::Consumed,
other => other,
}
} else {
result
}
}
fn handle_key(&mut self, key: KeyEvent) -> EventResult<Self::Action> {
if self.filtering {
match key.code {
KeyCode::Esc => {
self.stop_and_clear();
EventResult::Consumed
}
KeyCode::Enter => {
self.filtering = false;
self.inner.set_filter_active(false);
EventResult::Consumed
}
KeyCode::Tab => {
self.stop_and_clear();
EventResult::Action(FilterAction::FocusNext)
}
KeyCode::BackTab => {
self.stop_and_clear();
EventResult::Action(FilterAction::FocusPrev)
}
KeyCode::Backspace => {
self.filter_input.delete_char_backward();
self.sync_filter()
}
KeyCode::Char(c) => {
self.filter_input.insert_char(c);
self.sync_filter()
}
KeyCode::Up | KeyCode::Down => {
self.inner.handle_key(key).map(FilterAction::Inner)
}
_ => EventResult::Consumed,
}
} else {
match self.inner.handle_key(key) {
EventResult::NotHandled
if key.code == KeyCode::Esc && self.inner.has_active_filter() =>
{
self.stop_and_clear();
EventResult::Consumed
}
EventResult::NotHandled if key.code == KeyCode::Char('/') => {
self.start_filtering();
EventResult::Consumed
}
other => other.map(FilterAction::Inner),
}
}
}
fn handle_mouse(&mut self, event: MouseEvent, area: Rect) -> EventResult<Self::Action> {
self.inner.handle_mouse(event, area).map(FilterAction::Inner)
}
fn collect_overlays(&mut self) -> Vec<OverlayRequest> {
self.inner.collect_overlays()
}
}
impl<C: Filterable + RenderableComponent> RenderableComponent for FilterableComponent<C> {
fn render(&mut self, area: Rect, buf: &mut Buffer, colors: &UiColors) {
self.inner.render(area, buf, colors);
}
}