Skip to main content

native_theme_gpui/
lib.rs

1//! gpui toolkit connector for native-theme.
2//!
3//! Maps [`native_theme::NativeTheme`] data to gpui-component's theming system.
4//!
5//! # Overview
6//!
7//! This crate provides a thin mapping layer that converts native-theme's
8//! platform-agnostic color, font, and geometry data into gpui-component's
9//! `Theme` type. No intermediate types are introduced -- the mapping goes
10//! directly from `ThemeVariant` fields to gpui-component types.
11//!
12//! # Usage
13//!
14//! ```ignore
15//! use native_theme::NativeTheme;
16//! use native_theme_gpui::to_theme;
17//!
18//! let nt = NativeTheme::preset("default").unwrap();
19//! let variant = nt.pick_variant(false).unwrap();
20//! let theme = to_theme(variant, "Default", false);
21//! ```
22
23pub mod colors;
24pub mod config;
25pub mod derive;
26pub mod icons;
27
28use gpui_component::theme::{Theme, ThemeMode};
29use native_theme::{NativeTheme, ThemeVariant};
30
31/// Pick a theme variant based on the requested mode.
32///
33/// If `is_dark` is true, returns the dark variant (falling back to light).
34/// If `is_dark` is false, returns the light variant (falling back to dark).
35#[deprecated(since = "0.3.2", note = "Use NativeTheme::pick_variant() instead")]
36#[allow(deprecated)]
37pub fn pick_variant(theme: &NativeTheme, is_dark: bool) -> Option<&ThemeVariant> {
38    theme.pick_variant(is_dark)
39}
40
41/// Convert a [`ThemeVariant`] into a gpui-component [`Theme`].
42///
43/// Builds a complete Theme by:
44/// 1. Mapping all 108 ThemeColor fields via [`colors::to_theme_color`]
45/// 2. Building a ThemeConfig from fonts/geometry via [`config::to_theme_config`]
46/// 3. Constructing the Theme from the ThemeColor and applying the config
47pub fn to_theme(variant: &ThemeVariant, name: &str, is_dark: bool) -> Theme {
48    let theme_color = colors::to_theme_color(variant);
49    let mode = if is_dark {
50        ThemeMode::Dark
51    } else {
52        ThemeMode::Light
53    };
54    let theme_config = config::to_theme_config(variant, name, mode);
55
56    // gpui-component's `apply_config` sets non-color fields we need: font_family,
57    // font_size, radius, shadow, mode, light_theme/dark_theme Rc, and highlight_theme.
58    // However, `ThemeColor::apply_config` (called internally) overwrites ALL color
59    // fields with defaults, since our ThemeConfig has no explicit color overrides.
60    // We restore our carefully-mapped colors after. This is a known gpui-component
61    // API limitation -- there is no way to apply only non-color config fields.
62    let mut theme = Theme::from(&theme_color);
63    theme.apply_config(&theme_config.into());
64    theme.colors = theme_color;
65    theme
66}
67
68#[cfg(test)]
69#[allow(deprecated)]
70mod tests {
71    use super::*;
72    use native_theme::Rgba;
73
74    #[test]
75    fn pick_variant_light_first() {
76        let mut theme = NativeTheme::new("Test");
77        let mut light = ThemeVariant::default();
78        light.colors.background = Some(Rgba::rgb(255, 255, 255));
79        theme.light = Some(light);
80
81        let picked = pick_variant(&theme, false);
82        assert!(picked.is_some());
83        assert_eq!(
84            picked.unwrap().colors.background,
85            Some(Rgba::rgb(255, 255, 255))
86        );
87    }
88
89    #[test]
90    fn pick_variant_dark_first() {
91        let mut theme = NativeTheme::new("Test");
92        let mut dark = ThemeVariant::default();
93        dark.colors.background = Some(Rgba::rgb(30, 30, 30));
94        theme.dark = Some(dark);
95
96        let picked = pick_variant(&theme, true);
97        assert!(picked.is_some());
98        assert_eq!(
99            picked.unwrap().colors.background,
100            Some(Rgba::rgb(30, 30, 30))
101        );
102    }
103
104    #[test]
105    fn pick_variant_fallback() {
106        let mut theme = NativeTheme::new("Test");
107        let mut light = ThemeVariant::default();
108        light.colors.background = Some(Rgba::rgb(255, 255, 255));
109        theme.light = Some(light);
110        // No dark variant -- requesting dark should fall back to light
111        let picked = pick_variant(&theme, true);
112        assert!(picked.is_some());
113    }
114
115    #[test]
116    fn pick_variant_empty_returns_none() {
117        let theme = NativeTheme::new("Empty");
118        assert!(pick_variant(&theme, false).is_none());
119        assert!(pick_variant(&theme, true).is_none());
120    }
121
122    #[test]
123    fn to_theme_produces_valid_theme() {
124        let mut variant = ThemeVariant::default();
125        variant.colors.background = Some(Rgba::rgb(255, 255, 255));
126        variant.colors.foreground = Some(Rgba::rgb(0, 0, 0));
127        variant.colors.accent = Some(Rgba::rgb(0, 120, 215));
128        variant.fonts.family = Some("Inter".into());
129        variant.fonts.size = Some(14.0);
130        variant.geometry.radius = Some(4.0);
131
132        let theme = to_theme(&variant, "Test", false);
133
134        // Theme should have the correct mode
135        assert!(!theme.is_dark());
136    }
137}