diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg new file mode 100644 index 0000000..05eec51 --- /dev/null +++ b/assets/icons/ellipsis.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/panel-left-open.svg b/assets/icons/panel-left-open.svg new file mode 100644 index 0000000..a4914be --- /dev/null +++ b/assets/icons/panel-left-open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/panel-left.svg b/assets/icons/panel-left.svg new file mode 100644 index 0000000..a4914be --- /dev/null +++ b/assets/icons/panel-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index f23ba3a..2af1b2f 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -11,6 +11,8 @@ path = "src/main.rs" [dependencies] gpui.workspace = true components.workspace = true +reqwest_client.workspace = true + tokio.workspace = true nostr-sdk.workspace = true keyring-search.workspace = true @@ -18,7 +20,6 @@ keyring.workspace = true anyhow.workspace = true serde.workspace = true serde_json.workspace = true -reqwest_client.workspace = true client = { version = "0.1.0", path = "../client" } rust-embed = "8.5.0" diff --git a/crates/ui/src/views/app.rs b/crates/ui/src/views/app.rs index 3caddea..1d9c3cd 100644 --- a/crates/ui/src/views/app.rs +++ b/crates/ui/src/views/app.rs @@ -1,12 +1,30 @@ -use components::theme::{ActiveTheme, Theme}; +use std::sync::Arc; + +use components::{ + dock::{DockArea, DockItem}, + theme::{ActiveTheme, Theme}, +}; use gpui::*; -use super::{chat_space::ChatSpace, onboarding::Onboarding}; +use super::{ + block::{welcome::WelcomeBlock, BlockContainer}, + onboarding::Onboarding, +}; use crate::state::AppState; +pub struct DockAreaTab { + id: &'static str, + version: usize, +} + +pub const DOCK_AREA: DockAreaTab = DockAreaTab { + id: "dock", + version: 1, +}; + pub struct AppView { onboarding: View, - chat_space: View, + dock_area: View, } impl AppView { @@ -20,14 +38,50 @@ impl AppView { // Onboarding let onboarding = cx.new_view(Onboarding::new); - // Chat Space - let chat_space = cx.new_view(ChatSpace::new); + // Dock + let dock_area = cx.new_view(|cx| DockArea::new(DOCK_AREA.id, Some(DOCK_AREA.version), cx)); + let weak_dock_area = dock_area.downgrade(); + + // Set dock layout + Self::init_layout(weak_dock_area, cx); AppView { onboarding, - chat_space, + dock_area, } } + + fn init_layout(dock_area: WeakView, cx: &mut WindowContext) { + let dock_item = Self::init_dock_items(&dock_area, cx); + let left_panels = + DockItem::split_with_sizes(Axis::Vertical, vec![], vec![None, None], &dock_area, cx); + + _ = dock_area.update(cx, |view, cx| { + view.set_version(DOCK_AREA.version, cx); + view.set_left_dock(left_panels, Some(px(260.)), true, cx); + view.set_root(dock_item, cx); + // TODO: support right dock? + // TODO: support bottom dock? + }); + } + + fn init_dock_items(dock_area: &WeakView, cx: &mut WindowContext) -> DockItem { + DockItem::split_with_sizes( + Axis::Vertical, + vec![DockItem::tabs( + vec![ + Arc::new(BlockContainer::panel::(cx)), + // TODO: add chat block + ], + None, + dock_area, + cx, + )], + vec![None], + dock_area, + cx, + ) + } } impl Render for AppView { @@ -37,7 +91,7 @@ impl Render for AppView { if cx.global::().signer.is_none() { content = content.child(self.onboarding.clone()) } else { - content = content.child(self.chat_space.clone()) + content = content.child(self.dock_area.clone()) } div() diff --git a/crates/ui/src/views/block/mod.rs b/crates/ui/src/views/block/mod.rs new file mode 100644 index 0000000..e464beb --- /dev/null +++ b/crates/ui/src/views/block/mod.rs @@ -0,0 +1,190 @@ +use components::{ + button::Button, + dock::{DockItemState, Panel, PanelEvent, TitleStyle}, + h_flex, + popup_menu::PopupMenu, + theme::ActiveTheme, + v_flex, +}; +use gpui::*; +use prelude::FluentBuilder; + +pub mod welcome; + +actions!(block, [PanelInfo]); + +pub fn section(title: impl IntoElement, cx: &WindowContext) -> Div { + h_flex() + .items_center() + .gap_4() + .p_4() + .w_full() + .rounded_lg() + .border_1() + .border_color(cx.theme().border) + .flex_wrap() + .justify_around() + .child(div().flex_none().w_full().child(title)) +} + +pub struct BlockContainer { + focus_handle: FocusHandle, + name: SharedString, + title_bg: Option, + description: SharedString, + width: Option, + height: Option, + block: Option, + closeable: bool, + zoomable: bool, +} + +#[derive(Debug)] +pub enum ContainerEvent { + Close, +} + +pub trait Block: FocusableView { + fn klass() -> &'static str { + std::any::type_name::().split("::").last().unwrap() + } + + fn title() -> &'static str; + + fn description() -> &'static str { + "" + } + + fn closeable() -> bool { + true + } + + fn zoomable() -> bool { + true + } + + fn title_bg() -> Option { + None + } + + fn new_view(cx: &mut WindowContext) -> View; +} + +impl EventEmitter for BlockContainer {} + +impl BlockContainer { + pub fn new(cx: &mut WindowContext) -> Self { + let focus_handle = cx.focus_handle(); + + Self { + focus_handle, + name: "".into(), + title_bg: None, + description: "".into(), + width: None, + height: None, + block: None, + closeable: true, + zoomable: true, + } + } + + pub fn panel(cx: &mut WindowContext) -> View { + let name = B::title(); + let description = B::description(); + let block = B::new_view(cx); + let focus_handle = block.focus_handle(cx); + + cx.new_view(|cx| { + let mut story = Self::new(cx).block(block.into()); + + story.focus_handle = focus_handle; + story.closeable = B::closeable(); + story.zoomable = B::zoomable(); + story.name = name.into(); + story.description = description.into(); + story.title_bg = B::title_bg(); + + story + }) + } + + pub fn width(mut self, width: gpui::Pixels) -> Self { + self.width = Some(width); + self + } + + pub fn height(mut self, height: gpui::Pixels) -> Self { + self.height = Some(height); + self + } + + pub fn block(mut self, block: AnyView) -> Self { + self.block = Some(block); + self + } + + fn on_action_panel_info(&mut self, _: &PanelInfo, _cx: &mut ViewContext) { + // struct Info; + // let note = Notification::new(format!("You have clicked panel info on: {}", self.name)).id::(); + // cx.push_notification(note); + } +} + +impl Panel for BlockContainer { + fn panel_name(&self) -> &'static str { + "BlockContainer" + } + + fn title(&self, _cx: &WindowContext) -> AnyElement { + self.name.clone().into_any_element() + } + + fn title_style(&self, cx: &WindowContext) -> Option { + self.title_bg.map(|bg| TitleStyle { + background: bg, + foreground: cx.theme().foreground, + }) + } + + fn closeable(&self, _cx: &WindowContext) -> bool { + self.closeable + } + + fn zoomable(&self, _cx: &WindowContext) -> bool { + self.zoomable + } + + fn popup_menu(&self, menu: PopupMenu, _cx: &WindowContext) -> PopupMenu { + menu.track_focus(&self.focus_handle) + .menu("Info", Box::new(PanelInfo)) + } + + fn toolbar_buttons(&self, _cx: &WindowContext) -> Vec