use crate::components::navigation::{
NavigationConfig, calculate_visible_range, handle_list_navigation,
};
use crate::components::{Box as TinkBox, Text};
use crate::core::{Color, Element, FlexDirection};
use crate::hooks::{Signal, use_input, use_signal};
#[derive(Debug, Clone)]
pub struct SelectItem<T: Clone> {
pub label: String,
pub value: T,
}
impl<T: Clone> SelectItem<T> {
pub fn new(label: impl Into<String>, value: T) -> Self {
Self {
label: label.into(),
value,
}
}
}
impl<T: Clone + ToString> From<T> for SelectItem<T> {
fn from(value: T) -> Self {
Self {
label: value.to_string(),
value,
}
}
}
#[derive(Debug, Clone)]
pub struct SelectInputStyle {
pub highlight_color: Option<Color>,
pub highlight_bg: Option<Color>,
pub highlight_bold: bool,
pub indicator: String,
pub indicator_padding: String,
pub item_color: Option<Color>,
}
impl Default for SelectInputStyle {
fn default() -> Self {
Self {
highlight_color: Some(Color::Cyan),
highlight_bg: None,
highlight_bold: true,
indicator: "❯ ".to_string(),
indicator_padding: " ".to_string(),
item_color: None,
}
}
}
impl SelectInputStyle {
pub fn new() -> Self {
Self::default()
}
pub fn highlight_color(mut self, color: Color) -> Self {
self.highlight_color = Some(color);
self
}
pub fn highlight_bg(mut self, color: Color) -> Self {
self.highlight_bg = Some(color);
self
}
pub fn highlight_bold(mut self, bold: bool) -> Self {
self.highlight_bold = bold;
self
}
pub fn indicator(mut self, indicator: impl Into<String>) -> Self {
let ind = indicator.into();
self.indicator_padding = " ".repeat(ind.chars().count());
self.indicator = ind;
self
}
pub fn item_color(mut self, color: Color) -> Self {
self.item_color = Some(color);
self
}
}
pub struct SelectInput<T: Clone + 'static> {
items: Vec<SelectItem<T>>,
highlighted: usize,
limit: Option<usize>,
style: SelectInputStyle,
is_focused: bool,
vim_navigation: bool,
number_shortcuts: bool,
}
impl<T: Clone + 'static> SelectInput<T> {
pub fn new(items: Vec<SelectItem<T>>) -> Self {
Self {
items,
highlighted: 0,
limit: None,
style: SelectInputStyle::default(),
is_focused: true,
vim_navigation: true,
number_shortcuts: true,
}
}
pub fn from_items<I>(iter: I) -> Self
where
I: IntoIterator<Item = SelectItem<T>>,
{
Self::new(iter.into_iter().collect())
}
pub fn highlighted(mut self, index: usize) -> Self {
self.highlighted = index.min(self.items.len().saturating_sub(1));
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn style(mut self, style: SelectInputStyle) -> Self {
self.style = style;
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.is_focused = focused;
self
}
pub fn vim_navigation(mut self, enabled: bool) -> Self {
self.vim_navigation = enabled;
self
}
pub fn number_shortcuts(mut self, enabled: bool) -> Self {
self.number_shortcuts = enabled;
self
}
pub fn highlight_color(mut self, color: Color) -> Self {
self.style.highlight_color = Some(color);
self
}
pub fn indicator(mut self, indicator: impl Into<String>) -> Self {
self.style = self.style.indicator(indicator);
self
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn into_element(self) -> Element {
if self.items.is_empty() {
return TinkBox::new().into_element();
}
let items = self.items.clone();
let initial_highlighted = self.highlighted;
let limit = self.limit;
let style = self.style.clone();
let is_focused = self.is_focused;
let vim_navigation = self.vim_navigation;
let number_shortcuts = self.number_shortcuts;
let highlighted_signal = use_signal(|| initial_highlighted);
if is_focused {
let items_len = items.len();
let highlighted_for_input = highlighted_signal.clone();
use_input(move |input, key| {
let current = highlighted_for_input.get();
let config = NavigationConfig::new()
.vim_navigation(vim_navigation)
.number_shortcuts(number_shortcuts);
let result = handle_list_navigation(current, items_len, input, *key, &config);
if let Some(new_pos) = result.is_moved().then(|| result.unwrap_or(current)) {
if new_pos != current {
highlighted_for_input.set(new_pos);
}
}
});
}
render_select_list(&items, highlighted_signal, limit, &style)
}
}
fn render_select_list<T: Clone + 'static>(
items: &[SelectItem<T>],
highlighted_signal: Signal<usize>,
limit: Option<usize>,
style: &SelectInputStyle,
) -> Element {
let highlighted = highlighted_signal.get();
let total_items = items.len();
let (start, end) = calculate_visible_range(highlighted, total_items, limit);
let mut container = TinkBox::new().flex_direction(FlexDirection::Column);
for (idx, item) in items.iter().enumerate().skip(start).take(end - start) {
let is_highlighted = idx == highlighted;
let prefix = if is_highlighted {
&style.indicator
} else {
&style.indicator_padding
};
let label = format!("{}{}", prefix, item.label);
let mut text = Text::new(&label);
if is_highlighted {
if let Some(color) = style.highlight_color {
text = text.color(color);
}
if let Some(bg) = style.highlight_bg {
text = text.background(bg);
}
if style.highlight_bold {
text = text.bold();
}
} else if let Some(color) = style.item_color {
text = text.color(color);
}
container = container.child(text.into_element());
}
container.into_element()
}
pub fn select_input<T: Clone + ToString + 'static>(items: Vec<T>) -> SelectInput<T> {
let select_items: Vec<SelectItem<T>> = items
.into_iter()
.map(|item| SelectItem::new(item.to_string(), item))
.collect();
SelectInput::new(select_items)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_select_item_creation() {
let item = SelectItem::new("Test", 42);
assert_eq!(item.label, "Test");
assert_eq!(item.value, 42);
}
#[test]
fn test_select_input_creation() {
let items = vec![
SelectItem::new("One", 1),
SelectItem::new("Two", 2),
SelectItem::new("Three", 3),
];
let select = SelectInput::new(items);
assert_eq!(select.len(), 3);
assert!(!select.is_empty());
}
#[test]
fn test_select_input_empty() {
let select: SelectInput<i32> = SelectInput::new(vec![]);
assert!(select.is_empty());
assert_eq!(select.len(), 0);
}
#[test]
fn test_select_input_highlighted() {
let items = vec![
SelectItem::new("One", 1),
SelectItem::new("Two", 2),
SelectItem::new("Three", 3),
];
let select = SelectInput::new(items).highlighted(1);
assert_eq!(select.highlighted, 1);
}
#[test]
fn test_select_input_highlighted_bounds() {
let items = vec![SelectItem::new("One", 1), SelectItem::new("Two", 2)];
let select = SelectInput::new(items).highlighted(10);
assert_eq!(select.highlighted, 1); }
#[test]
fn test_select_input_style() {
let style = SelectInputStyle::new()
.highlight_color(Color::Green)
.indicator("> ");
assert_eq!(style.highlight_color, Some(Color::Green));
assert_eq!(style.indicator, "> ");
assert_eq!(style.indicator_padding, " ");
}
#[test]
fn test_select_input_limit() {
let items = vec![
SelectItem::new("One", 1),
SelectItem::new("Two", 2),
SelectItem::new("Three", 3),
SelectItem::new("Four", 4),
SelectItem::new("Five", 5),
];
let select = SelectInput::new(items).limit(3);
assert_eq!(select.limit, Some(3));
}
#[test]
fn test_select_input_builder_chain() {
let items = vec![SelectItem::new("Test", 1)];
let select = SelectInput::new(items)
.highlighted(0)
.limit(5)
.focused(true)
.vim_navigation(true)
.number_shortcuts(false)
.highlight_color(Color::Yellow)
.indicator("→ ");
assert_eq!(select.highlighted, 0);
assert_eq!(select.limit, Some(5));
assert!(select.is_focused);
assert!(select.vim_navigation);
assert!(!select.number_shortcuts);
}
#[test]
fn test_select_input_from_items() {
let items = vec![SelectItem::new("A", 'a'), SelectItem::new("B", 'b')];
let select = SelectInput::from_items(items);
assert_eq!(select.len(), 2);
}
}