feat: add simple workspace

This commit is contained in:
2025-12-11 10:44:45 +07:00
parent b9093f49a0
commit 187e53b3a2
7 changed files with 337 additions and 33 deletions

View File

@@ -2,7 +2,7 @@ use std::sync::Mutex;
use gpui::{actions, App}; use gpui::{actions, App};
actions!(coop, [Quit]); actions!(lume, [Quit, About, Open]);
pub fn load_embedded_fonts(cx: &App) { pub fn load_embedded_fonts(cx: &App) {
let asset_source = cx.asset_source(); let asset_source = cx.asset_source();

View File

@@ -3,16 +3,20 @@ use std::sync::Arc;
use assets::Assets; use assets::Assets;
use common::{APP_ID, CLIENT_NAME}; use common::{APP_ID, CLIENT_NAME};
use gpui::{ use gpui::{
div, point, px, size, AppContext, Application, Bounds, Context, IntoElement, KeyBinding, Menu, point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString,
MenuItem, ParentElement, Render, SharedString, Styled, TitlebarOptions, Window, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions, WindowOptions,
}; };
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::Root;
use gpui_component::{Root, StyledExt};
use crate::actions::{load_embedded_fonts, quit, Quit}; use crate::actions::{load_embedded_fonts, quit, Quit};
use crate::workspace::Workspace;
mod actions; mod actions;
mod menus;
mod themes;
mod title_bar;
mod workspace;
fn main() { fn main() {
// Initialize logging // Initialize logging
@@ -74,29 +78,12 @@ fn main() {
// Initialize components // Initialize components
gpui_component::init(cx); gpui_component::init(cx);
let view = cx.new(|_| HelloWorld); // Initialize themes
cx.new(|cx| Root::new(view, window, cx)) themes::init(cx);
let workspace = cx.new(|cx| Workspace::new(window, cx));
cx.new(|cx| Root::new(workspace, window, cx))
}) })
.expect("Failed to open window. Please restart the application."); .expect("Failed to open window. Please restart the application.");
}) })
} }
pub struct HelloWorld;
impl Render for HelloWorld {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.v_flex()
.gap_2()
.size_full()
.items_center()
.justify_center()
.child("Hello, World!")
.child(
Button::new("ok")
.primary()
.label("Let's Go!")
.on_click(|_, _, _| println!("Clicked!")),
)
}
}

101
crates/lume/src/menus.rs Normal file
View File

