Merge remote-tracking branch 'origin/main' into editor2-paint

This commit is contained in:
Antonio Scandurra 2023-11-07 13:09:48 +01:00
commit bdf6e8bcc7
204 changed files with 48828 additions and 1648 deletions

View file

@ -0,0 +1,93 @@
use crate::prelude::*;
use crate::{Icon, IconButton, Label, Panel, PanelSide};
use gpui::{rems, AbsoluteLength};
#[derive(Component)]
pub struct AssistantPanel {
id: ElementId,
current_side: PanelSide,
}
impl AssistantPanel {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
current_side: PanelSide::default(),
}
}
pub fn side(mut self, side: PanelSide) -> Self {
self.current_side = side;
self
}
fn render<V: 'static>(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
Panel::new(self.id.clone(), cx)
.children(vec![div()
.flex()
.flex_col()
.h_full()
.px_2()
.gap_2()
// Header
.child(
div()
.flex()
.justify_between()
.gap_2()
.child(
div()
.flex()
.child(IconButton::new("menu", Icon::Menu))
.child(Label::new("New Conversation")),
)
.child(
div()
.flex()
.items_center()
.gap_px()
.child(IconButton::new("split_message", Icon::SplitMessage))
.child(IconButton::new("quote", Icon::Quote))
.child(IconButton::new("magic_wand", Icon::MagicWand))
.child(IconButton::new("plus", Icon::Plus))
.child(IconButton::new("maximize", Icon::Maximize)),
),
)
// Chat Body
.child(
div()
.id("chat-body")
.w_full()
.flex()
.flex_col()
.gap_3()
.overflow_y_scroll()
.child(Label::new("Is this thing on?")),
)
.render()])
.side(self.current_side)
.width(AbsoluteLength::Rems(rems(32.)))
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::Story;
use gpui::{Div, Render};
pub struct AssistantPanelStory;
impl Render for AssistantPanelStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, AssistantPanel>(cx))
.child(Story::label(cx, "Default"))
.child(AssistantPanel::new("assistant-panel"))
}
}
}

View file

@ -0,0 +1,115 @@
use std::path::PathBuf;
use crate::prelude::*;
use crate::{h_stack, HighlightedText};
use gpui::Div;
#[derive(Clone)]
pub struct Symbol(pub Vec<HighlightedText>);
#[derive(Component)]
pub struct Breadcrumb {
path: PathBuf,
symbols: Vec<Symbol>,
}
impl Breadcrumb {
pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
Self { path, symbols }
}
fn render_separator<V: 'static>(&self, cx: &WindowContext) -> Div<V> {
div()
.child(" ")
.text_color(cx.theme().colors().text_muted)
}
fn render<V: 'static>(self, view_state: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let symbols_len = self.symbols.len();
h_stack()
.id("breadcrumb")
.px_1()
.text_sm()
.text_color(cx.theme().colors().text_muted)
.rounded_md()
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
.child(self.path.clone().to_str().unwrap().to_string())
.child(if !self.symbols.is_empty() {
self.render_separator(cx)
} else {
div()
})
.child(
div().flex().children(
self.symbols
.iter()
.enumerate()
// TODO: Could use something like `intersperse` here instead.
.flat_map(|(ix, symbol)| {
let mut items =
vec![div().flex().children(symbol.0.iter().map(|segment| {
div().child(segment.text.clone()).text_color(segment.color)
}))];
let is_last_segment = ix == symbols_len - 1;
if !is_last_segment {
items.push(self.render_separator(cx));
}
items
})
.collect::<Vec<_>>(),
),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::Story;
use gpui::Render;
use std::str::FromStr;
pub struct BreadcrumbStory;
impl Render for BreadcrumbStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Breadcrumb>(cx))
.child(Story::label(cx, "Default"))
.child(Breadcrumb::new(
PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
vec![
Symbol(vec![
HighlightedText {
text: "impl ".to_string(),
color: cx.theme().syntax_color("keyword"),
},
HighlightedText {
text: "BreadcrumbStory".to_string(),
color: cx.theme().syntax_color("function"),
},
]),
Symbol(vec![
HighlightedText {
text: "fn ".to_string(),
color: cx.theme().syntax_color("keyword"),
},
HighlightedText {
text: "render".to_string(),
color: cx.theme().syntax_color("function"),
},
]),
],
))
}
}
}

View file

@ -0,0 +1,266 @@
use gpui::{Hsla, WindowContext};
use crate::prelude::*;
use crate::{h_stack, v_stack, Icon, IconElement};
#[derive(Default, PartialEq, Copy, Clone)]
pub struct PlayerCursor {
color: Hsla,
index: usize,
}
#[derive(Default, PartialEq, Clone)]
pub struct HighlightedText {
pub text: String,
pub color: Hsla,
}
#[derive(Default, PartialEq, Clone)]
pub struct HighlightedLine {
pub highlighted_texts: Vec<HighlightedText>,
}
#[derive(Default, PartialEq, Clone)]
pub struct BufferRow {
pub line_number: usize,
pub code_action: bool,
pub current: bool,
pub line: Option<HighlightedLine>,
pub cursors: Option<Vec<PlayerCursor>>,
pub status: GitStatus,
pub show_line_number: bool,
}
#[derive(Clone)]
pub struct BufferRows {
pub show_line_numbers: bool,
pub rows: Vec<BufferRow>,
}
impl Default for BufferRows {
fn default() -> Self {
Self {
show_line_numbers: true,
rows: vec![BufferRow {
line_number: 1,
code_action: false,
current: true,
line: None,
cursors: None,
status: GitStatus::None,
show_line_number: true,
}],
}
}
}
impl BufferRow {
pub fn new(line_number: usize) -> Self {
Self {
line_number,
code_action: false,
current: false,
line: None,
cursors: None,
status: GitStatus::None,
show_line_number: true,
}
}
pub fn set_line(mut self, line: Option<HighlightedLine>) -> Self {
self.line = line;
self
}
pub fn set_cursors(mut self, cursors: Option<Vec<PlayerCursor>>) -> Self {
self.cursors = cursors;
self
}
pub fn add_cursor(mut self, cursor: PlayerCursor) -> Self {
if let Some(cursors) = &mut self.cursors {
cursors.push(cursor);
} else {
self.cursors = Some(vec![cursor]);
}
self
}
pub fn set_status(mut self, status: GitStatus) -> Self {
self.status = status;
self
}
pub fn set_show_line_number(mut self, show_line_number: bool) -> Self {
self.show_line_number = show_line_number;
self
}
pub fn set_code_action(mut self, code_action: bool) -> Self {
self.code_action = code_action;
self
}
pub fn set_current(mut self, current: bool) -> Self {
self.current = current;
self
}
}
#[derive(Component, Clone)]
pub struct Buffer {
id: ElementId,
rows: Option<BufferRows>,
readonly: bool,
language: Option<String>,
title: Option<String>,
path: Option<String>,
}
impl Buffer {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
rows: Some(BufferRows::default()),
readonly: false,
language: None,
title: Some("untitled".to_string()),
path: None,
}
}
pub fn set_title<T: Into<Option<String>>>(mut self, title: T) -> Self {
self.title = title.into();
self
}
pub fn set_path<P: Into<Option<String>>>(mut self, path: P) -> Self {
self.path = path.into();
self
}
pub fn set_readonly(mut self, readonly: bool) -> Self {
self.readonly = readonly;
self
}
pub fn set_rows<R: Into<Option<BufferRows>>>(mut self, rows: R) -> Self {
self.rows = rows.into();
self
}
pub fn set_language<L: Into<Option<String>>>(mut self, language: L) -> Self {
self.language = language.into();
self
}
fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl Component<V> {
let line_background = if row.current {
cx.theme().colors().editor_active_line_background
} else {
cx.theme().styles.system.transparent
};
let line_number_color = if row.current {
cx.theme().colors().text
} else {
cx.theme().syntax_color("comment")
};
h_stack()
.bg(line_background)
.w_full()
.gap_2()
.px_1()
.child(
h_stack()
.w_4()
.h_full()
.px_0p5()
.when(row.code_action, |c| {
div().child(IconElement::new(Icon::Bolt))
}),
)
.when(row.show_line_number, |this| {
this.child(
h_stack().justify_end().px_0p5().w_3().child(
div()
.text_color(line_number_color)
.child(row.line_number.to_string()),
),
)
})
.child(div().mx_0p5().w_1().h_full().bg(row.status.hsla(cx)))
.children(row.line.map(|line| {
div()
.flex()
.children(line.highlighted_texts.iter().map(|highlighted_text| {
div()
.text_color(highlighted_text.color)
.child(highlighted_text.text.clone())
}))
}))
}
fn render_rows<V: 'static>(&self, cx: &WindowContext) -> Vec<impl Component<V>> {
match &self.rows {
Some(rows) => rows
.rows
.iter()
.map(|row| Self::render_row(row.clone(), cx))
.collect(),
None => vec![],
}
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let rows = self.render_rows(cx);
v_stack()
.flex_1()
.w_full()
.h_full()
.bg(cx.theme().colors().editor_background)
.children(rows)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::{
empty_buffer_example, hello_world_rust_buffer_example,
hello_world_rust_buffer_with_status_example, Story,
};
use gpui::{rems, Div, Render};
pub struct BufferStory;
impl Render for BufferStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Buffer>(cx))
.child(Story::label(cx, "Default"))
.child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
.child(Story::label(cx, "Hello World (Rust)"))
.child(
div()
.w(rems(64.))
.h_96()
.child(hello_world_rust_buffer_example(cx)),
)
.child(Story::label(cx, "Hello World (Rust) with Status"))
.child(
div()
.w(rems(64.))
.h_96()
.child(hello_world_rust_buffer_with_status_example(cx)),
)
}
}
}

