feat(desktop): add semantic GPUI styling utilities

Co-authored-by: Maze <mazeincoding@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-01 21:11:56 +00:00
parent 400f097bec
commit 3e59d553a1
No known key found for this signature in database
9 changed files with 235 additions and 44 deletions

View File

@ -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),

View File

@ -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 {

View File

@ -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<dyn Fn(&mut Window, &mut App) + 'static>;
@ -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),

View File

@ -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)
}

View File

@ -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};

View File

@ -0,0 +1,3 @@
pub use gpui::prelude::*;
pub use super::semantic::{SemanticStyled, theme};

View File

@ -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<Self>) -> 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")

View File

@ -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<E: Styled> SemanticStyled for E {}
fn theme_radius(base: Rems, scale: f32) -> Rems {
gpui::rems(base.0 * scale)
}

View File

@ -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)),