@@ -0,0 +1,101 @@
use gpui::{App, Entity, Menu, MenuItem, SharedString};
use gpui_component::menu::AppMenuBar;
use gpui_component::{ActiveTheme as _, Theme, ThemeMode, ThemeRegistry};
use crate::actions::{About, Open, Quit};
use crate::themes::{SwitchTheme, SwitchThemeMode};
pub fn init(title: impl Into<SharedString>, cx: &mut App) -> Entity<AppMenuBar> {
let app_menu_bar = AppMenuBar::new(cx);
let title: SharedString = title.into();
update_app_menu(title.clone(), app_menu_bar.clone(), cx);
// Observe theme changes to update the menu to refresh the checked state
cx.observe_global::<Theme>({
let title = title.clone();
let app_menu_bar = app_menu_bar.clone();
move |cx| {
update_app_menu(title.clone(), app_menu_bar.clone(), cx);
}
})
.detach();
app_menu_bar
}
fn update_app_menu(title: impl Into<SharedString>, app_menu_bar: Entity<AppMenuBar>, cx: &mut App) {
let mode = cx.theme().mode;
cx.set_menus(vec![
Menu {
name: title.into(),
items: vec![
MenuItem::action("About", About),
MenuItem::Separator,
MenuItem::action("Open...", Open),
MenuItem::Separator,
MenuItem::Submenu(Menu {
name: "Appearance".into(),
items: vec![
MenuItem::action("Light", SwitchThemeMode(ThemeMode::Light))
.checked(!mode.is_dark()),
MenuItem::action("Dark", SwitchThemeMode(ThemeMode::Dark))
.checked(mode.is_dark()),
],
}),
theme_menu(cx),
MenuItem::Separator,
MenuItem::action("Quit", Quit),
],
},
Menu {
name: "Edit".into(),
items: vec![
MenuItem::action("Undo", gpui_component::input::Undo),
MenuItem::action("Redo", gpui_component::input::Redo),
MenuItem::separator(),
MenuItem::action("Cut", gpui_component::input::Cut),
MenuItem::action("Copy", gpui_component::input::Copy),
MenuItem::action("Paste", gpui_component::input::Paste),
MenuItem::separator(),
MenuItem::action("Delete", gpui_component::input::Delete),
MenuItem::action(
"Delete Previous Word",
gpui_component::input::DeleteToPreviousWordStart,
),
MenuItem::action(
"Delete Next Word",
gpui_component::input::DeleteToNextWordEnd,
),
MenuItem::separator(),
MenuItem::action("Find", gpui_component::input::Search),
MenuItem::separator(),
MenuItem::action("Select All", gpui_component::input::SelectAll),
],
},
Menu {
name: "Help".into(),
items: vec![MenuItem::action("Open Website", Open)],
},
]);
app_menu_bar.update(cx, |menu_bar, cx| {
menu_bar.reload(cx);
})
}
fn theme_menu(cx: &App) -> MenuItem {
let themes = ThemeRegistry::global(cx).sorted_themes();
let current_name = cx.theme().theme_name();
MenuItem::Submenu(Menu {
name: "Theme".into(),
items: themes
.iter()
.map(|theme| {
let checked = current_name == &theme.name;
MenuItem::action(theme.name.clone(), SwitchTheme(theme.name.clone()))
.checked(checked)
})
.collect(),
})
}

82
crates/lume/src/themes.rs Normal file
View File

@@ -0,0 +1,82 @@
use std::path::PathBuf;
use common::home_dir;
use gpui::{Action, App, SharedString};
use gpui_component::scroll::ScrollbarShow;
use gpui_component::{ActiveTheme, Theme, ThemeMode, ThemeRegistry};
use serde::{Deserialize, Serialize};
#[derive(Action, Clone, PartialEq)]
#[action(namespace = themes, no_json)]
pub(crate) struct SwitchTheme(pub(crate) SharedString);
#[derive(Action, Clone, PartialEq)]
#[action(namespace = themes, no_json)]
pub(crate) struct SwitchThemeMode(pub(crate) ThemeMode);
#[derive(Debug, Clone, Serialize, Deserialize)]
struct State {
theme: SharedString,
scrollbar_show: Option<ScrollbarShow>,
}
impl Default for State {
fn default() -> Self {
Self {
theme: "Default Light".into(),
scrollbar_show: None,
}
}
}
pub fn init(cx: &mut App) {
// Load last theme state
let path = home_dir().join("state.json");
let json = std::fs::read_to_string(path).unwrap_or_default();
let state = serde_json::from_str::<State>(&json).unwrap_or_default();
if let Err(err) = ThemeRegistry::watch_dir(PathBuf::from("./themes"), cx, move |cx| {
if let Some(theme) = ThemeRegistry::global(cx)
.themes()
.get(&state.theme)
.cloned()
{
Theme::global_mut(cx).apply_config(&theme);
}
}) {
log::error!("Failed to watch themes directory: {}", err);
}
if let Some(scrollbar_show) = state.scrollbar_show {
Theme::global_mut(cx).scrollbar_show = scrollbar_show;
}
cx.refresh_windows();
cx.observe_global::<Theme>(|cx| {
let state = State {
theme: cx.theme().theme_name().clone(),
scrollbar_show: Some(cx.theme().scrollbar_show),
};
if let Ok(json) = serde_json::to_string_pretty(&state) {
let path = home_dir().join("state.json");
// Ignore write errors - if STATE_FILE doesn't exist or can't be written, do nothing
let _ = std::fs::write(path, json);
}
})
.detach();
cx.on_action(|switch: &SwitchTheme, cx| {
let theme_name = switch.0.clone();
if let Some(theme_config) = ThemeRegistry::global(cx).themes().get(&theme_name).cloned() {
Theme::global_mut(cx).apply_config(&theme_config);
}
cx.refresh_windows();
});
cx.on_action(|switch: &SwitchThemeMode, cx| {
let mode = switch.0;
Theme::change(mode, None, cx);
cx.refresh_windows();
});
}

