Extract UI elements from storybook into new ui crate (#3008)

This PR extracts the various UI elements from the `storybook` crate into
a new `ui` library crate.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2023-09-21 19:25:35 -04:00 committed by GitHub
parent c252eae32e
commit baa07e935e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 154 additions and 117 deletions

View file

@ -17,6 +17,7 @@ serde.workspace = true
settings = { path = "../settings" }
simplelog = "0.9"
theme = { path = "../theme" }
ui = { path = "../ui" }
util = { path = "../util" }
[dev-dependencies]

View file

@ -1,10 +1,10 @@
use crate::theme::{theme, Theme};
use gpui2::{
elements::{div, div::ScrollState, img, svg},
style::{StyleHelpers, Styleable},
ArcCow, Element, IntoElement, ParentElement, ViewContext,
};
use std::marker::PhantomData;
use ui::{theme, Theme};
#[derive(Element)]
pub struct CollabPanelElement<V: 'static> {

View file

@ -1,97 +0,0 @@
use gpui2::{
elements::div, interactive::Interactive, platform::MouseButton, style::StyleHelpers, ArcCow,
Element, EventContext, IntoElement, ParentElement, ViewContext,
};
use std::{marker::PhantomData, rc::Rc};
struct ButtonHandlers<V, D> {
click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
}
impl<V, D> Default for ButtonHandlers<V, D> {
fn default() -> Self {
Self { click: None }
}
}
#[derive(Element)]
pub struct Button<V: 'static, D: 'static> {
handlers: ButtonHandlers<V, D>,
label: Option<ArcCow<'static, str>>,
icon: Option<ArcCow<'static, str>>,
data: Rc<D>,
view_type: PhantomData<V>,
}
// Impl block for buttons without data.
// See below for an impl block for any button.
impl<V: 'static> Button<V, ()> {
fn new() -> Self {
Self {
handlers: ButtonHandlers::default(),
label: None,
icon: None,
data: Rc::new(()),
view_type: PhantomData,
}
}
pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
Button {
handlers: ButtonHandlers::default(),
label: self.label,
icon: self.icon,
data: Rc::new(data),
view_type: PhantomData,
}
}
}
// Impl block for button regardless of its data type.
impl<V: 'static, D: 'static> Button<V, D> {
pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
self.label = Some(label.into());
self
}
pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn on_click(
mut self,
handler: impl Fn(&mut V, &D, &mut EventContext<V>) + 'static,
) -> Self {
self.handlers.click = Some(Rc::new(handler));
self
}
}
pub fn button<V>() -> Button<V, ()> {
Button::new()
}
impl<V: 'static, D: 'static> Button<V, D> {
fn render(
&mut self,
view: &mut V,
cx: &mut ViewContext<V>,
) -> impl IntoElement<V> + Interactive<V> {
// let colors = &cx.theme::<Theme>().colors;
let button = div()
// .fill(colors.error(0.5))
.h_4()
.children(self.label.clone());
if let Some(handler) = self.handlers.click.clone() {
let data = self.data.clone();
button.on_mouse_down(MouseButton::Left, move |view, event, cx| {
handler(view, data.as_ref(), cx)
})
} else {
button
}
}
}

View file

@ -1,22 +0,0 @@
use crate::theme::{Theme, Themed};
use gpui2::Element;
use std::marker::PhantomData;
pub trait ElementExt<V: 'static>: Element<V> {
fn themed(self, theme: Theme) -> Themed<V, Self>
where
Self: Sized;
}
impl<V: 'static, E: Element<V>> ElementExt<V> for E {
fn themed(self, theme: Theme) -> Themed<V, Self>
where
Self: Sized,
{
Themed {
child: self,
theme,
view_type: PhantomData,
}
}
}

View file

