tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 614Markdown 274TOML 149RON 310606 Sutichaykuna

⎇ main widgets/theme-switcher/src/lib.rs Rust · 194 siq'ikuna · 6.2 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! `llimphi-widget-theme-switcher` — botón que rota los presets de
2//! [`llimphi_theme::Theme`].
3//!
4//! Análogo Llimphi del `nahual-widget-theme-switcher` GPUI. Diferencia
5//! estructural: GPUI lleva el theme en un `Global` y el switcher lo
6//! reemplaza con `cx.set_global`; Llimphi no tiene globals — el caller
7//! guarda el theme en su `Model` y reasigna en su `update`. El widget
8//! sólo emite `on_change(next_theme)` cuando el botón se clickea, donde
9//! `next_theme` es el siguiente preset de [`Theme::next_after`].
10//!
11//! El label del botón muestra el nombre del preset actual con un signo
12//! de rotación (`Tema: Dark ▸`). Los colores salen del `Theme` actual
13//! para que el switcher sea coherente con el resto de la UI.
14//!
15//! # Uso
16//!
17//! ```ignore
18//! use llimphi_widget_theme_switcher::theme_switcher_view;
19//!
20//! // En App::view:
21//! let switcher = theme_switcher_view(&model.theme, Msg::ChangeTheme);
22//! ```
23//!
24//! `Msg::ChangeTheme(Theme)` lo define la app; en `update`:
25//!
26//! ```ignore
27//! Msg::ChangeTheme(t) => { model.theme = t; }
28//! ```
29
30#![forbid(unsafe_code)]
31
32
33// Sin `std`: este widget no usa nada del sistema —sólo `alloc` para
34// `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula
35// WASM o al kernel. Antes enlazaba `std` por omisión (W4.6).
36#![no_std]
37
38extern crate alloc;
39
40use alloc::format;
41use llimphi_theme::Theme;
42use llimphi_compositor::llimphi_layout::taffy::{
43    prelude::{length, percent, AlignItems, JustifyContent, Size, Style},
44    Rect,
45};
46use llimphi_compositor::peniko::Color;
47use llimphi_compositor::llimphi_text::Alignment;
48use llimphi_compositor::View;
49
50/// Paleta del switcher. Por default replica el patrón del switcher de
51/// nahual: `bg_panel_alt` + hover `bg_row_hover`, texto `fg_text`.
52#[derive(Debug, Clone, Copy)]
53pub struct ThemeSwitcherPalette {
54    pub bg: Color,
55    pub bg_hover: Color,
56    pub fg: Color,
57    pub radius: f64,
58}
59
60impl Default for ThemeSwitcherPalette {
61    fn default() -> Self {
62        Self::from_theme(&Theme::dark())
63    }
64}
65
66impl ThemeSwitcherPalette {
67    pub fn from_theme(t: &Theme) -> Self {
68        Self {
69            bg: t.bg_panel_alt,
70            bg_hover: t.bg_row_hover,
71            fg: t.fg_text,
72            radius: 3.0,
73        }
74    }
75}
76
77/// Compone el switcher: chip con texto `Tema: <nombre> ▸`. Click rota
78/// al siguiente preset y emite `on_change(next)`.
79///
80/// Toma el `current` por referencia para no clonar el `Theme` entero
81/// (es `Copy`, pero la API se mantiene consistente con `Palette::from_theme`).
82/// La paleta se deriva del `current` para que el chip use el mismo set
83/// de colores que el resto de la UI.
84pub fn theme_switcher_view<Msg: Clone + 'static>(
85    current: &Theme,
86    on_change: impl Fn(Theme) -> Msg,
87) -> View<Msg> {
88    let palette = ThemeSwitcherPalette::from_theme(current);
89    theme_switcher_styled(current, &palette, on_change)
90}
91
92/// Variante con paleta explícita — útil cuando la app quiere un look
93/// distinto al default (botón destacado, accent del switcher fijo, etc.).
94pub fn theme_switcher_styled<Msg: Clone + 'static>(
95    current: &Theme,
96    palette: &ThemeSwitcherPalette,
97    on_change: impl Fn(Theme) -> Msg,
98) -> View<Msg> {
99    let next = Theme::next_after(current.name);
100    let label = format!("Tema: {} ▸", current.name);
101
102    View::new(Style {
103        size: Size {
104            width: length(140.0_f32),
105            height: length(26.0_f32),
106        },
107        padding: Rect {
108            left: length(10.0_f32),
109            right: length(10.0_f32),
110            top: length(0.0_f32),
111            bottom: length(0.0_f32),
112        },
113        align_items: Some(AlignItems::CENTER),
114        justify_content: Some(JustifyContent::CENTER),
115        ..Default::default()
116    })
117    .fill(palette.bg)
118    .hover_fill(palette.bg_hover)
119    .radius(palette.radius)
120    .text_aligned(label, 11.0, palette.fg, Alignment::Start)
121    // El `▸` del label es decoración; el nombre accesible dice qué hace.
122    .role(llimphi_compositor::Role::Button)
123    .aria_label(format!("Cambiar tema (actual: {})", current.name))
124    .on_click(on_change(next))
125}
126
127/// Variante de tamaño flexible — toma el ancho dado por el padre y se
128/// adapta al alto natural del slot. Útil dentro de toolbars con flexbox.
129pub fn theme_switcher_flex<Msg: Clone + 'static>(
130    current: &Theme,
131    palette: &ThemeSwitcherPalette,
132    on_change: impl Fn(Theme) -> Msg,
133) -> View<Msg> {
134    let next = Theme::next_after(current.name);
135    let label = format!("Tema: {} ▸", current.name);
136
137    View::new(Style {
138        size: Size {
139            width: percent(1.0_f32),
140            height: length(26.0_f32),
141        },
142        padding: Rect {
143            left: length(10.0_f32),
144            right: length(10.0_f32),
145            top: length(0.0_f32),
146            bottom: length(0.0_f32),
147        },
148        align_items: Some(AlignItems::CENTER),
149        ..Default::default()
150    })
151    .fill(palette.bg)
152    .hover_fill(palette.bg_hover)
153    .radius(palette.radius)
154    .text_aligned(label, 11.0, palette.fg, Alignment::Start)
155    // El `▸` del label es decoración; el nombre accesible dice qué hace.
156    .role(llimphi_compositor::Role::Button)
157    .aria_label(format!("Cambiar tema (actual: {})", current.name))
158    .on_click(on_change(next))
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[derive(Debug, Clone, PartialEq)]
166    enum Msg {
167        Change(&'static str),
168    }
169
170    #[test]
171    fn switcher_constructs_with_a_default_theme() {
172        let t = Theme::dark();
173        let _v = theme_switcher_view::<Msg>(&t, |th| Msg::Change(th.name));
174        // Si el constructor no panicó, el widget queda armado.
175    }
176
177    #[test]
178    fn palette_from_theme_matches_panel_alt_slots() {
179        let t = Theme::dark();
180        let p = ThemeSwitcherPalette::from_theme(&t);
181        // No comparamos por igualdad de Color (no implementa PartialEq);
182        // sí garantizamos que la paleta derivó del theme — radius default.
183        assert_eq!(p.radius, 3.0);
184    }
185
186    #[test]
187    fn on_change_receives_the_next_preset() {
188        // Verificación funcional independiente: la rotación que verá el
189        // handler coincide con `Theme::next_after`.
190        let current = Theme::dark();
191        let expected_next = Theme::next_after(current.name).name;
192        assert_eq!(expected_next, "Light");
193    }
194}