Skip to content

Commit 0b106bc

Browse files
committed
combo_box: Add "combo-box.md" docs (EN + zh-CN) and cross-link from "select.md"
1 parent d7b5205 commit 0b106bc

6 files changed

Lines changed: 688 additions & 0 deletions

File tree

docs/docs/components/combo-box.md

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
---
2+
title: ComboBox
3+
description: An autocomplete input paired with a searchable dropdown list.
4+
---
5+
6+
# ComboBox
7+
8+
A combobox component that allows users to select one (or many) values from a searchable list.
9+
10+
Compared to [Select](select), `ComboBox` adds support for custom trigger rendering and custom item rendering, making it easy to build rich selection UIs without forking the underlying list behaviour.
11+
12+
`MultiComboBox` is the multi-select variant — it toggles items in the selection and keeps the dropdown open until the user dismisses it.
13+
14+
## Import
15+
16+
```rust
17+
use gpui_component::combo_box::{
18+
ComboBox, ComboBoxState, ComboBoxEvent,
19+
MultiComboBox, MultiComboBoxState, MultiComboBoxEvent,
20+
TriggerCtx, MultiTriggerCtx,
21+
};
22+
use gpui_component::searchable_list::{
23+
SearchableListItem, SearchableVec, SearchableGroup,
24+
};
25+
```
26+
27+
## Usage
28+
29+
### Basic Single-Select
30+
31+
```rust
32+
let state = cx.new(|cx| {
33+
ComboBoxState::new(
34+
SearchableVec::new(vec!["Next.js", "SvelteKit", "Nuxt.js"]),
35+
None, // no initial selection
36+
window,
37+
cx,
38+
)
39+
.searchable(true)
40+
});
41+
42+
ComboBox::new(&state)
43+
.placeholder("Select framework...")
44+
.search_placeholder("Search...")
45+
.w_full()
46+
```
47+
48+
### Pre-selected Item
49+
50+
Pass the index path of the item to pre-select:
51+
52+
```rust
53+
let state = cx.new(|cx| {
54+
ComboBoxState::new(items, Some(IndexPath::default()), window, cx)
55+
});
56+
```
57+
58+
### Grouped Items
59+
60+
Use `SearchableGroup` to group items under a heading:
61+
62+
```rust
63+
let grouped = SearchableVec::new(vec![
64+
SearchableGroup::new("Fruits").items(vec![
65+
FoodItem::new("Apples"),
66+
FoodItem::new("Bananas"),
67+
]),
68+
SearchableGroup::new("Vegetables").items(vec![
69+
FoodItem::new("Carrots"),
70+
FoodItem::new("Spinach"),
71+
]),
72+
]);
73+
74+
let state = cx.new(|cx| {
75+
ComboBoxState::new(grouped, None, window, cx).searchable(true)
76+
});
77+
78+
ComboBox::new(&state)
79+
```
80+
81+
### Implementing `SearchableListItem`
82+
83+
Built-in implementations of `SearchableListItem` exist for `String`, `SharedString`, and `&'static str`. For custom types implement the trait:
84+
85+
```rust
86+
#[derive(Clone)]
87+
struct Country {
88+
name: SharedString,
89+
code: SharedString,
90+
}
91+
92+
impl SearchableListItem for Country {
93+
type Value = SharedString;
94+
95+
fn title(&self) -> SharedString {
96+
self.name.clone()
97+
}
98+
99+
fn value(&self) -> &SharedString {
100+
&self.code
101+
}
102+
103+
fn matches(&self, query: &str) -> bool {
104+
self.name.to_lowercase().contains(query)
105+
|| self.code.to_lowercase().contains(query)
106+
}
107+
}
108+
```
109+
110+
### Disabled Items
111+
112+
Return `true` from `disabled()` on items that should not be selectable:
113+
114+
```rust
115+
impl SearchableListItem for MyItem {
116+
// ...
117+
fn disabled(&self) -> bool {
118+
self.is_unavailable
119+
}
120+
}
121+
```
122+
123+
### Custom Check Icon
124+
125+
```rust
126+
ComboBox::new(&state)
127+
.check_icon(Icon::new(IconName::CircleCheck))
128+
```
129+
130+
### Footer Action
131+
132+
Render a persistent action at the bottom of the dropdown (e.g. an "Add new" button):
133+
134+
```rust
135+
ComboBox::new(&state)
136+
.footer(|_, cx| {
137+
Button::new("add-new")
138+
.ghost()
139+
.label("New item")
140+
.icon(Icon::new(IconName::Plus))
141+
.w_full()
142+
.justify_start()
143+
.into_any_element()
144+
})
145+
```
146+
147+
### Custom Trigger
148+
149+
Override the entire trigger element. You control the label, icons, and layout. `TriggerCtx` exposes selection state, open/disabled flags, and the current size:
150+
151+
```rust
152+
ComboBox::new(&state)
153+
.render_trigger(|ctx, _, cx| {
154+
h_flex()
155+
.w_full()
156+
.items_center()
157+
.gap_2()
158+
.when_some(ctx.selected_item, |this, item| {
159+
this.child(
160+
div()
161+
.bg(cx.theme().accent)
162+
.rounded_sm()
163+
.px_1p5()
164+
.py_0p5()
165+
.text_sm()
166+
.child(item.title()),
167+
)
168+
})
169+
.when(ctx.selected_item.is_none(), |this| {
170+
this.text_color(cx.theme().muted_foreground)
171+
.child("Select...")
172+
})
173+
.into_any_element()
174+
})
175+
```
176+
177+
### Custom Item Renderer
178+
179+
Override how each item row is drawn. When set, the automatic trailing check icon is suppressed — your closure controls the full row:
180+
181+
```rust
182+
ComboBox::new(&state)
183+
.render_item(|item: &MyItem, is_selected, _, cx| {
184+
h_flex()
185+
.w_full()
186+
.gap_2()
187+
.items_center()
188+
.child(Icon::new(item.icon.clone()).small())
189+
.child(div().child(item.title()))
190+
.into_any_element()
191+
})
192+
```
193+
194+
### Sizes
195+
196+
```rust
197+
ComboBox::new(&state).large()
198+
ComboBox::new(&state) // medium (default)
199+
ComboBox::new(&state).small()
200+
```
201+
202+
### Cleanable
203+
204+
```rust
205+
ComboBox::new(&state).cleanable(true) // show clear button when value is selected
206+
```
207+
208+
### Disabled
209+
210+
```rust
211+
ComboBox::new(&state).disabled(true)
212+
```
213+
214+
### Events
215+
216+
```rust
217+
cx.subscribe_in(&state, window, |view, _, event, window, cx| {
218+
match event {
219+
ComboBoxEvent::Confirm(value) => {
220+
// value is Option<Value>
221+
}
222+
}
223+
});
224+
```
225+
226+
### Mutating
227+
228+
```rust
229+
// Set by index
230+
state.update(cx, |s, cx| {
231+
s.set_selected_index(Some(IndexPath::default()), window, cx);
232+
});
233+
234+
// Set by value (requires Value: PartialEq)
235+
state.update(cx, |s, cx| {
236+
s.set_selected_value(&"my-value".into(), window, cx);
237+
});
238+
239+
// Read current value
240+
let value = state.read(cx).selected_value(); // Option<&Value>
241+
```
242+
243+
## Multi-Select
244+
245+
### Basic Multi-Select
246+
247+
`MultiComboBoxState` holds a `Vec<Value>` selection. Selecting an item toggles it; the dropdown stays open until dismissed.
248+
249+
```rust
250+
let state = cx.new(|cx| {
251+
MultiComboBoxState::new(
252+
SearchableVec::new(vec!["React", "Vue", "Angular"]),
253+
vec!["React"], // pre-selected
254+
window,
255+
cx,
256+
)
257+
.searchable(true)
258+
});
259+
260+
MultiComboBox::new(&state)
261+
.placeholder("Select frameworks")
262+
```
263+
264+
### Custom Multi-Select Trigger
265+
266+
`MultiTriggerCtx` exposes `selected_values: &[Value]`:
267+
268+
```rust
269+
MultiComboBox::new(&state)
270+
.render_trigger(|ctx, _, cx| {
271+
if ctx.selected_values.is_empty() {
272+
return div()
273+
.text_color(cx.theme().muted_foreground)
274+
.child("Select...")
275+
.into_any_element();
276+
}
277+
278+
h_flex()
279+
.flex_wrap()
280+
.gap_1()
281+
.children(ctx.selected_values.iter().map(|val| {
282+
div()
283+
.rounded_sm()
284+
.border_1()
285+
.border_color(cx.theme().border)
286+
.px_1p5()
287+
.py_0p5()
288+
.text_sm()
289+
.child(*val)
290+
}))
291+
.into_any_element()
292+
})
293+
```
294+
295+
### Multi-Select Events
296+
297+
```rust
298+
cx.subscribe_in(&state, window, |view, _, event, window, cx| {
299+
match event {
300+
MultiComboBoxEvent::Change(values) => {
301+
// fired on every toggle
302+
}
303+
MultiComboBoxEvent::Confirm(values) => {
304+
// fired when the dropdown closes
305+
}
306+
}
307+
});
308+
```
309+
310+
### Mutating Multi-Select
311+
312+
```rust
313+
state.update(cx, |s, cx| {
314+
s.add_value("Vue", cx);
315+
s.remove_value(&"React", cx);
316+
s.clear_selection(cx);
317+
s.set_selected_values(vec!["Angular", "Svelte"], cx);
318+
});
319+
320+
let values = state.read(cx).selected_values(); // &[Value]
321+
```
322+
323+
## Keyboard Shortcuts
324+
325+
| Key | Action |
326+
| --------- | ---------------------------------------- |
327+
| `Tab` | Focus trigger |
328+
| `Enter` | Open menu or confirm highlighted item |
329+
| `Up/Down` | Navigate options (opens menu if closed) |
330+
| `Escape` | Close menu |
331+
332+
## Theming
333+
334+
- `background` — Dropdown input background
335+
- `input` — Trigger border color
336+
- `foreground` — Text color
337+
- `muted_foreground` — Placeholder and disabled text
338+
- `border` — Menu border
339+
- `radius` — Border radius

docs/docs/components/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ collapsed: false
3737

3838
- [Input](input) - An input field or a component that looks like an input field.
3939
- [Select](select) - A list of options for the user to pick.
40+
- [ComboBox](combo-box) - Searchable single-select or multi-select dropdown.
4041
- [NumberInput](number-input) - Numeric input with increment/decrement
4142
- [DatePicker](date-picker) - Date selection with calendar
4243
- [OtpInput](otp-input) - One-time password input

docs/docs/components/select.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ A select component that allows users to choose from a list of options.
1515

1616
Supports search functionality, grouped items, custom rendering, and various states. Built with keyboard navigation and accessibility in mind.
1717

18+
:::tip
19+
For richer selection UIs with custom trigger rendering or multi-select, see [ComboBox](combo-box).
20+
:::
21+
1822
## Import
1923

2024
```rust

0 commit comments

Comments
 (0)