feat: add combobox filter

This commit is contained in:
2026-11-06 21:27:31 +07:00
parent 265d2d129b
commit 0ad5c3fe90
8 changed files with 256 additions and 65 deletions
+32 -10
View File
@@ -12,8 +12,8 @@ use gpui::prelude::FluentBuilder as _;
use gpui::{
App, Bounds, Context, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
KeyBinding, MouseButton, MouseDownEvent, ParentElement as _, Pixels, Render, Role,
ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription, Window,
actions, canvas, deferred, div, px,
ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription, Task,
Window, actions, canvas, deferred, div, px,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, Size};
@@ -59,12 +59,12 @@ pub enum UnmatchedText {
/// An option for a [`Combobox`]: a value and the label the filter matches on.
pub type ComboboxOption<T> = (T, SharedString);
/// Reports the value whenever it changes — including when typing clears it.
/// Reports the value whenever it changes;
pub type ComboboxHandler<T> = Rc<dyn Fn(Option<T>, &mut Window, &mut App)>;
/// Receives unmatched text when the field loses focus in create mode.
pub type ComboboxCreateHandler = Rc<dyn Fn(SharedString, &mut Window, &mut App)>;
/// A filter over an option: the query, the value, the label.
type Filter<T> = Rc<dyn Fn(&str, &T, &SharedString) -> bool>;
/// Decides which options survive a query: the query, the value, the label.
pub type ComboboxFilter<T> = Rc<dyn Fn(&str, &T, &SharedString) -> bool>;
/// Builder for a combobox. Pass it to [`ComboboxState::new`].
pub struct Combobox<T>
@@ -78,6 +78,7 @@ where
placeholder: SharedString,
on_change: Option<ComboboxHandler<T>>,
on_create: Option<ComboboxCreateHandler>,
filter: Option<ComboboxFilter<T>>,
unmatched: UnmatchedText,
size: Size,
}
@@ -99,6 +100,7 @@ where
placeholder: SharedString::from("Select..."),
on_change: None,
on_create: None,
filter: None,
unmatched: UnmatchedText::default(),
size: Size::default(),
}
@@ -138,6 +140,15 @@ where
self
}
/// Sets the matcher that decides which options survive a query.
pub fn filter<F>(mut self, filter: F) -> Self
where
F: Fn(&str, &T, &SharedString) -> bool + 'static,
{
self.filter = Some(Rc::new(filter));
self
}
/// Leaves unmatched text in the field on blur.
pub fn keep_unmatched_text(mut self) -> Self {
self.unmatched = UnmatchedText::Keep;
@@ -175,9 +186,11 @@ where
highlight: Option<usize>,
on_change: Option<ComboboxHandler<T>>,
on_create: Option<ComboboxCreateHandler>,
filter: Filter<T>,
filter: ComboboxFilter<T>,
unmatched: UnmatchedText,
size: Size,
/// Handler reports in flight; held so they die with the state.
tasks: Vec<Task<()>>,
/// Keeps the query-event subscription tied to the state's lifetime.
_subscription: Subscription,
}
@@ -230,11 +243,12 @@ where
highlight: None,
on_change: combobox.on_change,
on_create: combobox.on_create,
filter: Rc::new(|query, _value, label| {
label.to_lowercase().contains(&query.to_lowercase())
filter: combobox.filter.unwrap_or_else(|| {
Rc::new(|query, _value, label| label.to_lowercase().contains(&query.to_lowercase()))
}),
unmatched: combobox.unmatched,
size: combobox.size,
tasks: Vec::new(),
_subscription: subscription,
}
}
@@ -313,7 +327,11 @@ where
}
self.selected = value.clone();
if let Some(on_change) = self.on_change.clone() {
on_change(value, window, cx);
let value = value.clone();
let task = window.spawn(cx, async move |cx| {
cx.update(|window, cx| on_change(value, window, cx)).ok();
});
self.tasks.push(task);
}
cx.emit(ComboboxChanged);
cx.notify();
@@ -367,7 +385,11 @@ where
if !text.is_empty()
&& let Some(on_create) = self.on_create.clone()
{
on_create(SharedString::from(text), window, cx);
let text = SharedString::from(text);
let task = window.spawn(cx, async move |cx| {
cx.update(|window, cx| on_create(text, window, cx)).ok();
});
self.tasks.push(task);
}
}
}