diff --git a/crates/storybook/src/stories/elements.rs b/crates/storybook/src/stories/elements.rs index f7afec4d88..29fc5ea030 100644 --- a/crates/storybook/src/stories/elements.rs +++ b/crates/storybook/src/stories/elements.rs @@ -1,5 +1,6 @@ pub mod avatar; pub mod button; +pub mod button; pub mod icon; pub mod input; pub mod label; diff --git a/crates/storybook2/src/stories/elements.rs b/crates/storybook2/src/stories/elements.rs index e177412a7e..f7afec4d88 100644 --- a/crates/storybook2/src/stories/elements.rs +++ b/crates/storybook2/src/stories/elements.rs @@ -1,4 +1,5 @@ pub mod avatar; +pub mod button; pub mod icon; pub mod input; pub mod label; diff --git a/crates/storybook2/src/stories/elements/button.rs b/crates/storybook2/src/stories/elements/button.rs new file mode 100644 index 0000000000..650d9fd9f7 --- /dev/null +++ b/crates/storybook2/src/stories/elements/button.rs @@ -0,0 +1,200 @@ +use std::marker::PhantomData; + +use gpui3::rems; +use strum::IntoEnumIterator; +use ui::prelude::*; +use ui::{h_stack, v_stack, Button, Icon, IconPosition, Label}; + +use crate::story::Story; + +#[derive(Element)] +pub struct ButtonStory { + state_type: PhantomData, +} + +impl ButtonStory { + pub fn new() -> Self { + Self { + state_type: PhantomData, + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let states = InteractionState::iter(); + + Story::container(cx) + .child(Story::title_for::<_, Button>(cx)) + .child( + div() + .flex() + .gap_8() + .child( + div() + .child(Story::label(cx, "Ghost (Default)")) + .child(h_stack().gap_2().children(states.clone().map(|state| { + v_stack() + .gap_1() + .child( + Label::new(state.to_string()) + .color(ui::LabelColor::Muted) + .size(ui::LabelSize::Small), + ) + .child( + Button::new("Label") + .variant(ButtonVariant::Ghost) + .state(state), + ) + }))) + .child(Story::label(cx, "Ghost – Left Icon")) + .child(h_stack().gap_2().children(states.clone().map(|state| { + v_stack() + .gap_1() + .child( + Label::new(state.to_string()) + .color(ui::LabelColor::Muted) + .size(ui::LabelSize::Small), + ) + .child( + Button::new("Label") + .variant(ButtonVariant::Ghost) + .icon(Icon::Plus) + .icon_position(IconPosition::Left) + .state(state), + ) + }))) + .child(Story::label(cx, "Ghost – Right Icon")) + .child(h_stack().gap_2().children(states.clone().map(|state| { + v_stack() + .gap_1() + .child( + Label::new(state.to_string()) + .color(ui::LabelColor::Muted) + .size(ui::LabelSize::Small), + ) + .child( + Button::new("Label") + .variant(ButtonVariant::Ghost) + .icon(Icon::Plus) + .icon_position(IconPosition::Right) + .state(state), + ) + }))), + ) + .child( + div() + .child(Story::label(cx, "Filled")) + .child(h_stack().gap_2().children(states.clone().map(|state| { + v_stack() + .gap_1() + .child( + Label::new(state.to_string()) + .color(ui::LabelColor::Muted) + .size(ui::LabelSize::Small), + ) + .child( + Button::new("Label") + .variant(ButtonVariant::Filled) + .state(state), + ) + }))) + .child(Story::label(cx, "Filled – Left Button")) + .child(h_stack().gap_2().children(states.clone().map(|state| { + v_stack() + .gap_1() + .child( + Label::new(state.to_string()) + .color(ui::LabelColor::Muted) + .size(ui::LabelSize::Small), + ) + .child( + Button::new("Label") + .variant(ButtonVariant::Filled) + .icon(Icon::Plus) + .icon_position(IconPosition::Left) + .state(state), + ) + }))) + .child(Story::label(cx, "Filled – Right Button")) + .child(h_stack().gap_2().children(states.clone().map(|state| { + v_stack() + .gap_1() + .child( + Label::new(state.to_string()) + .color(ui::LabelColor::Muted) + .size(ui::LabelSize::Small), + ) + .child( + Button::new("Label") + .variant(ButtonVariant::Filled) + .icon(Icon::Plus) + .icon_position(IconPosition::Right) + .state(state), + ) + }))), + ) + .child( + div() + .child(Story::label(cx, "Fixed With")) + .child(h_stack().gap_2().children(states.clone().map(|state| { + v_stack() + .gap_1() + .child( + Label::new(state.to_string()) + .color(ui::LabelColor::Muted) + .size(ui::LabelSize::Small), + ) + .child( + Button::new("Label") + .variant(ButtonVariant::Filled) + .state(state) + .width(Some(rems(6.).into())), + ) + }))) + .child(Story::label(cx, "Fixed With – Left Icon")) + .child(h_stack().gap_2().children(states.clone().map(|state| { + v_stack() + .gap_1() + .child( + Label::new(state.to_string()) + .color(ui::LabelColor::Muted) + .size(ui::LabelSize::Small), + ) + .child( + Button::new("Label") + .variant(ButtonVariant::Filled) + .state(state) + .icon(Icon::Plus) + .icon_position(IconPosition::Left) + .width(Some(rems(6.).into())), + ) + }))) + .child(Story::label(cx, "Fixed With – Right Icon")) + .child(h_stack().gap_2().children(states.clone().map(|state| { + v_stack() + .gap_1() + .child( + Label::new(state.to_string()) + .color(ui::LabelColor::Muted) + .size(ui::LabelSize::Small), + ) + .child( + Button::new("Label") + .variant(ButtonVariant::Filled) + .state(state) + .icon(Icon::Plus) + .icon_position(IconPosition::Right) + .width(Some(rems(6.).into())), + ) + }))), + ), + ) + .child(Story::label(cx, "Button with `on_click`")) + .child( + Button::new("Label") + .variant(ButtonVariant::Ghost) + // NOTE: There currently appears to be a bug in GPUI2 where only the last event handler will fire. + // So adding additional buttons with `on_click`s after this one will cause this `on_click` to not fire. + // .on_click(|_view, _cx| println!("Button clicked.")), + ) + } +} diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index ae21c03355..86c069a2b1 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -13,6 +13,7 @@ use ui::prelude::*; #[strum(serialize_all = "snake_case")] pub enum ElementStory { Avatar, + Button, Icon, Input, Label, @@ -24,6 +25,7 @@ impl ElementStory { match self { Self::Avatar => elements::avatar::AvatarStory::new().into_any(), + Self::Button => elements::button::ButtonStory::new().into_any(), Self::Icon => elements::icon::IconStory::new().into_any(), Self::Input => elements::input::InputStory::new().into_any(), Self::Label => elements::label::LabelStory::new().into_any(), diff --git a/crates/ui2/src/components/status_bar.rs b/crates/ui2/src/components/status_bar.rs index cc2cc519f6..e4ec2dda81 100644 --- a/crates/ui2/src/components/status_bar.rs +++ b/crates/ui2/src/components/status_bar.rs @@ -1,8 +1,7 @@ use std::marker::PhantomData; use crate::prelude::*; -use crate::theme::{theme, Theme}; -use crate::{Icon, IconButton, IconColor, ToolDivider}; +use crate::{Button, Icon, IconButton, IconColor, ToolDivider}; #[derive(Default, PartialEq)] pub enum Tool { @@ -30,14 +29,14 @@ impl Default for ToolGroup { } #[derive(Element)] -pub struct StatusBar { +pub struct StatusBar { state_type: PhantomData, left_tools: Option, right_tools: Option, bottom_tools: Option, } -impl StatusBar { +impl StatusBar { pub fn new() -> Self { Self { state_type: PhantomData, @@ -119,8 +118,8 @@ impl StatusBar { .flex() .items_center() .gap_1() - // .child(Button::new("116:25")) - // .child(Button::new("Rust")), + .child(Button::new("116:25")) + .child(Button::new("Rust")), ) .child(ToolDivider::new()) .child( diff --git a/crates/ui2/src/elements/button.rs b/crates/ui2/src/elements/button.rs index 2f2c8a46ec..fd4c4ffa1a 100644 --- a/crates/ui2/src/elements/button.rs +++ b/crates/ui2/src/elements/button.rs @@ -1,6 +1,204 @@ +use std::marker::PhantomData; +use std::rc::Rc; + +use gpui3::{DefiniteLength, Hsla, MouseButton, WindowContext}; + +use crate::prelude::*; +use crate::{h_stack, theme, Icon, IconColor, IconElement, Label, LabelColor, LabelSize}; + +#[derive(Default, PartialEq, Clone, Copy)] +pub enum IconPosition { + #[default] + Left, + Right, +} + #[derive(Default, Copy, Clone, PartialEq)] pub enum ButtonVariant { #[default] Ghost, Filled, } + +// struct ButtonHandlers { +// click: Option)>>, +// } + +// impl Default for ButtonHandlers { +// fn default() -> Self { +// Self { click: None } +// } +// } + +#[derive(Element)] +pub struct Button { + state_type: PhantomData, + label: String, + variant: ButtonVariant, + state: InteractionState, + icon: Option, + icon_position: Option, + width: Option, + // handlers: ButtonHandlers, +} + +impl Button { + pub fn new(label: L) -> Self + where + L: Into, + { + Self { + state_type: PhantomData, + label: label.into(), + variant: Default::default(), + state: Default::default(), + icon: None, + icon_position: None, + width: Default::default(), + // handlers: ButtonHandlers::default(), + } + } + + pub fn ghost(label: L) -> Self + where + L: Into, + { + Self::new(label).variant(ButtonVariant::Ghost) + } + + pub fn variant(mut self, variant: ButtonVariant) -> Self { + self.variant = variant; + self + } + + pub fn state(mut self, state: InteractionState) -> Self { + self.state = state; + self + } + + pub fn icon(mut self, icon: Icon) -> Self { + self.icon = Some(icon); + self + } + + pub fn icon_position(mut self, icon_position: IconPosition) -> Self { + if self.icon.is_none() { + panic!("An icon must be present if an icon_position is provided."); + } + self.icon_position = Some(icon_position); + self + } + + pub fn width(mut self, width: Option) -> Self { + self.width = width; + self + } + + // pub fn on_click(mut self, handler: impl Fn(&mut S, &mut EventContext) + 'static) -> Self { + // self.handlers.click = Some(Rc::new(handler)); + // self + // } + + fn background_color(&self, cx: &mut ViewContext) -> Hsla { + let theme = theme(cx); + let system_color = SystemColor::new(); + + match (self.variant, self.state) { + (ButtonVariant::Ghost, InteractionState::Hovered) => { + theme.lowest.base.hovered.background + } + (ButtonVariant::Ghost, InteractionState::Active) => { + theme.lowest.base.pressed.background + } + (ButtonVariant::Filled, InteractionState::Enabled) => { + theme.lowest.on.default.background + } + (ButtonVariant::Filled, InteractionState::Hovered) => { + theme.lowest.on.hovered.background + } + (ButtonVariant::Filled, InteractionState::Active) => theme.lowest.on.pressed.background, + (ButtonVariant::Filled, InteractionState::Disabled) => { + theme.lowest.on.disabled.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 border_color(&self, cx: &WindowContext) -> Hsla { + let theme = theme(cx); + let system_color = SystemColor::new(); + + match self.state { + InteractionState::Focused => theme.lowest.accent.default.border, + _ => system_color.transparent, + } + } + + fn render_label(&self) -> Label { + Label::new(self.label.clone()) + .size(LabelSize::Small) + .color(self.label_color()) + } + + fn render_icon(&self, icon_color: IconColor) -> Option> { + self.icon.map(|i| IconElement::new(i).color(icon_color)) + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let theme = theme(cx); + let icon_color = self.icon_color(); + let system_color = SystemColor::new(); + let border_color = self.border_color(cx); + + let mut el = h_stack() + .h_6() + .px_1() + .items_center() + .rounded_md() + .border() + .border_color(border_color) + .fill(self.background_color(cx)); + + match (self.icon, self.icon_position) { + (Some(_), Some(IconPosition::Left)) => { + el = el + .gap_1() + .child(self.render_label()) + .children(self.render_icon(icon_color)) + } + (Some(_), Some(IconPosition::Right)) => { + el = el + .gap_1() + .children(self.render_icon(icon_color)) + .child(self.render_label()) + } + (_, _) => el = el.child(self.render_label()), + } + + if let Some(width) = self.width { + el = el.w(width).justify_center(); + } + + // if let Some(click_handler) = self.handlers.click.clone() { + // el = el.on_mouse_down(MouseButton::Left, move |view, event, cx| { + // click_handler(view, cx); + // }); + // } + + el + } +}