use gpui::prelude::FluentBuilder;
use gpui::*;
use crate::theme::*;
use crate::components::basic::icon::{Icon, IconName};
#[derive(Clone, Debug)]
pub enum SelectEvent {
Changed(String),
MultiChanged(Vec<String>),
}
impl EventEmitter<SelectEvent> for Select {}
#[derive(Clone, Debug)]
pub struct SelectOption {
pub value: String,
pub label: String,
}
impl SelectOption {
pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
Self {
value: value.into(),
label: label.into(),
}
}
}
#[derive(Clone, Debug)]
pub struct SelectOptionGroup {
pub label: String,
pub options: Vec<SelectOption>,
}
impl SelectOptionGroup {
pub fn new(label: impl Into<String>, options: Vec<SelectOption>) -> Self {
Self {
label: label.into(),
options,
}
}
}
pub struct Select {
options: Vec<SelectOption>,
option_groups: Vec<SelectOptionGroup>,
selected_value: Option<String>,
selected_values: Vec<String>,
placeholder: String,
is_open: bool,
disabled: bool,
size: ComponentSize,
custom_font_size: Option<Pixels>,
multiple: bool,
clicking_menu: bool,
}
impl Select {
pub fn new(_cx: &mut Context<Self>) -> Self {
Self {
options: Vec::new(),
option_groups: Vec::new(),
selected_value: None,
selected_values: Vec::new(),
placeholder: "Select...".to_string(),
is_open: false,
disabled: false,
size: ComponentSize::Medium,
custom_font_size: None,
multiple: false,
clicking_menu: false,
}
}
pub fn options(mut self, options: Vec<SelectOption>) -> Self {
self.options = options;
self
}
pub fn option_groups(mut self, groups: Vec<SelectOptionGroup>) -> Self {
self.option_groups = groups;
self
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.selected_value = Some(value.into());
self
}
pub fn values(mut self, values: Vec<String>) -> Self {
self.selected_values = values;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn size(mut self, size: ComponentSize) -> Self {
self.size = size;
self
}
pub fn font_size(mut self, size: Pixels) -> Self {
self.custom_font_size = Some(size);
self
}
pub fn multiple(mut self, multiple: bool) -> Self {
self.multiple = multiple;
self
}
fn all_options(&self) -> Vec<SelectOption> {
let mut all = self.options.clone();
for group in &self.option_groups {
all.extend(group.options.clone());
}
all
}
fn display_text(&self) -> String {
if self.multiple {
if self.selected_values.is_empty() {
self.placeholder.clone()
} else {
format!("{} selected", self.selected_values.len())
}
} else if let Some(value) = &self.selected_value {
self.all_options()
.iter()
.find(|opt| &opt.value == value)
.map(|opt| opt.label.clone())
.unwrap_or_else(|| self.placeholder.clone())
} else {
self.placeholder.clone()
}
}
fn toggle_dropdown(&mut self) {
if !self.disabled {
self.is_open = !self.is_open;
}
}
fn close_dropdown(&mut self, cx: &mut Context<Self>) {
if self.clicking_menu && self.multiple {
self.clicking_menu = false;
return;
}
if self.is_open {
self.is_open = false;
cx.notify();
}
}
fn select_option(&mut self, value: String, cx: &mut Context<Self>) {
if self.multiple {
if let Some(pos) = self.selected_values.iter().position(|v| v == &value) {
self.selected_values.remove(pos);
} else {
self.selected_values.push(value);
}
cx.emit(SelectEvent::MultiChanged(self.selected_values.clone()));
} else {
self.selected_value = Some(value.clone());
self.is_open = false;
cx.emit(SelectEvent::Changed(value));
}
cx.notify();
}
fn remove_value(&mut self, value: String, cx: &mut Context<Self>) {
if let Some(pos) = self.selected_values.iter().position(|v| v == &value) {
self.selected_values.remove(pos);
cx.emit(SelectEvent::MultiChanged(self.selected_values.clone()));
cx.notify();
}
}
fn render_dropdown_overlay(&self, cx: &Context<Self>) -> impl IntoElement {
let theme = Theme::default();
div()
.absolute()
.top_full()
.left_0()
.right_0()
.mt_1()
.occlude()
.on_mouse_down(MouseButton::Left, cx.listener(|this, _event: &MouseDownEvent, _window, _cx| {
this.clicking_menu = true;
}))
.child(self.render_dropdown_menu(&theme, cx))
}
fn render_dropdown_menu(&self, theme: &Theme, cx: &Context<Self>) -> impl IntoElement {
let has_groups = !self.option_groups.is_empty();
let mut menu = div()
.occlude()
.id("select-popup")
.min_w(px(180.))
.max_h(px(300.))
.overflow_y_scroll()
.rounded(px(BorderRadius::LG))
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.background)
.shadow(vec![
BoxShadow {
color: rgba(0x00000010).into(),
offset: point(px(0.), px(4.)),
blur_radius: px(16.),
spread_radius: px(-2.),
},
BoxShadow {
color: rgba(0x00000008).into(),
offset: point(px(0.), px(2.)),
blur_radius: px(8.),
spread_radius: px(0.),
},
])
.p(px(6.));
if has_groups {
let mut item_counter: usize = 0;
menu = menu.children(self.option_groups.iter().enumerate().map(|(_group_idx, group)| {
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.px(px(12.))
.py(px(6.))
.text_xs()
.font_weight(FontWeight::SEMIBOLD)
.text_color(theme.colors.text_secondary)
.child(group.label.clone())
)
.children(group.options.iter().map(|option| {
let id = ("select-group-item", item_counter);
item_counter += 1;
self.render_option(option, id, theme, cx)
}))
}));
} else {
menu = menu.children(self.options.iter().enumerate().map(|(idx, option)| {
self.render_option(option, ("select-item", idx), theme, cx)
}));
}
menu
}
fn render_option(&self, option: &SelectOption, id: impl Into<ElementId>, theme: &Theme, cx: &Context<Self>) -> impl IntoElement {
let value = option.value.clone();
let label = option.label.clone();
let multiple = self.multiple;
let size = self.size;
let is_selected = if multiple {
self.selected_values.contains(&value)
} else {
self.selected_value.as_ref() == Some(&value)
};
div()
.id(id)
.relative()
.flex()
.items_center()
.justify_between()
.w_full()
.px(px(12.))
.py(px(8.))
.cursor(CursorStyle::PointingHand)
.text_size(self.custom_font_size.unwrap_or(size.font_size()))
.rounded(px(BorderRadius::SM))
.map(|this| {
if is_selected && !multiple {
this.bg(theme.colors.primary)
.text_color(rgb(0xFFFFFF))
} else {
this.text_color(theme.colors.text)
.hover(|style| style.bg(theme.colors.background_hover))
}
})
.on_mouse_down(MouseButton::Left, cx.listener(move |this, _event: &MouseDownEvent, _window, cx| {
this.select_option(value.clone(), cx);
}))
.child(
div()
.flex()
.items_center()
.gap_2()
.when(multiple, |this| {
this.child(self.render_checkbox(is_selected, theme))
})
.child(label)
)
.when(is_selected && !multiple, |this| {
this.child(
div()
.text_xs()
.child("✓")
)
})
}
fn render_checkbox(&self, checked: bool, theme: &Theme) -> impl IntoElement {
div()
.flex()
.items_center()
.justify_center()
.w(px(16.))
.h(px(16.))
.rounded(px(4.))
.border_1()
.border_color(if checked { theme.colors.primary } else { theme.colors.border })
.bg(if checked { theme.colors.primary } else { rgb(0xFFFFFF) })
.when(checked, |this| {
this.child(
div()
.text_xs()
.text_color(rgb(0xFFFFFF))
.child("✓")
)
})
}
fn render_selected_tags(&self, theme: &Theme, cx: &Context<Self>) -> Vec<impl IntoElement> {
let all_options = self.all_options();
self.selected_values.iter().map(|value| {
let label = all_options
.iter()
.find(|opt| &opt.value == value)
.map(|opt| opt.label.clone())
.unwrap_or_else(|| value.clone());
let value_for_remove = value.clone();
div()
.flex()
.items_center()
.gap_1()
.px(px(8.))
.py(px(4.))
.rounded(px(6.))
.bg(theme.colors.primary)
.text_color(rgb(0xFFFFFF))
.text_xs()
.child(label)
.child(
div()
.flex()
.items_center()
.justify_center()
.w(px(14.))
.h(px(14.))
.rounded(px(7.))
.cursor(CursorStyle::PointingHand)
.hover(|style| style.bg(rgba(0xFFFFFF20)))
.on_mouse_down(MouseButton::Left, cx.listener(move |this, _event: &MouseDownEvent, _window, cx| {
this.remove_value(value_for_remove.clone(), cx);
}))
.child(
div()
.text_xs()
.child("×")
)
)
}).collect()
}
}
impl Render for Select {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = Theme::default();
let disabled = self.disabled;
let is_open = self.is_open;
let multiple = self.multiple;
let (padding_y, padding_x) = self.size.padding();
let is_placeholder = if multiple {
self.selected_values.is_empty()
} else {
self.selected_value.is_none()
};
div()
.id("select-wrapper")
.w_full()
.when(is_open && !multiple, |this| {
this.on_mouse_down_out(cx.listener(|this, _event: &MouseDownEvent, _window, cx| {
this.close_dropdown(cx);
}))
})
.child(
div()
.id("select-container")
.relative()
.w_full()
.child(
div()
.id("select-trigger")
.relative()
.flex()
.w_full()
.items_center()
.justify_between()
.gap_2()
.py(padding_y)
.px(padding_x)
.rounded(px(BorderRadius::LG))
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.background)
.text_size(self.custom_font_size.unwrap_or(self.size.font_size()))
.shadow(vec![BoxShadow {
color: rgba(0x0000000A).into(),
offset: point(px(0.), px(1.)),
blur_radius: px(2.),
spread_radius: px(0.),
}])
.when(!disabled, |this| {
this.cursor(CursorStyle::PointingHand)
})
.when(disabled, |this| {
this.opacity(0.64)
})
.on_mouse_down(MouseButton::Left, cx.listener(|this, _event: &MouseDownEvent, _window, cx| {
this.toggle_dropdown();
cx.notify();
}))
.child(
div()
.flex()
.flex_1()
.items_center()
.gap_1()
.overflow_hidden()
.when(multiple && !self.selected_values.is_empty(), |this| {
this.children(self.render_selected_tags(&theme, cx))
})
.when(!multiple || self.selected_values.is_empty(), |this| {
this.child(
div()
.when(is_placeholder, |this| {
this.text_color(theme.colors.text_secondary)
})
.when(!is_placeholder, |this| {
this.text_color(theme.colors.text)
})
.child(self.display_text())
)
})
)
.child(
Icon::new(IconName::ChevronUpDown)
.small()
.color(rgb(0x666666))
)
)
.children(if is_open && !disabled {
Some(deferred(
self.render_dropdown_overlay(cx).into_any_element()
))
} else {
None
})
)
}
}