⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd
crates/modules/ui_engine/libs/theme/src/lib.rs
Historia
Quién tocó qué
crudo
chore: monorepo inicial con arje + minga + yahweh absorbidos
1//! `yahweh_theme` — paleta de colores y backgrounds compartidos.
2//!
3//! El `Theme` se instala como `Global` de GPUI. Los widgets lo leen vía
4//! `cx.global::<Theme>()` durante su `render`, y se subscriben con
5//! `cx.observe_global::<Theme>(…)` para auto-redibujarse cuando cambia.
6//!
7//! Filosofía: el theme es **dato puro** (sin lógica de UI). No conoce a
8//! ningún widget concreto. Cada widget pide slots semánticos (panel_bg,
9//! row_hover, accent…) sin acoplarse a colores hex específicos.
10
11use gpui::{Background, Global, Hsla, hsla, linear_color_stop, linear_gradient};
12
13/// Paleta semántica del theme. Cada slot tiene un nombre funcional, no
14/// cromático — así los widgets piden "fondo de panel" sin acoplarse a
15/// "azul oscuro".
16///
17/// Convención de slots:
18/// - `bg_*` se devuelve como `Background` (soporta gradientes); los widgets
19/// lo pasan a `.bg(...)` directamente.
20/// - `fg_*`, `accent`, `border` son `Hsla` (colores planos para texto y
21/// ornamentos).
22/// - `bg_row_*` son `Hsla` porque las filas de una lista virtualizada se
23/// beneficiarían poco de un gradiente individual.
24#[derive(Clone, Debug)]
25pub struct Theme {
26 pub name: &'static str,
27 pub is_dark: bool,
28
29 // Fondos.
30 pub bg_app: Background,
31 pub bg_panel: Background,
32 pub bg_panel_alt: Background,
33 pub bg_row_hover: Hsla,
34 pub bg_row_active: Hsla,
35
36 // Foregrounds.
37 pub fg_text: Hsla,
38 pub fg_muted: Hsla,
39 pub fg_disabled: Hsla,
40
41 // Acentos y ornamentos.
42 pub accent: Hsla,
43 pub accent_strong: Hsla,
44 pub border: Hsla,
45 pub border_strong: Hsla,
46
47 /// Marker colors para indicar "este file está abierto en container N".
48 /// Paleta circular — el N-ésimo container usa `marker_palette[n % len]`.
49 pub marker_palette: Vec<Hsla>,
50}
51
52impl Global for Theme {}
53
54impl Theme {
55 pub fn global(cx: &gpui::App) -> &Self {
56 cx.global::<Self>()
57 }
58
59 /// Default — primer preset de `all()`. La Shell lo carga al boot si no
60 /// hay otro persistido.
61 pub fn install_default(cx: &mut gpui::App) {
62 cx.set_global(Self::nebula());
63 }
64
65 /// Reemplaza el theme global. GPUI notifica a todos los `observe_global`
66 /// suscriptores en el siguiente frame.
67 pub fn set(cx: &mut gpui::App, theme: Self) {
68 cx.set_global(theme);
69 }
70
71 /// Lista todos los presets en orden estable. Usado por el switcher para
72 /// ciclar y por el menú de "Tema" cuando lo agreguemos.
73 pub fn all() -> Vec<Self> {
74 vec![
75 Self::nebula(),
76 Self::aurora(),
77 Self::sunset(),
78 Self::flat_dark(),
79 Self::solarized_light(),
80 Self::high_contrast(),
81 ]
82 }
83
84 /// Devuelve el preset cuyo `name` matchea (case-insensitive). `None` si
85 /// el nombre no existe — útil para validar input de usuario al cargar
86 /// preferencias persistidas.
87 pub fn by_name(name: &str) -> Option<Self> {
88 Self::all()
89 .into_iter()
90 .find(|t| t.name.eq_ignore_ascii_case(name))
91 }
92
93 /// Próximo preset en la rotación de `all()`. Si `current` no está, se
94 /// vuelve al primero. La rotación es circular (último → primero).
95 pub fn next_after(current: &str) -> Self {
96 let all = Self::all();
97 let idx = all.iter().position(|t| t.name == current);
98 match idx {
99 Some(i) => all[(i + 1) % all.len()].clone(),
100 None => all[0].clone(),
101 }
102 }
103
104 // =====================================================================
105 // Presets
106 // =====================================================================
107
108 /// **Nebula** — default. Gradiente vertical violáceo profundo → teal
109 /// medianoche. Pensado para sentirse moderno y descansado de noche.
110 pub fn nebula() -> Self {
111 let bg_app = linear_gradient(
112 165.0,
113 linear_color_stop(hsla(265.0 / 360.0, 0.38, 0.07, 1.0), 0.0),
114 linear_color_stop(hsla(195.0 / 360.0, 0.42, 0.09, 1.0), 1.0),
115 );
116 let bg_panel = linear_gradient(
117 165.0,
118 linear_color_stop(hsla(245.0 / 360.0, 0.28, 0.10, 1.0), 0.0),
119 linear_color_stop(hsla(210.0 / 360.0, 0.30, 0.12, 1.0), 1.0),
120 );
121 let bg_panel_alt = linear_gradient(
122 165.0,
123 linear_color_stop(hsla(255.0 / 360.0, 0.25, 0.13, 1.0), 0.0),
124 linear_color_stop(hsla(220.0 / 360.0, 0.27, 0.14, 1.0), 1.0),
125 );
126
127 Self {
128 name: "Nebula",
129 is_dark: true,
130 bg_app,
131 bg_panel,
132 bg_panel_alt,
133 bg_row_hover: hsla(220.0 / 360.0, 0.30, 0.20, 0.45),
134 bg_row_active: hsla(280.0 / 360.0, 0.55, 0.28, 0.65),
135 fg_text: hsla(210.0 / 360.0, 0.35, 0.88, 1.0),
136 fg_muted: hsla(215.0 / 360.0, 0.22, 0.58, 1.0),
137 fg_disabled: hsla(215.0 / 360.0, 0.10, 0.40, 1.0),
138 accent: hsla(280.0 / 360.0, 0.65, 0.65, 1.0),
139 accent_strong: hsla(285.0 / 360.0, 0.78, 0.74, 1.0),
140 border: hsla(225.0 / 360.0, 0.20, 0.22, 1.0),
141 border_strong: hsla(280.0 / 360.0, 0.40, 0.45, 1.0),
142 marker_palette: vec![
143 hsla(280.0 / 360.0, 0.65, 0.55, 0.45),
144 hsla(195.0 / 360.0, 0.65, 0.50, 0.45),
145 hsla(35.0 / 360.0, 0.75, 0.55, 0.45),
146 hsla(135.0 / 360.0, 0.55, 0.50, 0.45),
147 hsla(0.0, 0.60, 0.55, 0.45),
148 ],
149 }
150 }
151
152 /// **Aurora** — verdes-cian-azul, evoca aurora boreal. Más frío que
153 /// Nebula, contraste alto.
154 pub fn aurora() -> Self {
155 let bg_app = linear_gradient(
156 190.0,
157 linear_color_stop(hsla(170.0 / 360.0, 0.45, 0.06, 1.0), 0.0),
158 linear_color_stop(hsla(220.0 / 360.0, 0.50, 0.09, 1.0), 1.0),
159 );
160 let bg_panel = linear_gradient(
161 190.0,
162 linear_color_stop(hsla(165.0 / 360.0, 0.32, 0.10, 1.0), 0.0),
163 linear_color_stop(hsla(215.0 / 360.0, 0.36, 0.12, 1.0), 1.0),
164 );
165 let bg_panel_alt = linear_gradient(
166 190.0,
167 linear_color_stop(hsla(170.0 / 360.0, 0.30, 0.13, 1.0), 0.0),
168 linear_color_stop(hsla(220.0 / 360.0, 0.32, 0.15, 1.0), 1.0),
169 );
170
171 Self {
172 name: "Aurora",
173 is_dark: true,
174 bg_app,
175 bg_panel,
176 bg_panel_alt,
177 bg_row_hover: hsla(180.0 / 360.0, 0.40, 0.22, 0.50),
178 bg_row_active: hsla(160.0 / 360.0, 0.55, 0.30, 0.65),
179 fg_text: hsla(180.0 / 360.0, 0.20, 0.92, 1.0),
180 fg_muted: hsla(185.0 / 360.0, 0.18, 0.62, 1.0),
181 fg_disabled: hsla(185.0 / 360.0, 0.10, 0.40, 1.0),
182 accent: hsla(150.0 / 360.0, 0.70, 0.55, 1.0),
183 accent_strong: hsla(160.0 / 360.0, 0.85, 0.65, 1.0),
184 border: hsla(195.0 / 360.0, 0.25, 0.20, 1.0),
185 border_strong: hsla(160.0 / 360.0, 0.55, 0.45, 1.0),
186 marker_palette: vec![
187 hsla(150.0 / 360.0, 0.75, 0.50, 0.45),
188 hsla(195.0 / 360.0, 0.70, 0.50, 0.45),
189 hsla(225.0 / 360.0, 0.70, 0.55, 0.45),
190 hsla(85.0 / 360.0, 0.65, 0.50, 0.45),
191 hsla(330.0 / 360.0, 0.65, 0.55, 0.45),
192 ],
193 }
194 }
195
196 /// **Sunset** — naranjas-rosas-violetas profundos. Cálido, alto contraste
197 /// con texto claro.
198 pub fn sunset() -> Self {
199 let bg_app = linear_gradient(
200 170.0,
201 linear_color_stop(hsla(20.0 / 360.0, 0.50, 0.08, 1.0), 0.0),
202 linear_color_stop(hsla(310.0 / 360.0, 0.45, 0.10, 1.0), 1.0),
203 );
204 let bg_panel = linear_gradient(
205 170.0,
206 linear_color_stop(hsla(15.0 / 360.0, 0.32, 0.12, 1.0), 0.0),
207 linear_color_stop(hsla(315.0 / 360.0, 0.30, 0.13, 1.0), 1.0),
208 );
209 let bg_panel_alt = linear_gradient(
210 170.0,
211 linear_color_stop(hsla(20.0 / 360.0, 0.30, 0.15, 1.0), 0.0),
212 linear_color_stop(hsla(320.0 / 360.0, 0.28, 0.16, 1.0), 1.0),
213 );
214
215 Self {
216 name: "Sunset",
217 is_dark: true,
218 bg_app,
219 bg_panel,
220 bg_panel_alt,
221 bg_row_hover: hsla(25.0 / 360.0, 0.40, 0.25, 0.45),
222 bg_row_active: hsla(5.0 / 360.0, 0.55, 0.32, 0.65),
223 fg_text: hsla(30.0 / 360.0, 0.30, 0.92, 1.0),
224 fg_muted: hsla(25.0 / 360.0, 0.20, 0.62, 1.0),
225 fg_disabled: hsla(25.0 / 360.0, 0.10, 0.42, 1.0),
226 accent: hsla(15.0 / 360.0, 0.78, 0.62, 1.0),
227 accent_strong: hsla(355.0 / 360.0, 0.85, 0.68, 1.0),
228 border: hsla(15.0 / 360.0, 0.25, 0.25, 1.0),
229 border_strong: hsla(355.0 / 360.0, 0.55, 0.45, 1.0),
230 marker_palette: vec![
231 hsla(15.0 / 360.0, 0.80, 0.55, 0.45),
232 hsla(310.0 / 360.0, 0.65, 0.55, 0.45),
233 hsla(45.0 / 360.0, 0.80, 0.55, 0.45),
234 hsla(285.0 / 360.0, 0.65, 0.60, 0.45),
235 hsla(355.0 / 360.0, 0.70, 0.55, 0.45),
236 ],
237 }
238 }
239
240 /// **Flat Dark** — sin gradientes, paleta cool gris-azulado. Para quien
241 /// prefiere monocromía. Útil para contrastar visualmente con los temas
242 /// de gradiente.
243 pub fn flat_dark() -> Self {
244 let bg_app: Background = hsla(220.0 / 360.0, 0.15, 0.09, 1.0).into();
245 let bg_panel: Background = hsla(220.0 / 360.0, 0.15, 0.12, 1.0).into();
246 let bg_panel_alt: Background = hsla(220.0 / 360.0, 0.15, 0.14, 1.0).into();
247 Self {
248 name: "Flat Dark",
249 is_dark: true,
250 bg_app,
251 bg_panel,
252 bg_panel_alt,
253 bg_row_hover: hsla(220.0 / 360.0, 0.20, 0.20, 1.0),
254 bg_row_active: hsla(220.0 / 360.0, 0.40, 0.30, 1.0),
255 fg_text: hsla(210.0 / 360.0, 0.20, 0.85, 1.0),
256 fg_muted: hsla(215.0 / 360.0, 0.15, 0.55, 1.0),
257 fg_disabled: hsla(215.0 / 360.0, 0.10, 0.40, 1.0),
258 accent: hsla(210.0 / 360.0, 0.70, 0.55, 1.0),
259 accent_strong: hsla(210.0 / 360.0, 0.85, 0.65, 1.0),
260 border: hsla(220.0 / 360.0, 0.15, 0.20, 1.0),
261 border_strong: hsla(220.0 / 360.0, 0.30, 0.35, 1.0),
262 marker_palette: vec![
263 hsla(210.0 / 360.0, 0.65, 0.55, 0.40),
264 hsla(160.0 / 360.0, 0.55, 0.50, 0.40),
265 hsla(30.0 / 360.0, 0.75, 0.55, 0.40),
266 hsla(0.0, 0.55, 0.55, 0.40),
267 ],
268 }
269 }
270
271 /// **Solarized Light** — preset claro inspirado en la paleta clásica de
272 /// Schoonover. Sin gradientes (en light un gradiente sutil pasa
273 /// desapercibido y solo introduce ruido).
274 pub fn solarized_light() -> Self {
275 let bg_app: Background = hsla(44.0 / 360.0, 0.87, 0.94, 1.0).into();
276 let bg_panel: Background = hsla(46.0 / 360.0, 0.42, 0.88, 1.0).into();
277 let bg_panel_alt: Background = hsla(46.0 / 360.0, 0.42, 0.92, 1.0).into();
278 Self {
279 name: "Solarized Light",
280 is_dark: false,
281 bg_app,
282 bg_panel,
283 bg_panel_alt,
284 bg_row_hover: hsla(46.0 / 360.0, 0.45, 0.80, 0.65),
285 bg_row_active: hsla(45.0 / 360.0, 0.55, 0.72, 0.85),
286 fg_text: hsla(196.0 / 360.0, 0.13, 0.30, 1.0),
287 fg_muted: hsla(196.0 / 360.0, 0.13, 0.45, 1.0),
288 fg_disabled: hsla(196.0 / 360.0, 0.10, 0.62, 1.0),
289 accent: hsla(205.0 / 360.0, 0.69, 0.42, 1.0),
290 accent_strong: hsla(205.0 / 360.0, 0.82, 0.38, 1.0),
291 border: hsla(46.0 / 360.0, 0.30, 0.78, 1.0),
292 border_strong: hsla(205.0 / 360.0, 0.40, 0.55, 1.0),
293 marker_palette: vec![
294 hsla(205.0 / 360.0, 0.69, 0.42, 0.30),
295 hsla(175.0 / 360.0, 0.74, 0.32, 0.30),
296 hsla(45.0 / 360.0, 1.00, 0.36, 0.30),
297 hsla(331.0 / 360.0, 0.74, 0.42, 0.30),
298 hsla(18.0 / 360.0, 0.89, 0.40, 0.30),
299 ],
300 }
301 }
302
303 /// **High Contrast** — accesibilidad. Negro puro con texto blanco y
304 /// ornamentos amarillo/verde fuertes. Suficientemente diferente para
305 /// notar inmediatamente al usar el switcher.
306 pub fn high_contrast() -> Self {
307 let bg_app: Background = hsla(0.0, 0.0, 0.0, 1.0).into();
308 let bg_panel: Background = hsla(0.0, 0.0, 0.05, 1.0).into();
309 let bg_panel_alt: Background = hsla(0.0, 0.0, 0.10, 1.0).into();
310 Self {
311 name: "High Contrast",
312 is_dark: true,
313 bg_app,
314 bg_panel,
315 bg_panel_alt,
316 bg_row_hover: hsla(60.0 / 360.0, 1.00, 0.50, 0.35),
317 bg_row_active: hsla(120.0 / 360.0, 1.00, 0.40, 0.55),
318 fg_text: hsla(0.0, 0.0, 1.0, 1.0),
319 fg_muted: hsla(0.0, 0.0, 0.75, 1.0),
320 fg_disabled: hsla(0.0, 0.0, 0.50, 1.0),
321 accent: hsla(60.0 / 360.0, 1.00, 0.60, 1.0),
322 accent_strong: hsla(60.0 / 360.0, 1.00, 0.75, 1.0),
323 border: hsla(0.0, 0.0, 0.30, 1.0),
324 border_strong: hsla(60.0 / 360.0, 1.00, 0.60, 1.0),
325 marker_palette: vec![
326 hsla(60.0 / 360.0, 1.00, 0.55, 0.50),
327 hsla(120.0 / 360.0, 1.00, 0.50, 0.50),
328 hsla(180.0 / 360.0, 1.00, 0.55, 0.50),
329 hsla(0.0, 1.00, 0.60, 0.50),
330 hsla(300.0 / 360.0, 1.00, 0.65, 0.50),
331 ],
332 }
333 }
334}