use super::{Item, ItemDelegate, Model};
use bubbletea_rs::{Cmd, Msg};
use lipgloss_extras::prelude::*;
pub(super) fn apply_character_highlighting(
text: &str,
matches: &[usize],
highlight_style: &Style,
normal_style: &Style,
) -> String {
if matches.is_empty() {
return normal_style.render(text);
}
let chars: Vec<char> = text.chars().collect();
let mut result = String::new();
let mut sorted_matches = matches.to_vec();
sorted_matches.sort_unstable();
sorted_matches.dedup();
let valid_matches: Vec<usize> = sorted_matches
.into_iter()
.filter(|&idx| idx < chars.len())
.collect();
if valid_matches.is_empty() {
return normal_style.render(text);
}
let mut segments: Vec<(usize, usize, bool)> = Vec::new(); let mut current_pos = 0;
let mut i = 0;
while i < valid_matches.len() {
let match_start = valid_matches[i];
if current_pos < match_start {
segments.push((current_pos, match_start, false));
}
let mut match_end = match_start + 1;
while i + 1 < valid_matches.len() && valid_matches[i + 1] == valid_matches[i] + 1 {
i += 1;
match_end = valid_matches[i] + 1;
}
segments.push((match_start, match_end, true));
current_pos = match_end;
i += 1;
}
if current_pos < chars.len() {
segments.push((current_pos, chars.len(), false));
}
for (start, end, is_highlighted) in segments {
let segment: String = chars[start..end].iter().collect();
if !segment.is_empty() {
if is_highlighted {
result.push_str(&highlight_style.render(&segment));
} else {
result.push_str(&normal_style.render(&segment));
}
}
}
result
}
#[derive(Debug, Clone)]
pub struct DefaultItemStyles {
pub normal_title: Style,
pub normal_desc: Style,
pub selected_title: Style,
pub selected_desc: Style,
pub dimmed_title: Style,
pub dimmed_desc: Style,
pub filter_match: Style,
}
impl Default for DefaultItemStyles {
fn default() -> Self {
let normal_title = Style::new()
.foreground(AdaptiveColor {
Light: "#1a1a1a",
Dark: "#dddddd",
})
.padding(0, 0, 0, 2);
let normal_desc = Style::new()
.foreground(AdaptiveColor {
Light: "#A49FA5",
Dark: "#777777",
})
.padding(0, 0, 0, 2);
let selected_title = Style::new()
.border_style(normal_border())
.border_top(false)
.border_right(false)
.border_bottom(false)
.border_left(true)
.border_left_foreground(AdaptiveColor {
Light: "#F793FF",
Dark: "#AD58B4",
})
.foreground(AdaptiveColor {
Light: "#EE6FF8",
Dark: "#EE6FF8",
})
.padding(0, 0, 0, 1);
let selected_desc = selected_title.clone().foreground(AdaptiveColor {
Light: "#F793FF",
Dark: "#AD58B4",
});
let dimmed_title = Style::new()
.foreground(AdaptiveColor {
Light: "#A49FA5",
Dark: "#777777",
})
.padding(0, 0, 0, 2);
let dimmed_desc = Style::new()
.foreground(AdaptiveColor {
Light: "#C2B8C2",
Dark: "#4D4D4D",
})
.padding(0, 0, 0, 2);
let filter_match = Style::new().underline(true);
Self {
normal_title,
normal_desc,
selected_title,
selected_desc,
dimmed_title,
dimmed_desc,
filter_match,
}
}
}
#[derive(Debug, Clone)]
pub struct DefaultItem {
pub title: String,
pub desc: String,
}
impl DefaultItem {
pub fn new(title: &str, desc: &str) -> Self {
Self {
title: title.to_string(),
desc: desc.to_string(),
}
}
}
impl std::fmt::Display for DefaultItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.title)
}
}
impl Item for DefaultItem {
fn filter_value(&self) -> String {
self.title.clone()
}
}
#[derive(Debug, Clone)]
pub struct DefaultDelegate {
pub show_description: bool,
pub styles: DefaultItemStyles,
height: usize,
spacing: usize,
}
impl Default for DefaultDelegate {
fn default() -> Self {
Self {
show_description: true,
styles: Default::default(),
height: 2,
spacing: 1,
}
}
}
impl DefaultDelegate {
pub fn new() -> Self {
Self::default()
}
}
impl<I: Item + 'static> ItemDelegate<I> for DefaultDelegate {
fn render(&self, m: &Model<I>, index: usize, item: &I) -> String {
let title = item.to_string();
let desc = if let Some(di) = (item as &dyn std::any::Any).downcast_ref::<DefaultItem>() {
di.desc.clone()
} else {
String::new()
};
if m.width == 0 {
return String::new();
}
let s = &self.styles;
let is_selected = index == m.cursor;
let empty_filter =
m.filter_state == super::FilterState::Filtering && m.filter_input.value().is_empty();
let is_filtered = matches!(
m.filter_state,
super::FilterState::Filtering | super::FilterState::FilterApplied
);
let matches = if is_filtered {
m.filtered_items
.iter()
.find(|fi| fi.index == index) .map(|fi| &fi.matches) } else {
None
};
let mut title_out = title.clone();
let mut desc_out = desc.clone();
if empty_filter {
title_out = s.dimmed_title.render(&title_out);
desc_out = s.dimmed_desc.render(&desc_out);
} else if is_selected && m.filter_state != super::FilterState::Filtering {
if let Some(match_indices) = matches {
let selected_base_style = Style::new().foreground(AdaptiveColor {
Light: "#EE6FF8", Dark: "#EE6FF8",
});
let selected_desc_base_style = Style::new().foreground(AdaptiveColor {
Light: "#F793FF", Dark: "#AD58B4",
});
let highlight_style = selected_base_style.clone().inherit(s.filter_match.clone());
title_out = apply_character_highlighting(
&title,
match_indices,
&highlight_style, &selected_base_style, );
if !desc.is_empty() {
let desc_highlight_style = selected_desc_base_style
.clone()
.inherit(s.filter_match.clone());
desc_out = apply_character_highlighting(
&desc,
match_indices,
&desc_highlight_style,
&selected_desc_base_style,
);
}
let border_char = "│";
let padding = " "; title_out = format!(
"{}{}{}",
Style::new()
.foreground(AdaptiveColor {
Light: "#F793FF", Dark: "#AD58B4",
})
.render(border_char), padding, title_out );
if !desc.is_empty() {
desc_out = format!(
"{}{}{}",
Style::new()
.foreground(AdaptiveColor {
Light: "#F793FF", Dark: "#AD58B4",
})
.render(border_char),
padding,
desc_out
);
}
} else {
title_out = s.selected_title.render(&title_out);
desc_out = s.selected_desc.render(&desc_out);
}
} else {
if let Some(match_indices) = matches {
let normal_base_style = Style::new().foreground(AdaptiveColor {
Light: "#1a1a1a", Dark: "#dddddd",
});
let normal_desc_base_style = Style::new().foreground(AdaptiveColor {
Light: "#A49FA5", Dark: "#777777",
});
let highlight_style = normal_base_style.clone().inherit(s.filter_match.clone());
title_out = apply_character_highlighting(
&title,
match_indices,
&highlight_style, &normal_base_style, );
if !desc.is_empty() {
let desc_highlight_style = normal_desc_base_style
.clone()
.inherit(s.filter_match.clone());
desc_out = apply_character_highlighting(
&desc,
match_indices,
&desc_highlight_style,
&normal_desc_base_style,
);
}
let padding = " "; title_out = format!("{}{}", padding, title_out);
if !desc.is_empty() {
desc_out = format!("{}{}", padding, desc_out);
}
} else {
title_out = s.normal_title.render(&title_out);
desc_out = s.normal_desc.render(&desc_out);
}
}
if self.show_description && !desc_out.is_empty() {
format!("{}\n{}", title_out, desc_out)
} else {
title_out
}
}
fn height(&self) -> usize {
if self.show_description {
self.height
} else {
1
}
}
fn spacing(&self) -> usize {
self.spacing
}
fn update(&self, _msg: &Msg, _m: &mut Model<I>) -> Option<Cmd> {
None
}
}