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