From 6d5d1544862db92676e997625e92fe17d2b0e88c Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 6 Aug 2026 14:21:03 +0700 Subject: [PATCH] update sidebar --- crates/workspace/src/views/sidebar.rs | 290 ------------------ .../views/sidebar/import_identity_dialog.rs | 11 + crates/workspace/src/views/sidebar/mod.rs | 170 ++++++++++ .../src/views/sidebar/onboarding_dialog.rs | 138 +++++++++ 4 files changed, 319 insertions(+), 290 deletions(-) delete mode 100644 crates/workspace/src/views/sidebar.rs create mode 100644 crates/workspace/src/views/sidebar/import_identity_dialog.rs create mode 100644 crates/workspace/src/views/sidebar/mod.rs create mode 100644 crates/workspace/src/views/sidebar/onboarding_dialog.rs diff --git a/crates/workspace/src/views/sidebar.rs b/crates/workspace/src/views/sidebar.rs deleted file mode 100644 index 1734e40..0000000 --- a/crates/workspace/src/views/sidebar.rs +++ /dev/null @@ -1,290 +0,0 @@ -use std::sync::Arc; - -use gpui::prelude::*; -use gpui::{ - App, Context, EventEmitter, FocusHandle, Focusable, Render, SharedString, Subscription, - WeakEntity, Window, div, px, -}; -use gpui_component::button::{Button, ButtonVariants}; -use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle}; -use gpui_component::dock::{DockArea, DockPlacement, Panel, PanelEvent}; -use gpui_component::form::{field, v_form}; -use gpui_component::input::{Input, InputState}; -use gpui_component::{ActiveTheme, Disableable, WindowExt, v_flex}; -use signed_state::{Backend, BackendEvent}; - -use crate::views::RepoListView; - -/// Shared state for the Join Now dialog, so async results can be rendered. -#[derive(Default)] -struct JoinNowState { - busy: bool, - error: Option, -} - -/// 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 Join Now dialog. - fn open_join_now(&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(|_| JoinNowState::default()); - - window.open_dialog(cx, move |dialog, _window, _cx| { - let name_input = name_input.clone(); - let pass_input = pass_input.clone(); - let repass_input = repass_input.clone(); - let state = state.clone(); - - dialog - .width(px(520.)) - .margin_top(px(50.)) - .content(move |content, _window, cx| { - let busy = state.read(cx).busy; - let error = state.read(cx).error.clone(); - - content - .child( - DialogHeader::new() - .child(DialogTitle::new().child("Create identity")) - .child( - DialogDescription::new() - .child("Set up your Signed identity to get started."), - ), - ) - .child( - v_form() - .child( - field() - .label("Name") - .description("Max 255 characters") - .required(true) - .child(Input::new(&name_input)), - ) - .child( - field() - .label("Passphrase") - .required(true) - .child(Input::new(&pass_input)), - ) - .child(field().required(true).child(Input::new(&repass_input))), - ) - .children(error.map(|message| { - div().text_sm().text_color(cx.theme().danger).child(message) - })) - .child( - DialogFooter::new().justify_end().child( - Button::new("continue") - .primary() - .label("Create new identity") - .tooltip("Create identity") - .loading(busy) - .disabled(busy) - .on_click({ - let name_input = name_input.clone(); - let pass_input = pass_input.clone(); - let repass_input = repass_input.clone(); - let state = state.clone(); - - move |_ev, window, cx| { - let name = name_input.read(cx).value().to_string(); - let pass = pass_input.read(cx).value().to_string(); - let repass = repass_input.read(cx).value().to_string(); - - if pass != repass { - state.update(cx, |state, _| { - state.busy = false; - state.error = - Some("Passphrases do not match".into()); - }); - return; - } - - state.update(cx, |state, _| { - state.busy = true; - state.error = None; - }); - - let rx = - Backend::global(cx).update(cx, |backend, cx| { - backend.create_identity(&name, &pass, cx) - }); - - let window_handle = window.window_handle(); - let state = state.clone(); - - cx.spawn(async move |cx| match rx.recv_async().await { - Ok(Ok(_)) => { - cx.update_window( - window_handle, - |_, window, cx| { - window.close_dialog(cx); - }, - ) - .ok(); - } - Ok(Err(e)) => { - cx.update_window( - window_handle, - |_, _window, cx| { - state.update(cx, |state, _| { - state.busy = false; - state.error = - Some(e.to_string().into()); - }); - }, - ) - .ok(); - } - Err(_) => {} - }) - .detach(); - } - }), - ), - ) - }) - }); - } - - /// Show the Import Identity dialog. - fn open_import(&mut self, window: &mut Window, cx: &mut Context) { - window.open_dialog(cx, move |dialog, _window, _cx| { - dialog.title("Import identity").width(px(400.)) - }); - } -} - -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 { - v_flex().child( - Button::new("explore") - .label("Explore") - .w_full() - .on_click(cx.listener(|this, _, window, cx| this.open_explore(window, cx))), - ) - } else { - 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("join") - .label("Join Now") - .primary() - .w_full() - .on_click( - cx.listener(|this, _ev, window, cx| this.open_join_now(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)), - ), - ) - } - } -} diff --git a/crates/workspace/src/views/sidebar/import_identity_dialog.rs b/crates/workspace/src/views/sidebar/import_identity_dialog.rs new file mode 100644 index 0000000..cfe8c37 --- /dev/null +++ b/crates/workspace/src/views/sidebar/import_identity_dialog.rs @@ -0,0 +1,11 @@ +use gpui::{App, Window, px}; +use gpui_component::WindowExt; + +/// Open the Import Identity dialog. +/// +/// Currently a placeholder — the dialog only shows a title for now. +pub fn open(window: &mut Window, cx: &mut App) { + window.open_dialog(cx, move |dialog, _window, _cx| { + dialog.title("Import identity").width(px(400.)) + }); +} diff --git a/crates/workspace/src/views/sidebar/mod.rs b/crates/workspace/src/views/sidebar/mod.rs new file mode 100644 index 0000000..b0b276d --- /dev/null +++ b/crates/workspace/src/views/sidebar/mod.rs @@ -0,0 +1,170 @@ +use std::sync::Arc; + +use gpui::prelude::*; +use gpui::{ + App, Context, EventEmitter, FocusHandle, Focusable, Render, Subscription, WeakEntity, Window, + div, +}; +use gpui_component::button::{Button, ButtonVariants}; +use gpui_component::dock::{DockArea, DockPlacement, Panel, PanelEvent}; +use gpui_component::input::InputState; +use gpui_component::{ActiveTheme, v_flex}; +use signed_state::{Backend, BackendEvent}; + +use super::RepoListView; + +mod import_identity_dialog; +mod onboarding_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_identity_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 { + v_flex().child( + Button::new("explore") + .label("Explore") + .w_full() + .on_click(cx.listener(|this, _, window, cx| this.open_explore(window, cx))), + ) + } else { + 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)), + ), + ) + } + } +} diff --git a/crates/workspace/src/views/sidebar/onboarding_dialog.rs b/crates/workspace/src/views/sidebar/onboarding_dialog.rs new file mode 100644 index 0000000..be9f5bb --- /dev/null +++ b/crates/workspace/src/views/sidebar/onboarding_dialog.rs @@ -0,0 +1,138 @@ +use gpui::prelude::*; +use gpui::{App, Entity, SharedString, Window, div, px}; +use gpui_component::button::{Button, ButtonVariants}; +use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle}; +use gpui_component::form::{field, v_form}; +use gpui_component::input::{Input, InputState}; +use gpui_component::{ActiveTheme, Disableable, WindowExt}; +use signed_state::Backend; + +/// Shared state for the Onboarding dialog, so async results can be rendered. +#[derive(Default)] +pub struct OnboardingState { + pub busy: bool, + pub error: Option, +} + +/// Open the Onboarding dialog for creating a new identity. +/// +/// The caller is responsible for creating the input and state entities and +/// passing them in. This function only builds the dialog UI and wires up +/// the continue-button handler. +pub fn open( + name_input: Entity, + pass_input: Entity, + repass_input: Entity, + state: Entity, + window: &mut Window, + cx: &mut App, +) { + window.open_dialog(cx, move |dialog, _window, _cx| { + let name_input = name_input.clone(); + let pass_input = pass_input.clone(); + let repass_input = repass_input.clone(); + let state = state.clone(); + + dialog + .width(px(520.)) + .margin_top(px(50.)) + .content(move |content, _window, cx| { + let busy = state.read(cx).busy; + let error = state.read(cx).error.clone(); + + content + .child( + DialogHeader::new() + .child(DialogTitle::new().child("Create identity")) + .child( + DialogDescription::new() + .child("Set up your Signed identity to get started."), + ), + ) + .child( + v_form() + .child( + field() + .label("Name") + .description("Max 255 characters") + .required(true) + .child(Input::new(&name_input)), + ) + .child( + field() + .label("Passphrase") + .required(true) + .child(Input::new(&pass_input)), + ) + .child(field().required(true).child(Input::new(&repass_input))), + ) + .children(error.map(|message| { + div().text_sm().text_color(cx.theme().danger).child(message) + })) + .child( + DialogFooter::new().justify_end().child( + Button::new("continue") + .primary() + .label("Create new identity") + .tooltip("Create identity") + .loading(busy) + .disabled(busy) + .on_click({ + let name_input = name_input.clone(); + let pass_input = pass_input.clone(); + let repass_input = repass_input.clone(); + let state = state.clone(); + + move |_ev, window, cx| { + let backend = Backend::global(cx); + let name = name_input.read(cx).value().to_string(); + let pass = pass_input.read(cx).value().to_string(); + let repass = repass_input.read(cx).value().to_string(); + + if pass != repass { + state.update(cx, |state, _| { + state.busy = false; + state.error = + Some("Passphrases do not match".into()); + }); + return; + } + + state.update(cx, |state, _| { + state.busy = true; + state.error = None; + }); + + let rx = backend.update(cx, |backend, cx| { + backend.create_identity(&name, &pass, cx) + }); + + let handle = window.window_handle(); + let state = state.clone(); + + cx.spawn(async move |cx| match rx.recv_async().await { + Ok(Ok(_)) => { + cx.update_window(handle, |_, window, cx| { + window.close_dialog(cx); + }) + .ok(); + } + Ok(Err(e)) => { + cx.update_window(handle, |_, _window, cx| { + state.update(cx, |state, _| { + state.busy = false; + state.error = Some(e.to_string().into()); + }); + }) + .ok(); + } + Err(_) => {} + }) + .detach(); + } + }), + ), + ) + }) + }); +}