View file

@ -0,0 +1,46 @@
use gpui::{Div, Render, View, VisualContext};
use crate::prelude::*;
use crate::{h_stack, Icon, IconButton, IconColor, Input};
#[derive(Clone)]
pub struct BufferSearch {
is_replace_open: bool,
}
impl BufferSearch {
pub fn new() -> Self {
Self {
is_replace_open: false,
}
}
fn toggle_replace(&mut self, cx: &mut ViewContext<Self>) {
self.is_replace_open = !self.is_replace_open;
cx.notify();
}
pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.build_view(|cx| Self::new())
}
}
impl Render for BufferSearch {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
h_stack()
.bg(cx.theme().colors().toolbar_background)
.p_2()
.child(
h_stack().child(Input::new("Search")).child(
IconButton::<Self>::new("replace", Icon::Replace)
.when(self.is_replace_open, |this| this.color(IconColor::Accent))
.on_click(|buffer_search, cx| {
buffer_search.toggle_replace(cx);
}),
),
)
}
}

View file

@ -0,0 +1,151 @@
use chrono::NaiveDateTime;
use crate::prelude::*;
use crate::{Icon, IconButton, Input, Label, LabelColor};
#[derive(Component)]
pub struct ChatPanel {
element_id: ElementId,
messages: Vec<ChatMessage>,
}
impl ChatPanel {
pub fn new(element_id: impl Into<ElementId>) -> Self {
Self {
element_id: element_id.into(),
messages: Vec::new(),
}
}
pub fn messages(mut self, messages: Vec<ChatMessage>) -> Self {
self.messages = messages;
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.id(self.element_id.clone())
.flex()
.flex_col()
.justify_between()
.h_full()
.px_2()
.gap_2()
// Header
.child(
div()
.flex()
.justify_between()
.py_2()
.child(div().flex().child(Label::new("#design")))
.child(
div()
.flex()
.items_center()
.gap_px()
.child(IconButton::new("file", Icon::File))
.child(IconButton::new("audio_on", Icon::AudioOn)),
),
)
.child(
div()
.flex()
.flex_col()
// Chat Body
.child(
div()
.id("chat-body")
.w_full()
.flex()
.flex_col()
.gap_3()
.overflow_y_scroll()
.children(self.messages),
)
// Composer
.child(div().flex().my_2().child(Input::new("Message #design"))),
)
}
}
#[derive(Component)]
pub struct ChatMessage {
author: String,
text: String,
sent_at: NaiveDateTime,
}
impl ChatMessage {
pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self {
Self {
author,
text,
sent_at,
}
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.flex()
.flex_col()
.child(
div()
.flex()
.gap_2()
.child(Label::new(self.author.clone()))
.child(
Label::new(self.sent_at.format("%m/%d/%Y").to_string())
.color(LabelColor::Muted),
),
)
.child(div().child(Label::new(self.text.clone())))
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use chrono::DateTime;
use gpui::{Div, Render};
use crate::{Panel, Story};
use super::*;
pub struct ChatPanelStory;
impl Render for ChatPanelStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, ChatPanel>(cx))
.child(Story::label(cx, "Default"))
.child(
Panel::new("chat-panel-1-outer", cx)
.child(ChatPanel::new("chat-panel-1-inner")),
)
.child(Story::label(cx, "With Mesages"))
.child(Panel::new("chat-panel-2-outer", cx).child(
ChatPanel::new("chat-panel-2-inner").messages(vec![
ChatMessage::new(
"osiewicz".to_string(),
"is this thing on?".to_string(),
DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
.unwrap()
.naive_local(),
),
ChatMessage::new(
"maxdeviant".to_string(),
"Reading you loud and clear!".to_string(),
DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
.unwrap()
.naive_local(),
),
]),
))
}
}
}

View file

@ -0,0 +1,109 @@
use crate::{prelude::*, Toggle};
use crate::{
static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List, ListHeader,
};
#[derive(Component)]
pub struct CollabPanel {
id: ElementId,
}
impl CollabPanel {
pub fn new(id: impl Into<ElementId>) -> Self {
Self { id: id.into() }
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
v_stack()
.id(self.id.clone())
.h_full()
.bg(cx.theme().colors().surface_background)
.child(
v_stack()
.id("crdb")
.w_full()
.overflow_y_scroll()
.child(
div()
.pb_1()
.border_color(cx.theme().colors().border)
.border_b()
.child(
List::new(static_collab_panel_current_call())
.header(
ListHeader::new("CRDB")
.left_icon(Icon::Hash.into())
.toggle(Toggle::Toggled(true)),
)
.toggle(Toggle::Toggled(true)),
),
)
.child(
v_stack().id("channels").py_1().child(
List::new(static_collab_panel_channels())
.header(ListHeader::new("CHANNELS").toggle(Toggle::Toggled(true)))
.empty_message("No channels yet. Add a channel to get started.")
.toggle(Toggle::Toggled(true)),
),
)
.child(
v_stack().id("contacts-online").py_1().child(
List::new(static_collab_panel_current_call())
.header(
ListHeader::new("CONTACTS ONLINE")
.toggle(Toggle::Toggled(true)),
)
.toggle(Toggle::Toggled(true)),
),
)
.child(
v_stack().id("contacts-offline").py_1().child(
List::new(static_collab_panel_current_call())
.header(
ListHeader::new("CONTACTS OFFLINE")
.toggle(Toggle::Toggled(false)),
)
.toggle(Toggle::Toggled(false)),
),
),
)
.child(
div()
.h_7()
.px_2()
.border_t()
.border_color(cx.theme().colors().border)
.flex()
.items_center()
.child(
div()
.text_sm()
.text_color(cx.theme().colors().text_placeholder)
.child("Find..."),
),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::Story;
use gpui::{Div, Render};
pub struct CollabPanelStory;
impl Render for CollabPanelStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, CollabPanel>(cx))
.child(Story::label(cx, "Default"))
.child(CollabPanel::new("collab-panel"))
}
}
}

View file

