feat: add simple workspace
This commit is contained in:
@@ -2,7 +2,7 @@ use std::sync::Mutex;
|
||||
|
||||
use gpui::{actions, App};
|
||||
|
||||
actions!(coop, [Quit]);
|
||||
actions!(lume, [Quit, About, Open]);
|
||||
|
||||
pub fn load_embedded_fonts(cx: &App) {
|
||||
let asset_source = cx.asset_source();
|
||||
|
||||
@@ -3,16 +3,20 @@ use std::sync::Arc;
|
||||
use assets::Assets;
|
||||
use common::{APP_ID, CLIENT_NAME};
|
||||
use gpui::{
|
||||
div, point, px, size, AppContext, Application, Bounds, Context, IntoElement, KeyBinding, Menu,
|
||||
MenuItem, ParentElement, Render, SharedString, Styled, TitlebarOptions, Window,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||
point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString,
|
||||
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
|
||||
WindowOptions,
|
||||
};
|
||||
use gpui_component::button::{Button, ButtonVariants};
|
||||
use gpui_component::{Root, StyledExt};
|
||||
use gpui_component::Root;
|
||||
|
||||
use crate::actions::{load_embedded_fonts, quit, Quit};
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
mod actions;
|
||||
mod menus;
|
||||
mod themes;
|
||||
mod title_bar;
|
||||
mod workspace;
|
||||
|
||||
fn main() {
|
||||
// Initialize logging
|
||||
@@ -74,29 +78,12 @@ fn main() {
|
||||
// Initialize components
|
||||
gpui_component::init(cx);
|
||||
|
||||
let view = cx.new(|_| HelloWorld);
|
||||
cx.new(|cx| Root::new(view, window, cx))
|
||||
// Initialize themes
|
||||
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.");
|
||||
})
|
||||
}
|
||||
|
||||
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
101
crates/lume/src/menus.rs
Normal 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
82
crates/lume/src/themes.rs
Normal 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();
|
||||
});
|
||||
}
|
||||
64
crates/lume/src/title_bar.rs
Normal file
64
crates/lume/src/title_bar.rs
Normal 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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
70
crates/lume/src/workspace.rs
Normal file
70
crates/lume/src/workspace.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
async fn connect(client: &Client) {
|
||||
// Get all bootstrapping relays
|
||||
@@ -103,9 +108,4 @@ impl NostrRegistry {
|
||||
// Connect to all added relays
|
||||
client.connect().await;
|
||||
}
|
||||
|
||||
/// Get the nostr client instance
|
||||
pub fn client(&self) -> Client {
|
||||
self.client.clone()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user