Move UI out of storybook2 and into ui2
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
use std::any::Any;
|
||||
|
||||
use gpui3::{AnyElement, ViewContext};
|
||||
|
||||
pub type HackyChildren<S> = fn(&mut ViewContext<S>, &dyn Any) -> Vec<AnyElement<S>>;
|
||||
|
||||
pub type HackyChildrenPayload = Box<dyn Any>;
|
||||
@@ -0,0 +1,5 @@
|
||||
mod list;
|
||||
mod panel;
|
||||
|
||||
pub use list::*;
|
||||
pub use panel::*;
|
||||
@@ -0,0 +1,519 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use gpui3::{div, Div, Hsla, WindowContext};
|
||||
|
||||
use crate::theme::theme;
|
||||
use crate::prelude::*;
|
||||
use crate::{
|
||||
h_stack, token, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, LabelColor,
|
||||
LabelSize,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq)]
|
||||
pub enum ListItemVariant {
|
||||
/// The list item extends to the far left and right of the list.
|
||||
#[default]
|
||||
FullWidth,
|
||||
Inset,
|
||||
}
|
||||
|
||||
#[derive(Element, Clone)]
|
||||
pub struct ListHeader<S: 'static + Send + Sync + Clone> {
|
||||
state_type: PhantomData<S>,
|
||||
label: &'static str,
|
||||
left_icon: Option<Icon>,
|
||||
variant: ListItemVariant,
|
||||
state: InteractionState,
|
||||
toggleable: Toggleable,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync + Clone> ListHeader<S> {
|
||||
pub fn new(label: &'static str) -> Self {
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
label,
|
||||
left_icon: None,
|
||||
variant: ListItemVariant::default(),
|
||||
state: InteractionState::default(),
|
||||
toggleable: Toggleable::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
|
||||
self.toggleable = toggle.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_toggleable(mut self, toggleable: Toggleable) -> Self {
|
||||
self.toggleable = toggleable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||
self.left_icon = left_icon;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: InteractionState) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
|
||||
fn disclosure_control(&self) -> Div<S> {
|
||||
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
|
||||
let is_toggled = Toggleable::is_toggled(&self.toggleable);
|
||||
|
||||
match (is_toggleable, is_toggled) {
|
||||
(false, _) => div(),
|
||||
(_, true) => div().child(IconElement::new(Icon::ChevronRight).color(IconColor::Muted)),
|
||||
(_, false) => div().child(IconElement::new(Icon::ChevronDown).size(IconSize::Small)),
|
||||
}
|
||||
}
|
||||
|
||||
fn background_color(&self, cx: &WindowContext) -> Hsla {
|
||||
let theme = theme(cx);
|
||||
let system_color = SystemColor::new();
|
||||
|
||||
match self.state {
|
||||
InteractionState::Hovered => theme.lowest.base.hovered.background,
|
||||
InteractionState::Active => theme.lowest.base.pressed.background,
|
||||
InteractionState::Enabled => theme.lowest.on.default.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 render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
let theme = theme(cx);
|
||||
let token = token();
|
||||
let system_color = SystemColor::new();
|
||||
let background_color = self.background_color(cx);
|
||||
|
||||
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
|
||||
let is_toggled = Toggleable::is_toggled(&self.toggleable);
|
||||
|
||||
let disclosure_control = self.disclosure_control();
|
||||
|
||||
h_stack()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.fill(background_color)
|
||||
.when(self.state == InteractionState::Focused, |this| {
|
||||
this.border()
|
||||
.border_color(theme.lowest.accent.default.border)
|
||||
})
|
||||
.relative()
|
||||
.py_1()
|
||||
.child(
|
||||
div()
|
||||
.h_6()
|
||||
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||
.flex()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(IconColor::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(
|
||||
Label::new(self.label.clone())
|
||||
.color(LabelColor::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.child(disclosure_control),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Element, Clone)]
|
||||
pub struct ListSubHeader<S: 'static + Send + Sync + Clone> {
|
||||
state_type: PhantomData<S>,
|
||||
label: &'static str,
|
||||
left_icon: Option<Icon>,
|
||||
variant: ListItemVariant,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync + Clone> ListSubHeader<S> {
|
||||
pub fn new(label: &'static str) -> Self {
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
label,
|
||||
left_icon: None,
|
||||
variant: ListItemVariant::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||
self.left_icon = left_icon;
|
||||
self
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
let theme = theme(cx);
|
||||
let token = token();
|
||||
|
||||
h_stack().flex_1().w_full().relative().py_1().child(
|
||||
div()
|
||||
.h_6()
|
||||
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||
.flex()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(IconColor::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(
|
||||
Label::new(self.label.clone())
|
||||
.color(LabelColor::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum LeftContent {
|
||||
Icon(Icon),
|
||||
Avatar(&'static str),
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum ListEntrySize {
|
||||
#[default]
|
||||
Small,
|
||||
Medium,
|
||||
}
|
||||
|
||||
#[derive(Clone, Element)]
|
||||
pub enum ListItem<S: 'static + Send + Sync + Clone> {
|
||||
Entry(ListEntry<S>),
|
||||
Separator(ListSeparator<S>),
|
||||
Header(ListSubHeader<S>),
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync + Clone> From<ListEntry<S>> for ListItem<S> {
|
||||
fn from(entry: ListEntry<S>) -> Self {
|
||||
Self::Entry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync + Clone> From<ListSeparator<S>> for ListItem<S> {
|
||||
fn from(entry: ListSeparator<S>) -> Self {
|
||||
Self::Separator(entry)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync + Clone> From<ListSubHeader<S>> for ListItem<S> {
|
||||
fn from(entry: ListSubHeader<S>) -> Self {
|
||||
Self::Header(entry)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync + Clone> ListItem<S> {
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
match self {
|
||||
ListItem::Entry(entry) => div().child(entry.render(cx)),
|
||||
ListItem::Separator(separator) => div().child(separator.render(cx)),
|
||||
ListItem::Header(header) => div().child(header.render(cx)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(label: Label<S>) -> Self {
|
||||
Self::Entry(ListEntry::new(label))
|
||||
}
|
||||
|
||||
pub fn as_entry(&mut self) -> Option<&mut ListEntry<S>> {
|
||||
if let Self::Entry(entry) = self {
|
||||
Some(entry)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Element, Clone)]
|
||||
pub struct ListEntry<S: 'static + Send + Sync + Clone> {
|
||||
disclosure_control_style: DisclosureControlVisibility,
|
||||
indent_level: u32,
|
||||
label: Label<S>,
|
||||
left_content: Option<LeftContent>,
|
||||
variant: ListItemVariant,
|
||||
size: ListEntrySize,
|
||||
state: InteractionState,
|
||||
toggle: Option<ToggleState>,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
|
||||
pub fn new(label: Label<S>) -> Self {
|
||||
Self {
|
||||
disclosure_control_style: DisclosureControlVisibility::default(),
|
||||
indent_level: 0,
|
||||
label,
|
||||
variant: ListItemVariant::default(),
|
||||
left_content: None,
|
||||
size: ListEntrySize::default(),
|
||||
state: InteractionState::default(),
|
||||
toggle: None,
|
||||
}
|
||||
}
|
||||
pub fn variant(mut self, variant: ListItemVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
pub fn indent_level(mut self, indent_level: u32) -> Self {
|
||||
self.indent_level = indent_level;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
|
||||
self.toggle = Some(toggle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_content(mut self, left_content: LeftContent) -> Self {
|
||||
self.left_content = Some(left_content);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Icon) -> Self {
|
||||
self.left_content = Some(LeftContent::Icon(left_icon));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_avatar(mut self, left_avatar: &'static str) -> Self {
|
||||
self.left_content = Some(LeftContent::Avatar(left_avatar));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: InteractionState) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: ListEntrySize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disclosure_control_style(
|
||||
mut self,
|
||||
disclosure_control_style: DisclosureControlVisibility,
|
||||
) -> Self {
|
||||
self.disclosure_control_style = disclosure_control_style;
|
||||
self
|
||||
}
|
||||
|
||||
fn background_color(&self, cx: &WindowContext) -> Hsla {
|
||||
let theme = theme(cx);
|
||||
let system_color = SystemColor::new();
|
||||
|
||||
match self.state {
|
||||
InteractionState::Hovered => theme.lowest.base.hovered.background,
|
||||
InteractionState::Active => theme.lowest.base.pressed.background,
|
||||
InteractionState::Enabled => theme.lowest.on.default.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 disclosure_control(&mut self, cx: &mut ViewContext<S>) -> Option<impl Element<State = S>> {
|
||||
let theme = theme(cx);
|
||||
let token = token();
|
||||
|
||||
let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
|
||||
IconElement::new(Icon::ChevronDown)
|
||||
} else {
|
||||
IconElement::new(Icon::ChevronRight)
|
||||
}
|
||||
.color(IconColor::Muted)
|
||||
.size(IconSize::Small);
|
||||
|
||||
match (self.toggle, self.disclosure_control_style) {
|
||||
(Some(_), DisclosureControlVisibility::OnHover) => {
|
||||
Some(
|
||||
div()
|
||||
.absolute()
|
||||
// .neg_left_5()
|
||||
.child(disclosure_control_icon),
|
||||
)
|
||||
}
|
||||
(Some(_), DisclosureControlVisibility::Always) => {
|
||||
Some(div().child(disclosure_control_icon))
|
||||
}
|
||||
(None, _) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
let theme = theme(cx);
|
||||
let token = token();
|
||||
let system_color = SystemColor::new();
|
||||
let background_color = self.background_color(cx);
|
||||
|
||||
let left_content = match self.left_content {
|
||||
Some(LeftContent::Icon(i)) => {
|
||||
Some(h_stack().child(IconElement::new(i).size(IconSize::Small)))
|
||||
}
|
||||
Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let sized_item = match self.size {
|
||||
ListEntrySize::Small => div().h_6(),
|
||||
ListEntrySize::Medium => div().h_7(),
|
||||
};
|
||||
|
||||
div()
|
||||
.fill(background_color)
|
||||
.when(self.state == InteractionState::Focused, |this| {
|
||||
this.border()
|
||||
.border_color(theme.lowest.accent.default.border)
|
||||
})
|
||||
.relative()
|
||||
.py_1()
|
||||
.child(
|
||||
sized_item
|
||||
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||
// .ml(rems(0.75 * self.indent_level as f32))
|
||||
.children((0..self.indent_level).map(|_| {
|
||||
div()
|
||||
// .w(token.list_indent_depth)
|
||||
.h_full()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.child(h_stack().child(div().w_px().h_full()).child(
|
||||
div().w_px().h_full().fill(theme.middle.base.default.border),
|
||||
))
|
||||
}))
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.relative()
|
||||
.children(self.disclosure_control(cx))
|
||||
.children(left_content)
|
||||
.child(self.label.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Element)]
|
||||
pub struct ListSeparator<S: 'static + Send + Sync> {
|
||||
state_type: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync> ListSeparator<S> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
let theme = theme(cx);
|
||||
|
||||
div().h_px().w_full().fill(theme.lowest.base.default.border)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Element)]
|
||||
pub struct List<S: 'static + Send + Sync + Clone> {
|
||||
items: Vec<ListItem<S>>,
|
||||
empty_message: &'static str,
|
||||
header: Option<ListHeader<S>>,
|
||||
toggleable: Toggleable,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync + Clone> List<S> {
|
||||
pub fn new(items: Vec<ListItem<S>>) -> Self {
|
||||
Self {
|
||||
items,
|
||||
empty_message: "No items",
|
||||
header: None,
|
||||
toggleable: Toggleable::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_message(mut self, empty_message: &'static str) -> Self {
|
||||
self.empty_message = empty_message;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header(mut self, header: ListHeader<S>) -> Self {
|
||||
self.header = Some(header);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
|
||||
self.toggleable = toggle.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
let theme = theme(cx);
|
||||
let token = token();
|
||||
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
|
||||
let is_toggled = Toggleable::is_toggled(&self.toggleable);
|
||||
|
||||
let list_content = match (self.items.is_empty(), is_toggled) {
|
||||
(_, false) => div(),
|
||||
(false, _) => div().children(self.items.iter().cloned()),
|
||||
(true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)),
|
||||
};
|
||||
|
||||
v_stack()
|
||||
.py_1()
|
||||
.children(
|
||||
self.header
|
||||
.clone()
|
||||
.map(|header| header.set_toggleable(self.toggleable)),
|
||||
)
|
||||
.child(list_content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use gpui3::AbsoluteLength;
|
||||
|
||||
use crate::{prelude::*, theme};
|
||||
use crate::{token, v_stack};
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum PanelAllowedSides {
|
||||
LeftOnly,
|
||||
RightOnly,
|
||||
BottomOnly,
|
||||
#[default]
|
||||
LeftAndRight,
|
||||
All,
|
||||
}
|
||||
|
||||
impl PanelAllowedSides {
|
||||
/// Return a `HashSet` that contains the allowable `PanelSide`s.
|
||||
pub fn allowed_sides(&self) -> HashSet<PanelSide> {
|
||||
match self {
|
||||
Self::LeftOnly => HashSet::from_iter([PanelSide::Left]),
|
||||
Self::RightOnly => HashSet::from_iter([PanelSide::Right]),
|
||||
Self::BottomOnly => HashSet::from_iter([PanelSide::Bottom]),
|
||||
Self::LeftAndRight => HashSet::from_iter([PanelSide::Left, PanelSide::Right]),
|
||||
Self::All => HashSet::from_iter([PanelSide::Left, PanelSide::Right, PanelSide::Bottom]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum PanelSide {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Element)]
|
||||
pub struct Panel<S: 'static + Send + Sync> {
|
||||
state_type: PhantomData<S>,
|
||||
scroll_state: ScrollState,
|
||||
current_side: PanelSide,
|
||||
/// Defaults to PanelAllowedSides::LeftAndRight
|
||||
allowed_sides: PanelAllowedSides,
|
||||
initial_width: AbsoluteLength,
|
||||
width: Option<AbsoluteLength>,
|
||||
children: HackyChildren<S>,
|
||||
payload: HackyChildrenPayload,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync> Panel<S> {
|
||||
pub fn new(
|
||||
scroll_state: ScrollState,
|
||||
children: HackyChildren<S>,
|
||||
payload: HackyChildrenPayload,
|
||||
) -> Self {
|
||||
let token = token();
|
||||
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
scroll_state,
|
||||
current_side: PanelSide::default(),
|
||||
allowed_sides: PanelAllowedSides::default(),
|
||||
initial_width: token.default_panel_size,
|
||||
width: None,
|
||||
children,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initial_width(mut self, initial_width: AbsoluteLength) -> Self {
|
||||
self.initial_width = initial_width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: AbsoluteLength) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn allowed_sides(mut self, allowed_sides: PanelAllowedSides) -> Self {
|
||||
self.allowed_sides = allowed_sides;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn side(mut self, side: PanelSide) -> Self {
|
||||
let allowed_sides = self.allowed_sides.allowed_sides();
|
||||
|
||||
if allowed_sides.contains(&side) {
|
||||
self.current_side = side;
|
||||
} else {
|
||||
panic!(
|
||||
"The panel side {:?} was not added as allowed before it was set.",
|
||||
side
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
let token = token();
|
||||
let theme = theme(cx);
|
||||
|
||||
let panel_base;
|
||||
let current_width = self.width.unwrap_or(self.initial_width);
|
||||
|
||||
match self.current_side {
|
||||
PanelSide::Left => {
|
||||
panel_base = v_stack()
|
||||
.flex_initial()
|
||||
.h_full()
|
||||
// .w(current_width)
|
||||
.w_64()
|
||||
.fill(theme.middle.base.default.background)
|
||||
.border_r()
|
||||
.border_color(theme.middle.base.default.border);
|
||||
}
|
||||
PanelSide::Right => {
|
||||
panel_base = v_stack()
|
||||
.flex_initial()
|
||||
.h_full()
|
||||
// .w(current_width)
|
||||
.w_64()
|
||||
.fill(theme.middle.base.default.background)
|
||||
.border_l()
|
||||
.border_color(theme.middle.base.default.border);
|
||||
}
|
||||
PanelSide::Bottom => {
|
||||
panel_base = v_stack()
|
||||
.flex_initial()
|
||||
.w_full()
|
||||
// .h(current_width)
|
||||
.h_64()
|
||||
.fill(theme.middle.base.default.background)
|
||||
.border_t()
|
||||
.border_color(theme.middle.base.default.border);
|
||||
}
|
||||
}
|
||||
|
||||
panel_base.children_any((self.children)(cx, self.payload.as_ref()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use gpui3::Element;
|
||||
|
||||
pub trait ElementExt<S: 'static + Send + Sync>: Element<State = S> {
|
||||
/// Applies a given function `then` to the current element if `condition` is true.
|
||||
/// This function is used to conditionally modify the element based on a given condition.
|
||||
/// If `condition` is false, it just returns the current element as it is.
|
||||
fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if condition {
|
||||
self = then(self);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync, E: Element<State = S>> ElementExt<S> for E {}
|
||||
@@ -0,0 +1,9 @@
|
||||
mod avatar;
|
||||
mod icon;
|
||||
mod label;
|
||||
mod stack;
|
||||
|
||||
pub use avatar::*;
|
||||
pub use icon::*;
|
||||
pub use label::*;
|
||||
pub use stack::*;
|
||||
@@ -0,0 +1,44 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use gpui3::{img, ArcCow};
|
||||
|
||||
use crate::theme::theme;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Element, Clone)]
|
||||
pub struct Avatar<S: 'static + Send + Sync> {
|
||||
state_type: PhantomData<S>,
|
||||
src: ArcCow<'static, str>,
|
||||
shape: Shape,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync> Avatar<S> {
|
||||
pub fn new(src: impl Into<ArcCow<'static, str>>) -> Self {
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
src: src.into(),
|
||||
shape: Shape::Circle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shape(mut self, shape: Shape) -> Self {
|
||||
self.shape = shape;
|
||||
self
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
let theme = theme(cx);
|
||||
|
||||
let mut img = img();
|
||||
|
||||
if self.shape == Shape::Circle {
|
||||
img = img.rounded_full();
|
||||
} else {
|
||||
img = img.rounded_md();
|
||||
}
|
||||
|
||||
img.uri(self.src.clone())
|
||||
.size_4()
|
||||
.fill(theme.middle.warning.default.foreground)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui3::{svg, Hsla};
|
||||
use strum::EnumIter;
|
||||
|
||||
use crate::theme::{theme, Theme};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum IconSize {
|
||||
Small,
|
||||
#[default]
|
||||
Large,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum IconColor {
|
||||
#[default]
|
||||
Default,
|
||||
Muted,
|
||||
Disabled,
|
||||
Placeholder,
|
||||
Accent,
|
||||
Error,
|
||||
Warning,
|
||||
Success,
|
||||
Info,
|
||||
}
|
||||
|
||||
impl IconColor {
|
||||
pub fn color(self, theme: Arc<Theme>) -> Hsla {
|
||||
match self {
|
||||
IconColor::Default => theme.lowest.base.default.foreground,
|
||||
IconColor::Muted => theme.lowest.variant.default.foreground,
|
||||
IconColor::Disabled => theme.lowest.base.disabled.foreground,
|
||||
IconColor::Placeholder => theme.lowest.base.disabled.foreground,
|
||||
IconColor::Accent => theme.lowest.accent.default.foreground,
|
||||
IconColor::Error => theme.lowest.negative.default.foreground,
|
||||
IconColor::Warning => theme.lowest.warning.default.foreground,
|
||||
IconColor::Success => theme.lowest.positive.default.foreground,
|
||||
IconColor::Info => theme.lowest.accent.default.foreground,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone, EnumIter)]
|
||||
pub enum Icon {
|
||||
Ai,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
AudioOff,
|
||||
AudioOn,
|
||||
Bolt,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
Close,
|
||||
ExclamationTriangle,
|
||||
File,
|
||||
FileGeneric,
|
||||
FileDoc,
|
||||
FileGit,
|
||||
FileLock,
|
||||
FileRust,
|
||||
FileToml,
|
||||
FileTree,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
FolderX,
|
||||
#[default]
|
||||
Hash,
|
||||
InlayHint,
|
||||
MagicWand,
|
||||
MagnifyingGlass,
|
||||
Maximize,
|
||||
Menu,
|
||||
MessageBubbles,
|
||||
Mic,
|
||||
MicMute,
|
||||
Plus,
|
||||
Quote,
|
||||
Screen,
|
||||
SelectAll,
|
||||
Split,
|
||||
SplitMessage,
|
||||
Terminal,
|
||||
XCircle,
|
||||
Copilot,
|
||||
Envelope,
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn path(self) -> &'static str {
|
||||
match self {
|
||||
Icon::Ai => "icons/ai.svg",
|
||||
Icon::ArrowLeft => "icons/arrow_left.svg",
|
||||
Icon::ArrowRight => "icons/arrow_right.svg",
|
||||
Icon::ArrowUpRight => "icons/arrow_up_right.svg",
|
||||
Icon::AudioOff => "icons/speaker-off.svg",
|
||||
Icon::AudioOn => "icons/speaker-loud.svg",
|
||||
Icon::Bolt => "icons/bolt.svg",
|
||||
Icon::ChevronDown => "icons/chevron_down.svg",
|
||||
Icon::ChevronLeft => "icons/chevron_left.svg",
|
||||
Icon::ChevronRight => "icons/chevron_right.svg",
|
||||
Icon::ChevronUp => "icons/chevron_up.svg",
|
||||
Icon::Close => "icons/x.svg",
|
||||
Icon::ExclamationTriangle => "icons/warning.svg",
|
||||
Icon::File => "icons/file.svg",
|
||||
Icon::FileGeneric => "icons/file_icons/file.svg",
|
||||
Icon::FileDoc => "icons/file_icons/book.svg",
|
||||
Icon::FileGit => "icons/file_icons/git.svg",
|
||||
Icon::FileLock => "icons/file_icons/lock.svg",
|
||||
Icon::FileRust => "icons/file_icons/rust.svg",
|
||||
Icon::FileToml => "icons/file_icons/toml.svg",
|
||||
Icon::FileTree => "icons/project.svg",
|
||||
Icon::Folder => "icons/file_icons/folder.svg",
|
||||
Icon::FolderOpen => "icons/file_icons/folder_open.svg",
|
||||
Icon::FolderX => "icons/stop_sharing.svg",
|
||||
Icon::Hash => "icons/hash.svg",
|
||||
Icon::InlayHint => "icons/inlay_hint.svg",
|
||||
Icon::MagicWand => "icons/magic-wand.svg",
|
||||
Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
|
||||
Icon::Maximize => "icons/maximize.svg",
|
||||
Icon::Menu => "icons/menu.svg",
|
||||
Icon::MessageBubbles => "icons/conversations.svg",
|
||||
Icon::Mic => "icons/mic.svg",
|
||||
Icon::MicMute => "icons/mic-mute.svg",
|
||||
Icon::Plus => "icons/plus.svg",
|
||||
Icon::Quote => "icons/quote.svg",
|
||||
Icon::Screen => "icons/desktop.svg",
|
||||
Icon::SelectAll => "icons/select-all.svg",
|
||||
Icon::Split => "icons/split.svg",
|
||||
Icon::SplitMessage => "icons/split_message.svg",
|
||||
Icon::Terminal => "icons/terminal.svg",
|
||||
Icon::XCircle => "icons/error.svg",
|
||||
Icon::Copilot => "icons/copilot.svg",
|
||||
Icon::Envelope => "icons/feedback.svg",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Element, Clone)]
|
||||
pub struct IconElement<S: 'static + Send + Sync> {
|
||||
state_type: PhantomData<S>,
|
||||
icon: Icon,
|
||||
color: IconColor,
|
||||
size: IconSize,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync> IconElement<S> {
|
||||
pub fn new(icon: Icon) -> Self {
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
icon,
|
||||
color: IconColor::default(),
|
||||
size: IconSize::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: IconColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: IconSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
let theme = theme(cx);
|
||||
let fill = self.color.color(theme);
|
||||
|
||||
let sized_svg = match self.size {
|
||||
IconSize::Small => svg().size_3p5(),
|
||||
IconSize::Large => svg().size_4(),
|
||||
};
|
||||
|
||||
sized_svg.flex_none().path(self.icon.path()).fill(fill)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use gpui3::{Hsla, WindowContext};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::theme::theme;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum LabelColor {
|
||||
#[default]
|
||||
Default,
|
||||
Muted,
|
||||
Created,
|
||||
Modified,
|
||||
Deleted,
|
||||
Disabled,
|
||||
Hidden,
|
||||
Placeholder,
|
||||
Accent,
|
||||
}
|
||||
|
||||
impl LabelColor {
|
||||
pub fn hsla(&self, cx: &WindowContext) -> Hsla {
|
||||
let theme = theme(cx);
|
||||
|
||||
match self {
|
||||
Self::Default => theme.middle.base.default.foreground,
|
||||
Self::Muted => theme.middle.variant.default.foreground,
|
||||
Self::Created => theme.middle.positive.default.foreground,
|
||||
Self::Modified => theme.middle.warning.default.foreground,
|
||||
Self::Deleted => theme.middle.negative.default.foreground,
|
||||
Self::Disabled => theme.middle.base.disabled.foreground,
|
||||
Self::Hidden => theme.middle.variant.default.foreground,
|
||||
Self::Placeholder => theme.middle.base.disabled.foreground,
|
||||
Self::Accent => theme.middle.accent.default.foreground,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
pub enum LabelSize {
|
||||
#[default]
|
||||
Default,
|
||||
Small,
|
||||
}
|
||||
|
||||
#[derive(Element, Clone)]
|
||||
pub struct Label<S: 'static + Send + Sync + Clone> {
|
||||
state_type: PhantomData<S>,
|
||||
label: String,
|
||||
color: LabelColor,
|
||||
size: LabelSize,
|
||||
highlight_indices: Vec<usize>,
|
||||
strikethrough: bool,
|
||||
}
|
||||
|
||||
impl<S: 'static + Send + Sync + Clone> Label<S> {
|
||||
pub fn new<L>(label: L) -> Self
|
||||
where
|
||||
L: Into<String>,
|
||||
{
|
||||
Self {
|
||||
state_type: PhantomData,
|
||||
label: label.into(),
|
||||
color: LabelColor::Default,
|
||||
size: LabelSize::Default,
|
||||
highlight_indices: Vec::new(),
|
||||
strikethrough: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: LabelColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: LabelSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
|
||||
self.highlight_indices = indices;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
|
||||
let theme = theme(cx);
|
||||
|
||||
let highlight_color = theme.lowest.accent.default.foreground;
|
||||
|
||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
||||
|
||||
let mut runs: SmallVec<[Run; 8]> = SmallVec::new();
|
||||
|
||||
for (char_ix, char) in self.label.char_indices() {
|
||||
let mut color = self.color.hsla(cx);
|
||||
|
||||
if let Some(highlight_ix) = highlight_indices.peek() {
|
||||
if char_ix == *highlight_ix {
|
||||
color = highlight_color;
|
||||
|
||||
highlight_indices.next();
|
||||
}
|
||||
}
|
||||
|
||||
let last_run = runs.last_mut();
|
||||
|
||||
let start_new_run = if let Some(last_run) = last_run {
|
||||
if color == last_run.color {
|
||||
last_run.text.push(char);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if start_new_run {
|
||||
runs.push(Run {
|
||||
text: char.to_string(),
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
div()
|
||||
.flex()
|
||||
// .when(self.strikethrough, |this| {
|
||||
// this.relative().child(
|
||||
// div()
|
||||
// .absolute()
|
||||
// .top_px()
|
||||
// .my_auto()
|
||||
// .w_full()
|
||||
// .h_px()
|
||||
// .fill(LabelColor::Hidden.hsla(cx)),
|
||||
// )
|
||||
// })
|
||||
.children(runs.into_iter().map(|run| {
|
||||
let mut div = div();
|
||||
|
||||
if self.size == LabelSize::Small {
|
||||
div = div.text_xs();
|
||||
} else {
|
||||
div = div.text_sm();
|
||||
}
|
||||
|
||||
div.text_color(run.color).child(run.text)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// A run of text that receives the same style.
|
||||
struct Run {
|
||||
pub text: String,
|
||||
pub color: Hsla,
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use gpui3::{div, Div};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub trait Stack: StyleHelpers {
|
||||
/// Horizontally stacks elements.
|
||||
fn h_stack(self) -> Self {
|
||||
self.flex().flex_row().items_center()
|
||||
}
|
||||
|
||||
/// Vertically stacks elements.
|
||||
fn v_stack(self) -> Self {
|
||||
self.flex().flex_col()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Stack for Div<S> {}
|
||||
|
||||
/// Horizontally stacks elements.
|
||||
///
|
||||
/// Sets `flex()`, `flex_row()`, `items_center()`
|
||||
pub fn h_stack<S: 'static>() -> Div<S> {
|
||||
div().h_stack()
|
||||
}
|
||||
|
||||
/// Vertically stacks elements.
|
||||
///
|
||||
/// Sets `flex()`, `flex_col()`
|
||||
pub fn v_stack<S: 'static>() -> Div<S> {
|
||||
div().v_stack()
|
||||
}
|
||||
+16
-12
@@ -1,14 +1,18 @@
|
||||
pub fn add(left: usize, right: usize) -> usize {
|
||||
left + right
|
||||
}
|
||||
#![allow(dead_code, unused_variables)]
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
mod children;
|
||||
mod components;
|
||||
mod element_ext;
|
||||
mod elements;
|
||||
pub mod prelude;
|
||||
mod theme;
|
||||
mod tokens;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
pub use children::*;
|
||||
pub use components::*;
|
||||
pub use element_ext::*;
|
||||
pub use elements::*;
|
||||
pub use prelude::*;
|
||||
pub use tokens::*;
|
||||
|
||||
pub use crate::theme::*;
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
pub use gpui3::{
|
||||
div, Element, IntoAnyElement, ParentElement, ScrollState, StyleHelpers, ViewContext,
|
||||
WindowContext,
|
||||
};
|
||||
|
||||
pub use crate::{HackyChildren, HackyChildrenPayload, ElementExt};
|
||||
|
||||
use gpui3::{hsla, rgb, Hsla};
|
||||
use strum::EnumIter;
|
||||
|
||||
use crate::theme::{theme, Theme};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SystemColor {
|
||||
pub transparent: Hsla,
|
||||
pub mac_os_traffic_light_red: Hsla,
|
||||
pub mac_os_traffic_light_yellow: Hsla,
|
||||
pub mac_os_traffic_light_green: Hsla,
|
||||
}
|
||||
|
||||
impl SystemColor {
|
||||
pub fn new() -> SystemColor {
|
||||
SystemColor {
|
||||
transparent: hsla(0.0, 0.0, 0.0, 0.0),
|
||||
mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
|
||||
mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
|
||||
mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
|
||||
}
|
||||
}
|
||||
pub fn color(&self) -> Hsla {
|
||||
self.transparent
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
|
||||
pub enum HighlightColor {
|
||||
#[default]
|
||||
Default,
|
||||
Comment,
|
||||
String,
|
||||
Function,
|
||||
Keyword,
|
||||
}
|
||||
|
||||
impl HighlightColor {
|
||||
pub fn hsla(&self, theme: &Theme) -> Hsla {
|
||||
let system_color = SystemColor::new();
|
||||
|
||||
match self {
|
||||
Self::Default => theme
|
||||
.syntax
|
||||
.get("primary")
|
||||
.expect("no theme.syntax.primary")
|
||||
.clone(),
|
||||
Self::Comment => theme
|
||||
.syntax
|
||||
.get("comment")
|
||||
.expect("no theme.syntax.comment")
|
||||
.clone(),
|
||||
Self::String => theme
|
||||
.syntax
|
||||
.get("string")
|
||||
.expect("no theme.syntax.string")
|
||||
.clone(),
|
||||
Self::Function => theme
|
||||
.syntax
|
||||
.get("function")
|
||||
.expect("no theme.syntax.function")
|
||||
.clone(),
|
||||
Self::Keyword => theme
|
||||
.syntax
|
||||
.get("keyword")
|
||||
.expect("no theme.syntax.keyword")
|
||||
.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum FileSystemStatus {
|
||||
#[default]
|
||||
None,
|
||||
Conflict,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
impl FileSystemStatus {
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::None => "None".to_string(),
|
||||
Self::Conflict => "Conflict".to_string(),
|
||||
Self::Deleted => "Deleted".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum GitStatus {
|
||||
#[default]
|
||||
None,
|
||||
Created,
|
||||
Modified,
|
||||
Deleted,
|
||||
Conflict,
|
||||
Renamed,
|
||||
}
|
||||
|
||||
impl GitStatus {
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::None => "None".to_string(),
|
||||
Self::Created => "Created".to_string(),
|
||||
Self::Modified => "Modified".to_string(),
|
||||
Self::Deleted => "Deleted".to_string(),
|
||||
Self::Conflict => "Conflict".to_string(),
|
||||
Self::Renamed => "Renamed".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hsla(&self, cx: &WindowContext) -> Hsla {
|
||||
let theme = theme(cx);
|
||||
let system_color = SystemColor::new();
|
||||
|
||||
match self {
|
||||
Self::None => system_color.transparent,
|
||||
Self::Created => theme.lowest.positive.default.foreground,
|
||||
Self::Modified => theme.lowest.warning.default.foreground,
|
||||
Self::Deleted => theme.lowest.negative.default.foreground,
|
||||
Self::Conflict => theme.lowest.warning.default.foreground,
|
||||
Self::Renamed => theme.lowest.accent.default.foreground,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum DiagnosticStatus {
|
||||
#[default]
|
||||
None,
|
||||
Error,
|
||||
Warning,
|
||||
Info,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum IconSide {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum OrderMethod {
|
||||
#[default]
|
||||
Ascending,
|
||||
Descending,
|
||||
MostRecent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum Shape {
|
||||
#[default]
|
||||
Circle,
|
||||
RoundedRectangle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum DisclosureControlVisibility {
|
||||
#[default]
|
||||
OnHover,
|
||||
Always,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
|
||||
pub enum InteractionState {
|
||||
#[default]
|
||||
Enabled,
|
||||
Hovered,
|
||||
Active,
|
||||
Focused,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl InteractionState {
|
||||
pub fn if_enabled(&self, enabled: bool) -> Self {
|
||||
if enabled {
|
||||
*self
|
||||
} else {
|
||||
InteractionState::Disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
pub enum SelectedState {
|
||||
#[default]
|
||||
Unselected,
|
||||
PartiallySelected,
|
||||
Selected,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Toggleable {
|
||||
Toggleable(ToggleState),
|
||||
#[default]
|
||||
NotToggleable,
|
||||
}
|
||||
|
||||
impl Toggleable {
|
||||
pub fn is_toggled(&self) -> bool {
|
||||
match self {
|
||||
Self::Toggleable(ToggleState::Toggled) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToggleState> for Toggleable {
|
||||
fn from(state: ToggleState) -> Self {
|
||||
Self::Toggleable(state)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ToggleState {
|
||||
/// The "on" state of a toggleable element.
|
||||
///
|
||||
/// Example:
|
||||
/// - A collasable list that is currently expanded
|
||||
/// - A toggle button that is currently on.
|
||||
Toggled,
|
||||
/// The "off" state of a toggleable element.
|
||||
///
|
||||
/// Example:
|
||||
/// - A collasable list that is currently collapsed
|
||||
/// - A toggle button that is currently off.
|
||||
#[default]
|
||||
NotToggled,
|
||||
}
|
||||
|
||||
impl From<Toggleable> for ToggleState {
|
||||
fn from(toggleable: Toggleable) -> Self {
|
||||
match toggleable {
|
||||
Toggleable::Toggleable(state) => state,
|
||||
Toggleable::NotToggleable => ToggleState::NotToggled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for ToggleState {
|
||||
fn from(toggled: bool) -> Self {
|
||||
if toggled {
|
||||
ToggleState::Toggled
|
||||
} else {
|
||||
ToggleState::NotToggled
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui3::{
|
||||
BorrowAppContext, Bounds, Element, Hsla, LayoutId, Pixels, Result, ViewContext, WindowContext,
|
||||
};
|
||||
use serde::{de::Visitor, Deserialize, Deserializer};
|
||||
|
||||
#[derive(Deserialize, Clone, Default, Debug)]
|
||||
pub struct Theme {
|
||||
pub name: String,
|
||||
pub is_light: bool,
|
||||
pub lowest: Layer,
|
||||
pub middle: Layer,
|
||||
pub highest: Layer,
|
||||
pub popover_shadow: Shadow,
|
||||
pub modal_shadow: Shadow,
|
||||
#[serde(deserialize_with = "deserialize_player_colors")]
|
||||
pub players: Vec<PlayerColors>,
|
||||
#[serde(deserialize_with = "deserialize_syntax_colors")]
|
||||
pub syntax: HashMap<String, Hsla>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Default, Debug)]
|
||||
pub struct Layer {
|
||||
pub base: StyleSet,
|
||||
pub variant: StyleSet,
|
||||
pub on: StyleSet,
|
||||
pub accent: StyleSet,
|
||||
pub positive: StyleSet,
|
||||
pub warning: StyleSet,
|
||||
pub negative: StyleSet,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Default, Debug)]
|
||||
pub struct StyleSet {
|
||||
#[serde(rename = "default")]
|
||||
pub default: ContainerColors,
|
||||
pub hovered: ContainerColors,
|
||||
pub pressed: ContainerColors,
|
||||
pub active: ContainerColors,
|
||||
pub disabled: ContainerColors,
|
||||
pub inverted: ContainerColors,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Default, Debug)]
|
||||
pub struct ContainerColors {
|
||||
pub background: Hsla,
|
||||
pub foreground: Hsla,
|
||||
pub border: Hsla,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Default, Debug)]
|
||||
pub struct PlayerColors {
|
||||
pub selection: Hsla,
|
||||
pub cursor: Hsla,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Default, Debug)]
|
||||
pub struct Shadow {
|
||||
pub blur: u8,
|
||||
pub color: Hsla,
|
||||
pub offset: Vec<u8>,
|
||||
}
|
||||
|
||||
fn deserialize_player_colors<'de, D>(deserializer: D) -> Result<Vec<PlayerColors>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct PlayerArrayVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for PlayerArrayVisitor {
|
||||
type Value = Vec<PlayerColors>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an object with integer keys")
|
||||
}
|
||||
|
||||
fn visit_map<A: serde::de::MapAccess<'de>>(
|
||||
self,
|
||||
mut map: A,
|
||||
) -> Result<Self::Value, A::Error> {
|
||||
let mut players = Vec::with_capacity(8);
|
||||
while let Some((key, value)) = map.next_entry::<usize, PlayerColors>()? {
|
||||
if key < 8 {
|
||||
players.push(value);
|
||||
} else {
|
||||
return Err(serde::de::Error::invalid_value(
|
||||
serde::de::Unexpected::Unsigned(key as u64),
|
||||
&"a key in range 0..7",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(players)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(PlayerArrayVisitor)
|
||||
}
|
||||
|
||||
fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result<HashMap<String, Hsla>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct ColorWrapper {
|
||||
color: Hsla,
|
||||
}
|
||||
|
||||
struct SyntaxVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for SyntaxVisitor {
|
||||
type Value = HashMap<String, Hsla>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a map with keys and objects with a single color field as values")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<HashMap<String, Hsla>, M::Error>
|
||||
where
|
||||
M: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut result = HashMap::new();
|
||||
while let Some(key) = map.next_key()? {
|
||||
let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla
|
||||
result.insert(key, wrapper.color);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_map(SyntaxVisitor)
|
||||
}
|
||||
|
||||
pub fn themed<E, F>(theme: Theme, cx: &mut ViewContext<E::State>, build_child: F) -> Themed<E>
|
||||
where
|
||||
E: Element,
|
||||
F: FnOnce(&mut ViewContext<E::State>) -> E,
|
||||
{
|
||||
let child = cx.with_state(theme.clone(), |cx| build_child(cx));
|
||||
Themed { theme, child }
|
||||
}
|
||||
|
||||
pub struct Themed<E> {
|
||||
pub(crate) theme: Theme,
|
||||
pub(crate) child: E,
|
||||
}
|
||||
|
||||
impl<E: Element> Element for Themed<E> {
|
||||
type State = E::State;
|
||||
type FrameState = E::FrameState;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
state: &mut E::State,
|
||||
cx: &mut ViewContext<E::State>,
|
||||
) -> anyhow::Result<(LayoutId, Self::FrameState)>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
cx.with_state(self.theme.clone(), |cx| self.child.layout(state, cx))
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::State,
|
||||
frame_state: &mut Self::FrameState,
|
||||
cx: &mut ViewContext<Self::State>,
|
||||
) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
cx.with_state(self.theme.clone(), |cx| {
|
||||
self.child.paint(bounds, state, frame_state, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn theme(cx: &WindowContext) -> Arc<Theme> {
|
||||
Arc::new(cx.state::<Theme>().clone())
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use gpui3::{hsla, rems, AbsoluteLength, Hsla};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Token {
|
||||
pub list_indent_depth: AbsoluteLength,
|
||||
pub default_panel_size: AbsoluteLength,
|
||||
pub state_hover_background: Hsla,
|
||||
pub state_active_background: Hsla,
|
||||
}
|
||||
|
||||
impl Default for Token {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
list_indent_depth: AbsoluteLength::Rems(rems(0.5)),
|
||||
default_panel_size: AbsoluteLength::Rems(rems(16.)),
|
||||
state_hover_background: hsla(0.0, 0.0, 0.0, 0.08),
|
||||
state_active_background: hsla(0.0, 0.0, 0.0, 0.16),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token() -> Token {
|
||||
Token::default()
|
||||
}
|
||||
Reference in New Issue
Block a user