@ -0,0 +1,48 @@
use crate::prelude::*;
use crate::{example_editor_actions, OrderMethod, Palette};
#[derive(Component)]
pub struct CommandPalette {
id: ElementId,
}
impl CommandPalette {
pub fn new(id: impl Into<ElementId>) -> Self {
Self { id: id.into() }
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div().id(self.id.clone()).child(
Palette::new("palette")
.items(example_editor_actions())
.placeholder("Execute a command...")
.empty_string("No items found.")
.default_order(OrderMethod::Ascending),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use gpui::{Div, Render};
use crate::Story;
use super::*;
pub struct CommandPaletteStory;
impl Render for CommandPaletteStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, CommandPalette>(cx))
.child(Story::label(cx, "Default"))
.child(CommandPalette::new("command-palette"))
}
}
}

View file

@ -0,0 +1,46 @@
use crate::{prelude::*, Button, Label, LabelColor, Modal};
#[derive(Component)]
pub struct CopilotModal {
id: ElementId,
}
impl CopilotModal {
pub fn new(id: impl Into<ElementId>) -> Self {
Self { id: id.into() }
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div().id(self.id.clone()).child(
Modal::new("some-id")
.title("Connect Copilot to Zed")
.child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(LabelColor::Muted))
.primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use gpui::{Div, Render};
use crate::Story;
use super::*;
pub struct CopilotModalStory;
impl Render for CopilotModalStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, CopilotModal>(cx))
.child(Story::label(cx, "Default"))
.child(CopilotModal::new("copilot-modal"))
}
}
}

View file

@ -0,0 +1,77 @@
use std::path::PathBuf;
use gpui::{Div, Render, View, VisualContext};
use crate::prelude::*;
use crate::{
hello_world_rust_editor_with_status_example, v_stack, Breadcrumb, Buffer, BufferSearch, Icon,
IconButton, IconColor, Symbol, Tab, TabBar, Toolbar,
};
#[derive(Clone)]
pub struct EditorPane {
tabs: Vec<Tab>,
path: PathBuf,
symbols: Vec<Symbol>,
buffer: Buffer,
buffer_search: View<BufferSearch>,
is_buffer_search_open: bool,
}
impl EditorPane {
pub fn new(
cx: &mut ViewContext<Self>,
tabs: Vec<Tab>,
path: PathBuf,
symbols: Vec<Symbol>,
buffer: Buffer,
) -> Self {
Self {
tabs,
path,
symbols,
buffer,
buffer_search: BufferSearch::view(cx),
is_buffer_search_open: false,
}
}
pub fn toggle_buffer_search(&mut self, cx: &mut ViewContext<Self>) {
self.is_buffer_search_open = !self.is_buffer_search_open;
cx.notify();
}
pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.build_view(|cx| hello_world_rust_editor_with_status_example(cx))
}
}
impl Render for EditorPane {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
v_stack()
.w_full()
.h_full()
.flex_1()
.child(TabBar::new("editor-pane-tabs", self.tabs.clone()).can_navigate((false, true)))
.child(
Toolbar::new()
.left_item(Breadcrumb::new(self.path.clone(), self.symbols.clone()))
.right_items(vec![
IconButton::new("toggle_inlay_hints", Icon::InlayHint),
IconButton::<Self>::new("buffer_search", Icon::MagnifyingGlass)
.when(self.is_buffer_search_open, |this| {
this.color(IconColor::Accent)
})
.on_click(|editor, cx| {
editor.toggle_buffer_search(cx);
}),
IconButton::new("inline_assist", Icon::MagicWand),
]),
)
.children(Some(self.buffer_search.clone()).filter(|_| self.is_buffer_search_open))
.child(self.buffer.clone())
}
}

View file

@ -0,0 +1,57 @@
use crate::prelude::*;
use crate::{OrderMethod, Palette, PaletteItem};
#[derive(Component)]
pub struct LanguageSelector {
id: ElementId,
}
impl LanguageSelector {
pub fn new(id: impl Into<ElementId>) -> Self {
Self { id: id.into() }
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div().id(self.id.clone()).child(
Palette::new("palette")
.items(vec![
PaletteItem::new("C"),
PaletteItem::new("C++"),
PaletteItem::new("CSS"),
PaletteItem::new("Elixir"),
PaletteItem::new("Elm"),
PaletteItem::new("ERB"),
PaletteItem::new("Rust (current)"),
PaletteItem::new("Scheme"),
PaletteItem::new("TOML"),
PaletteItem::new("TypeScript"),
])
.placeholder("Select a language...")
.empty_string("No matches")
.default_order(OrderMethod::Ascending),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::Story;
use gpui::{Div, Render};
pub struct LanguageSelectorStory;
impl Render for LanguageSelectorStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, LanguageSelector>(cx))
.child(Story::label(cx, "Default"))
.child(LanguageSelector::new("language-selector"))
}
}
}

View file

@ -0,0 +1,63 @@
use crate::prelude::*;
use crate::{v_stack, Buffer, Icon, IconButton, Label};
#[derive(Component)]
pub struct MultiBuffer {
buffers: Vec<Buffer>,
}
impl MultiBuffer {
pub fn new(buffers: Vec<Buffer>) -> Self {
Self { buffers }
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
v_stack()
.w_full()
.h_full()
.flex_1()
.children(self.buffers.clone().into_iter().map(|buffer| {
v_stack()
.child(
div()
.flex()
.items_center()
.justify_between()
.p_4()
.bg(cx.theme().colors().editor_subheader_background)
.child(Label::new("main.rs"))
.child(IconButton::new("arrow_up_right", Icon::ArrowUpRight)),
)
.child(buffer)
}))
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::{hello_world_rust_buffer_example, Story};
use gpui::{Div, Render};
pub struct MultiBufferStory;
impl Render for MultiBufferStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, MultiBuffer>(cx))
.child(Story::label(cx, "Default"))
.child(MultiBuffer::new(vec![
hello_world_rust_buffer_example(cx),
hello_world_rust_buffer_example(cx),
hello_world_rust_buffer_example(cx),
hello_world_rust_buffer_example(cx),
hello_world_rust_buffer_example(cx),
]))
}
}
}

View file

