presentar_terminal/widgets/
horizon.rs1use presentar_core::{
12 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
13 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
14};
15use std::any::Any;
16use std::time::Duration;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum HorizonScheme {
21 #[default]
23 Blues,
24 Reds,
26 Greens,
28 Purples,
30}
31
32impl HorizonScheme {
33 fn band_colors(self, bands: u8) -> Vec<Color> {
35 let base = match self {
36 Self::Blues => (0.2, 0.4, 0.9),
37 Self::Reds => (0.9, 0.3, 0.2),
38 Self::Greens => (0.2, 0.8, 0.3),
39 Self::Purples => (0.7, 0.3, 0.9),
40 };
41
42 (0..bands)
43 .map(|i| {
44 let factor = 0.4 + 0.6 * (i as f32 / bands as f32);
45 Color::new(base.0 * factor, base.1 * factor, base.2 * factor, 1.0)
46 })
47 .collect()
48 }
49}
50
51#[derive(Debug, Clone)]
65pub struct HorizonGraph {
66 data: Vec<f64>,
68 bands: u8,
70 scheme: HorizonScheme,
72 label: Option<String>,
74 bounds: Rect,
76}
77
78impl Default for HorizonGraph {
79 fn default() -> Self {
80 Self::new(Vec::new())
81 }
82}
83
84impl HorizonGraph {
85 #[must_use]
87 pub fn new(data: Vec<f64>) -> Self {
88 Self {
89 data,
90 bands: 3,
91 scheme: HorizonScheme::default(),
92 label: None,
93 bounds: Rect::default(),
94 }
95 }
96
97 #[must_use]
99 pub fn with_bands(mut self, bands: u8) -> Self {
100 debug_assert!((1..=6).contains(&bands), "bands must be 1-6");
101 self.bands = bands.clamp(1, 6);
102 self
103 }
104
105 #[must_use]
107 pub fn with_scheme(mut self, scheme: HorizonScheme) -> Self {
108 self.scheme = scheme;
109 self
110 }
111
112 #[must_use]
114 pub fn with_label(mut self, label: impl Into<String>) -> Self {
115 self.label = Some(label.into());
116 self
117 }
118
119 pub fn set_data(&mut self, data: Vec<f64>) {
121 self.data = data;
122 }
123
124 fn value_to_band(&self, value: f64) -> (u8, f64) {
126 let clamped = value.clamp(0.0, 1.0);
127 let band_height = 1.0 / self.bands as f64;
128 let band = (clamped / band_height).floor() as u8;
129 let within_band = (clamped % band_height) / band_height;
130 (band.min(self.bands - 1), within_band)
131 }
132
133 fn render_horizon(&self, canvas: &mut dyn Canvas) {
135 if self.data.is_empty() || self.bounds.width < 1.0 || self.bounds.height < 1.0 {
136 return;
137 }
138
139 let colors = self.scheme.band_colors(self.bands);
140 let width = self.bounds.width as usize;
141 let height = self.bounds.height as usize;
142
143 let data_len = self.data.len();
145 let step = if data_len > width {
146 data_len as f64 / width as f64
147 } else {
148 1.0
149 };
150
151 let blocks = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
153
154 for x in 0..width.min(data_len) {
155 let idx = (x as f64 * step) as usize;
156 if idx >= data_len {
157 break;
158 }
159
160 let value = self.data[idx];
161 let (band, intensity) = self.value_to_band(value);
162
163 let block_idx = (intensity * 7.0) as usize;
165 let block = blocks[block_idx.min(7)];
166
167 let color = if (band as usize) < colors.len() {
169 colors[band as usize]
170 } else {
171 colors[colors.len() - 1]
172 };
173
174 let style = TextStyle {
176 color,
177 ..Default::default()
178 };
179 canvas.draw_text(
180 &block.to_string(),
181 Point::new(
182 self.bounds.x + x as f32,
183 self.bounds.y + height as f32 - 1.0,
184 ),
185 &style,
186 );
187
188 for b in 0..band {
190 let offset = 2 + b as usize;
191 if height > offset {
192 let layer_color = if (b as usize) < colors.len() {
193 colors[b as usize]
194 } else {
195 colors[0]
196 };
197 let layer_style = TextStyle {
198 color: layer_color,
199 ..Default::default()
200 };
201 canvas.draw_text(
202 "█",
203 Point::new(
204 self.bounds.x + x as f32,
205 self.bounds.y + (height - offset) as f32,
206 ),
207 &layer_style,
208 );
209 }
210 }
211 }
212
213 if let Some(ref label) = self.label {
215 let style = TextStyle {
216 color: Color::WHITE,
217 ..Default::default()
218 };
219 canvas.draw_text(label, Point::new(self.bounds.x, self.bounds.y), &style);
220 }
221 }
222}
223
224impl Widget for HorizonGraph {
225 fn type_id(&self) -> TypeId {
226 TypeId::of::<Self>()
227 }
228
229 fn measure(&self, constraints: Constraints) -> Size {
230 let width = constraints.max_width.min(self.data.len() as f32);
231 let height = constraints.max_height.min(self.bands as f32 + 1.0);
232 Size::new(width, height)
233 }
234
235 fn layout(&mut self, bounds: Rect) -> LayoutResult {
236 self.bounds = bounds;
237 LayoutResult {
238 size: Size::new(bounds.width, bounds.height),
239 }
240 }
241
242 fn paint(&self, canvas: &mut dyn Canvas) {
243 self.render_horizon(canvas);
244 }
245
246 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
247 None
248 }
249
250 fn children(&self) -> &[Box<dyn Widget>] {
251 &[]
252 }
253
254 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
255 &mut []
256 }
257}
258
259impl Brick for HorizonGraph {
260 fn brick_name(&self) -> &'static str {
261 "horizon_graph"
262 }
263
264 fn assertions(&self) -> &[BrickAssertion] {
265 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
266 ASSERTIONS
267 }
268
269 fn budget(&self) -> BrickBudget {
270 BrickBudget::uniform(16)
271 }
272
273 fn verify(&self) -> BrickVerification {
274 BrickVerification {
275 passed: self.assertions().to_vec(),
276 failed: vec![],
277 verification_time: Duration::from_micros(5),
278 }
279 }
280
281 fn to_html(&self) -> String {
282 String::new()
283 }
284
285 fn to_css(&self) -> String {
286 String::new()
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_horizon_graph_default() {
296 let graph = HorizonGraph::default();
297 assert!(graph.data.is_empty());
298 assert_eq!(graph.bands, 3);
299 }
300
301 #[test]
302 fn test_horizon_graph_with_data() {
303 let graph = HorizonGraph::new(vec![0.1, 0.5, 0.9])
304 .with_bands(4)
305 .with_label("CPU0");
306 assert_eq!(graph.data.len(), 3);
307 assert_eq!(graph.bands, 4);
308 assert_eq!(graph.label, Some("CPU0".to_string()));
309 }
310
311 #[test]
312 fn test_value_to_band() {
313 let graph = HorizonGraph::new(vec![]).with_bands(3);
314
315 let (band, _) = graph.value_to_band(0.1);
316 assert_eq!(band, 0);
317
318 let (band, _) = graph.value_to_band(0.5);
319 assert_eq!(band, 1);
320
321 let (band, _) = graph.value_to_band(0.9);
322 assert_eq!(band, 2);
323 }
324
325 #[test]
326 fn test_band_colors() {
327 let colors = HorizonScheme::Blues.band_colors(3);
328 assert_eq!(colors.len(), 3);
329 }
330
331 #[test]
332 fn test_horizon_implements_widget() {
333 let mut graph = HorizonGraph::new(vec![0.5, 0.6, 0.7]);
334 let size = graph.measure(Constraints {
335 min_width: 0.0,
336 min_height: 0.0,
337 max_width: 100.0,
338 max_height: 10.0,
339 });
340 assert!(size.width > 0.0);
341 assert!(size.height > 0.0);
342 }
343
344 #[test]
345 fn test_horizon_implements_brick() {
346 let graph = HorizonGraph::new(vec![0.5]);
347 assert_eq!(graph.brick_name(), "horizon_graph");
348 assert!(graph.verify().is_valid());
349 }
350
351 #[test]
352 fn test_horizon_event() {
353 let mut graph = HorizonGraph::new(vec![]);
354 let event = Event::KeyDown {
355 key: presentar_core::Key::Enter,
356 };
357 assert!(graph.event(&event).is_none());
358 }
359
360 #[test]
361 fn test_horizon_children() {
362 let graph = HorizonGraph::new(vec![]);
363 assert!(graph.children().is_empty());
364 }
365
366 #[test]
367 fn test_horizon_children_mut() {
368 let mut graph = HorizonGraph::new(vec![]);
369 assert!(graph.children_mut().is_empty());
370 }
371
372 #[test]
373 fn test_horizon_to_html() {
374 let graph = HorizonGraph::new(vec![]);
375 assert!(graph.to_html().is_empty());
376 }
377
378 #[test]
379 fn test_horizon_to_css() {
380 let graph = HorizonGraph::new(vec![]);
381 assert!(graph.to_css().is_empty());
382 }
383
384 #[test]
385 fn test_horizon_budget() {
386 let graph = HorizonGraph::new(vec![]);
387 let budget = graph.budget();
388 assert!(budget.paint_ms > 0);
389 }
390
391 #[test]
392 fn test_horizon_assertions() {
393 let graph = HorizonGraph::new(vec![]);
394 assert!(!graph.assertions().is_empty());
395 }
396
397 #[test]
398 fn test_horizon_type_id() {
399 let graph = HorizonGraph::new(vec![]);
400 assert_eq!(Widget::type_id(&graph), TypeId::of::<HorizonGraph>());
401 }
402
403 #[test]
404 fn test_horizon_layout_and_paint() {
405 use crate::direct::{CellBuffer, DirectTerminalCanvas};
406
407 let mut graph = HorizonGraph::new(vec![0.1, 0.3, 0.5, 0.7, 0.9])
408 .with_bands(4)
409 .with_label("Test")
410 .with_scheme(HorizonScheme::Blues);
411
412 let bounds = presentar_core::Rect::new(0.0, 0.0, 40.0, 3.0);
413 graph.layout(bounds);
414
415 let mut buffer = CellBuffer::new(40, 3);
416 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
417 graph.paint(&mut canvas);
418 }
420
421 #[test]
422 fn test_horizon_all_schemes() {
423 use crate::direct::{CellBuffer, DirectTerminalCanvas};
424
425 for scheme in [
426 HorizonScheme::Blues,
427 HorizonScheme::Greens,
428 HorizonScheme::Reds,
429 HorizonScheme::Purples,
430 ] {
431 let mut graph = HorizonGraph::new(vec![0.2, 0.5, 0.8]).with_scheme(scheme);
432 let bounds = presentar_core::Rect::new(0.0, 0.0, 20.0, 2.0);
433 graph.layout(bounds);
434
435 let mut buffer = CellBuffer::new(20, 2);
436 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
437 graph.paint(&mut canvas);
438 }
439 }
440
441 #[test]
442 fn test_horizon_different_band_counts() {
443 use crate::direct::{CellBuffer, DirectTerminalCanvas};
444
445 for bands in [2, 3, 4, 5, 6] {
446 let mut graph = HorizonGraph::new(vec![0.1, 0.5, 0.9]).with_bands(bands);
447 let bounds = presentar_core::Rect::new(0.0, 0.0, 30.0, 2.0);
448 graph.layout(bounds);
449
450 let mut buffer = CellBuffer::new(30, 2);
451 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
452 graph.paint(&mut canvas);
453 }
454 }
455
456 #[test]
457 fn test_horizon_empty_data() {
458 use crate::direct::{CellBuffer, DirectTerminalCanvas};
459
460 let mut graph = HorizonGraph::new(vec![]);
461 let bounds = presentar_core::Rect::new(0.0, 0.0, 20.0, 2.0);
462 graph.layout(bounds);
463
464 let mut buffer = CellBuffer::new(20, 2);
465 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
466 graph.paint(&mut canvas);
467 }
468
469 #[test]
470 fn test_horizon_edge_values() {
471 let graph = HorizonGraph::new(vec![]).with_bands(4);
472
473 let (band, _) = graph.value_to_band(0.0);
475 assert_eq!(band, 0);
476
477 let (band, _) = graph.value_to_band(1.0);
478 assert_eq!(band, 3); let (band, _) = graph.value_to_band(-0.1);
481 assert_eq!(band, 0); let (band, _) = graph.value_to_band(1.5);
484 assert_eq!(band, 3); }
486}