ZIm/crates/ui2/src/elements/button.rs
Nate Butler 8b637e194e Update approach to settings
Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2023-10-18 16:16:58 -04:00

406 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::marker::PhantomData;
use std::sync::Arc;
use gpui3::rems;
use gpui3::{DefiniteLength, Hsla, Interactive, MouseButton, WindowContext};
use crate::prelude::*;
use crate::settings::user_settings;
use crate::{h_stack, 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,
}
pub type ClickHandler<S> = Arc<dyn Fn(&mut S, &mut ViewContext<S>) + 'static + Send + Sync>;
struct ButtonHandlers<S: 'static + Send + Sync> {
click: Option<ClickHandler<S>>,
}
impl<S: 'static + Send + Sync> Default for ButtonHandlers<S> {
fn default() -> Self {
Self { click: None }
}
}
#[derive(Element)]
pub struct Button<S: 'static + Send + Sync + Clone> {
state_type: PhantomData<S>,
label: SharedString,
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(label: impl Into<SharedString>) -> Self {
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: impl Into<SharedString>) -> Self {
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: ClickHandler<S>) -> Self {
self.handlers.click = Some(handler);
self
}
fn background_color(&self, cx: &mut ViewContext<S>) -> Hsla {
let color = ThemeColor::new(cx);
match (self.variant, self.state) {
(ButtonVariant::Ghost, InteractionState::Enabled) => color.ghost_element,
(ButtonVariant::Ghost, InteractionState::Focused) => color.ghost_element,
(ButtonVariant::Ghost, InteractionState::Hovered) => color.ghost_element_hover,
(ButtonVariant::Ghost, InteractionState::Active) => color.ghost_element_active,
(ButtonVariant::Ghost, InteractionState::Disabled) => color.filled_element_disabled,
(ButtonVariant::Filled, InteractionState::Enabled) => color.filled_element,
(ButtonVariant::Filled, InteractionState::Focused) => color.filled_element,
(ButtonVariant::Filled, InteractionState::Hovered) => color.filled_element_hover,
(ButtonVariant::Filled, InteractionState::Active) => color.filled_element_active,
(ButtonVariant::Filled, InteractionState::Disabled) => color.filled_element_disabled,
}
}
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 color = ThemeColor::new(cx);
match self.state {
InteractionState::Focused => color.border_focused,
_ => color.border_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, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
let icon_color = self.icon_color();
let border_color = self.border_color(cx);
let setting = user_settings();
let mut el = h_stack()
.p_1()
.text_size(ui_size(1.125))
.rounded_md()
.border()
.border_color(border_color)
.bg(self.background_color(cx))
.hover(|style| {
let color = ThemeColor::new(cx);
style.bg(match self.variant {
ButtonVariant::Ghost => color.ghost_element_hover,
ButtonVariant::Filled => color.filled_element_hover,
})
});
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 |state, event, cx| {
click_handler(state, cx);
});
}
el
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use gpui3::rems;
use strum::IntoEnumIterator;
use crate::{h_stack, v_stack, LabelColor, LabelSize, Story};
use super::*;
#[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,
_view: &mut S,
cx: &mut ViewContext<S>,
) -> impl Element<ViewState = 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(LabelColor::Muted)
.size(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(LabelColor::Muted)
.size(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(LabelColor::Muted)
.size(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(LabelColor::Muted)
.size(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(LabelColor::Muted)
.size(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(LabelColor::Muted)
.size(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(LabelColor::Muted)
.size(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(LabelColor::Muted)
.size(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(LabelColor::Muted)
.size(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)
.on_click(Arc::new(|_view, _cx| println!("Button clicked."))),
)
}
}
}