use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::{
layout::{Constraint, Layout},
symbols::{
border::{ROUNDED, Set},
line::{VERTICAL_LEFT, VERTICAL_RIGHT},
},
widgets::{Block, Borders, List, ListItem, ListState, StatefulWidget, Widget},
};
use crate::{
misc::{buffer_ext::BufferExt, rect_ext::RectExt},
tui::{
app_default::{AppDefault, AppTitle},
component::Component,
widgets::input::Input,
},
};
#[derive(Debug, Default)]
pub struct TextPickerWithSuggestion<P: Provider> {
title: Option<String>,
input: Input,
list: ListState,
args: (String, usize),
items: Vec<P::Suggestion>,
provider: P,
}
impl<P> TextPickerWithSuggestion<P>
where
P: Provider,
{
pub fn new(provider: P) -> Self {
Self {
title: Default::default(),
input: Default::default(),
list: ListState::default().with_selected(0.into()),
args: (String::default(), 0),
items: provider.suggestions("", 0),
provider,
}
}
pub fn with_title(self, title: impl Into<String>) -> Self {
Self {
title: Some(title.into()),
..self
}
}
pub fn with_value(self, value: impl Into<String>) -> Self {
Self {
input: self.input.with_value(value.into()),
..self
}
}
pub fn value(&self) -> &str {
self.input.value()
}
pub fn has_suggestions(&self) -> bool {
!self.items.is_empty()
}
pub fn selected_suggestion(&self) -> Option<&<P as Provider>::Suggestion> {
self.list.selected().and_then(|idx| self.items.get(idx))
}
pub fn clear_suggestion_selection(&mut self) {
self.list = ListState::default().with_selected(0.into());
}
pub fn apply_selected_suggestion(&mut self) {
if let Some(suggestion) = self.list.selected().and_then(|idx| self.items.get(idx)) {
suggestion.apply_to(&mut self.input);
}
}
}
impl<P> Component for TextPickerWithSuggestion<P>
where
P: Provider,
{
fn render(
&mut self,
_area: ratatui::prelude::Rect,
buf: &mut ratatui::prelude::Buffer,
focus_state: crate::tui::component::FocusState,
) {
if self.args.0 != self.input.value() || self.args.1 != self.input.cursor() {
self.args = (self.input.value().to_owned(), self.input.cursor());
self.items = self
.provider
.suggestions(self.input.value(), self.input.cursor());
}
let list = List::app_default()
.block(Block::app_default().border_set(Set {
top_left: VERTICAL_RIGHT,
top_right: VERTICAL_LEFT,
..ROUNDED
}))
.items(
self.items
.iter()
.map(|suggestion| ListItem::new(suggestion.title())),
);
let height = list.len().saturating_add(4).min(25) as u16;
let area = buf.area.palette(height);
buf.clear(area);
let [input_area, list_area] =
Layout::vertical([Constraint::Length(2), Constraint::Fill(1)]).areas(area);
let input_area = {
let mut block =
Block::app_default().borders(Borders::LEFT | Borders::RIGHT | Borders::TOP);
if let Some(title) = &self.title {
block = block.app_title(title.as_str());
}
let input_inner = block.inner(input_area);
Widget::render(block, area, buf);
input_inner
};
self.input.render(input_area, buf, focus_state);
StatefulWidget::render(list, list_area, buf, &mut self.list);
}
fn handle(&mut self, event: crossterm::event::KeyEvent) -> bool {
self.input.handle(event)
|| match (event.code, event.modifiers) {
(KeyCode::Up, KeyModifiers::NONE) | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
if self.list.selected() != Some(0) {
self.list.select_previous();
} else {
self.list.select(Some(self.items.len().saturating_sub(1)));
}
true
}
(KeyCode::Down, KeyModifiers::NONE)
| (KeyCode::Char('n'), KeyModifiers::CONTROL) => {
if self.list.selected() != Some(self.items.len().saturating_sub(1)) {
self.list.select_next();
} else {
self.list.select_first();
}
true
}
_ => false,
}
}
}
pub trait Suggestion {
fn title(&self) -> &str;
fn apply_to(&self, input: &mut Input);
}
pub trait Provider {
type Suggestion: Suggestion;
fn suggestions(&self, query: &str, cursor: usize) -> Vec<Self::Suggestion>;
}
impl<S> Provider for fn(&str, usize) -> Vec<S>
where
S: Suggestion,
{
type Suggestion = S;
fn suggestions(&self, query: &str, cursor: usize) -> Vec<Self::Suggestion> {
self(query, cursor)
}
}