From e5473fc51ac4436c80ac82ce52884d50ff900462 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 9 Oct 2023 11:15:50 -0400 Subject: [PATCH] Add `Palette` component --- crates/storybook2/src/stories/components.rs | 1 + .../src/stories/components/palette.rs | 63 ++++++++ crates/storybook2/src/story_selector.rs | 2 + crates/ui2/src/components.rs | 2 + crates/ui2/src/components/palette.rs | 152 ++++++++++++++++++ 5 files changed, 220 insertions(+) create mode 100644 crates/storybook2/src/stories/components/palette.rs create mode 100644 crates/ui2/src/components/palette.rs diff --git a/crates/storybook2/src/stories/components.rs b/crates/storybook2/src/stories/components.rs index b5afc107a17c786a947bfe0bf9654e5d1cd7dc61..9b62c55921cdae546437e4b27ba5ab2647ec0925 100644 --- a/crates/storybook2/src/stories/components.rs +++ b/crates/storybook2/src/stories/components.rs @@ -5,6 +5,7 @@ pub mod chat_panel; pub mod collab_panel; pub mod facepile; pub mod keybinding; +pub mod palette; pub mod panel; pub mod project_panel; pub mod tab; diff --git a/crates/storybook2/src/stories/components/palette.rs b/crates/storybook2/src/stories/components/palette.rs new file mode 100644 index 0000000000000000000000000000000000000000..1cdbd995a676c4dd8dbffea5c01333d436a998fb --- /dev/null +++ b/crates/storybook2/src/stories/components/palette.rs @@ -0,0 +1,63 @@ +use std::marker::PhantomData; + +use ui::prelude::*; +use ui::{Keybinding, ModifierKeys, Palette, PaletteItem}; + +use crate::story::Story; + +#[derive(Element)] +pub struct PaletteStory { + state_type: PhantomData, +} + +impl PaletteStory { + pub fn new() -> Self { + Self { + state_type: PhantomData, + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + Story::container(cx) + .child(Story::title_for::<_, Palette>(cx)) + .child(Story::label(cx, "Default")) + .child(Palette::new(ScrollState::default())) + .child(Story::label(cx, "With Items")) + .child( + Palette::new(ScrollState::default()) + .placeholder("Execute a command...") + .items(vec![ + PaletteItem::new("theme selector: toggle").keybinding( + Keybinding::new_chord( + ("k".to_string(), ModifierKeys::new().command(true)), + ("t".to_string(), ModifierKeys::new().command(true)), + ), + ), + PaletteItem::new("assistant: inline assist").keybinding(Keybinding::new( + "enter".to_string(), + ModifierKeys::new().command(true), + )), + PaletteItem::new("assistant: quote selection").keybinding(Keybinding::new( + ">".to_string(), + ModifierKeys::new().command(true), + )), + PaletteItem::new("assistant: toggle focus").keybinding(Keybinding::new( + "?".to_string(), + ModifierKeys::new().command(true), + )), + PaletteItem::new("auto update: check"), + PaletteItem::new("auto update: view release notes"), + PaletteItem::new("branches: open recent").keybinding(Keybinding::new( + "b".to_string(), + ModifierKeys::new().command(true).alt(true), + )), + PaletteItem::new("chat panel: toggle focus"), + PaletteItem::new("cli: install"), + PaletteItem::new("client: sign in"), + PaletteItem::new("client: sign out"), + PaletteItem::new("editor: cancel") + .keybinding(Keybinding::new("escape".to_string(), ModifierKeys::new())), + ]), + ) + } +} diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 2bb2d04a7ebfa82a4ff7f60f3bac14ceed2252b0..cb62fceeca1a048fa8393d65538dde07d6e6ef3e 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -43,6 +43,7 @@ pub enum ComponentStory { CollabPanel, Facepile, Keybinding, + Palette, Panel, ProjectPanel, Tab, @@ -68,6 +69,7 @@ impl ComponentStory { 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::Palette => components::palette::PaletteStory::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 a2f54a3055005a1d031bb7c92f32c64e59a0741f..96abcb3838100a600842ea23a2d8f5d0c97d977a 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -8,6 +8,7 @@ mod facepile; mod icon_button; mod keybinding; mod list; +mod palette; mod panel; mod panes; mod player_stack; @@ -31,6 +32,7 @@ pub use facepile::*; pub use icon_button::*; pub use keybinding::*; pub use list::*; +pub use palette::*; pub use panel::*; pub use panes::*; pub use player_stack::*; diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs new file mode 100644 index 0000000000000000000000000000000000000000..7f023b1aef2551deaace78c8b65fc212389d26d5 --- /dev/null +++ b/crates/ui2/src/components/palette.rs @@ -0,0 +1,152 @@ +use std::marker::PhantomData; + +use crate::prelude::*; +use crate::theme::theme; +use crate::{h_stack, v_stack, Keybinding, Label, LabelColor}; + +#[derive(Element)] +pub struct Palette { + state_type: PhantomData, + scroll_state: ScrollState, + input_placeholder: &'static str, + empty_string: &'static str, + items: Vec>, + default_order: OrderMethod, +} + +impl Palette { + pub fn new(scroll_state: ScrollState) -> Self { + Self { + state_type: PhantomData, + scroll_state, + input_placeholder: "Find something...", + empty_string: "No items found.", + items: vec![], + default_order: OrderMethod::default(), + } + } + + pub fn items(mut self, items: Vec>) -> Self { + self.items = items; + self + } + + pub fn placeholder(mut self, input_placeholder: &'static str) -> Self { + self.input_placeholder = input_placeholder; + self + } + + pub fn empty_string(mut self, empty_string: &'static str) -> Self { + self.empty_string = empty_string; + self + } + + // TODO: Hook up sort order + pub fn default_order(mut self, default_order: OrderMethod) -> Self { + self.default_order = default_order; + self + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let theme = theme(cx); + + v_stack() + .w_96() + .rounded_lg() + .fill(theme.lowest.base.default.background) + .border() + .border_color(theme.lowest.base.default.border) + .child( + v_stack() + .gap_px() + .child(v_stack().py_0p5().px_1().child( + div().px_2().py_0p5().child( + Label::new(self.input_placeholder).color(LabelColor::Placeholder), + ), + )) + .child(div().h_px().w_full().fill(theme.lowest.base.default.border)) + .child( + v_stack() + .py_0p5() + .px_1() + .grow() + .max_h_96() + .overflow_y_scroll(self.scroll_state.clone()) + .children( + vec![if self.items.is_empty() { + Some(h_stack().justify_between().px_2().py_1().child( + Label::new(self.empty_string).color(LabelColor::Muted), + )) + } else { + None + }] + .into_iter() + .flatten(), + ) + .children(self.items.iter().map(|item| { + h_stack() + .justify_between() + .px_2() + .py_0p5() + .rounded_lg() + // .hover() + // .fill(theme.lowest.base.hovered.background) + // .active() + // .fill(theme.lowest.base.pressed.background) + .child(item.clone()) + })), + ), + ) + } +} + +#[derive(Element, Clone)] +pub struct PaletteItem { + pub label: &'static str, + pub sublabel: Option<&'static str>, + pub keybinding: Option>, +} + +impl PaletteItem { + pub fn new(label: &'static str) -> Self { + Self { + label, + sublabel: None, + keybinding: None, + } + } + + pub fn label(mut self, label: &'static str) -> Self { + self.label = label; + self + } + + pub fn sublabel>>(mut self, sublabel: L) -> Self { + self.sublabel = sublabel.into(); + self + } + + pub fn keybinding(mut self, keybinding: K) -> Self + where + K: Into>>, + { + self.keybinding = keybinding.into(); + self + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let theme = theme(cx); + + div() + .flex() + .flex_row() + .grow() + .justify_between() + .child( + v_stack() + .child(Label::new(self.label)) + .children(self.sublabel.map(|sublabel| Label::new(sublabel))), + ) + .children(self.keybinding.clone()) + } +}