@ -0,0 +1,372 @@
use crate::utils::naive_format_distance_from_now;
use crate::{
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};
#[derive(Component)]
pub struct NotificationsPanel {
id: ElementId,
}
impl NotificationsPanel {
pub fn new(id: impl Into<ElementId>) -> Self {
Self { id: id.into() }
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.id(self.id.clone())
.flex()
.flex_col()
.size_full()
.bg(cx.theme().colors().surface_background)
.child(
ListHeader::new("Notifications").meta(Some(ListHeaderMeta::Tools(vec![
Icon::AtSign,
Icon::BellOff,
Icon::MailOpen,
]))),
)
.child(ListSeparator::new())
.child(
v_stack()
.id("notifications-panel-scroll-view")
.py_1()
.overflow_y_scroll()
.flex_1()
.child(
div()
.mx_2()
.p_1()
// TODO: Add cursor style
// .cursor(Cursor::IBeam)
.bg(cx.theme().colors().element_background)
.border()
.border_color(cx.theme().colors().border_variant)
.child(
Label::new("Search...")
.color(LabelColor::Placeholder)
.line_height_style(LineHeightStyle::UILabel),
),
)
.child(v_stack().px_1().children(static_new_notification_items_2())),
)
}
}
pub struct NotificationAction<V: 'static> {
button: ButtonOrIconButton<V>,
tooltip: SharedString,
/// Shows after action is chosen
///
/// For example, if the action is "Accept" the taken message could be:
///
/// - `(None,"Accepted")` - "Accepted"
///
/// - `(Some(Icon::Check),"Accepted")` - ✓ "Accepted"
taken_message: (Option<Icon>, SharedString),
}
impl<V: 'static> NotificationAction<V> {
pub fn new(
button: impl Into<ButtonOrIconButton<V>>,
tooltip: impl Into<SharedString>,
(icon, taken_message): (Option<Icon>, impl Into<SharedString>),
) -> Self {
Self {
button: button.into(),
tooltip: tooltip.into(),
taken_message: (icon, taken_message.into()),
}
}
}
pub enum ActorOrIcon {
Actor(PublicPlayer),
Icon(Icon),
}
pub struct NotificationMeta<V: 'static> {
items: Vec<(Option<Icon>, SharedString, Option<ClickHandler<V>>)>,
}
struct NotificationHandlers<V: 'static> {
click: Option<ClickHandler<V>>,
}
impl<V: 'static> Default for NotificationHandlers<V> {
fn default() -> Self {
Self { click: None }
}
}
#[derive(Component)]
pub struct Notification<V: 'static> {
id: ElementId,
slot: ActorOrIcon,
message: SharedString,
date_received: NaiveDateTime,
meta: Option<NotificationMeta<V>>,
actions: Option<[NotificationAction<V>; 2]>,
unread: bool,
new: bool,
action_taken: Option<NotificationAction<V>>,
handlers: NotificationHandlers<V>,
}
impl<V> Notification<V> {
fn new(
id: ElementId,
message: SharedString,
date_received: NaiveDateTime,
slot: ActorOrIcon,
click_action: Option<ClickHandler<V>>,
) -> Self {
let handlers = if click_action.is_some() {
NotificationHandlers {
click: click_action,
}
} else {
NotificationHandlers::default()
};
Self {
id,
date_received,
message,
meta: None,
slot,
actions: None,
unread: true,
new: false,
action_taken: None,
handlers,
}
}
/// Creates a new notification with an actor slot.
///
/// Requires a click action.
pub fn new_actor_message(
id: impl Into<ElementId>,
message: impl Into<SharedString>,
date_received: NaiveDateTime,
actor: PublicPlayer,
click_action: ClickHandler<V>,
) -> Self {
Self::new(
id.into(),
message.into(),
date_received,
ActorOrIcon::Actor(actor),
Some(click_action),
)
}
/// Creates a new notification with an icon slot.
///
/// Requires a click action.
pub fn new_icon_message(
id: impl Into<ElementId>,
message: impl Into<SharedString>,
date_received: NaiveDateTime,
icon: Icon,
click_action: ClickHandler<V>,
) -> Self {
Self::new(
id.into(),
message.into(),
date_received,
ActorOrIcon::Icon(icon),
Some(click_action),
)
}
/// Creates a new notification with an actor slot
/// and a Call To Action row.
///
/// Cannot take a click action due to required actions.
pub fn new_actor_with_actions(
id: impl Into<ElementId>,
message: impl Into<SharedString>,
date_received: NaiveDateTime,
actor: PublicPlayer,
actions: [NotificationAction<V>; 2],
) -> Self {
Self::new(
id.into(),
message.into(),
date_received,
ActorOrIcon::Actor(actor),
None,
)
.actions(actions)
}
/// Creates a new notification with an icon slot
/// and a Call To Action row.
///
/// Cannot take a click action due to required actions.
pub fn new_icon_with_actions(
id: impl Into<ElementId>,
message: impl Into<SharedString>,
date_received: NaiveDateTime,
icon: Icon,
actions: [NotificationAction<V>; 2],
) -> Self {
Self::new(
id.into(),
message.into(),
date_received,
ActorOrIcon::Icon(icon),
None,
)
.actions(actions)
}
fn on_click(mut self, handler: ClickHandler<V>) -> Self {
self.handlers.click = Some(handler);
self
}
pub fn actions(mut self, actions: [NotificationAction<V>; 2]) -> Self {
self.actions = Some(actions);
self
}
pub fn meta(mut self, meta: NotificationMeta<V>) -> Self {
self.meta = Some(meta);
self
}
fn render_meta_items(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
if let Some(meta) = &self.meta {
h_stack().children(
meta.items
.iter()
.map(|(icon, text, _)| {
let mut meta_el = div();
if let Some(icon) = icon {
meta_el = meta_el.child(IconElement::new(icon.clone()));
}
meta_el.child(Label::new(text.clone()).color(LabelColor::Muted))
})
.collect::<Vec<_>>(),
)
} else {
div()
}
}
fn render_slot(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
match &self.slot {
ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(),
ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(),
}
}
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.relative()
.id(self.id.clone())
.p_1()
.flex()
.flex_col()
.w_full()
.children(
Some(
div()
.absolute()
.left(px(3.0))
.top_3()
.z_index(2)
.child(UnreadIndicator::new()),
)
.filter(|_| self.unread),
)
.child(
v_stack()
.z_index(1)
.gap_1()
.w_full()
.child(
h_stack()
.w_full()
.gap_2()
.child(self.render_slot(cx))
.child(div().flex_1().child(Label::new(self.message.clone()))),
)
.child(
h_stack()
.justify_between()
.child(
h_stack()
.gap_1()
.child(
Label::new(naive_format_distance_from_now(
self.date_received,
true,
true,
))
.color(LabelColor::Muted),
)
.child(self.render_meta_items(cx)),
)
.child(match (self.actions, self.action_taken) {
// Show nothing
(None, _) => div(),
// Show the taken_message
(Some(_), Some(action_taken)) => h_stack()
.children(action_taken.taken_message.0.map(|icon| {
IconElement::new(icon).color(crate::IconColor::Muted)
}))
.child(
Label::new(action_taken.taken_message.1.clone())
.color(LabelColor::Muted),
),
// Show the actions
(Some(actions), None) => {
h_stack().children(actions.map(|action| match action.button {
ButtonOrIconButton::Button(button) => {
Component::render(button)
}
ButtonOrIconButton::IconButton(icon_button) => {
Component::render(icon_button)
}
}))
}
}),
),
)
}
}
use chrono::NaiveDateTime;
use gpui::{px, Styled};
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::{Panel, Story};
use gpui::{Div, Render};
pub struct NotificationsPanelStory;
impl Render for NotificationsPanelStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, NotificationsPanel>(cx))
.child(Story::label(cx, "Default"))
.child(
Panel::new("panel", cx).child(NotificationsPanel::new("notifications_panel")),
)
}
}
}

View file

@ -0,0 +1,128 @@
use gpui::{hsla, red, AnyElement, ElementId, ExternalPaths, Hsla, Length, Size, View};
use smallvec::SmallVec;
use crate::prelude::*;
#[derive(Default, PartialEq)]
pub enum SplitDirection {
#[default]
Horizontal,
Vertical,
}
#[derive(Component)]
pub struct Pane<V: 'static> {
id: ElementId,
size: Size<Length>,
fill: Hsla,
children: SmallVec<[AnyElement<V>; 2]>,
}
impl<V: 'static> Pane<V> {
pub fn new(id: impl Into<ElementId>, size: Size<Length>) -> Self {
// Fill is only here for debugging purposes, remove before release
Self {
id: id.into(),
size,
fill: hsla(0.3, 0.3, 0.3, 1.),
children: SmallVec::new(),
}
}
pub fn fill(mut self, fill: Hsla) -> Self {
self.fill = fill;
self
}
fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.id(self.id.clone())
.flex()
.flex_initial()
.bg(self.fill)
.w(self.size.width)
.h(self.size.height)
.relative()
.child(div().z_index(0).size_full().children(self.children))
.child(
div()
.z_index(1)
.id("drag-target")
.drag_over::<ExternalPaths>(|d| d.bg(red()))
.on_drop(|_, files: View<ExternalPaths>, cx| {
eprintln!("dropped files! {:?}", files.read(cx));
})
.absolute()
.inset_0(),
)
}
}
impl<V: 'static> ParentElement<V> for Pane<V> {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
&mut self.children
}
}
#[derive(Component)]
pub struct PaneGroup<V: 'static> {
groups: Vec<PaneGroup<V>>,
panes: Vec<Pane<V>>,
split_direction: SplitDirection,
}
impl<V: 'static> PaneGroup<V> {
pub fn new_groups(groups: Vec<PaneGroup<V>>, split_direction: SplitDirection) -> Self {
Self {
groups,
panes: Vec::new(),
split_direction,
}
}
pub fn new_panes(panes: Vec<Pane<V>>, split_direction: SplitDirection) -> Self {
Self {
groups: Vec::new(),
panes,
split_direction,
}
}
fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
if !self.panes.is_empty() {
let el = div()
.flex()
.flex_1()
.gap_px()
.w_full()
.h_full()
.children(self.panes.into_iter().map(|pane| pane.render(view, cx)));
if self.split_direction == SplitDirection::Horizontal {
return el;
} else {
return el.flex_col();
}
}
if !self.groups.is_empty() {
let el = div()
.flex()
.flex_1()
.gap_px()
.w_full()
.h_full()
.bg(cx.theme().colors().editor_background)
.children(self.groups.into_iter().map(|group| group.render(view, cx)));
if self.split_direction == SplitDirection::Horizontal {
return el;
} else {
return el.flex_col();
}
}
unreachable!()
}
}

