Move UI out of storybook2 and into ui2

This commit is contained in:
Marshall Bowers
2023-10-06 17:07:59 -04:00
parent 1cf5cdbeca
commit bcad2f4e9e
26 changed files with 51 additions and 78 deletions
+7
View File
@@ -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>;
+5
View File
@@ -0,0 +1,5 @@
mod list;
mod panel;
pub use list::*;
pub use panel::*;
+519
View File
@@ -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)
}
}
+145
View File
@@ -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()))
}
}
+18
View File
@@ -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 {}
+9
View File
@@ -0,0 +1,9 @@
mod avatar;
mod icon;
mod label;
mod stack;
pub use avatar::*;
pub use icon::*;
pub use label::*;
pub use stack::*;
+44
View File
@@ -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)
}
}
+184
View File
@@ -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)
}
}
+165
View File
@@ -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,
}
+31
View File
@@ -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
View File
@@ -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::*;
+257
View File
@@ -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
}
}
}
+182
View File
@@ -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())
}
+24
View File
@@ -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()
}