Skip to main content

ui/
pending.rs

1//! [`PendingKeys`] — the prefix of a key sequence gpui is holding, and what
2//! can follow it.
3//!
4//! ```ignore
5//! let pending = cx.new(|cx| PendingKeys::new(window, cx));
6//! div().relative().child(content).child(
7//!     div().absolute().bottom_2().right_2().child(pending.clone()),
8//! )
9//! ```
10
11use 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
24/// Renders nothing while no sequence is pending in its window.
25pub 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    /// Names each continuation's action. The default is the action's name
39    /// without its namespace, split into words: `pane::SplitRight` reads
40    /// `Split right`.
41    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            // A shadowed binding on the same keys cannot fire.
64            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
92/// `pane::SplitRight` → `Split right`.
93fn 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}