View file

@ -0,0 +1,74 @@
use crate::prelude::*;
use crate::{
static_project_panel_project_items, static_project_panel_single_items, Input, List, ListHeader,
};
#[derive(Component)]
pub struct ProjectPanel {
id: ElementId,
}
impl ProjectPanel {
pub fn new(id: impl Into<ElementId>) -> Self {
Self { id: id.into() }
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.id(self.id.clone())
.flex()
.flex_col()
.size_full()
.bg(cx.theme().colors().surface_background)
.child(
div()
.id("project-panel-contents")
.w_full()
.flex()
.flex_col()
.overflow_y_scroll()
.child(
List::new(static_project_panel_single_items())
.header(ListHeader::new("FILES"))
.empty_message("No files in directory"),
)
.child(
List::new(static_project_panel_project_items())
.header(ListHeader::new("PROJECT"))
.empty_message("No folders in directory"),
),
)
.child(
Input::new("Find something...")
.value("buffe".to_string())
.state(InteractionState::Focused),
)
}
}
use gpui::ElementId;
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::{Panel, Story};
use gpui::{Div, Render};
pub struct ProjectPanelStory;
impl Render for ProjectPanelStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, ProjectPanel>(cx))
.child(Story::label(cx, "Default"))
.child(
Panel::new("project-panel-outer", cx)
.child(ProjectPanel::new("project-panel-inner")),
)
}
}
}

View file

@ -0,0 +1,53 @@
use crate::prelude::*;
use crate::{OrderMethod, Palette, PaletteItem};
#[derive(Component)]
pub struct RecentProjects {
id: ElementId,
}
impl RecentProjects {
pub fn new(id: impl Into<ElementId>) -> Self {
Self { id: id.into() }
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div().id(self.id.clone()).child(
Palette::new("palette")
.items(vec![
PaletteItem::new("zed").sublabel(SharedString::from("~/projects/zed")),
PaletteItem::new("saga").sublabel(SharedString::from("~/projects/saga")),
PaletteItem::new("journal").sublabel(SharedString::from("~/journal")),
PaletteItem::new("dotfiles").sublabel(SharedString::from("~/dotfiles")),
PaletteItem::new("zed.dev").sublabel(SharedString::from("~/projects/zed.dev")),
PaletteItem::new("laminar").sublabel(SharedString::from("~/projects/laminar")),
])
.placeholder("Recent Projects...")
.empty_string("No matches")
.default_order(OrderMethod::Ascending),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::Story;
use gpui::{Div, Render};
pub struct RecentProjectsStory;
impl Render for RecentProjectsStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, RecentProjects>(cx))
.child(Story::label(cx, "Default"))
.child(RecentProjects::new("recent-projects"))
}
}
}

View file

@ -0,0 +1,203 @@
use std::sync::Arc;
use crate::prelude::*;
use crate::{Button, Icon, IconButton, IconColor, ToolDivider, Workspace};
#[derive(Default, PartialEq)]
pub enum Tool {
#[default]
ProjectPanel,
CollaborationPanel,
Terminal,
Assistant,
Feedback,
Diagnostics,
}
struct ToolGroup {
active_index: Option<usize>,
tools: Vec<Tool>,
}
impl Default for ToolGroup {
fn default() -> Self {
ToolGroup {
active_index: None,
tools: vec![],
}
}
}
#[derive(Component)]
#[component(view_type = "Workspace")]
pub struct StatusBar {
left_tools: Option<ToolGroup>,
right_tools: Option<ToolGroup>,
bottom_tools: Option<ToolGroup>,
}
impl StatusBar {
pub fn new() -> Self {
Self {
left_tools: None,
right_tools: None,
bottom_tools: None,
}
}
pub fn left_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
self.left_tools = {
let mut tools = vec![tool];
tools.extend(self.left_tools.take().unwrap_or_default().tools);
Some(ToolGroup {
active_index,
tools,
})
};
self
}
pub fn right_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
self.right_tools = {
let mut tools = vec![tool];
tools.extend(self.left_tools.take().unwrap_or_default().tools);
Some(ToolGroup {
active_index,
tools,
})
};
self
}
pub fn bottom_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
self.bottom_tools = {
let mut tools = vec![tool];
tools.extend(self.left_tools.take().unwrap_or_default().tools);
Some(ToolGroup {
active_index,
tools,
})
};
self
}
fn render(
self,
view: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) -> impl Component<Workspace> {
div()
.py_0p5()
.px_1()
.flex()
.items_center()
.justify_between()
.w_full()
.bg(cx.theme().colors().status_bar_background)
.child(self.left_tools(view, cx))
.child(self.right_tools(view, cx))
}
fn left_tools(
&self,
workspace: &mut Workspace,
cx: &WindowContext,
) -> impl Component<Workspace> {
div()
.flex()
.items_center()
.gap_1()
.child(
IconButton::<Workspace>::new("project_panel", Icon::FileTree)
.when(workspace.is_project_panel_open(), |this| {
this.color(IconColor::Accent)
})
.on_click(|workspace, cx| {
workspace.toggle_project_panel(cx);
}),
)
.child(
IconButton::<Workspace>::new("collab_panel", Icon::Hash)
.when(workspace.is_collab_panel_open(), |this| {
this.color(IconColor::Accent)
})
.on_click(|workspace, cx| {
workspace.toggle_collab_panel();
}),
)
.child(ToolDivider::new())
.child(IconButton::new("diagnostics", Icon::XCircle))
}
fn right_tools(
&self,
workspace: &mut Workspace,
cx: &WindowContext,
) -> impl Component<Workspace> {
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.flex()
.items_center()
.gap_1()
.child(Button::new("116:25"))
.child(
Button::<Workspace>::new("Rust").on_click(Arc::new(|workspace, cx| {
workspace.toggle_language_selector(cx);
})),
),
)
.child(ToolDivider::new())
.child(
div()
.flex()
.items_center()
.gap_1()
.child(
IconButton::new("copilot", Icon::Copilot)
.on_click(|_, _| println!("Copilot clicked.")),
)
.child(
IconButton::new("envelope", Icon::Envelope)
.on_click(|_, _| println!("Send Feedback clicked.")),
),
)
.child(ToolDivider::new())
.child(
div()
.flex()
.items_center()
.gap_1()
.child(
IconButton::<Workspace>::new("terminal", Icon::Terminal)
.when(workspace.is_terminal_open(), |this| {
this.color(IconColor::Accent)
})
.on_click(|workspace, cx| {
workspace.toggle_terminal(cx);
}),
)
.child(
IconButton::<Workspace>::new("chat_panel", Icon::MessageBubbles)
.when(workspace.is_chat_panel_open(), |this| {
this.color(IconColor::Accent)
})
.on_click(|workspace, cx| {
workspace.toggle_chat_panel(cx);
}),
)
.child(
IconButton::<Workspace>::new("assistant_panel", Icon::Ai)
.when(workspace.is_assistant_panel_open(), |this| {
this.color(IconColor::Accent)
})
.on_click(|workspace, cx| {
workspace.toggle_assistant_panel(cx);
}),
),
)
}
}

View file