@ -1,55 +0,0 @@
#[derive(Default, PartialEq)]
pub enum ButtonVariant {
#[default]
Ghost,
Filled,
}
#[derive(Default, PartialEq)]
pub enum InputVariant {
#[default]
Ghost,
Filled,
}
#[derive(Default, PartialEq, Clone, Copy)]
pub enum Shape {
#[default]
Circle,
RoundedRectangle,
}
#[derive(Default, PartialEq, Clone, Copy)]
pub enum InteractionState {
#[default]
Enabled,
Hovered,
Active,
Focused,
Dragged,
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(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ToggleState {
Toggled,
NotToggled,
}

View file

@ -1,20 +1,14 @@
#![allow(dead_code, unused_variables)]
use crate::theme::Theme;
use ::theme as legacy_theme;
use element_ext::ElementExt;
use gpui2::{serde_json, vec2f, view, Element, RectF, ViewContext, WindowBounds};
use legacy_theme::ThemeSettings;
use log::LevelFilter;
use settings::{default_settings, SettingsStore};
use simplelog::SimpleLogger;
use ui::{ElementExt, Theme};
mod collab_panel;
mod components;
mod element_ext;
mod prelude;
mod theme;
mod ui;
mod workspace;
gpui2::actions! {

View file

@ -1,192 +0,0 @@
use gpui2::{
color::Hsla, element::Element, serde_json, AppContext, IntoElement, Vector2F, ViewContext,
WindowContext,
};
use serde::{de::Visitor, Deserialize, Deserializer};
use std::{collections::HashMap, fmt, marker::PhantomData};
use theme::ThemeSettings;
#[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)
}
#[derive(IntoElement)]
pub struct Themed<V: 'static, E: Element<V>> {
pub(crate) theme: Theme,
pub(crate) child: E,
pub(crate) view_type: PhantomData<V>,
}
impl<V: 'static, E: Element<V>> Element<V> for Themed<V, E> {
type PaintState = E::PaintState;
fn layout(
&mut self,
view: &mut V,
cx: &mut ViewContext<V>,
) -> anyhow::Result<(gpui2::LayoutId, Self::PaintState)>
where
Self: Sized,
{
cx.push_theme(self.theme.clone());
let result = self.child.layout(view, cx);
cx.pop_theme();
result
}
fn paint(
&mut self,
view: &mut V,
parent_origin: Vector2F,
layout: &gpui2::Layout,
state: &mut Self::PaintState,
cx: &mut ViewContext<V>,
) where
Self: Sized,
{
cx.push_theme(self.theme.clone());
self.child.paint(view, parent_origin, layout, state, cx);
cx.pop_theme();
}
}
fn preferred_theme<V: 'static>(cx: &AppContext) -> Theme {
settings::get::<ThemeSettings>(cx)
.theme
.deserialized_base_theme
.lock()
.get_or_insert_with(|| {
let theme: Theme =
serde_json::from_value(settings::get::<ThemeSettings>(cx).theme.base_theme.clone())
.unwrap();
Box::new(theme)
})
.downcast_ref::<Theme>()
.unwrap()
.clone()
}
pub fn theme<'a>(cx: &'a WindowContext) -> &'a Theme {
cx.theme::<Theme>()
}

View file

@ -1,7 +0,0 @@
mod component;
mod element;
mod module;
pub use component::*;
pub use element::*;
pub use module::*;

View file

@ -1,9 +0,0 @@
mod facepile;
mod follow_group;
mod list_item;
mod tab;
pub use facepile::*;
pub use follow_group::*;
pub use list_item::*;
pub use tab::*;

View file

@ -1,27 +0,0 @@
use crate::{theme::theme, ui::Avatar};
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct Facepile {
players: Vec<Avatar>,
}
pub fn facepile(players: Vec<Avatar>) -> Facepile {
Facepile { players }
}
impl Facepile {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let player_count = self.players.len();
let player_list = self.players.iter().enumerate().map(|(ix, player)| {
let isnt_last = ix < player_count - 1;
div()
.when(isnt_last, |div| div.neg_mr_1())
.child(player.clone())
});
div().p_1().flex().items_center().children(player_list)
}
}

View file

@ -1,52 +0,0 @@
use crate::theme::theme;
use crate::ui::{facepile, indicator, Avatar};
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct FollowGroup {
player: usize,
players: Vec<Avatar>,
}
pub fn follow_group(players: Vec<Avatar>) -> FollowGroup {
FollowGroup { player: 0, players }
}
impl FollowGroup {
pub fn player(mut self, player: usize) -> Self {
self.player = player;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let player_bg = theme.players[self.player].selection;
div()
.h_full()
.flex()
.flex_col()
.gap_px()
.justify_center()
.child(
div()
.flex()
.justify_center()
.w_full()
.child(indicator().player(self.player)),
)
.child(
div()
.flex()
.items_center()
.justify_center()
.h_6()
.px_1()
.rounded_lg()
.fill(player_bg)
.child(facepile(self.players.clone())),
)
}
}

View file

