bubbletea_widgets/list/
defaultitem.rs

1//! Default item implementation and delegate for list components.
2//!
3//! This module provides the standard item type and delegate implementation for the list component.
4//! The `DefaultItem` is a simple item with a title and description, while `DefaultDelegate` handles
5//! the rendering and interaction logic for these items.
6//!
7//! ## Default Item Structure
8//!
9//! The `DefaultItem` represents a basic list item with:
10//! - A title (main text)
11//! - A description (secondary text, optional display)
12//!
13//! ## Default Delegate
14//!
15//! The `DefaultDelegate` handles:
16//! - Rendering items with different visual states (normal, selected, dimmed)
17//! - Managing item height and spacing
18//! - Filtering and match highlighting (when implemented)
19//!
20//! ## Styling
21//!
22//! The `DefaultItemStyles` provides comprehensive styling options:
23//! - Normal state styles for title and description
24//! - Selected state styles with borders and highlighting
25//! - Dimmed state styles for filtered-out items
26//! - Filter match highlighting styles
27//!
28//! ## Example
29//!
30//! ```rust
31//! use bubbletea_widgets::list::{DefaultItem, DefaultDelegate};
32//!
33//! let item = DefaultItem::new("Task 1", "Complete the documentation");
34//! let delegate = DefaultDelegate::new();
35//! ```
36
37use super::{Item, ItemDelegate, Model};
38use bubbletea_rs::{Cmd, Msg};
39use lipgloss::{self, style::Style, Color};
40
41/// Styling for the default list item in various states.
42#[derive(Debug, Clone)]
43pub struct DefaultItemStyles {
44    /// Title style in normal (unselected) state.
45    pub normal_title: Style,
46    /// Description style in normal (unselected) state.
47    pub normal_desc: Style,
48    /// Title style when the item is selected.
49    pub selected_title: Style,
50    /// Description style when the item is selected.
51    pub selected_desc: Style,
52    /// Title style when the item is dimmed (e.g., during filtering).
53    pub dimmed_title: Style,
54    /// Description style when the item is dimmed.
55    pub dimmed_desc: Style,
56    /// Style used to highlight filter matches.
57    pub filter_match: Style,
58}
59
60impl Default for DefaultItemStyles {
61    fn default() -> Self {
62        let normal_title = Style::new()
63            .foreground(Color::from("#dddddd"))
64            .padding(0, 0, 0, 2);
65        let normal_desc = normal_title.clone().foreground(Color::from("#777777"));
66        let selected_title = Style::new()
67            .border_style(lipgloss::normal_border())
68            .border_left(true)
69            .border_left_foreground(Color::from("#AD58B4"))
70            .foreground(Color::from("#EE6FF8"))
71            .padding(0, 0, 0, 1);
72        let selected_desc = selected_title.clone().foreground(Color::from("#AD58B4"));
73        let dimmed_title = Style::new()
74            .foreground(Color::from("#777777"))
75            .padding(0, 0, 0, 2);
76        let dimmed_desc = dimmed_title.clone().foreground(Color::from("#4D4D4D"));
77        let filter_match = Style::new().underline(true);
78        Self {
79            normal_title,
80            normal_desc,
81            selected_title,
82            selected_desc,
83            dimmed_title,
84            dimmed_desc,
85            filter_match,
86        }
87    }
88}
89
90/// Simple item with a title and optional description.
91#[derive(Debug, Clone)]
92pub struct DefaultItem {
93    /// Main item text.
94    pub title: String,
95    /// Secondary item text (optional display).
96    pub desc: String,
97}
98
99impl DefaultItem {
100    /// Creates a new default item with title and description.
101    pub fn new(title: &str, desc: &str) -> Self {
102        Self {
103            title: title.to_string(),
104            desc: desc.to_string(),
105        }
106    }
107}
108
109impl std::fmt::Display for DefaultItem {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        write!(f, "{}", self.title)
112    }
113}
114
115impl Item for DefaultItem {
116    fn filter_value(&self) -> String {
117        self.title.clone()
118    }
119}
120
121/// Delegate that renders `DefaultItem` instances.
122#[derive(Debug, Clone)]
123pub struct DefaultDelegate {
124    /// Whether to show the description beneath the title.
125    pub show_description: bool,
126    /// Styling used for different visual states.
127    pub styles: DefaultItemStyles,
128    height: usize,
129    spacing: usize,
130}
131
132impl Default for DefaultDelegate {
133    fn default() -> Self {
134        Self {
135            show_description: true,
136            styles: Default::default(),
137            height: 2,
138            spacing: 1,
139        }
140    }
141}
142impl DefaultDelegate {
143    /// Creates a new delegate with default styles and layout.
144    pub fn new() -> Self {
145        Self::default()
146    }
147}
148
149impl<I: Item + 'static> ItemDelegate<I> for DefaultDelegate {
150    fn render(&self, m: &Model<I>, index: usize, item: &I) -> String {
151        let title = item.to_string();
152        let desc = if let Some(di) = (item as &dyn std::any::Any).downcast_ref::<DefaultItem>() {
153            di.desc.clone()
154        } else {
155            String::new()
156        };
157
158        if m.width == 0 {
159            return String::new();
160        }
161
162        let s = &self.styles;
163        let is_selected = index == m.cursor;
164        let empty_filter =
165            m.filter_state == super::FilterState::Filtering && m.filter_input.value().is_empty();
166        let is_filtered = matches!(
167            m.filter_state,
168            super::FilterState::Filtering | super::FilterState::FilterApplied
169        );
170
171        let mut title_out = title.clone();
172        let mut desc_out = desc.clone();
173
174        if empty_filter {
175            title_out = s.dimmed_title.clone().render(&title_out);
176            desc_out = s.dimmed_desc.clone().render(&desc_out);
177        } else if is_selected && m.filter_state != super::FilterState::Filtering {
178            // Highlight matches if filtered
179            if is_filtered { /* TODO: apply rune-level match highlighting using stored matches */ }
180            title_out = s.selected_title.clone().render(&title_out);
181            desc_out = s.selected_desc.clone().render(&desc_out);
182        } else {
183            if is_filtered { /* TODO: apply match highlighting */ }
184            title_out = s.normal_title.clone().render(&title_out);
185            desc_out = s.normal_desc.clone().render(&desc_out);
186        }
187
188        if self.show_description && !desc_out.is_empty() {
189            format!("{}\n{}", title_out, desc_out)
190        } else {
191            title_out
192        }
193    }
194    fn height(&self) -> usize {
195        if self.show_description {
196            self.height
197        } else {
198            1
199        }
200    }
201    fn spacing(&self) -> usize {
202        self.spacing
203    }
204    fn update(&self, _msg: &Msg, _m: &mut Model<I>) -> Option<Cmd> {
205        None
206    }
207}