@ -0,0 +1,150 @@
use crate::prelude::*;
use crate::{Icon, IconButton, Tab};
#[derive(Component)]
pub struct TabBar {
id: ElementId,
/// Backwards, Forwards
can_navigate: (bool, bool),
tabs: Vec<Tab>,
}
impl TabBar {
pub fn new(id: impl Into<ElementId>, tabs: Vec<Tab>) -> Self {
Self {
id: id.into(),
can_navigate: (false, false),
tabs,
}
}
pub fn can_navigate(mut self, can_navigate: (bool, bool)) -> Self {
self.can_navigate = can_navigate;
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let (can_navigate_back, can_navigate_forward) = self.can_navigate;
div()
.group("tab_bar")
.id(self.id.clone())
.w_full()
.flex()
.bg(cx.theme().colors().tab_bar_background)
// Left Side
.child(
div()
.relative()
.px_1()
.flex()
.flex_none()
.gap_2()
// Nav Buttons
.child(
div()
.right_0()
.flex()
.items_center()
.gap_px()
.child(
IconButton::new("arrow_left", Icon::ArrowLeft)
.state(InteractionState::Enabled.if_enabled(can_navigate_back)),
)
.child(
IconButton::new("arrow_right", Icon::ArrowRight).state(
InteractionState::Enabled.if_enabled(can_navigate_forward),
),
),
),
)
.child(
div().w_0().flex_1().h_full().child(
div()
.id("tabs")
.flex()
.overflow_x_scroll()
.children(self.tabs.clone()),
),
)
// Right Side
.child(
div()
// We only use absolute here since we don't
// have opacity or `hidden()` yet
.absolute()
.neg_top_7()
.px_1()
.flex()
.flex_none()
.gap_2()
.group_hover("tab_bar", |this| this.top_0())
// Nav Buttons
.child(
div()
.flex()
.items_center()
.gap_px()
.child(IconButton::new("plus", Icon::Plus))
.child(IconButton::new("split", Icon::Split)),
),
)
}
}
use gpui::ElementId;
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::Story;
use gpui::{Div, Render};
pub struct TabBarStory;
impl Render for TabBarStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, TabBar>(cx))
.child(Story::label(cx, "Default"))
.child(TabBar::new(
"tab-bar",
vec![
Tab::new(1)
.title("Cargo.toml".to_string())
.current(false)
.git_status(GitStatus::Modified),
Tab::new(2)
.title("Channels Panel".to_string())
.current(false),
Tab::new(3)
.title("channels_panel.rs".to_string())
.current(true)
.git_status(GitStatus::Modified),
Tab::new(4)
.title("workspace.rs".to_string())
.current(false)
.git_status(GitStatus::Modified),
Tab::new(5)
.title("icon_button.rs".to_string())
.current(false),
Tab::new(6)
.title("storybook.rs".to_string())
.current(false)
.git_status(GitStatus::Created),
Tab::new(7).title("theme.rs".to_string()).current(false),
Tab::new(8)
.title("theme_registry.rs".to_string())
.current(false),
Tab::new(9)
.title("styleable_helpers.rs".to_string())
.current(false),
],
))
}
}
}

View file

@ -0,0 +1,99 @@
use gpui::{relative, rems, Size};
use crate::prelude::*;
use crate::{Icon, IconButton, Pane, Tab};
#[derive(Component)]
pub struct Terminal;
impl Terminal {
pub fn new() -> Self {
Self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let can_navigate_back = true;
let can_navigate_forward = false;
div()
.flex()
.flex_col()
.w_full()
.child(
// Terminal Tabs.
div()
.w_full()
.flex()
.bg(cx.theme().colors().surface_background)
.child(
div().px_1().flex().flex_none().gap_2().child(
div()
.flex()
.items_center()
.gap_px()
.child(
IconButton::new("arrow_left", Icon::ArrowLeft).state(
InteractionState::Enabled.if_enabled(can_navigate_back),
),
)
.child(IconButton::new("arrow_right", Icon::ArrowRight).state(
InteractionState::Enabled.if_enabled(can_navigate_forward),
)),
),
)
.child(
div().w_0().flex_1().h_full().child(
div()
.flex()
.child(
Tab::new(1)
.title("zed — fish".to_string())
.icon(Icon::Terminal)
.close_side(IconSide::Right)
.current(true),
)
.child(
Tab::new(2)
.title("zed — fish".to_string())
.icon(Icon::Terminal)
.close_side(IconSide::Right)
.current(false),
),
),
),
)
// Terminal Pane.
.child(
Pane::new(
"terminal",
Size {
width: relative(1.).into(),
height: rems(36.).into(),
},
)
.child(crate::static_data::terminal_buffer(cx)),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::Story;
use gpui::{Div, Render};
pub struct TerminalStory;
impl Render for TerminalStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Terminal>(cx))
.child(Story::label(cx, "Default"))
.child(Terminal::new())
}
}
}

View file

@ -0,0 +1,60 @@
use crate::prelude::*;
use crate::{OrderMethod, Palette, PaletteItem};
#[derive(Component)]
pub struct ThemeSelector {
id: ElementId,
}
impl ThemeSelector {
pub fn new(id: impl Into<ElementId>) -> Self {
Self { id: id.into() }
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div().child(
Palette::new(self.id.clone())
.items(vec![
PaletteItem::new("One Dark"),
PaletteItem::new("Rosé Pine"),
PaletteItem::new("Rosé Pine Moon"),
PaletteItem::new("Sandcastle"),
PaletteItem::new("Solarized Dark"),
PaletteItem::new("Summercamp"),
PaletteItem::new("Atelier Cave Light"),
PaletteItem::new("Atelier Dune Light"),
PaletteItem::new("Atelier Estuary Light"),
PaletteItem::new("Atelier Forest Light"),
PaletteItem::new("Atelier Heath Light"),
])
.placeholder("Select Theme...")
.empty_string("No matches")
.default_order(OrderMethod::Ascending),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use gpui::{Div, Render};
use crate::Story;
use super::*;
pub struct ThemeSelectorStory;
impl Render for ThemeSelectorStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, ThemeSelector>(cx))
.child(Story::label(cx, "Default"))
.child(ThemeSelector::new("theme-selector"))
}
}
}

View file

