⎇ 8fdef818cc0524aa4bac39801cefd5960113dcdf
crates/modules/ui_engine/widgets/theme-switcher/src/lib.rs
History
Who touched what
raw
feat(yahweh-widget-theme-switcher): control para ciclar themes en runtime
1//! `yahweh-widget-theme-switcher` — botón clickable para ciclar
2//! entre los presets de `Theme`.
3//!
4//! El botón muestra el nombre del theme actual; al click avanza al
5//! siguiente preset según [`Theme::next_after`] (rotación circular
6//! sobre [`Theme::all`]).
7//!
8//! El cambio se aplica con `Theme::set(cx, ...)` que invalida el
9//! global y dispara redraws en todos los widgets que observan el
10//! theme via `cx.observe_global::<Theme>()`. Para widgets que NO
11//! observan el theme (ej. los themed wrappers de banner/card en su
12//! versión actual, que leen el theme dentro de `render`), basta con
13//! que el render se vuelva a invocar — esto sucede automáticamente
14//! tras `cx.set_global` que marca todos los views como dirty.
15//!
16//! # Uso
17//!
18//! ```ignore
19//! use yahweh_widget_theme_switcher::theme_switcher;
20//!
21//! // Adentro de Render::render:
22//! let switcher = theme_switcher(cx);
23//! header.child(switcher)
24//! ```
25
26#![forbid(unsafe_code)]
27
28use gpui::{div, prelude::*, px, App, ClickEvent, IntoElement, SharedString, Window};
29use yahweh_theme::Theme;
30
31/// Construye el switcher: una `Div` clickable con el nombre del
32/// theme actual + flecha indicadora. Al click rota al siguiente
33/// preset.
34///
35/// Estilo: padding consistente con el resto de los chrome controls
36/// del repo (`px(8/4)`), `bg(theme.bg_panel_alt)`, `text_color(fg_text)`.
37/// Sin border, hover sutil con `bg_row_hover`.
38///
39/// El handler del click usa `cx.update_global::<Theme>` para
40/// reemplazar el theme global; los widgets que leen
41/// `Theme::global` en su próximo render verán el nuevo.
42pub fn theme_switcher(cx: &mut App) -> impl IntoElement {
43 let theme = Theme::global(cx).clone();
44 let label = format!("Tema: {} ▸", theme.name);
45
46 div()
47 .id("yahweh-theme-switcher")
48 .px(px(8.))
49 .py(px(4.))
50 .bg(theme.bg_panel_alt.clone())
51 .text_color(theme.fg_text)
52 .text_size(px(11.))
53 .rounded(px(3.))
54 .hover(move |d| d.bg(theme.bg_row_hover))
55 .child(SharedString::from(label))
56 .on_click(|_event: &ClickEvent, _window: &mut Window, cx: &mut App| {
57 let current_name = Theme::global(cx).name;
58 let next = Theme::next_after(current_name);
59 Theme::set(cx, next);
60 })
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use gpui::TestAppContext;
67
68 #[gpui::test]
69 fn switcher_constructs_with_theme_installed(cx: &mut TestAppContext) {
70 cx.update(|cx| {
71 Theme::install_default(cx);
72 let _div = theme_switcher(cx);
73 // Smoke: si llegamos aquí sin panic, el constructor lee
74 // el global, deriva colors, y construye un Div.
75 });
76 }
77
78 #[gpui::test]
79 fn theme_set_changes_global(cx: &mut TestAppContext) {
80 cx.update(|cx| {
81 Theme::install_default(cx);
82 let initial_name = Theme::global(cx).name;
83 // Ciclo manual sin pasar por el handler del click.
84 let next = Theme::next_after(initial_name);
85 Theme::set(cx, next.clone());
86 let after = Theme::global(cx).name;
87 assert_eq!(after, next.name);
88 assert_ne!(after, initial_name, "el ciclo debe cambiar el name");
89 });
90 }
91}