Skip to main content

native_theme_gpui/
lib.rs

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