1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CurrentPriceConfig {
16 pub show_line: bool,
18 pub show_label: bool,
20 pub line_style: LineStyle,
22 pub line_width: f32,
24 pub label_padding: f32,
26 pub line_color: Option<[f32; 4]>,
28 pub label_bg_color: Option<[f32; 4]>,
30 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, 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#[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 pub fn set_config(&mut self, config: CurrentPriceConfig) {
88 self.config = config;
89 self.needs_render = true;
90 }
91
92 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 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 }
168
169 fn clip_rect(&self, viewport: &Viewport) -> Rect {
170 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 self.symbol = data.symbol().to_string();
185
186 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 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 let screen_y = viewport.chart_to_screen_y(price as f32);
223
224 if screen_y < content_rect.y || screen_y > content_rect.y + content_rect.height {
226 return Ok(());
227 }
228
229 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)); 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 if self.config.show_label {
286 let price_text = format!("{:.2}", price);
287
288 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 let margin = 4.0;
298 let label_x = price_axis_rect.x + price_axis_rect.width - label_width - margin;
299
300 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 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 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 #[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 }
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}