diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 213cb6c..7ee8808 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -1,7 +1,7 @@ mod views; mod workspace; -pub use views::RepoListView; +pub use views::{RepoListView, SidebarPanel}; pub use workspace::Workspace; use gpui::{App, AppContext, Entity, Window}; diff --git a/crates/workspace/src/views/mod.rs b/crates/workspace/src/views/mod.rs index 7d8f7f2..3a96c03 100644 --- a/crates/workspace/src/views/mod.rs +++ b/crates/workspace/src/views/mod.rs @@ -1,3 +1,5 @@ mod repo_list; +mod sidebar; pub use repo_list::RepoListView; +pub use sidebar::SidebarPanel; diff --git a/crates/workspace/src/views/repo_list.rs b/crates/workspace/src/views/repo_list.rs index a51b2e0..1d6b6af 100644 --- a/crates/workspace/src/views/repo_list.rs +++ b/crates/workspace/src/views/repo_list.rs @@ -1,8 +1,9 @@ use gpui::prelude::*; use gpui::{ - AnyElement, App, Context, Entity, Render, SharedString, Subscription, Window, div, px, - uniform_list, + AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, + Subscription, Window, div, px, uniform_list, }; +use gpui_component::dock::{Panel, PanelEvent}; use gpui_component::{ActiveTheme, StyledExt}; use signed_core::Announcement; use signed_state::{ProfileStore, RepoListStore}; @@ -10,6 +11,7 @@ use signed_state::{ProfileStore, RepoListStore}; /// Browse all announced repositories (works anonymously). pub struct RepoListView { store: Entity, + focus_handle: FocusHandle, _subscription: Subscription, } @@ -20,11 +22,30 @@ impl RepoListView { Self { store, + focus_handle: cx.focus_handle(), _subscription: subscription, } } } +impl Panel for RepoListView { + fn panel_name(&self) -> &'static str { + "repo_list" + } + + fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + "Explore" + } +} + +impl EventEmitter for RepoListView {} + +impl Focusable for RepoListView { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + fn render_card(announcement: &Announcement, cx: &mut App) -> AnyElement { let name = announcement .name diff --git a/crates/workspace/src/views/sidebar.rs b/crates/workspace/src/views/sidebar.rs new file mode 100644 index 0000000..5c5a62a --- /dev/null +++ b/crates/workspace/src/views/sidebar.rs @@ -0,0 +1,77 @@ +use std::sync::Arc; + +use gpui::prelude::*; +use gpui::{App, Context, EventEmitter, FocusHandle, Focusable, Render, WeakEntity, Window}; +use gpui_component::button::Button; +use gpui_component::dock::{DockArea, DockPlacement, Panel, PanelEvent}; +use gpui_component::v_flex; + +use crate::views::RepoListView; + +/// Left-dock panel with navigation entries. Entries open content panels in +/// the dock area. +pub struct SidebarPanel { + focus_handle: FocusHandle, + dock_area: WeakEntity, + explore: Option>, +} + +impl SidebarPanel { + pub fn new(dock_area: WeakEntity, cx: &mut Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + dock_area, + explore: None, + } + } + + /// Open the Explore (repository list) panel in the center of the dock + /// area. No-op if it's already open. + pub fn open_explore(&mut self, window: &mut Window, cx: &mut Context) { + if self.explore.as_ref().and_then(WeakEntity::upgrade).is_some() { + return; + } + + let panel = cx.new(|cx| RepoListView::new(window, cx)); + self.explore = Some(panel.downgrade()); + + self.dock_area + .update(cx, |dock_area, cx| { + dock_area.add_panel(Arc::new(panel), DockPlacement::Center, None, window, cx); + }) + .ok(); + } +} + +impl Panel for SidebarPanel { + fn panel_name(&self) -> &'static str { + "sidebar" + } + + fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + "Navigation" + } + + fn closable(&self, _cx: &App) -> bool { + false + } +} + +impl EventEmitter for SidebarPanel {} + +impl Focusable for SidebarPanel { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for SidebarPanel { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex().gap_2().p_2().child( + Button::new("explore") + .label("Explore") + .w_full() + .on_click(cx.listener(|this, _, window, cx| this.open_explore(window, cx))), + ) + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e8bbd42..333af3c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,13 +1,14 @@ use gpui::prelude::*; -use gpui::{AnyView, Context, Render, SharedString, Subscription, Window, div, px}; -use gpui_component::{ActiveTheme, StyledExt}; +use gpui::{Context, Entity, Render, SharedString, Subscription, Window, div, px}; +use gpui_component::dock::{DockArea, DockItem}; +use gpui_component::{ActiveTheme, Root, StyledExt, TitleBar, v_flex}; use signed_state::{Backend, BackendEvent}; -use crate::views::RepoListView; +use crate::views::SidebarPanel; -/// Root view of the app: header, active screen, status bar. +/// Root view of the app: title bar, dock area, status bar. pub struct Workspace { - active_screen: AnyView, + dock: Entity, status: SharedString, _subscription: Subscription, } @@ -15,11 +16,34 @@ pub struct Workspace { impl Workspace { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let backend = Backend::global(cx); - let repo_list = cx.new(|cx| RepoListView::new(window, cx)); + let dock = cx.new(|cx| DockArea::new("dock", Some(1), window, cx)); + + let weak_dock = dock.downgrade(); + let sidebar = cx.new(|cx| SidebarPanel::new(weak_dock.clone(), cx)); + + dock.update(cx, |dock_area, cx| { + dock_area.set_left_dock( + DockItem::tab(sidebar.clone(), &weak_dock, window, cx), + Some(px(240.)), + true, + window, + cx, + ); + }); + + sidebar.update(cx, |sidebar, cx| sidebar.open_explore(window, cx)); let connected = backend.read(cx).is_connected(); let sync_progress = backend.read(cx).sync_progress(); + let status = if let Some((total, current)) = sync_progress { + format!("Syncing repositories... {current}/{total}").into() + } else if connected { + "Connected".into() + } else { + "Connecting...".into() + }; + let subscription = cx.subscribe(&backend, |this, _backend, event, cx| { match event { BackendEvent::Connected => this.status = "Connected".into(), @@ -34,54 +58,48 @@ impl Workspace { }); Self { - active_screen: repo_list.into(), - status: if let Some((total, current)) = sync_progress { - format!("Syncing repositories... {current}/{total}").into() - } else if connected { - "Connected".into() - } else { - "Connecting...".into() - }, + dock, + status, _subscription: subscription, } } } impl Render for Workspace { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let dialog_layer = Root::render_dialog_layer(window, cx); + let notification_layer = Root::render_notification_layer(window, cx); + div() + .id("workspace") .v_flex() .size_full() - .bg(cx.theme().background) - .text_color(cx.theme().foreground) .child( - div() - .h_flex() - .px_4() - .py_2() - .border_b(px(1.)) - .border_color(cx.theme().border) - .child(div().text_lg().font_semibold().child("Signed")), - ) - .child( - div() - .flex_1() - .overflow_hidden() - .child(self.active_screen.clone()), - ) - .child( - div() - .h_flex() - .px_4() - .py_1() - .border_t(px(1.)) - .border_color(cx.theme().border) + v_flex() + .size_full() + // Title Bar + .child(TitleBar::new()) + // Dock Area + .child(self.dock.clone()) + // Status Bar .child( div() - .text_xs() - .text_color(cx.theme().muted_foreground) - .child(self.status.clone()), + .h_flex() + .px_4() + .py_1() + .border_t(px(1.)) + .border_color(cx.theme().border) + .child( + div() + .text_xs() + .text_color(cx.theme().muted_foreground) + .child(self.status.clone()), + ), ), ) + // Notifications + .children(notification_layer) + // Modals + .children(dialog_layer) } }