⎇ dc8554d1231f9f351385afa13d82d56504b6309f
crates/modules/pineal/phosphor/src/element.rs
Historia
Quién tocó qué
crudo
refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
1//! `LapalomaPhosphorElement` — Element GPUI con trail CRT.
2//!
3//! El render pinta el RingBuffer como N segmentos polilíneas con
4//! alpha decreciente del más nuevo al más viejo. Wraparound se
5//! parte en dos sub-polilíneas para no introducir la línea
6//! horizontal "del slot cap-1 al slot 0".
7
8use std::panic;
9
10use gpui::{
11 App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
12 Pixels, Style, Window,
13};
14
15use pineal_core::ring::RingBuffer;
16use pineal_render::{Canvas, Color, Rect, StrokeStyle, WindowCanvas};
17
18/// Cantidad de tramos del trail. Más tramos = gradiente más suave,
19/// más draw calls. 16 cubre la mayoría de los casos sin ser caro.
20const DEFAULT_TRAIL_SEGMENTS: usize = 16;
21
22pub struct LapalomaPhosphorElement {
23 pub buffer: RingBuffer,
24 /// Color y ancho base. El alpha se modula por tramo;
25 /// `base_stroke.color.a` es el alpha máximo (cabeza del trail).
26 pub base_stroke: StrokeStyle,
27 pub background: Option<Color>,
28 pub y_min: f32,
29 pub y_max: f32,
30 pub padding: f32,
31 pub trail_segments: usize,
32 /// Si > 0, se aplica una pasada adicional con `width × glow_width_mult`
33 /// y `alpha × glow_alpha` debajo del trazo principal — efecto halo CRT.
34 pub glow_width_mult: f32,
35 pub glow_alpha: f32,
36 scratch: Vec<f32>,
37}
38
39impl LapalomaPhosphorElement {
40 pub fn new(buffer: RingBuffer, base_stroke: StrokeStyle) -> Self {
41 Self {
42 buffer,
43 base_stroke,
44 background: None,
45 y_min: -1.0,
46 y_max: 1.0,
47 padding: 8.0,
48 trail_segments: DEFAULT_TRAIL_SEGMENTS,
49 glow_width_mult: 3.0,
50 glow_alpha: 0.25,
51 scratch: Vec::new(),
52 }
53 }
54
55 pub fn background(mut self, color: Color) -> Self {
56 self.background = Some(color);
57 self
58 }
59
60 pub fn y_range(mut self, min: f32, max: f32) -> Self {
61 debug_assert!(max > min);
62 self.y_min = min;
63 self.y_max = max;
64 self
65 }
66
67 pub fn trail_segments(mut self, n: usize) -> Self {
68 self.trail_segments = n.max(2);
69 self
70 }
71
72 pub fn glow(mut self, width_mult: f32, alpha: f32) -> Self {
73 self.glow_width_mult = width_mult;
74 self.glow_alpha = alpha;
75 self
76 }
77
78 pub fn no_glow(mut self) -> Self {
79 self.glow_width_mult = 0.0;
80 self.glow_alpha = 0.0;
81 self
82 }
83
84 fn plot_rect(&self, bounds: Rect) -> Rect {
85 Rect::new(
86 bounds.x + self.padding,
87 bounds.y + self.padding,
88 (bounds.w - self.padding * 2.0).max(1.0),
89 (bounds.h - self.padding * 2.0).max(1.0),
90 )
91 }
92}
93
94impl IntoElement for LapalomaPhosphorElement {
95 type Element = Self;
96 fn into_element(self) -> Self::Element {
97 self
98 }
99}
100
101impl Element for LapalomaPhosphorElement {
102 type RequestLayoutState = ();
103 type PrepaintState = ();
104
105 fn id(&self) -> Option<ElementId> {
106 None
107 }
108 fn source_location(&self) -> Option<&'static panic::Location<'static>> {
109 None
110 }
111
112 fn request_layout(
113 &mut self,
114 _id: Option<&GlobalElementId>,
115 _inspector_id: Option<&InspectorElementId>,
116 window: &mut Window,
117 cx: &mut App,
118 ) -> (LayoutId, Self::RequestLayoutState) {
119 let mut style = Style::default();
120 style.size.width = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
121 style.size.height = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
122 let id = window.request_layout(style, [], cx);
123 (id, ())
124 }
125
126 fn prepaint(
127 &mut self,
128 _id: Option<&GlobalElementId>,
129 _inspector_id: Option<&InspectorElementId>,
130 _bounds: Bounds<Pixels>,
131 _request_layout: &mut Self::RequestLayoutState,
132 _window: &mut Window,
133 _cx: &mut App,
134 ) -> Self::PrepaintState {
135 }
136
137 fn paint(
138 &mut self,
139 _id: Option<&GlobalElementId>,
140 _inspector_id: Option<&InspectorElementId>,
141 bounds: Bounds<Pixels>,
142 _request_layout: &mut Self::RequestLayoutState,
143 _prepaint: &mut Self::PrepaintState,
144 window: &mut Window,
145 _cx: &mut App,
146 ) {
147 let ox: f32 = bounds.origin.x.into();
148 let oy: f32 = bounds.origin.y.into();
149 let w: f32 = bounds.size.width.into();
150 let h: f32 = bounds.size.height.into();
151 let outer = Rect::new(ox, oy, w, h);
152 let plot = self.plot_rect(outer);
153
154 let mut canvas = WindowCanvas::new(window);
155
156 if let Some(bg) = self.background {
157 canvas.fill_rect(outer, bg);
158 }
159
160 let filled = self.buffer.filled_len();
161 if filled < 2 {
162 return;
163 }
164
165 let cap = self.buffer.capacity();
166 let head = self.buffer.head();
167 let coords = self.buffer.coords();
168
169 // El slot del sample temporalmente más viejo:
170 // - is_full → `head` (el siguiente a sobrescribirse).
171 // - !is_full → 0.
172 let start_slot = if self.buffer.is_full() { head } else { 0 };
173
174 let n_segs = self.trail_segments.min(filled / 2).max(2);
175 let base_per_seg = filled / n_segs;
176 let glow_enabled = self.glow_alpha > 0.0 && self.glow_width_mult > 1.0;
177
178 for k in 0..n_segs {
179 // Rango temporal del segmento. El segmento `k` cubre los
180 // samples [k*base_per_seg, (k+1)*base_per_seg). El último
181 // incluye el remainder.
182 let t_lo = k * base_per_seg;
183 let t_hi = if k == n_segs - 1 {
184 filled
185 } else {
186 // +1 incluye el primer sample del siguiente segmento
187 // para que las polilíneas se "toquen" sin gap visual.
188 ((k + 1) * base_per_seg) + 1
189 };
190 if t_hi <= t_lo + 1 {
191 continue;
192 }
193
194 // Alpha decrece linealmente del más nuevo al más viejo.
195 // k = n_segs - 1 → 1.0; k = 0 → 1/n_segs.
196 let life = (k as f32 + 1.0) / n_segs as f32;
197 let alpha = self.base_stroke.color.a * life;
198 let mut color = self.base_stroke.color;
199 color.a = alpha;
200 let stroke = StrokeStyle::new(self.base_stroke.width, color);
201
202 // Glow underneath, mismo path con más ancho y menos alpha.
203 let glow_stroke = if glow_enabled {
204 let mut gc = color;
205 gc.a *= self.glow_alpha;
206 Some(StrokeStyle::new(
207 self.base_stroke.width * self.glow_width_mult,
208 gc,
209 ))
210 } else {
211 None
212 };
213
214 // Proyectar el rango temporal a slots físicos, partiendo
215 // si cruzamos el final del buffer.
216 let seg_len = t_hi - t_lo;
217 let abs_start = (start_slot + t_lo) % cap;
218 let contiguous_len = cap - abs_start;
219
220 if seg_len <= contiguous_len {
221 let slice = &coords[abs_start * 2..(abs_start + seg_len) * 2];
222 self.scratch.clear();
223 project_segment(slice, plot, self.y_min, self.y_max, &mut self.scratch);
224 if self.scratch.len() >= 4 {
225 if let Some(gs) = glow_stroke {
226 canvas.stroke_polyline(&self.scratch, gs);
227 }
228 canvas.stroke_polyline(&self.scratch, stroke);
229 }
230 } else {
231 // Wraparound: dos sub-polilíneas separadas.
232 let slice_a = &coords[abs_start * 2..];
233 self.scratch.clear();
234 project_segment(slice_a, plot, self.y_min, self.y_max, &mut self.scratch);
235 if self.scratch.len() >= 4 {
236 if let Some(gs) = glow_stroke {
237 canvas.stroke_polyline(&self.scratch, gs);
238 }
239 canvas.stroke_polyline(&self.scratch, stroke);
240 }
241
242 let remaining = seg_len - contiguous_len;
243 let slice_b = &coords[..remaining * 2];
244 self.scratch.clear();
245 project_segment(slice_b, plot, self.y_min, self.y_max, &mut self.scratch);
246 if self.scratch.len() >= 4 {
247 if let Some(gs) = glow_stroke {
248 canvas.stroke_polyline(&self.scratch, gs);
249 }
250 canvas.stroke_polyline(&self.scratch, stroke);
251 }
252 }
253 }
254 }
255}
256
257/// Helper builder.
258pub fn pineal_phosphor(
259 buffer: RingBuffer,
260 base_stroke: StrokeStyle,
261) -> LapalomaPhosphorElement {
262 LapalomaPhosphorElement::new(buffer, base_stroke)
263}
264
265/// Proyecta `[x_norm, y_value, …]` del ring a píxeles del plot.
266fn project_segment(segment: &[f32], plot: Rect, y_min: f32, y_max: f32, out: &mut Vec<f32>) {
267 let y_span = y_max - y_min;
268 if y_span.abs() < 1e-9 {
269 return;
270 }
271 let inv = 1.0 / y_span;
272 for chunk in segment.chunks_exact(2) {
273 let xn = chunk[0];
274 let yv = chunk[1];
275 let py_norm = (yv - y_min) * inv;
276 out.push(plot.x + xn * plot.w);
277 out.push(plot.bottom() - py_norm * plot.h);
278 }
279}