tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1//! Volcado headless del renderizador `allichay` a PNG: monta `allichay_view`
2//! sobre el schema REAL de mirada, computa el layout, lo pinta a una
3//! `vello::Scene` y lee la textura (GPU llvmpipe). Sirve para VER el panel sin
4//! levantar ventana.
5//!
6//! `cargo run -p llimphi-module-allichay --example dump_panel -- [out.png] [seccion]`
7//! (seccion = índice de diente; 1 = Decoración, rica en sliders + colores)
8
9use std::fs::File;
10use std::io::BufWriter;
11
12use allichay::{Configurable, Field, Schema, Section};
13use llimphi_module_allichay::{schema_panel, settings_overlay, AllichayMsg, AllichayState};
14use llimphi_widget_dock_rail::{dock_rail_view, DockRailItem, DockRailPalette};
15use llimphi_ui::llimphi_text::Alignment;
16
17use llimphi_ui::llimphi_compositor::{measure_text_node, mount, paint};
18use llimphi_ui::llimphi_hal::{wgpu, Hal};
19use llimphi_ui::llimphi_layout::taffy;
20use llimphi_ui::llimphi_layout::taffy::prelude::{
21 auto, length, percent, FlexDirection, Position, Size, Style,
22};
23use llimphi_ui::llimphi_layout::taffy::{AlignItems, Rect};
24use llimphi_ui::llimphi_layout::LayoutTree;
25use llimphi_ui::llimphi_raster::peniko::Color;
26use llimphi_ui::llimphi_raster::{vello, Renderer};
27use llimphi_ui::llimphi_text::Typesetter;
28use llimphi_ui::View;
29
30const W: u32 = 960;
31const H: u32 = 620;
32const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
33
34fn prefix(mut s: Schema, target: &str) -> Schema {
35 for sec in &mut s.sections {
36 sec.id = format!("{target}::{}", sec.id);
37 }
38 s
39}
40
41fn main() {
42 let out = std::env::args().nth(1).unwrap_or_else(|| "allichay.png".to_string());
43 let sel: usize = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(1);
44
45 let theme = llimphi_theme::Theme::default();
46
47 // Rail de pestañas representativo del panel: la categoría Sistema (varios
48 // items) + dos apps. Aprovecha la triple jerarquía (sin paneles de 1 item).
49 let eo = allichay::EnumOption::new;
50 let sistema = Schema::new()
51 .section(
52 Section::new("wawa::apariencia", "Apariencia")
53 .icon("🎨")
54 .field(Field::toggle("oscuro", "Modo oscuro", true))
55 .field(Field::dropdown("acento", "Acento", "tawasuyu", vec![eo("tawasuyu", "tawasuyu"), eo("yachay", "yachay")])),
56 )
57 .section(
58 Section::new("wawa::idioma", "Idioma")
59 .icon("🌐")
60 .field(Field::dropdown("lang", "Idioma", "es-PE", vec![eo("es-PE", "Español"), eo("en-US", "English")])),
61 )
62 .section(
63 Section::new("wawa::interfaz", "Interfaz")
64 .icon("🎛")
65 .field(Field::display("toolkit", "Toolkit", "llimphi")),
66 )
67 .section(
68 Section::new("wawa::arranque", "Arranque")
69 .icon("▶")
70 .field(Field::display("init", "Init", "systemd (actual)")),
71 )
72 .section(
73 Section::new("wawa::modulos", "Módulos")
74 .icon("☸")
75 .field(Field::toggle("mirada", "mirada", true))
76 .field(Field::toggle("shuma", "shuma", true)),
77 );
78 // Mirada con un par de entradas de menú, para que la tabla del menú raíz se
79 // vea poblada (el default trae el menú vacío).
80 let mut mirada_cfg = mirada_brain::Config::default();
81 mirada_cfg.menu = vec![
82 mirada_brain::MenuEntry {
83 label: "Editor".into(),
84 command: "nada".into(),
85 submenu: Vec::new(),
86 },
87 mirada_brain::MenuEntry {
88 label: "Terminal".into(),
89 command: "alacritty".into(),
90 submenu: Vec::new(),
91 },
92 ];
93 mirada_cfg.outputs = vec![mirada_brain::OutputOverride {
94 name: "HDMI-A-1".into(),
95 wallpaper_path: "/fondos/mar.png".into(),
96 wallpaper_fit: "fill".into(),
97 order: 0,
98 scale_120: 0,
99 transform: String::new(),
100 disabled: false,
101 color_profile: String::new(),
102 }];
103 // El keymap de mirada como tabla (combinación · acción) — una tabla alta
104 // que ejercita el scroll del panel.
105 let keymap_rows = mirada_brain::Keymap::default().to_rows();
106 let atajos = Schema::new().section(
107 Section::new("mirada::atajos", "Atajos").icon("⌨").field(Field::table(
108 "bindings",
109 "Atajos de teclado",
110 vec![
111 allichay::Column::new("combo", "Combinación"),
112 allichay::Column::new("action", "Acción"),
113 ],
114 keymap_rows,
115 )),
116 );
117 let dientes: Vec<(&str, &str, Schema)> = vec![
118 ("⚙", "Sistema", sistema),
119 ("☸", "mirada", prefix(mirada_cfg.schema(), "mirada")),
120 ("🎛", "pata", prefix(pata_core::Config::preset().schema(), "pata")),
121 ("⌨", "Atajos", atajos),
122 ];
123
124 let mut state = AllichayState::new();
125 state.select(sel);
126
127 let rail_items: Vec<DockRailItem> = dientes
128 .iter()
129 .enumerate()
130 .map(|(i, d)| DockRailItem::nombrado(i as u64, i == sel, d.1))
131 .collect();
132 let icons: Vec<String> = dientes.iter().map(|(icon, _, _)| (*icon).to_string()).collect();
133 let rail = dock_rail_view::<(), _, _, _>(
134 &rail_items,
135 52.0,
136 &DockRailPalette::from_theme(&theme),
137 move |id, size, color| {
138 let g = icons.get(id as usize).cloned().unwrap_or_default();
139 View::<()>::new(Style {
140 size: Size {
141 width: percent(1.0),
142 height: percent(1.0),
143 },
144 ..Default::default()
145 })
146 .text_aligned(g, size * 0.9, color, Alignment::Center)
147 },
148 |_id| (),
149 |_| None,
150 );
151 // 3 niveles: sidebar (items = secciones de la pestaña activa) | pestañas que
152 // sobresalen | canvas (contenido del item activo). El item se elige con el
153 // 3er arg (default 0) — útil para fotografiar una sección puntual (p. ej. la
154 // tabla del menú raíz de mirada).
155 let active = &dientes[sel.min(dientes.len() - 1)].2;
156 let item: usize = std::env::args()
157 .nth(3)
158 .and_then(|s| s.parse().ok())
159 .unwrap_or(0)
160 .min(active.sections.len().saturating_sub(1));
161
162 // Sidebar: lista de items (secciones) con su iconito.
163 let mut sidebar_kids: Vec<View<()>> = Vec::new();
164 for (i, sec) in active.sections.iter().enumerate() {
165 let act = i == item;
166 let icon = if sec.icon.is_empty() { "·" } else { sec.icon.as_str() };
167 let fg = if act { theme.fg_text } else { theme.fg_muted };
168 let row = View::<()>::new(Style {
169 flex_direction: FlexDirection::Row,
170 size: Size {
171 width: percent(1.0),
172 height: length(32.0),
173 },
174 align_items: Some(AlignItems::CENTER),
175 padding: Rect {
176 left: length(8.0),
177 right: length(8.0),
178 top: length(0.0),
179 bottom: length(0.0),
180 },
181 gap: Size {
182 width: length(6.0),
183 height: length(0.0),
184 },
185 ..Default::default()
186 })
187 .fill(if act { theme.bg_selected } else { theme.bg_panel })
188 .radius(4.0)
189 .children(vec![
190 View::<()>::new(Style {
191 size: Size {
192 width: length(22.0),
193 height: auto(),
194 },
195 ..Default::default()
196 })
197 .text_aligned(icon.to_string(), 14.0, fg, Alignment::Center),
198 View::<()>::new(Style {
199 size: Size {
200 width: percent(1.0),
201 height: auto(),
202 },
203 ..Default::default()
204 })
205 .text_aligned(sec.title.clone(), 12.5, fg, Alignment::Start),
206 ]);
207 sidebar_kids.push(row);
208 }
209 let sidebar = View::<()>::new(Style {
210 flex_direction: FlexDirection::Column,
211 size: Size {
212 width: length(232.0),
213 height: percent(1.0),
214 },
215 ..Default::default()
216 })
217 .fill(theme.bg_panel)
218 .children(sidebar_kids);
219
220 // Canvas: el contenido del item activo (una sección).
221 let one = Schema {
222 sections: vec![active.sections[item.min(active.sections.len() - 1)].clone()],
223 };
224 let canvas_content =
225 schema_panel::<(), _>(&one, &state, &theme, H as f32 - 40.0, |_m: AllichayMsg| ());
226 let canvas = View::<()>::new(Style {
227 flex_grow: 1.0,
228 size: Size {
229 width: percent(1.0),
230 height: percent(1.0),
231 },
232 padding: Rect {
233 top: length(0.0),
234 bottom: length(0.0),
235 left: length(46.0),
236 right: length(0.0),
237 },
238 ..Default::default()
239 })
240 .children(vec![canvas_content]);
241 let rail_overlay = View::<()>::new(Style {
242 position: Position::Absolute,
243 inset: Rect {
244 top: length(6.0),
245 left: length(0.0),
246 right: auto(),
247 bottom: auto(),
248 },
249 size: Size {
250 width: length(46.0),
251 height: auto(),
252 },
253 ..Default::default()
254 })
255 .children(vec![rail]);
256 let center = View::<()>::new(Style {
257 position: Position::Relative,
258 flex_grow: 1.0,
259 size: Size {
260 width: percent(0.0),
261 height: percent(1.0),
262 },
263 ..Default::default()
264 })
265 .fill(theme.bg_app)
266 .children(vec![canvas, rail_overlay]);
267 let v = View::<()>::new(Style {
268 flex_direction: FlexDirection::Row,
269 size: Size {
270 width: percent(1.0),
271 height: percent(1.0),
272 },
273 ..Default::default()
274 })
275 .fill(theme.bg_app)
276 .children(vec![sidebar, center]);
277
278 // Modo "overlay": pasa `overlay` como arg para ver `settings_overlay`
279 // (el modal embebible) sobre el panel como fondo de app.
280 let v = if std::env::args().any(|a| a == "overlay") {
281 let modal = settings_overlay::<(), _>(
282 "Configuración",
283 "Cerrar",
284 active,
285 &state,
286 &theme,
287 (W as f32, H as f32),
288 |_m: AllichayMsg| (),
289 (),
290 );
291 View::<()>::new(Style {
292 position: Position::Relative,
293 size: Size {
294 width: percent(1.0),
295 height: percent(1.0),
296 },
297 ..Default::default()
298 })
299 .children(vec![v, modal])
300 } else {
301 v
302 };
303
304 // view → layout → scene (misma secuencia que el eventloop).
305 let mut layout = LayoutTree::new();
306 let mounted = mount(&mut layout, v);
307 let mut ts = Typesetter::new();
308 let computed = {
309 let tmap = &mounted.text_measures;
310 layout
311 .compute_with_measure(mounted.root, (W as f32, H as f32), |nid, known, avail| {
312 match tmap.get(&nid) {
313 Some(tm) => measure_text_node(&mut ts, tm, known, avail),
314 None => taffy::Size::ZERO,
315 }
316 })
317 .expect("layout")
318 };
319 let mut scene = vello::Scene::new();
320 paint(&mut scene, &mounted, &computed, &mut ts, None, None);
321
322 let hal = pollster::block_on(Hal::new(None)).expect("hal");
323 let mut renderer = Renderer::new(&hal).expect("renderer");
324 let target = hal.device.create_texture(&wgpu::TextureDescriptor {
325 label: Some("dump-allichay"),
326 size: wgpu::Extent3d {
327 width: W,
328 height: H,
329 depth_or_array_layers: 1,
330 },
331 mip_level_count: 1,
332 sample_count: 1,
333 dimension: wgpu::TextureDimension::D2,
334 format: FMT,
335 usage: wgpu::TextureUsages::STORAGE_BINDING
336 | wgpu::TextureUsages::RENDER_ATTACHMENT
337 | wgpu::TextureUsages::COPY_SRC,
338 view_formats: &[],
339 });
340 let view = target.create_view(&wgpu::TextureViewDescriptor::default());
341 let [r, g, b, _] = theme.bg_app.components;
342 let bg = Color::from_rgba8((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8, 255);
343 renderer
344 .render_to_view(&hal, &scene, &view, W, H, bg)
345 .expect("render_to_view");
346
347 write_png(&hal, &target, &out);
348 eprintln!("dump_panel: escrito {out} ({W}x{H}) · diente {sel}");
349}
350
351fn write_png(hal: &Hal, target: &wgpu::Texture, path: &str) {
352 let unpadded = (W * 4) as usize;
353 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
354 let padded = unpadded.div_ceil(align) * align;
355 let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
356 label: Some("readback"),
357 size: (padded * H as usize) as u64,
358 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
359 mapped_at_creation: false,
360 });
361 let mut enc = hal
362 .device
363 .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
364 enc.copy_texture_to_buffer(
365 wgpu::TexelCopyTextureInfo {
366 texture: target,
367 mip_level: 0,
368 origin: wgpu::Origin3d::ZERO,
369 aspect: wgpu::TextureAspect::All,
370 },
371 wgpu::TexelCopyBufferInfo {
372 buffer: &buf,
373 layout: wgpu::TexelCopyBufferLayout {
374 offset: 0,
375 bytes_per_row: Some(padded as u32),
376 rows_per_image: Some(H),
377 },
378 },
379 wgpu::Extent3d {
380 width: W,
381 height: H,
382 depth_or_array_layers: 1,
383 },
384 );
385 hal.queue.submit(std::iter::once(enc.finish()));
386 let slice = buf.slice(..);
387 let (tx, rx) = std::sync::mpsc::channel();
388 slice.map_async(wgpu::MapMode::Read, move |r| {
389 let _ = tx.send(r);
390 });
391 let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
392 rx.recv().unwrap().unwrap();
393 let data = slice.get_mapped_range();
394 let mut pixels = Vec::with_capacity((W * H * 4) as usize);
395 for row in 0..H as usize {
396 let s = row * padded;
397 pixels.extend_from_slice(&data[s..s + unpadded]);
398 }
399 drop(data);
400 buf.unmap();
401 let file = File::create(path).expect("png");
402 let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
403 enc.set_color(png::ColorType::Rgba);
404 enc.set_depth(png::BitDepth::Eight);
405 let mut w = enc.write_header().unwrap();
406 w.write_image_data(&pixels).unwrap();
407}