diff --git a/crates/gpui3/src/color.rs b/crates/gpui3/src/color.rs index 1ace030482..d05caba1a6 100644 --- a/crates/gpui3/src/color.rs +++ b/crates/gpui3/src/color.rs @@ -4,7 +4,7 @@ use serde::de::{self, Deserialize, Deserializer, Visitor}; use std::fmt; use std::num::ParseIntError; -pub fn rgb(hex: u32) -> Rgba { +pub fn rgb>(hex: u32) -> C { let r = ((hex >> 16) & 0xFF) as f32 / 255.0; let g = ((hex >> 8) & 0xFF) as f32 / 255.0; let b = (hex & 0xFF) as f32 / 255.0; diff --git a/crates/storybook2/src/stories/components/panel.rs b/crates/storybook2/src/stories/components/panel.rs index 15b900c5ff..ed317115fb 100644 --- a/crates/storybook2/src/stories/components/panel.rs +++ b/crates/storybook2/src/stories/components/panel.rs @@ -6,11 +6,11 @@ use crate::ui::{Label, Panel}; use crate::story::Story; #[derive(Element)] -pub struct PanelStory { +pub struct PanelStory { state_type: PhantomData, } -impl PanelStory { +impl PanelStory { pub fn new() -> Self { Self { state_type: PhantomData, diff --git a/crates/storybook2/src/stories/elements/label.rs b/crates/storybook2/src/stories/elements/label.rs index 5255b35286..f4f5649f16 100644 --- a/crates/storybook2/src/stories/elements/label.rs +++ b/crates/storybook2/src/stories/elements/label.rs @@ -6,11 +6,11 @@ use crate::ui::Label; use crate::story::Story; #[derive(Element)] -pub struct LabelStory { +pub struct LabelStory { state_type: PhantomData, } -impl LabelStory { +impl LabelStory { pub fn new() -> Self { Self { state_type: PhantomData, diff --git a/crates/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs index a944dcded3..c6cc4adcb1 100644 --- a/crates/storybook2/src/stories/kitchen_sink.rs +++ b/crates/storybook2/src/stories/kitchen_sink.rs @@ -7,11 +7,11 @@ use crate::story_selector::{ComponentStory, ElementStory}; use crate::ui::prelude::*; #[derive(Element)] -pub struct KitchenSinkStory { +pub struct KitchenSinkStory { state_type: PhantomData, } -impl KitchenSinkStory { +impl KitchenSinkStory { pub fn new() -> Self { Self { state_type: PhantomData, diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index ac3d359da3..289a98c698 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -18,7 +18,7 @@ pub enum ElementStory { } impl ElementStory { - pub fn story(&self) -> AnyElement { + pub fn story(&self) -> AnyElement { use crate::stories::elements; match self { @@ -36,7 +36,7 @@ pub enum ComponentStory { } impl ComponentStory { - pub fn story(&self) -> AnyElement { + pub fn story(&self) -> AnyElement { use crate::stories::components; match self { @@ -81,7 +81,7 @@ impl FromStr for StorySelector { } impl StorySelector { - pub fn story(&self) -> AnyElement { + pub fn story(&self) -> AnyElement { match self { Self::Element(element_story) => element_story.story(), Self::Component(component_story) => component_story.story(), diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 16a3e04fe0..de48103366 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -91,6 +91,7 @@ fn main() { }); } +#[derive(Clone)] pub struct StoryWrapper { selector: StorySelector, } diff --git a/crates/storybook2/src/ui/components.rs b/crates/storybook2/src/ui/components.rs index ef2e41f583..90121daf36 100644 --- a/crates/storybook2/src/ui/components.rs +++ b/crates/storybook2/src/ui/components.rs @@ -1,3 +1,5 @@ +mod list; mod panel; +pub use list::*; pub use panel::*; diff --git a/crates/storybook2/src/ui/components/list.rs b/crates/storybook2/src/ui/components/list.rs new file mode 100644 index 0000000000..a491045757 --- /dev/null +++ b/crates/storybook2/src/ui/components/list.rs @@ -0,0 +1,519 @@ +use std::marker::PhantomData; + +use gpui3::{div, Div, Hsla, WindowContext}; + +use crate::theme::theme; +use crate::ui::prelude::*; +use crate::ui::{ + h_stack, token, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, LabelColor, + LabelSize, +}; + +#[derive(Clone, Copy, Default, Debug, PartialEq)] +pub enum ListItemVariant { + /// The list item extends to the far left and right of the list. + #[default] + FullWidth, + Inset, +} + +#[derive(Element, Clone)] +pub struct ListHeader { + state_type: PhantomData, + label: &'static str, + left_icon: Option, + variant: ListItemVariant, + state: InteractionState, + toggleable: Toggleable, +} + +impl ListHeader { + pub fn new(label: &'static str) -> Self { + Self { + state_type: PhantomData, + label, + left_icon: None, + variant: ListItemVariant::default(), + state: InteractionState::default(), + toggleable: Toggleable::default(), + } + } + + pub fn set_toggle(mut self, toggle: ToggleState) -> Self { + self.toggleable = toggle.into(); + self + } + + pub fn set_toggleable(mut self, toggleable: Toggleable) -> Self { + self.toggleable = toggleable; + self + } + + pub fn left_icon(mut self, left_icon: Option) -> Self { + self.left_icon = left_icon; + self + } + + pub fn state(mut self, state: InteractionState) -> Self { + self.state = state; + self + } + + fn disclosure_control(&self) -> Div { + let is_toggleable = self.toggleable != Toggleable::NotToggleable; + let is_toggled = Toggleable::is_toggled(&self.toggleable); + + match (is_toggleable, is_toggled) { + (false, _) => div(), + (_, true) => div().child(IconElement::new(Icon::ChevronRight).color(IconColor::Muted)), + (_, false) => div().child(IconElement::new(Icon::ChevronDown).size(IconSize::Small)), + } + } + + fn background_color(&self, cx: &WindowContext) -> Hsla { + let theme = theme(cx); + let system_color = SystemColor::new(); + + match self.state { + InteractionState::Hovered => theme.lowest.base.hovered.background, + InteractionState::Active => theme.lowest.base.pressed.background, + InteractionState::Enabled => theme.lowest.on.default.background, + _ => system_color.transparent, + } + } + + fn label_color(&self) -> LabelColor { + match self.state { + InteractionState::Disabled => LabelColor::Disabled, + _ => Default::default(), + } + } + + fn icon_color(&self) -> IconColor { + match self.state { + InteractionState::Disabled => IconColor::Disabled, + _ => Default::default(), + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let theme = theme(cx); + let token = token(); + let system_color = SystemColor::new(); + let background_color = self.background_color(cx); + + let is_toggleable = self.toggleable != Toggleable::NotToggleable; + let is_toggled = Toggleable::is_toggled(&self.toggleable); + + let disclosure_control = self.disclosure_control(); + + h_stack() + .flex_1() + .w_full() + .fill(background_color) + // .when(self.state == InteractionState::Focused, |this| { + // this.border() + // .border_color(theme.lowest.accent.default.border) + // }) + .relative() + .py_1() + .child( + div() + .h_6() + // .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) + .flex() + .flex_1() + .w_full() + .gap_1() + .items_center() + .justify_between() + .child( + div() + .flex() + .gap_1() + .items_center() + .children(self.left_icon.map(|i| { + IconElement::new(i) + .color(IconColor::Muted) + .size(IconSize::Small) + })) + .child( + Label::new(self.label.clone()) + .color(LabelColor::Muted) + .size(LabelSize::Small), + ), + ) + .child(disclosure_control), + ) + } +} + +#[derive(Element, Clone)] +pub struct ListSubHeader { + state_type: PhantomData, + label: &'static str, + left_icon: Option, + variant: ListItemVariant, +} + +impl ListSubHeader { + pub fn new(label: &'static str) -> Self { + Self { + state_type: PhantomData, + label, + left_icon: None, + variant: ListItemVariant::default(), + } + } + + pub fn left_icon(mut self, left_icon: Option) -> Self { + self.left_icon = left_icon; + self + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let theme = theme(cx); + let token = token(); + + h_stack().flex_1().w_full().relative().py_1().child( + div() + .h_6() + // .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) + .flex() + .flex_1() + .w_full() + .gap_1() + .items_center() + .justify_between() + .child( + div() + .flex() + .gap_1() + .items_center() + .children(self.left_icon.map(|i| { + IconElement::new(i) + .color(IconColor::Muted) + .size(IconSize::Small) + })) + .child( + Label::new(self.label.clone()) + .color(LabelColor::Muted) + .size(LabelSize::Small), + ), + ), + ) + } +} + +#[derive(Clone)] +pub enum LeftContent { + Icon(Icon), + Avatar(&'static str), +} + +#[derive(Default, PartialEq, Copy, Clone)] +pub enum ListEntrySize { + #[default] + Small, + Medium, +} + +#[derive(Clone, Element)] +pub enum ListItem { + Entry(ListEntry), + Separator(ListSeparator), + Header(ListSubHeader), +} + +impl From> for ListItem { + fn from(entry: ListEntry) -> Self { + Self::Entry(entry) + } +} + +impl From> for ListItem { + fn from(entry: ListSeparator) -> Self { + Self::Separator(entry) + } +} + +impl From> for ListItem { + fn from(entry: ListSubHeader) -> Self { + Self::Header(entry) + } +} + +impl ListItem { + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + match self { + ListItem::Entry(entry) => div().child(entry.render(cx)), + ListItem::Separator(separator) => div().child(separator.render(cx)), + ListItem::Header(header) => div().child(header.render(cx)), + } + } + + pub fn new(label: Label) -> Self { + Self::Entry(ListEntry::new(label)) + } + + pub fn as_entry(&mut self) -> Option<&mut ListEntry> { + if let Self::Entry(entry) = self { + Some(entry) + } else { + None + } + } +} + +#[derive(Element, Clone)] +pub struct ListEntry { + disclosure_control_style: DisclosureControlVisibility, + indent_level: u32, + label: Label, + left_content: Option, + variant: ListItemVariant, + size: ListEntrySize, + state: InteractionState, + toggle: Option, +} + +impl ListEntry { + pub fn new(label: Label) -> Self { + Self { + disclosure_control_style: DisclosureControlVisibility::default(), + indent_level: 0, + label, + variant: ListItemVariant::default(), + left_content: None, + size: ListEntrySize::default(), + state: InteractionState::default(), + toggle: None, + } + } + pub fn variant(mut self, variant: ListItemVariant) -> Self { + self.variant = variant; + self + } + pub fn indent_level(mut self, indent_level: u32) -> Self { + self.indent_level = indent_level; + self + } + + pub fn set_toggle(mut self, toggle: ToggleState) -> Self { + self.toggle = Some(toggle); + self + } + + pub fn left_content(mut self, left_content: LeftContent) -> Self { + self.left_content = Some(left_content); + self + } + + pub fn left_icon(mut self, left_icon: Icon) -> Self { + self.left_content = Some(LeftContent::Icon(left_icon)); + self + } + + pub fn left_avatar(mut self, left_avatar: &'static str) -> Self { + self.left_content = Some(LeftContent::Avatar(left_avatar)); + self + } + + pub fn state(mut self, state: InteractionState) -> Self { + self.state = state; + self + } + + pub fn size(mut self, size: ListEntrySize) -> Self { + self.size = size; + self + } + + pub fn disclosure_control_style( + mut self, + disclosure_control_style: DisclosureControlVisibility, + ) -> Self { + self.disclosure_control_style = disclosure_control_style; + self + } + + fn background_color(&self, cx: &WindowContext) -> Hsla { + let theme = theme(cx); + let system_color = SystemColor::new(); + + match self.state { + InteractionState::Hovered => theme.lowest.base.hovered.background, + InteractionState::Active => theme.lowest.base.pressed.background, + InteractionState::Enabled => theme.lowest.on.default.background, + _ => system_color.transparent, + } + } + + fn label_color(&self) -> LabelColor { + match self.state { + InteractionState::Disabled => LabelColor::Disabled, + _ => Default::default(), + } + } + + fn icon_color(&self) -> IconColor { + match self.state { + InteractionState::Disabled => IconColor::Disabled, + _ => Default::default(), + } + } + + fn disclosure_control(&mut self, cx: &mut ViewContext) -> Option> { + let theme = theme(cx); + let token = token(); + + let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle { + IconElement::new(Icon::ChevronDown) + } else { + IconElement::new(Icon::ChevronRight) + } + .color(IconColor::Muted) + .size(IconSize::Small); + + match (self.toggle, self.disclosure_control_style) { + (Some(_), DisclosureControlVisibility::OnHover) => { + Some( + div() + .absolute() + // .neg_left_5() + .child(disclosure_control_icon), + ) + } + (Some(_), DisclosureControlVisibility::Always) => { + Some(div().child(disclosure_control_icon)) + } + (None, _) => None, + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let theme = theme(cx); + let token = token(); + let system_color = SystemColor::new(); + let background_color = self.background_color(cx); + + let left_content = match self.left_content { + Some(LeftContent::Icon(i)) => { + Some(h_stack().child(IconElement::new(i).size(IconSize::Small))) + } + Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))), + None => None, + }; + + let sized_item = match self.size { + ListEntrySize::Small => div().h_6(), + ListEntrySize::Medium => div().h_7(), + }; + + div() + .fill(background_color) + // .when(self.state == InteractionState::Focused, |this| { + // this.border() + // .border_color(theme.lowest.accent.default.border) + // }) + .relative() + .py_1() + .child( + sized_item + // .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) + // .ml(rems(0.75 * self.indent_level as f32)) + .children((0..self.indent_level).map(|_| { + div() + // .w(token.list_indent_depth) + .h_full() + .flex() + .justify_center() + .child(h_stack().child(div().w_px().h_full()).child( + div().w_px().h_full().fill(theme.middle.base.default.border), + )) + })) + .flex() + .gap_1() + .items_center() + .relative() + .children(self.disclosure_control(cx)) + .children(left_content) + .child(self.label.clone()), + ) + } +} + +#[derive(Clone, Element)] +pub struct ListSeparator { + state_type: PhantomData, +} + +impl ListSeparator { + pub fn new() -> Self { + Self { + state_type: PhantomData, + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let theme = theme(cx); + + div().h_px().w_full().fill(theme.lowest.base.default.border) + } +} + +#[derive(Element)] +pub struct List { + items: Vec>, + empty_message: &'static str, + header: Option>, + toggleable: Toggleable, +} + +impl List { + pub fn new(items: Vec>) -> Self { + Self { + items, + empty_message: "No items", + header: None, + toggleable: Toggleable::default(), + } + } + + pub fn empty_message(mut self, empty_message: &'static str) -> Self { + self.empty_message = empty_message; + self + } + + pub fn header(mut self, header: ListHeader) -> Self { + self.header = Some(header); + self + } + + pub fn set_toggle(mut self, toggle: ToggleState) -> Self { + self.toggleable = toggle.into(); + self + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let theme = theme(cx); + let token = token(); + let is_toggleable = self.toggleable != Toggleable::NotToggleable; + let is_toggled = Toggleable::is_toggled(&self.toggleable); + + let list_content = match (self.items.is_empty(), is_toggled) { + (_, false) => div(), + (false, _) => div().children(self.items.iter().cloned()), + (true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)), + }; + + v_stack() + .py_1() + .children( + self.header + .clone() + .map(|header| header.set_toggleable(self.toggleable)), + ) + .child(list_content) + } +} diff --git a/crates/storybook2/src/ui/elements/label.rs b/crates/storybook2/src/ui/elements/label.rs index c2d5a7422b..d87587a529 100644 --- a/crates/storybook2/src/ui/elements/label.rs +++ b/crates/storybook2/src/ui/elements/label.rs @@ -46,7 +46,7 @@ pub enum LabelSize { } #[derive(Element, Clone)] -pub struct Label { +pub struct Label { state_type: PhantomData, label: String, color: LabelColor, @@ -55,7 +55,7 @@ pub struct Label { strikethrough: bool, } -impl Label { +impl Label { pub fn new(label: L) -> Self where L: Into, diff --git a/crates/storybook2/src/ui/prelude.rs b/crates/storybook2/src/ui/prelude.rs index 65b2f50647..a2ab86d372 100644 --- a/crates/storybook2/src/ui/prelude.rs +++ b/crates/storybook2/src/ui/prelude.rs @@ -1,14 +1,257 @@ pub use gpui3::{ div, Element, IntoAnyElement, ParentElement, ScrollState, StyleHelpers, ViewContext, + WindowContext, }; pub use crate::ui::{HackyChildren, HackyChildrenPayload}; +use gpui3::{hsla, rgb, Hsla}; use strum::EnumIter; +use crate::theme::{theme, Theme}; + +#[derive(Default)] +pub struct SystemColor { + pub transparent: Hsla, + pub mac_os_traffic_light_red: Hsla, + pub mac_os_traffic_light_yellow: Hsla, + pub mac_os_traffic_light_green: Hsla, +} + +impl SystemColor { + pub fn new() -> SystemColor { + SystemColor { + transparent: hsla(0.0, 0.0, 0.0, 0.0), + mac_os_traffic_light_red: rgb::(0xEC695E), + mac_os_traffic_light_yellow: rgb::(0xF4BF4F), + mac_os_traffic_light_green: rgb::(0x62C554), + } + } + pub fn color(&self) -> Hsla { + self.transparent + } +} + +#[derive(Default, PartialEq, EnumIter, Clone, Copy)] +pub enum HighlightColor { + #[default] + Default, + Comment, + String, + Function, + Keyword, +} + +impl HighlightColor { + pub fn hsla(&self, theme: &Theme) -> Hsla { + let system_color = SystemColor::new(); + + match self { + Self::Default => theme + .syntax + .get("primary") + .expect("no theme.syntax.primary") + .clone(), + Self::Comment => theme + .syntax + .get("comment") + .expect("no theme.syntax.comment") + .clone(), + Self::String => theme + .syntax + .get("string") + .expect("no theme.syntax.string") + .clone(), + Self::Function => theme + .syntax + .get("function") + .expect("no theme.syntax.function") + .clone(), + Self::Keyword => theme + .syntax + .get("keyword") + .expect("no theme.syntax.keyword") + .clone(), + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] +pub enum FileSystemStatus { + #[default] + None, + Conflict, + Deleted, +} + +impl FileSystemStatus { + pub fn to_string(&self) -> String { + match self { + Self::None => "None".to_string(), + Self::Conflict => "Conflict".to_string(), + Self::Deleted => "Deleted".to_string(), + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] +pub enum GitStatus { + #[default] + None, + Created, + Modified, + Deleted, + Conflict, + Renamed, +} + +impl GitStatus { + pub fn to_string(&self) -> String { + match self { + Self::None => "None".to_string(), + Self::Created => "Created".to_string(), + Self::Modified => "Modified".to_string(), + Self::Deleted => "Deleted".to_string(), + Self::Conflict => "Conflict".to_string(), + Self::Renamed => "Renamed".to_string(), + } + } + + pub fn hsla(&self, cx: &WindowContext) -> Hsla { + let theme = theme(cx); + let system_color = SystemColor::new(); + + match self { + Self::None => system_color.transparent, + Self::Created => theme.lowest.positive.default.foreground, + Self::Modified => theme.lowest.warning.default.foreground, + Self::Deleted => theme.lowest.negative.default.foreground, + Self::Conflict => theme.lowest.warning.default.foreground, + Self::Renamed => theme.lowest.accent.default.foreground, + } + } +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] +pub enum DiagnosticStatus { + #[default] + None, + Error, + Warning, + Info, +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] +pub enum IconSide { + #[default] + Left, + Right, +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] +pub enum OrderMethod { + #[default] + Ascending, + Descending, + MostRecent, +} + #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum Shape { #[default] Circle, RoundedRectangle, } + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] +pub enum DisclosureControlVisibility { + #[default] + OnHover, + Always, +} + +#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)] +pub enum InteractionState { + #[default] + Enabled, + Hovered, + Active, + Focused, + Disabled, +} + +impl InteractionState { + pub fn if_enabled(&self, enabled: bool) -> Self { + if enabled { + *self + } else { + InteractionState::Disabled + } + } +} + +#[derive(Default, PartialEq)] +pub enum SelectedState { + #[default] + Unselected, + PartiallySelected, + Selected, +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +pub enum Toggleable { + Toggleable(ToggleState), + #[default] + NotToggleable, +} + +impl Toggleable { + pub fn is_toggled(&self) -> bool { + match self { + Self::Toggleable(ToggleState::Toggled) => true, + _ => false, + } + } +} + +impl From for Toggleable { + fn from(state: ToggleState) -> Self { + Self::Toggleable(state) + } +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +pub enum ToggleState { + /// The "on" state of a toggleable element. + /// + /// Example: + /// - A collasable list that is currently expanded + /// - A toggle button that is currently on. + Toggled, + /// The "off" state of a toggleable element. + /// + /// Example: + /// - A collasable list that is currently collapsed + /// - A toggle button that is currently off. + #[default] + NotToggled, +} + +impl From for ToggleState { + fn from(toggleable: Toggleable) -> Self { + match toggleable { + Toggleable::Toggleable(state) => state, + Toggleable::NotToggleable => ToggleState::NotToggled, + } + } +} + +impl From for ToggleState { + fn from(toggled: bool) -> Self { + if toggled { + ToggleState::Toggled + } else { + ToggleState::NotToggled + } + } +}