diff --git a/src/api.rs b/src/api.rs index 4450b12..c4cba3e 100644 --- a/src/api.rs +++ b/src/api.rs @@ -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, } -impl query::Context for QueryContext {} \ No newline at end of file +#[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 {} diff --git a/src/api/repo.rs b/src/api/repo.rs index 09c8993..d4888d0 100644 --- a/src/api/repo.rs +++ b/src/api/repo.rs @@ -12,6 +12,7 @@ impl query::QueryFn for List { "repo.list" } - async fn run(&self, c: &QueryContext) -> Result { + async fn run(&self, _c: &QueryContext) -> Result { + Ok(()) } } diff --git a/src/api/user.rs b/src/api/user.rs new file mode 100644 index 0000000..714f79f --- /dev/null +++ b/src/api/user.rs @@ -0,0 +1,18 @@ +use crate::{api, query}; + +#[derive(Clone)] +pub struct Fetch; + +impl query::QueryFn for Fetch { + type Data = api::Error; + + type Error = api::Error; + + fn key(&self) -> &'static str { + "user" + } + + async fn run(&self, c: &api::QueryContext) -> Result { + Err(api::Error::Unauthenticated) + } +} diff --git a/src/app.rs b/src/app.rs index a15a5a5..4534029 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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, pub current_theme: theme::Theme, - pub query_store: query::Store } pub struct Chrome {} @@ -17,8 +16,9 @@ pub struct Chrome {} impl Chrome { pub fn new(window: &mut gpui::Window, cx: &mut gpui::Context) -> Self { cx.observe_window_appearance(window, |_, window, cx| { - cx.update_global::(|global, _cx| { + cx.update_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, ) -> 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) -> &'a theme::Theme { } pub fn query_store<'a, E>(cx: &'a gpui::Context) -> &'a query::Store { - &cx.global::().query_store + cx.global::>() } diff --git a/src/component.rs b/src/component.rs index 8e7fad6..0d77b04 100644 --- a/src/component.rs +++ b/src/component.rs @@ -1,2 +1,3 @@ +pub mod button; pub mod font_icon; pub mod text; diff --git a/src/component/button.rs b/src/component/button.rs new file mode 100644 index 0000000..2b72241 --- /dev/null +++ b/src/component/button.rs @@ -0,0 +1,35 @@ +use gpui::{FontWeight, InteractiveElement, ParentElement, Styled, div}; + +use crate::{app, component::text::Text}; + +pub struct Button(gpui::Stateful); + +pub fn button(id: impl Into, cx: &gpui::Context) -> 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; + + fn into_element(self) -> Self::Element { + self.0 + } +} diff --git a/src/dashboard.rs b/src/dashboard.rs index a824b8b..4faf5e5 100644 --- a/src/dashboard.rs +++ b/src/dashboard.rs @@ -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, + cx: &mut gpui::Context, ) -> 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(), ) } } diff --git a/src/main.rs b/src/main.rs index 1b3e834..259d72a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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() diff --git a/src/query.rs b/src/query.rs index c3230c0..bae2ddf 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,10 +1,9 @@ -use crate::app; use gpui::{AppContext, BorrowAppContext}; use std::any::Any; pub trait QueryFn: 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, +) -> QueryStatus<'a, F::Data, F::Error> +where + C: Context + 'static, + F: QueryFn, + T: 'static, +{ + let store = cx.global::>(); + 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::().unwrap()), + QueryData::Err(error) => QueryStatus::Err(error.downcast_ref::().unwrap()), + } +} + // ================= Store ================== pub(crate) struct QueryState { @@ -61,7 +81,7 @@ pub(crate) enum QueryData { pub(crate) type Entity = gpui::Entity; -pub(crate) trait Context {} +pub(crate) trait Context: Clone {} pub struct Store where @@ -75,7 +95,7 @@ impl Store where C: Context + 'static, { - pub fn new(ctx: C, cx: &mut gpui::Context) -> 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 gpui::Global for Store where C: Context + 'static {} diff --git a/src/theme.rs b/src/theme.rs index 8204752..5197173 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -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), diff --git a/src/titlebar.rs b/src/titlebar.rs index 7b004a8..361c017 100644 --- a/src/titlebar.rs +++ b/src/titlebar.rs @@ -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 { + 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, ) -> impl gpui::IntoElement { let g = cx.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 { use_query(api::repo::List, cx); + use_query(api::user::Fetch, cx); + Self {} } } -fn repo_selector(cx: &gpui::Context) -> gpui::Div { - let store = app::query_store(cx); - let repo = store.read_query(api::repo::List, cx); - +fn repo_selector(cx: &gpui::Context) -> gpui::Div { div() .flex() .flex_row()