initial commit

This commit is contained in:
2026-04-20 15:13:26 +01:00
commit b521c4e4a0
22 changed files with 956 additions and 0 deletions

178
.gitignore vendored Normal file
View File

@@ -0,0 +1,178 @@
# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,rust,swift
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,rust,swift
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Rust ###
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
### Swift ###
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.build/
# CocoaPods
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
# Pods/
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# Accio dependency management
Dependencies/
.accio/
# fastlane
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
# Code Injection
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,rust,swift

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/runa.iml" filepath="$PROJECT_DIR$/.idea/runa.iml" />
</modules>
</component>
</project>

11
.idea/runa.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

9
Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[package]
name = "novem"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1"
gpui = { version = "*" }
paste = "1.0"

122
build.rs Normal file
View File

@@ -0,0 +1,122 @@
use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug)]
struct AssetFile {
virtual_path: String,
disk_path: PathBuf,
}
fn main() {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
let asset_root = manifest_dir.join("src/asset");
let out_file = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR")).join("asset.rs");
println!("cargo::rerun-if-changed={}", asset_root.display());
let mut directory_entries = BTreeMap::<String, BTreeSet<String>>::new();
directory_entries
.entry(String::new())
.or_default()
.insert(String::from("asset"));
let mut asset_files = Vec::new();
collect_assets(
&asset_root,
"asset",
&mut directory_entries,
&mut asset_files,
);
let generated = render_assets(&asset_files, &directory_entries);
fs::write(out_file, generated).expect("failed to write generated assets module");
}
fn collect_assets(
disk_dir: &Path,
virtual_dir: &str,
directory_entries: &mut BTreeMap<String, BTreeSet<String>>,
asset_files: &mut Vec<AssetFile>,
) {
let mut entries = fs::read_dir(disk_dir)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", disk_dir.display()))
.map(|entry| entry.expect("failed to read directory entry"))
.collect::<Vec<_>>();
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
let file_name = entry
.file_name()
.into_string()
.unwrap_or_else(|_| panic!("non-utf8 asset name in {}", disk_dir.display()));
let child_virtual_path = format!("{virtual_dir}/{file_name}");
let path = entry.path();
directory_entries
.entry(String::from(virtual_dir))
.or_default()
.insert(file_name);
if path.is_dir() {
collect_assets(&path, &child_virtual_path, directory_entries, asset_files);
} else if path.is_file() {
asset_files.push(AssetFile {
virtual_path: child_virtual_path,
disk_path: path,
});
}
}
}
fn render_assets(
asset_files: &[AssetFile],
directory_entries: &BTreeMap<String, BTreeSet<String>>,
) -> String {
let mut output = String::new();
output.push_str(
"pub fn load_asset(path: &str) -> gpui::Result<Option<std::borrow::Cow<'static, [u8]>>> {\n",
);
output.push_str(" match path {\n");
for file in asset_files {
output.push_str(" ");
output.push_str(&string_literal(&file.virtual_path));
output.push_str(" => Ok(Some(std::borrow::Cow::Borrowed(include_bytes!(");
output.push_str(&string_literal(&file.disk_path.to_string_lossy()));
output.push_str(")))),\n");
}
output.push_str(" _ => Err(anyhow::anyhow!(\"asset not found: {path}\")),\n");
output.push_str(" }\n");
output.push_str("}\n\n");
output.push_str("pub fn list_assets(path: &str) -> gpui::Result<Vec<gpui::SharedString>> {\n");
output.push_str(" let normalized = path.trim_end_matches('/');\n");
output.push_str(" let normalized = if normalized == \".\" { \"\" } else { normalized };\n");
output.push_str(" let entries: &[&str] = match normalized {\n");
for (directory, entries) in directory_entries {
output.push_str(" ");
output.push_str(&string_literal(directory));
output.push_str(" => &[\n");
for entry in entries {
output.push_str(" ");
output.push_str(&string_literal(entry));
output.push_str(",\n");
}
output.push_str(" ],\n");
}
output.push_str(" _ => &[],\n");
output.push_str(" };\n");
output.push_str(" Ok(entries.iter().copied().map(gpui::SharedString::from).collect())\n");
output.push_str("}\n");
output
}
fn string_literal(value: &str) -> String {
format!("{value:?}")
}

