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