1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//! Crosshair rendering layer
use crate::data::ChartData;
use crate::error::Result;
use crate::layers::{Layer, LayerStage};
use crate::renderer::RenderContext;
use crate::style::ChartStyle;
use crate::theme::ChartTheme;
use crate::viewport::{Rect, Viewport};
/// Layer for rendering crosshair
#[derive(Debug)]
pub struct CrosshairLayer {
/// Whether the layer is enabled
enabled: bool,
/// Whether the layer needs to be re-rendered
needs_render: bool,
/// Current cursor position in SCREEN coordinates (pixels).
/// None means there is no active cursor inside the chart area and
/// nothing should be drawn.
position: Option<glam::Vec2>,
}
impl CrosshairLayer {
pub fn new() -> Self {
Self {
enabled: true,
needs_render: true,
position: None,
}
}
/// Update the cursor position in **screen space**. Call this from the
/// event system whenever the mouse moves.
pub fn set_position(&mut self, x: f32, y: f32) {
let new_pos = glam::Vec2::new(x, y);
// Only mark for re-render if the position actually changed.
if self.position != Some(new_pos) {
self.position = Some(new_pos);
self.needs_render = true;
}
}
/// Clear the current position – used when the cursor leaves the chart
/// area so the crosshair disappears.
pub fn clear_position(&mut self) {
if self.position.is_some() {
self.position = None;
self.needs_render = true;
}
}
}
impl Default for CrosshairLayer {
fn default() -> Self {
Self::new()
}
}
impl Layer for CrosshairLayer {
fn name(&self) -> &str {
"Crosshair"
}
fn stage(&self) -> LayerStage {
LayerStage::Hud
}
fn clip_rect(&self, viewport: &Viewport) -> Rect {
viewport.chart_content_rect()
}
fn update(
&mut self,
_data: &ChartData,
_viewport: &Viewport,
_theme: &ChartTheme,
_style: &ChartStyle,
) {
// Update does nothing for now. Rendering happens based on the last
// recorded cursor position. We only re-render if `needs_render` was
// flagged by `set_position` / `clear_position`.
}
fn render(
&self,
context: &mut RenderContext,
_render_pass: &mut wgpu::RenderPass,
) -> Result<()> {
// Early-out if there is no cursor inside the chart or the layer is disabled
let Some(pos) = self.position else {
return Ok(());
};
// Retrieve viewport & theme
let viewport = context.viewport();
let theme = context.theme();
// Work inside the chart content rectangle only
let content_rect = viewport.chart_content_rect();
// Bail out if the cursor is outside the content area
if pos.x < content_rect.x
|| pos.x > content_rect.x + content_rect.width
|| pos.y < content_rect.y
|| pos.y > content_rect.y + content_rect.height
{
return Ok(());
}
let color = theme.colors.crosshair;
let thickness = theme.spacing.crosshair_width;
// Dash parameters chosen to visually resemble TradingView style
let dash = 4.0;
let gap = 4.0;
// Horizontal line
context.draw_dashed_line(
[content_rect.x, pos.y],
[content_rect.x + content_rect.width, pos.y],
color,
thickness,
dash,
gap,
);
// Vertical line
context.draw_dashed_line(
[pos.x, content_rect.y],
[pos.x, content_rect.y + content_rect.height],
color,
thickness,
dash,
gap,
);
Ok(())
}
fn needs_render(&self) -> bool {
self.needs_render
}
fn z_order(&self) -> i32 {
100 // Top layer
}
fn is_enabled(&self) -> bool {
self.enabled
}
fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
self.needs_render = true;
}
}