Merge branch 'main' into import-theme

This commit is contained in:
Marshall Bowers 2023-11-06 12:08:46 -05:00
commit f8504c349c
64 changed files with 1053 additions and 924 deletions

4
Cargo.lock generated
View File

@ -2459,8 +2459,10 @@ dependencies = [
"client",
"collections",
"editor",
"futures 0.3.28",
"gpui",
"language",
"log",
"lsp",
"postage",
"project",
@ -9742,7 +9744,7 @@ dependencies = [
[[package]]
name = "tree-sitter-vue"
version = "0.0.1"
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=95b2890#95b28908d90e928c308866f7631e73ef6e1d4b5f"
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58#9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"
dependencies = [
"cc",
"tree-sitter",

View File

@ -177,7 +177,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
tree-sitter-lua = "0.0.14"
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "95b2890"}
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }

1
assets/icons/dash.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minus"><path d="M5 12h14"/></svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@ -75,23 +75,23 @@ impl ChannelView {
let workspace = workspace.read(cx);
let project = workspace.project().to_owned();
let channel_store = ChannelStore::global(cx);
let markdown = workspace
.app_state()
.languages
.language_for_name("Markdown");
let language_registry = workspace.app_state().languages.clone();
let markdown = language_registry.language_for_name("Markdown");
let channel_buffer =
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
cx.spawn(|mut cx| async move {
let channel_buffer = channel_buffer.await?;
let markdown = markdown.await.log_err();
if let Some(markdown) = markdown.await.log_err() {
channel_buffer.update(&mut cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
channel_buffer.update(&mut cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.set_language_registry(language_registry);
if let Some(markdown) = markdown {
buffer.set_language(Some(markdown), cx);
})
});
}
}
})
});
pane.update(&mut cx, |pane, cx| {
let buffer_id = channel_buffer.read(cx).remote_id(cx);

View File

@ -20,7 +20,9 @@ theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
log.workspace = true
anyhow.workspace = true
futures.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true

View File

@ -2,8 +2,8 @@ pub mod items;
mod project_diagnostics_settings;
mod toolbar_controls;
use anyhow::Result;
use collections::{BTreeSet, HashSet};
use anyhow::{Context, Result};
use collections::{HashMap, HashSet};
use editor::{
diagnostic_block_renderer,
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
@ -11,9 +11,10 @@ use editor::{
scroll::autoscroll::Autoscroll,
Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
use futures::future::try_join_all;
use gpui::{
actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
@ -28,6 +29,7 @@ use std::{
any::{Any, TypeId},
borrow::Cow,
cmp::Ordering,
mem,
ops::Range,
path::PathBuf,
sync::Arc,
@ -60,8 +62,10 @@ struct ProjectDiagnosticsEditor {
summary: DiagnosticSummary,
excerpts: ModelHandle<MultiBuffer>,
path_states: Vec<PathState>,
paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
paths_to_update: HashMap<LanguageServerId, HashSet<ProjectPath>>,
current_diagnostics: HashMap<LanguageServerId, HashSet<ProjectPath>>,
include_warnings: bool,
_subscriptions: Vec<Subscription>,
}
struct PathState {
@ -125,9 +129,12 @@ impl View for ProjectDiagnosticsEditor {
"summary": project.diagnostic_summary(cx),
}),
"summary": self.summary,
"paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
(path.path.to_string_lossy(), server_id.0)
).collect::<Vec<_>>(),
"paths_to_update": self.paths_to_update.iter().map(|(server_id, paths)|
(server_id.0, paths.into_iter().map(|path| path.path.to_string_lossy()).collect::<Vec<_>>())
).collect::<HashMap<_, _>>(),
"current_diagnostics": self.current_diagnostics.iter().map(|(server_id, paths)|
(server_id.0, paths.into_iter().map(|path| path.path.to_string_lossy()).collect::<Vec<_>>())
).collect::<HashMap<_, _>>(),
"paths_states": self.path_states.iter().map(|state|
json!({
"path": state.path.path.to_string_lossy(),
@ -149,21 +156,30 @@ impl ProjectDiagnosticsEditor {
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.subscribe(&project_handle, |this, _, event, cx| match event {
project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
this.update_excerpts(Some(*language_server_id), cx);
this.update_title(cx);
}
project::Event::DiagnosticsUpdated {
language_server_id,
path,
} => {
this.paths_to_update
.insert((path.clone(), *language_server_id));
}
_ => {}
})
.detach();
let project_event_subscription =
cx.subscribe(&project_handle, |this, _, event, cx| match event {
project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
log::debug!("Disk based diagnostics finished for server {language_server_id}");
this.update_excerpts(Some(*language_server_id), cx);
}
project::Event::DiagnosticsUpdated {
language_server_id,
path,
} => {
log::debug!("Adding path {path:?} to update for server {language_server_id}");
this.paths_to_update
.entry(*language_server_id)
.or_default()
.insert(path.clone());
let no_multiselections = this.editor.update(cx, |editor, cx| {
editor.selections.all::<usize>(cx).len() <= 1
});
if no_multiselections && !this.is_dirty(cx) {
this.update_excerpts(Some(*language_server_id), cx);
}
}
_ => {}
});
let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
let editor = cx.add_view(|cx| {
@ -172,19 +188,14 @@ impl ProjectDiagnosticsEditor {
editor.set_vertical_scroll_margin(5, cx);
editor
});
cx.subscribe(&editor, |this, _, event, cx| {
let editor_event_subscription = cx.subscribe(&editor, |this, _, event, cx| {
cx.emit(event.clone());
if event == &editor::Event::Focused && this.path_states.is_empty() {
cx.focus_self()
}
})
.detach();
});
let project = project_handle.read(cx);
let paths_to_update = project
.diagnostic_summaries(cx)
.map(|(path, server_id, _)| (path, server_id))
.collect();
let summary = project.diagnostic_summary(cx);
let mut this = Self {
project: project_handle,
@ -193,8 +204,10 @@ impl ProjectDiagnosticsEditor {
excerpts,
editor,
path_states: Default::default(),
paths_to_update,
paths_to_update: HashMap::default(),
include_warnings: settings::get::<ProjectDiagnosticsSettings>(cx).include_warnings,
current_diagnostics: HashMap::default(),
_subscriptions: vec![project_event_subscription, editor_event_subscription],
};
this.update_excerpts(None, cx);
this
@ -214,12 +227,6 @@ impl ProjectDiagnosticsEditor {
fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
self.include_warnings = !self.include_warnings;
self.paths_to_update = self
.project
.read(cx)
.diagnostic_summaries(cx)
.map(|(path, server_id, _)| (path, server_id))
.collect();
self.update_excerpts(None, cx);
cx.notify();
}
@ -229,29 +236,94 @@ impl ProjectDiagnosticsEditor {
language_server_id: Option<LanguageServerId>,
cx: &mut ViewContext<Self>,
) {
let mut paths = Vec::new();
self.paths_to_update.retain(|(path, server_id)| {
if language_server_id
.map_or(true, |language_server_id| language_server_id == *server_id)
{
paths.push(path.clone());
false
log::debug!("Updating excerpts for server {language_server_id:?}");
let mut paths_to_recheck = HashSet::default();
let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
.project
.read(cx)
.diagnostic_summaries(cx)
.fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
summaries.entry(server_id).or_default().insert(path);
summaries
});
let mut old_diagnostics = if let Some(language_server_id) = language_server_id {
new_summaries.retain(|server_id, _| server_id == &language_server_id);
self.paths_to_update.retain(|server_id, paths| {
if server_id == &language_server_id {
paths_to_recheck.extend(paths.drain());
false
} else {
true
}
});
let mut old_diagnostics = HashMap::default();
if let Some(new_paths) = new_summaries.get(&language_server_id) {
if let Some(old_paths) = self
.current_diagnostics
.insert(language_server_id, new_paths.clone())
{
old_diagnostics.insert(language_server_id, old_paths);
}
} else {
true
if let Some(old_paths) = self.current_diagnostics.remove(&language_server_id) {
old_diagnostics.insert(language_server_id, old_paths);
}
}
});
old_diagnostics
} else {
paths_to_recheck.extend(self.paths_to_update.drain().flat_map(|(_, paths)| paths));
mem::replace(&mut self.current_diagnostics, new_summaries.clone())
};
for (server_id, new_paths) in new_summaries {
match old_diagnostics.remove(&server_id) {
Some(mut old_paths) => {
paths_to_recheck.extend(
new_paths
.into_iter()
.filter(|new_path| !old_paths.remove(new_path)),
);
paths_to_recheck.extend(old_paths);
}
None => paths_to_recheck.extend(new_paths),
}
}
paths_to_recheck.extend(old_diagnostics.into_iter().flat_map(|(_, paths)| paths));
if paths_to_recheck.is_empty() {
log::debug!("No paths to recheck for language server {language_server_id:?}");
return;
}
log::debug!(
"Rechecking {} paths for language server {:?}",
paths_to_recheck.len(),
language_server_id
);
let project = self.project.clone();
cx.spawn(|this, mut cx| {
async move {
for path in paths {
let buffer = project
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
.await?;
this.update(&mut cx, |this, cx| {
this.populate_excerpts(path, language_server_id, buffer, cx)
})?;
}
Result::<_, anyhow::Error>::Ok(())
let _: Vec<()> = try_join_all(paths_to_recheck.into_iter().map(|path| {
let mut cx = cx.clone();
let project = project.clone();
async move {
let buffer = project
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
.await
.with_context(|| format!("opening buffer for path {path:?}"))?;
this.update(&mut cx, |this, cx| {
this.populate_excerpts(path, language_server_id, buffer, cx);
})
.context("missing project")?;
anyhow::Ok(())
}
}))
.await
.context("rechecking diagnostics for paths")?;
this.update(&mut cx, |this, cx| {
this.summary = this.project.read(cx).diagnostic_summary(cx);
cx.emit(Event::TitleChanged);
})?;
anyhow::Ok(())
}
.log_err()
})
@ -554,11 +626,6 @@ impl ProjectDiagnosticsEditor {
}
cx.notify();
}
fn update_title(&mut self, cx: &mut ViewContext<Self>) {
self.summary = self.project.read(cx).diagnostic_summary(cx);
cx.emit(Event::TitleChanged);
}
}
impl Item for ProjectDiagnosticsEditor {
@ -1301,25 +1368,6 @@ mod tests {
cx,
)
.unwrap();
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
diagnostic: Diagnostic {
message: "warning 1".to_string(),
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 2,
..Default::default()
},
}],
cx,
)
.unwrap();
});
// The first language server finishes
@ -1353,6 +1401,25 @@ mod tests {
// The second language server finishes
project.update(cx, |project, cx| {
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
diagnostic: Diagnostic {
message: "warning 1".to_string(),
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 2,
..Default::default()
},
}],
cx,
)
.unwrap();
project.disk_based_diagnostics_finished(server_id_2, cx);
});

View File

@ -33,9 +33,9 @@ use util::{
paths::{PathExt, FILE_ROW_COLUMN_DELIMITER},
ResultExt, TryFutureExt,
};
use workspace::item::{BreadcrumbText, FollowableItemHandle};
use workspace::item::{BreadcrumbText, FollowableItemHandle, ItemHandle};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
item::{FollowableItem, Item, ItemEvent, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace,
WorkspaceId,

View File

@ -170,6 +170,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
#max_retries,
#detect_nondeterminism,
&mut |cx, foreground_platform, deterministic, seed| {
// some of the macro contents do not use all variables, silence the warnings
let _ = (&cx, &foreground_platform, &deterministic, &seed);
#cx_vars
cx.foreground().run(#inner_fn_name(#inner_fn_args));
#cx_teardowns
@ -247,6 +249,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
#max_retries,
#detect_nondeterminism,
&mut |cx, foreground_platform, deterministic, seed| {
// some of the macro contents do not use all variables, silence the warnings
let _ = (&cx, &foreground_platform, &deterministic, &seed);
#cx_vars
#inner_fn_name(#inner_fn_args);
#cx_teardowns

View File

@ -16,9 +16,33 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
..
} = parse_macro_input!(input);
let impl_debug_on_refinement = attrs
.iter()
.any(|attr| attr.path.is_ident("refineable") && attr.tokens.to_string().contains("debug"));
let refineable_attr = attrs.iter().find(|attr| attr.path.is_ident("refineable"));
let mut impl_debug_on_refinement = false;
let mut derive_serialize_on_refinement = false;
let mut derive_deserialize_on_refinement = false;
if let Some(refineable_attr) = refineable_attr {
if let Ok(syn::Meta::List(meta_list)) = refineable_attr.parse_meta() {
for nested in meta_list.nested {
let syn::NestedMeta::Meta(syn::Meta::Path(path)) = nested else {
continue;
};
if path.is_ident("debug") {
impl_debug_on_refinement = true;
}
if path.is_ident("serialize") {
derive_serialize_on_refinement = true;
}
if path.is_ident("deserialize") {
derive_deserialize_on_refinement = true;
}
}
}
}
let refinement_ident = format_ident!("{}Refinement", ident);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
@ -235,8 +259,22 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
quote! {}
};
let derive_serialize = if derive_serialize_on_refinement {
quote! { #[derive(serde::Serialize)]}
} else {
quote! {}
};
let derive_deserialize = if derive_deserialize_on_refinement {
quote! { #[derive(serde::Deserialize)]}
} else {
quote! {}
};
let gen = quote! {
#[derive(Clone)]
#derive_serialize
#derive_deserialize
pub struct #refinement_ident #impl_generics {
#( #field_visibilities #field_names: #wrapped_types ),*
}

View File

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

View File

@ -1,7 +1,4 @@
use crate::{
story::Story,
story_selector::{ComponentStory, ElementStory},
};
use crate::{story::Story, story_selector::ComponentStory};
use gpui2::{Div, Render, StatefulInteraction, View, VisualContext};
use strum::IntoEnumIterator;
use ui::prelude::*;
@ -18,9 +15,6 @@ impl Render for KitchenSinkStory {
type Element = Div<Self, StatefulInteraction<Self>>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let element_stories = ElementStory::iter()
.map(|selector| selector.story(cx))
.collect::<Vec<_>>();
let component_stories = ComponentStory::iter()
.map(|selector| selector.story(cx))
.collect::<Vec<_>>();
@ -29,8 +23,6 @@ impl Render for KitchenSinkStory {
.id("kitchen-sink")
.overflow_y_scroll()
.child(Story::title(cx, "Kitchen Sink"))
.child(Story::label(cx, "Elements"))
.child(div().flex().flex_col().children(element_stories))
.child(Story::label(cx, "Components"))
.child(div().flex().flex_col().children(component_stories))
// Add a bit of space at the bottom of the kitchen sink so elements

View File

@ -7,55 +7,31 @@ use clap::builder::PossibleValue;
use clap::ValueEnum;
use gpui2::{AnyView, VisualContext};
use strum::{EnumIter, EnumString, IntoEnumIterator};
use ui::{prelude::*, AvatarStory, ButtonStory, DetailsStory, IconStory, InputStory, LabelStory};
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum ElementStory {
Avatar,
Button,
Colors,
Details,
Focus,
Icon,
Input,
Label,
Scroll,
Text,
ZIndex,
}
impl ElementStory {
pub fn story(&self, cx: &mut WindowContext) -> AnyView {
match self {
Self::Colors => cx.build_view(|_| ColorsStory).into(),
Self::Avatar => cx.build_view(|_| AvatarStory).into(),
Self::Button => cx.build_view(|_| ButtonStory).into(),
Self::Details => cx.build_view(|_| DetailsStory).into(),
Self::Focus => FocusStory::view(cx).into(),
Self::Icon => cx.build_view(|_| IconStory).into(),
Self::Input => cx.build_view(|_| InputStory).into(),
Self::Label => cx.build_view(|_| LabelStory).into(),
Self::Scroll => ScrollStory::view(cx).into(),
Self::Text => TextStory::view(cx).into(),
Self::ZIndex => cx.build_view(|_| ZIndexStory).into(),
}
}
}
use ui::prelude::*;
use ui::{AvatarStory, ButtonStory, DetailsStory, IconStory, InputStory, LabelStory};
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum ComponentStory {
AssistantPanel,
Avatar,
Breadcrumb,
Buffer,
Button,
ChatPanel,
Checkbox,
CollabPanel,
Colors,
CommandPalette,
Copilot,
ContextMenu,
Copilot,
Details,
Facepile,
Focus,
Icon,
Input,
Keybinding,
Label,
LanguageSelector,
MultiBuffer,
NotificationsPanel,
@ -63,29 +39,42 @@ pub enum ComponentStory {
Panel,
ProjectPanel,
RecentProjects,
Scroll,
Tab,
TabBar,
Terminal,
Text,
ThemeSelector,
TitleBar,
Toast,
Toolbar,
TrafficLights,
Workspace,
ZIndex,
}
impl ComponentStory {
pub fn story(&self, cx: &mut WindowContext) -> AnyView {
match self {
Self::AssistantPanel => cx.build_view(|_| ui::AssistantPanelStory).into(),
Self::Buffer => cx.build_view(|_| ui::BufferStory).into(),
Self::Avatar => cx.build_view(|_| AvatarStory).into(),
Self::Breadcrumb => cx.build_view(|_| ui::BreadcrumbStory).into(),
Self::Buffer => cx.build_view(|_| ui::BufferStory).into(),
Self::Button => cx.build_view(|_| ButtonStory).into(),
Self::ChatPanel => cx.build_view(|_| ui::ChatPanelStory).into(),
Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(),
Self::CollabPanel => cx.build_view(|_| ui::CollabPanelStory).into(),
Self::Colors => cx.build_view(|_| ColorsStory).into(),
Self::CommandPalette => cx.build_view(|_| ui::CommandPaletteStory).into(),
Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(),
Self::Copilot => cx.build_view(|_| ui::CopilotModalStory).into(),
Self::Details => cx.build_view(|_| DetailsStory).into(),
Self::Facepile => cx.build_view(|_| ui::FacepileStory).into(),
Self::Focus => FocusStory::view(cx).into(),
Self::Icon => cx.build_view(|_| IconStory).into(),
Self::Input => cx.build_view(|_| InputStory).into(),
Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
Self::Label => cx.build_view(|_| LabelStory).into(),
Self::LanguageSelector => cx.build_view(|_| ui::LanguageSelectorStory).into(),
Self::MultiBuffer => cx.build_view(|_| ui::MultiBufferStory).into(),
Self::NotificationsPanel => cx.build_view(|cx| ui::NotificationsPanelStory).into(),
@ -93,23 +82,24 @@ impl ComponentStory {
Self::Panel => cx.build_view(|cx| ui::PanelStory).into(),
Self::ProjectPanel => cx.build_view(|_| ui::ProjectPanelStory).into(),
Self::RecentProjects => cx.build_view(|_| ui::RecentProjectsStory).into(),
Self::Scroll => ScrollStory::view(cx).into(),
Self::Tab => cx.build_view(|_| ui::TabStory).into(),
Self::TabBar => cx.build_view(|_| ui::TabBarStory).into(),
Self::Terminal => cx.build_view(|_| ui::TerminalStory).into(),
Self::Text => TextStory::view(cx).into(),
Self::ThemeSelector => cx.build_view(|_| ui::ThemeSelectorStory).into(),
Self::TitleBar => ui::TitleBarStory::view(cx).into(),
Self::Toast => cx.build_view(|_| ui::ToastStory).into(),
Self::Toolbar => cx.build_view(|_| ui::ToolbarStory).into(),
Self::TrafficLights => cx.build_view(|_| ui::TrafficLightsStory).into(),
Self::Copilot => cx.build_view(|_| ui::CopilotModalStory).into(),
Self::TitleBar => ui::TitleBarStory::view(cx).into(),
Self::Workspace => ui::WorkspaceStory::view(cx).into(),
Self::ZIndex => cx.build_view(|_| ZIndexStory).into(),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum StorySelector {
Element(ElementStory),
Component(ComponentStory),
KitchenSink,
}
@ -126,13 +116,6 @@ impl FromStr for StorySelector {
return Ok(Self::KitchenSink);
}
if let Some((_, story)) = story.split_once("elements/") {
let element_story = ElementStory::from_str(story)
.with_context(|| format!("story not found for element '{story}'"))?;
return Ok(Self::Element(element_story));
}
if let Some((_, story)) = story.split_once("components/") {
let component_story = ComponentStory::from_str(story)
.with_context(|| format!("story not found for component '{story}'"))?;
@ -147,7 +130,6 @@ impl FromStr for StorySelector {
impl StorySelector {
pub fn story(&self, cx: &mut WindowContext) -> AnyView {
match self {
Self::Element(element_story) => element_story.story(cx),
Self::Component(component_story) => component_story.story(cx),
Self::KitchenSink => KitchenSinkStory::view(cx).into(),
}
@ -160,11 +142,9 @@ static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new();
impl ValueEnum for StorySelector {
fn value_variants<'a>() -> &'a [Self] {
let stories = ALL_STORY_SELECTORS.get_or_init(|| {
let element_stories = ElementStory::iter().map(StorySelector::Element);
let component_stories = ComponentStory::iter().map(StorySelector::Component);
element_stories
.chain(component_stories)
component_stories
.chain(std::iter::once(StorySelector::KitchenSink))
.collect::<Vec<_>>()
});
@ -174,7 +154,6 @@ impl ValueEnum for StorySelector {
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
let value = match self {
Self::Element(story) => format!("elements/{story}"),
Self::Component(story) => format!("components/{story}"),
Self::KitchenSink => "kitchen_sink".to_string(),
};

View File

@ -49,12 +49,14 @@ pub struct GitStatusColors {
}
#[derive(Refineable, Clone, Debug)]
#[refineable(debug)]
#[refineable(debug, deserialize)]
pub struct ThemeColors {
pub border: Hsla,
pub border_variant: Hsla,
pub border_focused: Hsla,
pub border_selected: Hsla,
pub border_transparent: Hsla,
pub border_disabled: Hsla,
pub elevated_surface_background: Hsla,
pub surface_background: Hsla,
pub background: Hsla,
@ -122,6 +124,8 @@ pub struct ThemeStyles {
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
@ -163,4 +167,16 @@ mod tests {
assert_eq!(colors.text, magenta);
assert_eq!(colors.background, green);
}
#[test]
fn deserialize_theme_colors_refinement_from_json() {
let colors: ThemeColorsRefinement = serde_json::from_value(json!({
"background": "#ff00ff",
"text": "#ff0000"
}))
.unwrap();
assert_eq!(colors.background, Some(gpui::rgb(0xff00ff)));
assert_eq!(colors.text, Some(gpui::rgb(0xff0000)));
}
}

View File

@ -205,6 +205,8 @@ impl ThemeColors {
border: neutral().light().step_6(),
border_variant: neutral().light().step_5(),
border_focused: blue().light().step_5(),
border_disabled: neutral().light().step_3(),
border_selected: blue().light().step_5(),
border_transparent: system.transparent,
elevated_surface_background: neutral().light().step_2(),
surface_background: neutral().light().step_2(),
@ -267,6 +269,8 @@ impl ThemeColors {
border: neutral().dark().step_6(),
border_variant: neutral().dark().step_5(),
border_focused: blue().dark().step_5(),
border_disabled: neutral().dark().step_3(),
border_selected: blue().dark().step_5(),
border_transparent: system.transparent,
elevated_surface_background: neutral().dark().step_2(),
surface_background: neutral().dark().step_2(),

View File

@ -0,0 +1,49 @@
# Building UI with GPUI
## Common patterns
### Method ordering
- id
- Flex properties
- Position properties
- Size properties
- Style properties
- Handlers
- State properties
### Using the Label Component to Create UI Text
The `Label` component helps in displaying text on user interfaces. It creates an interface where specific parameters such as label color, line height style, and strikethrough can be set.
Firstly, to create a `Label` instance, use the `Label::new()` function. This function takes a string that will be displayed as text in the interface.
```rust
Label::new("Hello, world!");
```
Now let's dive a bit deeper into how to customize `Label` instances:
- **Setting Color:** To set the color of the label using various predefined color options such as `Default`, `Muted`, `Created`, `Modified`, `Deleted`, etc, the `color()` function is called on the `Label` instance:
```rust
Label::new("Hello, world!").color(LabelColor::Default);
```
- **Setting Line Height Style:** To set the line height style, the `line_height_style()` function is utilized:
```rust
Label::new("Hello, world!").line_height_style(LineHeightStyle::TextLabel);
```
- **Adding a Strikethrough:** To add a strikethrough in a `Label`, the `set_strikethrough()` function is used:
```rust
Label::new("Hello, world!").set_strikethrough(true);
```
That's it! Now you can use the `Label` component to create and customize text on your application's interface.
## Building a new component
TODO

View File

@ -1,57 +0,0 @@
# 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 buttons 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 components elevation is ascertained by combining the components 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

View File

@ -0,0 +1,160 @@
# Hello World
Let's work through the prototypical "Build a todo app" example to showcase how we might build a simple component from scratch.
## Setup
We'll create a headline, a list of todo items, and a form to add new items.
~~~rust
struct TodoList<V: 'static> {
headline: SharedString,
items: Vec<TodoItem>,
submit_form: ClickHandler<V>
}
struct TodoItem<V: 'static> {
text: SharedString,
completed: bool,
delete: ClickHandler<V>
}
impl<V: 'static> TodoList<V> {
pub fn new(
// Here we impl Into<SharedString>
headline: impl Into<SharedString>,
items: Vec<TodoItem>,
submit_form: ClickHandler<V>
) -> Self {
Self {
// and here we call .into() so we can simply pass a string
// when creating the headline. This pattern is used throughout
// outr components
headline: headline.into(),
items: Vec::new(),
submit_form,
}
}
}
~~~
All of this is relatively straightforward.
We use [gpui2::SharedString] in components instead of [std::string::String]. This allows us to [TODO: someone who actually knows please explain why we use SharedString].
When we want to pass an action we pass a `ClickHandler`. Whenever we want to add an action, the struct it belongs to needs to be generic over the view type `V`.
~~~rust
use gpui2::hsla
impl<V: 'static> TodoList<V> {
// ...
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div().size_4().bg(hsla(50.0/360.0, 1.0, 0.5, 1.0))
}
}
~~~
Every component needs a render method, and it should return `impl Component<V>`. This basic component will render a 16x16px yellow square on the screen.
A couple of questions might come to mind:
**Why is `size_4()` 16px, not 4px?**
gpui's style system is based on conventions created by [Tailwind CSS](https://tailwindcss.com/). Here is an example of the list of sizes for `width`: [Width - TailwindCSS Docs](https://tailwindcss.com/docs/width).
I'll quote from the Tailwind [Core Concepts](https://tailwindcss.com/docs/utility-first) docs here:
> Now I know what youre thinking, “this is an atrocity, what a horrible mess!”
> and youre right, its kind of ugly. In fact its just about impossible to
> think this is a good idea the first time you see it —
> you have to actually try it.
As you start using the Tailwind-style conventions you will be surprised how quick it makes it to build out UIs.
**Why `50.0/360.0` in `hsla()`?**
gpui [gpui2::Hsla] use `0.0-1.0` for all it's values, but it is common for tools to use `0-360` for hue.
This may change in the future, but this is a little trick that let's you use familiar looking values.
## Building out the container
Let's grab our [theme2::colors::ThemeColors] from the theme and start building out a basic container.
We can access the current theme's colors like this:
~~~rust
impl<V: 'static> TodoList<V> {
// ...
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let color = cx.theme().colors()
div().size_4().hsla(50.0/360.0, 1.0, 0.5, 1.0)
}
}
~~~
Now we have access to the complete set of colors defined in the theme.
~~~rust
use gpui2::hsla
impl<V: 'static> TodoList<V> {
// ...
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let color = cx.theme().colors()
div().size_4().bg(color.surface)
}
}
~~~
Let's finish up some basic styles for the container then move on to adding the other elements.
~~~rust
use gpui2::hsla
impl<V: 'static> TodoList<V> {
// ...
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let color = cx.theme().colors()
div()
// Flex properties
.flex()
.flex_col() // Stack elements vertically
.gap_2() // Add 8px of space between elements
// Size properties
.w_96() // Set width to 384px
.p_4() // Add 16px of padding on all sides
// Color properties
.bg(color.surface) // Set background color
.text_color(color.text) // Set text color
// Border properties
.rounded_md() // Add 4px of border radius
.border() // Add a 1px border
.border_color(color.border)
.child(
"Hello, world!"
)
}
}
~~~
### Headline
TODO
### List of todo items
TODO
### Input
TODO
### End result
TODO

25
crates/ui2/docs/todo.md Normal file
View File

@ -0,0 +1,25 @@
## Documentation priorities:
These are the priorities to get documented, in a rough stack rank order:
- [ ] label
- [ ] button
- [ ] icon_button
- [ ] icon
- [ ] list
- [ ] avatar
- [ ] panel
- [ ] modal
- [ ] palette
- [ ] input
- [ ] facepile
- [ ] player
- [ ] stacks
- [ ] context menu
- [ ] input
- [ ] textarea/multiline input (not built - not an editor)
- [ ] indicator
- [ ] public actor
- [ ] keybinding
- [ ] tab
- [ ] toast

View File

@ -1,71 +1,51 @@
mod assistant_panel;
mod breadcrumb;
mod buffer;
mod buffer_search;
mod chat_panel;
mod collab_panel;
mod command_palette;
mod avatar;
mod button;
mod checkbox;
mod context_menu;
mod copilot;
mod editor_pane;
mod details;
mod facepile;
mod icon;
mod icon_button;
mod indicator;
mod input;
mod keybinding;
mod language_selector;
mod label;
mod list;
mod modal;
mod multi_buffer;
mod notification_toast;
mod notifications_panel;
mod palette;
mod panel;
mod panes;
mod player;
mod player_stack;
mod project_panel;
mod recent_projects;
mod status_bar;
mod slot;
mod stack;
mod tab;
mod tab_bar;
mod terminal;
mod theme_selector;
mod title_bar;
mod toast;
mod toolbar;
mod traffic_lights;
mod workspace;
mod toggle;
mod tool_divider;
pub use assistant_panel::*;
pub use breadcrumb::*;
pub use buffer::*;
pub use buffer_search::*;
pub use chat_panel::*;
pub use collab_panel::*;
pub use command_palette::*;
pub use avatar::*;
pub use button::*;
pub use checkbox::*;
pub use context_menu::*;
pub use copilot::*;
pub use editor_pane::*;
pub use details::*;
pub use facepile::*;
pub use icon::*;
pub use icon_button::*;
pub use indicator::*;
pub use input::*;
pub use keybinding::*;
pub use language_selector::*;
pub use label::*;
pub use list::*;
pub use modal::*;
pub use multi_buffer::*;
pub use notification_toast::*;
pub use notifications_panel::*;
pub use palette::*;
pub use panel::*;
pub use panes::*;
pub use player::*;
pub use player_stack::*;
pub use project_panel::*;
pub use recent_projects::*;
pub use status_bar::*;
pub use slot::*;
pub use stack::*;
pub use tab::*;
pub use tab_bar::*;
pub use terminal::*;
pub use theme_selector::*;
pub use title_bar::*;
pub use toast::*;
pub use toolbar::*;
pub use traffic_lights::*;
pub use workspace::*;
pub use toggle::*;
pub use tool_divider::*;

View File

@ -2,8 +2,27 @@ use std::sync::Arc;
use gpui2::{div, rems, DefiniteLength, Hsla, MouseButton, WindowContext};
use crate::prelude::*;
use crate::{h_stack, Icon, IconColor, IconElement, Label, LabelColor, LineHeightStyle};
use crate::{prelude::*, IconButton};
/// Provides the flexibility to use either a standard
/// button or an icon button in a given context.
pub enum ButtonOrIconButton<V: 'static> {
Button(Button<V>),
IconButton(IconButton<V>),
}
impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
fn from(value: Button<V>) -> Self {
Self::Button(value)
}
}
impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
fn from(value: IconButton<V>) -> Self {
Self::IconButton(value)
}
}
#[derive(Default, PartialEq, Clone, Copy)]
pub enum IconPosition {

View File

@ -0,0 +1,220 @@
///! # Checkbox
///!
///! Checkboxes are used for multiple choices, not for mutually exclusive choices.
///! Each checkbox works independently from other checkboxes in the list,
///! therefore checking an additional box does not affect any other selections.
use gpui2::{
div, Component, ParentElement, SharedString, StatelessInteractive, Styled, ViewContext,
};
use theme2::ActiveTheme;
use crate::{Icon, IconColor, IconElement, Selected};
#[derive(Component)]
pub struct Checkbox {
id: SharedString,
checked: Selected,
disabled: bool,
}
impl Checkbox {
pub fn new(id: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
checked: Selected::Unselected,
disabled: false,
}
}
pub fn toggle(mut self) -> Self {
self.checked = match self.checked {
Selected::Selected => Selected::Unselected,
Selected::Unselected => Selected::Selected,
Selected::Indeterminate => Selected::Selected,
};
self
}
pub fn set_indeterminate(mut self) -> Self {
self.checked = Selected::Indeterminate;
self
}
pub fn set_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let group_id = format!("checkbox_group_{}", self.id);
// The icon is different depending on the state of the checkbox.
//
// We need the match to return all the same type,
// so we wrap the eatch result in a div.
//
// We are still exploring the best way to handle this.
let icon = match self.checked {
// When selected, we show a checkmark.
Selected::Selected => {
div().child(
IconElement::new(Icon::Check)
.size(crate::IconSize::Small)
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
IconColor::Disabled
} else {
IconColor::Selected
},
),
)
}
// In an indeterminate state, we show a dash.
Selected::Indeterminate => {
div().child(
IconElement::new(Icon::Dash)
.size(crate::IconSize::Small)
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
IconColor::Disabled
} else {
IconColor::Selected
},
),
)
}
// When unselected, we show nothing.
Selected::Unselected => div(),
};
// A checkbox could be in an indeterminate state,
// for example the indeterminate state could represent:
// - a group of options of which only some are selected
// - an enabled option that is no longer available
// - a previously agreed to license that has been updated
//
// For the sake of styles we treat the indeterminate state as selected,
// but it's icon will be different.
let selected =
self.checked == Selected::Selected || self.checked == Selected::Indeterminate;
// We could use something like this to make the checkbox background when selected:
//
// ~~~rust
// ...
// .when(selected, |this| {
// this.bg(cx.theme().colors().element_selected)
// })
// ~~~
//
// But we use a match instead here because the checkbox might be disabled,
// and it could be disabled _while_ it is selected, as well as while it is not selected.
let (bg_color, border_color) = match (self.disabled, selected) {
(true, _) => (
cx.theme().colors().ghost_element_disabled,
cx.theme().colors().border_disabled,
),
(false, true) => (
cx.theme().colors().element_selected,
cx.theme().colors().border,
),
(false, false) => (
cx.theme().colors().element_background,
cx.theme().colors().border,
),
};
div()
// Rather than adding `px_1()` to add some space around the checkbox,
// we use a larger parent element to create a slightly larger
// click area for the checkbox.
.size_5()
// Because we've enlarged the click area, we need to create a
// `group` to pass down interaction events to the checkbox.
.group(group_id.clone())
.child(
div()
.flex()
// This prevent the flex element from growing
// or shrinking in response to any size changes
.flex_none()
// The combo of `justify_center()` and `items_center()`
// is used frequently to center elements in a flex container.
//
// We use this to center the icon in the checkbox.
.justify_center()
.items_center()
.m_1()
.size_4()
.rounded_sm()
.bg(bg_color)
.border()
.border_color(border_color)
// We only want the interaction states to fire when we
// are in a checkbox that isn't disabled.
.when(!self.disabled, |this| {
// Here instead of `hover()` we use `group_hover()`
// to pass it the group id.
this.group_hover(group_id.clone(), |el| {
el.bg(cx.theme().colors().element_hover)
})
})
.child(icon),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::{h_stack, Story};
use gpui2::{Div, Render};
pub struct CheckboxStory;
impl Render for CheckboxStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Checkbox>(cx))
.child(Story::label(cx, "Default"))
.child(
h_stack()
.p_2()
.gap_2()
.rounded_md()
.border()
.border_color(cx.theme().colors().border)
.child(Checkbox::new("checkbox-enabled"))
.child(Checkbox::new("checkbox-intermediate").set_indeterminate())
.child(Checkbox::new("checkbox-selected").toggle()),
)
.child(Story::label(cx, "Disabled"))
.child(
h_stack()
.p_2()
.gap_2()
.rounded_md()
.border()
.border_color(cx.theme().colors().border)
.child(Checkbox::new("checkbox-disabled").set_disabled(true))
.child(
Checkbox::new("checkbox-disabled-intermediate")
.set_disabled(true)
.set_indeterminate(),
)
.child(
Checkbox::new("checkbox-disabled-selected")
.set_disabled(true)
.toggle(),
),
)
}
}
}

View File

@ -8,7 +8,7 @@ pub enum ContextMenuItem {
}
impl ContextMenuItem {
fn to_list_item<V: 'static>(self) -> ListItem<V> {
fn to_list_item<V: 'static>(self) -> ListItem {
match self {
ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
ContextMenuItem::Entry(label) => {
@ -49,15 +49,12 @@ impl ContextMenu {
.bg(cx.theme().colors().elevated_surface_background)
.border()
.border_color(cx.theme().colors().border)
.child(
List::new(
self.items
.into_iter()
.map(ContextMenuItem::to_list_item)
.collect(),
)
.toggle(ToggleState::Toggled),
)
.child(List::new(
self.items
.into_iter()
.map(ContextMenuItem::to_list_item::<V>)
.collect(),
))
}
}

View File

@ -22,6 +22,7 @@ pub enum IconColor {
Warning,
Success,
Info,
Selected,
}
impl IconColor {
@ -36,6 +37,7 @@ impl IconColor {
IconColor::Warning => cx.theme().status().warning,
IconColor::Success => cx.theme().status().success,
IconColor::Info => cx.theme().status().info,
IconColor::Selected => cx.theme().colors().icon_accent,
}
}
}
@ -55,6 +57,7 @@ pub enum Icon {
ChevronRight,
ChevronUp,
Close,
Dash,
Exit,
ExclamationTriangle,
File,
@ -112,6 +115,7 @@ impl Icon {
Icon::ChevronRight => "icons/chevron_right.svg",
Icon::ChevronUp => "icons/chevron_up.svg",
Icon::Close => "icons/x.svg",
Icon::Dash => "icons/dash.svg",
Icon::Exit => "icons/exit.svg",
Icon::ExclamationTriangle => "icons/warning.svg",
Icon::File => "icons/file.svg",

View File

@ -1,11 +1,11 @@
use gpui2::{div, px, relative, Div};
use gpui2::div;
use crate::settings::user_settings;
use crate::{
h_stack, v_stack, Avatar, ClickHandler, Icon, IconColor, IconElement, IconSize, Label,
LabelColor,
disclosure_control, h_stack, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label,
LabelColor, Toggle,
};
use crate::{prelude::*, Button};
use crate::{prelude::*, GraphicSlot};
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub enum ListItemVariant {
@ -29,7 +29,7 @@ pub struct ListHeader {
left_icon: Option<Icon>,
meta: Option<ListHeaderMeta>,
variant: ListItemVariant,
toggleable: Toggleable,
toggle: Toggle,
}
impl ListHeader {
@ -39,17 +39,12 @@ impl ListHeader {
left_icon: None,
meta: None,
variant: ListItemVariant::default(),
toggleable: Toggleable::NotToggleable,
toggle: Toggle::NotToggleable,
}
}
pub fn toggle(mut self, toggle: ToggleState) -> Self {
self.toggleable = toggle.into();
self
}
pub fn toggleable(mut self, toggleable: Toggleable) -> Self {
self.toggleable = toggleable;
pub fn toggle(mut self, toggle: Toggle) -> Self {
self.toggle = toggle;
self
}
@ -63,30 +58,8 @@ impl ListHeader {
self
}
fn disclosure_control<V: 'static>(&self) -> Div<V> {
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
let is_toggled = Toggleable::is_toggled(&self.toggleable);
match (is_toggleable, is_toggled) {
(false, _) => div(),
(_, true) => div().child(
IconElement::new(Icon::ChevronDown)
.color(IconColor::Muted)
.size(IconSize::Small),
),
(_, false) => div().child(
IconElement::new(Icon::ChevronRight)
.color(IconColor::Muted)
.size(IconSize::Small),
),
}
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
let is_toggled = self.toggleable.is_toggled();
let disclosure_control = self.disclosure_control();
let disclosure_control = disclosure_control(self.toggle);
let meta = match self.meta {
Some(ListHeaderMeta::Tools(icons)) => div().child(
@ -193,12 +166,6 @@ impl ListSubHeader {
}
}
#[derive(Clone)]
pub enum LeftContent {
Icon(Icon),
Avatar(SharedString),
}
#[derive(Default, PartialEq, Copy, Clone)]
pub enum ListEntrySize {
#[default]
@ -207,44 +174,36 @@ pub enum ListEntrySize {
}
#[derive(Component)]
pub enum ListItem<V: 'static> {
pub enum ListItem {
Entry(ListEntry),
Details(ListDetailsEntry<V>),
Separator(ListSeparator),
Header(ListSubHeader),
}
impl<V: 'static> From<ListEntry> for ListItem<V> {
impl From<ListEntry> for ListItem {
fn from(entry: ListEntry) -> Self {
Self::Entry(entry)
}
}
impl<V: 'static> From<ListDetailsEntry<V>> for ListItem<V> {
fn from(entry: ListDetailsEntry<V>) -> Self {
Self::Details(entry)
}
}
impl<V: 'static> From<ListSeparator> for ListItem<V> {
impl From<ListSeparator> for ListItem {
fn from(entry: ListSeparator) -> Self {
Self::Separator(entry)
}
}
impl<V: 'static> From<ListSubHeader> for ListItem<V> {
impl From<ListSubHeader> for ListItem {
fn from(entry: ListSubHeader) -> Self {
Self::Header(entry)
}
}
impl<V: 'static> ListItem<V> {
fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
impl ListItem {
fn render<V: 'static>(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
match self {
ListItem::Entry(entry) => div().child(entry.render(view, cx)),
ListItem::Separator(separator) => div().child(separator.render(view, cx)),
ListItem::Header(header) => div().child(header.render(view, cx)),
ListItem::Details(details) => div().child(details.render(view, cx)),
}
}
@ -263,31 +222,29 @@ impl<V: 'static> ListItem<V> {
#[derive(Component)]
pub struct ListEntry {
disclosure_control_style: DisclosureControlVisibility,
disabled: bool,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
indent_level: u32,
label: Label,
left_content: Option<LeftContent>,
variant: ListItemVariant,
size: ListEntrySize,
state: InteractionState,
toggle: Option<ToggleState>,
left_slot: Option<GraphicSlot>,
overflow: OverflowStyle,
size: ListEntrySize,
toggle: Toggle,
variant: ListItemVariant,
}
impl ListEntry {
pub fn new(label: Label) -> Self {
Self {
disclosure_control_style: DisclosureControlVisibility::default(),
disabled: false,
indent_level: 0,
label,
variant: ListItemVariant::default(),
left_content: None,
size: ListEntrySize::default(),
state: InteractionState::default(),
// TODO: Should use Toggleable::NotToggleable
// or remove Toggleable::NotToggleable from the system
toggle: None,
left_slot: None,
overflow: OverflowStyle::Hidden,
size: ListEntrySize::default(),
toggle: Toggle::NotToggleable,
variant: ListItemVariant::default(),
}
}
@ -301,28 +258,23 @@ impl ListEntry {
self
}
pub fn toggle(mut self, toggle: ToggleState) -> Self {
self.toggle = Some(toggle);
pub fn toggle(mut self, toggle: Toggle) -> Self {
self.toggle = toggle;
self
}
pub fn left_content(mut self, left_content: LeftContent) -> Self {
self.left_content = Some(left_content);
pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
self.left_slot = Some(left_content);
self
}
pub fn left_icon(mut self, left_icon: Icon) -> Self {
self.left_content = Some(LeftContent::Icon(left_icon));
self.left_slot = Some(GraphicSlot::Icon(left_icon));
self
}
pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
self.left_content = Some(LeftContent::Avatar(left_avatar.into()));
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
self
}
@ -331,63 +283,19 @@ impl ListEntry {
self
}
pub fn disclosure_control_style(
mut self,
disclosure_control_style: DisclosureControlVisibility,
) -> Self {
self.disclosure_control_style = disclosure_control_style;
self
}
fn label_color(&self) -> LabelColor {
match self.state {
InteractionState::Disabled => LabelColor::Disabled,
_ => Default::default(),
}
}
fn icon_color(&self) -> IconColor {
match self.state {
InteractionState::Disabled => IconColor::Disabled,
_ => Default::default(),
}
}
fn disclosure_control<V: 'static>(
&mut self,
cx: &mut ViewContext<V>,
) -> Option<impl Component<V>> {
let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
IconElement::new(Icon::ChevronDown)
} else {
IconElement::new(Icon::ChevronRight)
}
.color(IconColor::Muted)
.size(IconSize::Small);
match (self.toggle, self.disclosure_control_style) {
(Some(_), DisclosureControlVisibility::OnHover) => {
Some(div().absolute().neg_left_5().child(disclosure_control_icon))
}
(Some(_), DisclosureControlVisibility::Always) => {
Some(div().child(disclosure_control_icon))
}
(None, _) => None,
}
}
fn render<V: 'static>(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let settings = user_settings(cx);
let left_content = match self.left_content.clone() {
Some(LeftContent::Icon(i)) => Some(
let left_content = match self.left_slot.clone() {
Some(GraphicSlot::Icon(i)) => Some(
h_stack().child(
IconElement::new(i)
.size(IconSize::Small)
.color(IconColor::Muted),
),
),
Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))),
None => None,
};
@ -400,10 +308,11 @@ impl ListEntry {
.relative()
.group("")
.bg(cx.theme().colors().surface_background)
.when(self.state == InteractionState::Focused, |this| {
this.border()
.border_color(cx.theme().colors().border_focused)
})
// TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| {
// this.border()
// .border_color(cx.theme().colors().border_focused)
// })
.child(
sized_item
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
@ -425,131 +334,13 @@ impl ListEntry {
.gap_1()
.items_center()
.relative()
.children(self.disclosure_control(cx))
.child(disclosure_control(self.toggle))
.children(left_content)
.child(self.label),
)
}
}
struct ListDetailsEntryHandlers<V: 'static> {
click: Option<ClickHandler<V>>,
}
impl<V: 'static> Default for ListDetailsEntryHandlers<V> {
fn default() -> Self {
Self { click: None }
}
}
#[derive(Component)]
pub struct ListDetailsEntry<V: 'static> {
label: SharedString,
meta: Option<SharedString>,
left_content: Option<LeftContent>,
handlers: ListDetailsEntryHandlers<V>,
actions: Option<Vec<Button<V>>>,
// TODO: make this more generic instead of
// specifically for notifications
seen: bool,
}
impl<V: 'static> ListDetailsEntry<V> {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
meta: None,
left_content: None,
handlers: ListDetailsEntryHandlers::default(),
actions: None,
seen: false,
}
}
pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
self.meta = Some(meta.into());
self
}
pub fn seen(mut self, seen: bool) -> Self {
self.seen = seen;
self
}
pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
self.handlers.click = Some(handler);
self
}
pub fn actions(mut self, actions: Vec<Button<V>>) -> Self {
self.actions = Some(actions);
self
}
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let settings = user_settings(cx);
let (item_bg, item_bg_hover, item_bg_active) = (
cx.theme().colors().ghost_element_background,
cx.theme().colors().ghost_element_hover,
cx.theme().colors().ghost_element_active,
);
let label_color = match self.seen {
true => LabelColor::Muted,
false => LabelColor::Default,
};
div()
.relative()
.group("")
.bg(item_bg)
.px_2()
.py_1p5()
.w_full()
.z_index(1)
.when(!self.seen, |this| {
this.child(
div()
.absolute()
.left(px(3.0))
.top_3()
.rounded_full()
.border_2()
.border_color(cx.theme().colors().surface_background)
.w(px(9.0))
.h(px(9.0))
.z_index(2)
.bg(cx.theme().status().info),
)
})
.child(
v_stack()
.w_full()
.line_height(relative(1.2))
.gap_1()
.child(
div()
.w_5()
.h_5()
.rounded_full()
.bg(cx.theme().colors().icon_accent),
)
.child(Label::new(self.label.clone()).color(label_color))
.children(
self.meta
.map(|meta| Label::new(meta).color(LabelColor::Muted)),
)
.child(
h_stack()
.gap_1()
.justify_end()
.children(self.actions.unwrap_or_default()),
),
)
}
}
#[derive(Clone, Component)]
pub struct ListSeparator;
@ -564,20 +355,22 @@ impl ListSeparator {
}
#[derive(Component)]
pub struct List<V: 'static> {
items: Vec<ListItem<V>>,
pub struct List {
items: Vec<ListItem>,
/// Message to display when the list is empty
/// Defaults to "No items"
empty_message: SharedString,
header: Option<ListHeader>,
toggleable: Toggleable,
toggle: Toggle,
}
impl<V: 'static> List<V> {
pub fn new(items: Vec<ListItem<V>>) -> Self {
impl List {
pub fn new(items: Vec<ListItem>) -> Self {
Self {
items,
empty_message: "No items".into(),
header: None,
toggleable: Toggleable::default(),
toggle: Toggle::NotToggleable,
}
}
@ -591,19 +384,16 @@ impl<V: 'static> List<V> {
self
}
pub fn toggle(mut self, toggle: ToggleState) -> Self {
self.toggleable = toggle.into();
pub fn toggle(mut self, toggle: Toggle) -> Self {
self.toggle = toggle;
self
}
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
let is_toggled = Toggleable::is_toggled(&self.toggleable);
let list_content = match (self.items.is_empty(), is_toggled) {
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let list_content = match (self.items.is_empty(), self.toggle) {
(false, _) => div().children(self.items),
(true, false) => div(),
(true, true) => {
(true, Toggle::Toggled(false)) => div(),
(true, _) => {
div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
}
};
@ -611,7 +401,7 @@ impl<V: 'static> List<V> {
v_stack()
.w_full()
.py_1()
.children(self.header.map(|header| header.toggleable(self.toggleable)))
.children(self.header.map(|header| header))
.child(list_content)
}
}

View File

@ -2,6 +2,24 @@ use gpui2::{Hsla, ViewContext};
use crate::prelude::*;
/// Represents a person with a Zed account's public profile.
/// All data in this struct should be considered public.
pub struct PublicPlayer {
pub username: SharedString,
pub avatar: SharedString,
pub is_contact: bool,
}
impl PublicPlayer {
pub fn new(username: impl Into<SharedString>, avatar: impl Into<SharedString>) -> Self {
Self {
username: username.into(),
avatar: avatar.into(),
is_contact: false,
}
}
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum PlayerStatus {
#[default]

View File

@ -0,0 +1,14 @@
use gpui2::SharedString;
use crate::Icon;
#[derive(Debug, Clone)]
/// A slot utility that provides a way to to pass either
/// an icon or an image to a component.
///
/// Can be filled with a []
pub enum GraphicSlot {
Icon(Icon),
Avatar(SharedString),
PublicActor(SharedString),
}

View File

@ -0,0 +1,61 @@
use gpui2::{div, Component, ParentElement};
use crate::{Icon, IconColor, IconElement, IconSize};
/// Whether the entry is toggleable, and if so, whether it is currently toggled.
///
/// To make an element toggleable, simply add a `Toggle::Toggled(_)` and handle it's cases.
///
/// You can check if an element is toggleable with `.is_toggleable()`
///
/// Possible values:
/// - `Toggle::NotToggleable` - The entry is not toggleable
/// - `Toggle::Toggled(true)` - The entry is toggleable and toggled
/// - `Toggle::Toggled(false)` - The entry is toggleable and not toggled
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Toggle {
NotToggleable,
Toggled(bool),
}
impl Toggle {
/// Returns true if the entry is toggled (or is not toggleable.)
///
/// As element that isn't toggleable is always "expanded" or "enabled"
/// returning true in that case makes sense.
pub fn is_toggled(&self) -> bool {
match self {
Self::Toggled(false) => false,
_ => true,
}
}
pub fn is_toggleable(&self) -> bool {
match self {
Self::Toggled(_) => true,
_ => false,
}
}
}
impl From<bool> for Toggle {
fn from(toggled: bool) -> Self {
Toggle::Toggled(toggled)
}
}
pub fn disclosure_control<V: 'static>(toggle: Toggle) -> impl Component<V> {
match (toggle.is_toggleable(), toggle.is_toggled()) {
(false, _) => div(),
(_, true) => div().child(
IconElement::new(Icon::ChevronDown)
.color(IconColor::Muted)
.size(IconSize::Small),
),
(_, false) => div().child(
IconElement::new(Icon::ChevronRight)
.color(IconColor::Muted)
.size(IconSize::Small),
),
}
}

View File

@ -1,21 +0,0 @@
mod avatar;
mod button;
mod details;
mod icon;
mod indicator;
mod input;
mod label;
mod player;
mod stack;
mod tool_divider;
pub use avatar::*;
pub use button::*;
pub use details::*;
pub use icon::*;
pub use indicator::*;
pub use input::*;
pub use label::*;
pub use player::*;
pub use stack::*;
pub use tool_divider::*;

View File

@ -7,28 +7,25 @@
//! This crate is still a work in progress. The initial primitives and components are built for getting all the UI on the screen,
//! much of the state and functionality is mocked or hard codeded, and performance has not been a focus.
//!
//! Expect some inconsistencies from component to component as we work out the best way to build these components.
//!
//! ## Design Philosophy
//!
//! Work in Progress!
//!
#![doc = include_str!("../docs/hello-world.md")]
#![doc = include_str!("../docs/building-ui.md")]
#![doc = include_str!("../docs/todo.md")]
// TODO: Fix warnings instead of supressing.
#![allow(dead_code, unused_variables)]
mod components;
mod elements;
mod elevation;
pub mod prelude;
pub mod settings;
mod static_data;
mod to_extract;
pub mod utils;
pub use components::*;
pub use elements::*;
pub use prelude::*;
pub use static_data::*;
pub use to_extract::*;
// This needs to be fully qualified with `crate::` otherwise we get a panic
// at:

View File

@ -10,24 +10,6 @@ pub use theme2::ActiveTheme;
use gpui2::Hsla;
use strum::EnumIter;
/// Represents a person with a Zed account's public profile.
/// All data in this struct should be considered public.
pub struct PublicActor {
pub username: SharedString,
pub avatar: SharedString,
pub is_contact: bool,
}
impl PublicActor {
pub fn new(username: impl Into<SharedString>, avatar: impl Into<SharedString>) -> Self {
Self {
username: username.into(),
avatar: avatar.into(),
is_contact: false,
}
}
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum FileSystemStatus {
#[default]
@ -172,68 +154,10 @@ impl InteractionState {
}
}
#[derive(Default, PartialEq)]
pub enum SelectedState {
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Selected {
#[default]
Unselected,
PartiallySelected,
Indeterminate,
Selected,
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
pub enum Toggleable {
Toggleable(ToggleState),
#[default]
NotToggleable,
}
impl Toggleable {
pub fn is_toggled(&self) -> bool {
match self {
Self::Toggleable(ToggleState::Toggled) => true,
_ => false,
}
}
}
impl From<ToggleState> for Toggleable {
fn from(state: ToggleState) -> Self {
Self::Toggleable(state)
}
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
pub enum ToggleState {
/// The "on" state of a toggleable element.
///
/// Example:
/// - A collasable list that is currently expanded
/// - A toggle button that is currently on.
Toggled,
/// The "off" state of a toggleable element.
///
/// Example:
/// - A collasable list that is currently collapsed
/// - A toggle button that is currently off.
#[default]
NotToggled,
}
impl From<Toggleable> for ToggleState {
fn from(toggleable: Toggleable) -> Self {
match toggleable {
Toggleable::Toggleable(state) => state,
Toggleable::NotToggleable => ToggleState::NotToggled,
}
}
}
impl From<bool> for ToggleState {
fn from(toggled: bool) -> Self {
if toggled {
ToggleState::Toggled
} else {
ToggleState::NotToggled
}
}
}

View File

@ -7,13 +7,13 @@ use gpui2::{AppContext, ViewContext};
use rand::Rng;
use theme2::ActiveTheme;
use crate::HighlightedText;
use crate::{
Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListSubHeader,
Livestream, MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus,
PlayerWithCallStatus, PublicActor, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus,
HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream,
MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus,
PlayerWithCallStatus, PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus,
};
use crate::{HighlightedText, ListDetailsEntry};
use crate::{ListItem, NotificationAction};
pub fn static_tabs_example() -> Vec<Tab> {
@ -345,7 +345,7 @@ pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z")
.unwrap()
.naive_local(),
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
[
NotificationAction::new(
Button::new("Decline"),
@ -374,7 +374,7 @@ pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
DateTime::parse_from_rfc3339("2023-11-01T12:09:07Z")
.unwrap()
.naive_local(),
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
[
NotificationAction::new(
Button::new("Decline"),
@ -403,7 +403,7 @@ pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
DateTime::parse_from_rfc3339("2022-10-25T12:09:07Z")
.unwrap()
.naive_local(),
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
[
NotificationAction::new(
Button::new("Decline"),
@ -432,7 +432,7 @@ pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
DateTime::parse_from_rfc3339("2021-10-12T12:09:07Z")
.unwrap()
.naive_local(),
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
[
NotificationAction::new(
Button::new("Decline"),
@ -461,7 +461,7 @@ pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z")
.unwrap()
.naive_local(),
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
[
NotificationAction::new(
Button::new("Decline"),
@ -478,89 +478,12 @@ pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
]
}
pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
vec![
ListItem::Header(ListSubHeader::new("New")),
ListItem::Details(
ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.")
.meta("4 people in stream."),
),
ListItem::Details(ListDetailsEntry::new(
"nathansobo accepted your contact request.",
)),
ListItem::Header(ListSubHeader::new("Earlier")),
ListItem::Details(
ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
Button::new("Decline"),
Button::new("Accept").variant(crate::ButtonVariant::Filled),
]),
),
ListItem::Details(
ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
.seen(true)
.meta("This stream has ended."),
),
ListItem::Details(ListDetailsEntry::new(
"as-cii accepted your contact request.",
)),
ListItem::Details(
ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true),
),
ListItem::Details(ListDetailsEntry::new(
"osiewicz accepted your contact request.",
)),
ListItem::Details(ListDetailsEntry::new(
"ConradIrwin accepted your contact request.",
)),
ListItem::Details(
ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.")
.seen(true)
.meta("This stream has ended."),
),
ListItem::Details(ListDetailsEntry::new(
"nathansobo accepted your contact request.",
)),
ListItem::Header(ListSubHeader::new("Earlier")),
ListItem::Details(
ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
Button::new("Decline"),
Button::new("Accept").variant(crate::ButtonVariant::Filled),
]),
),
ListItem::Details(
ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
.seen(true)
.meta("This stream has ended."),
),
ListItem::Details(ListDetailsEntry::new(
"as-cii accepted your contact request.",
)),
ListItem::Details(
ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true),
),
ListItem::Details(ListDetailsEntry::new(
"osiewicz accepted your contact request.",
)),
ListItem::Details(ListDetailsEntry::new(
"ConradIrwin accepted your contact request.",
)),
ListItem::Details(
ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.")
.seen(true)
.meta("This stream has ended."),
),
]
.into_iter()
.map(From::from)
.collect()
}
pub fn static_project_panel_project_items<V: 'static>() -> Vec<ListItem<V>> {
pub fn static_project_panel_project_items() -> Vec<ListItem> {
vec![
ListEntry::new(Label::new("zed"))
.left_icon(Icon::FolderOpen.into())
.indent_level(0)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
ListEntry::new(Label::new(".cargo"))
.left_icon(Icon::Folder.into())
.indent_level(1),
@ -579,14 +502,14 @@ pub fn static_project_panel_project_items<V: 'static>() -> Vec<ListItem<V>> {
ListEntry::new(Label::new("assets"))
.left_icon(Icon::Folder.into())
.indent_level(1)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden))
.left_icon(Icon::Folder.into())
.indent_level(1),
ListEntry::new(Label::new("crates"))
.left_icon(Icon::FolderOpen.into())
.indent_level(1)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
ListEntry::new(Label::new("activity_indicator"))
.left_icon(Icon::Folder.into())
.indent_level(2),
@ -608,38 +531,38 @@ pub fn static_project_panel_project_items<V: 'static>() -> Vec<ListItem<V>> {
ListEntry::new(Label::new("sqlez").color(LabelColor::Modified))
.left_icon(Icon::Folder.into())
.indent_level(2)
.toggle(ToggleState::NotToggled),
.toggle(Toggle::Toggled(false)),
ListEntry::new(Label::new("gpui2"))
.left_icon(Icon::FolderOpen.into())
.indent_level(2)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
ListEntry::new(Label::new("src"))
.left_icon(Icon::FolderOpen.into())
.indent_level(3)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
ListEntry::new(Label::new("derive_element.rs"))
.left_icon(Icon::FileRust.into())
.indent_level(4),
ListEntry::new(Label::new("storybook").color(LabelColor::Modified))
.left_icon(Icon::FolderOpen.into())
.indent_level(1)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
ListEntry::new(Label::new("docs").color(LabelColor::Default))
.left_icon(Icon::Folder.into())
.indent_level(2)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
ListEntry::new(Label::new("src").color(LabelColor::Modified))
.left_icon(Icon::FolderOpen.into())
.indent_level(3)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
ListEntry::new(Label::new("ui").color(LabelColor::Modified))
.left_icon(Icon::FolderOpen.into())
.indent_level(4)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
ListEntry::new(Label::new("component").color(LabelColor::Created))
.left_icon(Icon::FolderOpen.into())
.indent_level(5)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default))
.left_icon(Icon::FileRust.into())
.indent_level(6),
@ -682,7 +605,7 @@ pub fn static_project_panel_project_items<V: 'static>() -> Vec<ListItem<V>> {
.collect()
}
pub fn static_project_panel_single_items<V: 'static>() -> Vec<ListItem<V>> {
pub fn static_project_panel_single_items() -> Vec<ListItem> {
vec![
ListEntry::new(Label::new("todo.md"))
.left_icon(Icon::FileDoc.into())
@ -699,7 +622,7 @@ pub fn static_project_panel_single_items<V: 'static>() -> Vec<ListItem<V>> {
.collect()
}
pub fn static_collab_panel_current_call<V: 'static>() -> Vec<ListItem<V>> {
pub fn static_collab_panel_current_call() -> Vec<ListItem> {
vec![
ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
ListEntry::new(Label::new("nathansobo"))
@ -712,7 +635,7 @@ pub fn static_collab_panel_current_call<V: 'static>() -> Vec<ListItem<V>> {
.collect()
}
pub fn static_collab_panel_channels<V: 'static>() -> Vec<ListItem<V>> {
pub fn static_collab_panel_channels() -> Vec<ListItem> {
vec![
ListEntry::new(Label::new("zed"))
.left_icon(Icon::Hash.into())

View File

@ -0,0 +1,47 @@
mod assistant_panel;
mod breadcrumb;
mod buffer;
mod buffer_search;
mod chat_panel;
mod collab_panel;
mod command_palette;
mod copilot;
mod editor_pane;
mod language_selector;
mod multi_buffer;
mod notifications_panel;
mod panes;
mod project_panel;
mod recent_projects;
mod status_bar;
mod tab_bar;
mod terminal;
mod theme_selector;
mod title_bar;
mod toolbar;
mod traffic_lights;
mod workspace;
pub use assistant_panel::*;
pub use breadcrumb::*;
pub use buffer::*;
pub use buffer_search::*;
pub use chat_panel::*;
pub use collab_panel::*;
pub use command_palette::*;
pub use copilot::*;
pub use editor_pane::*;
pub use language_selector::*;
pub use multi_buffer::*;
pub use notifications_panel::*;
pub use panes::*;
pub use project_panel::*;
pub use recent_projects::*;
pub use status_bar::*;
pub use tab_bar::*;
pub use terminal::*;
pub use theme_selector::*;
pub use title_bar::*;
pub use toolbar::*;
pub use traffic_lights::*;
pub use workspace::*;

View File

@ -1,7 +1,6 @@
use crate::prelude::*;
use crate::{prelude::*, Toggle};
use crate::{
static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List,
ListHeader, ToggleState,
static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List, ListHeader,
};
#[derive(Component)]
@ -34,17 +33,17 @@ impl CollabPanel {
.header(
ListHeader::new("CRDB")
.left_icon(Icon::Hash.into())
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
),
)
.child(
v_stack().id("channels").py_1().child(
List::new(static_collab_panel_channels())
.header(ListHeader::new("CHANNELS").toggle(ToggleState::Toggled))
.header(ListHeader::new("CHANNELS").toggle(Toggle::Toggled(true)))
.empty_message("No channels yet. Add a channel to get started.")
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
),
)
.child(
@ -52,9 +51,9 @@ impl CollabPanel {
List::new(static_collab_panel_current_call())
.header(
ListHeader::new("CONTACTS ONLINE")
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
)
.toggle(ToggleState::Toggled),
.toggle(Toggle::Toggled(true)),
),
)
.child(
@ -62,9 +61,9 @@ impl CollabPanel {
List::new(static_collab_panel_current_call())
.header(
ListHeader::new("CONTACTS OFFLINE")
.toggle(ToggleState::NotToggled),
.toggle(Toggle::Toggled(false)),
)
.toggle(ToggleState::NotToggled),
.toggle(Toggle::Toggled(false)),
),
),
)

View File

@ -1,8 +1,8 @@
use crate::utils::naive_format_distance_from_now;
use crate::{
h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, Button, Icon,
IconButton, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator,
UnreadIndicator,
h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, ButtonOrIconButton,
Icon, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator,
PublicPlayer, UnreadIndicator,
};
use crate::{ClickHandler, ListHeader};
@ -57,23 +57,6 @@ impl NotificationsPanel {
}
}
pub enum ButtonOrIconButton<V: 'static> {
Button(Button<V>),
IconButton(IconButton<V>),
}
impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
fn from(value: Button<V>) -> Self {
Self::Button(value)
}
}
impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
fn from(value: IconButton<V>) -> Self {
Self::IconButton(value)
}
}
pub struct NotificationAction<V: 'static> {
button: ButtonOrIconButton<V>,
tooltip: SharedString,
@ -102,7 +85,7 @@ impl<V: 'static> NotificationAction<V> {
}
pub enum ActorOrIcon {
Actor(PublicActor),
Actor(PublicPlayer),
Icon(Icon),
}
@ -171,7 +154,7 @@ impl<V> Notification<V> {
id: impl Into<ElementId>,
message: impl Into<SharedString>,
date_received: NaiveDateTime,
actor: PublicActor,
actor: PublicPlayer,
click_action: ClickHandler<V>,
) -> Self {
Self::new(
@ -210,7 +193,7 @@ impl<V> Notification<V> {
id: impl Into<ElementId>,
message: impl Into<SharedString>,
date_received: NaiveDateTime,
actor: PublicActor,
actor: PublicPlayer,
actions: [NotificationAction<V>; 2],
) -> Self {
Self::new(

View File

@ -18,8 +18,7 @@ impl ProjectPanel {
.id(self.id.clone())
.flex()
.flex_col()
.w_full()
.h_full()
.size_full()
.bg(cx.theme().colors().surface_background)
.child(
div()
@ -30,15 +29,13 @@ impl ProjectPanel {
.overflow_y_scroll()
.child(
List::new(static_project_panel_single_items())
.header(ListHeader::new("FILES").toggle(ToggleState::Toggled))
.empty_message("No files in directory")
.toggle(ToggleState::Toggled),
.header(ListHeader::new("FILES"))
.empty_message("No files in directory"),
)
.child(
List::new(static_project_panel_project_items())
.header(ListHeader::new("PROJECT").toggle(ToggleState::Toggled))
.empty_message("No folders in directory")
.toggle(ToggleState::Toggled),
.header(ListHeader::new("PROJECT"))
.empty_message("No folders in directory"),
),
)
.child(

View File

@ -140,8 +140,8 @@ impl LspAdapter for ElixirLspAdapter {
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
let version_dir = container_dir.join(format!("elixir-ls_{}", version.name));
let binary_path = version_dir.join("language_server.sh");
let folder_path = container_dir.join("elixir-ls");
let binary_path = folder_path.join("language_server.sh");
if fs::metadata(&binary_path).await.is_err() {
let mut response = delegate
@ -160,13 +160,13 @@ impl LspAdapter for ElixirLspAdapter {
}
futures::io::copy(response.body_mut(), &mut file).await?;
fs::create_dir_all(&version_dir)
fs::create_dir_all(&folder_path)
.await
.with_context(|| format!("failed to create directory {}", version_dir.display()))?;
.with_context(|| format!("failed to create directory {}", folder_path.display()))?;
let unzip_status = smol::process::Command::new("unzip")
.arg(&zip_path)
.arg("-d")
.arg(&version_dir)
.arg(&folder_path)
.output()
.await?
.status;
@ -174,7 +174,7 @@ impl LspAdapter for ElixirLspAdapter {
Err(anyhow!("failed to unzip elixir-ls archive"))?;
}
remove_matching(&container_dir, |entry| entry != version_dir).await;
remove_matching(&container_dir, |entry| entry != folder_path).await;
}
Ok(LanguageServerBinary {
@ -285,20 +285,16 @@ impl LspAdapter for ElixirLspAdapter {
async fn get_cached_server_binary_elixir_ls(
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
(|| async move {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
last = Some(entry?.path());
}
last.map(|path| LanguageServerBinary {
path,
let server_path = container_dir.join("elixir-ls/language_server.sh");
if server_path.exists() {
Some(LanguageServerBinary {
path: server_path,
arguments: vec![],
})
.ok_or_else(|| anyhow!("no cached binary"))
})()
.await
.log_err()
} else {
log::error!("missing executable in directory {:?}", server_path);
None
}
}
pub struct NextLspAdapter;

View File

@ -140,8 +140,8 @@ impl LspAdapter for ElixirLspAdapter {
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
let version_dir = container_dir.join(format!("elixir-ls_{}", version.name));
let binary_path = version_dir.join("language_server.sh");
let folder_path = container_dir.join("elixir-ls");
let binary_path = folder_path.join("language_server.sh");
if fs::metadata(&binary_path).await.is_err() {
let mut response = delegate
@ -160,13 +160,13 @@ impl LspAdapter for ElixirLspAdapter {
}
futures::io::copy(response.body_mut(), &mut file).await?;
fs::create_dir_all(&version_dir)
fs::create_dir_all(&folder_path)
.await
.with_context(|| format!("failed to create directory {}", version_dir.display()))?;
.with_context(|| format!("failed to create directory {}", folder_path.display()))?;
let unzip_status = smol::process::Command::new("unzip")
.arg(&zip_path)
.arg("-d")
.arg(&version_dir)
.arg(&folder_path)
.output()
.await?
.status;
@ -174,7 +174,7 @@ impl LspAdapter for ElixirLspAdapter {
Err(anyhow!("failed to unzip elixir-ls archive"))?;
}
remove_matching(&container_dir, |entry| entry != version_dir).await;
remove_matching(&container_dir, |entry| entry != folder_path).await;
}
Ok(LanguageServerBinary {
@ -285,20 +285,16 @@ impl LspAdapter for ElixirLspAdapter {
async fn get_cached_server_binary_elixir_ls(
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
(|| async move {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
last = Some(entry?.path());
}
last.map(|path| LanguageServerBinary {
path,
let server_path = container_dir.join("elixir-ls/language_server.sh");
if server_path.exists() {
Some(LanguageServerBinary {
path: server_path,
arguments: vec![],
})
.ok_or_else(|| anyhow!("no cached binary"))
})()
.await
.log_err()
} else {
log::error!("missing executable in directory {:?}", server_path);
None
}
}
pub struct NextLspAdapter;