//! Pantallazo headless del `llimphi-widget-button` para VER el relieve nuevo //! (sombra + borde hairline + sheen + specular) en varios themes claros y //! oscuros, sin levantar ventana. //! //! `cargo run -p llimphi-compositor --example pantallazo_button --release -- [out.png]` use std::fs::File; use std::io::BufWriter; use llimphi_compositor::{measure_text_node, mount, paint, View}; use llimphi_hal::{wgpu, Hal}; use llimphi_layout::taffy; use llimphi_layout::taffy::prelude::{length, percent, FlexDirection, Size, Style}; use llimphi_layout::taffy::{AlignItems, JustifyContent, Rect}; use llimphi_layout::LayoutTree; use llimphi_raster::peniko::Color; use llimphi_raster::{vello, Renderer}; use llimphi_text::{Alignment, Typesetter}; use llimphi_widget_button::{button_styled, ButtonPalette}; const W: u32 = 1120; const H: u32 = 620; const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; fn mix(a: Color, b: Color, t: f32) -> Color { let (ca, cb) = (a.components, b.components); Color { components: [ ca[0] + (cb[0] - ca[0]) * t, ca[1] + (cb[1] - ca[1]) * t, ca[2] + (cb[2] - ca[2]) * t, ca[3], ], ..a } } /// Un botón de ancho fijo dentro de una caja (button_view es 100% de ancho). fn chip(label: &str, pal: &ButtonPalette) -> View<()> { let inner = button_styled( label, Style { size: Size { width: percent(1.0_f32), height: length(34.0_f32) }, padding: Rect { left: length(14.0_f32), right: length(14.0_f32), top: length(0.0_f32), bottom: length(0.0_f32) }, align_items: Some(AlignItems::CENTER), justify_content: Some(JustifyContent::CENTER), ..Default::default() }, Alignment::Center, pal, (), ); View::<()>::new(Style { size: Size { width: length(150.0_f32), height: length(34.0_f32) }, ..Default::default() }) .children(vec![inner]) } /// Fila de una demo por theme: fondo bg_app + nombre + 3 botones /// (default · hover-preview · primario accent). fn fila_theme(nombre: &str) -> View<()> { let theme = llimphi_theme::Theme::by_name(nombre).expect("theme"); let default_pal = ButtonPalette::from_theme(&theme); // Preview del estado hover: una paleta cuyo bg es el bg_button_hover. let mut hover_pal = default_pal; hover_pal.bg = theme.bg_button_hover; // Primario: fondo = accent, texto que contraste, radio un toque mayor. let white = Color::from_rgba8(255, 255, 255, 255); let black = Color::from_rgba8(0, 0, 0, 255); let lum = { let k = theme.accent.components; 0.2126 * k[0] + 0.7152 * k[1] + 0.0722 * k[2] }; let primary_pal = ButtonPalette { bg: theme.accent, bg_hover: mix(theme.accent, white, 0.12), fg: if lum < 0.55 { white } else { black }, radius: 7.0, }; let etiqueta = View::<()>::new(Style { size: Size { width: length(120.0_f32), height: length(34.0_f32) }, align_items: Some(AlignItems::CENTER), ..Default::default() }) .text_aligned(nombre.to_string(), 13.0, theme.fg_muted, Alignment::Start); View::<()>::new(Style { size: Size { width: percent(1.0_f32), height: length(72.0_f32) }, flex_direction: FlexDirection::Row, align_items: Some(AlignItems::CENTER), gap: Size { width: length(16.0_f32), height: length(0.0_f32) }, padding: Rect { left: length(24.0_f32), right: length(24.0_f32), top: length(0.0_f32), bottom: length(0.0_f32) }, ..Default::default() }) .fill(theme.bg_app) .children(vec![ etiqueta, chip("Guardar", &default_pal), chip("Guardar", &hover_pal), chip("Aceptar", &primary_pal), ]) } fn main() { let out = std::env::args().nth(1).unwrap_or_else(|| "pantallazo_button.png".to_string()); let temas = ["Dark", "Light", "Tawa", "macOS", "Sunset", "Breeze", "Print", "Aurora"]; let filas: Vec> = temas.iter().map(|t| fila_theme(t)).collect(); let root = View::<()>::new(Style { size: Size { width: percent(1.0_f32), height: percent(1.0_f32) }, flex_direction: FlexDirection::Column, ..Default::default() }) .fill(Color::from_rgba8(20, 20, 24, 255)) .children(filas); let mut layout = LayoutTree::new(); let mounted = mount(&mut layout, root); let mut ts = Typesetter::new(); let computed = { let tmap = &mounted.text_measures; layout .compute_with_measure(mounted.root, (W as f32, H as f32), |nid, known, avail| { match tmap.get(&nid) { Some(tm) => measure_text_node(&mut ts, tm, known, avail), None => taffy::Size::ZERO, } }) .expect("layout") }; let mut scene = vello::Scene::new(); paint(&mut scene, &mounted, &computed, &mut ts, None, None); let hal = pollster::block_on(Hal::new(None)).expect("hal"); let mut renderer = Renderer::new(&hal).expect("renderer"); let target = hal.device.create_texture(&wgpu::TextureDescriptor { label: Some("pantallazo-button"), size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: FMT, usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, view_formats: &[], }); let view = target.create_view(&wgpu::TextureViewDescriptor::default()); renderer .render_to_view(&hal, &scene, &view, W, H, Color::from_rgba8(20, 20, 24, 255)) .expect("render_to_view"); write_png(&hal, &target, &out); eprintln!("pantallazo_button: escrito {out} ({W}x{H})"); } fn write_png(hal: &Hal, target: &wgpu::Texture, path: &str) { let unpadded = (W * 4) as usize; let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; let padded = unpadded.div_ceil(align) * align; let buf = hal.device.create_buffer(&wgpu::BufferDescriptor { label: Some("readback"), size: (padded * H as usize) as u64, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let mut enc = hal .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); enc.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture: target, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyBufferInfo { buffer: &buf, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded as u32), rows_per_image: Some(H), }, }, wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 }, ); hal.queue.submit(std::iter::once(enc.finish())); let slice = buf.slice(..); let (tx, rx) = std::sync::mpsc::channel(); slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); }); let _ = hal.device.poll(wgpu::PollType::wait_indefinitely()); rx.recv().unwrap().unwrap(); let data = slice.get_mapped_range(); let mut pixels = Vec::with_capacity((W * H * 4) as usize); for row in 0..H as usize { let s = row * padded; pixels.extend_from_slice(&data[s..s + unpadded]); } drop(data); buf.unmap(); let file = File::create(path).expect("png"); let mut enc = png::Encoder::new(BufWriter::new(file), W, H); enc.set_color(png::ColorType::Rgba); enc.set_depth(png::BitDepth::Eight); let mut w = enc.write_header().unwrap(); w.write_image_data(&pixels).unwrap(); }