diff --git a/crates/ui/doc/elevation.md b/crates/ui/doc/elevation.md new file mode 100644 index 0000000000000000000000000000000000000000..bd34de3396dea1f1042bb267f8a23abe6a45441f --- /dev/null +++ b/crates/ui/doc/elevation.md @@ -0,0 +1,57 @@ +# Elevation + +Elevation in Zed applies to all surfaces and components. Elevation is categorized into levels. + +Elevation accomplishes the following: +- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars. +- Reflects spatial relationships, for instance, how a floating action button’s shadow intimates its disconnection from a collection of cards. +- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces. + +Elevations are the initial elevation values assigned to components by default. + +Components may transition to a higher elevation in some cases, like user interations. + +On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest. + +## Understanding Elevation + +Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations. + +Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview) + +## Elevation Levels + +Zed integrates six unique elevation levels in its design system. The elevation of a surface is expressed as a whole number ranging from 0 to 5, both numbers inclusive. A component’s elevation is ascertained by combining the component’s resting elevation with any dynamic elevation offsets. + +The levels are detailed as follows: + +0. App Background +1. UI Surface +2. Elevated Elements +3. Wash +4. Focused Element +5. Dragged Element + +### 0. App Background + +The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app. + +### 1. UI Surface + +The UI Surface is the standard elevation for components and is placed above the app background. It is generally used for the background color of the app bar, card, and sheet. + +### 2. Elevated Elements + +Elevated elements appear above the UI surface layer surfaces and components. Elevated elements are predominantly used for creating popovers, context menus, and tooltips. + +### 3. Wash + +Wash denotes a distinct elevation reserved to isolate app UI layers from high elevation components such as modals, notifications, and overlaid panels. The wash may not consistently be visible when these components are active. This layer is often referred to as a scrim or overlay and the background color of the wash is typically deployed in its design. + +### 4. Focused Element + +Focused elements obtain a higher elevation above surfaces and components at wash elevation. They are often used for modals, notifications, and overlaid panels and indicate that they are the sole element the user is interacting with at the moment. + +### 5. Dragged Element + +Dragged elements gain the highest elevation, thus appearing above surfaces and components at the elevation of focused elements. These are typically used for elements that are being dragged, following the cursor diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index a82d28eb8cb2539cef985c12a59ba712b97699ae..1d513db7d29e4415923b59acb997f735b5ea91f1 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,11 +1,15 @@ mod facepile; mod follow_group; mod list_item; +mod list_section_header; +mod palette_item; mod tab; pub use facepile::*; pub use follow_group::*; pub use list_item::*; +pub use list_section_header::*; +pub use palette_item::*; pub use tab::*; use std::marker::PhantomData; diff --git a/crates/ui/src/components/list_item.rs b/crates/ui/src/components/list_item.rs index 868b58e449f778d8356a1823c91f3312328ab4aa..19e5b26abe4c4d21103f771b1b2b26403c83238f 100644 --- a/crates/ui/src/components/list_item.rs +++ b/crates/ui/src/components/list_item.rs @@ -1,17 +1,18 @@ -use gpui2::elements::div; -use gpui2::geometry::rems; +use crate::prelude::{DisclosureControlVisibility, InteractionState, ToggleState}; +use crate::theme::theme; +use crate::tokens::token; +use crate::{icon, IconAsset, Label}; use gpui2::style::{StyleHelpers, Styleable}; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; -use crate::prelude::*; -use crate::{icon, theme, IconAsset, Label}; - -#[derive(Element)] +#[derive(Element, Clone)] pub struct ListItem { label: Label, left_icon: Option, indent_level: u32, state: InteractionState, + disclosure_control_style: DisclosureControlVisibility, toggle: Option, } @@ -20,6 +21,7 @@ pub fn list_item(label: Label) -> ListItem { label, indent_level: 0, left_icon: None, + disclosure_control_style: DisclosureControlVisibility::default(), state: InteractionState::default(), toggle: None, } @@ -46,8 +48,30 @@ impl ListItem { self } + pub fn disclosure_control_style( + mut self, + disclosure_control_style: DisclosureControlVisibility, + ) -> Self { + self.disclosure_control_style = disclosure_control_style; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); + let token = token(); + let mut disclosure_control = match self.toggle { + Some(ToggleState::NotToggled) => Some(div().child(icon(IconAsset::ChevronRight))), + Some(ToggleState::Toggled) => Some(div().child(icon(IconAsset::ChevronDown))), + None => Some(div()), + }; + + match self.disclosure_control_style { + DisclosureControlVisibility::OnHover => { + disclosure_control = + disclosure_control.map(|c| div().absolute().neg_left_5().child(c)); + } + DisclosureControlVisibility::Always => {} + } div() .fill(theme.middle.base.default.background) @@ -56,31 +80,31 @@ impl ListItem { .active() .fill(theme.middle.base.pressed.background) .relative() + .py_1() .child( div() - .h_7() + .h_6() .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), - ) + div() + .w(token.list_indent_depth) + .h_full() + .flex() + .justify_center() + .child( + div() + .ml_px() + .w_px() + .h_full() + .fill(theme.middle.base.default.border), + ) })) .flex() - .gap_2() + .gap_1() .items_center() - .children(match self.toggle { - Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)), - Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)), - None => None, - }) + .relative() + .children(disclosure_control) .children(self.left_icon.map(|i| icon(i))) .child(self.label.clone()), ) diff --git a/crates/ui/src/components/list_section_header.rs b/crates/ui/src/components/list_section_header.rs new file mode 100644 index 0000000000000000000000000000000000000000..76a0d4cee7612fb291b325f08dd6a8600d13fbbe --- /dev/null +++ b/crates/ui/src/components/list_section_header.rs @@ -0,0 +1,88 @@ +use crate::prelude::{InteractionState, ToggleState}; +use crate::theme::theme; +use crate::tokens::token; +use crate::{icon, label, IconAsset, LabelColor, LabelSize}; +use gpui2::style::{StyleHelpers, Styleable}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; + +#[derive(Element, Clone, Copy)] +pub struct ListSectionHeader { + label: &'static str, + left_icon: Option, + state: InteractionState, + toggle: Option, +} + +pub fn list_section_header(label: &'static str) -> ListSectionHeader { + ListSectionHeader { + label, + left_icon: None, + state: InteractionState::default(), + toggle: None, + } +} + +impl ListSectionHeader { + pub fn set_toggle(mut self, toggle: ToggleState) -> Self { + self.toggle = Some(toggle); + self + } + + pub fn left_icon(mut self, left_icon: Option) -> Self { + self.left_icon = left_icon; + self + } + + pub fn state(mut self, state: InteractionState) -> Self { + self.state = state; + self + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + let token = token(); + + let disclosure_control = match self.toggle { + Some(ToggleState::NotToggled) => Some(div().child(icon(IconAsset::ChevronRight))), + Some(ToggleState::Toggled) => Some(div().child(icon(IconAsset::ChevronDown))), + None => Some(div()), + }; + + div() + .flex() + .flex_1() + .w_full() + .fill(theme.middle.base.default.background) + .hover() + .fill(theme.middle.base.hovered.background) + .active() + .fill(theme.middle.base.pressed.background) + .relative() + .py_1() + .child( + div() + .h_6() + .px_2() + .flex() + .flex_1() + .w_full() + .gap_1() + .items_center() + .justify_between() + .child( + div() + .flex() + .gap_1() + .items_center() + .children(self.left_icon.map(|i| icon(i))) + .child( + label(self.label.clone()) + .color(LabelColor::Muted) + .size(LabelSize::Small), + ), + ) + .children(disclosure_control), + ) + } +} diff --git a/crates/ui/src/components/palette_item.rs b/crates/ui/src/components/palette_item.rs new file mode 100644 index 0000000000000000000000000000000000000000..9e7883d700252e621eabf569f91261ffc58d6824 --- /dev/null +++ b/crates/ui/src/components/palette_item.rs @@ -0,0 +1,63 @@ +use crate::theme::theme; +use crate::{label, LabelColor, LabelSize}; +use gpui2::elements::div; +use gpui2::style::StyleHelpers; +use gpui2::{Element, IntoElement}; +use gpui2::{ParentElement, ViewContext}; + +#[derive(Element)] +pub struct PaletteItem { + pub label: &'static str, + pub keybinding: Option<&'static str>, +} + +pub fn palette_item(label: &'static str, keybinding: Option<&'static str>) -> PaletteItem { + PaletteItem { label, keybinding } +} + +impl PaletteItem { + pub fn label(mut self, label: &'static str) -> Self { + self.label = label; + self + } + + pub fn keybinding(mut self, keybinding: Option<&'static str>) -> Self { + self.keybinding = keybinding; + self + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + let keybinding_label = match self.keybinding { + Some(keybind) => label(keybind) + .color(LabelColor::Muted) + .size(LabelSize::Small), + None => label(""), + }; + + div() + .flex() + .flex_row() + .grow() + .justify_between() + .child(label(self.label)) + .child( + self.keybinding + .map(|_| { + div() + .flex() + .items_center() + .justify_center() + .px_1() + .py_0() + .my_0p5() + .rounded_md() + .text_sm() + .fill(theme.lowest.on.default.background) + .child(keybinding_label) + }) + .unwrap_or_else(|| div()), + ) + } +} diff --git a/crates/ui/src/elements/icon.rs b/crates/ui/src/elements/icon.rs index dbe30cb4f941c9b4cfb2bf1d3386f768c35c43da..08b8e3e7c5733589537f46623c257d0801de9852 100644 --- a/crates/ui/src/elements/icon.rs +++ b/crates/ui/src/elements/icon.rs @@ -1,29 +1,30 @@ +use crate::theme::theme; use gpui2::elements::svg; use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ViewContext}; - -use crate::theme; - -// Icon::Hash -// icon(IconAsset::Hash).color(IconColor::Warning) -// Icon::new(IconAsset::Hash).color(IconColor::Warning) +use gpui2::IntoElement; +use gpui2::{Element, ViewContext}; #[derive(Default, PartialEq, Copy, Clone)] pub enum IconAsset { Ai, ArrowLeft, ArrowRight, - #[default] ArrowUpRight, Bolt, - Hash, - File, - Folder, - FolderOpen, ChevronDown, - ChevronUp, ChevronLeft, ChevronRight, + ChevronUp, + #[default] + File, + FileDoc, + FileGit, + FileLock, + FileRust, + FileToml, + Folder, + FolderOpen, + Hash, } impl IconAsset { @@ -34,14 +35,19 @@ impl IconAsset { 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::ChevronUp => "icons/chevron_up.svg", IconAsset::File => "icons/file_icons/file.svg", + IconAsset::FileDoc => "icons/file_icons/book.svg", + IconAsset::FileGit => "icons/file_icons/git.svg", + IconAsset::FileLock => "icons/file_icons/lock.svg", + IconAsset::FileRust => "icons/file_icons/rust.svg", + IconAsset::FileToml => "icons/file_icons/toml.svg", IconAsset::Folder => "icons/file_icons/folder.svg", IconAsset::FolderOpen => "icons/file_icons/folder_open.svg", + IconAsset::Hash => "icons/hash.svg", } } } @@ -55,19 +61,14 @@ pub fn icon(asset: IconAsset) -> Icon { Icon { asset } } -// impl Icon { -// pub fn new(asset: IconAsset) -> Icon { -// Icon { asset } -// } -// } - impl Icon { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); svg() + .flex_none() .path(self.asset.path()) .size_4() - .fill(theme.lowest.base.default.foreground) + .fill(theme.lowest.variant.default.foreground) } } diff --git a/crates/ui/src/elements/label.rs b/crates/ui/src/elements/label.rs index d3e94297ad891f2670a35c76aaf28ab69f0e5de1..90e6b525ebb2fc1c675a07bd73325fb6504efa08 100644 --- a/crates/ui/src/elements/label.rs +++ b/crates/ui/src/elements/label.rs @@ -1,29 +1,40 @@ +use crate::theme::theme; use gpui2::elements::div; use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::theme; +use gpui2::{Element, ViewContext}; +use gpui2::{IntoElement, ParentElement}; #[derive(Default, PartialEq, Copy, Clone)] pub enum LabelColor { #[default] Default, + Muted, Created, Modified, Deleted, Hidden, + Placeholder, +} + +#[derive(Default, PartialEq, Copy, Clone)] +pub enum LabelSize { + #[default] + Default, + Small, } #[derive(Element, Clone)] pub struct Label { label: &'static str, color: LabelColor, + size: LabelSize, } pub fn label(label: &'static str) -> Label { Label { label, color: LabelColor::Default, + size: LabelSize::Default, } } @@ -33,17 +44,32 @@ impl Label { self } + pub fn size(mut self, size: LabelSize) -> Self { + self.size = size; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let color = match self.color { LabelColor::Default => theme.lowest.base.default.foreground, + LabelColor::Muted => theme.lowest.variant.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, + LabelColor::Placeholder => theme.lowest.base.disabled.foreground, }; - div().text_sm().text_color(color).child(self.label.clone()) + let mut div = div(); + + if self.size == LabelSize::Small { + div = div.text_xs(); + } else { + div = div.text_sm(); + } + + div.text_color(color).child(self.label.clone()) } } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index c7bab1e0b0786a9390dcf3612cc87dd80276675f..0bcd96164393716fe60cdba409a012988c630e03 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -5,10 +5,17 @@ mod element_ext; mod elements; mod modules; pub mod prelude; +mod static_data; +mod templates; mod theme; +mod tokens; pub use crate::theme::*; pub use components::*; pub use element_ext::*; pub use elements::*; pub use modules::*; +pub use prelude::*; +pub use static_data::*; +pub use templates::*; +pub use tokens::*; diff --git a/crates/ui/src/modules.rs b/crates/ui/src/modules.rs index a1cead7df9df397602906cdfca7750508532522a..d29e31072b5662ad574fb31d58f9b9bb224a4b2a 100644 --- a/crates/ui/src/modules.rs +++ b/crates/ui/src/modules.rs @@ -1,11 +1,5 @@ -mod chat_panel; -mod project_panel; -mod status_bar; -mod tab_bar; -mod title_bar; +mod list; +mod palette; -pub use chat_panel::*; -pub use project_panel::*; -pub use status_bar::*; -pub use tab_bar::*; -pub use title_bar::*; +pub use list::*; +pub use palette::*; diff --git a/crates/ui/src/modules/list.rs b/crates/ui/src/modules/list.rs new file mode 100644 index 0000000000000000000000000000000000000000..a7fb06132faf861931b4cba3c18a3ddd7d36d573 --- /dev/null +++ b/crates/ui/src/modules/list.rs @@ -0,0 +1,64 @@ +use crate::theme::theme; +use crate::tokens::token; +use crate::{icon, label, prelude::*, IconAsset, LabelColor, ListItem, ListSectionHeader}; +use gpui2::style::StyleHelpers; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; + +#[derive(Element)] +pub struct List { + header: Option, + items: Vec, + empty_message: &'static str, + toggle: Option, + // footer: Option, +} + +pub fn list(items: Vec) -> List { + List { + header: None, + items, + empty_message: "No items", + toggle: None, + } +} + +impl List { + pub fn header(mut self, header: ListSectionHeader) -> Self { + self.header = Some(header); + self + } + + pub fn empty_message(mut self, empty_message: &'static str) -> Self { + self.empty_message = empty_message; + self + } + + pub fn set_toggle(mut self, toggle: ToggleState) -> Self { + self.toggle = Some(toggle); + self + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + let token = token(); + + let disclosure_control = match self.toggle { + Some(ToggleState::NotToggled) => Some(icon(IconAsset::ChevronRight)), + Some(ToggleState::Toggled) => Some(icon(IconAsset::ChevronDown)), + None => None, + }; + + div() + .py_1() + .flex() + .flex_col() + .children(self.header.map(|h| h)) + .children( + self.items + .is_empty() + .then(|| label(self.empty_message).color(LabelColor::Muted)), + ) + .children(self.items.iter().cloned()) + } +} diff --git a/crates/ui/src/modules/palette.rs b/crates/ui/src/modules/palette.rs new file mode 100644 index 0000000000000000000000000000000000000000..a540d29169b0fa65d8fe7d9e7fba2f048efb2ee3 --- /dev/null +++ b/crates/ui/src/modules/palette.rs @@ -0,0 +1,124 @@ +use std::marker::PhantomData; + +use crate::prelude::OrderMethod; +use crate::theme::theme; +use crate::{label, palette_item, LabelColor, PaletteItem}; +use gpui2::elements::div::ScrollState; +use gpui2::style::{StyleHelpers, Styleable}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; + +#[derive(Element)] +pub struct Palette { + view_type: PhantomData, + scroll_state: ScrollState, + input_placeholder: &'static str, + empty_string: &'static str, + items: Vec, + default_order: OrderMethod, +} + +pub fn palette(scroll_state: ScrollState) -> Palette { + Palette { + view_type: PhantomData, + scroll_state, + input_placeholder: "Find something...", + empty_string: "No items found.", + items: vec![], + default_order: OrderMethod::default(), + } +} + +impl Palette { + pub fn items(mut self, mut items: Vec) -> Self { + items.sort_by_key(|item| item.label); + self.items = items; + self + } + + pub fn placeholder(mut self, input_placeholder: &'static str) -> Self { + self.input_placeholder = input_placeholder; + self + } + + pub fn empty_string(mut self, empty_string: &'static str) -> Self { + self.empty_string = empty_string; + self + } + + // TODO: Hook up sort order + pub fn default_order(mut self, default_order: OrderMethod) -> Self { + self.default_order = default_order; + self + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + .w_96() + .rounded_lg() + .fill(theme.lowest.base.default.background) + .border() + .border_color(theme.lowest.base.default.border) + .flex() + .flex_col() + .child( + div() + .flex() + .flex_col() + .gap_px() + .child( + div().py_0p5().px_1().flex().flex_col().child( + div().px_2().py_0p5().child( + label(self.input_placeholder).color(LabelColor::Placeholder), + ), + ), + ) + .child(div().h_px().w_full().fill(theme.lowest.base.default.border)) + .child( + div() + .py_0p5() + .px_1() + .flex() + .flex_col() + .grow() + .max_h_96() + .overflow_y_scroll(self.scroll_state.clone()) + .children( + vec![if self.items.is_empty() { + Some( + div() + .flex() + .flex_row() + .justify_between() + .px_2() + .py_1() + .child( + label(self.empty_string).color(LabelColor::Muted), + ), + ) + } else { + None + }] + .into_iter() + .flatten(), + ) + .children(self.items.iter().map(|item| { + div() + .flex() + .flex_row() + .justify_between() + .px_2() + .py_0p5() + .rounded_lg() + .hover() + .fill(theme.lowest.base.hovered.background) + .active() + .fill(theme.lowest.base.pressed.background) + .child(palette_item(item.label, item.keybinding)) + })), + ), + ) + } +} diff --git a/crates/ui/src/modules/project_panel.rs b/crates/ui/src/modules/project_panel.rs deleted file mode 100644 index e17ff4c8edf425801731a37f60d1e05d65f61186..0000000000000000000000000000000000000000 --- a/crates/ui/src/modules/project_panel.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::marker::PhantomData; - -use gpui2::elements::div; -use gpui2::elements::div::ScrollState; -use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::prelude::*; -use crate::{details, input, label, list_item, theme, IconAsset, LabelColor}; - -#[derive(Element)] -pub struct ProjectPanel { - view_type: PhantomData, - scroll_state: ScrollState, -} - -pub fn project_panel(scroll_state: ScrollState) -> ProjectPanel { - ProjectPanel { - view_type: PhantomData, - scroll_state, - } -} - -impl ProjectPanel { - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - 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), - ) - } -} diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 61dbb676e2d7d01a4843f2c5a84db7f32064c30b..70b9ab4a5ed0e0dbbc6f0c13670c50fd452cf225 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -1,3 +1,11 @@ +#[derive(Default, PartialEq)] +pub enum OrderMethod { + #[default] + Ascending, + Descending, + MostRecent, +} + #[derive(Default, PartialEq)] pub enum ButtonVariant { #[default] @@ -19,6 +27,13 @@ pub enum Shape { RoundedRectangle, } +#[derive(Default, PartialEq, Clone, Copy)] +pub enum DisclosureControlVisibility { + #[default] + OnHover, + Always, +} + #[derive(Default, PartialEq, Clone, Copy)] pub enum InteractionState { #[default] diff --git a/crates/ui/src/static_data.rs b/crates/ui/src/static_data.rs new file mode 100644 index 0000000000000000000000000000000000000000..de946dab28d9cbabd5ba95a28ab162386d8e101c --- /dev/null +++ b/crates/ui/src/static_data.rs @@ -0,0 +1,166 @@ +use crate::{ + label, list_item, palette_item, IconAsset, LabelColor, ListItem, PaletteItem, ToggleState, +}; + +pub fn static_project_panel_project_items() -> Vec { + vec![ + list_item(label("zed")) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(0) + .set_toggle(ToggleState::Toggled), + list_item(label(".cargo")) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label(".config")) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label(".git").color(LabelColor::Hidden)) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label(".cargo")) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label(".idea").color(LabelColor::Hidden)) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label("assets")) + .left_icon(IconAsset::Folder.into()) + .indent_level(1) + .set_toggle(ToggleState::Toggled), + list_item(label("cargo-target").color(LabelColor::Hidden)) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label("crates")) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(1) + .set_toggle(ToggleState::Toggled), + list_item(label("activity_indicator")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("ai")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("audio")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("auto_update")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("breadcrumbs")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("call")) + .left_icon(IconAsset::Folder.into()) + .indent_level(2), + list_item(label("sqlez").color(LabelColor::Modified)) + .left_icon(IconAsset::Folder.into()) + .indent_level(2) + .set_toggle(ToggleState::NotToggled), + list_item(label("gpui2")) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(2) + .set_toggle(ToggleState::Toggled), + list_item(label("src")) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(3) + .set_toggle(ToggleState::Toggled), + list_item(label("derrive_element.rs")) + .left_icon(IconAsset::FileRust.into()) + .indent_level(4), + list_item(label("storybook").color(LabelColor::Modified)) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(1) + .set_toggle(ToggleState::Toggled), + list_item(label("docs").color(LabelColor::Default)) + .left_icon(IconAsset::Folder.into()) + .indent_level(2) + .set_toggle(ToggleState::Toggled), + list_item(label("src").color(LabelColor::Modified)) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(3) + .set_toggle(ToggleState::Toggled), + list_item(label("ui").color(LabelColor::Modified)) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(4) + .set_toggle(ToggleState::Toggled), + list_item(label("component").color(LabelColor::Created)) + .left_icon(IconAsset::FolderOpen.into()) + .indent_level(5) + .set_toggle(ToggleState::Toggled), + list_item(label("facepile.rs").color(LabelColor::Default)) + .left_icon(IconAsset::FileRust.into()) + .indent_level(6), + list_item(label("follow_group.rs").color(LabelColor::Default)) + .left_icon(IconAsset::FileRust.into()) + .indent_level(6), + list_item(label("list_item.rs").color(LabelColor::Created)) + .left_icon(IconAsset::FileRust.into()) + .indent_level(6), + list_item(label("tab.rs").color(LabelColor::Default)) + .left_icon(IconAsset::FileRust.into()) + .indent_level(6), + list_item(label("target").color(LabelColor::Hidden)) + .left_icon(IconAsset::Folder.into()) + .indent_level(1), + list_item(label(".dockerignore")) + .left_icon(IconAsset::File.into()) + .indent_level(1), + list_item(label(".DS_Store").color(LabelColor::Hidden)) + .left_icon(IconAsset::File.into()) + .indent_level(1), + list_item(label("Cargo.lock")) + .left_icon(IconAsset::FileLock.into()) + .indent_level(1), + list_item(label("Cargo.toml")) + .left_icon(IconAsset::FileToml.into()) + .indent_level(1), + list_item(label("Dockerfile")) + .left_icon(IconAsset::File.into()) + .indent_level(1), + list_item(label("Procfile")) + .left_icon(IconAsset::File.into()) + .indent_level(1), + list_item(label("README.md")) + .left_icon(IconAsset::FileDoc.into()) + .indent_level(1), + ] +} + +pub fn static_project_panel_single_items() -> Vec { + vec![ + list_item(label("todo.md")) + .left_icon(IconAsset::FileDoc.into()) + .indent_level(0), + list_item(label("README.md")) + .left_icon(IconAsset::FileDoc.into()) + .indent_level(0), + list_item(label("config.json")) + .left_icon(IconAsset::File.into()) + .indent_level(0), + ] +} + +pub fn example_editor_actions() -> Vec { + vec![ + palette_item("New File", Some("Ctrl+N")), + palette_item("Open File", Some("Ctrl+O")), + palette_item("Save File", Some("Ctrl+S")), + palette_item("Cut", Some("Ctrl+X")), + palette_item("Copy", Some("Ctrl+C")), + palette_item("Paste", Some("Ctrl+V")), + palette_item("Undo", Some("Ctrl+Z")), + palette_item("Redo", Some("Ctrl+Shift+Z")), + palette_item("Find", Some("Ctrl+F")), + palette_item("Replace", Some("Ctrl+R")), + palette_item("Jump to Line", None), + palette_item("Select All", None), + palette_item("Deselect All", None), + palette_item("Switch Document", None), + palette_item("Insert Line Below", None), + palette_item("Insert Line Above", None), + palette_item("Move Line Up", None), + palette_item("Move Line Down", None), + palette_item("Toggle Comment", None), + palette_item("Delete Line", None), + ] +} diff --git a/crates/ui/src/templates.rs b/crates/ui/src/templates.rs new file mode 100644 index 0000000000000000000000000000000000000000..f09a8a7ea4e93795d4a5b669b2f2816476c6c26c --- /dev/null +++ b/crates/ui/src/templates.rs @@ -0,0 +1,17 @@ +mod chat_panel; +mod collab_panel; +mod command_palette; +mod project_panel; +mod status_bar; +mod tab_bar; +mod title_bar; +mod workspace; + +pub use chat_panel::*; +pub use collab_panel::*; +pub use command_palette::*; +pub use project_panel::*; +pub use status_bar::*; +pub use tab_bar::*; +pub use title_bar::*; +pub use workspace::*; diff --git a/crates/ui/src/modules/chat_panel.rs b/crates/ui/src/templates/chat_panel.rs similarity index 92% rename from crates/ui/src/modules/chat_panel.rs rename to crates/ui/src/templates/chat_panel.rs index 77c5b2ef20ba8a2de7cc47fead7d715b1b055706..7c3462b3fe46c31cb1e158548435f572915ff15c 100644 --- a/crates/ui/src/modules/chat_panel.rs +++ b/crates/ui/src/templates/chat_panel.rs @@ -1,11 +1,11 @@ use std::marker::PhantomData; -use gpui2::elements::div; +use crate::icon_button; +use crate::theme::theme; use gpui2::elements::div::ScrollState; use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::{icon_button, theme}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; #[derive(Element)] pub struct ChatPanel { diff --git a/crates/ui/src/templates/collab_panel.rs b/crates/ui/src/templates/collab_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..7e76cb68355b3c09bf45e02a40c62376d4934612 --- /dev/null +++ b/crates/ui/src/templates/collab_panel.rs @@ -0,0 +1,177 @@ +use crate::theme::{theme, Theme}; +use gpui2::{ + elements::{div, div::ScrollState, img, svg}, + style::{StyleHelpers, Styleable}, + ArcCow, Element, IntoElement, ParentElement, ViewContext, +}; +use std::marker::PhantomData; + +#[derive(Element)] +pub struct CollabPanelElement { + view_type: PhantomData, + scroll_state: ScrollState, +} + +// When I improve child view rendering, I'd like to have V implement a trait that +// provides the scroll state, among other things. +pub fn collab_panel(scroll_state: ScrollState) -> CollabPanelElement { + CollabPanelElement { + view_type: PhantomData, + scroll_state, + } +} + +impl CollabPanelElement { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + // Panel + div() + .w_64() + .h_full() + .flex() + .flex_col() + .font("Zed Sans Extended") + .text_color(theme.middle.base.default.foreground) + .border_color(theme.middle.base.default.border) + .border() + .fill(theme.middle.base.default.background) + .child( + div() + .w_full() + .flex() + .flex_col() + .overflow_y_scroll(self.scroll_state.clone()) + // List Container + .child( + div() + .fill(theme.lowest.base.default.background) + .pb_1() + .border_color(theme.lowest.base.default.border) + .border_b() + //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state + // .group() + // List Section Header + .child(self.list_section_header("#CRDB", true, theme)) + // List Item Large + .child(self.list_item( + "http://github.com/maxbrunsfeld.png?s=50", + "maxbrunsfeld", + theme, + )), + ) + .child( + div() + .py_2() + .flex() + .flex_col() + .child(self.list_section_header("CHANNELS", true, theme)), + ) + .child( + div() + .py_2() + .flex() + .flex_col() + .child(self.list_section_header("CONTACTS", true, theme)) + .children( + std::iter::repeat_with(|| { + vec![ + self.list_item( + "http://github.com/as-cii.png?s=50", + "as-cii", + theme, + ), + self.list_item( + "http://github.com/nathansobo.png?s=50", + "nathansobo", + theme, + ), + self.list_item( + "http://github.com/maxbrunsfeld.png?s=50", + "maxbrunsfeld", + theme, + ), + ] + }) + .take(3) + .flatten(), + ), + ), + ) + .child( + div() + .h_7() + .px_2() + .border_t() + .border_color(theme.middle.variant.default.border) + .flex() + .items_center() + .child( + div() + .text_sm() + .text_color(theme.middle.variant.default.foreground) + .child("Find..."), + ), + ) + } + + fn list_section_header( + &self, + label: impl Into>, + expanded: bool, + theme: &Theme, + ) -> impl Element { + div() + .h_7() + .px_2() + .flex() + .justify_between() + .items_center() + .child(div().flex().gap_1().text_sm().child(label)) + .child( + div().flex().h_full().gap_1().items_center().child( + svg() + .path(if expanded { + "icons/caret_down.svg" + } else { + "icons/caret_up.svg" + }) + .w_3p5() + .h_3p5() + .fill(theme.middle.variant.default.foreground), + ), + ) + } + + fn list_item( + &self, + avatar_uri: impl Into>, + label: impl Into>, + theme: &Theme, + ) -> impl Element { + div() + .h_7() + .px_2() + .flex() + .items_center() + .hover() + .fill(theme.lowest.variant.hovered.background) + .active() + .fill(theme.lowest.variant.pressed.background) + .child( + div() + .flex() + .items_center() + .gap_1() + .text_sm() + .child( + img() + .uri(avatar_uri) + .size_3p5() + .rounded_full() + .fill(theme.middle.positive.default.foreground), + ) + .child(label), + ) + } +} diff --git a/crates/ui/src/templates/command_palette.rs b/crates/ui/src/templates/command_palette.rs new file mode 100644 index 0000000000000000000000000000000000000000..303e2d6de9f69b0dc75a53f705878ff25319f1ae --- /dev/null +++ b/crates/ui/src/templates/command_palette.rs @@ -0,0 +1,31 @@ +use gpui2::elements::div; +use gpui2::{elements::div::ScrollState, ViewContext}; +use gpui2::{Element, IntoElement, ParentElement}; +use std::marker::PhantomData; + +use crate::{example_editor_actions, palette, OrderMethod}; + +#[derive(Element)] +pub struct CommandPalette { + view_type: PhantomData, + scroll_state: ScrollState, +} + +pub fn command_palette(scroll_state: ScrollState) -> CommandPalette { + CommandPalette { + view_type: PhantomData, + scroll_state, + } +} + +impl CommandPalette { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + div().child( + palette(self.scroll_state.clone()) + .items(example_editor_actions()) + .placeholder("Execute a command...") + .empty_string("No items found.") + .default_order(OrderMethod::Ascending), + ) + } +} diff --git a/crates/ui/src/templates/project_panel.rs b/crates/ui/src/templates/project_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..8204ad26c077c2aa03e5a8a245925cd97987c1c6 --- /dev/null +++ b/crates/ui/src/templates/project_panel.rs @@ -0,0 +1,62 @@ +use crate::{ + input, list, list_section_header, prelude::*, static_project_panel_project_items, + static_project_panel_single_items, theme, +}; + +use gpui2::{ + elements::{div, div::ScrollState}, + style::StyleHelpers, + ParentElement, ViewContext, +}; +use gpui2::{Element, IntoElement}; +use std::marker::PhantomData; + +#[derive(Element)] +pub struct ProjectPanel { + view_type: PhantomData, + scroll_state: ScrollState, +} + +pub fn project_panel(scroll_state: ScrollState) -> ProjectPanel { + ProjectPanel { + view_type: PhantomData, + scroll_state, + } +} + +impl ProjectPanel { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + 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( + list(static_project_panel_single_items()) + .header(list_section_header("FILES").set_toggle(ToggleState::Toggled)) + .empty_message("No files in directory") + .set_toggle(ToggleState::Toggled), + ) + .child( + list(static_project_panel_project_items()) + .header(list_section_header("PROJECT").set_toggle(ToggleState::Toggled)) + .empty_message("No folders in directory") + .set_toggle(ToggleState::Toggled), + ), + ) + .child( + input("Find something...") + .value("buffe".to_string()) + .state(InteractionState::Focused), + ) + } +} diff --git a/crates/ui/src/modules/status_bar.rs b/crates/ui/src/templates/status_bar.rs similarity index 97% rename from crates/ui/src/modules/status_bar.rs rename to crates/ui/src/templates/status_bar.rs index 763a585176128fa1e832bfc35fa35e670aeb5e41..79265a757296394c3603abbca02750d2a855cb23 100644 --- a/crates/ui/src/modules/status_bar.rs +++ b/crates/ui/src/templates/status_bar.rs @@ -1,11 +1,10 @@ use std::marker::PhantomData; -use gpui2::elements::div; -use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - use crate::theme::{theme, Theme}; use crate::{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 { diff --git a/crates/ui/src/modules/tab_bar.rs b/crates/ui/src/templates/tab_bar.rs similarity index 95% rename from crates/ui/src/modules/tab_bar.rs rename to crates/ui/src/templates/tab_bar.rs index 08caad310729f66b1f723366994a73c660b447a3..e1ca6b1dc793ed87bcc7d278ef1e2a3c6a9cc0b3 100644 --- a/crates/ui/src/modules/tab_bar.rs +++ b/crates/ui/src/templates/tab_bar.rs @@ -1,12 +1,12 @@ use std::marker::PhantomData; -use gpui2::elements::div; +use crate::prelude::InteractionState; +use crate::theme::theme; +use crate::{icon_button, tab}; use gpui2::elements::div::ScrollState; use gpui2::style::StyleHelpers; -use gpui2::{Element, IntoElement, ParentElement, ViewContext}; - -use crate::prelude::InteractionState; -use crate::{icon_button, tab, theme}; +use gpui2::{elements::div, IntoElement}; +use gpui2::{Element, ParentElement, ViewContext}; #[derive(Element)] pub struct TabBar { diff --git a/crates/ui/src/modules/title_bar.rs b/crates/ui/src/templates/title_bar.rs similarity index 100% rename from crates/ui/src/modules/title_bar.rs rename to crates/ui/src/templates/title_bar.rs diff --git a/crates/ui/src/templates/workspace.rs b/crates/ui/src/templates/workspace.rs new file mode 100644 index 0000000000000000000000000000000000000000..fb785a317f71be0b476f294b2c95141c06b56566 --- /dev/null +++ b/crates/ui/src/templates/workspace.rs @@ -0,0 +1,80 @@ +use crate::{chat_panel, collab_panel, project_panel, status_bar, tab_bar, theme, title_bar}; + +use gpui2::{ + elements::{div, div::ScrollState}, + style::StyleHelpers, + Element, IntoElement, ParentElement, ViewContext, +}; + +#[derive(Element, Default)] +struct WorkspaceElement { + project_panel_scroll_state: ScrollState, + collab_panel_scroll_state: ScrollState, + right_scroll_state: ScrollState, + tab_bar_scroll_state: ScrollState, + palette_scroll_state: ScrollState, +} + +pub fn workspace() -> impl Element { + WorkspaceElement::default() +} + +impl WorkspaceElement { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + div() + // Elevation Level 0 + .size_full() + .flex() + .flex_col() + .font("Zed Sans Extended") + .gap_0() + .justify_start() + .items_start() + .text_color(theme.lowest.base.default.foreground) + .fill(theme.lowest.base.default.background) + .relative() + // Elevation Level 1 + .child(title_bar()) + .child( + div() + .flex_1() + .w_full() + .flex() + .flex_row() + .overflow_hidden() + .child(project_panel(self.project_panel_scroll_state.clone())) + .child(collab_panel(self.collab_panel_scroll_state.clone())) + .child( + div() + .h_full() + .flex_1() + .fill(theme.highest.base.default.background) + .child( + div() + .flex() + .flex_col() + .flex_1() + .child(tab_bar(self.tab_bar_scroll_state.clone())), + ), + ) + .child(chat_panel(self.right_scroll_state.clone())), + ) + .child(status_bar()) + // Elevation Level 3 + // .child( + // div() + // .absolute() + // .top_0() + // .left_0() + // .size_full() + // .flex() + // .justify_center() + // .items_center() + // // .fill(theme.lowest.base.default.background) + // // Elevation Level 4 + // .child(command_palette(self.palette_scroll_state.clone())), + // ) + } +} diff --git a/crates/ui/src/tokens.rs b/crates/ui/src/tokens.rs new file mode 100644 index 0000000000000000000000000000000000000000..79128205330c4e1424024f9af4823749f47f3a06 --- /dev/null +++ b/crates/ui/src/tokens.rs @@ -0,0 +1,18 @@ +use gpui2::geometry::AbsoluteLength; + +#[derive(Clone, Copy)] +pub struct Token { + pub list_indent_depth: AbsoluteLength, +} + +impl Default for Token { + fn default() -> Self { + Self { + list_indent_depth: AbsoluteLength::Rems(0.5), + } + } +} + +pub fn token() -> Token { + Token::default() +}