⎇ 1fdbd358c8cd914fb767cbee1483d79935aa1fec
crates/modules/pineal/stream/src/element.rs
History
Who touched what
raw
refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
1//! `LapalomaStreamElement` — Element GPUI para visualización de
2//! telemetría con RingBuffer.
3//!
4//! Modo **sweep** (canónico del osciloscopio):
5//! - Los slots `[0, capacity)` tienen `x_norm` fijo precomputado.
6//! - El `head` marca el slot donde se va a escribir el próximo
7//! sample → visualmente, el "cursor" que separa la traza vieja
8//! (a la derecha del head) de la nueva (a la izquierda).
9//! - Render en **dos segmentos** split-at-head para evitar la
10//! línea horizontal del wraparound. Antes del fill (count <
11//! capacity), sólo se pinta `[0, head)`.
12
13use std::panic;
14
15use gpui::{
16 App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
17 Pixels, Style, Window,
18};
19
20use pineal_core::ring::RingBuffer;
21use pineal_render::{Canvas, Color, Rect, StrokeStyle, WindowCanvas};
22
23/// Element que pinta un `RingBuffer` en modo sweep.
24pub struct LapalomaStreamElement {
25 pub buffer: RingBuffer,
26 pub stroke: StrokeStyle,
27 pub background: Option<Color>,
28 /// Rango Y para proyectar los samples a píxeles. Default `-1..1`.
29 pub y_min: f32,
30 pub y_max: f32,
31 pub padding: f32,
32 /// Scratch reusable entre los dos segmentos del frame.
33 scratch: Vec<f32>,
34}
35
36impl LapalomaStreamElement {
37 pub fn new(buffer: RingBuffer, stroke: StrokeStyle) -> Self {
38 Self {
39 buffer,
40 stroke,
41 background: None,
42 y_min: -1.0,
43 y_max: 1.0,
44 padding: 8.0,
45 scratch: Vec::new(),
46 }
47 }
48
49 pub fn background(mut self, color: Color) -> Self {
50 self.background = Some(color);
51 self
52 }
53
54 pub fn y_range(mut self, min: f32, max: f32) -> Self {
55 debug_assert!(max > min);
56 self.y_min = min;
57 self.y_max = max;
58 self
59 }
60
61 pub fn padding(mut self, px: f32) -> Self {
62 self.padding = px;
63 self
64 }
65
66 fn plot_rect(&self, bounds: Rect) -> Rect {
67 Rect::new(
68 bounds.x + self.padding,
69 bounds.y + self.padding,
70 (bounds.w - self.padding * 2.0).max(1.0),
71 (bounds.h - self.padding * 2.0).max(1.0),
72 )
73 }
74
75}
76
77/// Proyecta una slice de coords `[x_norm, y_value, …]` del
78/// RingBuffer al sistema de píxeles del plot. `out` se extiende
79/// (no se clearea acá; el caller decide).
80fn project_segment(segment: &[f32], plot: Rect, y_min: f32, y_max: f32, out: &mut Vec<f32>) {
81 let y_span = y_max - y_min;
82 if y_span.abs() < 1e-9 {
83 return;
84 }
85 let inv_y_span = 1.0 / y_span;
86 for chunk in segment.chunks_exact(2) {
87 let xn = chunk[0];
88 let yv = chunk[1];
89 let py_norm = (yv - y_min) * inv_y_span;
90 let px = plot.x + xn * plot.w;
91 let py = plot.bottom() - py_norm * plot.h;
92 out.push(px);
93 out.push(py);
94 }
95}
96
97impl IntoElement for LapalomaStreamElement {
98 type Element = Self;
99 fn into_element(self) -> Self::Element {
100 self
101 }
102}
103
104impl Element for LapalomaStreamElement {
105 type RequestLayoutState = ();
106 type PrepaintState = ();
107
108 fn id(&self) -> Option<ElementId> {
109 None
110 }
111
112 fn source_location(&self) -> Option<&'static panic::Location<'static>> {
113 None
114 }
115
116 fn request_layout(
117 &mut self,
118 _id: Option<&GlobalElementId>,
119 _inspector_id: Option<&InspectorElementId>,
120 window: &mut Window,
121 cx: &mut App,
122 ) -> (LayoutId, Self::RequestLayoutState) {
123 let mut style = Style::default();
124 style.size.width = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
125 style.size.height = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
126 let id = window.request_layout(style, [], cx);
127 (id, ())
128 }
129
130 fn prepaint(
131 &mut self,
132 _id: Option<&GlobalElementId>,
133 _inspector_id: Option<&InspectorElementId>,
134 _bounds: Bounds<Pixels>,
135 _request_layout: &mut Self::RequestLayoutState,
136 _window: &mut Window,
137 _cx: &mut App,
138 ) -> Self::PrepaintState {
139 }
140
141 fn paint(
142 &mut self,
143 _id: Option<&GlobalElementId>,
144 _inspector_id: Option<&InspectorElementId>,
145 bounds: Bounds<Pixels>,
146 _request_layout: &mut Self::RequestLayoutState,
147 _prepaint: &mut Self::PrepaintState,
148 window: &mut Window,
149 _cx: &mut App,
150 ) {
151 let ox: f32 = bounds.origin.x.into();
152 let oy: f32 = bounds.origin.y.into();
153 let w: f32 = bounds.size.width.into();
154 let h: f32 = bounds.size.height.into();
155 let outer = Rect::new(ox, oy, w, h);
156 let plot = self.plot_rect(outer);
157
158 let mut canvas = WindowCanvas::new(window);
159
160 if let Some(bg) = self.background {
161 canvas.fill_rect(outer, bg);
162 }
163
164 let coords = self.buffer.coords();
165 let head = self.buffer.head();
166 let cap = self.buffer.capacity();
167
168 if !self.buffer.is_full() {
169 // Pre-fill: sólo [0, head). Evita la línea plana del
170 // 1.0.2 fix del Flutter doc.
171 let filled = head;
172 if filled >= 2 {
173 let slice = &coords[..filled * 2];
174 self.scratch.clear();
175 project_segment(slice, plot, self.y_min, self.y_max, &mut self.scratch);
176 canvas.stroke_polyline(&self.scratch, self.stroke);
177 }
178 return;
179 }
180
181 // Ya filled — dos segmentos split-at-head.
182 let split = head * 2;
183 // Segmento "viejo": [head*2 .. cap*2) — temporalmente más antiguo.
184 if split < cap * 2 {
185 let seg1 = &coords[split..];
186 if seg1.len() >= 4 {
187 self.scratch.clear();
188 project_segment(seg1, plot, self.y_min, self.y_max, &mut self.scratch);
189 canvas.stroke_polyline(&self.scratch, self.stroke);
190 }
191 }
192 // Segmento "nuevo": [0 .. head*2) — más reciente, dibujado a
193 // la izquierda del cursor.
194 if split > 0 {
195 let seg2 = &coords[..split];
196 if seg2.len() >= 4 {
197 self.scratch.clear();
198 project_segment(seg2, plot, self.y_min, self.y_max, &mut self.scratch);
199 canvas.stroke_polyline(&self.scratch, self.stroke);
200 }
201 }
202 }
203}
204
205/// Helper builder-style.
206pub fn pineal_stream(buffer: RingBuffer, stroke: StrokeStyle) -> LapalomaStreamElement {
207 LapalomaStreamElement::new(buffer, stroke)
208}