Skip to main content

gpui_component/searchable_list/
delegate.rs

1use gpui::{AnyElement, App, IntoElement, SharedString, Task, Window};
2
3use crate::IndexPath;
4
5use super::change::SearchableListChange;
6
7/// An item that can appear in a searchable list (Select, ComboBox).
8pub trait SearchableListItem: Clone {
9    type Value: Clone + PartialEq;
10
11    /// Short display label shown in the dropdown row and in the trigger by default.
12    ///
13    /// This is also what assistive technology reads as the committed value, so
14    /// it has to stand on its own as text even when [`Self::display_title`]
15    /// draws something richer.
16    fn title(&self) -> SharedString;
17
18    /// Override the trigger display element (e.g. "Country (US)" instead of just "United States").
19    ///
20    /// Returns `None` to fall back to `title()`.
21    ///
22    /// This is presentation only. An element is not text, so the accessible
23    /// value keeps reporting [`Self::title`]; if the two would read
24    /// differently, put the meaning a listener needs in `title()`.
25    fn display_title(&self) -> Option<AnyElement> {
26        None
27    }
28
29    /// Render this item's row content inside the dropdown.
30    ///
31    /// Override to add icons, avatars, secondary text, etc.
32    /// The default renders `title()`.
33    fn render(&self, _: &mut Window, _: &mut App) -> impl IntoElement {
34        self.title()
35    }
36
37    /// The value that identifies this item.
38    fn value(&self) -> &Self::Value;
39
40    /// Whether this item matches the search query.
41    ///
42    /// Defaults to case-insensitive substring match on `title()`.
43    fn matches(&self, query: &str) -> bool {
44        self.title().to_lowercase().contains(&query.to_lowercase())
45    }
46
47    /// Whether this item should be shown as non-interactive (grayed-out, unclickable).
48    fn disabled(&self) -> bool {
49        false
50    }
51}
52
53/// Provides data and search behaviour to a searchable list component.
54pub trait SearchableListDelegate: Sized + 'static {
55    type Item: SearchableListItem;
56
57    /// Number of sections (groups) in the list.  Defaults to 1.
58    fn sections_count(&self, _: &App) -> usize {
59        1
60    }
61
62    /// Optional header element for the given section index.
63    ///
64    /// Deprecated: override [`render_section_header`] instead (provides `Window` + `App` access).
65    #[deprecated]
66    fn section(&self, _section: usize) -> Option<AnyElement> {
67        None
68    }
69
70    /// Number of items in the given section.
71    fn items_count(&self, section: usize) -> usize;
72
73    /// Return a reference to the item at the given index path.
74    fn item(&self, ix: IndexPath) -> Option<&Self::Item>;
75
76    /// Find the index path of the item whose value equals `value`.
77    fn position<V>(&self, _value: &V) -> Option<IndexPath>
78    where
79        Self::Item: SearchableListItem<Value = V>,
80        V: PartialEq;
81
82    /// Called when the search query changes.
83    ///
84    /// Implementations should filter or fetch items and may return an async `Task`.
85    /// The `App` context allows spawning background work.
86    fn perform_search(&mut self, _query: &str, _window: &mut Window, _cx: &mut App) -> Task<()> {
87        Task::ready(())
88    }
89
90    // MARK: Rendering hooks
91
92    /// Override the row content for the item at `ix`.
93    ///
94    /// When `Some(_)` is returned, the adapter suppresses its default `SearchableListItemElement`
95    /// layout (including the automatic trailing check icon) — the returned element is rendered
96    /// as-is. Return `None` to fall back to the standard rendering.
97    ///
98    /// `checked` is `true` when the item is in the current selection (as determined by
99    /// `is_item_checked`), letting custom renderers show their own selection indicator.
100    ///
101    /// Replaces the `item_renderer` closure that was previously set on `SearchableListAdapter`.
102    fn render_item(
103        &self,
104        _ix: IndexPath,
105        _item: &Self::Item,
106        _checked: bool,
107        _window: &mut Window,
108        _cx: &mut App,
109    ) -> Option<AnyElement> {
110        None
111    }
112
113    /// Render the header element for the given section (full render access).
114    ///
115    /// When `Some(_)` is returned, it is rendered directly — the adapter's default div wrapper
116    /// (padding, muted colour) is bypassed. Return `None` to fall back to the deprecated
117    /// `section()` wrapped in the standard div (no visual change for existing delegates).
118    fn render_section_header(
119        &self,
120        _section: usize,
121        _window: &mut Window,
122        _cx: &mut App,
123    ) -> Option<AnyElement> {
124        None
125    }
126
127    // MARK: Item state hooks
128
129    /// Whether the item at `ix` should be rendered as interactive.
130    ///
131    /// Default: `!item.disabled()`.
132    fn is_item_enabled(&self, _ix: IndexPath, item: &Self::Item, _cx: &App) -> bool {
133        !item.disabled()
134    }
135
136    /// Whether the item at `ix` should show a checkmark.
137    ///
138    /// `current_selection` is the slice of currently selected `(IndexPath, Item)` pairs.
139    ///
140    /// Default: checks whether the item's value is present in `current_selection`.
141    fn is_item_checked(
142        &self,
143        _ix: IndexPath,
144        item: &Self::Item,
145        current_selection: &[(IndexPath, Self::Item)],
146        _cx: &App,
147    ) -> bool {
148        current_selection
149            .iter()
150            .any(|(_, selected_item)| selected_item.value() == item.value())
151    }
152
153    // MARK: Lifecycle / selection hooks
154
155    /// Called before a user-triggered selection change is committed.
156    ///
157    /// `selection` is the live selection vec — the delegate may freely mutate it: add items,
158    /// remove items, reorder, or leave it unchanged to effectively veto the operation.
159    ///
160    /// `changes` is the slice of atomic changes the mode-strategy computed (e.g. Single
161    /// replacement deselects all then selects one; Multi toggles the clicked item). The delegate
162    /// is not required to apply them — they are informational. The default implementation applies
163    /// every change in order.
164    ///
165    /// No `cx` is available: this hook runs synchronously during the item-click handler while
166    /// the list entity is mutably borrowed. Side effects that need cx belong in `on_confirm`.
167    fn on_will_change(
168        &mut self,
169        selection: &mut Vec<(IndexPath, Self::Item)>,
170        changes: &[SearchableListChange],
171    ) {
172        for change in changes {
173            match change {
174                SearchableListChange::Select { index } => {
175                    let Some(item) = self.item(*index) else {
176                        continue;
177                    };
178
179                    if !selection
180                        .iter()
181                        .any(|(_, selected_item)| selected_item.value() == item.value())
182                    {
183                        selection.push((*index, item.clone()));
184                    }
185                }
186                SearchableListChange::Deselect { index } => {
187                    if let Some(item) = self.item(*index) {
188                        let has_value = selection
189                            .iter()
190                            .any(|(_, selected_item)| selected_item.value() == item.value());
191
192                        if has_value {
193                            selection
194                                .retain(|(_, selected_item)| selected_item.value() != item.value());
195                            continue;
196                        }
197                    }
198
199                    selection.retain(|(selected_ix, _)| selected_ix != index);
200                }
201            }
202        }
203    }
204
205    /// Called when the dropdown/popover is committed (Escape, `close_on_select`, or explicit
206    /// confirm). `final_selection` is the selection after the last committed change.
207    fn on_confirm(&mut self, _final_selection: &[(IndexPath, Self::Item)]) {}
208}