Skip to main content

rgpui_component/searchable_list/
delegate.rs

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