Detailed changes
@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.70312 4L7.26046 2.97339C7.10239 2.60679 6.74141 2.36933 6.34219 2.36933H2.5C2.22386 2.36933 2 2.59319 2 2.86933V4.375V8" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>
@@ -1,12 +1,12 @@
-use std::fmt::Debug;
-
use super::scene::{Path, PathVertex};
use crate::{color::Color, json::ToJson};
+use derive_more::Neg;
pub use pathfinder_geometry::*;
use rect::RectF;
use refineable::Refineable;
use serde::{Deserialize, Deserializer};
use serde_json::json;
+use std::fmt::Debug;
use vector::{vec2f, Vector2F};
pub struct PathBuilder {
@@ -194,8 +194,8 @@ where
impl Size<DefiniteLength> {
pub fn zero() -> Self {
Self {
- width: pixels(0.),
- height: pixels(0.),
+ width: pixels(0.).into(),
+ height: pixels(0.).into(),
}
}
@@ -235,6 +235,17 @@ pub struct Edges<T: Clone + Default + Debug> {
pub left: T,
}
+impl<T: Clone + Default + Debug> Edges<T> {
+ pub fn uniform(value: T) -> Self {
+ Self {
+ top: value.clone(),
+ right: value.clone(),
+ bottom: value.clone(),
+ left: value.clone(),
+ }
+ }
+}
+
impl Edges<Length> {
pub fn auto() -> Self {
Self {
@@ -247,10 +258,10 @@ impl Edges<Length> {
pub fn zero() -> Self {
Self {
- top: pixels(0.),
- right: pixels(0.),
- bottom: pixels(0.),
- left: pixels(0.),
+ top: pixels(0.).into(),
+ right: pixels(0.).into(),
+ bottom: pixels(0.).into(),
+ left: pixels(0.).into(),
}
}
@@ -270,10 +281,10 @@ impl Edges<Length> {
impl Edges<DefiniteLength> {
pub fn zero() -> Self {
Self {
- top: pixels(0.),
- right: pixels(0.),
- bottom: pixels(0.),
- left: pixels(0.),
+ top: pixels(0.).into(),
+ right: pixels(0.).into(),
+ bottom: pixels(0.).into(),
+ left: pixels(0.).into(),
}
}
@@ -322,7 +333,7 @@ impl Edges<f32> {
}
}
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Neg)]
pub enum AbsoluteLength {
Pixels(f32),
Rems(f32),
@@ -360,7 +371,7 @@ impl Default for AbsoluteLength {
}
/// A non-auto length that can be defined in pixels, rems, or percent of parent.
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Neg)]
pub enum DefiniteLength {
Absolute(AbsoluteLength),
Relative(f32), // 0. to 1.
@@ -404,7 +415,7 @@ impl Default for DefiniteLength {
}
/// A length that can be defined in pixels, rems, percent of parent, or auto.
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Neg)]
pub enum Length {
Definite(DefiniteLength),
Auto,
@@ -419,16 +430,16 @@ impl std::fmt::Debug for Length {
}
}
-pub fn relative<T: From<DefiniteLength>>(fraction: f32) -> T {
- DefiniteLength::Relative(fraction).into()
+pub fn relative(fraction: f32) -> DefiniteLength {
+ DefiniteLength::Relative(fraction)
}
-pub fn rems<T: From<AbsoluteLength>>(rems: f32) -> T {
- AbsoluteLength::Rems(rems).into()
+pub fn rems(rems: f32) -> AbsoluteLength {
+ AbsoluteLength::Rems(rems)
}
-pub fn pixels<T: From<AbsoluteLength>>(pixels: f32) -> T {
- AbsoluteLength::Pixels(pixels).into()
+pub fn pixels(pixels: f32) -> AbsoluteLength {
+ AbsoluteLength::Pixels(pixels)
}
pub fn auto() -> Length {
@@ -34,6 +34,27 @@ pub trait Element<V: 'static>: 'static + IntoElement<V> {
phase: ElementPhase::Init,
}))
}
+
+ /// 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.
+ ///
+ /// # Parameters
+ /// - `self`: The current element
+ /// - `condition`: The boolean condition based on which `then` is applied to the element.
+ /// - `then`: A function that takes in the current element and returns a possibly modified element.
+ ///
+ /// # Return
+ /// It returns the potentially modified element.
+ fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
+ where
+ Self: Sized,
+ {
+ if condition {
+ self = then(self);
+ }
+ self
+ }
}
/// Used to make ElementState<V, E> into a trait object, so we can wrap it in AnyElement<V>.
@@ -73,10 +73,15 @@ impl<V: 'static, E: Element<V> + Styleable> Element<V> for Pressable<E> {
if bounds.contains_point(event.position) {
pressed.set(true);
cx.repaint();
+ } else {
+ cx.bubble_event();
}
- } else if pressed.get() {
- pressed.set(false);
- cx.repaint();
+ } else {
+ if pressed.get() {
+ pressed.set(false);
+ cx.repaint();
+ }
+ cx.bubble_event();
}
});
@@ -314,6 +314,8 @@ pub trait Styleable {
}
}
+use crate as gpui2;
+
// Helpers methods that take and return mut self. This includes tailwind style methods for standard sizes etc.
//
// Example:
@@ -322,33 +324,12 @@ pub trait Styleable {
pub trait StyleHelpers: Styleable<Style = Style> {
styleable_helpers!();
- fn h(mut self, height: Length) -> Self
- where
- Self: Sized,
- {
- self.declared_style().size.height = Some(height);
- self
- }
-
- /// size_{n}: Sets width & height to {n}
- ///
- /// Example:
- /// size_1: Sets width & height to 1
- fn size(mut self, size: Length) -> Self
- where
- Self: Sized,
- {
- self.declared_style().size.height = Some(size);
- self.declared_style().size.width = Some(size);
- self
- }
-
fn full(mut self) -> Self
where
Self: Sized,
{
- self.declared_style().size.width = Some(relative(1.));
- self.declared_style().size.height = Some(relative(1.));
+ self.declared_style().size.width = Some(relative(1.).into());
+ self.declared_style().size.height = Some(relative(1.).into());
self
}
@@ -406,7 +387,7 @@ pub trait StyleHelpers: Styleable<Style = Style> {
{
self.declared_style().flex_grow = Some(1.);
self.declared_style().flex_shrink = Some(1.);
- self.declared_style().flex_basis = Some(relative(0.));
+ self.declared_style().flex_basis = Some(relative(0.).into());
self
}
@@ -28,49 +28,100 @@ fn generate_methods() -> Vec<TokenStream2> {
let mut methods = Vec::new();
for (prefix, auto_allowed, fields) in box_prefixes() {
+ methods.push(generate_custom_value_setter(
+ prefix,
+ if auto_allowed {
+ quote! { Length }
+ } else {
+ quote! { DefiniteLength }
+ },
+ &fields,
+ ));
+
for (suffix, length_tokens, doc_string) in box_suffixes() {
- if auto_allowed || suffix != "auto" {
- let method = generate_method(prefix, suffix, &fields, length_tokens, doc_string);
- methods.push(method);
+ if suffix != "auto" || auto_allowed {
+ methods.push(generate_predefined_setter(
+ prefix,
+ suffix,
+ &fields,
+ &length_tokens,
+ false,
+ doc_string,
+ ));
+ }
+
+ if suffix != "auto" {
+ methods.push(generate_predefined_setter(
+ prefix,
+ suffix,
+ &fields,
+ &length_tokens,
+ true,
+ doc_string,
+ ));
}
}
}
for (prefix, fields) in corner_prefixes() {
+ methods.push(generate_custom_value_setter(
+ prefix,
+ quote! { AbsoluteLength },
+ &fields,
+ ));
+
for (suffix, radius_tokens, doc_string) in corner_suffixes() {
- let method = generate_method(prefix, suffix, &fields, radius_tokens, doc_string);
- methods.push(method);
+ methods.push(generate_predefined_setter(
+ prefix,
+ suffix,
+ &fields,
+ &radius_tokens,
+ false,
+ doc_string,
+ ));
}
}
for (prefix, fields) in border_prefixes() {
for (suffix, width_tokens, doc_string) in border_suffixes() {
- let method = generate_method(prefix, suffix, &fields, width_tokens, doc_string);
- methods.push(method);
+ methods.push(generate_predefined_setter(
+ prefix,
+ suffix,
+ &fields,
+ &width_tokens,
+ false,
+ doc_string,
+ ));
}
}
-
methods
}
-fn generate_method(
- prefix: &'static str,
- suffix: &'static str,
+fn generate_predefined_setter(
+ name: &'static str,
+ length: &'static str,
fields: &Vec<TokenStream2>,
- length_tokens: TokenStream2,
+ length_tokens: &TokenStream2,
+ negate: bool,
doc_string: &'static str,
) -> TokenStream2 {
- let method_name = if suffix.is_empty() {
- format_ident!("{}", prefix)
+ let (negation_prefix, negation_token) = if negate {
+ ("neg_", quote! { - })
+ } else {
+ ("", quote! {})
+ };
+
+ let method_name = if length.is_empty() {
+ format_ident!("{}{}", negation_prefix, name)
} else {
- format_ident!("{}_{}", prefix, suffix)
+ format_ident!("{}{}_{}", negation_prefix, name, length)
};
let field_assignments = fields
.iter()
.map(|field_tokens| {
quote! {
- style.#field_tokens = Some(gpui::geometry::#length_tokens);
+ style.#field_tokens = Some((#negation_token gpui2::geometry::#length_tokens).into());
}
})
.collect::<Vec<_>>();
@@ -84,6 +135,41 @@ fn generate_method(
}
};
+ if negate {
+ dbg!(method.to_string());
+ }
+
+ method
+}
+
+fn generate_custom_value_setter(
+ prefix: &'static str,
+ length_type: TokenStream2,
+ fields: &Vec<TokenStream2>,
+) -> TokenStream2 {
+ let method_name = format_ident!("{}", prefix);
+
+ let mut iter = fields.into_iter();
+ let last = iter.next_back().unwrap();
+ let field_assignments = iter
+ .map(|field_tokens| {
+ quote! {
+ style.#field_tokens = Some(length.clone().into());
+ }
+ })
+ .chain(std::iter::once(quote! {
+ style.#last = Some(length.into());
+ }))
+ .collect::<Vec<_>>();
+
+ let method = quote! {
+ fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui2::geometry::#length_type>) -> Self where Self: std::marker::Sized {
+ let mut style = self.declared_style();
+ #(#field_assignments)*
+ self
+ }
+ };
+
method
}
@@ -96,10 +182,10 @@ fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
true,
vec![quote! {size.width}, quote! {size.height}],
),
- ("min_w", false, vec![quote! { min_size.width }]),
- ("min_h", false, vec![quote! { min_size.height }]),
- ("max_w", false, vec![quote! { max_size.width }]),
- ("max_h", false, vec![quote! { max_size.height }]),
+ ("min_w", true, vec![quote! { min_size.width }]),
+ ("min_h", true, vec![quote! { min_size.height }]),
+ ("max_w", true, vec![quote! { max_size.width }]),
+ ("max_h", true, vec![quote! { max_size.height }]),
(
"m",
true,
@@ -4,12 +4,6 @@ use gpui2::{
};
use std::{marker::PhantomData, rc::Rc};
-mod icon_button;
-mod tab;
-
-pub(crate) use icon_button::{icon_button, ButtonVariant};
-pub(crate) use tab::tab;
-
struct ButtonHandlers<V, D> {
click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
}
@@ -1,3 +0,0 @@
-mod tab_bar;
-
-pub(crate) use tab_bar::tab_bar;
@@ -0,0 +1,55 @@
+#[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,
+}
@@ -12,8 +12,9 @@ use simplelog::SimpleLogger;
mod collab_panel;
mod components;
mod element_ext;
-mod modules;
+mod prelude;
mod theme;
+mod ui;
mod workspace;
gpui2::actions! {
@@ -0,0 +1,23 @@
+mod element;
+pub use element::avatar::*;
+pub use element::details::*;
+pub use element::icon::*;
+pub use element::icon_button::*;
+pub use element::indicator::*;
+pub use element::input::*;
+pub use element::label::*;
+pub use element::text_button::*;
+pub use element::tool_divider::*;
+
+mod component;
+pub use component::facepile::*;
+pub use component::follow_group::*;
+pub use component::list_item::*;
+pub use component::tab::*;
+
+mod module;
+pub use module::chat_panel::*;
+pub use module::project_panel::*;
+pub use module::status_bar::*;
+pub use module::tab_bar::*;
+pub use module::title_bar::*;
@@ -0,0 +1,4 @@
+pub(crate) mod facepile;
+pub(crate) mod follow_group;
+pub(crate) mod list_item;
+pub(crate) mod tab;
@@ -0,0 +1,27 @@
+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)
+ }
+}
@@ -0,0 +1,52 @@
+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())),
+ )
+ }
+}
@@ -0,0 +1,88 @@
+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()),
+ )
+ }
+}
@@ -4,7 +4,7 @@ use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
-pub(crate) struct Tab {
+pub struct Tab {
title: &'static str,
enabled: bool,
}
@@ -0,0 +1,9 @@
+pub(crate) mod avatar;
+pub(crate) mod details;
+pub(crate) mod icon;
+pub(crate) mod icon_button;
+pub(crate) mod indicator;
+pub(crate) mod input;
+pub(crate) mod label;
+pub(crate) mod text_button;
+pub(crate) mod tool_divider;
@@ -0,0 +1,42 @@
+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)
+ }
+}
@@ -0,0 +1,36 @@
+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))
+ }
+}
@@ -0,0 +1,73 @@
+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)
+ }
+}
@@ -1,3 +1,4 @@
+use crate::prelude::{ButtonVariant, InteractionState};
use crate::theme::theme;
use gpui2::elements::svg;
use gpui2::style::{StyleHelpers, Styleable};
@@ -5,25 +6,42 @@ use gpui2::{elements::div, IntoElement};
use gpui2::{Element, ParentElement, ViewContext};
#[derive(Element)]
-pub(crate) struct IconButton {
+pub struct IconButton {
path: &'static str,
variant: ButtonVariant,
+ state: InteractionState,
}
-#[derive(PartialEq)]
-pub enum ButtonVariant {
- Ghost,
- Filled,
-}
-
-pub fn icon_button<V: 'static>(path: &'static str, variant: ButtonVariant) -> impl Element<V> {
- IconButton { path, variant }
+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);
@@ -39,12 +57,6 @@ impl IconButton {
.fill(theme.highest.base.hovered.background)
.active()
.fill(theme.highest.base.pressed.background)
- .child(
- svg()
- .path(self.path)
- .w_4()
- .h_4()
- .fill(theme.highest.variant.default.foreground),
- )
+ .child(svg().path(self.path).w_4().h_4().fill(icon_color))
}
}
@@ -0,0 +1,32 @@
+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)
+ }
+}
@@ -0,0 +1,99 @@
+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("|")),
+ )
+ }
+}
@@ -0,0 +1,49 @@
+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())
+ }
+}
@@ -0,0 +1,81 @@
+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())
+ }
+}
@@ -0,0 +1,19 @@
+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)
+ }
+}
@@ -0,0 +1,5 @@
+pub(crate) mod chat_panel;
+pub(crate) mod project_panel;
+pub(crate) mod status_bar;
+pub(crate) mod tab_bar;
+pub(crate) mod title_bar;
@@ -0,0 +1,65 @@
+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")),
+ ),
+ )
+ }
+}
@@ -0,0 +1,97 @@
+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),
+ )
+ }
+}
@@ -0,0 +1,146 @@
+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")),
+ )
+ }
+}
@@ -1,7 +1,8 @@
use std::marker::PhantomData;
-use crate::components::{icon_button, tab, ButtonVariant};
+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};
@@ -23,7 +24,8 @@ pub fn tab_bar<V: 'static>(scroll_state: ScrollState) -> TabBar<V> {
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()
@@ -40,15 +42,22 @@ impl<V: 'static> TabBar<V> {
.flex()
.items_center()
.gap_px()
- .child(icon_button("icons/arrow_left.svg", ButtonVariant::Filled))
- .child(icon_button("icons/arrow_right.svg", ButtonVariant::Ghost)),
+ .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_px()
+ .gap_1()
.overflow_x_scroll(self.scroll_state.clone())
.child(tab("Cargo.toml", false))
.child(tab("Channels Panel", true))
@@ -74,8 +83,8 @@ impl<V: 'static> TabBar<V> {
.flex()
.items_center()
.gap_px()
- .child(icon_button("icons/plus.svg", ButtonVariant::Ghost))
- .child(icon_button("icons/split.svg", ButtonVariant::Ghost)),
+ .child(icon_button("icons/plus.svg"))
+ .child(icon_button("icons/split.svg")),
),
)
}
@@ -0,0 +1,117 @@
+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),
+ ),
+ ),
+ )
+ }
+}
@@ -0,0 +1,133 @@
+* = 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
@@ -1,7 +1,10 @@
-use crate::{collab_panel::collab_panel, modules::tab_bar, theme::theme};
+use crate::{
+ theme::theme,
+ ui::{chat_panel, project_panel, status_bar, tab_bar, title_bar},
+};
use gpui2::{
- elements::{div, div::ScrollState, img, svg},
- style::{StyleHelpers, Styleable},
+ elements::{div, div::ScrollState},
+ style::StyleHelpers,
Element, IntoElement, ParentElement, ViewContext,
};
@@ -29,8 +32,8 @@ impl WorkspaceElement {
.justify_start()
.items_start()
.text_color(theme.lowest.base.default.foreground)
- .fill(theme.middle.base.default.background)
- .child(titlebar())
+ .fill(theme.lowest.base.default.background)
+ .child(title_bar())
.child(
div()
.flex_1()
@@ -38,7 +41,7 @@ impl WorkspaceElement {
.flex()
.flex_row()
.overflow_hidden()
- .child(collab_panel(self.left_scroll_state.clone()))
+ .child(project_panel(self.left_scroll_state.clone()))
.child(
div()
.h_full()
@@ -52,397 +55,8 @@ impl WorkspaceElement {
.child(tab_bar(self.tab_bar_scroll_state.clone())),
),
)
- .child(collab_panel(self.right_scroll_state.clone())),
- )
- .child(statusbar())
- }
-}
-
-#[derive(Element)]
-struct TitleBar;
-
-pub fn titlebar<V: 'static>() -> impl Element<V> {
- TitleBar
-}
-
-impl TitleBar {
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- let theme = theme(cx);
- div()
- .flex()
- .items_center()
- .justify_between()
- .w_full()
- .h_8()
- .fill(theme.lowest.base.default.background)
- .child(self.left_group(cx))
- .child(self.right_group(cx))
- }
-
- fn left_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- let theme = theme(cx);
- 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(
- div()
- .h_full()
- .flex()
- .items_center()
- .justify_center()
- .px_2()
- .rounded_md()
- .hover()
- .fill(theme.lowest.base.hovered.background)
- .active()
- .fill(theme.lowest.base.pressed.background)
- .child(div().text_sm().child("project")),
- )
- .child(
- div()
- .h_full()
- .flex()
- .items_center()
- .justify_center()
- .px_2()
- .rounded_md()
- .text_color(theme.lowest.variant.default.foreground)
- .hover()
- .fill(theme.lowest.base.hovered.background)
- .active()
- .fill(theme.lowest.base.pressed.background)
- .child(div().text_sm().child("branch")),
- ),
- )
- }
-
- fn right_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- let theme = theme(cx);
- div()
- .flex()
- .items_center()
- .h_full()
- .gap_3()
- .px_2()
- // === Actions === //
- .child(
- div().child(
- div().flex().items_center().gap_1().child(
- div().size_4().flex().items_center().justify_center().child(
- svg()
- .path("icons/exit.svg")
- .size_4()
- .fill(theme.lowest.base.default.foreground),
- ),
- ),
- ),
- )
- .child(div().w_px().h_3().fill(theme.lowest.base.default.border))
- // === Comms === //
- .child(
- div().child(
- div()
- .flex()
- .items_center()
- .gap_px()
- .child(
- div()
- .px_2()
- .py_1()
- .rounded_md()
- .h_full()
- .flex()
- .items_center()
- .justify_center()
- .hover()
- .fill(theme.lowest.base.hovered.background)
- .active()
- .fill(theme.lowest.base.pressed.background)
- .child(
- svg()
- .path("icons/microphone.svg")
- .size_3p5()
- .fill(theme.lowest.base.default.foreground),
- ),
- )
- .child(
- div()
- .px_2()
- .py_1()
- .rounded_md()
- .h_full()
- .flex()
- .items_center()
- .justify_center()
- .hover()
- .fill(theme.lowest.base.hovered.background)
- .active()
- .fill(theme.lowest.base.pressed.background)
- .child(
- svg()
- .path("icons/speaker-loud.svg")
- .size_3p5()
- .fill(theme.lowest.base.default.foreground),
- ),
- )
- .child(
- div()
- .px_2()
- .py_1()
- .rounded_md()
- .h_full()
- .flex()
- .items_center()
- .justify_center()
- .hover()
- .fill(theme.lowest.base.hovered.background)
- .active()
- .fill(theme.lowest.base.pressed.background)
- .child(
- svg()
- .path("icons/desktop.svg")
- .size_3p5()
- .fill(theme.lowest.base.default.foreground),
- ),
- ),
- ),
- )
- .child(div().w_px().h_3().fill(theme.lowest.base.default.border))
- // User Group
- .child(
- div().child(
- div()
- .px_1()
- .py_1()
- .flex()
- .items_center()
- .justify_center()
- .rounded_md()
- .gap_0p5()
- .hover()
- .fill(theme.lowest.base.hovered.background)
- .active()
- .fill(theme.lowest.base.pressed.background)
- .child(
- img()
- .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
- .size_4()
- .rounded_md()
- .fill(theme.middle.on.default.foreground),
- )
- .child(
- svg()
- .path("icons/caret_down.svg")
- .w_2()
- .h_2()
- .fill(theme.lowest.variant.default.foreground),
- ),
- ),
- )
- }
-}
-
-// ================================================================================ //
-
-#[derive(Element)]
-struct StatusBar;
-
-pub fn statusbar<V: 'static>() -> impl Element<V> {
- StatusBar
-}
-
-impl StatusBar {
- fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- let theme = theme(cx);
- div()
- .flex()
- .items_center()
- .justify_between()
- .w_full()
- .h_8()
- .fill(theme.lowest.base.default.background)
- .child(self.left_group(cx))
- .child(self.right_group(cx))
- }
-
- fn left_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- let theme = theme(cx);
- div()
- .flex()
- .items_center()
- .h_full()
- .gap_4()
- .px_2()
- // === Tools === //
- .child(
- div()
- .flex()
- .items_center()
- .gap_1()
- .child(
- div()
- .w_6()
- .h_full()
- .flex()
- .items_center()
- .justify_center()
- .child(
- svg()
- .path("icons/project.svg")
- .w_4()
- .h_4()
- .fill(theme.lowest.base.default.foreground),
- ),
- )
- .child(
- div()
- .w_6()
- .h_full()
- .flex()
- .items_center()
- .justify_center()
- .child(
- svg()
- .path("icons/conversations.svg")
- .w_4()
- .h_4()
- .fill(theme.lowest.base.default.foreground),
- ),
- )
- .child(
- div()
- .w_6()
- .h_full()
- .flex()
- .items_center()
- .justify_center()
- .child(
- svg()
- .path("icons/file_icons/notebook.svg")
- .w_4()
- .h_4()
- .fill(theme.lowest.accent.default.foreground),
- ),
- ),
- )
- // === Diagnostics === //
- .child(
- div()
- .flex()
- .items_center()
- .gap_2()
- .child(
- div()
- .h_full()
- .flex()
- .items_center()
- .justify_center()
- .gap_0p5()
- .px_1()
- .text_color(theme.lowest.variant.default.foreground)
- .hover()
- .fill(theme.lowest.base.hovered.background)
- .active()
- .fill(theme.lowest.base.pressed.background)
- .child(
- svg()
- .path("icons/error.svg")
- .w_4()
- .h_4()
- .fill(theme.lowest.negative.default.foreground),
- )
- .child(div().text_sm().child("2")),
- )
- .child(
- div()
- .text_sm()
- .text_color(theme.lowest.variant.default.foreground)
- .child("Something is wrong"),
- ),
- )
- }
-
- fn right_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
- let theme = theme(cx);
- div()
- .flex()
- .items_center()
- .h_full()
- .gap_4()
- .px_2()
- // === Tools === //
- .child(
- div()
- .flex()
- .items_center()
- .gap_1()
- .child(
- div()
- .w_6()
- .h_full()
- .flex()
- .items_center()
- .justify_center()
- .child(
- svg()
- .path("icons/check_circle.svg")
- .w_4()
- .h_4()
- .fill(theme.lowest.base.default.foreground),
- ),
- )
- .child(
- div()
- .w_6()
- .h_full()
- .flex()
- .items_center()
- .justify_center()
- .child(
- svg()
- .path("icons/copilot.svg")
- .w_4()
- .h_4()
- .fill(theme.lowest.accent.default.foreground),
- ),
- ),
+ .child(chat_panel(self.right_scroll_state.clone())),
)
+ .child(status_bar())
}
}
@@ -0,0 +1,43 @@
+## Interaction State
+
+**Enabled**
+
+An enabled state communicates an interactive component or element.
+
+**Disabled**
+
+A disabled state communicates a inoperable component or element.
+
+**Hover**
+
+A hover state communicates when a user has placed a cursor above an interactive element.
+
+**Focused**
+
+A focused state communicates when a user has highlighted an element, using an input method such as a keyboard or voice.
+
+**Activated**
+
+An activated state communicates a highlighted destination, whether initiated by the user or by default.
+
+**Pressed**
+
+A pressed state communicates a user tap.
+
+**Dragged**
+
+A dragged state communicates when a user presses and moves an element.
+
+## Selected State
+
+**Unselected**
+
+dfa
+
+**Partially Selected**
+
+daf
+
+**Selected**
+
+dfa