e65e9cc623
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>
251 lines
7.1 KiB
Rust
251 lines
7.1 KiB
Rust
//! Fase 3 de Llimphi: 3 paneles (sidebar + header/body/footer) que se
|
|
//! reorganizan al redimensionar la ventana. Pintados por vello a través
|
|
//! de llimphi-raster.
|
|
//!
|
|
//! Corre con: `cargo run -p llimphi-layout --example layout_panels --release`.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use llimphi_hal::winit::application::ApplicationHandler;
|
|
use llimphi_hal::winit::dpi::LogicalSize;
|
|
use llimphi_hal::winit::event::WindowEvent;
|
|
use llimphi_hal::winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
|
use llimphi_hal::winit::window::{Window, WindowAttributes, WindowId};
|
|
use llimphi_hal::{Hal, Surface, WinitSurface};
|
|
use llimphi_layout::{
|
|
taffy::{prelude::*, Style},
|
|
ComputedLayout, LayoutTree, Rect,
|
|
};
|
|
use llimphi_raster::kurbo::{Affine, RoundedRect};
|
|
use llimphi_raster::peniko::{color::palette, Color, Fill};
|
|
use llimphi_raster::{vello, Renderer};
|
|
|
|
struct Panels {
|
|
sidebar: NodeId,
|
|
header: NodeId,
|
|
body: NodeId,
|
|
footer: NodeId,
|
|
root: NodeId,
|
|
}
|
|
|
|
struct State {
|
|
window: Arc<Window>,
|
|
hal: Hal,
|
|
surface: WinitSurface,
|
|
renderer: Renderer,
|
|
scene: vello::Scene,
|
|
layout: LayoutTree,
|
|
panels: Panels,
|
|
}
|
|
|
|
struct App {
|
|
state: Option<State>,
|
|
}
|
|
|
|
fn build_tree(layout: &mut LayoutTree) -> Panels {
|
|
let sidebar = layout
|
|
.leaf(Style {
|
|
size: Size {
|
|
width: length(220.0_f32),
|
|
height: percent(1.0_f32),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.unwrap();
|
|
|
|
let header = layout
|
|
.leaf(Style {
|
|
size: Size {
|
|
width: percent(1.0_f32),
|
|
height: length(64.0_f32),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.unwrap();
|
|
|
|
let body = layout
|
|
.leaf(Style {
|
|
size: Size {
|
|
width: percent(1.0_f32),
|
|
height: Dimension::auto(),
|
|
},
|
|
flex_grow: 1.0,
|
|
..Default::default()
|
|
})
|
|
.unwrap();
|
|
|
|
let footer = layout
|
|
.leaf(Style {
|
|
size: Size {
|
|
width: percent(1.0_f32),
|
|
height: length(40.0_f32),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.unwrap();
|
|
|
|
let content = layout
|
|
.node(
|
|
Style {
|
|
flex_direction: FlexDirection::Column,
|
|
flex_grow: 1.0,
|
|
size: Size {
|
|
width: Dimension::auto(),
|
|
height: percent(1.0_f32),
|
|
},
|
|
gap: Size {
|
|
width: length(0.0_f32),
|
|
height: length(8.0_f32),
|
|
},
|
|
padding: Rect_(length(8.0_f32)),
|
|
..Default::default()
|
|
},
|
|
&[header, body, footer],
|
|
)
|
|
.unwrap();
|
|
|
|
let root = layout
|
|
.node(
|
|
Style {
|
|
flex_direction: FlexDirection::Row,
|
|
size: Size {
|
|
width: percent(1.0_f32),
|
|
height: percent(1.0_f32),
|
|
},
|
|
..Default::default()
|
|
},
|
|
&[sidebar, content],
|
|
)
|
|
.unwrap();
|
|
|
|
Panels {
|
|
sidebar,
|
|
header,
|
|
body,
|
|
footer,
|
|
root,
|
|
}
|
|
}
|
|
|
|
/// Helper para pasar el mismo length a todos los lados de un Rect.
|
|
#[allow(non_snake_case)]
|
|
fn Rect_(v: LengthPercentage) -> taffy::Rect<LengthPercentage> {
|
|
taffy::Rect {
|
|
left: v,
|
|
right: v,
|
|
top: v,
|
|
bottom: v,
|
|
}
|
|
}
|
|
|
|
fn paint(scene: &mut vello::Scene, computed: &ComputedLayout, panels: &Panels) {
|
|
fn rect(scene: &mut vello::Scene, r: Rect, color: Color, radius: f64) {
|
|
let rr = RoundedRect::new(
|
|
r.x as f64,
|
|
r.y as f64,
|
|
(r.x + r.w) as f64,
|
|
(r.y + r.h) as f64,
|
|
radius,
|
|
);
|
|
scene.fill(Fill::NonZero, Affine::IDENTITY, color, None, &rr);
|
|
}
|
|
|
|
if let Some(r) = computed.get(panels.sidebar) {
|
|
rect(scene, r, Color::from_rgba8(36, 44, 60, 255), 0.0);
|
|
}
|
|
if let Some(r) = computed.get(panels.header) {
|
|
rect(scene, r, Color::from_rgba8(60, 80, 110, 255), 8.0);
|
|
}
|
|
if let Some(r) = computed.get(panels.body) {
|
|
rect(scene, r, Color::from_rgba8(80, 110, 150, 255), 8.0);
|
|
}
|
|
if let Some(r) = computed.get(panels.footer) {
|
|
rect(scene, r, Color::from_rgba8(60, 80, 110, 255), 8.0);
|
|
}
|
|
}
|
|
|
|
impl ApplicationHandler for App {
|
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
|
if self.state.is_some() {
|
|
return;
|
|
}
|
|
let window = event_loop
|
|
.create_window(
|
|
WindowAttributes::default()
|
|
.with_title("llimphi · layout_panels")
|
|
.with_inner_size(LogicalSize::new(960u32, 540u32)),
|
|
)
|
|
.expect("create window");
|
|
let window = Arc::new(window);
|
|
let hal = pollster::block_on(Hal::new(None)).expect("hal");
|
|
let surface = WinitSurface::new(&hal, window.clone()).expect("surface");
|
|
let renderer = Renderer::new(&hal).expect("renderer");
|
|
let mut layout = LayoutTree::new();
|
|
let panels = build_tree(&mut layout);
|
|
window.request_redraw();
|
|
self.state = Some(State {
|
|
window,
|
|
hal,
|
|
surface,
|
|
renderer,
|
|
scene: vello::Scene::new(),
|
|
layout,
|
|
panels,
|
|
});
|
|
}
|
|
|
|
fn window_event(
|
|
&mut self,
|
|
event_loop: &ActiveEventLoop,
|
|
_id: WindowId,
|
|
event: WindowEvent,
|
|
) {
|
|
let Some(state) = self.state.as_mut() else {
|
|
return;
|
|
};
|
|
match event {
|
|
WindowEvent::CloseRequested => event_loop.exit(),
|
|
WindowEvent::Resized(size) => {
|
|
state.surface.resize(size.width, size.height);
|
|
state.window.request_redraw();
|
|
}
|
|
WindowEvent::RedrawRequested => {
|
|
let frame = match state.surface.acquire() {
|
|
Ok(f) => f,
|
|
Err(_) => {
|
|
let (w, h) = state.surface.size();
|
|
state.surface.resize(w, h);
|
|
state.window.request_redraw();
|
|
return;
|
|
}
|
|
};
|
|
let (w, h) = frame.size();
|
|
let computed = state
|
|
.layout
|
|
.compute(state.panels.root, (w as f32, h as f32))
|
|
.expect("compute layout");
|
|
state.scene.reset();
|
|
paint(&mut state.scene, &computed, &state.panels);
|
|
if let Err(e) = state.renderer.render(
|
|
&state.hal,
|
|
&state.scene,
|
|
&frame,
|
|
palette::css::BLACK,
|
|
) {
|
|
eprintln!("render error: {e}");
|
|
}
|
|
state.surface.present(frame, &state.hal);
|
|
state.window.request_redraw();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let event_loop = EventLoop::new().expect("event loop");
|
|
event_loop.set_control_flow(ControlFlow::Poll);
|
|
let mut app = App { state: None };
|
|
event_loop.run_app(&mut app).expect("run app");
|
|
}
|