use gpui::{elements::SafeStylable, Action}; use crate::{Interactive, Toggleable}; use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; pub type IconButtonStyle = Interactive>; pub type ToggleIconButtonStyle = Toggleable; pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; fn disclosable(self, disclosed: Option, action: Box) -> Disclosable; } impl ComponentExt for C { fn toggleable(self, active: bool) -> Toggle { Toggle::new(self, active) } /// Some(True) => disclosed => content is visible /// Some(false) => closed => content is hidden /// None => No disclosure button, but reserve disclosure spacing fn disclosable(self, disclosed: Option, action: Box) -> Disclosable { Disclosable::new(disclosed, self, action) } } pub mod disclosure { use gpui::{ elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable}, Action, Element, }; use schemars::JsonSchema; use serde_derive::Deserialize; use super::{action_button::Button, svg::Svg, IconButtonStyle}; #[derive(Clone, Default, Deserialize, JsonSchema)] pub struct DisclosureStyle { pub button: IconButtonStyle, #[serde(flatten)] pub container: ContainerStyle, pub spacing: f32, #[serde(flatten)] content: S, } impl DisclosureStyle { pub fn button_space(&self) -> f32 { self.spacing + self.button.button_width.unwrap() } } pub struct Disclosable { disclosed: Option, action: Box, id: usize, content: C, style: S, } impl Disclosable<(), ()> { pub fn new( disclosed: Option, content: C, action: Box, ) -> Disclosable { Disclosable { disclosed, content, action, id: 0, style: (), } } } impl Disclosable { pub fn with_id(mut self, id: usize) -> Disclosable { self.id = id; self } } impl SafeStylable for Disclosable { type Style = DisclosureStyle; type Output = Disclosable; fn with_style(self, style: Self::Style) -> Self::Output { Disclosable { disclosed: self.disclosed, action: self.action, content: self.content, id: self.id, style, } } } impl Component for Disclosable> { fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { Flex::row() .with_spacing(self.style.spacing) .with_child(if let Some(disclosed) = self.disclosed { Button::dynamic_action(self.action) .with_id(self.id) .with_contents(Svg::new(if disclosed { "icons/file_icons/chevron_down.svg" } else { "icons/file_icons/chevron_right.svg" })) .with_style(self.style.button) .element() .into_any() } else { Empty::new() .into_any() .constrained() // TODO: Why is this optional at all? .with_width(self.style.button.button_width.unwrap()) .into_any() }) .with_child( self.content .with_style(self.style.content) .render(cx) .flex(1., true), ) .align_children_center() .contained() .with_style(self.style.container) .into_any() } } } pub mod toggle { use gpui::elements::{Component, SafeStylable}; use crate::Toggleable; pub struct Toggle { style: S, active: bool, component: C, } impl Toggle { pub fn new(component: C, active: bool) -> Self { Toggle { active, component, style: (), } } } impl SafeStylable for Toggle { type Style = Toggleable; type Output = Toggle; fn with_style(self, style: Self::Style) -> Self::Output { Toggle { active: self.active, component: self.component, style, } } } impl Component for Toggle> { fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { self.component .with_style(self.style.in_state(self.active).clone()) .render(cx) } } } pub mod action_button { use std::borrow::Cow; use gpui::{ elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle}, platform::{CursorStyle, MouseButton}, Action, Element, TypeTag, }; use schemars::JsonSchema; use serde_derive::Deserialize; use crate::Interactive; #[derive(Clone, Deserialize, Default, JsonSchema)] pub struct ButtonStyle { #[serde(flatten)] pub container: ContainerStyle, // TODO: These are incorrect for the intended usage of the buttons. // The size should be constant, but putting them here duplicates them // across the states the buttons can be in pub button_width: Option, pub button_height: Option, #[serde(flatten)] contents: C, } pub struct Button { action: Box, tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, id: usize, contents: C, style: Interactive, } impl Button<(), ()> { pub fn dynamic_action(action: Box) -> Button<(), ()> { Self { contents: (), tag: action.type_tag(), action, style: Interactive::new_blank(), tooltip: None, id: 0, } } pub fn action(action: A) -> Self { Self::dynamic_action(Box::new(action)) } pub fn with_tooltip( mut self, tooltip: impl Into>, tooltip_style: TooltipStyle, ) -> Self { self.tooltip = Some((tooltip.into(), tooltip_style)); self } pub fn with_id(mut self, id: usize) -> Self { self.id = id; self } pub fn with_contents(self, contents: C) -> Button { Button { action: self.action, tag: self.tag, style: self.style, tooltip: self.tooltip, id: self.id, contents, } } } impl SafeStylable for Button { type Style = Interactive>; type Output = Button>; fn with_style(self, style: Self::Style) -> Self::Output { Button { action: self.action, tag: self.tag, contents: self.contents, tooltip: self.tooltip, id: self.id, style, } } } impl Component for Button> { fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| { let style = self.style.style_for(state); let mut contents = self .contents .with_style(style.contents.to_owned()) .render(cx) .contained() .with_style(style.container) .constrained(); if let Some(height) = style.button_height { contents = contents.with_height(height); } if let Some(width) = style.button_width { contents = contents.with_width(width); } contents.into_any() }) .on_click(MouseButton::Left, { let action = self.action.boxed_clone(); move |_, _, cx| { let window = cx.window(); let view = cx.view_id(); let action = action.boxed_clone(); cx.spawn(|_, mut cx| async move { window.dispatch_action(view, action.as_ref(), &mut cx) }) .detach(); } }) .with_cursor_style(CursorStyle::PointingHand) .into_any(); if let Some((tooltip, style)) = self.tooltip { button = button .with_dynamic_tooltip(self.tag, 0, tooltip, Some(self.action), style, cx) .into_any() } button } } } pub mod svg { use std::borrow::Cow; use gpui::{ elements::{Component, Empty, SafeStylable}, Element, }; use schemars::JsonSchema; use serde::Deserialize; #[derive(Clone, Default, JsonSchema)] pub struct SvgStyle { icon_width: f32, icon_height: f32, color: gpui::color::Color, } impl<'de> Deserialize<'de> for SvgStyle { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] pub enum IconSize { IconSize { icon_size: f32 }, Dimensions { width: f32, height: f32 }, IconDimensions { icon_width: f32, icon_height: f32 }, } #[derive(Deserialize)] struct SvgStyleHelper { #[serde(flatten)] size: IconSize, color: gpui::color::Color, } let json = SvgStyleHelper::deserialize(deserializer)?; let color = json.color; let result = match json.size { IconSize::IconSize { icon_size } => SvgStyle { icon_width: icon_size, icon_height: icon_size, color, }, IconSize::Dimensions { width, height } => SvgStyle { icon_width: width, icon_height: height, color, }, IconSize::IconDimensions { icon_width, icon_height, } => SvgStyle { icon_width, icon_height, color, }, }; Ok(result) } } pub struct Svg { path: Option>, style: S, } impl Svg<()> { pub fn new(path: impl Into>) -> Self { Self { path: Some(path.into()), style: (), } } pub fn optional(path: Option>>) -> Self { Self { path: path.map(Into::into), style: (), } } } impl SafeStylable for Svg<()> { type Style = SvgStyle; type Output = Svg; fn with_style(self, style: Self::Style) -> Self::Output { Svg { path: self.path, style, } } } impl Component for Svg { fn render(self, _: &mut gpui::ViewContext) -> gpui::AnyElement { if let Some(path) = self.path { gpui::elements::Svg::new(path) .with_color(self.style.color) .constrained() } else { Empty::new().constrained() } .constrained() .with_width(self.style.icon_width) .with_height(self.style.icon_height) .into_any() } } } pub mod label { use std::borrow::Cow; use gpui::{ elements::{Component, LabelStyle, SafeStylable}, fonts::TextStyle, Element, }; pub struct Label { text: Cow<'static, str>, style: S, } impl Label<()> { pub fn new(text: impl Into>) -> Self { Self { text: text.into(), style: (), } } } impl SafeStylable for Label<()> { type Style = TextStyle; type Output = Label; fn with_style(self, style: Self::Style) -> Self::Output { Label { text: self.text, style: style.into(), } } } impl Component for Label { fn render(self, _: &mut gpui::ViewContext) -> gpui::AnyElement { gpui::elements::Label::new(self.text, self.style).into_any() } } }