use std::sync::Arc;
use gpui_kit::{AnyElement, App, IntoElement as _, SharedString, Window};
use crate::Icon;
type LeadingBuilder = Arc<dyn Fn(&mut Window, &mut App) -> AnyElement + Send + Sync>;
#[derive(Clone)]
enum Leading {
Icon(Box<Icon>),
Element(LeadingBuilder),
}
pub trait SelectValue: Clone + PartialEq + 'static {
fn key(&self) -> SharedString;
}
impl SelectValue for SharedString {
fn key(&self) -> SharedString {
self.clone()
}
}
impl SelectValue for String {
fn key(&self) -> SharedString {
self.clone().into()
}
}
impl SelectValue for &'static str {
fn key(&self) -> SharedString {
(*self).into()
}
}
macro_rules! integer_values {
($($int:ty),*) => {
$(
impl SelectValue for $int {
fn key(&self) -> SharedString {
self.to_string().into()
}
}
)*
};
}
integer_values!(usize, u32, u64, i32, i64);
#[derive(Clone)]
pub struct SelectItem<V> {
value: V,
label: SharedString,
description: Option<SharedString>,
leading: Option<Leading>,
search_text: String,
disabled: bool,
}
impl<V: SelectValue> std::fmt::Debug for SelectItem<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SelectItem")
.field("key", &self.value.key())
.field("label", &self.label)
.field("description", &self.description)
.field("disabled", &self.disabled)
.finish_non_exhaustive()
}
}
impl<V: SelectValue> SelectItem<V> {
pub fn new(value: V, label: impl Into<SharedString>) -> Self {
let label = label.into();
Self {
value,
search_text: label.to_lowercase(),
label,
description: None,
leading: None,
disabled: false,
}
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.leading = Some(Leading::Icon(Box::new(icon.into())));
self
}
pub fn leading<E: gpui_kit::IntoElement>(
mut self,
build: impl Fn(&mut Window, &mut App) -> E + Send + Sync + 'static,
) -> Self {
self.leading = Some(Leading::Element(Arc::new(move |window, cx| {
build(window, cx).into_any_element()
})));
self
}
pub fn keywords(mut self, keywords: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
for keyword in keywords {
self.search_text.push('\n');
self.search_text.push_str(&keyword.into().to_lowercase());
}
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn value(&self) -> &V {
&self.value
}
pub fn key(&self) -> SharedString {
self.value.key()
}
pub fn label(&self) -> &SharedString {
&self.label
}
pub fn description_text(&self) -> Option<&SharedString> {
self.description.as_ref()
}
pub fn icon_source(&self) -> Option<&Icon> {
match &self.leading {
Some(Leading::Icon(icon)) => Some(icon),
_ => None,
}
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub(crate) fn render_leading(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
match self.leading.as_ref()? {
Leading::Icon(icon) => Some((**icon).clone().into_any_element()),
Leading::Element(build) => Some(build(window, cx)),
}
}
pub fn matches(&self, query: &str) -> bool {
let query = query.trim();
if query.is_empty() {
return true;
}
self.matches_lowercase(&query.to_lowercase())
}
fn matches_lowercase(&self, query: &str) -> bool {
query.is_empty() || self.search_text.contains(query)
}
}
#[derive(Clone, Debug)]
pub enum SelectEntry<V: SelectValue> {
Item(SelectItem<V>),
Label(SharedString),
Separator,
}
impl<V: SelectValue> SelectEntry<V> {
pub fn label(text: impl Into<SharedString>) -> Self {
Self::Label(text.into())
}
pub fn item(&self) -> Option<&SelectItem<V>> {
match self {
Self::Item(item) => Some(item),
_ => None,
}
}
}
impl<V: SelectValue> From<SelectItem<V>> for SelectEntry<V> {
fn from(item: SelectItem<V>) -> Self {
Self::Item(item)
}
}
pub(crate) fn visible_entries<V: SelectValue>(
entries: &[SelectEntry<V>],
query: &str,
) -> Vec<usize> {
let query = query.trim().to_lowercase();
let mut visible = Vec::with_capacity(entries.len());
let mut pending_label: Option<usize> = None;
let mut pending_separator: Option<usize> = None;
for (index, entry) in entries.iter().enumerate() {
match entry {
SelectEntry::Item(item) => {
if !item.matches_lowercase(&query) {
continue;
}
if let Some(separator) = pending_separator.take()
&& !visible.is_empty()
{
visible.push(separator);
}
if let Some(label) = pending_label.take() {
visible.push(label);
}
visible.push(index);
}
SelectEntry::Label(_) => pending_label = Some(index),
SelectEntry::Separator => {
pending_label = None;
pending_separator = Some(index);
}
}
}
visible
}
#[cfg(test)]
mod tests {
use super::*;
fn entries() -> Vec<SelectEntry<&'static str>> {
vec![
SelectEntry::label("Type"),
SelectItem::new("all", "All chats").into(),
SelectItem::new("local", "Local").into(),
SelectEntry::Separator,
SelectEntry::label("Sort by"),
SelectItem::new("updated", "Updated").into(),
SelectItem::new("created", "Created")
.keywords(["time"])
.into(),
]
}
#[test]
fn matching_ignores_case_and_reads_keywords() {
let item = SelectItem::new("de", "Deutsch").keywords(["German"]);
assert!(item.matches(""));
assert!(item.matches(" "));
assert!(item.matches("deu"));
assert!(item.matches("GERM"));
assert!(!item.matches("french"));
}
#[test]
fn an_empty_query_shows_every_row() {
assert_eq!(visible_entries(&entries(), ""), vec![0, 1, 2, 3, 4, 5, 6]);
}
#[test]
fn a_query_keeps_only_the_labels_and_separators_that_still_frame_an_item() {
let entries = entries();
assert_eq!(visible_entries(&entries, "local"), vec![0, 2]);
assert_eq!(visible_entries(&entries, "time"), vec![4, 6]);
assert_eq!(visible_entries(&entries, "ated"), vec![4, 5, 6]);
assert_eq!(visible_entries(&entries, "l"), vec![0, 1, 2]);
assert_eq!(
visible_entries(&entries, " LOC "),
vec![0, 2],
"case and space"
);
assert_eq!(visible_entries(&entries, "zzz"), Vec::<usize>::new());
}
#[test]
fn a_separator_never_leads_or_trails() {
let entries: Vec<SelectEntry<&'static str>> = vec![
SelectEntry::Separator,
SelectItem::new("a", "A").into(),
SelectEntry::Separator,
SelectEntry::Separator,
SelectItem::new("b", "B").into(),
SelectEntry::Separator,
];
assert_eq!(visible_entries(&entries, ""), vec![1, 3, 4]);
}
#[test]
fn rows_can_cross_threads() {
fn assert_send<T: Send>(_: &T) {}
let entries: Vec<SelectEntry<usize>> = vec![
SelectItem::new(1, "One")
.leading(|_, _| gpui_kit::div())
.into(),
SelectEntry::Separator,
];
assert_send(&entries);
}
#[test]
fn readers_read_back() {
let item = SelectItem::new("v", "Label")
.description("More")
.disabled(true);
assert_eq!(*item.value(), "v");
assert_eq!(item.key(), "v");
assert_eq!(item.label(), "Label");
assert_eq!(item.description_text().map(|d| d.as_ref()), Some("More"));
assert!(item.is_disabled());
assert!(item.icon_source().is_none());
assert!(SelectEntry::from(item.clone()).item().is_some());
assert!(SelectEntry::<&str>::Separator.item().is_none());
assert!(format!("{item:?}").contains("Label"));
assert_eq!(7usize.key(), "7");
assert_eq!(String::from("s").key(), "s");
}
}