Skip to main content

shape_viz_core/layers/
current_price.rs

1//! Current price line layer
2
3use crate::data::ChartData;
4use crate::error::Result;
5use crate::layers::{Layer, LayerStage};
6use crate::renderer::RenderContext;
7use crate::style::ChartStyle;
8use crate::theme::{ChartTheme, Color};
9use crate::viewport::{Rect, Viewport};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13/// Configuration for current price line
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CurrentPriceConfig {
16    /// Show the horizontal price line
17    pub show_line: bool,
18    /// Show the price label box
19    pub show_label: bool,
20    /// Line style (solid, dashed, dotted)
21    pub line_style: LineStyle,
22    /// Line width
23    pub line_width: f32,
24    /// Label padding
25    pub label_padding: f32,
26    /// Custom line color (if None, uses theme)
27    pub line_color: Option<[f32; 4]>,
28    /// Custom label background color (if None, uses candle color)
29    pub label_bg_color: Option<[f32; 4]>,
30    /// Custom label text color (if None, uses theme)
31    pub label_text_color: Option<[f32; 4]>,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
35pub enum LineStyle {
36    Solid,
37    Dashed,
38    Dotted,
39}
40
41impl Default for CurrentPriceConfig {
42    fn default() -> Self {
43        Self {
44            show_line: true,
45            show_label: true,
46            line_style: LineStyle::Dashed, // Dashed line like TradingView
47            line_width: 1.0,
48            label_padding: 4.0,
49            line_color: None,
50            label_bg_color: None,
51            label_text_color: None,
52        }
53    }
54}
55
56/// Layer for rendering current price line and label
57#[derive(Debug)]
58pub struct CurrentPriceLayer {
59    enabled: bool,
60    needs_render: bool,
61    config: CurrentPriceConfig,
62    current_price: Option<f64>,
63    is_bullish: bool,
64    symbol: String,
65}
66
67impl CurrentPriceLayer {
68    pub fn new() -> Self {
69        Self {
70            enabled: true,
71            needs_render: true,
72            config: CurrentPriceConfig::default(),
73            current_price: None,
74            is_bullish: true,
75            symbol: String::new(),
76        }
77    }
78
79    pub fn with_config(config: CurrentPriceConfig) -> Self {
80        Self {
81            config,
82            ..Self::new()
83        }
84    }
85
86    /// Update configuration
87    pub fn set_config(&mut self, config: CurrentPriceConfig) {
88        self.config = config;
89        self.needs_render = true;
90    }
91
92    /// Draw dashed or dotted line
93    fn draw_styled_line(
94        &self,
95        context: &mut RenderContext,
96        start: [f32; 2],
97        end: [f32; 2],
98        color: Color,
99        width: f32,
100        dash: f32,
101        gap: f32,
102    ) {
103        match self.config.line_style {
104            LineStyle::Solid => {
105                context.draw_line(start, end, color, width);
106            }
107            LineStyle::Dashed => {
108                let total_length =
109                    ((end[0] - start[0]).powi(2) + (end[1] - start[1]).powi(2)).sqrt();
110                let dx = (end[0] - start[0]) / total_length;
111                let dy = (end[1] - start[1]) / total_length;
112
113                let mut current_length = 0.0;
114                let mut drawing = true;
115
116                while current_length < total_length {
117                    let segment_length = if drawing { dash } else { gap };
118                    let next_length = (current_length + segment_length).min(total_length);
119
120                    if drawing {
121                        let x1 = start[0] + dx * current_length;
122                        let y1 = start[1] + dy * current_length;
123                        let x2 = start[0] + dx * next_length;
124                        let y2 = start[1] + dy * next_length;
125                        context.draw_line([x1, y1], [x2, y2], color, width);
126                    }
127
128                    current_length = next_length;
129                    drawing = !drawing;
130                }
131            }
132            LineStyle::Dotted => {
133                let dot_spacing = 5.0;
134                let total_length =
135                    ((end[0] - start[0]).powi(2) + (end[1] - start[1]).powi(2)).sqrt();
136                let num_dots = (total_length / dot_spacing) as i32;
137
138                for i in 0..=num_dots {
139                    let t = i as f32 / num_dots as f32;
140                    let x = start[0] + (end[0] - start[0]) * t;
141                    let y = start[1] + (end[1] - start[1]) * t;
142
143                    // Draw small dot
144                    context.draw_rect(
145                        crate::viewport::Rect::new(x - width / 2.0, y - width / 2.0, width, width),
146                        color,
147                    );
148                }
149            }
150        }
151    }
152}
153
154impl Default for CurrentPriceLayer {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160impl Layer for CurrentPriceLayer {
161    fn name(&self) -> &str {
162        "CurrentPrice"
163    }
164
165    fn stage(&self) -> LayerStage {
166        LayerStage::Hud // Render after price axis so label appears on top
167    }
168
169    fn clip_rect(&self, viewport: &Viewport) -> Rect {
170        // Use full screen rect so label renders over price axis without clipping
171        viewport.screen_rect
172    }
173
174    fn update(
175        &mut self,
176        data: &ChartData,
177        _viewport: &Viewport,
178        _theme: &ChartTheme,
179        style: &ChartStyle,
180    ) {
181        self.config.line_width = style.current_price.line_width;
182        self.config.label_padding = style.current_price.label_padding;
183        // Store name
184        self.symbol = data.symbol().to_string();
185
186        // Get the last value (e.g., close price)
187        if !data.main_series.is_empty() {
188            let last_idx = data.main_series.len() - 1;
189            let current_value = data.main_series.get_y(last_idx);
190            self.current_price = Some(current_value);
191
192            // Determine if positive or negative (for coloring)
193            // Compare current value with previous value
194            if last_idx > 0 {
195                let prev = data.main_series.get_y(last_idx - 1);
196                self.is_bullish = current_value >= prev;
197            } else {
198                self.is_bullish = true;
199            }
200        } else {
201            self.current_price = None;
202        }
203        self.needs_render = true;
204    }
205
206    fn render(
207        &self,
208        context: &mut RenderContext,
209        _render_pass: &mut wgpu::RenderPass,
210    ) -> Result<()> {
211        if !self.enabled || self.current_price.is_none() {
212            return Ok(());
213        }
214
215        let price = self.current_price.unwrap();
216        let viewport = context.viewport().clone();
217        let theme = context.theme().clone();
218        let content_rect = viewport.chart_content_rect();
219        let price_axis_rect = viewport.price_axis_rect();
220
221        // Convert price to screen Y coordinate
222        let screen_y = viewport.chart_to_screen_y(price as f32);
223
224        // Only render if price is visible
225        if screen_y < content_rect.y || screen_y > content_rect.y + content_rect.height {
226            return Ok(());
227        }
228
229        // TradingView style: use candle color for current price
230        let candle_color = if self.is_bullish {
231            theme.colors.candle_bullish
232        } else {
233            theme.colors.candle_bearish
234        };
235
236        let line_color = self
237            .config
238            .line_color
239            .map(|c| Color {
240                r: c[0],
241                g: c[1],
242                b: c[2],
243                a: c[3],
244            })
245            .unwrap_or(candle_color.with_alpha(0.6));
246
247        let label_bg_color = self
248            .config
249            .label_bg_color
250            .map(|c| Color {
251                r: c[0],
252                g: c[1],
253                b: c[2],
254                a: c[3],
255            })
256            .unwrap_or(candle_color);
257
258        let label_text_color = self
259            .config
260            .label_text_color
261            .map(|c| Color {
262                r: c[0],
263                g: c[1],
264                b: c[2],
265                a: c[3],
266            })
267            .unwrap_or(Color::hex(0xffffff)); // White text on colored background
268
269        // Draw horizontal line across content area
270        if self.config.show_line {
271            let line_end_x = content_rect.x + content_rect.width;
272            let cp_style = &context.style().current_price;
273            self.draw_styled_line(
274                context,
275                [content_rect.x, screen_y],
276                [line_end_x, screen_y],
277                line_color,
278                self.config.line_width,
279                cp_style.dash_length,
280                cp_style.dash_gap,
281            );
282        }
283
284        // Draw price label - TradingView style: rounded rect overlapping axis values
285        if self.config.show_label {
286            let price_text = format!("{:.2}", price);
287
288            // Calculate label size based on text
289            let font_size = theme.typography.secondary_font_size;
290            let char_width = font_size * 0.6;
291            let label_width =
292                price_text.len() as f32 * char_width + self.config.label_padding * 2.0;
293            let label_height = font_size + self.config.label_padding * 2.0;
294            let corner_radius = 3.0;
295
296            // Position: right-aligned within price axis with small margin
297            let margin = 4.0;
298            let label_x = price_axis_rect.x + price_axis_rect.width - label_width - margin;
299
300            // Center vertically on the price line, clamped to axis bounds
301            let mut label_y = screen_y - label_height / 2.0;
302            let axis_top = price_axis_rect.y + margin;
303            let axis_bottom = price_axis_rect.y + price_axis_rect.height - label_height - margin;
304            label_y = label_y.clamp(axis_top, axis_bottom);
305
306            // Draw rounded rectangle background
307            let label_rect = Rect::new(label_x, label_y, label_width, label_height);
308            context.draw_rounded_rect(label_rect, corner_radius, label_bg_color);
309
310            // Draw left-pointing triangle arrow
311            let arrow_width = 6.0;
312            let arrow_x = label_x;
313            let arrow_y = label_y + label_height / 2.0;
314
315            context.draw_triangle(
316                [arrow_x - arrow_width, arrow_y],
317                [arrow_x, arrow_y - label_height / 2.0],
318                [arrow_x, arrow_y + label_height / 2.0],
319                label_bg_color,
320            );
321
322            // Draw price text centered in label
323            #[cfg(feature = "text-rendering")]
324            {
325                use crate::text::{TextAnchor, TextBaseline};
326                context.draw_text_anchored(
327                    &price_text,
328                    label_x + label_width / 2.0,
329                    label_y + label_height / 2.0,
330                    label_text_color,
331                    Some(font_size),
332                    TextAnchor::Middle,
333                    TextBaseline::Middle,
334                );
335            }
336        }
337
338        Ok(())
339    }
340
341    fn needs_render(&self) -> bool {
342        self.needs_render
343    }
344
345    fn z_order(&self) -> i32 {
346        70 // Above axes
347    }
348
349    fn is_enabled(&self) -> bool {
350        self.enabled
351    }
352
353    fn set_enabled(&mut self, enabled: bool) {
354        self.enabled = enabled;
355        self.needs_render = true;
356    }
357
358    fn get_config(&self) -> Value {
359        serde_json::to_value(&self.config).unwrap_or(Value::Null)
360    }
361
362    fn set_config(&mut self, config: Value) -> Result<()> {
363        if let Ok(new_config) = serde_json::from_value::<CurrentPriceConfig>(config) {
364            self.config = new_config;
365            self.needs_render = true;
366        }
367        Ok(())
368    }
369}