keybinding.rs

  1use gpui::{actions, relative, rems, Action, Styled};
  2use strum::EnumIter;
  3
  4use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
  5
  6#[derive(Component, Clone)]
  7pub struct KeyBinding {
  8    /// A keybinding consists of a key and a set of modifier keys.
  9    /// More then one keybinding produces a chord.
 10    ///
 11    /// This should always contain at least one element.
 12    key_binding: gpui::KeyBinding,
 13}
 14
 15impl KeyBinding {
 16    pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<Self> {
 17        // todo! this last is arbitrary, we want to prefer users key bindings over defaults,
 18        // and vim over normal (in vim mode), etc.
 19        let key_binding = cx.bindings_for_action(action).last().cloned()?;
 20        Some(Self::new(key_binding))
 21    }
 22
 23    pub fn new(key_binding: gpui::KeyBinding) -> Self {
 24        Self { key_binding }
 25    }
 26
 27    fn icon_for_key(key: &str) -> Option<Icon> {
 28        match key {
 29            "left" => Some(Icon::ArrowLeft),
 30            "right" => Some(Icon::ArrowRight),
 31            "up" => Some(Icon::ArrowUp),
 32            "down" => Some(Icon::ArrowDown),
 33            _ => None,
 34        }
 35    }
 36
 37    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
 38        h_stack()
 39            .flex_none()
 40            .gap_1()
 41            .children(self.key_binding.keystrokes().iter().map(|keystroke| {
 42                let key_icon = Self::icon_for_key(&keystroke.key);
 43
 44                h_stack()
 45                    .flex_none()
 46                    .gap_0p5()
 47                    .bg(cx.theme().colors().element_background)
 48                    .p_0p5()
 49                    .rounded_sm()
 50                    .when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
 51                    .when(keystroke.modifiers.control, |el| {
 52                        el.child(KeyIcon::new(Icon::Control))
 53                    })
 54                    .when(keystroke.modifiers.alt, |el| {
 55                        el.child(KeyIcon::new(Icon::Option))
 56                    })
 57                    .when(keystroke.modifiers.command, |el| {
 58                        el.child(KeyIcon::new(Icon::Command))
 59                    })
 60                    .when(keystroke.modifiers.shift, |el| {
 61                        el.child(KeyIcon::new(Icon::Shift))
 62                    })
 63                    .when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon)))
 64                    .when(key_icon.is_none(), |el| {
 65                        el.child(Key::new(keystroke.key.to_uppercase().clone()))
 66                    })
 67            }))
 68    }
 69}
 70
 71#[derive(Component)]
 72pub struct Key {
 73    key: SharedString,
 74}
 75
 76impl Key {
 77    pub fn new(key: impl Into<SharedString>) -> Self {
 78        Self { key: key.into() }
 79    }
 80
 81    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
 82        let single_char = self.key.len() == 1;
 83
 84        div()
 85            // .px_0p5()
 86            .py_0()
 87            .when(single_char, |el| {
 88                el.w(rems(14. / 16.)).flex().flex_none().justify_center()
 89            })
 90            .when(!single_char, |el| el.px_0p5())
 91            .h(rems(14. / 16.))
 92            // .rounded_md()
 93            .text_ui()
 94            .line_height(relative(1.))
 95            .text_color(cx.theme().colors().text)
 96            // .bg(cx.theme().colors().element_background)
 97            .child(self.key.clone())
 98    }
 99}
100
101#[derive(Component)]
102pub struct KeyIcon {
103    icon: Icon,
104}
105
106impl KeyIcon {
107    pub fn new(icon: Icon) -> Self {
108        Self { icon }
109    }
110
111    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
112        div()
113            .w(rems(14. / 16.))
114            // .bg(cx.theme().colors().element_background)
115            .child(IconElement::new(self.icon).size(IconSize::Small))
116    }
117}
118
119// NOTE: The order the modifier keys appear in this enum impacts the order in
120// which they are rendered in the UI.
121#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
122pub enum ModifierKey {
123    Control,
124    Alt, // Option
125    Shift,
126    Command,
127}
128
129actions!(NoAction);
130
131pub fn binding(key: &str) -> gpui::KeyBinding {
132    gpui::KeyBinding::new(key, NoAction {}, None)
133}
134
135#[cfg(feature = "stories")]
136pub use stories::*;
137
138#[cfg(feature = "stories")]
139mod stories {
140    use super::*;
141    pub use crate::KeyBinding;
142    use crate::{binding, Story};
143    use gpui::{Div, Render};
144    use itertools::Itertools;
145    pub struct KeybindingStory;
146
147    impl Render for KeybindingStory {
148        type Element = Div<Self>;
149
150        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
151            let all_modifier_permutations =
152                ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
153
154            Story::container(cx)
155                .child(Story::title_for::<_, KeyBinding>(cx))
156                .child(Story::label(cx, "Single Key"))
157                .child(KeyBinding::new(binding("Z")))
158                .child(Story::label(cx, "Single Key with Modifier"))
159                .child(
160                    div()
161                        .flex()
162                        .gap_3()
163                        .child(KeyBinding::new(binding("ctrl-c")))
164                        .child(KeyBinding::new(binding("alt-c")))
165                        .child(KeyBinding::new(binding("cmd-c")))
166                        .child(KeyBinding::new(binding("shift-c"))),
167                )
168                .child(Story::label(cx, "Single Key with Modifier (Permuted)"))
169                .child(
170                    div().flex().flex_col().children(
171                        all_modifier_permutations
172                            .chunks(4)
173                            .into_iter()
174                            .map(|chunk| {
175                                div()
176                                    .flex()
177                                    .gap_4()
178                                    .py_3()
179                                    .children(chunk.map(|permutation| {
180                                        KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
181                                    }))
182                            }),
183                    ),
184                )
185                .child(Story::label(cx, "Single Key with All Modifiers"))
186                .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
187                .child(Story::label(cx, "Chord"))
188                .child(KeyBinding::new(binding("a z")))
189                .child(Story::label(cx, "Chord with Modifier"))
190                .child(KeyBinding::new(binding("ctrl-a shift-z")))
191                .child(KeyBinding::new(binding("fn-s")))
192        }
193    }
194}