1use std::rc::Rc;
12
13use gpui::{
14 Action, Context, IntoElement, KeybindingKeystroke, SharedString, Subscription, Window, div,
15 prelude::*, px,
16};
17
18use theme::{TextStyle, Theme, Typeset};
19
20use crate::{keys, popover, surface::Surfaced as _};
21
22type Label = Rc<dyn Fn(&dyn Action) -> SharedString>;
23
24pub struct PendingKeys {
26 label: Label,
27 _pending: Subscription,
28}
29
30impl PendingKeys {
31 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
32 Self {
33 label: Rc::new(|action| label(action.name())),
34 _pending: cx.observe_pending_input(window, |_, _, cx| cx.notify()),
35 }
36 }
37
38 pub fn with_label(mut self, label: impl Fn(&dyn Action) -> SharedString + 'static) -> Self {
42 self.label = Rc::new(label);
43 self
44 }
45}
46
47impl Render for PendingKeys {
48 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
49 let Some(held) = window.pending_input_keystrokes().map(<[_]>::to_vec) else {
50 return div().into_any_element();
51 };
52 let theme = Theme::of(cx).clone();
53 let prefix = keys::format(
54 &held
55 .iter()
56 .cloned()
57 .map(KeybindingKeystroke::from_keystroke)
58 .collect::<Vec<_>>(),
59 );
60 let mut next: Vec<(SharedString, SharedString)> = Vec::new();
61 for binding in window.possible_bindings_for_input(&held) {
62 let rest = keys::format(&binding.keystrokes()[held.len()..]);
63 if !next.iter().any(|(keys, _)| *keys == rest) {
65 next.push((rest, (self.label)(binding.action())));
66 }
67 }
68 popover::popover_card(&theme)
69 .flex()
70 .flex_col()
71 .gap(px(4.0))
72 .text_style(TextStyle::Callout)
73 .child(
74 div()
75 .text_style(TextStyle::Subheadline)
76 .text_color(theme.text_faint)
77 .child(format!("{prefix} …")),
78 )
79 .children(next.into_iter().map(|(keys, label)| {
80 div()
81 .flex()
82 .flex_row()
83 .gap(px(Theme::SPACE))
84 .child(div().min_w(px(48.0)).child(keys))
85 .child(div().text_color(theme.text_muted).child(label))
86 }))
87 .surface(&theme, theme.popover_surface)
88 .into_any_element()
89 }
90}
91
92fn label(name: &str) -> SharedString {
94 let name = name.rsplit("::").next().unwrap_or(name);
95 let mut out = String::with_capacity(name.len() + 4);
96 for (index, char) in name.char_indices() {
97 if char.is_uppercase() && index > 0 {
98 out.push(' ');
99 out.extend(char.to_lowercase());
100 } else {
101 out.push(char);
102 }
103 }
104 out.into()
105}
106
107#[cfg(test)]
108mod tests {
109 #[test]
110 fn a_name_reads_as_words() {
111 assert_eq!(super::label("pane::SplitRight").as_ref(), "Split right");
112 assert_eq!(super::label("Close").as_ref(), "Close");
113 }
114}