tawasuyu

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Definiciones

⎇ main llimphi-compositor/examples/pantallazo_button.rs Rust · 210 líneas · 7.8 KB Historia Quién tocó qué crudo

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

1//! Pantallazo headless del `llimphi-widget-button` para VER el relieve nuevo
2//! (sombra + borde hairline + sheen + specular) en varios themes claros y
3//! oscuros, sin levantar ventana.
4//!
5//! `cargo run -p llimphi-compositor --example pantallazo_button --release -- [out.png]`
6
7use std::fs::File;
8use std::io::BufWriter;
9
10use llimphi_compositor::{measure_text_node, mount, paint, View};
11use llimphi_hal::{wgpu, Hal};
12use llimphi_layout::taffy;
13use llimphi_layout::taffy::prelude::{length, percent, FlexDirection, Size, Style};
14use llimphi_layout::taffy::{AlignItems, JustifyContent, Rect};
15use llimphi_layout::LayoutTree;
16use llimphi_raster::peniko::Color;
17use llimphi_raster::{vello, Renderer};
18use llimphi_text::{Alignment, Typesetter};
19use llimphi_widget_button::{button_styled, ButtonPalette};
20
21const W: u32 = 1120;
22const H: u32 = 620;
23const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
24
25fn mix(a: Color, b: Color, t: f32) -> Color {
26    let (ca, cb) = (a.components, b.components);
27    Color {
28        components: [
29            ca[0] + (cb[0] - ca[0]) * t,
30            ca[1] + (cb[1] - ca[1]) * t,
31            ca[2] + (cb[2] - ca[2]) * t,
32            ca[3],
33        ],
34        ..a
35    }
36}
37
38/// Un botón de ancho fijo dentro de una caja (button_view es 100% de ancho).
39fn chip(label: &str, pal: &ButtonPalette) -> View<()> {
40    let inner = button_styled(
41        label,
42        Style {
43            size: Size { width: percent(1.0_f32), height: length(34.0_f32) },
44            padding: Rect { left: length(14.0_f32), right: length(14.0_f32), top: length(0.0_f32), bottom: length(0.0_f32) },
45            align_items: Some(AlignItems::CENTER),
46            justify_content: Some(JustifyContent::CENTER),
47            ..Default::default()
48        },
49        Alignment::Center,
50        pal,
51        (),
52    );
53    View::<()>::new(Style {
54        size: Size { width: length(150.0_f32), height: length(34.0_f32) },
55        ..Default::default()
56    })
57    .children(vec![inner])
58}
59
60/// Fila de una demo por theme: fondo bg_app + nombre + 3 botones
61/// (default · hover-preview · primario accent).
62fn fila_theme(nombre: &str) -> View<()> {
63    let theme = llimphi_theme::Theme::by_name(nombre).expect("theme");
64    let default_pal = ButtonPalette::from_theme(&theme);
65    // Preview del estado hover: una paleta cuyo bg es el bg_button_hover.
66    let mut hover_pal = default_pal;
67    hover_pal.bg = theme.bg_button_hover;
68    // Primario: fondo = accent, texto que contraste, radio un toque mayor.
69    let white = Color::from_rgba8(255, 255, 255, 255);
70    let black = Color::from_rgba8(0, 0, 0, 255);
71    let lum = {
72        let k = theme.accent.components;
73        0.2126 * k[0] + 0.7152 * k[1] + 0.0722 * k[2]
74    };
75    let primary_pal = ButtonPalette {
76        bg: theme.accent,
77        bg_hover: mix(theme.accent, white, 0.12),
78        fg: if lum < 0.55 { white } else { black },
79        radius: 7.0,
80    };
81
82    let etiqueta = View::<()>::new(Style {
83        size: Size { width: length(120.0_f32), height: length(34.0_f32) },
84        align_items: Some(AlignItems::CENTER),
85        ..Default::default()
86    })
87    .text_aligned(nombre.to_string(), 13.0, theme.fg_muted, Alignment::Start);
88
89    View::<()>::new(Style {
90        size: Size { width: percent(1.0_f32), height: length(72.0_f32) },
91        flex_direction: FlexDirection::Row,
92        align_items: Some(AlignItems::CENTER),
93        gap: Size { width: length(16.0_f32), height: length(0.0_f32) },
94        padding: Rect { left: length(24.0_f32), right: length(24.0_f32), top: length(0.0_f32), bottom: length(0.0_f32) },
95        ..Default::default()
96    })
97    .fill(theme.bg_app)
98    .children(vec![
99        etiqueta,
100        chip("Guardar", &default_pal),
101        chip("Guardar", &hover_pal),
102        chip("Aceptar", &primary_pal),
103    ])
104}
105
106fn main() {
107    let out = std::env::args().nth(1).unwrap_or_else(|| "pantallazo_button.png".to_string());
108    let temas = ["Dark", "Light", "Tawa", "macOS", "Sunset", "Breeze", "Print", "Aurora"];
109    let filas: Vec<View<()>> = temas.iter().map(|t| fila_theme(t)).collect();
110
111    let root = View::<()>::new(Style {
112        size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
113        flex_direction: FlexDirection::Column,
114        ..Default::default()
115    })
116    .fill(Color::from_rgba8(20, 20, 24, 255))
117    .children(filas);
118
119    let mut layout = LayoutTree::new();
120    let mounted = mount(&mut layout, root);
121    let mut ts = Typesetter::new();
122    let computed = {
123        let tmap = &mounted.text_measures;
124        layout
125            .compute_with_measure(mounted.root, (W as f32, H as f32), |nid, known, avail| {
126                match tmap.get(&nid) {
127                    Some(tm) => measure_text_node(&mut ts, tm, known, avail),
128                    None => taffy::Size::ZERO,
129                }
130            })
131            .expect("layout")
132    };
133    let mut scene = vello::Scene::new();
134    paint(&mut scene, &mounted, &computed, &mut ts, None, None);
135
136    let hal = pollster::block_on(Hal::new(None)).expect("hal");
137    let mut renderer = Renderer::new(&hal).expect("renderer");
138    let target = hal.device.create_texture(&wgpu::TextureDescriptor {
139        label: Some("pantallazo-button"),
140        size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
141        mip_level_count: 1,
142        sample_count: 1,
143        dimension: wgpu::TextureDimension::D2,
144        format: FMT,
145        usage: wgpu::TextureUsages::STORAGE_BINDING
146            | wgpu::TextureUsages::RENDER_ATTACHMENT
147            | wgpu::TextureUsages::COPY_SRC,
148        view_formats: &[],
149    });
150    let view = target.create_view(&wgpu::TextureViewDescriptor::default());
151    renderer
152        .render_to_view(&hal, &scene, &view, W, H, Color::from_rgba8(20, 20, 24, 255))
153        .expect("render_to_view");
154    write_png(&hal, &target, &out);
155    eprintln!("pantallazo_button: escrito {out} ({W}x{H})");
156}
157
158fn write_png(hal: &Hal, target: &wgpu::Texture, path: &str) {
159    let unpadded = (W * 4) as usize;
160    let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
161    let padded = unpadded.div_ceil(align) * align;
162    let buf = hal.device.create_buffer(&wgpu::BufferDescriptor {
163        label: Some("readback"),
164        size: (padded * H as usize) as u64,
165        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
166        mapped_at_creation: false,
167    });
168    let mut enc = hal
169        .device
170        .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
171    enc.copy_texture_to_buffer(
172        wgpu::TexelCopyTextureInfo {
173            texture: target,
174            mip_level: 0,
175            origin: wgpu::Origin3d::ZERO,
176            aspect: wgpu::TextureAspect::All,
177        },
178        wgpu::TexelCopyBufferInfo {
179            buffer: &buf,
180            layout: wgpu::TexelCopyBufferLayout {
181                offset: 0,
182                bytes_per_row: Some(padded as u32),
183                rows_per_image: Some(H),
184            },
185        },
186        wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
187    );
188    hal.queue.submit(std::iter::once(enc.finish()));
189    let slice = buf.slice(..);
190    let (tx, rx) = std::sync::mpsc::channel();
191    slice.map_async(wgpu::MapMode::Read, move |r| {
192        let _ = tx.send(r);
193    });
194    let _ = hal.device.poll(wgpu::PollType::wait_indefinitely());
195    rx.recv().unwrap().unwrap();
196    let data = slice.get_mapped_range();
197    let mut pixels = Vec::with_capacity((W * H * 4) as usize);
198    for row in 0..H as usize {
199        let s = row * padded;
200        pixels.extend_from_slice(&data[s..s + unpadded]);
201    }
202    drop(data);
203    buf.unmap();
204    let file = File::create(path).expect("png");
205    let mut enc = png::Encoder::new(BufWriter::new(file), W, H);
206    enc.set_color(png::ColorType::Rgba);
207    enc.set_depth(png::BitDepth::Eight);
208    let mut w = enc.write_header().unwrap();
209    w.write_image_data(&pixels).unwrap();
210}