@ -1,88 +0,0 @@
use crate::prelude::{InteractionState, ToggleState};
use crate::theme::theme;
use crate::ui::{icon, IconAsset, Label};
use gpui2::geometry::rems;
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct ListItem {
label: Label,
left_icon: Option<IconAsset>,
indent_level: u32,
state: InteractionState,
toggle: Option<ToggleState>,
}
pub fn list_item(label: Label) -> ListItem {
ListItem {
label,
indent_level: 0,
left_icon: None,
state: InteractionState::default(),
toggle: None,
}
}
impl ListItem {
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_icon(mut self, left_icon: Option<IconAsset>) -> Self {
self.left_icon = left_icon;
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.fill(theme.middle.base.default.background)
.hover()
.fill(theme.middle.base.hovered.background)
.active()
.fill(theme.middle.base.pressed.background)
.relative()
.child(
div()
.h_7()
.px_2()
// .ml(rems(0.75 * self.indent_level as f32))
.children((0..self.indent_level).map(|_| {
div().w(rems(0.75)).h_full().flex().justify_center().child(
div()
.w_px()
.h_full()
.fill(theme.middle.base.default.border)
.hover()
.fill(theme.middle.warning.default.border)
.active()
.fill(theme.middle.negative.default.border),
)
}))
.flex()
.gap_2()
.items_center()
.children(match self.toggle {
Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)),
Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)),
None => None,
})
.children(self.left_icon.map(|i| icon(i)))
.child(self.label.clone()),
)
}
}

View file

@ -1,55 +0,0 @@
use crate::theme::theme;
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct Tab {
title: &'static str,
enabled: bool,
}
pub fn tab<V: 'static>(title: &'static str, enabled: bool) -> impl Element<V> {
Tab { title, enabled }
}
impl Tab {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.px_2()
.py_0p5()
.flex()
.items_center()
.justify_center()
.rounded_lg()
.fill(if self.enabled {
theme.highest.on.default.background
} else {
theme.highest.base.default.background
})
.hover()
.fill(if self.enabled {
theme.highest.on.hovered.background
} else {
theme.highest.base.hovered.background
})
.active()
.fill(if self.enabled {
theme.highest.on.pressed.background
} else {
theme.highest.base.pressed.background
})
.child(
div()
.text_sm()
.text_color(if self.enabled {
theme.highest.base.default.foreground
} else {
theme.highest.variant.default.foreground
})
.child(self.title),
)
}
}

View file

@ -1,19 +0,0 @@
mod avatar;
mod details;
mod icon;
mod icon_button;
mod indicator;
mod input;
mod label;
mod text_button;
mod tool_divider;
pub use avatar::*;
pub use details::*;
pub use icon::*;
pub use icon_button::*;
pub use indicator::*;
pub use input::*;
pub use label::*;
pub use text_button::*;
pub use tool_divider::*;

View file

@ -1,42 +0,0 @@
use crate::prelude::Shape;
use crate::theme::theme;
use gpui2::elements::img;
use gpui2::style::StyleHelpers;
use gpui2::{ArcCow, IntoElement};
use gpui2::{Element, ViewContext};
#[derive(Element, Clone)]
pub struct Avatar {
src: ArcCow<'static, str>,
shape: Shape,
}
pub fn avatar(src: impl Into<ArcCow<'static, str>>) -> Avatar {
Avatar {
src: src.into(),
shape: Shape::Circle,
}
}
impl Avatar {
pub fn shape(mut self, shape: Shape) -> Self {
self.shape = shape;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
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)
}
}

View file

@ -1,36 +0,0 @@
use crate::theme::theme;
use gpui2::elements::div;
use gpui2::style::StyleHelpers;
use gpui2::{Element, ViewContext};
use gpui2::{IntoElement, ParentElement};
#[derive(Element, Clone)]
pub struct Details {
text: &'static str,
meta: Option<&'static str>,
}
pub fn details(text: &'static str) -> Details {
Details { text, meta: None }
}
impl Details {
pub fn meta_text(mut self, meta: &'static str) -> Self {
self.meta = Some(meta);
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
// .flex()
// .w_full()
.p_1()
.gap_0p5()
.text_xs()
.text_color(theme.lowest.base.default.foreground)
.child(self.text.clone())
.children(self.meta.map(|m| m))
}
}

View file

