1use gpui::*;
2use twrite_core::{KeyCode, PromptState, SearchAction, SearchSnapshot};
3
4use crate::editor::Editor;
5
6pub struct PromptBar;
15
16impl PromptBar {
17 pub fn bottom_bar(
22 prompt: &PromptState,
23 snapshot: Option<SearchSnapshot>,
24 cx: &mut Context<Editor>,
25 ) -> Div {
26 if !prompt.is_open() {
27 return div();
28 }
29 if let Some(snapshot) = snapshot {
30 return Self::search_toolbar(prompt, &snapshot, cx);
31 }
32 div()
33 .w_full()
34 .border_t_1()
35 .border_color(rgb(0x313244))
36 .bg(rgb(0x11111b))
37 .px(px(12.0))
38 .py(px(6.0))
39 .on_mouse_down(MouseButton::Left, |_event, _window, cx| {
40 cx.stop_propagation();
41 })
42 .flex()
43 .flex_col()
44 .gap_1()
45 .child(Self::input_row(prompt))
46 .child(Self::item_list(prompt, 6))
47 .child(Self::message_row(prompt))
48 }
49
50 pub fn palette(
52 prompt: &PromptState,
53 snapshot: Option<SearchSnapshot>,
54 cx: &mut Context<Editor>,
55 ) -> Div {
56 if !prompt.is_open() {
57 return div();
58 }
59 if let Some(snapshot) = snapshot {
60 return div()
61 .absolute()
62 .top(px(48.0))
63 .w_full()
64 .flex()
65 .justify_center()
66 .child(
67 div()
68 .w(px(720.0))
69 .rounded_md()
70 .border_1()
71 .border_color(rgb(0x45475a))
72 .bg(rgb(0x181825))
73 .p(px(8.0))
74 .on_mouse_down(MouseButton::Left, |_event, _window, cx| {
75 cx.stop_propagation();
76 })
77 .child(Self::search_toolbar_inner(prompt, &snapshot, cx)),
78 );
79 }
80 div()
81 .absolute()
82 .top(px(48.0))
83 .w_full()
84 .flex()
85 .justify_center()
86 .child(
87 div()
88 .w(px(560.0))
89 .rounded_md()
90 .border_1()
91 .border_color(rgb(0x45475a))
92 .bg(rgb(0x1e1e2e))
93 .px(px(12.0))
94 .py(px(8.0))
95 .on_mouse_down(MouseButton::Left, |_event, _window, cx| {
96 cx.stop_propagation();
97 })
98 .flex()
99 .flex_col()
100 .gap_1()
101 .child(Self::input_row(prompt))
102 .child(Self::item_list(prompt, 8))
103 .child(Self::message_row(prompt)),
104 )
105 }
106
107 fn search_toolbar(
109 prompt: &PromptState,
110 snapshot: &SearchSnapshot,
111 cx: &mut Context<Editor>,
112 ) -> Div {
113 div()
114 .w_full()
115 .border_t_1()
116 .border_color(rgb(0x313244))
117 .bg(rgb(0x181825))
118 .px(px(12.0))
119 .py(px(6.0))
120 .on_mouse_down(MouseButton::Left, |_event, _window, cx| {
121 cx.stop_propagation();
122 })
123 .child(Self::search_toolbar_inner(prompt, snapshot, cx))
124 }
125
126 fn search_toolbar_inner(
128 prompt: &PromptState,
129 snapshot: &SearchSnapshot,
130 cx: &mut Context<Editor>,
131 ) -> Div {
132 let mut col = div().flex().flex_col().gap_2();
133 col = col.child(Self::find_row(prompt, snapshot, cx));
134 if snapshot.replace_mode {
135 col = col.child(Self::replace_row(prompt, snapshot, cx));
136 }
137 col
138 }
139
140 fn find_row(prompt: &PromptState, snapshot: &SearchSnapshot, cx: &mut Context<Editor>) -> Div {
142 let is_find_active = !snapshot.is_replace_prompt;
143 let find_text = if is_find_active {
144 prompt.input().to_string()
145 } else {
146 snapshot.query.clone()
147 };
148 let cursor_idx = if is_find_active {
149 Some(prompt.cursor())
150 } else {
151 None
152 };
153
154 let count_text = if snapshot.matches.is_empty() {
155 if snapshot.query.is_empty() && prompt.input().is_empty() {
156 String::new()
157 } else {
158 "No matches".to_string()
159 }
160 } else {
161 format!(
162 "{}/{}",
163 snapshot.current.map(|i| i + 1).unwrap_or(0),
164 snapshot.matches.len()
165 )
166 };
167
168 let mut row = div()
169 .flex()
170 .flex_row()
171 .items_center()
172 .gap_2()
173 .child(Self::replace_toggle_button(snapshot.replace_mode, cx))
174 .child(Self::text_box(
175 "search-find-box",
176 &find_text,
177 cursor_idx,
178 "Find in page",
179 is_find_active,
180 SearchAction::FocusSearch,
181 cx,
182 ))
183 .child(Self::stepper_button(
184 "search-step-prev",
185 "^",
186 KeyCode::Up,
187 cx,
188 ))
189 .child(Self::stepper_button(
190 "search-step-next",
191 "v",
192 KeyCode::Down,
193 cx,
194 ));
195
196 if !count_text.is_empty() {
197 let color = if snapshot.matches.is_empty() {
198 rgb(0xf38ba8)
199 } else {
200 rgb(0xa6adc8)
201 };
202 row = row.child(div().text_xs().text_color(color).child(count_text));
203 }
204
205 row = row
206 .child(Self::checkbox(
207 "search-check-highlight",
208 "Highlight All",
209 Some(0),
210 snapshot.highlight_all,
211 KeyCode::Char('h'),
212 cx,
213 ))
214 .child(Self::checkbox(
215 "search-check-case",
216 "Match Case",
217 Some(6),
218 snapshot.case_sensitive,
219 KeyCode::Char('c'),
220 cx,
221 ))
222 .child(Self::checkbox(
223 "search-check-word",
224 "Whole Words",
225 Some(0),
226 snapshot.whole_word,
227 KeyCode::Char('w'),
228 cx,
229 ))
230 .child(div().flex_1())
231 .child(Self::close_button(cx));
232
233 row
234 }
235
236 fn replace_row(
238 prompt: &PromptState,
239 snapshot: &SearchSnapshot,
240 cx: &mut Context<Editor>,
241 ) -> Div {
242 let is_replace_active = snapshot.is_replace_prompt;
243 let replace_text = if is_replace_active {
244 prompt.input().to_string()
245 } else {
246 snapshot.replacement.clone()
247 };
248 let cursor_idx = if is_replace_active {
249 Some(prompt.cursor())
250 } else {
251 None
252 };
253
254 let mut row = div()
255 .flex()
256 .flex_row()
257 .items_center()
258 .gap_2()
259 .child(div().w(px(24.0)))
260 .child(Self::text_box(
261 "search-replace-box",
262 &replace_text,
263 cursor_idx,
264 "Replace with",
265 is_replace_active,
266 SearchAction::FocusReplace,
267 cx,
268 ))
269 .child(Self::action_button(
270 "search-btn-replace",
271 "Replace",
272 KeyCode::Enter,
273 true,
274 false,
275 cx,
276 ))
277 .child(Self::action_button(
278 "search-btn-replace-all",
279 "Replace All",
280 KeyCode::Char('a'),
281 false,
282 true,
283 cx,
284 ));
285
286 if let Some(msg) = prompt.message() {
287 row = row.child(
288 div()
289 .text_xs()
290 .text_color(rgb(0xa6e3a1))
291 .child(msg.to_string()),
292 );
293 }
294
295 row
296 }
297
298 fn text_box(
300 id: &'static str,
301 text: &str,
302 cursor: Option<usize>,
303 placeholder: &'static str,
304 focused: bool,
305 focus_action: SearchAction,
306 cx: &mut Context<Editor>,
307 ) -> Stateful<Div> {
308 let border_color = if focused {
309 rgb(0x89b4fa)
310 } else {
311 rgb(0x45475a)
312 };
313
314 let mut box_div = div()
315 .id(id)
316 .cursor_text()
317 .w(px(240.0))
318 .h(px(28.0))
319 .rounded_md()
320 .border_1()
321 .border_color(border_color)
322 .bg(rgb(0x11111b))
323 .px(px(8.0))
324 .flex()
325 .items_center()
326 .overflow_hidden();
327
328 if let Some(cursor_offset) = cursor {
329 if text.is_empty() {
330 box_div = box_div
331 .child(div().w(px(1.5)).h(px(15.0)).bg(rgb(0x89b4fa)).mr(px(2.0)))
332 .child(
333 div()
334 .text_sm()
335 .text_color(rgb(0x6c7086))
336 .child(placeholder.to_string()),
337 );
338 } else {
339 let cursor = cursor_offset.min(text.len());
340 let before = text[..cursor].to_string();
341 let after = text[cursor..].to_string();
342 if !before.is_empty() {
343 box_div =
344 box_div.child(div().text_sm().text_color(rgb(0xcdd6f4)).child(before));
345 }
346 box_div = box_div.child(div().w(px(1.5)).h(px(15.0)).bg(rgb(0x89b4fa)));
347 if !after.is_empty() {
348 box_div = box_div.child(div().text_sm().text_color(rgb(0xcdd6f4)).child(after));
349 }
350 }
351 } else if text.is_empty() {
352 box_div = box_div.child(
353 div()
354 .text_sm()
355 .text_color(rgb(0x6c7086))
356 .child(placeholder.to_string()),
357 );
358 } else {
359 box_div = box_div.child(
360 div()
361 .text_sm()
362 .text_color(rgb(0xcdd6f4))
363 .child(text.to_string()),
364 );
365 }
366
367 box_div = box_div.on_mouse_down(
368 MouseButton::Left,
369 cx.listener(move |this, _event: &MouseDownEvent, window, cx| {
370 if !focused {
371 this.press_search_action(focus_action, Some(window), cx);
372 }
373 cx.stop_propagation();
374 }),
375 );
376
377 box_div
378 }
379
380 fn checkbox(
382 id: &'static str,
383 label: &'static str,
384 underline_index: Option<usize>,
385 checked: bool,
386 alt_key: KeyCode,
387 cx: &mut Context<Editor>,
388 ) -> Stateful<Div> {
389 let box_bg = if checked {
390 rgb(0x89b4fa)
391 } else {
392 rgb(0x181825)
393 };
394 let box_border = if checked {
395 rgb(0x89b4fa)
396 } else {
397 rgb(0x585b70)
398 };
399 let text_color = if checked {
400 rgb(0xcdd6f4)
401 } else {
402 rgb(0xa6adc8)
403 };
404
405 let mut check_icon = div()
406 .w(px(14.0))
407 .h(px(14.0))
408 .rounded(px(3.0))
409 .border_1()
410 .border_color(box_border)
411 .bg(box_bg)
412 .flex()
413 .items_center()
414 .justify_center();
415
416 if checked {
417 check_icon = check_icon.child(
418 div()
419 .text_xs()
420 .font_weight(FontWeight::BOLD)
421 .text_color(rgb(0x11111b))
422 .child("✓"),
423 );
424 }
425
426 let mut label_div = div().text_xs().text_color(text_color).flex().flex_row();
427 if let Some(idx) = underline_index {
428 let before = &label[..idx];
429 let under = &label[idx..idx + 1];
430 let after = &label[idx + 1..];
431 if !before.is_empty() {
432 label_div = label_div.child(before.to_string());
433 }
434 label_div = label_div.child(div().underline().child(under.to_string()));
435 if !after.is_empty() {
436 label_div = label_div.child(after.to_string());
437 }
438 } else {
439 label_div = label_div.child(label.to_string());
440 }
441
442 div()
443 .id(id)
444 .cursor_pointer()
445 .rounded_md()
446 .px(px(6.0))
447 .py(px(2.0))
448 .flex()
449 .flex_row()
450 .items_center()
451 .gap_1()
452 .hover(|s| s.bg(rgb(0x313244)))
453 .child(check_icon)
454 .child(label_div)
455 .on_mouse_down(
456 MouseButton::Left,
457 cx.listener(move |this, _event: &MouseDownEvent, window, cx| {
458 this.press_search_key(alt_key, false, true, false, Some(window), cx);
459 cx.stop_propagation();
460 }),
461 )
462 }
463
464 fn stepper_button(
466 id: &'static str,
467 label: &'static str,
468 key: KeyCode,
469 cx: &mut Context<Editor>,
470 ) -> Stateful<Div> {
471 div()
472 .id(id)
473 .cursor_pointer()
474 .rounded_md()
475 .px(px(8.0))
476 .py(px(2.0))
477 .bg(rgb(0x313244))
478 .hover(|s| s.bg(rgb(0x45475a)))
479 .text_xs()
480 .font_weight(FontWeight::BOLD)
481 .text_color(rgb(0xcdd6f4))
482 .child(label.to_string())
483 .on_mouse_down(
484 MouseButton::Left,
485 cx.listener(move |this, _event: &MouseDownEvent, window, cx| {
486 this.press_search_key(key, false, false, false, Some(window), cx);
487 cx.stop_propagation();
488 }),
489 )
490 }
491
492 fn replace_toggle_button(expanded: bool, cx: &mut Context<Editor>) -> Stateful<Div> {
494 let (bg, fg, label) = if expanded {
495 (rgb(0x45475a), rgb(0x89b4fa), "▼")
496 } else {
497 (rgb(0x313244), rgb(0x6c7086), "▶")
498 };
499 div()
500 .id("search-toggle-replace")
501 .cursor_pointer()
502 .rounded_md()
503 .w(px(24.0))
504 .h(px(24.0))
505 .flex()
506 .items_center()
507 .justify_center()
508 .bg(bg)
509 .hover(|s| s.bg(rgb(0x45475a)))
510 .text_xs()
511 .font_weight(FontWeight::BOLD)
512 .text_color(fg)
513 .child(label.to_string())
514 .on_mouse_down(
515 MouseButton::Left,
516 cx.listener(move |this, _event: &MouseDownEvent, window, cx| {
517 this.press_search_action(SearchAction::ToggleReplace, Some(window), cx);
518 cx.stop_propagation();
519 }),
520 )
521 }
522
523 fn action_button(
525 id: &'static str,
526 label: &'static str,
527 key: KeyCode,
528 ctrl: bool,
529 alt: bool,
530 cx: &mut Context<Editor>,
531 ) -> Stateful<Div> {
532 div()
533 .id(id)
534 .cursor_pointer()
535 .rounded_md()
536 .border_1()
537 .border_color(rgb(0x45475a))
538 .bg(rgb(0x313244))
539 .hover(|s| s.bg(rgb(0x45475a)).border_color(rgb(0x89b4fa)))
540 .px(px(10.0))
541 .py(px(2.0))
542 .text_xs()
543 .font_weight(FontWeight::BOLD)
544 .text_color(rgb(0xcdd6f4))
545 .child(label.to_string())
546 .on_mouse_down(
547 MouseButton::Left,
548 cx.listener(move |this, _event: &MouseDownEvent, window, cx| {
549 this.press_search_key(key, ctrl, alt, false, Some(window), cx);
550 cx.stop_propagation();
551 }),
552 )
553 }
554
555 fn close_button(cx: &mut Context<Editor>) -> Stateful<Div> {
557 div()
558 .id("search-close")
559 .cursor_pointer()
560 .rounded_md()
561 .px(px(6.0))
562 .py(px(2.0))
563 .text_sm()
564 .text_color(rgb(0x6c7086))
565 .hover(|s| s.text_color(rgb(0xcdd6f4)).bg(rgb(0x313244)))
566 .child("✕")
567 .on_mouse_down(
568 MouseButton::Left,
569 cx.listener(move |this, _event: &MouseDownEvent, window, cx| {
570 this.press_search_key(KeyCode::Escape, false, false, false, Some(window), cx);
571 cx.stop_propagation();
572 }),
573 )
574 }
575
576 fn input_row(prompt: &PromptState) -> Div {
580 let mut row = div().w_full().flex().flex_row().items_center().gap_2();
581
582 let spec = prompt.spec();
583 let prefix = spec.map(|s| s.prefix.as_str()).unwrap_or("");
584 if !prefix.is_empty() {
585 row = row.child(
586 div()
587 .text_sm()
588 .font_weight(FontWeight::BOLD)
589 .text_color(rgb(0xf9e2af))
590 .child(prefix.to_string()),
591 );
592 }
593
594 if prompt.input().is_empty() {
595 let placeholder = spec.map(|s| s.placeholder.as_str()).unwrap_or("");
596 row = row
597 .child(div().w(px(1.5)).h(px(15.0)).bg(rgb(0x89b4fa)).mr(px(2.0)))
598 .child(
599 div()
600 .text_sm()
601 .text_color(rgb(0x6c7086))
602 .child(placeholder.to_string()),
603 );
604 } else {
605 let cursor = prompt.cursor().min(prompt.input().len());
606 let before = prompt.input()[..cursor].to_string();
607 let after = prompt.input()[cursor..].to_string();
608 let mut input_content = div().flex().flex_row().items_center();
609 if !before.is_empty() {
610 input_content =
611 input_content.child(div().text_sm().text_color(rgb(0xcdd6f4)).child(before));
612 }
613 input_content = input_content.child(div().w(px(1.5)).h(px(15.0)).bg(rgb(0x89b4fa)));
614 if !after.is_empty() {
615 input_content =
616 input_content.child(div().text_sm().text_color(rgb(0xcdd6f4)).child(after));
617 }
618 row = row.child(input_content);
619 }
620 row
621 }
622
623 fn item_list(prompt: &PromptState, limit: usize) -> Div {
625 if prompt.items().is_empty() {
626 return div();
627 }
628 let mut list = div().flex().flex_col();
629 for (idx, item) in prompt.items().iter().enumerate().take(limit) {
630 let selected = idx == prompt.selected_index();
631 let mut row = div()
632 .flex()
633 .flex_row()
634 .items_center()
635 .justify_between()
636 .rounded_md()
637 .px(px(8.0))
638 .py(px(2.0));
639 row = if selected { row.bg(rgb(0x45475a)) } else { row };
640 row = row.child(
641 div()
642 .text_sm()
643 .text_color(rgb(0xcdd6f4))
644 .child(item.label.clone()),
645 );
646 if let Some(hint) = &item.hint {
647 row = row.child(
648 div()
649 .text_xs()
650 .text_color(rgb(0x6c7086))
651 .child(hint.clone()),
652 );
653 }
654 list = list.child(row);
655 }
656 list
657 }
658
659 fn message_row(prompt: &PromptState) -> Div {
661 match prompt.message() {
662 Some(message) => div()
663 .text_xs()
664 .text_color(rgb(0xf38ba8))
665 .child(message.to_string()),
666 None => div(),
667 }
668 }
669}