1
src/api.rs Normal file
View File

@@ -0,0 +1 @@
pub(crate) mod repo;

17
src/api/repo.rs Normal file
View File

@@ -0,0 +1,17 @@
use crate::query;
#[derive(Clone)]
pub struct List;
impl query::QueryFn for List {
type Data = ();
type Error = ();
fn key(&self) -> &'static str {
"repo.list"
}
async fn run(&self) -> Result<Self::Data, Self::Error> {
todo!()
}
}

58
src/app.rs Normal file
View File

@@ -0,0 +1,58 @@
use gpui::{div, prelude::*};
use crate::app;
use crate::query;
use crate::dashboard;
use crate::theme;
use crate::titlebar;
pub struct Global {
pub safe_area: gpui::Bounds<gpui::Pixels>,
pub current_theme: theme::Theme,
pub query_store: query::Store
}
pub struct Chrome {}
impl Chrome {
pub fn new(window: &mut gpui::Window, cx: &mut gpui::Context<Self>) -> Self {
cx.observe_window_appearance(window, |_, window, cx| {
cx.update_global::<app::Global, ()>(|global, _cx| {
global.current_theme = window.appearance().into();
});
})
.detach();
Self {}
}
}
impl gpui::Render for Chrome {
fn render(
&mut self,
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let title_bar = cx.new(|_| titlebar::TitleBar {});
let dashboard = cx.new(|_| dashboard::Screen {
text: "World".into(),
});
div()
.size_full()
.flex_col()
.child(title_bar)
.child(dashboard)
}
}
impl gpui::Global for Global {}
pub fn current_theme<'a, E>(cx: &'a gpui::Context<E>) -> &'a theme::Theme {
&cx.global::<Global>().current_theme
}
pub fn query_store<'a, E>(cx: &'a gpui::Context<E>) -> &'a query::Store {
&cx.global::<Global>().query_store
}

13
src/asset.rs Normal file
View File

