tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1
llimphi-compositor/examples/pantallazo_button.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
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}