feat: add combobox component

This commit is contained in:
2026-11-04 08:08:02 +07:00
parent 7d1bceefd5
commit ded23ab2f2
7 changed files with 1126 additions and 3 deletions
+130
View File
@@ -11,6 +11,7 @@ use gpui_native_core::avatar::Avatar;
use gpui_native_core::badge::{Badge, BadgeVariant};
use gpui_native_core::button::{Button, ButtonVariant};
use gpui_native_core::checkbox::Checkbox;
use gpui_native_core::combobox::{Combobox, ComboboxOption, ComboboxState};
use gpui_native_core::common::{Sizable as _, Size, h_flex, v_flex};
use gpui_native_core::context_menu::ContextMenu;
use gpui_native_core::dialog::Dialog;
@@ -54,6 +55,7 @@ const CATALOG: &[(&str, &[(&str, &str)])] = &[
("button", "Button"),
("input", "Input"),
("field", "Field"),
("combobox", "Combobox"),
("status-dot", "Status dot"),
("checkbox", "Checkbox"),
("radio", "Radio"),
@@ -390,6 +392,12 @@ impl Gallery {
PanelDescriptor::new("field", "Field", |_, cx| cx.new(FieldDemo::new).into()),
cx,
);
dock.register_panel(
PanelDescriptor::new("combobox", "Combobox", |window, cx| {
cx.new(|cx| ComboboxDemo::new(window, cx)).into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("status-dot", "Status dot", |_, cx| {
cx.new(|_| StatusDotDemo).into()
@@ -1020,6 +1028,128 @@ impl Render for StatusDotDemo {
}
}
fn combo_option(label: &'static str) -> ComboboxOption<SharedString> {
(SharedString::from(label), SharedString::from(label))
}
struct ComboboxDemo {
branch: Entity<ComboboxState<SharedString>>,
label: Entity<ComboboxState<SharedString>>,
label_created: Option<SharedString>,
}
impl ComboboxDemo {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let view = cx.entity();
let branch = cx.new(|cx| {
ComboboxState::new(
Combobox::new(
"combo-demo-branch",
"Branch",
vec![
combo_option("main"),
combo_option("next"),
combo_option("release/1.0"),
combo_option("release/2.0"),
],
)
.selected(SharedString::from("main"))
.placeholder("Switch branch…")
.keep_unmatched_text(),
window,
cx,
)
});
let label_builder = Combobox::new(
"combo-demo-label",
"Label",
vec![
combo_option("bug"),
combo_option("docs"),
combo_option("enhancement"),
],
)
.placeholder("Pick or type a label…")
.on_create(move |text: SharedString, window, cx| {
// Push the typed text as a real option and choose it: the picker
// never mutates its own list behind the caller's back, so the
// demo does it here.
view.update(cx, |demo, cx| {
demo.label_created = Some(text.clone());
demo.label.update(cx, |label, cx| {
let mut options = label.options_snapshot();
if !options.iter().any(|(value, _)| value == &text) {
options.push((text.clone(), text.clone()));
label.set_options(options, cx);
}
label.set_selected(Some(text), window, cx);
});
});
});
let label = cx.new(|cx| ComboboxState::new(label_builder, window, cx));
Self {
branch,
label,
label_created: None,
}
}
}
impl Render for ComboboxDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
let branch_value = self
.branch
.read(cx)
.selected()
.map(|value| value.to_string())
.unwrap_or_else(|| "none".to_string());
let label_value = self
.label_created
.clone()
.unwrap_or_else(|| SharedString::from("none"));
ScrollArea::new("combobox-demo")
.size_full()
.p_4()
.child(demo_section(
"keep unmatched",
colors,
v_flex()
.gap_2()
.child(
Field::new("combo-demo-branch-field", "Branch")
.description("Type a name that is not in the list to keep it.")
.child(self.branch.clone()),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("branch: {branch_value}")),
),
))
.child(demo_section(
"create on close",
colors,
v_flex()
.gap_2()
.child(
Field::new("combo-demo-label-field", "Label")
.description("Typing a new label creates it when the field blurs.")
.child(self.label.clone()),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("label: {label_value}")),
),
))
}
}
struct CheckboxDemo {
checked: bool,
}