feat: simple dock layout

This commit is contained in:
2025-12-13 09:20:36 +07:00
parent 703fe988bd
commit af740f462c
12 changed files with 238 additions and 37 deletions

View File

@@ -1,6 +1,7 @@
use std::env;
use gpui::{App, AppContext, Context, Entity, Global, Task};
use anyhow::Error;
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
@@ -17,8 +18,11 @@ pub struct Account {
/// The public key of the account
public_key: Option<PublicKey>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 1]>,
_tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
}
impl Account {
@@ -51,11 +55,12 @@ impl Account {
let args: Vec<String> = env::args().collect();
let account = args.get(1).and_then(|s| Keys::parse(s).ok());
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
if let Some(keys) = account {
tasks.push(
// Background
// Set signer in background
cx.spawn(async move |this, cx| {
let public_key = keys.public_key();
@@ -72,13 +77,22 @@ impl Account {
this.public_key = Some(public_key);
cx.notify();
})
.ok();
}),
);
}
subscriptions.push(
// Listen for public key set
cx.observe_self(move |this, cx| {
if let Some(public_key) = this.public_key {
this.init(public_key, cx);
}
}),
);
Self {
public_key: None,
_subscriptions: subscriptions,
_tasks: tasks,
}
}
@@ -93,4 +107,37 @@ impl Account {
// This method is only called when user is logged in, so unwrap safely
self.public_key.unwrap()
}
fn init(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
// Construct a filter to get the user's metadata
let filter = Filter::new()
.kind(Kind::Metadata)
.author(public_key)
.limit(1);
// Subscribe to the user metadata
client.subscribe(filter, Some(opts)).await?;
// Construct a filter to get the user's contact list
let filter = Filter::new()
.kind(Kind::ContactList)
.author(public_key)
.limit(1);
// Subscribe to the user's contact list
client.subscribe(filter, Some(opts)).await?;
log::info!("Subscribed to user metadata and contact list");
Ok(())
});
self._tasks.push(task);
}
}

View File

@@ -19,3 +19,6 @@ pub const SEARCH_RELAYS: [&str; 3] = [
/// Default relay for Nostr Connect
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
/// Default width of the sidebar.
pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.;

View File

@@ -14,6 +14,8 @@ use crate::workspace::Workspace;
mod actions;
mod menus;
mod panels;
mod sidebar;
mod themes;
mod title_bar;
mod workspace;

View File

@@ -0,0 +1 @@
pub mod startup;

View File

@@ -0,0 +1,41 @@
use gpui::{
div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
ParentElement, Render, Window,
};
use gpui_component::dock::{Panel, PanelEvent};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Startup> {
cx.new(|cx| Startup::new(window, cx))
}
pub struct Startup {
focus_handle: FocusHandle,
}
impl Startup {
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
}
impl Panel for Startup {
fn panel_name(&self) -> &'static str {
"Startup"
}
}
impl EventEmitter<PanelEvent> for Startup {}
impl Focusable for Startup {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Startup {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().child("Startup")
}
}

View File

@@ -0,0 +1,41 @@
use gpui::{
div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
ParentElement, Render, Window,
};
use gpui_component::dock::{Panel, PanelEvent};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
cx.new(|cx| Sidebar::new(window, cx))
}
pub struct Sidebar {
focus_handle: FocusHandle,
}
impl Sidebar {
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
}
impl Panel for Sidebar {
fn panel_name(&self) -> &'static str {
"Sidebar"
}
}
impl EventEmitter<PanelEvent> for Sidebar {}
impl Focusable for Sidebar {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().child("Sidebar")
}
}

View File

@@ -9,6 +9,7 @@ use gpui_component::TitleBar;
use crate::menus;
#[allow(clippy::type_complexity)]
pub struct AppTitleBar {
/// The app menu bar
app_menu_bar: Entity<AppMenuBar>,
@@ -35,6 +36,7 @@ impl AppTitleBar {
}
}
#[allow(dead_code)]
pub fn child<F, E>(mut self, f: F) -> Self
where
E: IntoElement,

View File

@@ -1,12 +1,17 @@
use common::CLIENT_NAME;
use std::sync::Arc;
use account::Account;
use common::{CLIENT_NAME, DEFAULT_SIDEBAR_WIDTH};
use gpui::{
div, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render,
Styled, Subscription, Window,
div, px, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Render, Styled, Subscription, Window,
};
use gpui_component::dock::DockArea;
use gpui_component::dock::{DockArea, DockItem};
use gpui_component::{v_flex, Root, Theme};
use smallvec::{smallvec, SmallVec};
use crate::panels::startup;
use crate::sidebar;
use crate::title_bar::AppTitleBar;
#[derive(Debug)]
@@ -25,6 +30,8 @@ impl Workspace {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let dock = cx.new(|cx| DockArea::new("dock", None, window, cx));
let title_bar = cx.new(|cx| AppTitleBar::new(CLIENT_NAME, window, cx));
let account = Account::global(cx);
let mut subscriptions = smallvec![];
subscriptions.push(
@@ -34,12 +41,47 @@ impl Workspace {
}),
);
subscriptions.push(
// Observe account entity changes
cx.observe_in(&account, window, move |this, state, window, cx| {
if state.read(cx).has_account() {
this.init_app_layout(window, cx);
}
}),
);
Self {
dock,
title_bar,
_subscriptions: subscriptions,
}
}
fn init_app_layout(&self, window: &mut Window, cx: &mut Context<Self>) {
let weak_dock = self.dock.downgrade();
let sidebar = Arc::new(sidebar::init(window, cx));
let startup = Arc::new(startup::init(window, cx));
// Construct left dock (sidebar)
let left = DockItem::panel(sidebar);
// Construct center dock
let center = DockItem::split_with_sizes(
Axis::Vertical,
vec![DockItem::tabs(vec![startup], &weak_dock, window, cx)],
vec![None],
&weak_dock,
window,
cx,
);
// Update dock layout
self.dock.update(cx, |this, cx| {
this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx);
this.set_center(center, window, cx);
});
}
}
impl Render for Workspace {

View File

@@ -9,6 +9,7 @@ common = { path = "../common" }
nostr-sdk.workspace = true
nostr-lmdb.workspace = true
nostr-gossip-memory.workspace = true
gpui.workspace = true
smol.workspace = true

View File

@@ -2,6 +2,7 @@ use std::time::Duration;
use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_gossip_memory::prelude::*;
use nostr_lmdb::NostrLmdb;
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
@@ -61,7 +62,11 @@ impl NostrRegistry {
});
// Construct the nostr client
let client = ClientBuilder::default().database(lmdb).opts(opts).build();
let client = ClientBuilder::default()
.database(lmdb)
.gossip(NostrGossipMemory::unbounded())
.opts(opts)
.build();
let mut tasks = smallvec![];