some more bs

This commit is contained in:
2026-04-21 11:50:04 +01:00
parent 74a01bd9e6
commit 6c60013295
11 changed files with 177 additions and 58 deletions

View File

@@ -1,9 +1,22 @@
use crate::query;
pub(crate) mod repo;
pub(crate) mod user;
#[derive(Clone)]
pub struct QueryContext {
pub(crate) http: reqwest::Client,
pub(crate) auth: Option<Auth>,
}
impl query::Context for QueryContext {}
#[derive(Clone)]
pub(crate) struct Auth {
pub(crate) access_token: String,
pub(crate) refresh_token: String,
}
pub enum Error {
Unauthenticated,
}
impl query::Context for QueryContext {}

View File

@@ -12,6 +12,7 @@ impl query::QueryFn<QueryContext> for List {
"repo.list"
}
async fn run(&self, c: &QueryContext) -> Result<Self::Data, Self::Error> {
async fn run(&self, _c: &QueryContext) -> Result<Self::Data, Self::Error> {
Ok(())
}
}

18
src/api/user.rs Normal file
View File

@@ -0,0 +1,18 @@
use crate::{api, query};
#[derive(Clone)]
pub struct Fetch;
impl query::QueryFn<api::QueryContext> for Fetch {
type Data = api::Error;
type Error = api::Error;
fn key(&self) -> &'static str {
"user"
}
async fn run(&self, c: &api::QueryContext) -> Result<Self::Data, Self::Error> {
Err(api::Error::Unauthenticated)
}
}

View File

@@ -1,15 +1,14 @@
use gpui::{div, prelude::*};
use crate::{api, app};
use crate::query;
use crate::dashboard;
use crate::query;
use crate::theme;
use crate::titlebar;
use crate::{api, app};
pub struct Global {
pub safe_area: gpui::Bounds<gpui::Pixels>,
pub current_theme: theme::Theme,
pub query_store: query::Store<api::QueryContext>
}
pub struct Chrome {}
@@ -17,8 +16,9 @@ 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| {
cx.update_global::<app::Global, ()>(|global, cx| {
global.current_theme = window.appearance().into();
cx.notify();
});
})
.detach();
@@ -33,15 +33,16 @@ impl gpui::Render for Chrome {
_window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let title_bar = cx.new(|_| titlebar::TitleBar {});
let title_bar = cx.new(|cx| titlebar::TitleBar::new(cx));
let dashboard = cx.new(|_| dashboard::Screen {
text: "World".into(),
});
div()
.size_full()
.flex()
.flex_col()
.size_full()
.child(title_bar)
.child(dashboard)
}
@@ -54,5 +55,5 @@ pub fn current_theme<'a, E>(cx: &'a gpui::Context<E>) -> &'a theme::Theme {
}
pub fn query_store<'a, E>(cx: &'a gpui::Context<E>) -> &'a query::Store<api::QueryContext> {
&cx.global::<Global>().query_store
cx.global::<query::Store<api::QueryContext>>()
}

View File

@@ -1,2 +1,3 @@
pub mod button;
pub mod font_icon;
pub mod text;

35
src/component/button.rs Normal file
View File

@@ -0,0 +1,35 @@
use gpui::{FontWeight, InteractiveElement, ParentElement, Styled, div};
use crate::{app, component::text::Text};
pub struct Button(gpui::Stateful<gpui::Div>);
pub fn button<T>(id: impl Into<gpui::ElementId>, cx: &gpui::Context<T>) -> Button {
let theme = app::current_theme(cx);
Button(
div()
.id(id)
.rounded_sm()
.bg(theme.colors.accent)
.text_xs()
.text_color(theme.colors.accent_text)
.font_weight(FontWeight(500.))
.px_2p5()
.py_0p5(),
)
}
impl Button {
pub fn label(mut self, s: impl Text) -> Self {
self.0 = self.0.child(s);
self
}
}
impl gpui::IntoElement for Button {
type Element = gpui::Stateful<gpui::Div>;
fn into_element(self) -> Self::Element {
self.0
}
}

View File

@@ -1,6 +1,6 @@
use gpui::{div, prelude::*};
use crate::theme::Variant;
use crate::{app, theme::Variant};
pub struct Screen {
pub text: gpui::SharedString,
@@ -10,39 +10,39 @@ impl Render for Screen {
fn render(
&mut self,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Self>,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
let builtin = Variant::VioletDark;
let theme = builtin.theme();
let theme = app::current_theme(cx);
div()
.flex()
.flex_col()
.size_full()
.gap_3()
.flex_1()
.flex_row()
.w_full()
.gap_2()
.p_2p5()
.pt_0()
.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())),
.h_full()
.flex()
.w_1_3()
.bg(theme.colors.surface)
.rounded_lg(),
)
.child(
div()
.h_full()
.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)),
.w_2_3()
.bg(theme.colors.surface)
.rounded_lg(),
)
}
}

View File

