use gpui::{elements::SafeStylable, Action}; use crate::{Interactive, Toggleable}; use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; pub type IconButtonStyle = Interactive<ButtonStyle<SvgStyle>>;pub type ToggleIconButtonStyle = Toggleable<IconButtonStyle>; pub trait ComponentExt<C: SafeStylable> { fn toggleable(self, active: bool) -> Toggle<C, ()>; fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()>;} impl<C: SafeStylable> ComponentExt<C> for C { fn toggleable(self, active: bool) -> Toggle<C, ()> { 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<bool>, action: Box<dyn Action>) -> Disclosable<C, ()> { 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<S> { pub button: IconButtonStyle, #[serde(flatten)] pub container: ContainerStyle, pub spacing: f32, #[serde(flatten)] content: S, } impl<S> DisclosureStyle<S> { pub fn button_space(&self) -> f32 { self.spacing + self.button.button_width.unwrap() } } pub struct Disclosable<C, S> { disclosed: Option<bool>, action: Box<dyn Action>, id: usize, content: C, style: S, } impl Disclosable<(), ()> { pub fn new<C>( disclosed: Option<bool>, content: C, action: Box<dyn Action>, ) -> Disclosable<C, ()> { Disclosable { disclosed, content, action, id: 0, style: (), } } } impl<C> Disclosable<C, ()> { pub fn with_id(mut self, id: usize) -> Disclosable<C, ()> { self.id = id; self } } impl<C: SafeStylable> SafeStylable for Disclosable<C, ()> { type Style = DisclosureStyle<C::Style>; type Output = Disclosable<C, Self::Style>; fn with_style(self, style: Self::Style) -> Self::Output { Disclosable { disclosed: self.disclosed, action: self.action, content: self.content, id: self.id, style, } } } impl<C: SafeStylable> Component for Disclosable<C, DisclosureStyle<C::Style>> { fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> { 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<C, S> { style: S, active: bool, component: C, } impl<C: SafeStylable> Toggle<C, ()> { pub fn new(component: C, active: bool) -> Self { Toggle { active, component, style: (), } } } impl<C: SafeStylable> SafeStylable for Toggle<C, ()> { type Style = Toggleable<C::Style>; type Output = Toggle<C, Self::Style>; fn with_style(self, style: Self::Style) -> Self::Output { Toggle { active: self.active, component: self.component, style, } } } impl<C: SafeStylable> Component for Toggle<C, Toggleable<C::Style>> { fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> { 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<C> { #[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<f32>, pub button_height: Option<f32>, #[serde(flatten)] contents: C, } pub struct Button<C, S> { action: Box<dyn Action>, tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, id: usize, contents: C, style: Interactive<S>, } impl Button<(), ()> { pub fn dynamic_action(action: Box<dyn Action>) -> Button<(), ()> { Self { contents: (), tag: action.type_tag(), action, style: Interactive::new_blank(), tooltip: None, id: 0, } } pub fn action<A: Action + Clone>(action: A) -> Self { Self::dynamic_action(Box::new(action)) } pub fn with_tooltip( mut self, tooltip: impl Into<Cow<'static, str>>, 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<C: SafeStylable>(self, contents: C) -> Button<C, ()> { Button { action: self.action, tag: self.tag, style: self.style, tooltip: self.tooltip, id: self.id, contents, } } } impl<C: SafeStylable> SafeStylable for Button<C, ()> { type Style = Interactive<ButtonStyle<C::Style>>; type Output = Button<C, ButtonStyle<C::Style>>; 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<C: SafeStylable> Component for Button<C, ButtonStyle<C::Style>> { fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> { 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<D>(deserializer: D) -> Result<Self, D::Error> 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<S> { path: Option<Cow<'static, str>>, style: S, } impl Svg<()> { pub fn new(path: impl Into<Cow<'static, str>>) -> Self { Self { path: Some(path.into()), style: (), } } pub fn optional(path: Option<impl Into<Cow<'static, str>>>) -> Self { Self { path: path.map(Into::into), style: (), } } } impl SafeStylable for Svg<()> { type Style = SvgStyle; type Output = Svg<SvgStyle>; fn with_style(self, style: Self::Style) -> Self::Output { Svg { path: self.path, style, } } } impl Component for Svg<SvgStyle> { fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> { 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<S> { text: Cow<'static, str>, style: S, } impl Label<()> { pub fn new(text: impl Into<Cow<'static, str>>) -> Self { Self { text: text.into(), style: (), } } } impl SafeStylable for Label<()> { type Style = TextStyle; type Output = Label<LabelStyle>; fn with_style(self, style: Self::Style) -> Self::Output { Label { text: self.text, style: style.into(), } } } impl Component for Label<LabelStyle> { fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> { gpui::elements::Label::new(self.text, self.style).into_any() } }}
use gpui::{elements::SafeStylable, Action};
use crate::{Interactive, Toggleable};
use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle};
pub type IconButtonStyle = Interactive<ButtonStyle<SvgStyle>>;
pub type ToggleIconButtonStyle = Toggleable<IconButtonStyle>;
pub trait ComponentExt<C: SafeStylable> {
fn toggleable(self, active: bool) -> Toggle<C, ()>;
fn disclosable(self, disclosed: Option<bool>, action: Box<dyn Action>) -> Disclosable<C, ()>;
}
impl<C: SafeStylable> ComponentExt<C> for C {
fn toggleable(self, active: bool) -> Toggle<C, ()> {
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<bool>, action: Box<dyn Action>) -> Disclosable<C, ()> {
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<S> {
pub button: IconButtonStyle,
#[serde(flatten)]
pub container: ContainerStyle,
pub spacing: f32,
#[serde(flatten)]
content: S,
}
impl<S> DisclosureStyle<S> {
pub fn button_space(&self) -> f32 {
self.spacing + self.button.button_width.unwrap()
}
}
pub struct Disclosable<C, S> {
disclosed: Option<bool>,
action: Box<dyn Action>,
id: usize,
content: C,
style: S,
}
impl Disclosable<(), ()> {
pub fn new<C>(
disclosed: Option<bool>,
content: C,
action: Box<dyn Action>,
) -> Disclosable<C, ()> {
Disclosable {
disclosed,
content,
action,
id: 0,
style: (),
}
}
}
impl<C> Disclosable<C, ()> {
pub fn with_id(mut self, id: usize) -> Disclosable<C, ()> {
self.id = id;
self
}
}
impl<C: SafeStylable> SafeStylable for Disclosable<C, ()> {
type Style = DisclosureStyle<C::Style>;
type Output = Disclosable<C, Self::Style>;
fn with_style(self, style: Self::Style) -> Self::Output {
Disclosable {
disclosed: self.disclosed,
action: self.action,
content: self.content,
id: self.id,
style,
}
}
}
impl<C: SafeStylable> Component for Disclosable<C, DisclosureStyle<C::Style>> {
fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
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<C, S> {
style: S,
active: bool,
component: C,
}
impl<C: SafeStylable> Toggle<C, ()> {
pub fn new(component: C, active: bool) -> Self {
Toggle {
active,
component,
style: (),
}
}
}
impl<C: SafeStylable> SafeStylable for Toggle<C, ()> {
type Style = Toggleable<C::Style>;
type Output = Toggle<C, Self::Style>;
fn with_style(self, style: Self::Style) -> Self::Output {
Toggle {
active: self.active,
component: self.component,
style,
}
}
}
impl<C: SafeStylable> Component for Toggle<C, Toggleable<C::Style>> {
fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
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<C> {
#[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<f32>,
pub button_height: Option<f32>,
#[serde(flatten)]
contents: C,
}
pub struct Button<C, S> {
action: Box<dyn Action>,
tooltip: Option<(Cow<'static, str>, TooltipStyle)>,
tag: TypeTag,
id: usize,
contents: C,
style: Interactive<S>,
}
impl Button<(), ()> {
pub fn dynamic_action(action: Box<dyn Action>) -> Button<(), ()> {
Self {
contents: (),
tag: action.type_tag(),
action,
style: Interactive::new_blank(),
tooltip: None,
id: 0,
}
}
pub fn action<A: Action + Clone>(action: A) -> Self {
Self::dynamic_action(Box::new(action))
}
pub fn with_tooltip(
mut self,
tooltip: impl Into<Cow<'static, str>>,
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<C: SafeStylable>(self, contents: C) -> Button<C, ()> {
Button {
action: self.action,
tag: self.tag,
style: self.style,
tooltip: self.tooltip,
id: self.id,
contents,
}
}
}
impl<C: SafeStylable> SafeStylable for Button<C, ()> {
type Style = Interactive<ButtonStyle<C::Style>>;
type Output = Button<C, ButtonStyle<C::Style>>;
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<C: SafeStylable> Component for Button<C, ButtonStyle<C::Style>> {
fn render<V: 'static>(self, cx: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
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<D>(deserializer: D) -> Result<Self, D::Error>
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<S> {
path: Option<Cow<'static, str>>,
style: S,
}
impl Svg<()> {
pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
Self {
path: Some(path.into()),
style: (),
}
}
pub fn optional(path: Option<impl Into<Cow<'static, str>>>) -> Self {
Self {
path: path.map(Into::into),
style: (),
}
}
}
impl SafeStylable for Svg<()> {
type Style = SvgStyle;
type Output = Svg<SvgStyle>;
fn with_style(self, style: Self::Style) -> Self::Output {
Svg {
path: self.path,
style,
}
}
}
impl Component for Svg<SvgStyle> {
fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
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<S> {
text: Cow<'static, str>,
style: S,
}
impl Label<()> {
pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
Self {
text: text.into(),
style: (),
}
}
}
impl SafeStylable for Label<()> {
type Style = TextStyle;
type Output = Label<LabelStyle>;
fn with_style(self, style: Self::Style) -> Self::Output {
Label {
text: self.text,
style: style.into(),
}
}
}
impl Component for Label<LabelStyle> {
fn render<V: 'static>(self, _: &mut gpui::ViewContext<V>) -> gpui::AnyElement<V> {
gpui::elements::Label::new(self.text, self.style).into_any()
}
}
}