@@ -0,0 +1,13 @@
include!(concat!(env!("OUT_DIR"), "/asset.rs"));
pub struct Asset;
impl gpui::AssetSource for Asset {
fn load(&self, path: &str) -> gpui::Result<Option<std::borrow::Cow<'static, [u8]>>> {
load_asset(path)
}
fn list(&self, path: &str) -> gpui::Result<Vec<gpui::SharedString>> {
list_assets(path)
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>

After

Width:  |  Height:  |  Size: 272 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder-git2-icon lucide-folder-git-2"><path d="M18 19a5 5 0 0 1-5-5v8"/><path d="M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v5"/><circle cx="13" cy="12" r="2"/><circle cx="20" cy="19" r="2"/></svg>

After

Width:  |  Height:  |  Size: 457 B

80
src/colors.rs Normal file
View File

@@ -0,0 +1,80 @@
use gpui::Rgba;
pub const fn hex(hex: u32) -> Rgba {
let [_, r, g, b] = hex.to_be_bytes();
Rgba {
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
a: 1.0,
}
}
pub const fn neutral(shade: u16) -> Rgba {
match shade {
50 => hex(0xfafafa),
100 => hex(0xf5f5f5),
200 => hex(0xe5e5e5),
300 => hex(0xd4d4d4),
400 => hex(0xa3a3a3),
500 => hex(0x737373),
600 => hex(0x525252),
700 => hex(0x404040),
800 => hex(0x262626),
900 => hex(0x171717),
950 => hex(0x0a0a0a),
_ => panic!("unsupported Tailwind neutral shade"),
}
}
pub const fn violet(shade: u16) -> Rgba {
match shade {
50 => hex(0xf5f3ff),
100 => hex(0xede9fe),
200 => hex(0xddd6fe),
300 => hex(0xc4b5fd),
400 => hex(0xa78bfa),
500 => hex(0x8b5cf6),
600 => hex(0x7c3aed),
700 => hex(0x6d28d9),
800 => hex(0x5b21b6),
900 => hex(0x4c1d95),
950 => hex(0x2e1065),
_ => panic!("unsupported Tailwind violet shade"),
}
}
pub const fn amber(shade: u16) -> Rgba {
match shade {
50 => hex(0xfffbeb),
100 => hex(0xfef3c7),
200 => hex(0xfde68a),
300 => hex(0xfcd34d),
400 => hex(0xfbbf24),
500 => hex(0xf59e0b),
600 => hex(0xd97706),
700 => hex(0xb45309),
800 => hex(0x92400e),
900 => hex(0x78350f),
950 => hex(0x451a03),
_ => panic!("unsupported Tailwind amber shade"),
}
}
pub const fn red(shade: u16) -> Rgba {
match shade {
50 => hex(0xfef2f2),
100 => hex(0xfee2e2),
200 => hex(0xfecaca),
300 => hex(0xfca5a5),
400 => hex(0xf87171),
500 => hex(0xef4444),
600 => hex(0xdc2626),
700 => hex(0xb91c1c),
800 => hex(0x991b1b),
900 => hex(0x7f1d1d),
950 => hex(0x450a0a),
_ => panic!("unsupported Tailwind red shade"),
}
}

2
src/component.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod font_icon;
pub mod text;

View File

@@ -0,0 +1,34 @@
use gpui::{Styled, svg};
use paste::paste;
use crate::app;
macro_rules! define_font_icons {
($($name:ident),+ $(,)?) => {
paste! {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum FontIcon {
$($name),+
}
pub const fn icon_path(icon: FontIcon) -> &'static str {
match icon {
$(
FontIcon::$name => concat!(
"asset/font_icon/",
stringify!([<$name:snake>]),
".svg"
),
)+
}
}
}
};
}
define_font_icons!(ChevronDown, FolderGit);
pub fn font_icon<T>(icon: FontIcon, cx: &gpui::Context<T>) -> gpui::Svg {
let theme = cx.global::<app::Global>().current_theme;
svg().path(icon_path(icon)).text_color(theme.colors.text)
}

13
src/component/text.rs Normal file
View File

@@ -0,0 +1,13 @@
use crate::app;
use gpui::{ParentElement, Styled, div};
pub trait Text: gpui::IntoElement {}
impl Text for &'static str {}
impl Text for String {}
impl Text for gpui::SharedString {}
pub fn text<'a, Content: Text, T>(s: Content, cx: &gpui::Context<T>) -> gpui::Div {
let theme = cx.global::<app::Global>().current_theme;
div().text_color(theme.colors.text).child(s)
}

48
src/dashboard.rs Normal file
View File

@@ -0,0 +1,48 @@
use gpui::{div, prelude::*};
use crate::theme::Variant;
pub struct Screen {
pub text: gpui::SharedString,
}
impl Render for Screen {
fn render(
&mut self,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
let builtin = Variant::VioletDark;
let theme = builtin.theme();
div()
.flex()
.flex_col()
.size_full()
.gap_3()
.bg(theme.colors.background)
.justify_center()
.items_center()
.shadow_lg()
.text_xl()
.text_color(theme.colors.text)
.child(format!("Hello, {}!", &self.text))
.child(
div()
.text_sm()
.text_color(theme.colors.text_muted)
.child(format!("Built-in theme: {}", builtin.label())),
)
.child(
div()
.flex()
.gap_2()
.child(div().size_8().bg(theme.colors.surface))
.child(div().size_8().bg(theme.colors.surface_elevated))
.child(div().size_8().bg(theme.colors.accent))
.child(div().size_8().bg(theme.colors.success))
.child(div().size_8().bg(theme.colors.warning))
.child(div().size_8().bg(theme.colors.danger)),
)
}
}

45
src/main.rs Normal file
View File