View File

@@ -0,0 +1,64 @@
use std::rc::Rc;
use gpui::{
div, AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, MouseButton,
ParentElement as _, Render, SharedString, Styled as _, Subscription, Window,
};
use gpui_component::menu::AppMenuBar;
use gpui_component::TitleBar;
use crate::menus;
pub struct AppTitleBar {
/// The app menu bar
app_menu_bar: Entity<AppMenuBar>,
/// Child elements
child: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
/// Event subscriptions
_subscriptions: Vec<Subscription>,
}
impl AppTitleBar {
pub fn new(
title: impl Into<SharedString>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let app_menu_bar = menus::init(title, cx);
Self {
app_menu_bar,
child: Rc::new(|_, _| div().into_any_element()),
_subscriptions: vec![],
}
}
pub fn child<F, E>(mut self, f: F) -> Self
where
E: IntoElement,
F: Fn(&mut Window, &mut App) -> E + 'static,
{
self.child = Rc::new(move |window, cx| f(window, cx).into_any_element());
self
}
}
impl Render for AppTitleBar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
TitleBar::new()
// left side
.child(div().flex().items_center().child(self.app_menu_bar.clone()))
.child(
div()
.flex()
.items_center()
.justify_end()
.px_2()
.gap_2()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child((self.child.clone())(window, cx)),
)
}
}

View File

@@ -0,0 +1,70 @@
use common::CLIENT_NAME;
use gpui::{
div, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render,
Styled, Subscription, Window,
};
use gpui_component::dock::DockArea;
use gpui_component::{v_flex, Root, Theme};
use smallvec::{smallvec, SmallVec};
use crate::title_bar::AppTitleBar;
#[derive(Debug)]
pub struct Workspace {
/// The dock area for the workspace.
dock: Entity<DockArea>,
/// App's title bar.
title_bar: Entity<AppTitleBar>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
}
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 mut subscriptions = smallvec![];
subscriptions.push(
// Automatically sync theme with system appearance
window.observe_window_appearance(|window, cx| {
Theme::sync_system_appearance(Some(window), cx);
}),
);
Self {
dock,
title_bar,
_subscriptions: subscriptions,
}
}
}
impl Render for Workspace {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let dialog_layer = Root::render_dialog_layer(window, cx);
let sheet_layer = Root::render_sheet_layer(window, cx);
let notification_layer = Root::render_notification_layer(window, cx);
div()
.id("root")
.relative()
.size_full()
.child(
v_flex()
.size_full()
// Title bar
.child(self.title_bar.clone())
// Dock
.child(self.dock.clone()),
)
// Notifications
.children(notification_layer)
// Sheets
.children(sheet_layer)
// Modals
.children(dialog_layer)
}
}

View File

@@ -88,6 +88,11 @@ impl NostrRegistry {
} }
} }
/// Get the nostr client instance
pub fn client(&self) -> Client {
self.client.clone()
}
/// Establish connection to the bootstrap relays /// Establish connection to the bootstrap relays
async fn connect(client: &Client) { async fn connect(client: &Client) {
// Get all bootstrapping relays // Get all bootstrapping relays
@@ -103,9 +108,4 @@ impl NostrRegistry {
// Connect to all added relays // Connect to all added relays
client.connect().await; client.connect().await;
} }
/// Get the nostr client instance
pub fn client(&self) -> Client {
self.client.clone()
}
} }