@ -1,73 +0,0 @@
use crate::theme::theme;
use gpui2::elements::svg;
use gpui2::style::StyleHelpers;
use gpui2::IntoElement;
use gpui2::{Element, ViewContext};
// Icon::Hash
// icon(IconAsset::Hash).color(IconColor::Warning)
// Icon::new(IconAsset::Hash).color(IconColor::Warning)
#[derive(Default, PartialEq, Copy, Clone)]
pub enum IconAsset {
Ai,
ArrowLeft,
ArrowRight,
#[default]
ArrowUpRight,
Bolt,
Hash,
File,
Folder,
FolderOpen,
ChevronDown,
ChevronUp,
ChevronLeft,
ChevronRight,
}
impl IconAsset {
pub fn path(self) -> &'static str {
match self {
IconAsset::Ai => "icons/ai.svg",
IconAsset::ArrowLeft => "icons/arrow_left.svg",
IconAsset::ArrowRight => "icons/arrow_right.svg",
IconAsset::ArrowUpRight => "icons/arrow_up_right.svg",
IconAsset::Bolt => "icons/bolt.svg",
IconAsset::Hash => "icons/hash.svg",
IconAsset::ChevronDown => "icons/chevron_down.svg",
IconAsset::ChevronUp => "icons/chevron_up.svg",
IconAsset::ChevronLeft => "icons/chevron_left.svg",
IconAsset::ChevronRight => "icons/chevron_right.svg",
IconAsset::File => "icons/file_icons/file.svg",
IconAsset::Folder => "icons/file_icons/folder.svg",
IconAsset::FolderOpen => "icons/file_icons/folder_open.svg",
}
}
}
#[derive(Element, Clone)]
pub struct Icon {
asset: IconAsset,
}
pub fn icon(asset: IconAsset) -> Icon {
Icon { asset }
}
// impl Icon {
// pub fn new(asset: IconAsset) -> Icon {
// Icon { asset }
// }
// }
impl Icon {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
svg()
.path(self.asset.path())
.size_4()
.fill(theme.lowest.base.default.foreground)
}
}

View file

@ -1,62 +0,0 @@
use crate::prelude::{ButtonVariant, InteractionState};
use crate::theme::theme;
use gpui2::elements::svg;
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct IconButton {
path: &'static str,
variant: ButtonVariant,
state: InteractionState,
}
pub fn icon_button(path: &'static str) -> IconButton {
IconButton {
path,
variant: ButtonVariant::default(),
state: InteractionState::default(),
}
}
impl IconButton {
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let icon_color;
if self.state == InteractionState::Disabled {
icon_color = theme.highest.base.disabled.foreground;
} else {
icon_color = theme.highest.base.default.foreground;
}
let mut div = div();
if self.variant == ButtonVariant::Filled {
div = div.fill(theme.highest.on.default.background);
}
div.w_7()
.h_6()
.flex()
.items_center()
.justify_center()
.rounded_md()
.hover()
.fill(theme.highest.base.hovered.background)
.active()
.fill(theme.highest.base.pressed.background)
.child(svg().path(self.path).w_4().h_4().fill(icon_color))
}
}

View file

@ -1,32 +0,0 @@
use crate::theme::theme;
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ViewContext};
#[derive(Element)]
pub struct Indicator {
player: usize,
}
pub fn indicator() -> Indicator {
Indicator { player: 0 }
}
impl Indicator {
pub fn player(mut self, player: usize) -> Self {
self.player = player;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let player_color = theme.players[self.player].cursor;
div()
.w_4()
.h_1()
.rounded_bl_sm()
.rounded_br_sm()
.fill(player_color)
}
}

View file

