diff --git a/src/app.rs b/src/app.rs index f23d1ef..cfc036a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,6 +3,7 @@ use gpui::prelude::*; use crate::models::{FilterState, ProjectTree}; use crate::task::{TaskOverview, TaskService}; use crate::theme::ActiveTheme; +use crate::ui::{card_style, SECTION_GAP, ROOT_PADDING}; use crate::view::sidebar::{Sidebar, TagItem}; use crate::view::status_bar::StatusBar; use crate::view::task_table::TaskTable; @@ -24,31 +25,36 @@ impl gpui::Render for App { ) -> impl gpui::IntoElement { let theme = cx.theme(); + let sidebar = card_style(div(), theme) + .w(gpui::px(250.)) + .h_full() + .flex_shrink_0() + .overflow_hidden() + .child(self.sidebar.clone()); + + let main = card_style(div(), theme) + .flex_1() + .h_full() + .min_w_0() + .overflow_hidden() + .p_0() + .child(self.task_table.clone()); + let content = div() .flex() .flex_1() .min_h_0() - .overflow_hidden() - .child( - div() - .w(gpui::px(250.)) - .h_full() - .flex_shrink_0() - .child(self.sidebar.clone()), - ) - .child( - div() - .flex_1() - .h_full() - .min_w_0() - .child(self.task_table.clone()), - ); + .gap(SECTION_GAP) + .child(sidebar) + .child(main); div() .flex() .flex_col() .size_full() .bg(theme.background) + .p(ROOT_PADDING) + .gap(SECTION_GAP) .child(content) .child(self.status_bar.clone()) } diff --git a/src/components/button/dropdown.rs b/src/components/button/dropdown.rs index 458499e..c4f5bcb 100644 --- a/src/components/button/dropdown.rs +++ b/src/components/button/dropdown.rs @@ -300,6 +300,7 @@ impl gpui::Render for Dropdown { .bg(theme.background) .text_sm() .text_color(theme.foreground) + .whitespace_nowrap() .child(Label::new(label.clone())) .child(Label::new(arrow).text_color(theme.muted)); @@ -308,14 +309,14 @@ impl gpui::Render for Dropdown { } else { trigger = trigger .cursor_pointer() - .hover(|s: gpui::StyleRefinement| s.bg(theme.selection)); + .hover(|s: gpui::StyleRefinement| s.bg(theme.hover)); } trigger.into_any_element() }; let mut trigger_wrap = gpui::div() - .min_w(gpui::rems(12.0)) + .flex_shrink_0() .child(trigger); if !disabled { trigger_wrap = trigger_wrap.on_mouse_down( diff --git a/src/main.rs b/src/main.rs index 444243b..26061b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod components; mod models; mod task; mod theme; +mod ui; mod view; fn main() { diff --git a/src/theme.rs b/src/theme.rs index bbfe4ec..a806e63 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,79 +1,182 @@ +use gpui::prelude::*; + pub type Color = gpui::Rgba; #[derive(Debug, Clone)] pub struct Theme { pub background: Color, - pub foreground: Color, pub panel: Color, + pub card: Color, + pub raised: Color, + + pub foreground: Color, pub muted: Color, + pub disabled_fg: Color, + pub accent: Color, + pub focus_ring: Color, + pub border: Color, + pub divider: Color, + + pub field_bg: Color, + pub field_border: Color, + pub field_placeholder: Color, + + pub hover: Color, + pub selection: Color, + pub selection_foreground: Color, + + pub backdrop: Color, + pub error: Color, pub success: Color, pub warning: Color, pub info: Color, - pub selection: Color, - pub selection_foreground: Color, - pub text: Color, - pub text_size: Option>, pub high: Color, pub medium: Color, pub low: Color, + + pub text_size: Option>, } impl Theme { + pub fn alpha(c: Color, a: f32) -> Color { + gpui::Rgba { r: c.r, g: c.g, b: c.b, a: a.clamp(0.0, 1.0) } + } + pub fn dark() -> Self { + let background = gpui::rgb(0x0A0E14); + let panel = gpui::rgb(0x0F1419); + let foreground = gpui::rgb(0xB3B1AD); + let muted = gpui::rgb(0x5C6773); + let accent = gpui::rgb(0xFFB454); + let border = gpui::rgb(0x1F2430); + + let card = gpui::rgb(0x111823); + let raised = gpui::rgb(0x151E2B); + + let divider = Self::alpha(foreground, 0.10); + let hover = Self::alpha(foreground, 0.05); + let field_bg = raised; + let field_border = Self::alpha(foreground, 0.14); + let field_placeholder = Self::alpha(foreground, 0.45); + let focus_ring = Self::alpha(accent, 0.75); + let disabled_fg = Self::alpha(foreground, 0.35); + let backdrop = gpui::rgba(0x0000008C); + + let selection = gpui::rgb(0x273747); + let selection_foreground = gpui::rgb(0xE6E1CF); + + let error = gpui::rgb(0xF07178); + let success = gpui::rgb(0xAAD94C); + let warning = gpui::rgb(0xFFB454); + let info = gpui::rgb(0x59C2FF); + Self { - // ayu-dark - background: gpui::rgb(0x0A0E14), - panel: gpui::rgb(0x0F1419), - foreground: gpui::rgb(0xB3B1AD), - muted: gpui::rgb(0x5C6773), - accent: gpui::rgb(0xFFB454), - border: gpui::rgb(0x1F2430), + background, + panel, + card, + raised, - error: gpui::rgb(0xF07178), - success: gpui::rgb(0xAAD94C), - warning: gpui::rgb(0xFFB454), - info: gpui::rgb(0x59C2FF), + foreground, + muted, + disabled_fg, - selection: gpui::rgb(0x273747), - selection_foreground: gpui::rgb(0xE6E1CF), + accent, + focus_ring, + + border, + divider, + + field_bg, + field_border, + field_placeholder, + + hover, + selection, + selection_foreground, + + backdrop, + + error, + success, + warning, + info, + + high: error, + medium: warning, + low: success, - text: gpui::rgb(0xB3B1AD), text_size: Some(gpui::Size::new(14, 14)), - - high: gpui::rgb(0xF07178), - medium: gpui::rgb(0xFFB454), - low: gpui::rgb(0xAAD94C), } } pub fn light() -> Self { + let background = gpui::rgb(0xFAFAFA); + let panel = gpui::rgb(0xFFFFFF); + let foreground = gpui::rgb(0x5C6166); + let muted = gpui::rgb(0x8A9199); + let accent = gpui::rgb(0xFF8F40); + let border = gpui::rgb(0xE6E6E6); + + let card = gpui::rgb(0xF6F7F9); + let raised = gpui::rgb(0xEFF1F5); + + let divider = Self::alpha(foreground, 0.12); + let hover = Self::alpha(foreground, 0.06); + let field_bg = gpui::rgb(0xFFFFFF); + let field_border = Self::alpha(foreground, 0.18); + let field_placeholder = Self::alpha(foreground, 0.55); + let focus_ring = Self::alpha(accent, 0.70); + let disabled_fg = Self::alpha(foreground, 0.40); + let backdrop = gpui::rgba(0x00000040); + + let selection = gpui::rgb(0xD3EBFF); + let selection_foreground = foreground; + + let error = gpui::rgb(0xE65050); + let success = gpui::rgb(0x86B300); + let warning = gpui::rgb(0xF2AE49); + let info = gpui::rgb(0x399EE6); + Self { - // ayu-light - background: gpui::rgb(0xFAFAFA), - panel: gpui::rgb(0xFFFFFF), - foreground: gpui::rgb(0x5C6166), - muted: gpui::rgb(0x8A9199), - accent: gpui::rgb(0xFF8F40), - border: gpui::rgb(0xE6E6E6), + background, + panel, + card, + raised, - error: gpui::rgb(0xE65050), - success: gpui::rgb(0x86B300), - warning: gpui::rgb(0xF2AE49), - info: gpui::rgb(0x399EE6), + foreground, + muted, + disabled_fg, - selection: gpui::rgb(0xD3EBFF), - selection_foreground: gpui::rgb(0x5C6166), + accent, + focus_ring, + + border, + divider, + + field_bg, + field_border, + field_placeholder, + + hover, + selection, + selection_foreground, + + backdrop, + + error, + success, + warning, + info, + + high: error, + medium: warning, + low: success, - text: gpui::rgb(0x5C6166), text_size: Some(gpui::Size::new(14, 14)), - - high: gpui::rgb(0xE65050), - medium: gpui::rgb(0xF2AE49), - low: gpui::rgb(0x86B300), } } diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..d48dfba --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,82 @@ +use gpui::prelude::*; +use gpui::{px, Pixels}; + +use crate::theme::Theme; + +pub const CARD_RADIUS: Pixels = px(6.0); +pub const CARD_PADDING: Pixels = px(8.0); +pub const SECTION_GAP: Pixels = px(12.0); +pub const INSET_GAP: Pixels = px(8.0); +pub const ROOT_PADDING: Pixels = px(12.0); + +pub fn card_style(div: gpui::Div, theme: &Theme) -> gpui::Div { + div.bg(theme.card) + .border_1() + .border_color(theme.divider) + .rounded(CARD_RADIUS) + .p(CARD_PADDING) +} + +pub fn raised_style(div: gpui::Div, theme: &Theme) -> gpui::Div { + div.bg(theme.raised) + .border_1() + .border_color(theme.divider) + .rounded(CARD_RADIUS) + .p(CARD_PADDING) +} + +pub fn panel_style(div: gpui::Div, theme: &Theme) -> gpui::Div { + div.bg(theme.panel) + .border_1() + .border_color(theme.border) + .rounded(CARD_RADIUS) +} + +pub fn field_style(div: gpui::Div, theme: &Theme, focused: bool) -> gpui::Div { + div.bg(theme.field_bg) + .border_1() + .border_color(if focused { + theme.focus_ring + } else { + theme.field_border + }) + .rounded(CARD_RADIUS) + .px_2() + .py_1() +} + +pub fn divider_h(theme: &Theme) -> gpui::Div { + gpui::div().w_full().h(px(1.0)).bg(theme.divider) +} + +pub fn divider_v(theme: &Theme) -> gpui::Div { + gpui::div().h_full().w(px(1.0)).bg(theme.divider) +} + +pub fn section_header(label: &str, theme: &Theme) -> gpui::Div { + gpui::div() + .px_2() + .py_1() + .text_xs() + .text_color(theme.muted) + .child(label.to_uppercase()) +} + +pub fn priority_badge(priority: &str, theme: &Theme) -> gpui::Div { + let (bg, fg) = match priority { + "High" | "H" => (Theme::alpha(theme.high, 0.15), theme.high), + "Medium" | "M" => (Theme::alpha(theme.medium, 0.15), theme.medium), + "Low" | "L" => (Theme::alpha(theme.low, 0.15), theme.low), + _ => (gpui::rgba(0x00000000), theme.muted), + }; + + gpui::div() + .px_2() + .py(px(2.0)) + .rounded(px(4.0)) + .bg(bg) + .text_color(fg) + .text_xs() + .font_weight(gpui::FontWeight::MEDIUM) + .child(priority.to_string()) +} diff --git a/src/view/sidebar.rs b/src/view/sidebar.rs index 81a8986..f46c8bd 100644 --- a/src/view/sidebar.rs +++ b/src/view/sidebar.rs @@ -1,6 +1,6 @@ -use crate::components::{label::Label, panel::Panel}; use crate::models::{FilterState, ProjectTree}; use crate::theme::ActiveTheme; +use crate::ui::{divider_h, section_header}; use gpui::{Context, Div, Entity, IntoElement, Window, div, prelude::*, px}; #[derive(Debug, Clone)] @@ -100,9 +100,10 @@ impl Sidebar { .gap_2() .px_3() .py_1() + .rounded_sm() .cursor_pointer() .when(is_all_selected, |this| this.bg(theme.selection)) - .hover(|style| style.bg(theme.panel)) + .when(!is_all_selected, |this| this.hover(|style| style.bg(theme.hover))) .on_mouse_down( gpui::MouseButton::Left, cx.listener(|view, _event, window, cx| { @@ -149,9 +150,10 @@ impl Sidebar { .gap_1() .px_3() .py_1() + .rounded_sm() .cursor_pointer() .when(is_selected, |this| this.bg(theme.selection)) - .hover(|style| style.bg(theme.panel)) + .when(!is_selected, |this| this.hover(|style| style.bg(theme.hover))) .child(div().w(px(indent as f32))) .child( div() @@ -230,9 +232,10 @@ impl Sidebar { .gap_2() .px_3() .py_1() + .rounded_sm() .cursor_pointer() .when(is_active, |this| this.bg(theme.selection)) - .hover(|style| style.bg(theme.panel)) + .when(!is_active, |this| this.hover(|style| style.bg(theme.hover))) .on_mouse_down( gpui::MouseButton::Left, cx.listener(move |view, _event, window, cx| { @@ -279,115 +282,96 @@ impl Render for Sidebar { let has_project = filter.selected_project.is_some(); let has_tags = !filter.active_tags.is_empty(); - Panel::new("Sidebar").border(1.0).padding(0.0).child( - div() - .flex() - .flex_col() - .size_full() - .bg(theme.background) - .child( - div() - .flex() - .flex_col() - .flex_1() - .min_h_0() - .overflow_hidden() - .child( - div() - .flex_shrink_0() - .flex() - .items_center() - .justify_between() - .px_3() - .py_2() - .border_b_1() - .border_color(theme.border) - .child( - Label::new("PROJECTS") - .text_sm() - .font_weight(gpui::FontWeight::BOLD) - .text_color(theme.foreground), + div() + .flex() + .flex_col() + .size_full() + .child( + div() + .flex() + .flex_col() + .flex_1() + .min_h_0() + .overflow_hidden() + .child( + div() + .flex_shrink_0() + .flex() + .items_center() + .justify_between() + .px_2() + .py_2() + .child(section_header("Projects", &theme)) + .when(has_project, |this| { + this.child( + div() + .id("clear-project") + .text_xs() + .text_color(theme.muted) + .cursor_pointer() + .hover(|s| s.text_color(theme.accent)) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(|view, _, window, cx| { + view.handle_clear_project(window, cx); + }), + ) + .child("Clear"), ) - .when(has_project, |this| { - this.child( - div() - .id("clear-project") - .text_xs() - .text_color(theme.muted) - .cursor_pointer() - .hover(|s| s.text_color(theme.accent)) - .on_mouse_down( - gpui::MouseButton::Left, - cx.listener(|view, _, window, cx| { - view.handle_clear_project(window, cx); - }), - ) - .child("Clear"), - ) - }), - ) - .child( - div() - .id("sidebar-projects") - .flex_1() - .min_h_0() - .py_1() - .overflow_y_scroll() - .children(projects), - ), - ) - .child(div().flex_shrink_0().h_px().bg(theme.border)) - .child( - div() - .flex() - .flex_col() - .flex_1() - .min_h_0() - .overflow_hidden() - .child( - div() - .flex_shrink_0() - .flex() - .items_center() - .justify_between() - .px_3() - .py_2() - .border_b_1() - .border_color(theme.border) - .child( - Label::new("TAGS") - .text_sm() - .font_weight(gpui::FontWeight::BOLD) - .text_color(theme.foreground), + }), + ) + .child( + div() + .id("sidebar-projects") + .flex_1() + .min_h_0() + .overflow_y_scroll() + .children(projects), + ), + ) + .child(divider_h(&theme).my_1()) + .child( + div() + .flex() + .flex_col() + .flex_1() + .min_h_0() + .overflow_hidden() + .child( + div() + .flex_shrink_0() + .flex() + .items_center() + .justify_between() + .px_2() + .py_2() + .child(section_header("Tags", &theme)) + .when(has_tags, |this| { + this.child( + div() + .id("clear-tags") + .text_xs() + .text_color(theme.muted) + .cursor_pointer() + .hover(|s| s.text_color(theme.accent)) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(|view, _, window, cx| { + view.handle_clear_tags(window, cx); + }), + ) + .child("Clear"), ) - .when(has_tags, |this| { - this.child( - div() - .id("clear-tags") - .text_xs() - .text_color(theme.muted) - .cursor_pointer() - .hover(|s| s.text_color(theme.accent)) - .on_mouse_down( - gpui::MouseButton::Left, - cx.listener(|view, _, window, cx| { - view.handle_clear_tags(window, cx); - }), - ) - .child("Clear"), - ) - }), - ) - .child( - div() - .id("sidebar-tags") - .flex_1() - .min_h_0() - .py_1() - .overflow_y_scroll() - .children(tags), - ), - ), - ) + }), + ) + .child( + div() + .id("sidebar-tags") + .flex_1() + .min_h_0() + .overflow_y_scroll() + .children(tags), + ), + ) } } diff --git a/src/view/status_bar.rs b/src/view/status_bar.rs index a6b0949..068d0ab 100644 --- a/src/view/status_bar.rs +++ b/src/view/status_bar.rs @@ -1,6 +1,7 @@ use gpui::{Context, IntoElement, Render, Window, div, prelude::*, px}; use crate::theme::ActiveTheme; +use crate::ui::{card_style, CARD_RADIUS}; pub struct StatusBar { // TODO: Add vim_mode, sync_state, last_sync, etc. @@ -16,16 +17,14 @@ impl Render for StatusBar { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = cx.theme().clone(); - div() + card_style(div(), &theme) .flex() .items_center() .justify_between() .w_full() - .h(px(30.0)) - .px_4() + .h(px(28.0)) + .px_3() .py_1() - .bg(theme.panel) - .border_b_1() - .border_color(theme.border) + .rounded(CARD_RADIUS) } } diff --git a/src/view/task_table.rs b/src/view/task_table.rs index fe304ef..fdd8195 100644 --- a/src/view/task_table.rs +++ b/src/view/task_table.rs @@ -13,6 +13,7 @@ use crate::{ models::{DueFilter, FilterState, PriorityFilter, StatusFilter}, task::{self, TaskFilter, TaskService}, theme::{self, ActiveTheme}, + ui::priority_badge, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -604,15 +605,14 @@ impl TaskTable { let bar = gpui::div() .id("filter-bar") .flex() - .flex_wrap() - .gap_2() + .gap_3() .items_center() .px_4() .py_2() - .bg(theme.panel) .child( gpui::div() - .w(gpui::rems(14.0)) + .flex_1() + .min_w(gpui::rems(12.0)) .child(self.search_input.clone()), ) .child(self.status_dropdown.clone()) @@ -622,13 +622,14 @@ impl TaskTable { this.child( gpui::div() .id("clear-all-filters") + .flex_shrink_0() .px_2() .py_1() .rounded_md() .text_sm() .text_color(theme.error) .cursor_pointer() - .hover(|s| s.bg(theme.selection)) + .hover(|s| s.bg(theme.hover)) .on_mouse_down( gpui::MouseButton::Left, cx.listener(|table, _, _, cx| { @@ -702,10 +703,9 @@ impl TaskTable { .gap_2() .px_4() .py_2() - .border_t_1() - .border_color(theme.border) + .bg(theme.raised) .border_b_1() - .bg(theme.panel) + .border_color(theme.divider) .text_sm() .font_weight(gpui::FontWeight::MEDIUM) .child( @@ -756,12 +756,12 @@ impl TaskTable { .px_4() .py_1() .border_b_1() - .border_color(theme.border) + .border_color(theme.divider) .text_color(theme.foreground) .when(selected, |d| { d.bg(theme.selection).text_color(theme.selection_foreground) }) - .when(!selected, |d| d.hover(|s| s.bg(theme.panel))) + .when(!selected, |d| d.hover(|s| s.bg(theme.hover))) .cursor_pointer() .on_mouse_down( gpui::MouseButton::Left, @@ -802,11 +802,9 @@ impl TaskTable { components::label::Label::new(row.due.clone()).text_color(self.due_color(row, cx)), )) .child( - gpui::div().w(gpui::rems(5.0)).child( - components::label::Label::new(row.priority.clone()) - .text_color(self.priority_color(row, cx)) - .font_weight(gpui::FontWeight::BOLD), - ), + gpui::div() + .w(gpui::rems(5.0)) + .child(priority_badge(&row.priority, theme)), ) .child( gpui::div().w(gpui::rems(6.0)).child( @@ -831,8 +829,8 @@ impl TaskTable { .px_4() .py_2() .border_t_1() - .border_color(theme.border) - .bg(theme.panel) + .border_color(theme.divider) + .bg(theme.raised) .text_sm() .child( components::label::Label::new(format!( @@ -866,13 +864,15 @@ impl TaskTable { .px_2() .py_1() .rounded_md() + .border_1() + .border_color(theme.divider) .text_color(if can_prev { theme.foreground } else { - theme.muted + theme.disabled_fg }) .when(can_prev, |d| { - d.cursor_pointer().hover(|s| s.bg(theme.selection)) + d.cursor_pointer().hover(|s| s.bg(theme.hover)) }) .when(!can_prev, |d| d.cursor_not_allowed()) .on_mouse_down( @@ -887,13 +887,15 @@ impl TaskTable { .px_2() .py_1() .rounded_md() + .border_1() + .border_color(theme.divider) .text_color(if can_next { theme.foreground } else { - theme.muted + theme.disabled_fg }) .when(can_next, |d| { - d.cursor_pointer().hover(|s| s.bg(theme.selection)) + d.cursor_pointer().hover(|s| s.bg(theme.hover)) }) .when(!can_next, |d| d.cursor_not_allowed()) .on_mouse_down( @@ -924,7 +926,7 @@ impl gpui::Render for TaskTable { .size_full() .items_center() .justify_center() - .child(components::label::Label::new("Loading...").text_color(theme.text)); + .child(components::label::Label::new("Loading...").text_color(theme.foreground)); } let current_page = self.get_current_page_rows(); @@ -945,7 +947,7 @@ impl gpui::Render for TaskTable { .min_h_0() .overflow_hidden() .bg(theme.background) - .child(gpui::div().h(self.filter_bar_height)) + .child(gpui::div().h(self.filter_bar_height).mb_4()) .child(header) .child( gpui::div()