bubbletea_widgets/list/
defaultitem.rs1use super::{Item, ItemDelegate, Model};
38use bubbletea_rs::{Cmd, Msg};
39use lipgloss::{self, style::Style, Color};
40
41fn apply_character_highlighting(
55 text: &str,
56 matches: &[usize],
57 highlight_style: &Style,
58 normal_style: &Style,
59) -> String {
60 if matches.is_empty() {
61 return normal_style.render(text);
62 }
63
64 let chars: Vec<char> = text.chars().collect();
65 let mut result = String::new();
66 let mut current_pos = 0;
67
68 let mut sorted_matches = matches.to_vec();
70 sorted_matches.sort_unstable();
71 sorted_matches.dedup();
72
73 for &match_idx in &sorted_matches {
74 if match_idx >= chars.len() {
75 continue;
76 }
77
78 if current_pos < match_idx {
80 let segment: String = chars[current_pos..match_idx].iter().collect();
81 if !segment.is_empty() {
82 result.push_str(&normal_style.render(&segment));
83 }
84 }
85
86 let highlighted_char = chars[match_idx].to_string();
88 result.push_str(&highlight_style.render(&highlighted_char));
89
90 current_pos = match_idx + 1;
91 }
92
93 if current_pos < chars.len() {
95 let remaining: String = chars[current_pos..].iter().collect();
96 if !remaining.is_empty() {
97 result.push_str(&normal_style.render(&remaining));
98 }
99 }
100
101 result
102}
103
104#[derive(Debug, Clone)]
106pub struct DefaultItemStyles {
107 pub normal_title: Style,
109 pub normal_desc: Style,
111 pub selected_title: Style,
113 pub selected_desc: Style,
115 pub dimmed_title: Style,
117 pub dimmed_desc: Style,
119 pub filter_match: Style,
121}
122
123impl Default for DefaultItemStyles {
124 fn default() -> Self {
125 let normal_title = Style::new()
126 .foreground(Color::from("#dddddd"))
127 .padding(0, 0, 0, 2);
128 let normal_desc = normal_title.clone().foreground(Color::from("#777777"));
129 let selected_title = Style::new()
130 .border_style(lipgloss::normal_border())
131 .border_left(true)
132 .border_left_foreground(Color::from("#AD58B4"))
133 .foreground(Color::from("#EE6FF8"))
134 .padding(0, 0, 0, 1);
135 let selected_desc = selected_title.clone().foreground(Color::from("#AD58B4"));
136 let dimmed_title = Style::new()
137 .foreground(Color::from("#777777"))
138 .padding(0, 0, 0, 2);
139 let dimmed_desc = dimmed_title.clone().foreground(Color::from("#4D4D4D"));
140 let filter_match = Style::new().underline(true);
141 Self {
142 normal_title,
143 normal_desc,
144 selected_title,
145 selected_desc,
146 dimmed_title,
147 dimmed_desc,
148 filter_match,
149 }
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct DefaultItem {
156 pub title: String,
158 pub desc: String,
160}
161
162impl DefaultItem {
163 pub fn new(title: &str, desc: &str) -> Self {
165 Self {
166 title: title.to_string(),
167 desc: desc.to_string(),
168 }
169 }
170}
171
172impl std::fmt::Display for DefaultItem {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 write!(f, "{}", self.title)
175 }
176}
177
178impl Item for DefaultItem {
179 fn filter_value(&self) -> String {
180 self.title.clone()
181 }
182}
183
184#[derive(Debug, Clone)]
186pub struct DefaultDelegate {
187 pub show_description: bool,
189 pub styles: DefaultItemStyles,
191 height: usize,
192 spacing: usize,
193}
194
195impl Default for DefaultDelegate {
196 fn default() -> Self {
197 Self {
198 show_description: true,
199 styles: Default::default(),
200 height: 2,
201 spacing: 1,
202 }
203 }
204}
205impl DefaultDelegate {
206 pub fn new() -> Self {
208 Self::default()
209 }
210}
211
212impl<I: Item + 'static> ItemDelegate<I> for DefaultDelegate {
213 fn render(&self, m: &Model<I>, index: usize, item: &I) -> String {
214 let title = item.to_string();
215 let desc = if let Some(di) = (item as &dyn std::any::Any).downcast_ref::<DefaultItem>() {
216 di.desc.clone()
217 } else {
218 String::new()
219 };
220
221 if m.width == 0 {
222 return String::new();
223 }
224
225 let s = &self.styles;
226 let is_selected = index == m.cursor;
227 let empty_filter =
228 m.filter_state == super::FilterState::Filtering && m.filter_input.value().is_empty();
229 let is_filtered = matches!(
230 m.filter_state,
231 super::FilterState::Filtering | super::FilterState::FilterApplied
232 );
233
234 let matches = if is_filtered && index < m.filtered_items.len() {
236 Some(&m.filtered_items[index].matches)
237 } else {
238 None
239 };
240
241 let mut title_out = title.clone();
242 let mut desc_out = desc.clone();
243
244 if empty_filter {
245 title_out = s.dimmed_title.clone().render(&title_out);
246 desc_out = s.dimmed_desc.clone().render(&desc_out);
247 } else if is_selected && m.filter_state != super::FilterState::Filtering {
248 if let Some(match_indices) = matches {
250 let highlight_style = s.selected_title.clone().inherit(s.filter_match.clone());
251 title_out = apply_character_highlighting(
252 &title,
253 match_indices,
254 &highlight_style,
255 &s.selected_title,
256 );
257 if !desc.is_empty() {
258 let desc_highlight_style =
259 s.selected_desc.clone().inherit(s.filter_match.clone());
260 desc_out = apply_character_highlighting(
261 &desc,
262 match_indices,
263 &desc_highlight_style,
264 &s.selected_desc,
265 );
266 }
267 } else {
268 title_out = s.selected_title.clone().render(&title_out);
269 desc_out = s.selected_desc.clone().render(&desc_out);
270 }
271 } else {
272 if let Some(match_indices) = matches {
274 let highlight_style = s.normal_title.clone().inherit(s.filter_match.clone());
275 title_out = apply_character_highlighting(
276 &title,
277 match_indices,
278 &highlight_style,
279 &s.normal_title,
280 );
281 if !desc.is_empty() {
282 let desc_highlight_style =
283 s.normal_desc.clone().inherit(s.filter_match.clone());
284 desc_out = apply_character_highlighting(
285 &desc,
286 match_indices,
287 &desc_highlight_style,
288 &s.normal_desc,
289 );
290 }
291 } else {
292 title_out = s.normal_title.clone().render(&title_out);
293 desc_out = s.normal_desc.clone().render(&desc_out);
294 }
295 }
296
297 if self.show_description && !desc_out.is_empty() {
298 format!("{}\n{}", title_out, desc_out)
299 } else {
300 title_out
301 }
302 }
303 fn height(&self) -> usize {
304 if self.show_description {
305 self.height
306 } else {
307 1
308 }
309 }
310 fn spacing(&self) -> usize {
311 self.spacing
312 }
313 fn update(&self, _msg: &Msg, _m: &mut Model<I>) -> Option<Cmd> {
314 None
315 }
316}