Skip to main content

gpui_component/command/
command.rs

1use std::rc::Rc;
2
3use gpui::{
4    AnyElement, App, DefiniteLength, Entity, IntoElement, RenderOnce, SharedString,
5    StyleRefinement, Styled, Window, rems,
6};
7
8use crate::IndexPath;
9use crate::command::{
10    item::{CommandEntry, CommandGroup, CommandItem},
11    state::{CommandModel, CommandState, OnCancel, OnIndex, OnQuery},
12};
13
14pub(crate) type CommandSlot = dyn Fn(&CommandState, &mut Window, &mut App) -> AnyElement;
15
16/// Presentation of a [`Command`], pushed into its state on every render.
17#[derive(Clone)]
18pub(crate) struct CommandOptions {
19    pub(crate) style: StyleRefinement,
20    pub(crate) placeholder: Option<SharedString>,
21    pub(crate) empty: Option<Rc<CommandSlot>>,
22    pub(crate) max_h: DefiniteLength,
23    pub(crate) bordered: bool,
24    pub(crate) header: Option<Rc<CommandSlot>>,
25    pub(crate) footer: Option<Rc<CommandSlot>>,
26}
27
28impl Default for CommandOptions {
29    fn default() -> Self {
30        Self {
31            style: StyleRefinement::default(),
32            placeholder: None,
33            empty: None,
34            max_h: rems(18.75).into(),
35            bordered: true,
36            header: None,
37            footer: None,
38        }
39    }
40}
41
42/// A command palette: a search field over a filtered list of commands.
43///
44/// Entries and rendering policy are configured on each `Command`; interaction
45/// state such as the query and highlighted item lives in [`CommandState`].
46///
47/// ```ignore
48/// let state = cx.new(|cx| CommandState::new(window, cx));
49///
50/// Command::new(&state)
51///     .group(
52///         CommandGroup::new().label("Suggestions")
53///             .item(CommandItem::new().label("Calendar").icon(IconName::Calendar)),
54///     )
55///     .placeholder("Type a command or search...")
56/// ```
57#[derive(IntoElement)]
58pub struct Command {
59    state: Entity<CommandState>,
60    entries: Vec<CommandEntry>,
61    searchable: bool,
62    filterable: bool,
63    on_query: Option<Rc<OnQuery>>,
64    on_select: Option<Rc<OnIndex>>,
65    on_confirm: Option<Rc<OnIndex>>,
66    on_cancel: Option<Rc<OnCancel>>,
67    options: CommandOptions,
68}
69
70impl Command {
71    /// Render the palette held by `state`.
72    pub fn new(state: &Entity<CommandState>) -> Self {
73        Self {
74            state: state.clone(),
75            entries: Vec::new(),
76            searchable: true,
77            filterable: true,
78            on_query: None,
79            on_select: None,
80            on_confirm: None,
81            on_cancel: None,
82            options: CommandOptions::default(),
83        }
84    }
85
86    /// Add an ungrouped command item.
87    pub fn item(mut self, item: CommandItem) -> Self {
88        self.entries.push(CommandEntry::Item(item));
89        self
90    }
91
92    /// Add multiple ungrouped command items.
93    pub fn items(mut self, items: impl IntoIterator<Item = CommandItem>) -> Self {
94        self.entries
95            .extend(items.into_iter().map(CommandEntry::Item));
96        self
97    }
98
99    /// Add a group of command items.
100    pub fn group(mut self, group: CommandGroup) -> Self {
101        self.entries.push(CommandEntry::Group(group));
102        self
103    }
104
105    /// Add a separator between the preceding and following entries.
106    pub fn separator(mut self) -> Self {
107        self.entries.push(CommandEntry::Separator);
108        self
109    }
110
111    /// Show or hide the query field and local filtering.
112    pub fn searchable(mut self, searchable: bool) -> Self {
113        self.searchable = searchable;
114        self
115    }
116
117    /// Keep the query field but toggle the local filtering, default: `true`.
118    ///
119    /// Turn it off when an external source already answers the query, such as
120    /// an async search: every supplied item stays visible, the query still
121    /// reports through [`Self::on_query`], and a query change hands the
122    /// highlight back to the first item instead of a local textual match.
123    pub fn filterable(mut self, filterable: bool) -> Self {
124        self.filterable = filterable;
125        self
126    }
127
128    /// Run a callback after a searchable query actually changes and the
129    /// current [`CommandState`] update releases its lease.
130    pub fn on_query<F>(mut self, callback: F) -> Self
131    where
132        F: Fn(&str, &mut Window, &mut App) + 'static,
133    {
134        self.on_query = Some(Rc::new(callback));
135        self
136    }
137
138    /// Run a callback after the highlighted item's original index path changes and the current
139    /// [`CommandState`] update releases its lease.
140    ///
141    /// For [`Self::items`], `section` is 0 and `row` is the item's position in
142    /// the supplied iterator. Explicit groups use their group and item
143    /// positions and follow the implicit ungrouped section when both forms are
144    /// mixed. Local filtering never changes these coordinates.
145    pub fn on_select<F>(mut self, callback: F) -> Self
146    where
147        F: Fn(IndexPath, &mut Window, &mut App) + 'static,
148    {
149        self.on_select = Some(Rc::new(callback));
150        self
151    }
152
153    /// Run a callback with the confirmed item's original index path after its Action is dispatched,
154    /// provided the source window remains live. The callback runs after the
155    /// current [`CommandState`] update releases its lease.
156    /// The path follows the same input-model coordinates as [`Self::on_select`].
157    pub fn on_confirm<F>(mut self, callback: F) -> Self
158    where
159        F: Fn(IndexPath, &mut Window, &mut App) + 'static,
160    {
161        self.on_confirm = Some(Rc::new(callback));
162        self
163    }
164
165    /// Run a callback synchronously before an empty-query Cancel action
166    /// propagates. A hosting Dialog should perform the dismissal after this
167    /// callback instead of being closed by the callback itself.
168    pub fn on_cancel<F>(mut self, callback: F) -> Self
169    where
170        F: Fn(&mut Window, &mut App) + 'static,
171    {
172        self.on_cancel = Some(Rc::new(callback));
173        self
174    }
175
176    /// Set the placeholder of the search field.
177    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
178        self.options.placeholder = Some(placeholder.into());
179        self
180    }
181
182    /// Render custom content when no command matches the query.
183    pub fn empty<F, E>(mut self, f: F) -> Self
184    where
185        F: Fn(&CommandState, &mut Window, &mut App) -> E + 'static,
186        E: IntoElement,
187    {
188        self.options.empty = Some(Rc::new(move |state, window, cx| {
189            f(state, window, cx).into_any_element()
190        }));
191        self
192    }
193
194    /// Set the max height of the list, default: 18.75rem (300px).
195    pub fn max_h(mut self, height: impl Into<DefiniteLength>) -> Self {
196        self.options.max_h = height.into();
197        self
198    }
199
200    /// Set whether to draw the surrounding border and rounding, default: `true`.
201    ///
202    /// Turn it off when the palette already sits inside a frame of its own,
203    /// such as a [`crate::Dialog`].
204    pub fn bordered(mut self, bordered: bool) -> Self {
205        self.options.bordered = bordered;
206        self
207    }
208
209    /// Render a custom element above the search field and command list.
210    pub fn header<F, E>(mut self, f: F) -> Self
211    where
212        F: Fn(&CommandState, &mut Window, &mut App) -> E + 'static,
213        E: IntoElement,
214    {
215        self.options.header = Some(Rc::new(move |state, window, cx| {
216            f(state, window, cx).into_any_element()
217        }));
218        self
219    }
220
221    /// Render a custom element below the command list.
222    pub fn footer<F, E>(mut self, f: F) -> Self
223    where
224        F: Fn(&CommandState, &mut Window, &mut App) -> E + 'static,
225        E: IntoElement,
226    {
227        self.options.footer = Some(Rc::new(move |state, window, cx| {
228            f(state, window, cx).into_any_element()
229        }));
230        self
231    }
232}
233
234impl Styled for Command {
235    fn style(&mut self) -> &mut StyleRefinement {
236        &mut self.options.style
237    }
238}
239
240impl RenderOnce for Command {
241    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
242        let options = self.options;
243        let model = CommandModel {
244            entries: self.entries,
245            searchable: self.searchable,
246            filterable: self.filterable,
247            on_query: self.on_query,
248            on_select: self.on_select,
249            on_confirm: self.on_confirm,
250            on_cancel: self.on_cancel,
251        };
252        self.state.update(cx, |state, cx| {
253            state.options = options;
254            state.install_model(model, cx);
255        });
256
257        self.state
258    }
259}