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

@@ -229,8 +229,8 @@ fn render_github_fixtures(fixture_root: &Path) -> String {
output.push_str(&string_literal(&id));
output.push_str(", ");
match previous_end_cursor.as_deref() {
Some(after) => output.push_str(&format!("Some({})", string_literal(after))),
None => output.push_str("None"),
| Some(after) => output.push_str(&format!("Some({})", string_literal(after))),
| None => output.push_str("None"),
}
output.push_str(") => Some(");
output.push_str(&string_literal(&fixture.json));
@@ -343,9 +343,9 @@ fn issue_fixture_state(issue: &serde_json::Value) -> &'static str {
}
match required_string(issue, &["state"]) {
"open" => "OPEN",
"closed" => "CLOSED",
state => panic!("unsupported pull request state in fixture: {state}"),
| "open" => "OPEN",
| "closed" => "CLOSED",
| state => panic!("unsupported pull request state in fixture: {state}"),
}
}

View File

@@ -136,7 +136,7 @@ where
{
let mut body: graphql_client::Response<T> = res.json().await?;
match body.data.take() {
None => Err(Error::GraphQLError(body.errors.unwrap_or_default())),
Some(data) => Ok((body, data)),
| None => Err(Error::GraphQLError(body.errors.unwrap_or_default())),
| Some(data) => Ok((body, data)),
}
}

View File

