tawasuyu/pineal
Backend-agnostic GPU visualization — a catalog of painters over one Canvas trait, in Rust.
⎇ 514f17ba57e75d7e5db3a54d703d47377ca10fe6
00_unanchay/pineal/demos/pineal-heatmap-demo/src/main.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat: pineal standalone — catálogo de painters GPU sobre Llimphi (front-door liviano)
1//! `pineal-heatmap-demo` — campo 2D con onda viajera.
2//!
3//! Matriz 48×32 que se reescribe cada 33 ms (≈ 30 Hz). El valor de
4//! cada celda combina dos sinusoides con desplazamiento de fase
5//! ligado al tick: `sin(x·0.25 - t·0.1) + cos(y·0.30 + t·0.07)`.
6//! El ramp Viridis mapea `[min, max]` de la matriz a color.
7//!
8//! Cableado de UI: barra de menú principal (Archivo / Ver / Ayuda) +
9//! menú contextual sobre el plot (right-click). Como es un canvas sin
10//! texto editable no hay menú Editar ni clipboard.
11
12use std::sync::{Arc, Mutex};
13use std::time::Duration;
14
15use llimphi_theme::Theme;
16use llimphi_ui::llimphi_layout::taffy::prelude::{length, percent, FlexDirection, Size, Style};
17use llimphi_ui::llimphi_layout::taffy::Rect as TaffyRect;
18use llimphi_ui::llimphi_text::Alignment;
19use llimphi_ui::{App, Handle, View};
20use llimphi_widget_context_menu::{
21 context_menu_view, ContextMenuItem, ContextMenuPalette, ContextMenuSpec,
22};
23use llimphi_widget_menubar::{menubar_overlay, menubar_view, MenuBarSpec, DEFAULT_HEIGHT as MENU_H};
24
25use app_bus::{AppMenu, Menu, MenuItem};
26
27use pineal_heatmap::{paint, HeatmapMatrix, Ramp};
28use pineal_render::{Canvas as _, Color, Rect, SceneCanvas};
29
30const W: usize = 48;
31const H: usize = 32;
32const TICK_PERIOD: Duration = Duration::from_millis(33);
33
34#[derive(Clone)]
35enum Msg {
36 Tick,
37 /// Reinicia el campo al tick 0 (la "vista" del heatmap).
38 Reset,
39 /// Barra de menú principal: abrir/cerrar un menú raíz (`None` cerrar).
40 MenuOpen(Option<usize>),
41 /// Comando elegido en el menú principal — se traduce al `Msg` real.
42 MenuCommand(String),
43 /// Cierra cualquier menú abierto (click-fuera / Esc).
44 CloseMenus,
45 /// Cicla el tema claro/oscuro.
46 CycleTheme,
47 /// Right-click en la raíz → abre el menú contextual en `(x, y)`.
48 ContextMenuOpen(f32, f32),
49}
50
51struct Model {
52 matrix: Arc<Mutex<HeatmapMatrix>>,
53 t: u64,
54 theme: Theme,
55 menu_open: Option<usize>,
56 context_menu: Option<(f32, f32)>,
57}
58
59struct HeatmapDemo;
60
61impl App for HeatmapDemo {
62 type Model = Model;
63 type Msg = Msg;
64
65 fn title() -> &'static str {
66 "Lapaloma — heatmap 48×32 (Viridis · onda viajera)"
67 }
68 fn initial_size() -> (u32, u32) {
69 (960, 600)
70 }
71
72 fn init(handle: &Handle<Msg>) -> Model {
73 handle.spawn_periodic(TICK_PERIOD, || Msg::Tick);
74 let mut m = HeatmapMatrix::new(W, H);
75 fill(&mut m, 0);
76 Model {
77 matrix: Arc::new(Mutex::new(m)),
78 t: 0,
79 theme: Theme::dark(),
80 menu_open: None,
81 context_menu: None,
82 }
83 }
84
85 fn update(mut model: Model, msg: Msg, handle: &Handle<Msg>) -> Model {
86 match msg {
87 Msg::Tick => {
88 model.t = model.t.wrapping_add(1);
89 if let Ok(mut m) = model.matrix.lock() {
90 fill(&mut m, model.t);
91 }
92 }
93 Msg::Reset => {
94 model.t = 0;
95 if let Ok(mut m) = model.matrix.lock() {
96 fill(&mut m, 0);
97 }
98 }
99 Msg::MenuOpen(which) => {
100 model.menu_open = which;
101 model.context_menu = None;
102 }
103 Msg::MenuCommand(cmd) => {
104 model.menu_open = None;
105 return handle_menu_command(model, &cmd, handle);
106 }
107 Msg::CloseMenus => {
108 model.menu_open = None;
109 model.context_menu = None;
110 }
111 Msg::CycleTheme => {
112 model.theme = Theme::next_after(model.theme.name);
113 }
114 Msg::ContextMenuOpen(x, y) => {
115 model.menu_open = None;
116 model.context_menu = Some((x, y));
117 }
118 }
119 model
120 }
121
122 fn view(model: &Model) -> View<Msg> {
123 let theme = &model.theme;
124 let plot_bg = Color::rgba(0.06, 0.07, 0.10, 1.0);
125 let matrix = model.matrix.clone();
126
127 let menu = app_menu();
128 let menubar = menubar_view(&menubar_spec(&menu, model));
129
130 let header = View::new(Style {
131 size: Size { width: percent(1.0_f32), height: length(28.0_f32) },
132 ..Default::default()
133 })
134 .text_aligned(
135 "Lapaloma — heatmap".to_string(),
136 18.0,
137 theme.fg_text,
138 Alignment::Start,
139 );
140
141 let stats = format!("matriz {}×{} · tick = {} · ramp = Viridis", W, H, model.t);
142 let legend = View::new(Style {
143 size: Size { width: percent(1.0_f32), height: length(20.0_f32) },
144 ..Default::default()
145 })
146 .text_aligned(stats, 11.0, theme.fg_muted, Alignment::Start);
147
148 let panel = View::new(Style {
149 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
150 flex_grow: 1.0,
151 ..Default::default()
152 })
153 .clip(true)
154 .paint_with(move |scene, ts, rect| {
155 let outer = Rect::new(rect.x, rect.y, rect.w, rect.h);
156 let mut canvas = SceneCanvas::new(scene, ts);
157 canvas.fill_rect(outer, plot_bg);
158 if let Ok(m) = matrix.lock() {
159 paint(&m, Ramp::Viridis, outer, &mut canvas);
160 }
161 });
162
163 let body = View::new(Style {
164 flex_direction: FlexDirection::Column,
165 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
166 flex_grow: 1.0,
167 padding: TaffyRect {
168 left: length(16.0_f32),
169 right: length(16.0_f32),
170 top: length(16.0_f32),
171 bottom: length(16.0_f32),
172 },
173 gap: Size { width: length(0.0_f32), height: length(10.0_f32) },
174 ..Default::default()
175 })
176 .fill(theme.bg_app)
177 .children(vec![header, legend, panel]);
178
179 View::new(Style {
180 flex_direction: FlexDirection::Column,
181 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
182 ..Default::default()
183 })
184 .fill(theme.bg_app)
185 .on_right_click_at(|x, y, _w, _h| Some(Msg::ContextMenuOpen(x, y)))
186 .children(vec![menubar, body])
187 }
188
189 fn view_overlay(model: &Model) -> Option<View<Msg>> {
190 if let Some((x, y)) = model.context_menu {
191 let items = vec![
192 ContextMenuItem::action("Reiniciar vista"),
193 ContextMenuItem::action("Cambiar tema"),
194 ];
195 let on_pick: Arc<dyn Fn(usize) -> Msg + Send + Sync> =
196 Arc::new(|i: usize| match i {
197 0 => Msg::Reset,
198 _ => Msg::CycleTheme,
199 });
200 return Some(context_menu_view(ContextMenuSpec {
201 anchor: (x, y),
202 viewport: viewport_of(model),
203 header: Some("Heatmap".to_string()),
204 items,
205 active: usize::MAX,
206 on_pick,
207 on_dismiss: Msg::CloseMenus,
208 palette: ContextMenuPalette::from_theme(&model.theme),
209 }));
210 }
211 let menu = app_menu();
212 menubar_overlay(&menubar_spec(&menu, model))
213 }
214}
215
216fn viewport_of(_model: &Model) -> (f32, f32) {
217 let (w, h) = HeatmapDemo::initial_size();
218 (w as f32, h as f32)
219}
220
221fn menubar_spec<'a>(menu: &'a AppMenu, model: &'a Model) -> MenuBarSpec<'a, Msg> {
222 MenuBarSpec {
223 menu,
224 open: model.menu_open,
225 theme: &model.theme,
226 viewport: viewport_of(model),
227 height: MENU_H,
228 on_open: Arc::new(Msg::MenuOpen),
229 on_command: Arc::new(|c: &str| Msg::MenuCommand(c.to_string())),
230 }
231}
232
233fn app_menu() -> AppMenu {
234 AppMenu::new()
235 .menu(Menu::new("Archivo").item(MenuItem::new("Salir", "file.quit").shortcut("Ctrl+Q")))
236 .menu(
237 Menu::new("Ver")
238 .item(MenuItem::new("Reiniciar vista", "view.reset"))
239 .item(MenuItem::new("Cambiar tema", "view.theme").separated()),
240 )
241 .menu(Menu::new("Ayuda").item(MenuItem::new("Acerca de", "help.about")))
242}
243
244fn handle_menu_command(model: Model, cmd: &str, handle: &Handle<Msg>) -> Model {
245 match cmd {
246 "file.quit" => std::process::exit(0),
247 "view.reset" => {
248 handle.dispatch(Msg::Reset);
249 model
250 }
251 "view.theme" => {
252 handle.dispatch(Msg::CycleTheme);
253 model
254 }
255 // "help.about" y desconocidos: no-op (sin diálogo todavía).
256 _ => model,
257 }
258}
259
260fn fill(m: &mut HeatmapMatrix, t: u64) {
261 let phase = t as f32 * 0.1;
262 let phase2 = t as f32 * 0.07;
263 let mut data = Vec::with_capacity(W * H);
264 for y in 0..H {
265 for x in 0..W {
266 let v = (x as f32 * 0.25 - phase).sin() + (y as f32 * 0.30 + phase2).cos();
267 data.push(v);
268 }
269 }
270 m.replace_data(data);
271}
272
273fn main() {
274 llimphi_ui::run::<HeatmapDemo>();
275}