feat: llimphi standalone — framework UI soberano extraído del monorepo

Motor gráfico Llimphi como workspace independiente: bucle Elm
(input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley.
Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets
+ módulos, sin dependencias al resto del monorepo. cargo check --workspace
pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 04:23:42 +00:00
commit e65e9cc623
286 changed files with 46136 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
//! Smoke test del backend GPU directo (`llimphi_raster::gpu`).
//!
//! No verifica píxeles — eso requiere AA y un patrón conocido, y por
//! ahora el módulo no garantiza pixel-exactness. Sí verifica que:
//!
//! - `GpuPipelines::new` compila los 3 shaders WGSL sin errores de naga.
//! - `GpuBatch` acepta líneas, triángulos y rects mezclados sin pánico.
//! - `flush` ejecuta sin errores wgpu y la `Maintain::Wait` retorna
//! (= la GPU/llvmpipe terminó las pasadas).
//!
//! Corre en cualquier adapter wgpu disponible — en CI sin GPU usa
//! llvmpipe, donde igual valida el ensamblado y la sintaxis WGSL.
use llimphi_hal::{wgpu, Hal};
use llimphi_raster::gpu::{GpuBatch, GpuPipelines};
use llimphi_raster::peniko::Color;
const W: u32 = 256;
const H: u32 = 256;
const FMT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
fn make_target(device: &wgpu::Device) -> (wgpu::Texture, wgpu::TextureView) {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("smoke-target"),
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::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
(tex, view)
}
#[test]
fn batch_with_rects_lines_tris_does_not_panic() {
let hal = pollster::block_on(Hal::new(None)).expect("hal");
let pipelines = GpuPipelines::new(&hal.device, FMT);
let (_tex, view) = make_target(&hal.device);
let mut batch = GpuBatch::new(&pipelines);
batch.line_width(2.0);
// Cuadrícula 8×8 de rects con color que varía.
for j in 0..8 {
for i in 0..8 {
let x = 8.0 + i as f32 * 30.0;
let y = 8.0 + j as f32 * 30.0;
let c = Color::from_rgba8(
(i * 32) as u8,
(j * 32) as u8,
100,
255,
);
batch.add_rect(x, y, 24.0, 24.0, c);
}
}
// Diagonal de líneas.
for k in 0..16 {
batch.add_line(
(0.0, k as f32 * 16.0),
(W as f32, (k + 1) as f32 * 16.0),
Color::from_rgba8(220, 220, 250, 180),
);
}
// Triángulo grande con color por vértice.
batch.add_tri(
(128.0, 32.0),
(64.0, 220.0),
(220.0, 220.0),
Color::from_rgba8(255, 80, 80, 200),
Color::from_rgba8(80, 255, 80, 200),
Color::from_rgba8(80, 80, 255, 200),
);
assert!(batch.primitive_count() > 0, "batch debería tener primitivas");
let mut encoder = hal
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("smoke-enc"),
});
batch.flush(
&hal.device,
&hal.queue,
&mut encoder,
&view,
(W as f32, H as f32),
wgpu::LoadOp::Clear(wgpu::Color::BLACK),
);
hal.queue.submit(std::iter::once(encoder.finish()));
hal.device.poll(wgpu::Maintain::Wait);
}
#[test]
fn empty_batch_flush_is_no_op() {
let hal = pollster::block_on(Hal::new(None)).expect("hal");
let pipelines = GpuPipelines::new(&hal.device, FMT);
let (_tex, view) = make_target(&hal.device);
let batch = GpuBatch::new(&pipelines);
assert_eq!(batch.primitive_count(), 0);
let mut encoder = hal
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("smoke-empty-enc"),
});
// Con batch vacío, flush no debe crear render pass ni buffers.
batch.flush(
&hal.device,
&hal.queue,
&mut encoder,
&view,
(W as f32, H as f32),
wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
);
hal.queue.submit(std::iter::once(encoder.finish()));
hal.device.poll(wgpu::Maintain::Wait);
}