@@ -289,8 +289,8 @@ impl query::QueryFn for ListPullRequests {
}
let query_string = match self.filter {
| Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter),
| None => "is:pr archived:false sort:updated-desc".into(),
| Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter),
| None => "is:pr archived:false sort:updated-desc".into(),
};
let gql =
@@ -312,19 +312,19 @@ impl query::QueryFn for ListPullRequests {
.flatten()
.filter_map(|edge| {
edge.node.and_then(|n| match n {
| PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => {
Some(PullRequest {
id: p.id.into(),
title: p.title,
state: p.state,
is_draft: p.is_draft,
repo_slug: format!(
"{}/{}",
p.repository.owner.login, p.repository.name
),
})
}
| _ => None,
| PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => {
Some(PullRequest {
id: p.id.into(),
title: p.title,
state: p.state,
is_draft: p.is_draft,
repo_slug: format!(
"{}/{}",
p.repository.owner.login, p.repository.name
),
})
}
| _ => None,
})
})
.collect::<Vec<_>>()
@@ -369,32 +369,32 @@ impl query::QueryFn for FetchPullRequest {
"missing 'node' field on PullRequestQuery response".into(),
))
.and_then(|n| match n {
| PullRequestQueryNode::PullRequest(p) => {
let created_at =
chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| {
api::Error::MalformedResponse(format!(
"invalid pull request createdAt {:?}: {err}",
p.created_at
))
})?;
| PullRequestQueryNode::PullRequest(p) => {
let created_at =
chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| {
api::Error::MalformedResponse(format!(
"invalid pull request createdAt {:?}: {err}",
p.created_at
))
})?;
Ok(DetailedPullRequest {
title: p.title,
state: p.state,
is_draft: p.is_draft,
body: p.body,
author: p.author.map(|it| api::user::Actor {
login: it.login,
avatar_url: it.avatar_url,
}),
base_branch_name: p.base_ref.map(|r| r.name),
head_branch_name: p.head_ref.map(|r| r.name),
created_at: Some(created_at),
})
}
| _ => Err(api::Error::MalformedResponse(
"unexpected node type on PullRequestQuery".into(),
)),
Ok(DetailedPullRequest {
title: p.title,
state: p.state,
is_draft: p.is_draft,
body: p.body,
author: p.author.map(|it| api::user::Actor {
login: it.login,
avatar_url: it.avatar_url,
}),
base_branch_name: p.base_ref.map(|r| r.name),
head_branch_name: p.head_ref.map(|r| r.name),
created_at: Some(created_at),
})
}
| _ => Err(api::Error::MalformedResponse(
"unexpected node type on PullRequestQuery".into(),
)),
})
}
}
@@ -437,11 +437,11 @@ impl query::QueryFn for FetchPullRequestTimeline {
TimelineActor {
kind: match on {
| actorFieldsOn::Bot => "Bot",
| actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount",
| actorFieldsOn::Mannequin => "Mannequin",
| actorFieldsOn::Organization => "Organization",
| actorFieldsOn::User => "User",
| actorFieldsOn::Bot => "Bot",
| actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount",
| actorFieldsOn::Mannequin => "Mannequin",
| actorFieldsOn::Organization => "Organization",
| actorFieldsOn::User => "User",
}
.into(),
name: login,
@@ -451,62 +451,62 @@ impl query::QueryFn for FetchPullRequestTimeline {
fn normalize_assignee(actor: assigneeFields) -> TimelineActor {
match actor {
| assigneeFields::Bot(actor) => TimelineActor {
kind: "Bot".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| assigneeFields::Mannequin(actor) => TimelineActor {
kind: "Mannequin".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| assigneeFields::Organization(actor) => TimelineActor {
kind: "Organization".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| assigneeFields::User(actor) => TimelineActor {
kind: "User".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| assigneeFields::Bot(actor) => TimelineActor {
kind: "Bot".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| assigneeFields::Mannequin(actor) => TimelineActor {
kind: "Mannequin".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| assigneeFields::Organization(actor) => TimelineActor {
kind: "Organization".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| assigneeFields::User(actor) => TimelineActor {
kind: "User".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
}
}
fn normalize_requested_reviewer(actor: requestedReviewerFields) -> TimelineActor {
match actor {
| requestedReviewerFields::Bot(actor) => TimelineActor {
kind: "Bot".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| requestedReviewerFields::Mannequin(actor) => TimelineActor {
kind: "Mannequin".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| requestedReviewerFields::Team(actor) => TimelineActor {
kind: "Team".into(),
name: actor.name,
avatar_url: None,
},
| requestedReviewerFields::User(actor) => TimelineActor {
kind: "User".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| requestedReviewerFields::Bot(actor) => TimelineActor {
kind: "Bot".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| requestedReviewerFields::Mannequin(actor) => TimelineActor {
kind: "Mannequin".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
| requestedReviewerFields::Team(actor) => TimelineActor {
kind: "Team".into(),
name: actor.name,
avatar_url: None,
},
| requestedReviewerFields::User(actor) => TimelineActor {
kind: "User".into(),
name: actor.login,
avatar_url: Some(actor.avatar_url),
},
}
}
fn normalize_review_state(state: PullRequestReviewState) -> String {
match state {
| PullRequestReviewState::PENDING => "PENDING",
| PullRequestReviewState::COMMENTED => "COMMENTED",
| PullRequestReviewState::APPROVED => "APPROVED",
| PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED",
| PullRequestReviewState::DISMISSED => "DISMISSED",
| _ => "OTHER",
| PullRequestReviewState::PENDING => "PENDING",
| PullRequestReviewState::COMMENTED => "COMMENTED",
| PullRequestReviewState::APPROVED => "APPROVED",
| PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED",
| PullRequestReviewState::DISMISSED => "DISMISSED",
| _ => "OTHER",
}
.into()
}
@@ -726,10 +726,10 @@ impl query::QueryFn for FetchPullRequestTimeline {
"missing 'node' field on PullRequestTimelineQuery response".into(),
))
.and_then(|node| match node {
| PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request),
| _ => Err(api::Error::MalformedResponse(
"unexpected node type on PullRequestTimelineQuery".into(),
)),
| PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request),
| _ => Err(api::Error::MalformedResponse(
"unexpected node type on PullRequestTimelineQuery".into(),
)),
})?;
let timeline = pull_request.timeline_items;

View File

@@ -167,7 +167,10 @@ mod tests {
.map(|author| author.login.as_str()),
Some("kennethnym")
);
assert_eq!(documented_failover.base_branch_name.as_deref(), Some("main"));
assert_eq!(
documented_failover.base_branch_name.as_deref(),
Some("main")
);
assert_eq!(
documented_failover.head_branch_name.as_deref(),
Some("docs/manual-failover-steps")
@@ -203,7 +206,10 @@ mod tests {
Some(chrono::DateTime::parse_from_rfc3339("2026-05-03T07:40:00Z").unwrap())
);
assert_eq!(
worker_split.author.as_ref().map(|author| author.login.as_str()),
worker_split
.author
.as_ref()
.map(|author| author.login.as_str()),
Some("leaferiksen")
);
assert_eq!(worker_split.base_branch_name.as_deref(), Some("main"));
@@ -295,36 +301,36 @@ mod tests {
.expect("third timeline fixture json should parse");
let first_page_nodes = match first_page.node.as_ref() {
Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
pull_request
.timeline_items
.nodes
.as_ref()
.expect("first timeline fixture page should contain timeline nodes")
}
_ => panic!("first timeline fixture page should resolve to a pull request node"),
| _ => panic!("first timeline fixture page should resolve to a pull request node"),
};
let second_page_nodes = match second_page.node.as_ref() {
Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
pull_request
.timeline_items
.nodes
.as_ref()
.expect("second timeline fixture page should contain timeline nodes")
}
_ => panic!("second timeline fixture page should resolve to a pull request node"),
| _ => panic!("second timeline fixture page should resolve to a pull request node"),
};
let third_page_nodes = match third_page.node.as_ref() {
Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
| Some(issues::PullRequestTimelineResponseNode::PullRequest(pull_request)) => {
pull_request
.timeline_items
.nodes
.as_ref()
.expect("third timeline fixture page should contain timeline nodes")
}
_ => panic!("third timeline fixture page should resolve to a pull request node"),
| _ => panic!("third timeline fixture page should resolve to a pull request node"),
};
assert_eq!(

View File

@@ -11,74 +11,85 @@ pub const fn hex(hex: u32) -> Rgba {
}
}
pub const fn hex_alpha(hex: u32, alpha: f32) -> 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: alpha,
}
}
#[allow(dead_code)]
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"),
| 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"),
}
}
#[allow(dead_code)]
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"),
| 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"),
}
}
#[allow(dead_code)]
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"),
| 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"),
}
}
#[allow(dead_code)]
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"),
| 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"),
}
}

View File

@@ -87,8 +87,8 @@ impl gpui::RenderOnce for Button {
let theme = app::current_theme(cx);
let icon_color = match self.variant {
| Variant::Primary => theme.colors.accent_text,
| Variant::Secondary => theme.colors.text,
| Variant::Primary => theme.colors.accent_on_solid,
| Variant::Secondary => theme.colors.text,
};
let mut children: Vec<AnyElement> = Vec::with_capacity(3);
@@ -115,10 +115,10 @@ impl gpui::RenderOnce for Button {
.py_0p5()
.children(children)
.when(matches!(self.variant, Variant::Primary), |div| {
div.bg(theme.colors.accent)
.text_color(theme.colors.accent_text)
div.bg(theme.colors.accent_solid)
.text_color(theme.colors.accent_on_solid)
.border_1()
.border_color(theme.colors.border.blend(theme.colors.accent))
.border_color(theme.colors.border.blend(theme.colors.accent_solid))
})
.when(matches!(self.variant, Variant::Secondary), |div| {
div.bg(theme.colors.surface_elevated)

View File

@@ -115,7 +115,7 @@ pub(crate) fn new(
impl Styled for ContentBlock {
fn style(&mut self) -> &mut gpui::StyleRefinement {
match self {
| ContentBlock::Text { style, .. } => style,
| ContentBlock::Text { style, .. } => style,
}
}
}
@@ -179,54 +179,56 @@ impl MarkdownText {
}
match node.kind_id() {
| MARKDOWN_KIND_ID_EMPHASIS => {
highlights.push((
node_range!(),
gpui::HighlightStyle {
font_style: Some(gpui::FontStyle::Italic),
..Default::default()
},
));
}
| MARKDOWN_KIND_ID_STRONG_EMPHASIS => highlights.push((
node_range!(),
gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
..Default::default()
},
)),
| MARKDOWN_KIND_ID_LINK => {
if cursor.goto_first_child() {
| MARKDOWN_KIND_ID_EMPHASIS => {
highlights.push((
node_range!(),
gpui::HighlightStyle {
color: Some(theme.colors.accent.into()),
underline: Some(gpui::UnderlineStyle {
color: Some(theme.colors.accent.into()),
thickness: px(1.),
wavy: false,
}),
font_style: Some(gpui::FontStyle::Italic),
..Default::default()
},
));
}
| MARKDOWN_KIND_ID_STRONG_EMPHASIS => highlights.push((
node_range!(),
gpui::HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
..Default::default()
},
)),
if cursor.goto_next_sibling()
&& let Ok(src) = cursor.node().utf8_text(content.as_bytes())
{
links
.push((node_range!(), gpui::SharedString::from(String::from(src))));
} else {
// the link src is invalid, use an empty string as a fallback
// link on click handler will ignore empty string
links.push((node_range!(), "".into()))
| MARKDOWN_KIND_ID_LINK => {
if cursor.goto_first_child() {
highlights.push((
node_range!(),
gpui::HighlightStyle {
color: Some(theme.colors.link.into()),
underline: Some(gpui::UnderlineStyle {
color: Some(theme.colors.link.into()),
thickness: px(1.),
wavy: false,
}),
..Default::default()
},
));
if cursor.goto_next_sibling()
&& let Ok(src) = cursor.node().utf8_text(content.as_bytes())
{
links.push((
node_range!(),
gpui::SharedString::from(String::from(src)),
));
} else {
// the link src is invalid, use an empty string as a fallback
// link on click handler will ignore empty string
links.push((node_range!(), "".into()))
}
}
}
}
| _ => {
// extend here to support more markdown node stylings
}
| _ => {
// extend here to support more markdown node stylings
}
};
if !cursor.goto_next_sibling() {
@@ -303,23 +305,23 @@ impl MarkdownText {
let marker_content = &content[marker_node.byte_range()];
let list_marker_char = match marker_content {
// unordered list item
| "-" | "+" | "*" => Some("".to_string()),
// unordered list item
| "-" | "+" | "*" => Some("".to_string()),
| marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => {
let i = list_index.get_or_insert_with(|| {
marker_content
.strip_suffix('.')
.unwrap()
.parse::<usize>()
.unwrap()
});
let j = *i;
*i = j + 1;
Some(format!("{j}."))
}
| marker_content if ORDERED_LIST_MARKER_REGEX.is_match(marker_content) => {
let i = list_index.get_or_insert_with(|| {
marker_content
.strip_suffix('.')
.unwrap()
.parse::<usize>()
.unwrap()
});
let j = *i;
*i = j + 1;
Some(format!("{j}."))
}
| _ => None,
| _ => None,
};
let Some(list_marker_char) = list_marker_char else {
@@ -331,9 +333,9 @@ impl MarkdownText {
let block = if cursor.goto_next_sibling() {
let mut b = block_for_node(cursor, content, 0, theme);
match b {
| ContentBlock::Text {
ref mut decoration, ..
} => *decoration = Some(list_marker_char.into()),
| ContentBlock::Text {
ref mut decoration, ..
} => *decoration = Some(list_marker_char.into()),
}
b
} else {
@@ -372,150 +374,150 @@ impl MarkdownText {
}
match current_node.kind_id() {
| MARKDOWN_KIND_ID_ATX_HEADING => {
if !cursor.goto_first_child() {
render_fallback_content(&cursor, &self.content, &mut self.blocks);
continue;
}
let marker_node_kind = cursor.node().kind_id();
let block = if cursor.goto_next_sibling()
&& cursor.node().kind_id() == MARKDOWN_KIND_ID_HEADING_CONTENT
{
// because HEADING_CONTENT node includes the space after the heading marker
// offset by 1 to exclude the space
block_for_node(&mut cursor, &self.content, 1, theme)
} else {
ContentBlock::Text {
decoration: None,
text: gpui::SharedString::new(&self.content[current_node.byte_range()]),
highlights: Vec::new(),
links: Vec::new(),
style: gpui::StyleRefinement::default(),
| MARKDOWN_KIND_ID_ATX_HEADING => {
if !cursor.goto_first_child() {
render_fallback_content(&cursor, &self.content, &mut self.blocks);
continue;
}
};
let mut block = match marker_node_kind {
| MARKDOWN_KIND_ID_ATX_H1_MARKER => block
.text_size(rems(2.25))
.font_weight(gpui::FontWeight::EXTRA_BOLD)
.mb_6(),
| MARKDOWN_KIND_ID_ATX_H2_MARKER => block
.text_2xl()
.font_weight(gpui::FontWeight::BOLD)
.mt_12()
.mb_4(),
| MARKDOWN_KIND_ID_ATX_H3_MARKER => block
.text_xl()
.font_weight(gpui::FontWeight::SEMIBOLD)
.mt_8()
.mb_3(),
| MARKDOWN_KIND_ID_ATX_H4_MARKER => block
.text_base()
.font_weight(gpui::FontWeight::SEMIBOLD)
.mt_6()
.mb_2(),
| _ => block,
}
.text_color(theme.colors.text);
let marker_node_kind = cursor.node().kind_id();
if is_first_heading {
is_first_heading = false;
block = block.mt_0();
let block = if cursor.goto_next_sibling()
&& cursor.node().kind_id() == MARKDOWN_KIND_ID_HEADING_CONTENT
{
// because HEADING_CONTENT node includes the space after the heading marker
// offset by 1 to exclude the space
block_for_node(&mut cursor, &self.content, 1, theme)
} else {
ContentBlock::Text {
decoration: None,
text: gpui::SharedString::new(&self.content[current_node.byte_range()]),
highlights: Vec::new(),
links: Vec::new(),
style: gpui::StyleRefinement::default(),
}
};
let mut block = match marker_node_kind {
| MARKDOWN_KIND_ID_ATX_H1_MARKER => block
.text_size(rems(2.25))
.font_weight(gpui::FontWeight::EXTRA_BOLD)
.mb_6(),
| MARKDOWN_KIND_ID_ATX_H2_MARKER => block
.text_2xl()
.font_weight(gpui::FontWeight::BOLD)
.mt_12()
.mb_4(),
| MARKDOWN_KIND_ID_ATX_H3_MARKER => block
.text_xl()
.font_weight(gpui::FontWeight::SEMIBOLD)
.mt_8()
.mb_3(),
| MARKDOWN_KIND_ID_ATX_H4_MARKER => block
.text_base()
.font_weight(gpui::FontWeight::SEMIBOLD)
.mt_6()
.mb_2(),
| _ => block,
}
.text_color(theme.colors.text);
if is_first_heading {
is_first_heading = false;
block = block.mt_0();
}
cursor.goto_parent();
self.blocks.push(block);
}
cursor.goto_parent();
| MARKDOWN_KIND_ID_PARAGRAPH => {
let block = block_for_node(&mut cursor, &self.content, 0, theme)
.text_color(theme.colors.text)
.text_sm();
self.blocks.push(block);
}
| MARKDOWN_KIND_ID_PARAGRAPH => {
let block = block_for_node(&mut cursor, &self.content, 0, theme)
.text_color(theme.colors.text)
.text_sm();
self.blocks.push(block);
}
| MARKDOWN_KIND_ID_TIGHT_LIST => {
let is_rendered =
render_list_node(&mut cursor, &self.content, &mut self.blocks, theme, 0);
if !is_rendered {
continue;
}
}
| MARKDOWN_KIND_ID_FENCED_CODE_BLOCK => {
// expected tree shape:
// fenced_code_block
// ├── info_string? (present if there is a language annotation)
// └── code_fence_content? (present if there is some content between the backticks)
if !cursor.goto_first_child() {
render_fallback_content(&cursor, &self.content, &mut self.blocks);
continue;
self.blocks.push(block);
}
let content = if cursor.node().kind_id() == MARKDOWN_KIND_ID_INFO_STRING {
// skipping info string (which annotates the code block)
if cursor.goto_next_sibling() {
// this is code_fence_content node
| MARKDOWN_KIND_ID_TIGHT_LIST => {
let is_rendered =
render_list_node(&mut cursor, &self.content, &mut self.blocks, theme, 0);
if !is_rendered {
continue;
}
}
| MARKDOWN_KIND_ID_FENCED_CODE_BLOCK => {
// expected tree shape:
// fenced_code_block
// ├── info_string? (present if there is a language annotation)
// └── code_fence_content? (present if there is some content between the backticks)
if !cursor.goto_first_child() {
render_fallback_content(&cursor, &self.content, &mut self.blocks);
continue;
}
let content = if cursor.node().kind_id() == MARKDOWN_KIND_ID_INFO_STRING {
// skipping info string (which annotates the code block)
if cursor.goto_next_sibling() {
// this is code_fence_content node
gpui::SharedString::new(
cursor
.node()
.utf8_text(self.content.as_bytes())
.unwrap_or_default(),
)
} else {
gpui::SharedString::default()
}
} else {
// assuming the current node is already code_fence_content
gpui::SharedString::new(
cursor
.node()
.utf8_text(self.content.as_bytes())
.unwrap_or_default(),
)
} else {
gpui::SharedString::default()
};
cursor.goto_parent();
let block = ContentBlock::Text {
decoration: None,
text: content,
highlights: Vec::new(),
links: Vec::new(),
style: gpui::StyleRefinement::default(),
}
} else {
// assuming the current node is already code_fence_content
gpui::SharedString::new(
cursor
.node()
.utf8_text(self.content.as_bytes())
.unwrap_or_default(),
)
};
cursor.goto_parent();
let block = ContentBlock::Text {
decoration: None,
text: content,
highlights: Vec::new(),
links: Vec::new(),
style: gpui::StyleRefinement::default(),
}
.text_sm()
.text_color(theme.colors.text)
.line_height(relative(1.2))
.font_family("Menlo")
.px_3()
.py_2()
.rounded_sm()
.bg(theme.colors.surface)
.border_1()
.my_4()
.border_color(theme.colors.border);
self.blocks.push(block);
}
| _ => {
println!(
"[WARN] formatting not implemenetd for node type {:?}",
current_node.kind()
);
let block = block_for_node(&mut cursor, &self.content, 0, theme)
.text_sm()
.text_color(theme.colors.text)
.text_sm();
.line_height(relative(1.2))
.font_family("Menlo")
.px_3()
.py_2()
.rounded_sm()
.bg(theme.colors.code_bg)
.border_1()
.my_4()
.border_color(theme.colors.code_border);
self.blocks.push(block);
}
self.blocks.push(block);
}
| _ => {
println!(
"[WARN] formatting not implemenetd for node type {:?}",
current_node.kind()
);
let block = block_for_node(&mut cursor, &self.content, 0, theme)
.text_color(theme.colors.text)
.text_sm();
self.blocks.push(block);
}
}
if !cursor.goto_next_sibling() {
@@ -533,55 +535,55 @@ impl gpui::Render for MarkdownText {
) -> impl gpui::prelude::IntoElement {
let children = self.blocks.iter().enumerate().map(|(i, block)| {
match block {
| ContentBlock::Text {
decoration,
text,
highlights,
links,
style,
} => {
let styled_text =
gpui::StyledText::new(text.clone()).with_highlights(highlights.clone());
| ContentBlock::Text {
decoration,
text,
highlights,
links,
style,
} => {
let styled_text =
gpui::StyledText::new(text.clone()).with_highlights(highlights.clone());
let content = if links.is_empty() {
div().w_full().child(styled_text)
} else {
// if link in block, interactive text is needed
// to handle link clicks
let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip();
let content = if links.is_empty() {
div().w_full().child(styled_text)
} else {
// if link in block, interactive text is needed
// to handle link clicks
let (link_ranges, srcs): (Vec<_>, Vec<_>) = links.iter().cloned().unzip();
let weak = cx.entity();
let t = gpui::InteractiveText::new(i, styled_text).on_click(
link_ranges,
move |i, _, cx| {
if let Some(src) = srcs.get(i) {
weak.update(cx, |this, cx| {
this.on_open_link(src, cx);
cx.notify();
})
}
},
);
let weak = cx.entity();
let t = gpui::InteractiveText::new(i, styled_text).on_click(
link_ranges,
move |i, _, cx| {
if let Some(src) = srcs.get(i) {
weak.update(cx, |this, cx| {
this.on_open_link(src, cx);
cx.notify();
})
}
},
);
div().w_full().child(t)
};
div().w_full().child(t)
};
let mut div = match decoration {
| Some(d) => div()
.w_full()
.flex()
.flex_row()
.gap_2()
.items_start()
.child(d.clone())
.child(div().flex_1().min_w_0().child(content)),
| None => div().w_full().child(content),
};
let mut div = match decoration {
| Some(d) => div()
.w_full()
.flex()
.flex_row()
.gap_2()
.items_start()
.child(d.clone())
.child(div().flex_1().min_w_0().child(content)),
| None => div().w_full().child(content),
};
div.style().refine(&style);
div.style().refine(&style);
div
}
div
}
}
});

View File

@@ -73,17 +73,17 @@ fn setup_application(cx: &mut gpui::App) {
let start = resume_application_state(cx);
match start {
Start::FromScratch => {
| Start::FromScratch => {
let screen = setup_wizard::new();
_ = setup_wizard::open_window(screen, cx);
}
Start::FromSetup(state) => {
| Start::FromSetup(state) => {
let screen = setup_wizard::from_saved(state);
_ = setup_wizard::open_window(screen, cx);
}
Start::FromSaved(_) => {
| Start::FromSaved(_) => {
_ = dashboard::open_window(cx);
}
};
@@ -123,8 +123,8 @@ fn resume_application_state(cx: &mut gpui::App) -> Start {
println!("[main] setup status: {:?}", setup_status);
match setup_status {
setup_wizard::SetupStatus::NotStarted => Start::FromScratch,
setup_wizard::SetupStatus::InProgress(state) => Start::FromSetup(state),
setup_wizard::SetupStatus::Completed => Start::FromSaved(state),
| setup_wizard::SetupStatus::NotStarted => Start::FromScratch,
| setup_wizard::SetupStatus::InProgress(state) => Start::FromSetup(state),
| setup_wizard::SetupStatus::Completed => Start::FromSaved(state),
}
}

View File

@@ -137,10 +137,10 @@ where
})?;
match wait_state {
WaitState::Cached => {
| WaitState::Cached => {
return Ok(ent);
}
WaitState::Waiting { rx, sub } => {
| WaitState::Waiting { rx, sub } => {
_ = sub;
_ = rx.await;
}
@@ -181,9 +181,9 @@ where
let state = query.raw.read(cx);
match &state.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()),
| 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()),
}
}
@@ -284,11 +284,11 @@ where
entity.raw.update(cx, |state, cx| {
state.data = match result {
Ok(data) => {
| Ok(data) => {
println!("[query] OK {}", q.key());
QueryData::Some(Box::new(data))
}
Err(err) => {
| Err(err) => {
println!("[query] ERR {:?}: {:?}", q.key(), err);
QueryData::Err(Box::new(err))
}
@@ -317,8 +317,8 @@ where
.raw
.update(cx, |query, cx| {
query.data = match result {
Ok(data) => QueryData::Some(Box::new(data)),
Err(err) => QueryData::Err(Box::new(err)),
| Ok(data) => QueryData::Some(Box::new(data)),
| Err(err) => QueryData::Err(Box::new(err)),
};
cx.notify();
true

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()

View File

@@ -16,12 +16,12 @@ pub(crate) struct PersistedState {
pub(crate) fn data_dir_path() -> std::path::PathBuf {
match std::env::consts::OS {
"macos" => std::env::home_dir()
| "macos" => std::env::home_dir()
.unwrap()
.join("Library")
.join("Application Support")
.join("novem"),
_ => unimplemented!(
| _ => unimplemented!(
"data_dir_path is unimplemented for OS: {}",
std::env::consts::OS
),

View File

@@ -1,6 +1,6 @@
use gpui::Rgba;
mod catppuccin;
use crate::colors::hex;
use gpui::Rgba;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ThemeMode {
@@ -28,15 +28,49 @@ pub struct ThemeColors {
pub background: Rgba,
pub surface: Rgba,
pub surface_elevated: Rgba,
pub surface_chrome: Rgba,
pub surface_hover: Rgba,
pub surface_active: Rgba,
pub border: Rgba,
pub border_muted: Rgba,
pub border_strong: Rgba,
pub focus_ring: 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,
pub text_subtle: Rgba,
pub text_disabled: Rgba,
pub icon_muted: Rgba,
pub link: Rgba,
pub link_hover: Rgba,
pub code_bg: Rgba,
pub code_border: Rgba,
pub selection_bg: Rgba,
pub selection_border: Rgba,
pub accent_fg: Rgba,
pub accent_muted: Rgba,
pub accent_border: Rgba,
pub accent_solid: Rgba,
pub accent_on_solid: Rgba,
pub success_fg: Rgba,
pub success_muted: Rgba,
pub success_border: Rgba,
pub success_solid: Rgba,
pub success_on_solid: Rgba,
pub warning_fg: Rgba,
pub warning_muted: Rgba,
pub warning_border: Rgba,
pub warning_solid: Rgba,
pub warning_on_solid: Rgba,
pub danger_fg: Rgba,
pub danger_muted: Rgba,
pub danger_border: Rgba,
pub danger_solid: Rgba,
pub danger_on_solid: Rgba,
pub info_fg: Rgba,
pub info_muted: Rgba,
pub info_border: Rgba,
pub info_solid: Rgba,
pub info_on_solid: Rgba,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -52,20 +86,20 @@ impl ThemeFamily {
pub const fn id(self) -> &'static str {
match self {
| Self::Catppuccin => "catppuccin",
| Self::Catppuccin => catppuccin::FAMILY_ID,
}
}
pub const fn label(self) -> &'static str {
match self {
| Self::Catppuccin => "Catppuccin",
| Self::Catppuccin => catppuccin::FAMILY_LABEL,
}
}
pub const fn variant(self, mode: ThemeMode) -> ThemeVariant {
match (self, mode) {
| (Self::Catppuccin, ThemeMode::Light) => ThemeVariant::CatppuccinLatte,
| (Self::Catppuccin, ThemeMode::Dark) => ThemeVariant::CatppuccinMocha,
| (Self::Catppuccin, ThemeMode::Light) => ThemeVariant::CatppuccinLatte,
| (Self::Catppuccin, ThemeMode::Dark) => ThemeVariant::CatppuccinMocha,
}
}
@@ -92,64 +126,28 @@ impl ThemeVariant {
pub const fn family(self) -> ThemeFamily {
match self {
| Self::CatppuccinLatte | Self::CatppuccinMocha => ThemeFamily::Catppuccin,
| Self::CatppuccinLatte | Self::CatppuccinMocha => ThemeFamily::Catppuccin,
}
}
pub const fn mode(self) -> ThemeMode {
match self {
| Self::CatppuccinLatte => ThemeMode::Light,
| Self::CatppuccinMocha => ThemeMode::Dark,
| Self::CatppuccinLatte => ThemeMode::Light,
| Self::CatppuccinMocha => ThemeMode::Dark,
}
}
pub const fn label(self) -> &'static str {
match self {
| Self::CatppuccinLatte => "Catppuccin Latte",
| Self::CatppuccinMocha => "Catppuccin Mocha",
| Self::CatppuccinLatte => catppuccin::LATTE_LABEL,
| Self::CatppuccinMocha => catppuccin::MOCHA_LABEL,
}
}
pub const fn theme(self) -> Theme {
match self {
| Self::CatppuccinLatte => Theme {
id: "catppuccin-latte",
name: "Catppuccin Latte",
mode: ThemeMode::Light,
colors: ThemeColors {
background: hex(0xeff1f5),
surface: hex(0xeff1f5),
surface_elevated: hex(0xdce0e8),
border: hex(0xccd0da),
text: hex(0x4c4f69),
text_muted: hex(0x6c6f85),
accent: hex(0x8839ef),
accent_hover: hex(0x7287fd),
accent_text: hex(0xeff1f5),
success: hex(0x40a02b),
warning: hex(0xdf8e1d),
danger: hex(0xd20f39),
},
},
| Self::CatppuccinMocha => Theme {
id: "catppuccin-mocha",
name: "Catppuccin Mocha",
mode: ThemeMode::Dark,
colors: ThemeColors {
background: hex(0x1e1e2e),
surface: hex(0x181825),
surface_elevated: hex(0x313244),
border: hex(0x45475a),
text: hex(0xcdd6f4),
text_muted: hex(0xa6adc8),
accent: hex(0xcba6f7),
accent_hover: hex(0xb4befe),
accent_text: hex(0x1e1e2e),
success: hex(0xa6e3a1),
warning: hex(0xf9e2af),
danger: hex(0xf38ba8),
},
},
| Self::CatppuccinLatte => catppuccin::latte(),
| Self::CatppuccinMocha => catppuccin::mocha(),
}
}
}
@@ -157,8 +155,10 @@ impl ThemeVariant {
impl From<gpui::WindowAppearance> for ThemeMode {
fn from(value: gpui::WindowAppearance) -> Self {
match value {
| gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => ThemeMode::Light,
| gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => ThemeMode::Dark,
| gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => {
ThemeMode::Light
}
| gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => ThemeMode::Dark,
}
}
}

121
src/theme/catppuccin.rs Normal file
View File

@@ -0,0 +1,121 @@
use crate::colors::{hex, hex_alpha};
use super::{Theme, ThemeColors, ThemeMode};
pub(crate) const FAMILY_ID: &str = "catppuccin";
pub(crate) const FAMILY_LABEL: &str = "Catppuccin";
pub(crate) const LATTE_LABEL: &str = "Catppuccin Latte";
pub(crate) const MOCHA_LABEL: &str = "Catppuccin Mocha";
pub(crate) const fn latte() -> Theme {
Theme {
id: "catppuccin-latte",
name: LATTE_LABEL,
mode: ThemeMode::Light,
colors: ThemeColors {
background: hex(0xeff1f5),
surface: hex(0xeff1f5),
surface_elevated: hex(0xdce0e8),
surface_chrome: hex(0xe6e9ef),
surface_hover: hex(0xe6e9ef),
surface_active: hex(0xdce0e8),
border: hex(0xbcc0cc),
border_muted: hex(0xccd0da),
border_strong: hex(0xacb0be),
focus_ring: hex(0x7287fd),
text: hex(0x4c4f69),
text_muted: hex(0x5c5f77),
text_subtle: hex(0x6c6f85),
text_disabled: hex(0x9ca0b0),
icon_muted: hex(0x6c6f85),
link: hex(0x1e66f5),
link_hover: hex(0x7287fd),
code_bg: hex(0xe6e9ef),
code_border: hex(0xccd0da),
selection_bg: hex_alpha(0x8839ef, 0.10),
selection_border: hex_alpha(0x8839ef, 0.35),
accent_fg: hex(0x8839ef),
accent_muted: hex_alpha(0x8839ef, 0.12),
accent_border: hex_alpha(0x8839ef, 0.28),
accent_solid: hex(0x8839ef),
accent_on_solid: hex(0xeff1f5),
success_fg: hex(0x40a02b),
success_muted: hex_alpha(0x40a02b, 0.12),
success_border: hex_alpha(0x40a02b, 0.28),
success_solid: hex(0x40a02b),
success_on_solid: hex(0x1e1e2e),
warning_fg: hex(0xdf8e1d),
warning_muted: hex_alpha(0xdf8e1d, 0.12),
warning_border: hex_alpha(0xdf8e1d, 0.32),
warning_solid: hex(0xdf8e1d),
warning_on_solid: hex(0x1e1e2e),
danger_fg: hex(0xd20f39),
danger_muted: hex_alpha(0xd20f39, 0.12),
danger_border: hex_alpha(0xd20f39, 0.28),
danger_solid: hex(0xd20f39),
danger_on_solid: hex(0xeff1f5),
info_fg: hex(0x1e66f5),
info_muted: hex_alpha(0x1e66f5, 0.12),
info_border: hex_alpha(0x1e66f5, 0.28),
info_solid: hex(0x1e66f5),
info_on_solid: hex(0xeff1f5),
},
}
}
pub(crate) const fn mocha() -> Theme {
Theme {
id: "catppuccin-mocha",
name: MOCHA_LABEL,
mode: ThemeMode::Dark,
colors: ThemeColors {
background: hex(0x1e1e2e),
surface: hex(0x181825),
surface_elevated: hex(0x313244),
surface_chrome: hex(0x11111b),
surface_hover: hex(0x313244),
surface_active: hex(0x45475a),
border: hex(0x585b70),
border_muted: hex(0x45475a),
border_strong: hex(0x6c7086),
focus_ring: hex(0xb4befe),
text: hex(0xcdd6f4),
text_muted: hex(0xbac2de),
text_subtle: hex(0xa6adc8),
text_disabled: hex(0x7f849c),
icon_muted: hex(0xa6adc8),
link: hex(0x89b4fa),
link_hover: hex(0xb4befe),
code_bg: hex(0x11111b),
code_border: hex(0x45475a),
selection_bg: hex_alpha(0xcba6f7, 0.18),
selection_border: hex_alpha(0xcba6f7, 0.45),
accent_fg: hex(0xcba6f7),
accent_muted: hex_alpha(0xcba6f7, 0.18),
accent_border: hex_alpha(0xcba6f7, 0.34),
accent_solid: hex(0xcba6f7),
accent_on_solid: hex(0x1e1e2e),
success_fg: hex(0xa6e3a1),
success_muted: hex_alpha(0xa6e3a1, 0.18),
success_border: hex_alpha(0xa6e3a1, 0.34),
success_solid: hex(0xa6e3a1),
success_on_solid: hex(0x1e1e2e),
warning_fg: hex(0xf9e2af),
warning_muted: hex_alpha(0xf9e2af, 0.18),
warning_border: hex_alpha(0xf9e2af, 0.38),
warning_solid: hex(0xf9e2af),
warning_on_solid: hex(0x1e1e2e),
danger_fg: hex(0xf38ba8),
danger_muted: hex_alpha(0xf38ba8, 0.18),
danger_border: hex_alpha(0xf38ba8, 0.34),
danger_solid: hex(0xf38ba8),
danger_on_solid: hex(0x1e1e2e),
info_fg: hex(0x89b4fa),
info_muted: hex_alpha(0x89b4fa, 0.18),
info_border: hex_alpha(0x89b4fa, 0.34),
info_solid: hex(0x89b4fa),
info_on_solid: hex(0x1e1e2e),
},
}
}