use crate::style::Color;
use crate::widget::theme::{DARK_GRAY, DISABLED_FG, PLACEHOLDER_FG};
use crate::widget::traits::WidgetProps;
use crate::widget::{RenderContext, View};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Debug)]
pub struct SelectionItem {
pub text: String,
pub value: Option<String>,
pub disabled: bool,
pub description: Option<String>,
pub icon: Option<String>,
}
impl SelectionItem {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
value: None,
disabled: false,
description: None,
icon: None,
}
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
}
impl<S: Into<String>> From<S> for SelectionItem {
fn from(s: S) -> Self {
Self::new(s)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SelectionStyle {
#[default]
Checkbox,
Bullet,
Highlight,
Bracket,
}
#[derive(Clone, Debug)]
pub struct SelectionList {
items: Vec<SelectionItem>,
selected: Vec<usize>,
highlighted: usize,
style: SelectionStyle,
max_selections: usize,
min_selections: usize,
show_descriptions: bool,
title: Option<String>,
fg: Option<Color>,
selected_fg: Option<Color>,
highlighted_fg: Option<Color>,
bg: Option<Color>,
max_visible: usize,
scroll_offset: usize,
show_count: bool,
focused: bool,
props: WidgetProps,
}
impl SelectionList {
pub fn new<I, T>(items: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<SelectionItem>,
{
Self {
items: items.into_iter().map(|i| i.into()).collect(),
selected: Vec::new(),
highlighted: 0,
style: SelectionStyle::default(),
max_selections: 0,
min_selections: 0,
show_descriptions: false,
title: None,
fg: None,
selected_fg: None,
highlighted_fg: None,
bg: None,
max_visible: 0,
scroll_offset: 0,
show_count: false,
focused: false,
props: WidgetProps::new(),
}
}
pub fn selected(mut self, indices: Vec<usize>) -> Self {
self.selected = indices;
self
}
pub fn style(mut self, style: SelectionStyle) -> Self {
self.style = style;
self
}
pub fn show_checkboxes(self, show: bool) -> Self {
if show {
self.style(SelectionStyle::Checkbox)
} else {
self.style(SelectionStyle::Highlight)
}
}
pub fn max_selections(mut self, max: usize) -> Self {
self.max_selections = max;
self
}
pub fn min_selections(mut self, min: usize) -> Self {
self.min_selections = min;
self
}
pub fn show_descriptions(mut self, show: bool) -> Self {
self.show_descriptions = show;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn selected_fg(mut self, color: Color) -> Self {
self.selected_fg = Some(color);
self
}
pub fn highlighted_fg(mut self, color: Color) -> Self {
self.highlighted_fg = Some(color);
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
pub fn max_visible(mut self, max: usize) -> Self {
self.max_visible = max;
self
}
pub fn show_count(mut self, show: bool) -> Self {
self.show_count = show;
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn get_selected(&self) -> &[usize] {
&self.selected
}
pub fn get_selected_values(&self) -> Vec<&str> {
self.selected
.iter()
.filter_map(|&i| {
self.items
.get(i)
.map(|item| item.value.as_deref().unwrap_or(&item.text))
})
.collect()
}
pub fn get_selected_items(&self) -> Vec<&SelectionItem> {
self.selected
.iter()
.filter_map(|&i| self.items.get(i))
.collect()
}
pub fn is_selected(&self, index: usize) -> bool {
self.selected.contains(&index)
}
pub fn toggle(&mut self, index: usize) {
if index >= self.items.len() {
return;
}
if self.items[index].disabled {
return;
}
if self.is_selected(index) {
if self.selected.len() > self.min_selections {
self.selected.retain(|&i| i != index);
}
} else {
if self.max_selections == 0 || self.selected.len() < self.max_selections {
self.selected.push(index);
self.selected.sort();
}
}
}
pub fn toggle_highlighted(&mut self) {
self.toggle(self.highlighted);
}
pub fn select(&mut self, index: usize) {
if index >= self.items.len() || self.items[index].disabled {
return;
}
if !self.is_selected(index)
&& (self.max_selections == 0 || self.selected.len() < self.max_selections)
{
self.selected.push(index);
self.selected.sort();
}
}
pub fn deselect(&mut self, index: usize) {
if self.selected.len() > self.min_selections {
self.selected.retain(|&i| i != index);
}
}
pub fn select_all(&mut self) {
self.selected = (0..self.items.len())
.filter(|&i| !self.items[i].disabled)
.collect();
if self.max_selections > 0 {
self.selected.truncate(self.max_selections);
}
}
pub fn deselect_all(&mut self) {
if self.min_selections == 0 {
self.selected.clear();
} else {
self.selected.truncate(self.min_selections);
}
}
pub fn highlight_previous(&mut self) {
if self.highlighted > 0 {
self.highlighted -= 1;
self.ensure_visible();
}
}
pub fn highlight_next(&mut self) {
if self.highlighted < self.items.len().saturating_sub(1) {
self.highlighted += 1;
self.ensure_visible();
}
}
pub fn highlight_first(&mut self) {
self.highlighted = 0;
self.scroll_offset = 0;
}
pub fn highlight_last(&mut self) {
self.highlighted = self.items.len().saturating_sub(1);
self.ensure_visible();
}
fn ensure_visible(&mut self) {
let max_visible = if self.max_visible > 0 {
self.max_visible
} else {
self.items.len()
};
if self.highlighted < self.scroll_offset {
self.scroll_offset = self.highlighted;
} else if self.highlighted >= self.scroll_offset + max_visible {
self.scroll_offset = self.highlighted - max_visible + 1;
}
}
fn item_prefix(&self, index: usize) -> String {
let is_selected = self.is_selected(index);
let is_disabled = self.items[index].disabled;
match self.style {
SelectionStyle::Checkbox => {
if is_disabled {
"[-] ".to_string()
} else if is_selected {
"[x] ".to_string()
} else {
"[ ] ".to_string()
}
}
SelectionStyle::Bullet => {
if is_disabled {
"◌ ".to_string()
} else if is_selected {
"● ".to_string()
} else {
"○ ".to_string()
}
}
SelectionStyle::Highlight => if is_selected { "▸ " } else { " " }.to_string(),
SelectionStyle::Bracket => {
if is_selected {
"[".to_string()
} else {
" ".to_string()
}
}
}
}
fn item_suffix(&self, index: usize) -> String {
if self.style == SelectionStyle::Bracket && self.is_selected(index) {
"]".to_string()
} else {
String::new()
}
}
}
impl View for SelectionList {
fn render(&self, ctx: &mut RenderContext) {
use crate::widget::stack::vstack;
use crate::widget::Text;
let mut content = vstack();
if let Some(title) = &self.title {
content = content.child(Text::new(title).bold());
}
if self.show_count {
let count_text = if self.max_selections > 0 {
format!("Selected: {}/{}", self.selected.len(), self.max_selections)
} else {
format!("Selected: {}", self.selected.len())
};
content = content.child(Text::new(count_text).fg(PLACEHOLDER_FG));
}
let max_visible = if self.max_visible > 0 {
self.max_visible
} else {
self.items.len()
};
let start = self.scroll_offset;
let end = (start + max_visible).min(self.items.len());
if start > 0 {
content = content.child(Text::new(" ↑ more...").fg(DISABLED_FG));
}
for i in start..end {
let item = &self.items[i];
let prefix = self.item_prefix(i);
let suffix = self.item_suffix(i);
let icon = item.icon.as_deref().unwrap_or("");
let text = format!("{}{}{}{}", prefix, icon, item.text, suffix);
let is_highlighted = i == self.highlighted && self.focused;
let is_selected = self.is_selected(i);
let fg = if item.disabled {
DISABLED_FG
} else if is_highlighted {
self.highlighted_fg.unwrap_or(Color::CYAN)
} else if is_selected {
self.selected_fg.unwrap_or(Color::GREEN)
} else {
self.fg.unwrap_or(Color::WHITE)
};
let mut text_widget = Text::new(&text).fg(fg);
if is_highlighted {
text_widget = text_widget.bold();
}
content = content.child(text_widget);
if self.show_descriptions {
if let Some(desc) = &item.description {
let desc_text = format!(" {}", desc);
content = content.child(Text::new(desc_text).fg(PLACEHOLDER_FG));
}
}
}
if end < self.items.len() {
content = content.child(Text::new(" ↓ more...").fg(DISABLED_FG));
}
if self.focused {
content = content
.child(Text::new("↑↓: Navigate | Space: Toggle | a: All | n: None").fg(DARK_GRAY));
}
content.render(ctx);
}
crate::impl_view_meta!("SelectionList");
}
impl_styled_view!(SelectionList);
impl_props_builders!(SelectionList);
pub fn selection_list<I, T>(items: I) -> SelectionList
where
I: IntoIterator<Item = T>,
T: Into<SelectionItem>,
{
SelectionList::new(items)
}
pub fn selection_item(text: impl Into<String>) -> SelectionItem {
SelectionItem::new(text)
}