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;
Marshall Bowers created
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(+)
@@ -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;
@@ -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)),
+ ))
+ }
+}
@@ -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(),
@@ -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::*;
@@ -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
+ }
+}