refactor: redesign theme tokens and split catppuccin themes

This commit is contained in:
2026-05-13 20:02:26 +08:00
parent af5fd60eb5
commit 2c3de1fd6e
20 changed files with 797 additions and 667 deletions

View File

@@ -9,7 +9,7 @@ use crate::{
api::{self},
app,
component::{
font_icon::{FontIcon, font_icon},
font_icon::{FontIcon, FontIconSvg, font_icon},
text::text,
},
query::{self, QueryStatus, read_query, use_query},
@@ -134,6 +134,19 @@ impl gpui::RenderOnce for IssueListItem {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
let theme = app::current_theme(cx);
fn pill(label: impl gpui::IntoElement + gpui::Styled, icon: FontIconSvg) -> gpui::Div {
div()
.flex()
.flex_row()
.items_center()
.justify_start()
.rounded_full()
.px_2()
.gap_1()
.child(icon.size_3())
.child(label.text_xs())
}
let repo_name_text = match self.repo_name {
| Some(name) => text(name),
| None => text("Unknown repo"),
@@ -141,39 +154,43 @@ impl gpui::RenderOnce for IssueListItem {
.text_xs()
.opacity(0.5);
let icon = if self.is_draft {
font_icon(FontIcon::PullRequestDraft)
.text_color(theme.colors.text)
.opacity(0.5)
let status_pill = if self.is_draft {
pill(
text("Draft").text_color(theme.colors.text),
font_icon(FontIcon::PullRequestDraft).text_color(theme.colors.text),
)
.bg(theme.colors.surface)
} else {
match self.status {
| api::issues::PullRequestState::Closed => {
font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.danger)
| api::issues::PullRequestState::Closed => pill(
text("Closed").text_color(theme.colors.danger_on_solid),
font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.danger_on_solid),
)
.bg(theme.colors.danger_solid),
| api::issues::PullRequestState::Merged => pill(
text("Merged").text_color(theme.colors.accent_on_solid),
font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.accent_on_solid),
)
.bg(theme.colors.accent_solid),
| _ => pill(
text("Open").text_color(theme.colors.success_on_solid),
font_icon(FontIcon::PullRequestArrow).text_color(theme.colors.success_on_solid),
)
.bg(theme.colors.success_solid),
}
| api::issues::PullRequestState::Merged => {
font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.success)
}
| _ => font_icon(FontIcon::PullRequestArrow).text_color(theme.colors.success),
}
}
.flex_shrink_0()
.size_4();
let description_text = match self.description {
| Some(description) => text(description).text_xs(),
| None => text("No description provided").opacity(0.5).text_xs(),
};
let pills_row = div().flex().flex_row().gap_1().my_1().child(status_pill);
div()
.relative()
.w_full()
.px_1p5()
.px_3()
.py_1()
.gap_2()
.flex()
.flex_row()
.items_center()
.child(icon)
.child(
div()
.flex_1()
@@ -191,33 +208,30 @@ impl gpui::RenderOnce for IssueListItem {
.min_w_0()
.line_clamp(2),
)
.child(description_text),
.child(pills_row),
)
.when(!self.is_last, |it| {
it.border_b_1().border_color(theme.colors.border)
})
.when(self.is_selected, |it| {
it.bg(gpui::Rgba {
a: 0.05,
..theme.colors.accent
})
.overflow_hidden()
.border_r_1()
.child(
div()
.absolute()
.right_0()
.top_0()
.bottom_0()
.w_px()
.bg(theme.colors.accent)
.shadow(vec![gpui::BoxShadow {
blur_radius: px(16.),
spread_radius: px(2.),
color: gpui::Hsla::from(theme.colors.accent).alpha(0.8),
offset: point(px(-2.), px(0.)),
}]),
)
it.bg(theme.colors.selection_bg)
.overflow_hidden()
.border_r_1()
.child(
div()
.absolute()
.right_0()
.top_0()
.bottom_0()
.w_px()
.bg(theme.colors.selection_border)
.shadow(vec![gpui::BoxShadow {
blur_radius: px(16.),
spread_radius: px(2.),
color: gpui::Hsla::from(theme.colors.selection_border).alpha(0.8),
offset: point(px(-2.), px(0.)),
}]),
)
})
}
}