@ -0,0 +1,214 @@
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use gpui::{Div, Render, View, VisualContext};
use crate::prelude::*;
use crate::settings::user_settings;
use crate::{
Avatar, Button, Icon, IconButton, IconColor, MicStatus, PlayerStack, PlayerWithCallStatus,
ScreenShareStatus, ToolDivider, TrafficLights,
};
#[derive(Clone)]
pub struct Livestream {
pub players: Vec<PlayerWithCallStatus>,
pub channel: Option<String>, // projects
// windows
}
#[derive(Clone)]
pub struct TitleBar {
/// If the window is active from the OS's perspective.
is_active: Arc<AtomicBool>,
livestream: Option<Livestream>,
mic_status: MicStatus,
is_deafened: bool,
screen_share_status: ScreenShareStatus,
}
impl TitleBar {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let is_active = Arc::new(AtomicBool::new(true));
let active = is_active.clone();
// cx.observe_window_activation(move |_, is_active, cx| {
// active.store(is_active, std::sync::atomic::Ordering::SeqCst);
// cx.notify();
// })
// .detach();
Self {
is_active,
livestream: None,
mic_status: MicStatus::Unmuted,
is_deafened: false,
screen_share_status: ScreenShareStatus::NotShared,
}
}
pub fn set_livestream(mut self, livestream: Option<Livestream>) -> Self {
self.livestream = livestream;
self
}
pub fn is_mic_muted(&self) -> bool {
self.mic_status == MicStatus::Muted
}
pub fn toggle_mic_status(&mut self, cx: &mut ViewContext<Self>) {
self.mic_status = self.mic_status.inverse();
// Undeafen yourself when unmuting the mic while deafened.
if self.is_deafened && self.mic_status == MicStatus::Unmuted {
self.is_deafened = false;
}
cx.notify();
}
pub fn toggle_deafened(&mut self, cx: &mut ViewContext<Self>) {
self.is_deafened = !self.is_deafened;
self.mic_status = MicStatus::Muted;
cx.notify()
}
pub fn toggle_screen_share_status(&mut self, cx: &mut ViewContext<Self>) {
self.screen_share_status = self.screen_share_status.inverse();
cx.notify();
}
pub fn view(cx: &mut WindowContext, livestream: Option<Livestream>) -> View<Self> {
cx.build_view(|cx| Self::new(cx).set_livestream(livestream))
}
}
impl Render for TitleBar {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
let settings = user_settings(cx);
// let has_focus = cx.window_is_active();
let has_focus = true;
let player_list = if let Some(livestream) = &self.livestream {
livestream.players.clone().into_iter()
} else {
vec![].into_iter()
};
div()
.flex()
.items_center()
.justify_between()
.w_full()
.bg(cx.theme().colors().background)
.py_1()
.child(
div()
.flex()
.items_center()
.h_full()
.gap_4()
.px_2()
.child(TrafficLights::new().window_has_focus(has_focus))
// === Project Info === //
.child(
div()
.flex()
.items_center()
.gap_1()
.when(*settings.titlebar.show_project_owner, |this| {
this.child(Button::new("iamnbutler"))
})
.child(Button::new("zed"))
.child(Button::new("nate/gpui2-ui-components")),
)
.children(player_list.map(|p| PlayerStack::new(p)))
.child(IconButton::new("plus", Icon::Plus)),
)
.child(
div()
.flex()
.items_center()
.child(
div()
.px_2()
.flex()
.items_center()
.gap_1()
.child(IconButton::new("folder_x", Icon::FolderX))
.child(IconButton::new("exit", Icon::Exit)),
)
.child(ToolDivider::new())
.child(
div()
.px_2()
.flex()
.items_center()
.gap_1()
.child(
IconButton::<TitleBar>::new("toggle_mic_status", Icon::Mic)
.when(self.is_mic_muted(), |this| this.color(IconColor::Error))
.on_click(|title_bar, cx| title_bar.toggle_mic_status(cx)),
)
.child(
IconButton::<TitleBar>::new("toggle_deafened", Icon::AudioOn)
.when(self.is_deafened, |this| this.color(IconColor::Error))
.on_click(|title_bar, cx| title_bar.toggle_deafened(cx)),
)
.child(
IconButton::<TitleBar>::new("toggle_screen_share", Icon::Screen)
.when(
self.screen_share_status == ScreenShareStatus::Shared,
|this| this.color(IconColor::Accent),
)
.on_click(|title_bar, cx| {
title_bar.toggle_screen_share_status(cx)
}),
),
)
.child(
div().px_2().flex().items_center().child(
Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
.shape(Shape::RoundedRectangle),
),
),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use crate::Story;
pub struct TitleBarStory {
title_bar: View<TitleBar>,
}
impl TitleBarStory {
pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.build_view(|cx| Self {
title_bar: TitleBar::view(cx, None),
})
}
}
impl Render for TitleBarStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
Story::container(cx)
.child(Story::title_for::<_, TitleBar>(cx))
.child(Story::label(cx, "Default"))
.child(self.title_bar.clone())
}
}
}

View file

@ -0,0 +1,126 @@
use gpui::AnyElement;
use smallvec::SmallVec;
use crate::prelude::*;
#[derive(Clone)]
pub struct ToolbarItem {}
#[derive(Component)]
pub struct Toolbar<V: 'static> {
left_items: SmallVec<[AnyElement<V>; 2]>,
right_items: SmallVec<[AnyElement<V>; 2]>,
}
impl<V: 'static> Toolbar<V> {
pub fn new() -> Self {
Self {
left_items: SmallVec::new(),
right_items: SmallVec::new(),
}
}
pub fn left_item(mut self, child: impl Component<V>) -> Self
where
Self: Sized,
{
self.left_items.push(child.render());
self
}
pub fn left_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
where
Self: Sized,
{
self.left_items
.extend(iter.into_iter().map(|item| item.render()));
self
}
pub fn right_item(mut self, child: impl Component<V>) -> Self
where
Self: Sized,
{
self.right_items.push(child.render());
self
}
pub fn right_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
where
Self: Sized,
{
self.right_items
.extend(iter.into_iter().map(|item| item.render()));
self
}
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.bg(cx.theme().colors().toolbar_background)
.p_2()
.flex()
.justify_between()
.child(div().flex().children(self.left_items))
.child(div().flex().children(self.right_items))
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use std::path::PathBuf;
use std::str::FromStr;
use gpui::{Div, Render};
use crate::{Breadcrumb, HighlightedText, Icon, IconButton, Story, Symbol};
use super::*;
pub struct ToolbarStory;
impl Render for ToolbarStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, Toolbar<Self>>(cx))
.child(Story::label(cx, "Default"))
.child(
Toolbar::new()
.left_item(Breadcrumb::new(
PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
vec![
Symbol(vec![
HighlightedText {
text: "impl ".to_string(),
color: cx.theme().syntax_color("keyword"),
},
HighlightedText {
text: "ToolbarStory".to_string(),
color: cx.theme().syntax_color("function"),
},
]),
Symbol(vec![
HighlightedText {
text: "fn ".to_string(),
color: cx.theme().syntax_color("keyword"),
},
HighlightedText {
text: "render".to_string(),
color: cx.theme().syntax_color("function"),
},
]),
],
))
.right_items(vec![
IconButton::new("toggle_inlay_hints", Icon::InlayHint),
IconButton::new("buffer_search", Icon::MagnifyingGlass),
IconButton::new("inline_assist", Icon::MagicWand),
]),
)
}
}
}

View file

@ -0,0 +1,100 @@
use crate::prelude::*;
#[derive(Clone, Copy)]
enum TrafficLightColor {
Red,
Yellow,
Green,
}
#[derive(Component)]
struct TrafficLight {
color: TrafficLightColor,
window_has_focus: bool,
}
impl TrafficLight {
fn new(color: TrafficLightColor, window_has_focus: bool) -> Self {
Self {
color,
window_has_focus,
}
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let system_colors = &cx.theme().styles.system;
let fill = match (self.window_has_focus, self.color) {
(true, TrafficLightColor::Red) => system_colors.mac_os_traffic_light_red,
(true, TrafficLightColor::Yellow) => system_colors.mac_os_traffic_light_yellow,
(true, TrafficLightColor::Green) => system_colors.mac_os_traffic_light_green,
(false, _) => cx.theme().colors().element_background,
};
div().w_3().h_3().rounded_full().bg(fill)
}
}
#[derive(Component)]
pub struct TrafficLights {
window_has_focus: bool,
}
impl TrafficLights {
pub fn new() -> Self {
Self {
window_has_focus: true,
}
}
pub fn window_has_focus(mut self, window_has_focus: bool) -> Self {
self.window_has_focus = window_has_focus;
self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.flex()
.items_center()
.gap_2()
.child(TrafficLight::new(
TrafficLightColor::Red,
self.window_has_focus,
))
.child(TrafficLight::new(
TrafficLightColor::Yellow,
self.window_has_focus,
))
.child(TrafficLight::new(
TrafficLightColor::Green,
self.window_has_focus,
))
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use gpui::{Div, Render};
use crate::Story;
use super::*;
pub struct TrafficLightsStory;
impl Render for TrafficLightsStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx)
.child(Story::title_for::<_, TrafficLights>(cx))
.child(Story::label(cx, "Default"))
.child(TrafficLights::new())
.child(Story::label(cx, "Unfocused"))
.child(TrafficLights::new().window_has_focus(false))
}
}
}

View file

