diff --git a/apps/desktop/src/components/badge.rs b/apps/desktop/src/components/badge.rs index fde5c8f1..235f2a6c 100644 --- a/apps/desktop/src/components/badge.rs +++ b/apps/desktop/src/components/badge.rs @@ -1,9 +1,8 @@ use gpui::{ - App, FontWeight, IntoElement, RenderOnce, SharedString, Window, div, prelude::*, px, - transparent_black, + App, FontWeight, IntoElement, RenderOnce, SharedString, Window, div, px, transparent_black, }; -use crate::theme::ActiveTheme; +use crate::components::prelude::*; #[allow(dead_code)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -37,7 +36,8 @@ impl Badge { impl RenderOnce for Badge { fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let colors = window.theme().colors; + let theme = theme(window); + let colors = theme.colors; let transparent = transparent_black(); let (background, foreground, border) = match self.variant { BadgeVariant::Default => (colors.primary, colors.primary_foreground, transparent), diff --git a/apps/desktop/src/components/button.rs b/apps/desktop/src/components/button.rs index b08a3a72..03775d7c 100644 --- a/apps/desktop/src/components/button.rs +++ b/apps/desktop/src/components/button.rs @@ -1,9 +1,9 @@ use gpui::{ App, ClickEvent, ElementId, FontWeight, IntoElement, MouseButton, RenderOnce, SharedString, - Window, div, prelude::*, px, transparent_black, + Window, div, px, transparent_black, }; -use crate::theme::ActiveTheme; +use crate::components::prelude::*; #[allow(dead_code)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -115,7 +115,7 @@ impl Button { impl RenderOnce for Button { fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let theme = window.theme(); + let theme = theme(window); let colors = theme.colors; let transparent = transparent_black(); @@ -177,7 +177,7 @@ impl RenderOnce for Button { this.px(px(self.size.horizontal_padding())) }) .when(self.full_width, |this| this.w_full()) - .rounded(theme.radius) + .rounded_theme(theme) .border_1() .border_color(border) .bg(background) @@ -186,7 +186,7 @@ impl RenderOnce for Button { .font_weight(FontWeight::MEDIUM) .whitespace_nowrap() .tab_index(0) - .focus(|style| style.border_color(colors.ring)) + .focus(|style| style.border_ring(theme)) .child(self.label); if self.disabled { diff --git a/apps/desktop/src/components/context_menu.rs b/apps/desktop/src/components/context_menu.rs index 91caadc7..a9448bea 100644 --- a/apps/desktop/src/components/context_menu.rs +++ b/apps/desktop/src/components/context_menu.rs @@ -2,10 +2,10 @@ use std::rc::Rc; use gpui::{ AnyElement, App, ElementId, Entity, IntoElement, MouseButton, Pixels, Point, Render, - SharedString, Window, anchored, deferred, div, prelude::*, px, + SharedString, Window, anchored, deferred, div, px, }; -use crate::theme::ActiveTheme; +use crate::components::prelude::*; type SelectHandler = Rc; @@ -152,7 +152,7 @@ impl Render for ContextMenu { return div().into_any_element(); }; - let theme = window.theme(); + let theme = theme(window); let colors = theme.colors; let items = self .items @@ -168,11 +168,6 @@ impl Render for ContextMenu { variant, .. } => { - let foreground = if *variant == ContextMenuItemVariant::Destructive { - colors.destructive - } else { - colors.popover_foreground - }; let hover = if *variant == ContextMenuItemVariant::Destructive { colors.destructive.opacity(0.1) } else { @@ -189,7 +184,14 @@ impl Render for ContextMenu { .py(px(4.0)) .rounded(px(5.0)) .text_xs() - .text_color(foreground) + .when( + *variant == ContextMenuItemVariant::Destructive, + |this| this.text_destructive(theme), + ) + .when( + *variant != ContextMenuItemVariant::Destructive, + |this| this.text_popover_foreground(theme), + ) .when(*disabled, |this| this.opacity(0.5).cursor_not_allowed()) .when(!*disabled, |this| { this.cursor_default() @@ -210,7 +212,7 @@ impl Render for ContextMenu { this.child( div() .text_size(px(10.0)) - .text_color(colors.muted_foreground) + .text_muted_foreground(theme) .child(shortcut), ) }) @@ -220,14 +222,14 @@ impl Render for ContextMenu { .px(px(8.0)) .py(px(6.0)) .text_xs() - .text_color(colors.muted_foreground) + .text_muted_foreground(theme) .child(label.clone()) .into_any_element(), ContextMenuItem::Separator => div() .h(px(1.0)) .mx(px(-4.0)) .my(px(4.0)) - .bg(colors.border.opacity(0.5)) + .bg_border_subtle(theme) .into_any_element(), } }) @@ -245,11 +247,11 @@ impl Render for ContextMenu { .max_h(window.viewport_size().height - px(16.0)) .overflow_y_scroll() .p(px(4.0)) - .rounded(theme.radius) - .bg(colors.popover) - .text_color(colors.popover_foreground) + .rounded_theme(theme) + .bg_popover(theme) + .text_popover_foreground(theme) .border_1() - .border_color(colors.foreground.opacity(0.1)) + .border_foreground_muted(theme) .shadow_md() .on_mouse_down_out(cx.listener(|this, _, _, cx| this.close(cx))) .children(items), diff --git a/apps/desktop/src/components/label.rs b/apps/desktop/src/components/label.rs index 1150f82e..130dfe56 100644 --- a/apps/desktop/src/components/label.rs +++ b/apps/desktop/src/components/label.rs @@ -1,6 +1,6 @@ -use gpui::{App, FontWeight, IntoElement, RenderOnce, SharedString, Window, div, prelude::*}; +use gpui::{App, FontWeight, IntoElement, RenderOnce, SharedString, Window, div}; -use crate::theme::ActiveTheme; +use crate::components::prelude::*; #[derive(IntoElement)] pub(crate) struct Label { @@ -32,16 +32,13 @@ impl Label { impl RenderOnce for Label { fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let colors = window.theme().colors; + let theme = theme(window); div() .text_xs() .font_weight(FontWeight::MEDIUM) - .text_color(if self.muted { - colors.muted_foreground - } else { - colors.foreground - }) + .when(self.muted, |this| this.text_muted_foreground(theme)) + .when(!self.muted, |this| this.text_foreground(theme)) .when(self.disabled, |this| this.opacity(0.5)) .child(self.text) } diff --git a/apps/desktop/src/components/mod.rs b/apps/desktop/src/components/mod.rs index 17a9aa15..c63186ae 100644 --- a/apps/desktop/src/components/mod.rs +++ b/apps/desktop/src/components/mod.rs @@ -2,7 +2,9 @@ mod badge; mod button; mod context_menu; mod label; +pub(crate) mod prelude; mod resizable; +mod semantic; mod separator; pub(crate) use badge::{Badge, BadgeVariant}; diff --git a/apps/desktop/src/components/prelude.rs b/apps/desktop/src/components/prelude.rs new file mode 100644 index 00000000..def54362 --- /dev/null +++ b/apps/desktop/src/components/prelude.rs @@ -0,0 +1,3 @@ +pub use gpui::prelude::*; + +pub use super::semantic::{SemanticStyled, theme}; diff --git a/apps/desktop/src/components/resizable.rs b/apps/desktop/src/components/resizable.rs index 0d163418..a8123a2b 100644 --- a/apps/desktop/src/components/resizable.rs +++ b/apps/desktop/src/components/resizable.rs @@ -1,9 +1,9 @@ use gpui::{ AnyView, AppContext, ClickEvent, Context, DragMoveEvent, Entity, IntoElement, Render, Window, - div, prelude::*, px, relative, + div, px, relative, }; -use crate::theme::ActiveTheme; +use crate::components::prelude::*; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub(crate) enum Orientation { @@ -90,7 +90,7 @@ impl ResizablePanelGroup { impl Render for ResizablePanelGroup { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = window.theme().colors; + let theme = theme(window); let orientation = self.orientation; let first_fraction = self.fraction; @@ -105,8 +105,8 @@ impl Render for ResizablePanelGroup { div() .w(px(1.0)) .h_full() - .bg(colors.border) - .group_hover("resizable", |style| style.bg(colors.ring)), + .bg_border(theme) + .group_hover("resizable", |style| style.bg_ring(theme)), ) }) .when(orientation == Orientation::Vertical, |this| { @@ -114,8 +114,8 @@ impl Render for ResizablePanelGroup { div() .h(px(1.0)) .w_full() - .bg(colors.border) - .group_hover("resizable", |style| style.bg(colors.ring)), + .bg_border(theme) + .group_hover("resizable", |style| style.bg_ring(theme)), ) }) .group("resizable") diff --git a/apps/desktop/src/components/semantic.rs b/apps/desktop/src/components/semantic.rs new file mode 100644 index 00000000..51a50cd1 --- /dev/null +++ b/apps/desktop/src/components/semantic.rs @@ -0,0 +1,186 @@ +use gpui::{Rems, Styled, Window}; + +use crate::theme::{ActiveTheme, Theme}; + +/// Resolves the active theme for the current window. This is the sole bridge +/// from UI components to the runtime theme source. +pub fn theme(window: &Window) -> &'static Theme { + window.theme() +} + +/// Tailwind-like semantic styling helpers for GPUI [`Styled`] elements. +pub trait SemanticStyled: Styled { + fn bg_background(self, theme: &Theme) -> Self { + self.bg(theme.colors.background) + } + + fn bg_card(self, theme: &Theme) -> Self { + self.bg(theme.colors.card) + } + + fn bg_popover(self, theme: &Theme) -> Self { + self.bg(theme.colors.popover) + } + + fn bg_primary(self, theme: &Theme) -> Self { + self.bg(theme.colors.primary) + } + + fn bg_secondary(self, theme: &Theme) -> Self { + self.bg(theme.colors.secondary) + } + + fn bg_muted(self, theme: &Theme) -> Self { + self.bg(theme.colors.muted) + } + + fn bg_accent(self, theme: &Theme) -> Self { + self.bg(theme.colors.accent) + } + + fn bg_destructive(self, theme: &Theme) -> Self { + self.bg(theme.colors.destructive) + } + + fn bg_input(self, theme: &Theme) -> Self { + self.bg(theme.colors.input) + } + + fn bg_sidebar(self, theme: &Theme) -> Self { + self.bg(theme.colors.sidebar) + } + + fn bg_sidebar_primary(self, theme: &Theme) -> Self { + self.bg(theme.colors.sidebar_primary) + } + + fn bg_sidebar_accent(self, theme: &Theme) -> Self { + self.bg(theme.colors.sidebar_accent) + } + + fn bg_chart_1(self, theme: &Theme) -> Self { + self.bg(theme.colors.chart_1) + } + + fn bg_chart_2(self, theme: &Theme) -> Self { + self.bg(theme.colors.chart_2) + } + + fn bg_chart_3(self, theme: &Theme) -> Self { + self.bg(theme.colors.chart_3) + } + + fn bg_chart_4(self, theme: &Theme) -> Self { + self.bg(theme.colors.chart_4) + } + + fn bg_chart_5(self, theme: &Theme) -> Self { + self.bg(theme.colors.chart_5) + } + + fn bg_border_subtle(self, theme: &Theme) -> Self { + self.bg(theme.colors.border.opacity(0.5)) + } + + fn bg_border(self, theme: &Theme) -> Self { + self.bg(theme.colors.border) + } + + fn bg_ring(self, theme: &Theme) -> Self { + self.bg(theme.colors.ring) + } + + fn text_foreground(self, theme: &Theme) -> Self { + self.text_color(theme.colors.foreground) + } + + fn text_card_foreground(self, theme: &Theme) -> Self { + self.text_color(theme.colors.card_foreground) + } + + fn text_popover_foreground(self, theme: &Theme) -> Self { + self.text_color(theme.colors.popover_foreground) + } + + fn text_primary(self, theme: &Theme) -> Self { + self.text_color(theme.colors.primary) + } + + fn text_primary_foreground(self, theme: &Theme) -> Self { + self.text_color(theme.colors.primary_foreground) + } + + fn text_secondary_foreground(self, theme: &Theme) -> Self { + self.text_color(theme.colors.secondary_foreground) + } + + fn text_muted_foreground(self, theme: &Theme) -> Self { + self.text_color(theme.colors.muted_foreground) + } + + fn text_accent_foreground(self, theme: &Theme) -> Self { + self.text_color(theme.colors.accent_foreground) + } + + fn text_destructive(self, theme: &Theme) -> Self { + self.text_color(theme.colors.destructive) + } + + fn text_sidebar_foreground(self, theme: &Theme) -> Self { + self.text_color(theme.colors.sidebar_foreground) + } + + fn text_sidebar_primary_foreground(self, theme: &Theme) -> Self { + self.text_color(theme.colors.sidebar_primary_foreground) + } + + fn text_sidebar_accent_foreground(self, theme: &Theme) -> Self { + self.text_color(theme.colors.sidebar_accent_foreground) + } + + fn border_border(self, theme: &Theme) -> Self { + self.border_color(theme.colors.border) + } + + fn border_input(self, theme: &Theme) -> Self { + self.border_color(theme.colors.input) + } + + fn border_ring(self, theme: &Theme) -> Self { + self.border_color(theme.colors.ring) + } + + fn border_sidebar(self, theme: &Theme) -> Self { + self.border_color(theme.colors.sidebar_border) + } + + fn border_sidebar_ring(self, theme: &Theme) -> Self { + self.border_color(theme.colors.sidebar_ring) + } + + fn border_foreground_muted(self, theme: &Theme) -> Self { + self.border_color(theme.colors.foreground.opacity(0.1)) + } + + fn rounded_theme(self, theme: &Theme) -> Self { + self.rounded(theme.radius) + } + + fn rounded_sm(self, theme: &Theme) -> Self { + self.rounded(theme_radius(theme.radius, 0.6)) + } + + fn rounded_md(self, theme: &Theme) -> Self { + self.rounded(theme_radius(theme.radius, 0.8)) + } + + fn rounded_lg(self, theme: &Theme) -> Self { + self.rounded(theme.radius) + } +} + +impl SemanticStyled for E {} + +fn theme_radius(base: Rems, scale: f32) -> Rems { + gpui::rems(base.0 * scale) +} diff --git a/apps/desktop/src/components/separator.rs b/apps/desktop/src/components/separator.rs index 419f1964..76f05b1e 100644 --- a/apps/desktop/src/components/separator.rs +++ b/apps/desktop/src/components/separator.rs @@ -1,6 +1,7 @@ -use gpui::{App, IntoElement, RenderOnce, Window, div, prelude::*, px}; +use gpui::{App, IntoElement, RenderOnce, Window, div, px}; -use crate::{components::Orientation, theme::ActiveTheme}; +use crate::components::Orientation; +use crate::components::prelude::*; #[derive(IntoElement)] pub(crate) struct Separator { @@ -24,11 +25,11 @@ impl Separator { impl RenderOnce for Separator { fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let color = window.theme().colors.border.opacity(0.5); + let theme = theme(window); div() .flex_none() - .bg(color) + .bg_border_subtle(theme) .map(|this| match self.orientation { Orientation::Horizontal => this.w_full().h(px(1.0)), Orientation::Vertical => this.h_full().w(px(1.0)),