@ -1,99 +0,0 @@
use crate::prelude::{InputVariant, InteractionState};
use crate::theme::theme;
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct Input {
placeholder: &'static str,
value: String,
state: InteractionState,
variant: InputVariant,
}
pub fn input(placeholder: &'static str) -> Input {
Input {
placeholder,
value: "".to_string(),
state: InteractionState::default(),
variant: InputVariant::default(),
}
}
impl Input {
pub fn value(mut self, value: String) -> Self {
self.value = value;
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
pub fn variant(mut self, variant: InputVariant) -> Self {
self.variant = variant;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let text_el;
let text_color;
let background_color_default;
let background_color_active;
let mut border_color_default = theme.middle.base.default.border;
let mut border_color_hover = theme.middle.base.hovered.border;
let mut border_color_active = theme.middle.base.pressed.border;
let border_color_focus = theme.middle.base.pressed.background;
match self.variant {
InputVariant::Ghost => {
background_color_default = theme.middle.base.default.background;
background_color_active = theme.middle.base.active.background;
}
InputVariant::Filled => {
background_color_default = theme.middle.on.default.background;
background_color_active = theme.middle.on.active.background;
}
};
if self.state == InteractionState::Focused {
border_color_default = theme.players[0].cursor;
border_color_hover = theme.players[0].cursor;
border_color_active = theme.players[0].cursor;
}
if self.state == InteractionState::Focused || self.state == InteractionState::Active {
text_el = self.value.clone();
text_color = theme.lowest.base.default.foreground;
} else {
text_el = self.placeholder.to_string().clone();
text_color = theme.lowest.base.disabled.foreground;
}
div()
.h_7()
.px_2()
.border()
.border_color(border_color_default)
.fill(background_color_default)
.hover()
.border_color(border_color_hover)
.active()
.border_color(border_color_active)
.fill(background_color_active)
.flex()
.items_center()
.child(
div()
.flex()
.items_center()
.text_sm()
.text_color(text_color)
.child(text_el)
.child(div().text_color(theme.players[0].cursor).child("|")),
)
}
}

View file

@ -1,49 +0,0 @@
use crate::theme::theme;
use gpui2::elements::div;
use gpui2::style::StyleHelpers;
use gpui2::{Element, ViewContext};
use gpui2::{IntoElement, ParentElement};
#[derive(Default, PartialEq, Copy, Clone)]
pub enum LabelColor {
#[default]
Default,
Created,
Modified,
Deleted,
Hidden,
}
#[derive(Element, Clone)]
pub struct Label {
label: &'static str,
color: LabelColor,
}
pub fn label(label: &'static str) -> Label {
Label {
label,
color: LabelColor::Default,
}
}
impl Label {
pub fn color(mut self, color: LabelColor) -> Self {
self.color = color;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let color = match self.color {
LabelColor::Default => theme.lowest.base.default.foreground,
LabelColor::Created => theme.lowest.positive.default.foreground,
LabelColor::Modified => theme.lowest.warning.default.foreground,
LabelColor::Deleted => theme.lowest.negative.default.foreground,
LabelColor::Hidden => theme.lowest.variant.default.foreground,
};
div().text_sm().text_color(color).child(self.label.clone())
}
}

View file

@ -1,81 +0,0 @@
use crate::prelude::{ButtonVariant, InteractionState};
use crate::theme::theme;
use gpui2::style::{StyleHelpers, Styleable};
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct TextButton {
label: &'static str,
variant: ButtonVariant,
state: InteractionState,
}
pub fn text_button(label: &'static str) -> TextButton {
TextButton {
label,
variant: ButtonVariant::default(),
state: InteractionState::default(),
}
}
impl TextButton {
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let text_color_default;
let text_color_hover;
let text_color_active;
let background_color_default;
let background_color_hover;
let background_color_active;
let div = div();
match self.variant {
ButtonVariant::Ghost => {
text_color_default = theme.lowest.base.default.foreground;
text_color_hover = theme.lowest.base.hovered.foreground;
text_color_active = theme.lowest.base.pressed.foreground;
background_color_default = theme.lowest.base.default.background;
background_color_hover = theme.lowest.base.hovered.background;
background_color_active = theme.lowest.base.pressed.background;
}
ButtonVariant::Filled => {
text_color_default = theme.lowest.base.default.foreground;
text_color_hover = theme.lowest.base.hovered.foreground;
text_color_active = theme.lowest.base.pressed.foreground;
background_color_default = theme.lowest.on.default.background;
background_color_hover = theme.lowest.on.hovered.background;
background_color_active = theme.lowest.on.pressed.background;
}
};
div.h_6()
.px_1()
.flex()
.items_center()
.justify_center()
.rounded_md()
.text_xs()
.text_color(text_color_default)
.fill(background_color_default)
.hover()
.text_color(text_color_hover)
.fill(background_color_hover)
.active()
.text_color(text_color_active)
.fill(background_color_active)
.child(self.label.clone())
}
}

View file

@ -1,19 +0,0 @@
use crate::theme::theme;
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ViewContext};
#[derive(Element)]
pub struct ToolDivider {}
pub fn tool_divider<V: 'static>() -> impl Element<V> {
ToolDivider {}
}
impl ToolDivider {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div().w_px().h_3().fill(theme.lowest.base.default.border)
}
}

View file

@ -1,11 +0,0 @@
mod chat_panel;
mod project_panel;
mod status_bar;
mod tab_bar;
mod title_bar;
pub use chat_panel::*;
pub use project_panel::*;
pub use status_bar::*;
pub use tab_bar::*;
pub use title_bar::*;

View file

@ -1,65 +0,0 @@
use std::marker::PhantomData;
use crate::theme::theme;
use crate::ui::icon_button;
use gpui2::elements::div::ScrollState;
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct ChatPanel<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
}
pub fn chat_panel<V: 'static>(scroll_state: ScrollState) -> ChatPanel<V> {
ChatPanel {
view_type: PhantomData,
scroll_state,
}
}
impl<V: 'static> ChatPanel<V> {
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.h_full()
.flex()
// Header
.child(
div()
.px_2()
.flex()
.gap_2()
// Nav Buttons
.child("#gpui2"),
)
// Chat Body
.child(
div()
.w_full()
.flex()
.flex_col()
.overflow_y_scroll(self.scroll_state.clone())
.child("body"),
)
// Composer
.child(
div()
.px_2()
.flex()
.gap_2()
// Nav Buttons
.child(
div()
.flex()
.items_center()
.gap_px()
.child(icon_button("icons/plus.svg"))
.child(icon_button("icons/split.svg")),
),
)
}
}

