Add `Keybinding` component

Marshall Bowers created

Change summary

crates/storybook2/src/stories/components.rs            |   1 
crates/storybook2/src/stories/components/keybinding.rs |  74 +++++
crates/storybook2/src/story_selector.rs                |   2 
crates/ui2/src/components.rs                           |   2 
crates/ui2/src/components/keybinding.rs                | 167 ++++++++++++
5 files changed, 246 insertions(+)

Detailed changes

crates/storybook2/src/stories/components/keybinding.rs 🔗

@@ -0,0 +1,74 @@
+use std::marker::PhantomData;
+
+use itertools::Itertools;
+use strum::IntoEnumIterator;
+use ui::prelude::*;
+use ui::{Keybinding, ModifierKey, ModifierKeys};
+
+use crate::story::Story;
+
+#[derive(Element)]
+pub struct KeybindingStory<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> KeybindingStory<S> {
+    pub fn new() -> Self {
+        Self {
+            state_type: PhantomData,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let all_modifier_permutations = ModifierKey::iter().permutations(2);
+
+        Story::container(cx)
+            .child(Story::title_for::<_, Keybinding<S>>(cx))
+            .child(Story::label(cx, "Single Key"))
+            .child(Keybinding::new("Z".to_string(), ModifierKeys::new()))
+            .child(Story::label(cx, "Single Key with Modifier"))
+            .child(
+                div()
+                    .flex()
+                    .gap_3()
+                    .children(ModifierKey::iter().map(|modifier| {
+                        Keybinding::new("C".to_string(), ModifierKeys::new().add(modifier))
+                    })),
+            )
+            .child(Story::label(cx, "Single Key with Modifier (Permuted)"))
+            .child(
+                div().flex().flex_col().children(
+                    all_modifier_permutations
+                        .chunks(4)
+                        .into_iter()
+                        .map(|chunk| {
+                            div()
+                                .flex()
+                                .gap_4()
+                                .py_3()
+                                .children(chunk.map(|permutation| {
+                                    let mut modifiers = ModifierKeys::new();
+
+                                    for modifier in permutation {
+                                        modifiers = modifiers.add(modifier);
+                                    }
+
+                                    Keybinding::new("X".to_string(), modifiers)
+                                }))
+                        }),
+                ),
+            )
+            .child(Story::label(cx, "Single Key with All Modifiers"))
+            .child(Keybinding::new("Z".to_string(), ModifierKeys::all()))
+            .child(Story::label(cx, "Chord"))
+            .child(Keybinding::new_chord(
+                ("A".to_string(), ModifierKeys::new()),
+                ("Z".to_string(), ModifierKeys::new()),
+            ))
+            .child(Story::label(cx, "Chord with Modifier"))
+            .child(Keybinding::new_chord(
+                ("A".to_string(), ModifierKeys::new().control(true)),
+                ("Z".to_string(), ModifierKeys::new().shift(true)),
+            ))
+    }
+}

crates/storybook2/src/story_selector.rs 🔗

@@ -42,6 +42,7 @@ pub enum ComponentStory {
     ChatPanel,
     CollabPanel,
     Facepile,
+    Keybinding,
     Panel,
     ProjectPanel,
     Tab,
@@ -66,6 +67,7 @@ impl ComponentStory {
             Self::ChatPanel => components::chat_panel::ChatPanelStory::new().into_any(),
             Self::CollabPanel => components::collab_panel::CollabPanelStory::new().into_any(),
             Self::Facepile => components::facepile::FacepileStory::new().into_any(),
+            Self::Keybinding => components::keybinding::KeybindingStory::new().into_any(),
             Self::Panel => components::panel::PanelStory::new().into_any(),
             Self::ProjectPanel => components::project_panel::ProjectPanelStory::new().into_any(),
             Self::Tab => components::tab::TabStory::new().into_any(),

crates/ui2/src/components.rs 🔗

@@ -6,6 +6,7 @@ mod collab_panel;
 mod editor_pane;
 mod facepile;
 mod icon_button;
+mod keybinding;
 mod list;
 mod panel;
 mod panes;
@@ -28,6 +29,7 @@ pub use collab_panel::*;
 pub use editor_pane::*;
 pub use facepile::*;
 pub use icon_button::*;
+pub use keybinding::*;
 pub use list::*;
 pub use panel::*;
 pub use panes::*;

crates/ui2/src/components/keybinding.rs 🔗

@@ -0,0 +1,167 @@
+use std::collections::HashSet;
+use std::marker::PhantomData;
+
+use strum::{EnumIter, IntoEnumIterator};
+
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Element, Clone)]
+pub struct Keybinding<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+
+    /// A keybinding consists of a key and a set of modifier keys.
+    /// More then one keybinding produces a chord.
+    ///
+    /// This should always contain at least one element.
+    keybinding: Vec<(String, ModifierKeys)>,
+}
+
+impl<S: 'static + Send + Sync + Clone> Keybinding<S> {
+    pub fn new(key: String, modifiers: ModifierKeys) -> Self {
+        Self {
+            state_type: PhantomData,
+            keybinding: vec![(key, modifiers)],
+        }
+    }
+
+    pub fn new_chord(
+        first_note: (String, ModifierKeys),
+        second_note: (String, ModifierKeys),
+    ) -> Self {
+        Self {
+            state_type: PhantomData,
+            keybinding: vec![first_note, second_note],
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        div()
+            .flex()
+            .gap_2()
+            .children(self.keybinding.iter().map(|(key, modifiers)| {
+                div()
+                    .flex()
+                    .gap_1()
+                    .children(ModifierKey::iter().filter_map(|modifier| {
+                        if modifiers.0.contains(&modifier) {
+                            Some(Key::new(modifier.glyph()))
+                        } else {
+                            None
+                        }
+                    }))
+                    .child(Key::new(key.clone()))
+            }))
+    }
+}
+
+#[derive(Element)]
+pub struct Key<S: 'static + Send + Sync> {
+    state_type: PhantomData<S>,
+    key: String,
+}
+
+impl<S: 'static + Send + Sync> Key<S> {
+    pub fn new<K>(key: K) -> Self
+    where
+        K: Into<String>,
+    {
+        Self {
+            state_type: PhantomData,
+            key: key.into(),
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let theme = theme(cx);
+
+        div()
+            .px_2()
+            .py_0()
+            .rounded_md()
+            .text_sm()
+            .text_color(theme.lowest.on.default.foreground)
+            .fill(theme.lowest.on.default.background)
+            .child(self.key.clone())
+    }
+}
+
+// NOTE: The order the modifier keys appear in this enum impacts the order in
+// which they are rendered in the UI.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum ModifierKey {
+    Control,
+    Alt,
+    Command,
+    Shift,
+}
+
+impl ModifierKey {
+    /// Returns the glyph for the [`ModifierKey`].
+    pub fn glyph(&self) -> char {
+        match self {
+            Self::Control => '^',
+            Self::Alt => '⌥',
+            Self::Command => '⌘',
+            Self::Shift => '⇧',
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct ModifierKeys(HashSet<ModifierKey>);
+
+impl ModifierKeys {
+    pub fn new() -> Self {
+        Self(HashSet::new())
+    }
+
+    pub fn all() -> Self {
+        Self(HashSet::from_iter(ModifierKey::iter()))
+    }
+
+    pub fn add(mut self, modifier: ModifierKey) -> Self {
+        self.0.insert(modifier);
+        self
+    }
+
+    pub fn control(mut self, control: bool) -> Self {
+        if control {
+            self.0.insert(ModifierKey::Control);
+        } else {
+            self.0.remove(&ModifierKey::Control);
+        }
+
+        self
+    }
+
+    pub fn alt(mut self, alt: bool) -> Self {
+        if alt {
+            self.0.insert(ModifierKey::Alt);
+        } else {
+            self.0.remove(&ModifierKey::Alt);
+        }
+
+        self
+    }
+
+    pub fn command(mut self, command: bool) -> Self {
+        if command {
+            self.0.insert(ModifierKey::Command);
+        } else {
+            self.0.remove(&ModifierKey::Command);
+        }
+
+        self
+    }
+
+    pub fn shift(mut self, shift: bool) -> Self {
+        if shift {
+            self.0.insert(ModifierKey::Shift);
+        } else {
+            self.0.remove(&ModifierKey::Shift);
+        }
+
+        self
+    }
+}