View File

@@ -1,6 +1,6 @@
use gpui::{
AppContext, InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled,
div, img, point, prelude::FluentBuilder, px,
div, img, prelude::FluentBuilder,
};
use crate::{
@@ -24,7 +24,7 @@ pub(crate) struct PullRequestView {
#[derive(gpui::IntoElement)]
struct Toolbar {}
pub fn new(cx: &mut gpui::Context<PullRequestView>) -> PullRequestView {
pub fn new(_cx: &mut gpui::Context<PullRequestView>) -> PullRequestView {
PullRequestView {
markdown_viewer: None,
pull_request_query: None,
@@ -89,37 +89,41 @@ impl PullRequestView {
.rounded_full();
match pr.state {
| api::issues::PullRequestState::Open => {
status_pill = status_pill
.bg(theme.colors.success)
.child(
font_icon(FontIcon::PullRequestArrow)
.size_3()
.text_color(theme.colors.accent_text),
)
.child(text("Open").text_color(theme.colors.accent_text).text_xs());
}
| api::issues::PullRequestState::Closed => {
status_pill = status_pill
.bg(theme.colors.danger)
.child(
font_icon(FontIcon::PullRequestClosed)
.size_3()
.text_color(theme.colors.accent_text),
)
.child(
text("Closed")
.text_color(theme.colors.accent_text)
| api::issues::PullRequestState::Open => {
status_pill = status_pill
.bg(theme.colors.success_solid)
.child(
font_icon(FontIcon::PullRequestArrow)
.size_3()
.text_color(theme.colors.success_on_solid),
)
.child(
text("Open")
.text_color(theme.colors.success_on_solid)
.text_xs(),
);
}
| api::issues::PullRequestState::Closed => {
status_pill = status_pill
.bg(theme.colors.danger_solid)
.child(
font_icon(FontIcon::PullRequestClosed)
.size_3()
.text_color(theme.colors.danger_on_solid),
)
.child(
text("Closed")
.text_color(theme.colors.danger_on_solid)
.text_xs(),
);
}
| api::issues::PullRequestState::Merged => {
status_pill = status_pill.bg(theme.colors.accent_solid).child(
text("Merged")
.text_color(theme.colors.accent_on_solid)
.text_xs(),
);
}
| api::issues::PullRequestState::Merged => {
status_pill = status_pill.bg(theme.colors.accent).child(
text("Merged")
.text_color(theme.colors.accent_text)
.text_xs(),
);
}
}
}
let merge_text = match (
@@ -127,48 +131,48 @@ impl PullRequestView {
pr.base_branch_name.as_ref(),
pr.head_branch_name.as_ref(),
) {
| (Some(author), Some(base_branch), Some(head_branch)) => {
let str = format!(
"{} requested to merge {} into {}",
author.login, head_branch, base_branch
);
| (Some(author), Some(base_branch), Some(head_branch)) => {
let str = format!(
"{} requested to merge {} into {}",
author.login, head_branch, base_branch
);
let head_branch_text_offset = author.login.len() + 20;
let base_branch_text_offset = head_branch_text_offset + head_branch.len() + 6;
let head_branch_text_offset = author.login.len() + 20;
let base_branch_text_offset = head_branch_text_offset + head_branch.len() + 6;
let highlights = [
(
0..author.login.len(),
gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
..Default::default()
},
),
(
head_branch_text_offset..head_branch_text_offset + head_branch.len(),
gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
color: Some(theme.colors.accent.into()),
..Default::default()
},
),
(
base_branch_text_offset..base_branch_text_offset + base_branch.len(),
gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
color: Some(theme.colors.accent.into()),
..Default::default()
},
),
];
let highlights = [
(
0..author.login.len(),
gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
..Default::default()
},
),
(
head_branch_text_offset..head_branch_text_offset + head_branch.len(),
gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
color: Some(theme.colors.accent_fg.into()),
..Default::default()
},
),
(
base_branch_text_offset..base_branch_text_offset + base_branch.len(),
gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
color: Some(theme.colors.accent_fg.into()),
..Default::default()
},
),
];
Some((
author,
gpui::StyledText::new(str).with_highlights(highlights),
))
}
Some((
author,
gpui::StyledText::new(str).with_highlights(highlights),
))
}
| _ => None,
| _ => None,
};
let metadata_line =
@@ -257,24 +261,24 @@ impl gpui::Render for PullRequestView {
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
div().size_full().child(match &self.pull_request_query {
| Some(q) => match read_query(q, cx) {
| QueryStatus::Loaded(pr) => self.pr_content(pr, cx),
| QueryStatus::Err(e) => div()
.size_full()
.child(format!("{:?}", e))
.into_any_element(),
| QueryStatus::Loading => div()
.size_full()
.child("loading pr content")
.into_any_element(),
},
| None => div().size_full().child("no pr selected").into_any_element(),
| Some(q) => match read_query(q, cx) {
| QueryStatus::Loaded(pr) => self.pr_content(pr, cx),
| QueryStatus::Err(e) => div()
.size_full()
.child(format!("{:?}", e))
.into_any_element(),
| QueryStatus::Loading => div()
.size_full()
.child("loading pr content")
.into_any_element(),
},
| None => div().size_full().child("no pr selected").into_any_element(),
})
}
}
impl gpui::RenderOnce for Toolbar {
fn render(self, window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
fn toolbar_button(id: impl Into<gpui::ElementId>) -> Button {
button(id)
.px_2p5()

View File

@@ -5,7 +5,6 @@ use crate::{
screen::dashboard::{
issue_list::{self, IssueList},
pull_request_view::{self, PullRequestView},
sidebar::{self, Sidebar, SidebarItemValue},
titlebar::{self, TitleBar},
},
};
@@ -13,7 +12,6 @@ use crate::{
pub(crate) struct Screen {
titlebar: gpui::Entity<TitleBar>,
issue_list: gpui::Entity<IssueList>,
sidebar: gpui::Entity<Sidebar>,
pull_request_view: gpui::Entity<PullRequestView>,
issue_filter: Option<&'static str>,
@@ -23,7 +21,6 @@ pub(crate) fn new(cx: &mut gpui::Context<Screen>) -> Screen {
let mut screen = Screen {
titlebar: cx.new(titlebar::new),
issue_list: cx.new(issue_list::new),
sidebar: cx.new(|_| sidebar::new()),
pull_request_view: cx.new(pull_request_view::new),
issue_filter: None,
@@ -34,35 +31,15 @@ pub(crate) fn new(cx: &mut gpui::Context<Screen>) -> Screen {
impl Screen {
fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
let on_item_change = cx.listener(|this, value, _, cx| {
this.handle_sidebar_item_change(value, cx);
});
self.sidebar.update(cx, |sidebar, _| {
sidebar.on_item_change(on_item_change);
});
_ = cx
.subscribe(&self.issue_list, |this, _, event, cx| match event {
| issue_list::Event::ItemSelected(pr_id) => {
this.handle_issue_list_item_selected(pr_id, cx);
}
| issue_list::Event::ItemSelected(pr_id) => {
this.handle_issue_list_item_selected(pr_id, cx);
}
})
.detach();
}
fn handle_sidebar_item_change(
&mut self,
value: &SidebarItemValue,
cx: &mut gpui::Context<Self>,
) {
match value {
| SidebarItemValue::PullRequest { filter } => {
self.issue_filter = Some(*filter);
cx.notify();
}
}
}
fn handle_issue_list_item_selected(
&mut self,
id: &api::issues::Id,
@@ -97,21 +74,12 @@ impl gpui::Render for Screen {
.flex_1()
.min_h_0()
.w_full()
.child(
div()
.w_40()
.flex_shrink_0()
.h_full()
.child(self.sidebar.clone()),
)
.child(
div()
.w_64()
.flex_shrink_0()
.h_full()
.bg(theme.colors.surface)
.border_x_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface_chrome)
.overflow_hidden()
.child(self.issue_list.clone()),
)

View File

@@ -160,19 +160,19 @@ impl gpui::RenderOnce for SidebarItem {
.px_2()
.py_1()
.gap_2()
.child(font_icon(self.icon).size_3().when(self.is_selected, |it| {
it.text_color(theme.colors.accent_text)
}))
.child(
font_icon(self.icon)
.size_3()
.when(self.is_selected, |it| it.text_color(theme.colors.accent_fg)),
)
.child(
text(self.title)
.text_sm()
.leading_tight()
.when(self.is_selected, |it| {
it.text_color(theme.colors.accent_text)
}),
.when(self.is_selected, |it| it.text_color(theme.colors.accent_fg)),
),
)
.when_some(self.on_click, |it, f| it.on_click(f))
.when(self.is_selected, |it| it.bg(theme.colors.accent))
.when(self.is_selected, |it| it.bg(theme.colors.selection_bg))
}
}

View File

@@ -1,7 +1,7 @@
use gpui::{ParentElement, Styled, TitlebarOptions, div};
use gpui::{ParentElement, Styled, div};
use crate::component::button::button;
use crate::query::{self, QueryStatus, read_query, use_lazy_query, use_query};
use crate::query::{self, QueryStatus, read_query, use_lazy_query};
use crate::{
api, app,
component::{
@@ -32,13 +32,13 @@ impl gpui::Render for TitleBar {
let user = read_query(&self.fetch_user_query, cx);
let user_avatar = match user {
QueryStatus::Err(api::Error::Unauthenticated) => div().absolute().right_2p5().child(
| QueryStatus::Err(api::Error::Unauthenticated) => div().absolute().right_2p5().child(
button("login-btn")
.leading(font_icon(FontIcon::Github))
.label("Login"),
),
_ => div(),
| _ => div(),
};
div()
@@ -50,7 +50,7 @@ impl gpui::Render for TitleBar {
.flex()
.px(g.safe_area.size.width)
.py_2()
.bg(g.current_theme.colors.background)
.bg(g.current_theme.colors.surface_chrome)
.text_color(g.current_theme.colors.text)
.relative()
.border_b_1()
@@ -61,7 +61,7 @@ impl gpui::Render for TitleBar {
}
impl RepoSelector {
pub fn new(cx: &mut gpui::Context<Self>) -> Self {
pub fn new(_cx: &mut gpui::Context<Self>) -> Self {
Self {}
}
}

View File

@@ -189,7 +189,7 @@ impl GithubStepView {
let poll_interval = u64::from(*interval);
match read_query(query, cx) {
QueryStatus::Loaded(data) => {
| QueryStatus::Loaded(data) => {
let auth_tokens = api::AuthTokens {
access_token: data.access_token.clone(),
};
@@ -226,7 +226,7 @@ impl GithubStepView {
.detach();
}
QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => {
| QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => {
if error == "authorization_pending" {
cx.spawn(async move |weak, cx| {
Timer::after(Duration::from_secs(poll_interval)).await;
@@ -242,7 +242,7 @@ impl GithubStepView {
}
}
_ => {}
| _ => {}
}
}
@@ -263,8 +263,8 @@ impl GithubStepView {
let theme = app::current_theme(cx);
let (displayed_code, copyable_code) = match create_device_code_query {
QueryStatus::Loaded(data) => (data.user_code.as_str(), Some(data.user_code.clone())),
_ => (self.placeholder_code.as_str(), None),
| QueryStatus::Loaded(data) => (data.user_code.as_str(), Some(data.user_code.clone())),
| _ => (self.placeholder_code.as_str(), None),
};
let border_color = theme.colors.border.clone();
@@ -358,14 +358,14 @@ impl gpui::Render for GithubStepView {
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let (can_go_next, header, body) = match self.user_query {
None => (false, self.header(), self.device_code_area(cx)),
Some(ref q) => {
| None => (false, self.header(), self.device_code_area(cx)),
| Some(ref q) => {
let user_query = read_query(q, cx);
match user_query {
QueryStatus::Loaded(user) => {
| QueryStatus::Loaded(user) => {
(true, connected_header(), connected_body(user, cx))
}
_ => (false, self.header(), self.device_code_area(cx)),
| _ => (false, self.header(), self.device_code_area(cx)),
}
}
};
@@ -436,7 +436,7 @@ fn connected_body(user: &api::user::User, cx: &gpui::Context<GithubStepView>) ->
.rounded_2xl()
.border_1()
.w_full()
.border_color(theme.colors.surface_elevated)
.border_color(theme.colors.border)
.p_4()
.child(
div()
@@ -461,9 +461,13 @@ fn connected_body(user: &api::user::User, cx: &gpui::Context<GithubStepView>) ->
.child(
div()
.rounded_full()
.bg(theme.colors.accent)
.bg(theme.colors.success_solid)
.p_1()
.child(font_icon(FontIcon::Check).size_4()),
.child(
font_icon(FontIcon::Check)
.size_4()
.text_color(theme.colors.success_on_solid),
),
),
)
}

View File

@@ -51,9 +51,9 @@ pub fn open_window(screen: Screen, cx: &mut gpui::App) -> anyhow::Result<()> {
impl Step {
pub const fn order(&self) -> usize {
match self {
Step::Welcome => 0,
Step::ConnectToGithub => 1,
Step::SetupComplete => 2,
| Step::Welcome => 0,
| Step::ConnectToGithub => 1,
| Step::SetupComplete => 2,
}
}
}

View File

@@ -38,9 +38,9 @@ pub(crate) fn from_saved(state: StoredSetupState) -> Screen {
impl Screen {
fn advance_to_next_step(&mut self, cx: &mut gpui::Context<Self>) {
let next_step = match self.current_step {
Step::Welcome => Step::ConnectToGithub,
Step::ConnectToGithub => Step::SetupComplete,
_ => panic!(),
| Step::Welcome => Step::ConnectToGithub,
| Step::ConnectToGithub => Step::SetupComplete,
| _ => panic!(),
};
self.current_step = next_step;
cx.notify();
@@ -67,8 +67,8 @@ impl Screen {
cx: &mut gpui::Context<Self>,
) -> &gpui::Entity<github_step::GithubStepView> {
match self.github_step_view {
Some(ref v) => v,
None => {
| Some(ref v) => v,
| None => {
let weak = cx.weak_entity();
self.github_step_view = Some(cx.new(|cx| {
let mut v = github_step::new(cx);
@@ -90,9 +90,9 @@ impl Screen {
.enumerate()
.map(|(i, step)| {
let label = match step {
Step::Welcome => "Welcome!",
Step::ConnectToGithub => "Connect to GitHub",
Step::SetupComplete => "Complete!",
| Step::Welcome => "Welcome!",
| Step::ConnectToGithub => "Connect to GitHub",
| Step::SetupComplete => "Complete!",
};
let is_completed = i < self.current_step.order();
let is_current = self.current_step == *step;
@@ -138,16 +138,16 @@ impl gpui::Render for Screen {
cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
let step_view = match self.current_step {
Step::Welcome => welcome_step()
| Step::Welcome => welcome_step()
.on_next(cx.listener(|this, _, _, cx| this.advance_to_next_step(cx)))
.into_any_element(),
Step::ConnectToGithub => match self.github_step_view {
Some(ref view) => view.clone().into_any_element(),
None => self.init_github_step_view(cx).clone().into_any_element(),
| Step::ConnectToGithub => match self.github_step_view {
| Some(ref view) => view.clone().into_any_element(),
| None => self.init_github_step_view(cx).clone().into_any_element(),
},
Step::SetupComplete => setup_complete_step().into_any_element(),
| Step::SetupComplete => setup_complete_step().into_any_element(),
};
let theme = app::current_theme(cx);
@@ -165,7 +165,7 @@ impl gpui::Render for Screen {
.justify_center()
.w_1_3()
.h_full()
.bg(theme.colors.surface)
.bg(theme.colors.surface_chrome)
.relative()
.child(
div()