View file

@ -1,97 +0,0 @@
use crate::{
prelude::{InteractionState, ToggleState},
theme::theme,
ui::{details, input, label, list_item, IconAsset, LabelColor},
};
use gpui2::{
elements::{div, div::ScrollState},
style::StyleHelpers,
ParentElement, ViewContext,
};
use gpui2::{Element, IntoElement};
use std::marker::PhantomData;
#[derive(Element)]
pub struct ProjectPanel<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
}
pub fn project_panel<V: 'static>(scroll_state: ScrollState) -> ProjectPanel<V> {
ProjectPanel {
view_type: PhantomData,
scroll_state,
}
}
impl<V: 'static> ProjectPanel<V> {
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.w_56()
.h_full()
.flex()
.flex_col()
.fill(theme.middle.base.default.background)
.child(
div()
.w_56()
.flex()
.flex_col()
.overflow_y_scroll(self.scroll_state.clone())
.child(details("This is a long string that should wrap when it keeps going for a long time.").meta_text("6 h ago)"))
.child(
div().flex().flex_col().children(
std::iter::repeat_with(|| {
vec![
list_item(label("sqlez").color(LabelColor::Modified))
.left_icon(IconAsset::FolderOpen.into())
.indent_level(0)
.set_toggle(ToggleState::NotToggled),
list_item(label("storybook").color(LabelColor::Modified))
.left_icon(IconAsset::FolderOpen.into())
.indent_level(0)
.set_toggle(ToggleState::Toggled),
list_item(label("docs").color(LabelColor::Default))
.left_icon(IconAsset::Folder.into())
.indent_level(1)
.set_toggle(ToggleState::Toggled),
list_item(label("src").color(LabelColor::Modified))
.left_icon(IconAsset::FolderOpen.into())
.indent_level(2)
.set_toggle(ToggleState::Toggled),
list_item(label("ui").color(LabelColor::Modified))
.left_icon(IconAsset::FolderOpen.into())
.indent_level(3)
.set_toggle(ToggleState::Toggled),
list_item(label("component").color(LabelColor::Created))
.left_icon(IconAsset::FolderOpen.into())
.indent_level(4)
.set_toggle(ToggleState::Toggled),
list_item(label("facepile.rs").color(LabelColor::Default))
.left_icon(IconAsset::File.into())
.indent_level(5),
list_item(label("follow_group.rs").color(LabelColor::Default))
.left_icon(IconAsset::File.into())
.indent_level(5),
list_item(label("list_item.rs").color(LabelColor::Created))
.left_icon(IconAsset::File.into())
.indent_level(5),
list_item(label("tab.rs").color(LabelColor::Default))
.left_icon(IconAsset::File.into())
.indent_level(5),
]
})
.take(10)
.flatten(),
),
),
)
.child(
input("Find something...")
.value("buffe".to_string())
.state(InteractionState::Focused),
)
}
}

View file

@ -1,146 +0,0 @@
use std::marker::PhantomData;
use crate::theme::{theme, Theme};
use crate::ui::{icon_button, text_button, tool_divider};
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Default, PartialEq)]
pub enum Tool {
#[default]
ProjectPanel,
CollaborationPanel,
Terminal,
Assistant,
Feedback,
Diagnostics,
}
struct ToolGroup {
active_index: Option<usize>,
tools: Vec<Tool>,
}
impl Default for ToolGroup {
fn default() -> Self {
ToolGroup {
active_index: None,
tools: vec![],
}
}
}
#[derive(Element)]
pub struct StatusBar<V: 'static> {
view_type: PhantomData<V>,
left_tools: Option<ToolGroup>,
right_tools: Option<ToolGroup>,
bottom_tools: Option<ToolGroup>,
}
pub fn status_bar<V: 'static>() -> StatusBar<V> {
StatusBar {
view_type: PhantomData,
left_tools: None,
right_tools: None,
bottom_tools: None,
}
}
impl<V: 'static> StatusBar<V> {
pub fn left_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
self.left_tools = {
let mut tools = vec![tool];
tools.extend(self.left_tools.take().unwrap_or_default().tools);
Some(ToolGroup {
active_index,
tools,
})
};
self
}
pub fn right_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
self.right_tools = {
let mut tools = vec![tool];
tools.extend(self.left_tools.take().unwrap_or_default().tools);
Some(ToolGroup {
active_index,
tools,
})
};
self
}
pub fn bottom_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
self.bottom_tools = {
let mut tools = vec![tool];
tools.extend(self.left_tools.take().unwrap_or_default().tools);
Some(ToolGroup {
active_index,
tools,
})
};
self
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.py_0p5()
.px_1()
.flex()
.items_center()
.justify_between()
.w_full()
.fill(theme.lowest.base.default.background)
.child(self.left_tools(theme))
.child(self.right_tools(theme))
}
fn left_tools(&self, theme: &Theme) -> impl Element<V> {
div()
.flex()
.items_center()
.gap_1()
.child(icon_button("icons/project.svg"))
.child(icon_button("icons/hash.svg"))
.child(tool_divider())
.child(icon_button("icons/error.svg"))
}
fn right_tools(&self, theme: &Theme) -> impl Element<V> {
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.flex()
.items_center()
.gap_1()
.child(text_button("116:25"))
.child(text_button("Rust")),
)
.child(tool_divider())
.child(
div()
.flex()
.items_center()
.gap_1()
.child(icon_button("icons/copilot.svg"))
.child(icon_button("icons/feedback.svg")),
)
.child(tool_divider())
.child(
div()
.flex()
.items_center()
.gap_1()
.child(icon_button("icons/terminal.svg"))
.child(icon_button("icons/conversations.svg"))
.child(icon_button("icons/ai.svg")),
)
}
}