@@ -0,0 +1,45 @@
use gpui::{bounds, point, prelude::*, px, size};
mod app;
mod asset;
mod colors;
mod component;
mod dashboard;
mod query;
mod theme;
mod titlebar;
mod api;
fn main() {
gpui::Application::new()
.with_assets(asset::Asset)
.run(setup_application);
}
fn setup_application(cx: &mut gpui::App) {
let window_bounds = gpui::Bounds::centered(None, size(px(800.), px(600.0)), cx);
let global = app::Global {
safe_area: bounds(point(px(0.), px(0.)), size(px(72.), px(12.))),
current_theme: cx.window_appearance().into(),
query_store: query::Store::new(),
};
let top_left = global.safe_area.origin;
cx.set_global(global);
cx.open_window(
gpui::WindowOptions {
window_bounds: Some(gpui::WindowBounds::Windowed(window_bounds)),
titlebar: Some(gpui::TitlebarOptions {
appears_transparent: true,
traffic_light_position: Some(top_left + point(px(8.), px(8.))),
..Default::default()
}),
..Default::default()
},
|window, cx| cx.new(|cx| app::Chrome::new(window, cx)),
)
.unwrap();
}

123
src/query.rs Normal file
View File

@@ -0,0 +1,123 @@
use crate::app;
use gpui::{AppContext, BorrowAppContext};
use std::any::Any;
pub trait QueryFn: Clone + 'static {
type Data: 'static;
type Error: 'static;
fn key(&self) -> &'static str;
async fn run(&self) -> Result<Self::Data, Self::Error>;
}
pub enum QueryStatus<'a, Data, Error> {
Loading,
Loaded(&'a Data),
Err(&'a Error),
}
pub fn use_query<F, T>(query_fn: F, cx: &mut gpui::Context<T>)
where
F: QueryFn,
T: 'static,
{
let ent = cx.update_global::<app::Global, _>(|global, cx| {
let entity = global.query_store.ensure_query(&query_fn, cx);
if entity.read_with(cx, |state, _| matches!(state.data, QueryData::Pending)) {
global.query_store.execute_query(query_fn, entity.clone(), cx).detach();
}
entity
});
cx.observe(&ent, |_, _, cx| {
cx.notify();
})
.detach();
}
// ================= Store ==================
pub(crate) struct QueryState {
data: QueryData,
}
pub(crate) enum QueryData {
Pending,
Loading,
Some(Box<dyn Any>),
Err(Box<dyn Any>),
}
pub(crate) type Entity = gpui::Entity<QueryState>;
pub struct Store {
query_data: std::collections::HashMap<String, Entity>,
}
impl Store {
pub fn new() -> Self {
Self {
query_data: std::collections::HashMap::new(),
}
}
fn ensure_query<Q, T>(&mut self, query: &Q, cx: &mut gpui::Context<T>) -> Entity
where
Q: QueryFn,
T: 'static,
{
self.query_data
.entry(query.key().into())
.or_insert_with(|| {
cx.new(|_| QueryState {
data: QueryData::Pending,
})
})
.clone()
}
fn execute_query<Q, T>(
&mut self,
query: Q,
entity: Entity,
cx: &mut gpui::Context<T>,
) -> gpui::Task<anyhow::Result<()>>
where
Q: QueryFn,
T: 'static,
{
entity.update(cx, |state, cx| {
state.data = QueryData::Loading;
cx.notify();
});
cx.spawn(async move |_, cx| {
let result = query.run().await;
entity.update(cx, |state, cx| {
state.data = match result {
Ok(data) => QueryData::Some(Box::new(data)),
Err(err) => QueryData::Err(Box::new(err)),
};
cx.notify();
})?;
anyhow::Ok(())
})
}
pub(crate) fn read_query<'a, Q, E>(&self, query_fn: Q, cx: &'a gpui::Context<E>) -> QueryStatus<'a, Q::Data, Q::Error> where Q: QueryFn {
let Some(ent) = self.query_data.get(query_fn.key()) else {
return QueryStatus::Loading;
};
let QueryState { data } = ent.read(cx);
match data {
QueryData::Loading | QueryData::Pending => QueryStatus::Loading,
QueryData::Some(data) => QueryStatus::Loaded(data.downcast_ref::<Q::Data>().unwrap()),
QueryData::Err(error) => QueryStatus::Err(error.downcast_ref::<Q::Error>().unwrap())
}
}
}

