use std::sync::Arc; use assets::CustomIconName; use gpui::prelude::*; use gpui::{ App, ClickEvent, Context, ElementId, EventEmitter, FocusHandle, Focusable, Render, SharedString, StyleRefinement, Subscription, WeakEntity, Window, div, }; use gpui_component::avatar::Avatar; use gpui_component::button::{Button, ButtonVariants}; use gpui_component::dock::{DockArea, DockPlacement, Panel, PanelEvent}; use gpui_component::input::InputState; use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex}; use signed_state::{Backend, BackendEvent, ProfileStore}; use super::RepoListView; mod import_dialog; mod onboarding_dialog; pub(crate) mod passphrase_dialog; use self::onboarding_dialog::OnboardingState; /// 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>, logged_in: bool, _subscription: Subscription, } impl SidebarPanel { pub fn new(dock_area: WeakEntity, cx: &mut Context) -> Self { let backend = Backend::global(cx); let logged_in = backend.read(cx).current_user().is_some(); let subscription = cx.subscribe(&backend, |this, backend, event, cx| { match event { BackendEvent::SignerChanged => { this.logged_in = backend.read(cx).current_user().is_some(); } BackendEvent::SignerRequired => { this.logged_in = false; } _ => return, } cx.notify(); }); Self { focus_handle: cx.focus_handle(), dock_area, explore: None, logged_in, _subscription: subscription, } } /// 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()); let _ = self.dock_area.update(cx, |dock_area, cx| { dock_area.add_panel(Arc::new(panel), DockPlacement::Center, None, window, cx); }); } /// Show the Onboarding dialog. fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context) { let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Enter desired name")); let pass_input = cx.new(|cx| { InputState::new(window, cx) .placeholder("Passphrase to protect your keys") .masked(true) }); let repass_input = cx.new(|cx| { InputState::new(window, cx) .placeholder("Repeat passphrase") .masked(true) }); let state = cx.new(|_| OnboardingState::default()); onboarding_dialog::open(name_input, pass_input, repass_input, state, window, cx); } /// Show the Import Identity dialog. fn open_import(&mut self, window: &mut Window, cx: &mut Context) { import_dialog::open(window, cx); } } impl Panel for SidebarPanel { fn panel_name(&self) -> &'static str { "sidebar" } fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div() } fn closable(&self, _cx: &App) -> bool { false } fn inner_padding(&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 { if !self.logged_in { return v_flex() .p_4() .size_full() .items_center() .justify_center() .gap_2() .child( div() .text_sm() .text_color(cx.theme().muted_foreground) .child("Sign in to continue"), ) .child( Button::new("onboarding") .label("Join now") .primary() .w_full() .on_click( cx.listener(|this, _ev, window, cx| this.open_onboarding(window, cx)), ), ) .child( Button::new("import-identity") .label("Import identity") .secondary() .w_full() .on_click( cx.listener(|this, _ev, window, cx| this.open_import(window, cx)), ), ); } let backend = Backend::global(cx); let profile_store = ProfileStore::global(cx); let profile = backend .read(cx) .current_user() .map(|public_key| profile_store.read(cx).get(&public_key)); v_flex() .size_full() .justify_between() .bg(cx.theme().sidebar) .text_color(cx.theme().sidebar_foreground) .child( div() .flex_1() .when_some(profile.as_ref(), |this, profile| { let name = profile.name(); let picture = profile.picture(); this.child( h_flex() .h_12() .px_3() .gap_2() .child( Avatar::new() .name(name.clone()) .when_some(picture, |this, url| this.src(url)) .small() .border_0(), ) .child(div().text_sm().child(name)), ) }) .child( v_flex() .px_2() .gap_1() .items_start() .justify_start() .child(NavItem::new("inbox", "Inbox", IconName::Inbox).on_click( cx.listener(|this, _ev, window, cx| this.open_explore(window, cx)), )) .child(NavItem::new("explore", "Browse", IconName::Globe).on_click( cx.listener(|this, _ev, window, cx| this.open_explore(window, cx)), )) .child( v_flex().w_full().child( h_flex() .h_10() .w_full() .justify_between() .items_center() .child( h_flex() .px_2() .gap_2() .text_color(cx.theme().muted_foreground) .child(Icon::new(CustomIconName::Filter).small()) .child( div() .text_xs() .font_semibold() .child("All Repositories"), ), ) .child( Button::new("add").icon(IconName::Plus).small().ghost(), ), ), ), ), ) .child( v_flex() .p_2() .flex_shrink_0() .gap_1() .items_start() .justify_start() .child(NavItem::new("guide", "Guide", IconName::Info).on_click( cx.listener(|this, _ev, window, cx| this.open_explore(window, cx)), )) .child( NavItem::new("settings", "Settings", IconName::Settings).on_click( cx.listener(|this, _ev, window, cx| this.open_explore(window, cx)), ), ), ) } } /// A single navigation entry in the sidebar: an icon and label with a hover /// highlight and an optional click handler. #[allow(clippy::type_complexity)] #[derive(IntoElement)] struct NavItem { id: ElementId, style: StyleRefinement, icon: IconName, label: SharedString, on_click: Option>, } impl NavItem { fn new(id: I, label: L, icon: IconName) -> Self where I: Into, L: Into, { Self { id: id.into(), icon, label: label.into(), style: StyleRefinement::default(), on_click: None, } } fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self { self.on_click = Some(Box::new(listener)); self } } impl RenderOnce for NavItem { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { h_flex() .id(self.id) .refine_style(&self.style) .px_2() .py_1() .w_full() .gap_2() .rounded(cx.theme().radius) .child(Icon::new(self.icon).small()) .child(div().text_sm().child(self.label)) .hover(|this| this.bg(cx.theme().list_hover)) .when_some(self.on_click, |this, listener| this.on_click(listener)) } }