View file

@ -1,91 +0,0 @@
use std::marker::PhantomData;
use crate::prelude::InteractionState;
use crate::theme::theme;
use crate::ui::{icon_button, tab};
use gpui2::elements::div::ScrollState;
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct TabBar<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
}
pub fn tab_bar<V: 'static>(scroll_state: ScrollState) -> TabBar<V> {
TabBar {
view_type: PhantomData,
scroll_state,
}
}
impl<V: 'static> TabBar<V> {
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let can_navigate_back = true;
let can_navigate_forward = false;
div()
.w_full()
.flex()
// Left Side
.child(
div()
.px_1()
.flex()
.flex_none()
.gap_2()
// Nav Buttons
.child(
div()
.flex()
.items_center()
.gap_px()
.child(
icon_button("icons/arrow_left.svg")
.state(InteractionState::Enabled.if_enabled(can_navigate_back)),
)
.child(
icon_button("icons/arrow_right.svg").state(
InteractionState::Enabled.if_enabled(can_navigate_forward),
),
),
),
)
.child(
div().w_0().flex_1().h_full().child(
div()
.flex()
.gap_1()
.overflow_x_scroll(self.scroll_state.clone())
.child(tab("Cargo.toml", false))
.child(tab("Channels Panel", true))
.child(tab("channels_panel.rs", false))
.child(tab("workspace.rs", false))
.child(tab("icon_button.rs", false))
.child(tab("storybook.rs", false))
.child(tab("theme.rs", false))
.child(tab("theme_registry.rs", false))
.child(tab("styleable_helpers.rs", false)),
),
)
// Right Side
.child(
div()
.px_1()
.flex()
.flex_none()
.gap_2()
// Nav Buttons
.child(
div()
.flex()
.items_center()
.gap_px()
.child(icon_button("icons/plus.svg"))
.child(icon_button("icons/split.svg")),
),
)
}
}

View file

@ -1,117 +0,0 @@
use std::marker::PhantomData;
use crate::prelude::Shape;
use crate::theme::theme;
use crate::ui::{avatar, follow_group, icon_button, text_button, tool_divider};
use gpui2::style::StyleHelpers;
use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
pub struct TitleBar<V: 'static> {
view_type: PhantomData<V>,
}
pub fn title_bar<V: 'static>() -> TitleBar<V> {
TitleBar {
view_type: PhantomData,
}
}
impl<V: 'static> TitleBar<V> {
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let player_list = vec![
avatar("https://avatars.githubusercontent.com/u/1714999?v=4"),
avatar("https://avatars.githubusercontent.com/u/1714999?v=4"),
];
div()
.flex()
.items_center()
.justify_between()
.w_full()
.h_8()
.fill(theme.lowest.base.default.background)
.child(
div()
.flex()
.items_center()
.h_full()
.gap_4()
.px_2()
// === Traffic Lights === //
.child(
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.w_3()
.h_3()
.rounded_full()
.fill(theme.lowest.positive.default.foreground),
)
.child(
div()
.w_3()
.h_3()
.rounded_full()
.fill(theme.lowest.warning.default.foreground),
)
.child(
div()
.w_3()
.h_3()
.rounded_full()
.fill(theme.lowest.negative.default.foreground),
),
)
// === Project Info === //
.child(
div()
.flex()
.items_center()
.gap_1()
.child(text_button("maxbrunsfeld"))
.child(text_button("zed"))
.child(text_button("nate/gpui2-ui-components")),
)
.child(follow_group(player_list.clone()).player(0))
.child(follow_group(player_list.clone()).player(1))
.child(follow_group(player_list.clone()).player(2)),
)
.child(
div()
.flex()
.items_center()
.child(
div()
.px_2()
.flex()
.items_center()
.gap_1()
.child(icon_button("icons/stop_sharing.svg"))
.child(icon_button("icons/exit.svg")),
)
.child(tool_divider())
.child(
div()
.px_2()
.flex()
.items_center()
.gap_1()
.child(icon_button("icons/mic.svg"))
.child(icon_button("icons/speaker-loud.svg"))
.child(icon_button("icons/desktop.svg")),
)
.child(
div().px_2().flex().items_center().child(
avatar("https://avatars.githubusercontent.com/u/1714999?v=4")
.shape(Shape::RoundedRectangle),
),
),
)
}
}

