1use std::rc::Rc;
9
10use gpui::{
11 AnyElement, App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
12 prelude::FluentBuilder,
13};
14use gpui_kit_semantics::{NodeSpec, Role, Semantic};
15use gpui_kit_theme::{ActiveTheme, Elevation, Radius, Space, Surface, TypeScale};
16
17use crate::controls::button::Button;
18use crate::display::badge::Tone;
19use crate::display::tag::Tag;
20use crate::foundation::{Disableable, Ident, Sizable, StyledExt, text as foundation_text};
21use crate::strings::{ActiveStrings, StringKey};
22
23type RemoveHandler = Rc<dyn Fn(SharedString, &mut Window, &mut App)>;
24type ClearHandler = Rc<dyn Fn(&mut Window, &mut App)>;
25type AddHandler = Rc<dyn Fn(&mut Window, &mut App)>;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct FilterCondition {
30 id: SharedString,
31 field: SharedString,
32 operator: SharedString,
33 value: SharedString,
34 tone: Tone,
35}
36
37impl FilterCondition {
38 pub fn new(
39 id: impl Into<SharedString>,
40 field: impl Into<SharedString>,
41 operator: impl Into<SharedString>,
42 value: impl Into<SharedString>,
43 ) -> Self {
44 Self {
45 id: id.into(),
46 field: field.into(),
47 operator: operator.into(),
48 value: value.into(),
49 tone: Tone::Accent,
50 }
51 }
52
53 pub fn tone(mut self, tone: Tone) -> Self {
54 self.tone = tone;
55 self
56 }
57
58 pub fn id(&self) -> &SharedString {
59 &self.id
60 }
61
62 pub fn label(&self) -> SharedString {
64 SharedString::from(format!("{} {} {}", self.field, self.operator, self.value))
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Default)]
74pub enum ResultCount {
75 #[default]
77 Unknown,
78 Counting,
80 Known(usize),
82 Unavailable(SharedString),
84}
85
86impl ResultCount {
87 pub fn as_str(&self) -> &'static str {
88 match self {
89 Self::Unknown => "unknown",
90 Self::Counting => "counting",
91 Self::Known(_) => "known",
92 Self::Unavailable(_) => "unavailable",
93 }
94 }
95
96 fn sentence(&self, noun: &SharedString, cx: &App) -> Option<SharedString> {
98 match self {
99 Self::Unknown => None,
100 Self::Counting => Some(cx.strings().text(StringKey::FilterBarCounting)),
101 Self::Known(count) => Some(SharedString::from(format!("{count} {noun}"))),
102 Self::Unavailable(reason) => Some(reason.clone()),
103 }
104 }
105}
106
107#[derive(IntoElement)]
109pub struct FilterBar {
110 ident: Ident,
111 conditions: Vec<FilterCondition>,
112 count: ResultCount,
113 noun: Option<SharedString>,
114 add_control: Option<AnyElement>,
115 add_label: Option<SharedString>,
116 clear_label: Option<SharedString>,
117 size: gpui_kit_theme::ControlSize,
118 disabled: bool,
119 on_add: Option<AddHandler>,
120 on_remove: Option<RemoveHandler>,
121 on_clear: Option<ClearHandler>,
122}
123
124impl std::fmt::Debug for FilterBar {
125 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 formatter
127 .debug_struct("FilterBar")
128 .field("ident", &self.ident)
129 .field("conditions", &self.conditions.len())
130 .field("count", &self.count)
131 .field("disabled", &self.disabled)
132 .finish()
133 }
134}
135
136impl FilterBar {
137 pub fn new(ident: impl Into<Ident>) -> Self {
138 Self {
139 ident: ident.into(),
140 conditions: Vec::new(),
141 count: ResultCount::default(),
142 noun: None,
143 add_control: None,
144 add_label: None,
145 clear_label: None,
146 size: gpui_kit_theme::ControlSize::Sm,
147 disabled: false,
148 on_add: None,
149 on_remove: None,
150 on_clear: None,
151 }
152 }
153
154 pub fn condition(mut self, condition: FilterCondition) -> Self {
155 self.conditions.push(condition);
156 self
157 }
158
159 pub fn conditions(mut self, conditions: impl IntoIterator<Item = FilterCondition>) -> Self {
160 self.conditions.extend(conditions);
161 self
162 }
163
164 pub fn count(mut self, count: ResultCount) -> Self {
166 self.count = count;
167 self
168 }
169
170 pub fn noun(mut self, noun: impl Into<SharedString>) -> Self {
172 self.noun = Some(noun.into());
173 self
174 }
175
176 pub fn add_control(mut self, control: impl IntoElement) -> Self {
180 self.add_control = Some(control.into_any_element());
181 self
182 }
183
184 pub fn add_label(mut self, label: impl Into<SharedString>) -> Self {
185 self.add_label = Some(label.into());
186 self
187 }
188
189 pub fn clear_label(mut self, label: impl Into<SharedString>) -> Self {
190 self.clear_label = Some(label.into());
191 self
192 }
193
194 pub fn on_add(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
197 self.on_add = Some(Rc::new(handler));
198 self
199 }
200
201 pub fn on_remove(
203 mut self,
204 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
205 ) -> Self {
206 self.on_remove = Some(Rc::new(handler));
207 self
208 }
209
210 pub fn on_clear(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
211 self.on_clear = Some(Rc::new(handler));
212 self
213 }
214}
215
216impl Disableable for FilterBar {
217 fn disabled(mut self, disabled: bool) -> Self {
218 self.disabled = disabled;
219 self
220 }
221}
222
223impl Sizable for FilterBar {
224 fn control_size(mut self, size: gpui_kit_theme::ControlSize) -> Self {
225 self.size = size;
226 self
227 }
228}
229
230impl RenderOnce for FilterBar {
231 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
232 let theme = cx.theme().clone();
233 let active = self.conditions.len();
234 let removable = self.on_remove.clone().filter(|_| !self.disabled);
235
236 let chips = self.conditions.iter().map(|condition| {
237 let ident = self.ident.child(condition.id.as_ref());
238 let mut chip = Tag::new(ident, condition.label())
239 .tone(condition.tone)
240 .disabled(self.disabled);
241 if let Some(handler) = removable.clone() {
242 let id = condition.id.clone();
243 chip = chip.on_remove(move |window, cx| handler(id.clone(), window, cx));
244 }
245 chip
246 });
247
248 let add = match self.add_control {
249 Some(control) => Some(div().flex_none().child(control).into_any_element()),
250 None => self
251 .on_add
252 .clone()
253 .filter(|_| !self.disabled)
254 .map(|handler| {
255 Button::new(self.ident.child("add"))
256 .label(
257 self.add_label
258 .clone()
259 .unwrap_or_else(|| cx.strings().text(StringKey::FilterBarAdd)),
260 )
261 .ghost()
262 .control_size(self.size)
263 .semantic_parent(self.ident.semantic_id())
264 .icon(gpui_kit_assets::Icon::Plus)
265 .on_click(move |window, cx| handler(window, cx))
266 .into_any_element()
267 }),
268 };
269
270 let clear = self
271 .on_clear
272 .clone()
273 .filter(|_| !self.disabled && active > 0)
274 .map(|handler| {
275 Button::new(self.ident.child("clear"))
276 .label(
277 self.clear_label
278 .clone()
279 .unwrap_or_else(|| cx.strings().text(StringKey::FilterBarClear)),
280 )
281 .ghost()
282 .control_size(self.size)
283 .semantic_parent(self.ident.semantic_id())
284 .on_click(move |window, cx| handler(window, cx))
285 });
286
287 let count_ident = self.ident.child("count");
288 let noun = self
289 .noun
290 .clone()
291 .unwrap_or_else(|| cx.strings().text(StringKey::FilterBarResultsNoun));
292 let count = self.count.sentence(&noun, cx).map(|sentence| {
293 foundation_text(&theme, TypeScale::Caption, sentence.clone())
294 .flex_none()
295 .text_color(match self.count {
296 ResultCount::Unavailable(_) => theme.colors.warning,
297 ResultCount::Counting => theme.colors.text_faint,
298 _ => theme.colors.text_muted,
299 })
300 .semantic_in(
301 cx,
302 NodeSpec::new(count_ident.semantic_id(), Role::Status)
303 .parent(self.ident.semantic_id())
304 .text(sentence)
305 .value(self.count.as_str())
308 .busy(self.count == ResultCount::Counting),
309 )
310 });
311
312 div()
313 .row()
314 .w_full()
315 .flex_wrap()
316 .items_center()
317 .gap_token(&theme, Space::Sm)
318 .px_token(&theme, Space::Sm)
319 .py_token(&theme, Space::Xs)
320 .radius(&theme, Radius::Card)
321 .frame(&theme, Surface::Panel, Elevation::Raised)
322 .when(self.disabled, |element| {
323 element.opacity(theme.opacity.disabled)
324 })
325 .children(chips)
326 .children(add)
327 .child(div().flex_1())
328 .children(count)
329 .children(clear)
330 .semantic_in(
331 cx,
332 NodeSpec::new(self.ident.semantic_id(), Role::Toolbar)
333 .text(cx.strings().text(StringKey::FilterBarLabel))
334 .disabled(self.disabled)
335 .value(active.to_string()),
336 )
337 }
338}