tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main android/vello-hello-android/src/lib.rs Rust · 376 lines · 12.0 KB History Who touched what raw

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

1//! Tier 1.5 Android: chacana animada con vello + llimphi-raster.
2//!
3//! Smoke test del stack raster completo en device móvil:
4//!   wgpu (Vulkan/Adreno) → llimphi-hal (intermediate Rgba8) →
5//!   vello::Scene (kurbo paths + peniko brushes) →
6//!   llimphi_raster::Renderer (compute pipeline AA) →
7//!   blit a swapchain.
8//!
9//! El bootstrap es el mismo orden estricto que `clear-screen-android`:
10//! create_surface antes que request_adapter (compatible_surface=Some),
11//! WinitSurface::from_surface (no `new`), panic hook al logcat.
12//!
13//! Si esta app pinta y mantiene fps en device, todas las apps Llimphi
14//! basadas en vello están listas para portar mecánicamente — solo hay
15//! que envolver su `build_scene` con este shell.
16
17use std::sync::Arc;
18use std::time::Instant;
19
20use llimphi_hal::winit::application::ApplicationHandler;
21use llimphi_hal::winit::event::WindowEvent;
22use llimphi_hal::winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
23use llimphi_hal::winit::window::{Window, WindowAttributes, WindowId};
24use llimphi_hal::{wgpu, Hal, Surface, WinitSurface};
25use llimphi_raster::kurbo::{Affine, BezPath, Circle, Stroke};
26use llimphi_raster::peniko::{Color, Fill};
27use llimphi_raster::{vello, Renderer};
28
29const TAG: &str = "llimphi-vello";
30
31// Paleta gioser (mismos hex que la web/Llimphi-theme).
32const COSMOS_NIGHT: Color = Color::from_rgba8(0x0E, 0x10, 0x16, 255);
33const ACCENT_CYAN: Color = Color::from_rgba8(0xA6, 0xD8, 0xFF, 255);
34const ACCENT_AMBER: Color = Color::from_rgba8(0xE8, 0xC9, 0x7A, 255);
35const ACCENT_BLUE: Color = Color::from_rgba8(0x6E, 0x8C, 0xDC, 255);
36const ACCENT_VIOLET: Color = Color::from_rgba8(0xC3, 0x9C, 0xE8, 255);
37
38struct State {
39    window: Arc<Window>,
40    hal: Hal,
41    surface: WinitSurface,
42    renderer: Renderer,
43    scene: vello::Scene,
44}
45
46struct App {
47    state: Option<State>,
48    started: Instant,
49    frames: u64,
50    last_report: Instant,
51}
52
53impl App {
54    fn new() -> Self {
55        let now = Instant::now();
56        Self {
57            state: None,
58            started: now,
59            frames: 0,
60            last_report: now,
61        }
62    }
63
64    fn boot(&self, event_loop: &ActiveEventLoop) -> Result<State, String> {
65        log::info!("[boot] 1/8 Window");
66        let window = event_loop
67            .create_window(WindowAttributes::default().with_title("llimphi · vello-hello"))
68            .map_err(|e| format!("create_window: {e}"))?;
69        let window = Arc::new(window);
70
71        log::info!("[boot] 2/8 wgpu::Instance");
72        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
73            backends: wgpu::Backends::all(),
74            ..Default::default()
75        });
76
77        log::info!("[boot] 3/8 Surface (única create_surface en este boot)");
78        let surface = instance
79            .create_surface(window.clone())
80            .map_err(|e| format!("create_surface: {e}"))?;
81
82        log::info!("[boot] 4/8 Adapter compatible con surface");
83        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
84            power_preference: wgpu::PowerPreference::HighPerformance,
85            force_fallback_adapter: false,
86            compatible_surface: Some(&surface),
87        }))
88        .ok_or_else(|| "request_adapter → None".to_string())?;
89        let info = adapter.get_info();
90        log::info!(
91            "[boot] adapter ok · {:?} · {} · {:?}",
92            info.backend,
93            info.name,
94            info.driver_info
95        );
96
97        log::info!("[boot] 5/8 Device + Queue");
98        let limits = wgpu::Limits::default().using_resolution(adapter.limits());
99        let (device, queue) = pollster::block_on(adapter.request_device(
100            &wgpu::DeviceDescriptor {
101                label: Some("vello-hello-device"),
102                required_features: wgpu::Features::empty(),
103                required_limits: limits,
104                memory_hints: wgpu::MemoryHints::Performance,
105            },
106            None,
107        ))
108        .map_err(|e| format!("request_device: {e}"))?;
109
110        log::info!("[boot] 6/8 Hal");
111        let hal = Hal {
112            instance,
113            adapter,
114            device,
115            queue,
116        };
117
118        log::info!("[boot] 7/8 WinitSurface::from_surface");
119        let surface = WinitSurface::from_surface(&hal, window.clone(), surface)
120            .map_err(|e| format!("WinitSurface: {e}"))?;
121
122        log::info!("[boot] 8/8 vello Renderer");
123        let renderer =
124            Renderer::new(&hal).map_err(|e| format!("Renderer::new: {e}"))?;
125
126        log::info!("[boot] ✓ stack raster listo, primer redraw");
127        window.request_redraw();
128
129        Ok(State {
130            window,
131            hal,
132            surface,
133            renderer,
134            scene: vello::Scene::new(),
135        })
136    }
137}
138
139impl ApplicationHandler for App {
140    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
141        log::info!("Resumed");
142        match self.boot(event_loop) {
143            Ok(s) => self.state = Some(s),
144            Err(e) => log::error!("BOOT FAILED: {e}"),
145        }
146    }
147
148    fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
149        log::info!("Suspended — liberando state");
150        self.state = None;
151    }
152
153    fn window_event(
154        &mut self,
155        event_loop: &ActiveEventLoop,
156        _id: WindowId,
157        event: WindowEvent,
158    ) {
159        let Some(state) = self.state.as_mut() else {
160            return;
161        };
162        match event {
163            WindowEvent::CloseRequested => event_loop.exit(),
164            WindowEvent::Resized(size) => {
165                log::info!("Resized → {}x{}", size.width, size.height);
166                state.surface.resize(size.width, size.height);
167                state.window.request_redraw();
168            }
169            WindowEvent::RedrawRequested => {
170                let frame = match state.surface.acquire() {
171                    Ok(f) => f,
172                    Err(e) => {
173                        log::warn!("acquire {e}, reconfig");
174                        let (w, h) = state.surface.size();
175                        state.surface.resize(w, h);
176                        state.window.request_redraw();
177                        return;
178                    }
179                };
180                let (w, h) = frame.size();
181                let t = self.started.elapsed().as_secs_f64();
182                state.scene.reset();
183                build_chacana(&mut state.scene, w as f64, h as f64, t);
184                if let Err(e) = state.renderer.render(
185                    &state.hal,
186                    &state.scene,
187                    &frame,
188                    COSMOS_NIGHT,
189                ) {
190                    log::error!("render: {e}");
191                }
192                state.surface.present(frame, &state.hal);
193
194                self.frames += 1;
195                let elapsed = self.last_report.elapsed();
196                if elapsed.as_secs() >= 1 {
197                    let fps = self.frames as f64 / elapsed.as_secs_f64();
198                    log::info!("{fps:.1} fps · {w}x{h}");
199                    self.frames = 0;
200                    self.last_report = Instant::now();
201                }
202                state.window.request_redraw();
203            }
204            _ => {}
205        }
206    }
207}
208
209/// Construye la chacana (cruz andina escalonada) animada, centrada en el
210/// viewport. El sol central late con sin(t); cuatro rayos cardinales
211/// rotan en una vuelta cada 12 s; halo cyan constante.
212fn build_chacana(scene: &mut vello::Scene, w: f64, h: f64, t: f64) {
213    let cx = w * 0.5;
214    let cy = h * 0.5;
215    let unit = (w.min(h)) * 0.06; // tamaño de la escala de la cruz
216
217    // Halo radial (anillo cyan suave)
218    scene.stroke(
219        &Stroke::new(2.0),
220        Affine::IDENTITY,
221        Color::from_rgba8(0xA6, 0xD8, 0xFF, 80),
222        None,
223        &Circle::new((cx, cy), unit * 4.6),
224    );
225    scene.stroke(
226        &Stroke::new(1.0),
227        Affine::IDENTITY,
228        Color::from_rgba8(0xA6, 0xD8, 0xFF, 140),
229        None,
230        &Circle::new((cx, cy), unit * 4.0),
231    );
232
233    // Rayos cardinales rotantes (4 trazos a 90°)
234    let theta = t * (std::f64::consts::TAU / 12.0); // 1 vuelta cada 12 s
235    let rotate = Affine::translate((cx, cy)) * Affine::rotate(theta);
236    for i in 0..4 {
237        let angle = i as f64 * std::f64::consts::FRAC_PI_2;
238        let dir = (angle.cos(), angle.sin());
239        let mut p = BezPath::new();
240        p.move_to((dir.0 * unit * 3.2, dir.1 * unit * 3.2));
241        p.line_to((dir.0 * unit * 4.4, dir.1 * unit * 4.4));
242        scene.stroke(
243            &Stroke::new(1.5),
244            rotate,
245            ACCENT_BLUE,
246            None,
247            &p,
248        );
249    }
250
251    // Chacana: cruz escalonada de 12 puntas. Construida como BezPath.
252    // La forma clásica: cuadrado central + escalones en 4 direcciones.
253    let chacana = chacana_path(unit);
254    let center = Affine::translate((cx, cy));
255
256    // Glow ambar exterior
257    scene.stroke(
258        &Stroke::new(6.0),
259        center,
260        Color::from_rgba8(0xE8, 0xC9, 0x7A, 110),
261        None,
262        &chacana,
263    );
264    // Outline cyan
265    scene.stroke(
266        &Stroke::new(2.0),
267        center,
268        ACCENT_CYAN,
269        None,
270        &chacana,
271    );
272    // Relleno violeta tenue
273    scene.fill(
274        Fill::NonZero,
275        center,
276        Color::from_rgba8(0xC3, 0x9C, 0xE8, 40),
277        None,
278        &chacana,
279    );
280
281    // Sol central que late
282    let pulse = 1.0 + 0.18 * (t * 1.8).sin();
283    let r_sun = unit * 0.7 * pulse;
284    scene.fill(
285        Fill::NonZero,
286        Affine::IDENTITY,
287        ACCENT_AMBER,
288        None,
289        &Circle::new((cx, cy), r_sun),
290    );
291    // Corona
292    scene.stroke(
293        &Stroke::new(1.0),
294        Affine::IDENTITY,
295        Color::from_rgba8(0xE8, 0xC9, 0x7A, 120),
296        None,
297        &Circle::new((cx, cy), r_sun * 1.7),
298    );
299    // Punto interior violeta para contraste
300    scene.fill(
301        Fill::NonZero,
302        Affine::IDENTITY,
303        ACCENT_VIOLET,
304        None,
305        &Circle::new((cx, cy), r_sun * 0.35),
306    );
307}
308
309/// Path de la chacana centrada en el origen, con `u` como ancho de cada
310/// escalón. Reconstruye la forma clásica de 12 esquinas escalonadas
311/// (3 escalones por cada brazo cardinal).
312fn chacana_path(u: f64) -> BezPath {
313    let mut p = BezPath::new();
314    // Empezamos en la esquina superior-derecha del brazo norte y vamos
315    // en sentido horario alrededor de toda la cruz.
316    p.move_to((u, 3.0 * u));
317    p.line_to((u, u));
318    p.line_to((3.0 * u, u));
319    p.line_to((3.0 * u, -u));
320    p.line_to((u, -u));
321    p.line_to((u, -3.0 * u));
322    p.line_to((-u, -3.0 * u));
323    p.line_to((-u, -u));
324    p.line_to((-3.0 * u, -u));
325    p.line_to((-3.0 * u, u));
326    p.line_to((-u, u));
327    p.line_to((-u, 3.0 * u));
328    p.close_path();
329    p
330}
331
332#[cfg(target_os = "android")]
333fn install_panic_logger() {
334    std::panic::set_hook(Box::new(|info| {
335        let payload = info
336            .payload()
337            .downcast_ref::<&str>()
338            .copied()
339            .or_else(|| info.payload().downcast_ref::<String>().map(|s| s.as_str()))
340            .unwrap_or("<unknown>");
341        let loc = info
342            .location()
343            .map(|l| format!("{}:{}", l.file(), l.line()))
344            .unwrap_or_else(|| "<?>".into());
345        log::error!("PANIC at {loc} — {payload}");
346    }));
347}
348
349#[cfg(target_os = "android")]
350#[no_mangle]
351fn android_main(app: android_activity::AndroidApp) {
352    android_logger::init_once(
353        android_logger::Config::default()
354            .with_max_level(log::LevelFilter::Info)
355            .with_tag(TAG),
356    );
357    install_panic_logger();
358    log::info!("android_main START");
359
360    use llimphi_hal::winit::event_loop::EventLoopBuilder;
361    use llimphi_hal::winit::platform::android::EventLoopBuilderExtAndroid;
362
363    let event_loop: EventLoop<()> = match EventLoopBuilder::default().with_android_app(app).build()
364    {
365        Ok(el) => el,
366        Err(e) => {
367            log::error!("EventLoop: {e}");
368            return;
369        }
370    };
371    event_loop.set_control_flow(ControlFlow::Poll);
372    let mut handler = App::new();
373    if let Err(e) = event_loop.run_app(&mut handler) {
374        log::error!("run_app: {e}");
375    }
376}