tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ main modules/allichay/examples/settings_demo.rs Rust · 314 líneas · 11.2 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! Demo del renderizador `allichay`: una config de juguete editable.
2//!
3//! Ejercita el módulo sin levantar ninguna app de dominio: un `DemoConfig`
4//! implementa [`allichay::Configurable`], el renderizador lo pinta con dientes
5//! y controles, y cada cambio se aplica + se loguea por consola.
6//!
7//! ```bash
8//! cargo run -p llimphi-module-allichay --example settings_demo --release
9//! ```
10
11use allichay::{Column, Configurable, EnumOption, Field, FieldPath, FieldValue, Schema, Section};
12use llimphi_module_allichay::{allichay_view, AllichayMsg, AllichayState};
13use llimphi_theme::Theme;
14use llimphi_ui::llimphi_layout::taffy::prelude::{percent, Size, Style};
15use llimphi_ui::{App, Handle, KeyEvent, View};
16
17/// Config de juguete con un campo de cada tipo.
18#[derive(Clone)]
19struct DemoConfig {
20    oscuro: bool,
21    gap: f64,
22    columnas: i64,
23    idioma: String,
24    acento: [u8; 4],
25    nombre: String,
26    auto: bool,
27    /// Lista de textos (ejercita [`Control::List`]).
28    rutas: Vec<String>,
29    /// Tabla (etiqueta, comando) — ejercita [`Control::Table`].
30    accesos: Vec<(String, String)>,
31}
32
33impl Default for DemoConfig {
34    fn default() -> Self {
35        Self {
36            oscuro: true,
37            gap: 8.0,
38            columnas: 2,
39            idioma: "es-PE".into(),
40            acento: [92, 143, 235, 255],
41            nombre: "mi escritorio".into(),
42            auto: false,
43            rutas: vec!["~/proyectos".into(), "/usr/share".into()],
44            accesos: vec![
45                ("Editor".into(), "nada".into()),
46                ("Terminal".into(), "alacritty".into()),
47            ],
48        }
49    }
50}
51
52impl Configurable for DemoConfig {
53    fn schema(&self) -> Schema {
54        Schema::new()
55            .section(
56                Section::new("apariencia", "Apariencia")
57                    .icon("◐")
58                    .help("Cómo se ve el escritorio")
59                    .field(Field::toggle("oscuro", "Modo oscuro", self.oscuro))
60                    .field(
61                        Field::slider("gap", "Margen entre ventanas", self.gap, 0.0, 32.0, 1.0)
62                            .help("En píxeles"),
63                    )
64                    .field(Field::color("acento", "Color de acento", self.acento))
65                    .subsection(
66                        Section::new("teselado", "Teselado").field(Field::slider_int(
67                            "columnas",
68                            "Columnas",
69                            self.columnas,
70                            1,
71                            6,
72                        )),
73                    ),
74            )
75            .section(
76                Section::new("general", "General")
77                    .icon("≡")
78                    .field(Field::dropdown(
79                        "idioma",
80                        "Idioma",
81                        self.idioma.clone(),
82                        vec![
83                            EnumOption::new("es-PE", "Español"),
84                            EnumOption::new("en-US", "English"),
85                            EnumOption::new("qu-PE", "Runasimi"),
86                        ],
87                    ))
88                    .field(Field::text("nombre", "Nombre del equipo", self.nombre.clone()))
89                    .field(Field::toggle("auto", "Arrancar al inicio", self.auto)),
90            )
91            .section(
92                Section::new("agregados", "Listas y tablas")
93                    .icon("≣")
94                    .help("Los controles v2")
95                    .field(Field::list(
96                        "rutas",
97                        "Rutas de búsqueda",
98                        self.rutas.clone(),
99                        "ruta",
100                    ))
101                    .field(Field::table(
102                        "accesos",
103                        "Accesos directos",
104                        vec![Column::new("label", "Nombre"), Column::new("cmd", "Comando")],
105                        self.accesos
106                            .iter()
107                            .map(|(l, c)| vec![l.clone(), c.clone()])
108                            .collect(),
109                    )),
110            )
111    }
112
113    fn apply(
114        &mut self,
115        path: &FieldPath,
116        value: FieldValue,
117    ) -> Result<(), allichay::AllichayError> {
118        match path.leaf() {
119            Some("oscuro") => self.oscuro = value.as_bool().unwrap_or(self.oscuro),
120            Some("gap") => self.gap = value.as_float().unwrap_or(self.gap),
121            Some("columnas") => self.columnas = value.as_int().unwrap_or(self.columnas),
122            Some("idioma") => {
123                if let Some(s) = value.as_str() {
124                    self.idioma = s.to_string();
125                }
126            }
127            Some("acento") => self.acento = value.as_color().unwrap_or(self.acento),
128            Some("nombre") => {
129                if let Some(s) = value.as_str() {
130                    self.nombre = s.to_string();
131                }
132            }
133            Some("auto") => self.auto = value.as_bool().unwrap_or(self.auto),
134            Some("rutas") => {
135                if let Some(items) = value.as_list() {
136                    self.rutas = items.to_vec();
137                }
138            }
139            Some("accesos") => {
140                if let Some(rows) = value.as_table() {
141                    self.accesos = rows
142                        .iter()
143                        .map(|r| {
144                            (
145                                r.first().cloned().unwrap_or_default(),
146                                r.get(1).cloned().unwrap_or_default(),
147                            )
148                        })
149                        .collect();
150                }
151            }
152            other => {
153                return Err(allichay::AllichayError::UnknownPath(
154                    other.unwrap_or("").into(),
155                ))
156            }
157        }
158        Ok(())
159    }
160}
161
162struct Model {
163    cfg: DemoConfig,
164    state: AllichayState,
165}
166
167#[derive(Clone)]
168enum Msg {
169    Allichay(AllichayMsg),
170    Key(KeyEvent),
171}
172
173struct Demo;
174
175impl App for Demo {
176    type Model = Model;
177    type Msg = Msg;
178
179    fn title() -> &'static str {
180        "allichay · demo"
181    }
182
183    fn initial_size() -> (u32, u32) {
184        (760, 560)
185    }
186
187    fn init(_handle: &Handle<Msg>) -> Model {
188        Model {
189            cfg: DemoConfig::default(),
190            state: AllichayState::new(),
191        }
192    }
193
194    fn update(model: Model, msg: Msg, _handle: &Handle<Msg>) -> Model {
195        let mut m = model;
196        match msg {
197            Msg::Allichay(AllichayMsg::SelectSection(i)) => m.state.select(i),
198            Msg::Allichay(AllichayMsg::Focus(path)) => {
199                // Sembrar el buffer con el valor actual del campo.
200                let seed = m
201                    .cfg
202                    .schema()
203                    .find_field(&path)
204                    .and_then(|f| f.value.as_str().map(str::to_string))
205                    .unwrap_or_default();
206                m.state.focus(&path, &seed);
207            }
208            Msg::Allichay(AllichayMsg::FocusCell(path, row, col)) => {
209                // El estado siembra el buffer leyendo la celda del valor actual.
210                if let Some(f) = m.cfg.schema().find_field(&path) {
211                    m.state.focus_cell(&path, f.value.clone(), row, col);
212                }
213            }
214            Msg::Allichay(AllichayMsg::FocusHex(path)) => {
215                let seed = m
216                    .cfg
217                    .schema()
218                    .find_field(&path)
219                    .and_then(|f| f.value.as_color())
220                    .map(llimphi_module_allichay::color_hex)
221                    .unwrap_or_default();
222                m.state.focus_hex(&path, &seed);
223            }
224            Msg::Allichay(AllichayMsg::Change(path, value)) => {
225                println!("cambio: {path} = {value:?}");
226                if let Err(e) = m.cfg.apply(&path, value) {
227                    eprintln!("  error: {e}");
228                }
229            }
230            Msg::Allichay(AllichayMsg::ScrollTo(offset)) => m.state.set_scroll(offset),
231            Msg::Allichay(AllichayMsg::Input(path, ev)) => {
232                if matches!(ev, llimphi_widget_text_input::TextInputEvent::Press(_))
233                    && !m.state.is_focused(&path)
234                {
235                    let seed = m
236                        .cfg
237                        .schema()
238                        .find_field(&path)
239                        .and_then(|f| f.value.as_str().map(str::to_string))
240                        .unwrap_or_default();
241                    m.state.focus(&path, &seed);
242                }
243                if let Some((p, value)) = m.state.apply_input(ev) {
244                    let _ = m.cfg.apply(&p, value);
245                }
246            }
247            Msg::Allichay(AllichayMsg::EditAction(action)) => {
248                if let Some((p, value)) = m.state.apply_edit_action(action) {
249                    let _ = m.cfg.apply(&p, value);
250                }
251            }
252            Msg::Allichay(AllichayMsg::CloseEditMenu) => m.state.close_edit_menu(),
253            Msg::Allichay(AllichayMsg::ToggleDropdown(path, x, y, w)) => {
254                m.state.toggle_dropdown(&path, x, y, w)
255            }
256            Msg::Allichay(AllichayMsg::ToggleColorPicker(path, x, y, w)) => {
257                m.state.toggle_colorpicker(&path, x, y, w)
258            }
259            Msg::Allichay(AllichayMsg::ColorOrbit(dy, dp)) => m.state.orbit_cone(dy, dp),
260            // Ejes de disposición + buscador del sidebar unificado.
261            Msg::Allichay(AllichayMsg::Sidebar(sm)) => m.state.apply_sidebar(sm),
262            Msg::Key(event) => {
263                if let Some((path, value)) = m.state.apply_key(&event) {
264                    println!("texto: {path} = {value:?}");
265                    let _ = m.cfg.apply(&path, value);
266                }
267            }
268        }
269        m
270    }
271
272    fn on_key(model: &Model, event: &KeyEvent) -> Option<Msg> {
273        // Capturamos teclas cuando hay un campo de texto en edición, o cuando el
274        // buscador del sidebar unificado tiene el foco (las teclas van al filtro).
275        if model.state.is_editing() || model.state.sidebar.search_focused {
276            Some(Msg::Key(event.clone()))
277        } else {
278            None
279        }
280    }
281
282    fn view(model: &Model) -> View<Msg> {
283        let theme = Theme::dark();
284        let schema = model.cfg.schema();
285        let body = allichay_view(&schema, &model.state, &theme, Msg::Allichay);
286        View::new(Style {
287            size: Size {
288                width: percent(1.0_f32),
289                height: percent(1.0_f32),
290            },
291            ..Default::default()
292        })
293        .fill(theme.bg_app)
294        .children(vec![body])
295    }
296
297    fn view_overlay(model: &Model) -> Option<View<Msg>> {
298        let theme = Theme::dark();
299        let schema = model.cfg.schema();
300        let (w, h) = Self::initial_size();
301        let vp = (w as f32, h as f32);
302        // El select y el color-picker se pintan como overlays anclados (no inline).
303        if let Some(menu) =
304            llimphi_module_allichay::dropdown_overlay(&schema, &model.state, &theme, vp, Msg::Allichay)
305        {
306            return Some(menu);
307        }
308        llimphi_module_allichay::colorpicker_overlay(&schema, &model.state, &theme, vp, Msg::Allichay)
309    }
310}
311
312fn main() {
313    llimphi_ui::run::<Demo>();
314}