Add an initial set of GPUI2 components to the storybook (#2990)

This PR adds an initial set of components to `crates/storybook/src/ui`.

All changes still are contained to inside storybook. Merging to keep up
to date with main.
This commit is contained in:
Nate Butler 2023-09-20 13:52:47 -04:00 committed by GitHub
commit f7696114bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1530 additions and 499 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -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 {

View File

@ -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>.

View File

@ -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();
}
});

View File

@ -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
}

View File

@ -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 {
format_ident!("{}_{}", prefix, suffix)
("", quote! {})
};
let method_name = if length.is_empty() {
format_ident!("{}{}", negation_prefix, name)
} else {
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,

View File

@ -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>)>>,
}

View File

@ -1,3 +0,0 @@
mod tab_bar;
pub(crate) use tab_bar::tab_bar;

View File

@ -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,
}

View File

@ -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! {

View File

@ -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::*;

View File

@ -0,0 +1,4 @@
pub(crate) mod facepile;
pub(crate) mod follow_group;
pub(crate) mod list_item;
pub(crate) mod tab;

View File

@ -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)
}
}

View File

@ -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())),
)
}
}

View File

@ -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()),
)
}
}

View File

@ -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,
}

View File

@ -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;

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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("|")),
)
}
}

View File

@ -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())
}
}

View File

@ -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())
}
}

View File

@ -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)
}
}

View File

@ -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;

View File

@ -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")),
),
)
}
}

View File

@ -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),
)
}
}

View File

@ -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")),
)
}
}

View File

@ -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")),
),
)
}

View File

@ -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),
),
),
)
}
}

View File

@ -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

View File

@ -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())
}
}

43
docs/ui/states.md Normal file
View File

@ -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