gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main modules/allichay/examples/dump_panel.rs Rust · 407 lines · 14.2 KB History Who touched what raw

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

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}