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;
Marshall Bowers created
crates/storybook/src/stories/elements.rs | 1
crates/storybook2/src/stories/elements.rs | 1
crates/storybook2/src/stories/elements/button.rs | 200 ++++++++++++++++++
crates/storybook2/src/story_selector.rs | 2
crates/ui2/src/components/status_bar.rs | 11
crates/ui2/src/elements/button.rs | 198 +++++++++++++++++
6 files changed, 407 insertions(+), 6 deletions(-)
@@ -1,5 +1,6 @@
pub mod avatar;
pub mod button;
+pub mod button;
pub mod icon;
pub mod input;
pub mod label;
@@ -1,4 +1,5 @@
pub mod avatar;
+pub mod button;
pub mod icon;
pub mod input;
pub mod label;
@@ -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<S: 'static + Send + Sync + Clone> {
+ state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> ButtonStory<S> {
+ pub fn new() -> Self {
+ Self {
+ state_type: PhantomData,
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ let states = InteractionState::iter();
+
+ Story::container(cx)
+ .child(Story::title_for::<_, Button<S>>(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.")),
+ )
+ }
+}
@@ -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(),
@@ -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<S: 'static + Send + Sync> {
+pub struct StatusBar<S: 'static + Send + Sync + Clone> {
state_type: PhantomData<S>,
left_tools: Option<ToolGroup>,
right_tools: Option<ToolGroup>,
bottom_tools: Option<ToolGroup>,
}
-impl<S: 'static + Send + Sync> StatusBar<S> {
+impl<S: 'static + Send + Sync + Clone> StatusBar<S> {
pub fn new() -> Self {
Self {
state_type: PhantomData,
@@ -119,8 +118,8 @@ impl<S: 'static + Send + Sync> StatusBar<S> {
.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(
@@ -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<V> {
+// click: Option<Rc<dyn Fn(&mut V, &mut EventContext<V>)>>,
+// }
+
+// impl<V> Default for ButtonHandlers<V> {
+// fn default() -> Self {
+// Self { click: None }
+// }
+// }
+
+#[derive(Element)]
+pub struct Button<S: 'static + Send + Sync + Clone> {
+ state_type: PhantomData<S>,
+ label: String,
+ variant: ButtonVariant,
+ state: InteractionState,
+ icon: Option<Icon>,
+ icon_position: Option<IconPosition>,
+ width: Option<DefiniteLength>,
+ // handlers: ButtonHandlers<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> Button<S> {
+ pub fn new<L>(label: L) -> Self
+ where
+ L: Into<String>,
+ {
+ 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<L>(label: L) -> Self
+ where
+ L: Into<String>,
+ {
+ 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<DefiniteLength>) -> Self {
+ self.width = width;
+ self
+ }
+
+ // pub fn on_click(mut self, handler: impl Fn(&mut S, &mut EventContext<S>) + 'static) -> Self {
+ // self.handlers.click = Some(Rc::new(handler));
+ // self
+ // }
+
+ fn background_color(&self, cx: &mut ViewContext<S>) -> 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<S> {
+ Label::new(self.label.clone())
+ .size(LabelSize::Small)
+ .color(self.label_color())
+ }
+
+ fn render_icon(&self, icon_color: IconColor) -> Option<IconElement<S>> {
+ self.icon.map(|i| IconElement::new(i).color(icon_color))
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ 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
+ }
+}