1use std::{cell::Cell, rc::Rc};
2
3use gpui::{Bounds, Context, Pixels, Render, anchored, canvas, deferred, point};
4
5use crate::prelude::*;
6
7pub struct Select {
10 options: Vec<SharedString>,
11 selected: Option<usize>,
12 open: bool,
13 placeholder: SharedString,
14 trigger_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
23}
24
25impl Select {
26 pub fn new(options: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
27 Self {
28 options: options.into_iter().map(Into::into).collect(),
29 selected: None,
30 open: false,
31 placeholder: "Select…".into(),
32 trigger_bounds: Rc::new(Cell::new(None)),
33 }
34 }
35
36 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
37 self.placeholder = placeholder.into();
38 self
39 }
40
41 pub fn selected_index(mut self, index: usize) -> Self {
42 self.selected = Some(index);
43 self
44 }
45
46 pub fn value(&self) -> Option<&SharedString> {
48 self.selected.and_then(|i| self.options.get(i))
49 }
50}
51
52impl Render for Select {
53 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
54 let label: SharedString = self
55 .selected
56 .and_then(|i| self.options.get(i).cloned())
57 .unwrap_or_else(|| self.placeholder.clone());
58 let has_value = self.selected.is_some();
59
60 let trigger = h_flex()
61 .id("select-trigger")
62 .debug_selector(|| "SELECT-TRIGGER".into())
67 .w_full()
68 .items_center()
69 .justify_between()
70 .px_3()
71 .py_2()
72 .rounded_md()
73 .bg(semantic::surface(cx))
74 .border_1()
75 .border_color(if self.open {
76 palette::primary(500)
77 } else {
78 semantic::border(cx)
79 })
80 .cursor_pointer()
81 .on_click(cx.listener(|this, _, _, cx| {
82 this.open = !this.open;
83 cx.notify();
84 }))
85 .child(Label::new(label).color(if has_value {
86 Color::Default
87 } else {
88 Color::Placeholder
89 }))
90 .child(Icon::new(IconName::ChevronDown).size(IconSize::Small))
91 .child({
92 let trigger_bounds = self.trigger_bounds.clone();
93 canvas(
94 move |bounds, _window, _cx| trigger_bounds.set(Some(bounds)),
95 |_bounds, _state, _window, _cx| {},
96 )
97 .absolute()
98 .top_0()
99 .left_0()
100 .size_full()
101 });
102
103 let trigger_width = px(240.);
104
105 v_flex()
106 .w(trigger_width)
107 .gap_1()
108 .child(trigger)
109 .when(self.open, |this| {
110 let hover = semantic::hover_bg(cx);
111 let mut list = v_flex()
112 .w(trigger_width)
113 .p_1()
114 .rounded_md()
115 .bg(semantic::elevated_surface(cx))
116 .border_1()
117 .border_color(semantic::border(cx))
118 .shadow_level(Shadow::Lg);
119 for (i, option) in self.options.iter().enumerate() {
120 let option = option.clone();
121 list = list.child(
122 h_flex()
123 .id(("select-option", i))
124 .debug_selector(move || format!("SELECT-OPTION-{i}"))
125 .w_full()
126 .px_3()
127 .py_2()
128 .rounded_md()
129 .cursor_pointer()
130 .hover(move |s| s.bg(hover))
131 .on_click(cx.listener(move |this, _, _, cx| {
132 this.selected = Some(i);
133 this.open = false;
134 cx.notify();
135 }))
136 .child(Label::new(option)),
137 );
138 }
139
140 let mut anchor = anchored().snap_to_window_with_margin(px(8.));
147 if let Some(bounds) = self.trigger_bounds.get() {
148 anchor = anchor.position(point(
149 bounds.origin.x,
150 bounds.origin.y + bounds.size.height + px(4.),
151 ));
152 }
153 let floating_list = deferred(
154 anchor.child(
155 div()
156 .occlude()
157 .debug_selector(|| "SELECT-LIST".into())
158 .child(list),
159 ),
160 )
161 .with_priority(1);
162 this.child(floating_list)
163 })
164 }
165}