View file

@ -1,133 +0,0 @@
* = Not in the app today
## Template
- [ ] Workspace
- [ ] Title Bar
- [ ] Project Panel
- [ ] Collab Panel
- [ ] Project Diagnosics
- [ ] Project Search
- [ ] Feedback Editor
- [ ] Terminal
- [ ] Assistant
- [ ] Chat*
- [ ] Notifications*
- [ ] Status Bar
- [ ] Panes
- [ ] Pane
- [ ] Editor
- [ ] Tab Bar
- [ ] Tool Bar
- [ ] Buffer
- [ ] Zoomed Editor (Modal)
### Palettes
- [ ] Project Files Palette (⌘-P)
- [ ] Command Palette (⌘-SHIFT-P)
- [ ] Recent Projects Palette (⌘-OPT-O)
- [ ] Recent Branches Palette (⌘-OPT-B)
- [ ] Project Symbols (⌘-T)
- [ ] Theme Palette (⌘-K, ⌘-T)
- [ ] Outline View (⌘-SHIFT-O)
### Debug Views
- [ ] LSP Tool
- [ ] Syntax Tree
## Modules
### Title Bar
- [ ] Traffic Lights
- [ ] Host Menu
- [ ] Project Menu
- [ ] Branch Menu
- [ ] Collaborators
- [ ] Add Collaborator*
- [ ] Project Controls
- [ ] Call Controls
- [ ] User Menu
### Project Panel
- [ ] Open Editors*
- [ ] Open Files (Non-project files)
- [ ] Project Files
- [ ] Root Folder - Context Menu
- [ ] Folder - Context Menu
- [ ] File - Context Menu
- [ ] Project Filter*
### Collab Panel
- [ ] Current Call
- [ ] Channels
- [ ] Channel - Context Menu
- [ ] Contacts
- [ ] Collab Filter
### Project Diagnosics
WIP
### Feedback Editor
- [ ] Feedback Header
- [ ] Editor
- [ ] Feedback Actions
### Terminal
- [ ] Terminal Toolbar*
- [ ] Terminal Line
- [ ] Terminal Input
### Assistant
- [ ] Toolbar
- [ ] History / Past Conversations
- [ ] Model Controls / Token Counter
- [ ] Chat Editor
### Chat
WIP
### Notifications
WIP
### Status Bar
- [ ] Status Bar Tool (Icon)
- [ ] Status Bar Tool (Text)
- [ ] Status Bar Tool - Context Menu
- [ ] Status Bar Tool - Popover Palette
- [ ] Status Bar Tool - Popover Menu
- [ ] Diagnostic Message
- [ ] LSP Message
- [ ] Update message (New version available, downloading, etc)
### Panes/Pane
- [ ] Editor
- [ ] Split Divider/Control
### Editor
- [ ] Editor
- [ ] Read-only Editor
- [ ] Rendered Markdown View*
### Tab Bar
- [ ] Navigation History / Control
- [ ] Tabs
- [ ] Editor Controls (New, Split, Zoom)
### Tool Bar
- [ ] Breadcrumb
- [ ] Editor Tool (Togglable)
- [ ] Buffer Search
### Buffer
### Zoomed Editor (Modal)
- [ ] Modal View
### Palette
- [ ] Input
- [ ] Section Title
- [ ] List
## Components
- [ ] Context Menu

View file

@ -1,12 +1,9 @@
use crate::{
theme::theme,
ui::{chat_panel, project_panel, status_bar, tab_bar, title_bar},
};
use gpui2::{
elements::{div, div::ScrollState},
style::StyleHelpers,
Element, IntoElement, ParentElement, ViewContext,
};
use ui::{chat_panel, project_panel, status_bar, tab_bar, theme, title_bar};
#[derive(Element, Default)]
struct WorkspaceElement {