@ -0,0 +1,397 @@
use std::sync::Arc;
use chrono::DateTime;
use gpui::{px, relative, Div, Render, Size, View, VisualContext};
use settings2::Settings;
use theme2::ThemeSettings;
use crate::prelude::*;
use crate::{
static_livestream, v_stack, AssistantPanel, Button, ChatMessage, ChatPanel, Checkbox,
CollabPanel, EditorPane, Label, LanguageSelector, NotificationsPanel, Pane, PaneGroup, Panel,
PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
Toast, ToastOrigin,
};
#[derive(Clone)]
pub struct Gpui2UiDebug {
pub in_livestream: bool,
pub enable_user_settings: bool,
pub show_toast: bool,
}
impl Default for Gpui2UiDebug {
fn default() -> Self {
Self {
in_livestream: false,
enable_user_settings: false,
show_toast: false,
}
}
}
#[derive(Clone)]
pub struct Workspace {
title_bar: View<TitleBar>,
editor_1: View<EditorPane>,
show_project_panel: bool,
show_collab_panel: bool,
show_chat_panel: bool,
show_assistant_panel: bool,
show_notifications_panel: bool,
show_terminal: bool,
show_debug: bool,
show_language_selector: bool,
test_checkbox_selection: Selection,
debug: Gpui2UiDebug,
}
impl Workspace {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
title_bar: TitleBar::view(cx, None),
editor_1: EditorPane::view(cx),
show_project_panel: true,
show_collab_panel: false,
show_chat_panel: false,
show_assistant_panel: false,
show_terminal: true,
show_language_selector: false,
show_debug: false,
show_notifications_panel: true,
test_checkbox_selection: Selection::Unselected,
debug: Gpui2UiDebug::default(),
}
}
pub fn is_project_panel_open(&self) -> bool {
self.show_project_panel
}
pub fn toggle_project_panel(&mut self, cx: &mut ViewContext<Self>) {
self.show_project_panel = !self.show_project_panel;
self.show_collab_panel = false;
cx.notify();
}
pub fn is_collab_panel_open(&self) -> bool {
self.show_collab_panel
}
pub fn toggle_collab_panel(&mut self) {
self.show_collab_panel = !self.show_collab_panel;
self.show_project_panel = false;
}
pub fn is_terminal_open(&self) -> bool {
self.show_terminal
}
pub fn toggle_terminal(&mut self, cx: &mut ViewContext<Self>) {
self.show_terminal = !self.show_terminal;
cx.notify();
}
pub fn is_chat_panel_open(&self) -> bool {
self.show_chat_panel
}
pub fn toggle_chat_panel(&mut self, cx: &mut ViewContext<Self>) {
self.show_chat_panel = !self.show_chat_panel;
self.show_assistant_panel = false;
self.show_notifications_panel = false;
cx.notify();
}
pub fn is_notifications_panel_open(&self) -> bool {
self.show_notifications_panel
}
pub fn toggle_notifications_panel(&mut self, cx: &mut ViewContext<Self>) {
self.show_notifications_panel = !self.show_notifications_panel;
self.show_chat_panel = false;
self.show_assistant_panel = false;
cx.notify();
}
pub fn is_assistant_panel_open(&self) -> bool {
self.show_assistant_panel
}
pub fn toggle_assistant_panel(&mut self, cx: &mut ViewContext<Self>) {
self.show_assistant_panel = !self.show_assistant_panel;
self.show_chat_panel = false;
self.show_notifications_panel = false;
cx.notify();
}
pub fn is_language_selector_open(&self) -> bool {
self.show_language_selector
}
pub fn toggle_language_selector(&mut self, cx: &mut ViewContext<Self>) {
self.show_language_selector = !self.show_language_selector;
cx.notify();
}
pub fn toggle_debug(&mut self, cx: &mut ViewContext<Self>) {
self.show_debug = !self.show_debug;
cx.notify();
}
pub fn debug_toggle_user_settings(&mut self, cx: &mut ViewContext<Self>) {
self.debug.enable_user_settings = !self.debug.enable_user_settings;
let mut theme_settings = ThemeSettings::get_global(cx).clone();
if self.debug.enable_user_settings {
theme_settings.ui_font_size = 18.0.into();
} else {
theme_settings.ui_font_size = 16.0.into();
}
ThemeSettings::override_global(theme_settings.clone(), cx);
cx.set_rem_size(theme_settings.ui_font_size);
cx.notify();
}
pub fn debug_toggle_livestream(&mut self, cx: &mut ViewContext<Self>) {
self.debug.in_livestream = !self.debug.in_livestream;
self.title_bar = TitleBar::view(
cx,
Some(static_livestream()).filter(|_| self.debug.in_livestream),
);
cx.notify();
}
pub fn debug_toggle_toast(&mut self, cx: &mut ViewContext<Self>) {
self.debug.show_toast = !self.debug.show_toast;
cx.notify();
}
pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.build_view(|cx| Self::new(cx))
}
}
impl Render for Workspace {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
let root_group = PaneGroup::new_panes(
vec![Pane::new(
"pane-0",
Size {
width: relative(1.).into(),
height: relative(1.).into(),
},
)
.child(self.editor_1.clone())],
SplitDirection::Horizontal,
);
div()
.relative()
.size_full()
.flex()
.flex_col()
.font("Zed Sans")
.gap_0()
.justify_start()
.items_start()
.text_color(cx.theme().colors().text)
.bg(cx.theme().colors().background)
.child(self.title_bar.clone())
.child(
div()
.absolute()
.top_12()
.left_12()
.z_index(99)
.bg(cx.theme().colors().background)
.child(
Checkbox::new("test_checkbox", self.test_checkbox_selection).on_click(
|selection, workspace: &mut Workspace, cx| {
workspace.test_checkbox_selection = selection;
cx.notify();
},
),
),
)
.child(
div()
.flex_1()
.w_full()
.flex()
.flex_row()
.overflow_hidden()
.border_t()
.border_b()
.border_color(cx.theme().colors().border)
.children(
Some(
Panel::new("project-panel-outer", cx)
.side(PanelSide::Left)
.child(ProjectPanel::new("project-panel-inner")),
)
.filter(|_| self.is_project_panel_open()),
)
.children(
Some(
Panel::new("collab-panel-outer", cx)
.child(CollabPanel::new("collab-panel-inner"))
.side(PanelSide::Left),
)
.filter(|_| self.is_collab_panel_open()),
)
// .child(NotificationToast::new(
// "maxbrunsfeld has requested to add you as a contact.".into(),
// ))
.child(
v_stack()
.flex_1()
.h_full()
.child(div().flex().flex_1().child(root_group))
.children(
Some(
Panel::new("terminal-panel", cx)
.child(Terminal::new())
.allowed_sides(PanelAllowedSides::BottomOnly)
.side(PanelSide::Bottom),
)
.filter(|_| self.is_terminal_open()),
),
)
.children(
Some(
Panel::new("chat-panel-outer", cx)
.side(PanelSide::Right)
.child(ChatPanel::new("chat-panel-inner").messages(vec![
ChatMessage::new(
"osiewicz".to_string(),
"is this thing on?".to_string(),
DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
.unwrap()
.naive_local(),
),
ChatMessage::new(
"maxdeviant".to_string(),
"Reading you loud and clear!".to_string(),
DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
.unwrap()
.naive_local(),
),
])),
)
.filter(|_| self.is_chat_panel_open()),
)
.children(
Some(
Panel::new("notifications-panel-outer", cx)
.side(PanelSide::Right)
.child(NotificationsPanel::new("notifications-panel-inner")),
)
.filter(|_| self.is_notifications_panel_open()),
)
.children(
Some(
Panel::new("assistant-panel-outer", cx)
.child(AssistantPanel::new("assistant-panel-inner")),
)
.filter(|_| self.is_assistant_panel_open()),
),
)
.child(StatusBar::new())
.when(self.debug.show_toast, |this| {
this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
})
.children(
Some(
div()
.absolute()
.top(px(50.))
.left(px(640.))
.z_index(8)
.child(LanguageSelector::new("language-selector")),
)
.filter(|_| self.is_language_selector_open()),
)
.z_index(8)
// Debug
.child(
v_stack()
.z_index(9)
.absolute()
.top_20()
.left_1_4()
.w_40()
.gap_2()
.when(self.show_debug, |this| {
this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
))
.child(
Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
|workspace, cx| workspace.debug_toggle_toast(cx),
)),
)
.child(
Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
|workspace, cx| workspace.debug_toggle_livestream(cx),
)),
)
})
.child(
Button::<Workspace>::new("Toggle Debug")
.on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
),
)
}
}
#[cfg(feature = "stories")]
pub use stories::*;
#[cfg(feature = "stories")]
mod stories {
use super::*;
use gpui::VisualContext;
pub struct WorkspaceStory {
workspace: View<Workspace>,
}
impl WorkspaceStory {
pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.build_view(|cx| Self {
workspace: Workspace::view(cx),
})
}
}
impl Render for WorkspaceStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().child(self.workspace.clone())
}
}
}