From dc2ddfb42c93da314206275c0ed4feea70de6c61 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 9 Oct 2023 11:09:44 -0400 Subject: [PATCH] Add `Keybinding` component --- crates/storybook2/src/stories/components.rs | 1 + .../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(+) create mode 100644 crates/storybook2/src/stories/components/keybinding.rs create mode 100644 crates/ui2/src/components/keybinding.rs diff --git a/crates/storybook2/src/stories/components.rs b/crates/storybook2/src/stories/components.rs index 1b4797bc8ec88337095098d9e2f90224c900eb6c..b5afc107a17c786a947bfe0bf9654e5d1cd7dc61 100644 --- a/crates/storybook2/src/stories/components.rs +++ b/crates/storybook2/src/stories/components.rs @@ -4,6 +4,7 @@ pub mod buffer; pub mod chat_panel; pub mod collab_panel; pub mod facepile; +pub mod keybinding; pub mod panel; pub mod project_panel; pub mod tab; diff --git a/crates/storybook2/src/stories/components/keybinding.rs b/crates/storybook2/src/stories/components/keybinding.rs new file mode 100644 index 0000000000000000000000000000000000000000..434a587cfcfcc2927b30feb003884551d822ec1c --- /dev/null +++ b/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 { + state_type: PhantomData, +} + +impl KeybindingStory { + pub fn new() -> Self { + Self { + state_type: PhantomData, + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let all_modifier_permutations = ModifierKey::iter().permutations(2); + + Story::container(cx) + .child(Story::title_for::<_, Keybinding>(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)), + )) + } +} diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index b59febd974999ca756fe297d77f230527725d85a..2bb2d04a7ebfa82a4ff7f60f3bac14ceed2252b0 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/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(), diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 0b9e3cd74d9974192be77179601929bb452c3cb4..a2f54a3055005a1d031bb7c92f32c64e59a0741f 100644 --- a/crates/ui2/src/components.rs +++ b/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::*; diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs new file mode 100644 index 0000000000000000000000000000000000000000..e516ad177f3d77d7e0417d349eb7877675860dc0 --- /dev/null +++ b/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 { + state_type: PhantomData, + + /// 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 Keybinding { + 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) -> impl Element { + 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 { + state_type: PhantomData, + key: String, +} + +impl Key { + pub fn new(key: K) -> Self + where + K: Into, + { + Self { + state_type: PhantomData, + key: key.into(), + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + 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); + +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 + } +}