@@ -1,5 +1,6 @@
use gpui::{bounds, point, prelude::*, px, size};
mod api;
mod app;
mod asset;
mod colors;
@@ -8,7 +9,6 @@ mod dashboard;
mod query;
mod theme;
mod titlebar;
mod api;
fn main() {
gpui::Application::new()
@@ -18,25 +18,30 @@ fn main() {
fn setup_application(cx: &mut gpui::App) {
let window_bounds = gpui::Bounds::centered(None, size(px(800.), px(600.0)), cx);
let query_store = query::Store::new(
api::QueryContext {
http: reqwest::Client::new(),
auth: None,
},
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(api::QueryContext {
http: reqwest::Client::new(),
}),
};
let top_left = global.safe_area.origin;
cx.set_global(global);
cx.set_global(query_store);
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.))),
traffic_light_position: Some(top_left + point(px(12.), px(12.))),
..Default::default()
}),
..Default::default()

View File

@@ -1,10 +1,9 @@
use crate::app;
use gpui::{AppContext, BorrowAppContext};
use std::any::Any;
pub trait QueryFn<C>: Clone + 'static
where
C: Context + 'static,
C: Context,
{
type Data: 'static;
type Error: 'static;
@@ -45,6 +44,27 @@ where
.detach();
}
pub fn read_query<'a, F, T, C>(
query_fn: F,
cx: &'a gpui::Context<T>,
) -> QueryStatus<'a, F::Data, F::Error>
where
C: Context + 'static,
F: QueryFn<C>,
T: 'static,
{
let store = cx.global::<Store<C>>();
let Some(ent) = store.query_data.get(query_fn.key()) else {
return QueryStatus::Loading;
};
let QueryState { data } = ent.read(cx);
match data {
QueryData::Loading | QueryData::Pending | QueryData::Stale => QueryStatus::Loading,
QueryData::Some(data) => QueryStatus::Loaded(data.downcast_ref::<F::Data>().unwrap()),
QueryData::Err(error) => QueryStatus::Err(error.downcast_ref::<F::Error>().unwrap()),
}
}
// ================= Store ==================
pub(crate) struct QueryState {
@@ -61,7 +81,7 @@ pub(crate) enum QueryData {
pub(crate) type Entity = gpui::Entity<QueryState>;
pub(crate) trait Context {}
pub(crate) trait Context: Clone {}
pub struct Store<C>
where
@@ -75,7 +95,7 @@ impl<C> Store<C>
where
C: Context + 'static,
{
pub fn new<E>(ctx: C, cx: &mut gpui::Context<E>) -> Self {
pub fn new(ctx: C, cx: &mut gpui::App) -> Self {
Self {
query_data: std::collections::HashMap::new(),
query_context: cx.new(|_| ctx),
@@ -131,9 +151,12 @@ where
cx.notify();
});
let q = query.clone();
let query_context = self.query_context.clone();
cx.spawn(async move |_, cx| {
let query_context = self.query_context.read(cx);
let result = query.run(query_context).await;
let c = query_context.read_with(cx, |c, _| c.clone())?;
let result = q.run(&c).await;
entity.update(cx, |state, cx| {
state.data = match result {
@@ -180,3 +203,5 @@ where
}
}
}
impl<C> gpui::Global for Store<C> where C: Context + 'static {}

View File

@@ -90,7 +90,7 @@ impl Variant {
border: neutral(800),
text: neutral(50),
text_muted: neutral(400),
accent: violet(400),
accent: violet(600),
accent_hover: violet(300),
accent_text: neutral(100),
success: hex(0x22c55e),

View File

@@ -1,16 +1,27 @@
use gpui::{ParentElement, Styled, div, AppContext};
use gpui::prelude::FluentBuilder;
use gpui::{ParentElement, Styled, div};
use crate::{api, app, component::{
font_icon::{FontIcon, font_icon},
text::text,
}, query};
use crate::app::query_store;
use crate::query::use_query;
use crate::component::button::button;
use crate::query::{QueryStatus, read_query, use_query};
use crate::{
api, app,
component::{
font_icon::{FontIcon, font_icon},
text::text,
},
};
pub struct TitleBar {}
pub struct RepoSelector {}
impl TitleBar {
pub fn new(cx: &mut gpui::Context<Self>) -> Self {
use_query(api::user::Fetch, cx);
Self {}
}
}
impl gpui::Render for TitleBar {
fn render(
&mut self,
@@ -18,35 +29,44 @@ impl gpui::Render for TitleBar {
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let g = cx.global::<app::Global>();
let user = read_query(api::user::Fetch, cx);
let user_avatar = match user {
QueryStatus::Err(api::Error::Unauthenticated) => div()
.absolute()
.right_2p5()
.child(button("login-btn", cx).label("Login")),
_ => div(),
};
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)
.w_full()
.h_10()
.flex()
.px(g.safe_area.size.width)
.py_2()
.bg(g.current_theme.colors.background)
.text_color(g.current_theme.colors.text)
.relative()
.child(repo_selector(cx))
.child(user_avatar)
}
}
impl RepoSelector {
pub fn new(cx: &mut gpui::Context<Self>) -> Self {
use_query(api::repo::List, cx);
use_query(api::user::Fetch, 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);
fn repo_selector<T: 'static>(cx: &gpui::Context<T>) -> gpui::Div {
div()
.flex()
.flex_row()