117
src/theme.rs Normal file
View File

@@ -0,0 +1,117 @@
use gpui::Rgba;
use crate::colors::{amber, hex, neutral, red, violet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ThemeMode {
Light,
#[default]
Dark,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Theme {
pub id: &'static str,
pub name: &'static str,
pub mode: ThemeMode,
pub colors: ThemeColors,
}
impl Default for Theme {
fn default() -> Self {
Variant::default().theme()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ThemeColors {
pub background: Rgba,
pub surface: Rgba,
pub surface_elevated: Rgba,
pub border: Rgba,
pub text: Rgba,
pub text_muted: Rgba,
pub accent: Rgba,
pub accent_hover: Rgba,
pub accent_text: Rgba,
pub success: Rgba,
pub warning: Rgba,
pub danger: Rgba,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[allow(dead_code)]
pub enum Variant {
VioletLight,
#[default]
VioletDark,
}
impl Variant {
#[allow(dead_code)]
pub const ALL: [Self; 2] = [Self::VioletLight, Self::VioletDark];
pub const fn label(self) -> &'static str {
match self {
Self::VioletLight => "Violet Light",
Self::VioletDark => "Violet Dark",
}
}
pub const fn theme(self) -> Theme {
match self {
Self::VioletLight => Theme {
id: "violet-light",
name: "Violet Light",
mode: ThemeMode::Light,
colors: ThemeColors {
background: neutral(50),
surface: neutral(100),
surface_elevated: neutral(200),
border: neutral(200),
text: neutral(900),
text_muted: neutral(600),
accent: violet(600),
accent_hover: violet(500),
accent_text: neutral(100),
success: hex(0x16a34a),
warning: amber(600),
danger: red(600),
},
},
Self::VioletDark => Theme {
id: "violet-dark",
name: "Violet Dark",
mode: ThemeMode::Dark,
colors: ThemeColors {
background: neutral(950),
surface: neutral(900),
surface_elevated: neutral(800),
border: neutral(800),
text: neutral(50),
text_muted: neutral(400),
accent: violet(400),
accent_hover: violet(300),
accent_text: neutral(100),
success: hex(0x22c55e),
warning: amber(500),
danger: red(500),
},
},
}
}
}
impl From<gpui::WindowAppearance> for Theme {
fn from(value: gpui::WindowAppearance) -> Self {
let variant = match value {
gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => {
Variant::VioletLight
}
gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => {
Variant::VioletDark
}
};
variant.theme()
}
}

59
src/titlebar.rs Normal file
View File

@@ -0,0 +1,59 @@
use gpui::{ParentElement, Styled, div, AppContext};
use crate::{api, app, component::{
font_icon::{FontIcon, font_icon},
text::text,
}, query};
use crate::app::query_store;
use crate::query::use_query;
pub struct TitleBar {}
pub struct RepoSelector {}
impl gpui::Render for TitleBar {
fn render(
&mut self,
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let g = cx.global::<app::Global>();
div()
.w_full()
.h_8()
.flex()
.px(g.safe_area.size.width)
.py_2()
.flex_row()
.justify_center()
.items_center()
.border_b_1()
.border_color(g.current_theme.colors.border)
.bg(g.current_theme.colors.surface)
.text_color(g.current_theme.colors.text)
.child(repo_selector(cx))
}
}
impl RepoSelector {
pub fn new(cx: &mut gpui::Context<Self>) -> Self {
use_query(api::repo::List, cx);
Self {}
}
}
fn repo_selector<T>(cx: &gpui::Context<T>) -> gpui::Div {
let store = app::query_store(cx);
let repo = store.read_query(api::repo::List, cx);
div()
.flex()
.flex_row()
.items_center()
.gap_1()
.text_xs()
.child(font_icon(FontIcon::FolderGit, cx).size_3())
.child(text("test/repo", cx))
.child(font_icon(FontIcon::ChevronDown, cx).size_3())
}