tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
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}