use super::context::{ChartMapping, LinearPriceMap, RenderContext};
use crate::config::{CrosshairLineStyle, CrosshairMode, CrosshairStyle};
use crate::model::Bar;
use crate::styles::typography;
use crate::tokens::DESIGN_TOKENS;
use egui::{Color32, FontId, Pos2, Rect, Stroke};
pub fn render_crosshair_full(
context: &RenderContext,
hover_pos: Pos2,
visible_data: &[Bar],
price_scale: &LinearPriceMap,
coords: &ChartMapping,
mode: CrosshairMode,
style: CrosshairStyle,
crosshair_color: Color32,
line_width: f32,
line_style: CrosshairLineStyle,
) {
if style == CrosshairStyle::Arrow {
return;
}
let label_bg = DESIGN_TOKENS.semantic.extended.chart_crosshair_label_bg;
let bars_from_right = (context.rect.max.x - hover_pos.x) / coords.bar_spacing - 0.5;
let t_float = coords.base_idx as f32 - (bars_from_right - coords.right_offset);
let t_idx = t_float.floor() as isize;
let first = coords.start_idx as isize;
let last = (coords.start_idx + visible_data.len().saturating_sub(1)) as isize;
let (actual_hover_x, actual_hover_y) = if t_idx >= first && t_idx <= last {
let candle_idx = (t_idx as usize).saturating_sub(coords.start_idx);
let candle = &visible_data[candle_idx];
match mode {
CrosshairMode::Normal => (hover_pos.x, hover_pos.y),
CrosshairMode::Magnet => {
let candle_x = coords.idx_to_x(t_idx as usize);
let snap_price =
snap_to_nearest_ohlc(hover_pos.y, context.rect, price_scale, candle);
let snap_y = price_scale.price_to_y(snap_price, context.rect);
(candle_x, snap_y)
}
}
} else {
(hover_pos.x, hover_pos.y)
};
let actual_pos = Pos2::new(actual_hover_x, actual_hover_y);
match style {
CrosshairStyle::Full => {
match line_style {
CrosshairLineStyle::Solid => {
context.painter.line_segment(
[
Pos2::new(actual_pos.x, context.rect.min.y),
Pos2::new(actual_pos.x, context.rect.max.y),
],
Stroke::new(line_width, crosshair_color),
);
context.painter.line_segment(
[
Pos2::new(context.rect.min.x, actual_pos.y),
Pos2::new(context.rect.max.x, actual_pos.y),
],
Stroke::new(line_width, crosshair_color),
);
}
CrosshairLineStyle::Dashed => {
let dash_pattern = [
DESIGN_TOKENS.spacing.sm + 1.0,
DESIGN_TOKENS.spacing.xs + 1.0,
];
draw_dashed_vline(
context.painter,
actual_pos.x,
context.rect.y_range(),
crosshair_color,
line_width,
&dash_pattern,
);
draw_dashed_hline(
context.painter,
context.rect.x_range(),
actual_pos.y,
crosshair_color,
line_width,
&dash_pattern,
);
}
CrosshairLineStyle::Dotted => {
let dotted_pattern = [DESIGN_TOKENS.spacing.xs, DESIGN_TOKENS.spacing.xs];
draw_dashed_vline(
context.painter,
actual_pos.x,
context.rect.y_range(),
crosshair_color,
line_width,
&dotted_pattern,
);
draw_dashed_hline(
context.painter,
context.rect.x_range(),
actual_pos.y,
crosshair_color,
line_width,
&dotted_pattern,
);
}
}
}
CrosshairStyle::Dot => {
context.painter.circle_filled(
actual_pos,
DESIGN_TOKENS.sizing.crosshair.dot_radius,
crosshair_color,
);
}
CrosshairStyle::Arrow => {
unreachable!()
}
}
let price_ratio = (context.rect.max.y - actual_pos.y) / context.rect.height();
let price = price_scale.min_price + price_ratio as f64 * price_scale.price_range();
let price_label = format!("{price:.8}");
let price_pos = Pos2::new(
context.rect.max.x + DESIGN_TOKENS.sizing.crosshair.label_offset_x,
actual_pos.y,
);
let text_size = context.painter.text(
price_pos,
egui::Align2::LEFT_CENTER,
&price_label,
FontId::proportional(typography::XS),
Color32::TRANSPARENT,
);
let extended_bg = text_size.expand2(egui::Vec2::new(
DESIGN_TOKENS.sizing.crosshair.plus_symbol_spacing,
0.0,
)); context.painter.rect_filled(
extended_bg,
DESIGN_TOKENS.sizing.crosshair.label_rounding,
label_bg,
);
context.painter.text(
price_pos,
egui::Align2::LEFT_CENTER,
price_label,
FontId::proportional(typography::XS),
Color32::WHITE,
);
let plus_pos = Pos2::new(text_size.max.x + 4.0, actual_pos.y);
context.painter.text(
plus_pos,
egui::Align2::LEFT_CENTER,
"+",
FontId::proportional(typography::SM),
DESIGN_TOKENS.semantic.extended.chart_text_muted,
);
let bars_from_right = (context.rect.max.x - hover_pos.x) / coords.bar_spacing - 0.5;
let t_float = coords.base_idx as f32 - (bars_from_right - coords.right_offset);
let t_idx = t_float.floor() as isize;
let first = coords.start_idx as isize;
let last = (coords.start_idx + visible_data.len().saturating_sub(1)) as isize;
if t_idx >= first && t_idx <= last {
let candle_idx = (t_idx as usize).saturating_sub(coords.start_idx);
let candle = &visible_data[candle_idx];
let time_label = if visible_data.len() >= 2 {
let time_diff = visible_data[1]
.time
.signed_duration_since(visible_data[0].time);
let seconds = time_diff.num_seconds().abs();
if seconds < 1 {
candle.time.format("%H:%M:%S%.3f").to_string()
} else if seconds < 60 {
candle.time.format("%H:%M:%S").to_string()
} else if seconds < 3600 {
candle.time.format("%H:%M").to_string()
} else if seconds < 86400 {
candle.time.format("%b %d %H:%M").to_string()
} else {
candle.time.format("%b %d, %Y").to_string()
}
} else {
candle.time.format("%H:%M:%S").to_string()
};
let time_pos = Pos2::new(actual_pos.x, context.rect.max.y + 5.0);
let text_size = context.painter.text(
time_pos,
egui::Align2::CENTER_TOP,
&time_label,
FontId::proportional(typography::XS),
Color32::TRANSPARENT,
);
context.painter.rect_filled(text_size, 2.0, label_bg);
context.painter.text(
time_pos,
egui::Align2::CENTER_TOP,
time_label,
FontId::proportional(typography::XS),
Color32::WHITE,
);
}
}
fn snap_to_nearest_ohlc(
hover_y: f32,
price_rect: Rect,
price_scale: &LinearPriceMap,
candle: &Bar,
) -> f64 {
let open_y = price_scale.price_to_y(candle.open, price_rect);
let high_y = price_scale.price_to_y(candle.high, price_rect);
let low_y = price_scale.price_to_y(candle.low, price_rect);
let close_y = price_scale.price_to_y(candle.close, price_rect);
let candidates = [
(open_y, candle.open),
(high_y, candle.high),
(low_y, candle.low),
(close_y, candle.close),
];
let mut nearest_price = candle.close;
let mut min_distance = f32::MAX;
for (y, price) in candidates {
let distance = (hover_y - y).abs();
if distance < min_distance {
min_distance = distance;
nearest_price = price;
}
}
nearest_price
}
fn draw_dashed_vline(
painter: &egui::Painter,
x: f32,
y_range: egui::Rangef,
color: Color32,
width: f32,
pattern: &[f32], ) {
let mut y = y_range.min;
let y_end = y_range.max;
let mut pattern_idx = 0;
let mut is_dash = true;
while y < y_end {
let segment_len = pattern[pattern_idx % pattern.len()];
let next_y = (y + segment_len).min(y_end);
if is_dash {
painter.line_segment(
[Pos2::new(x, y), Pos2::new(x, next_y)],
Stroke::new(width, color),
);
}
y = next_y;
pattern_idx += 1;
is_dash = !is_dash;
}
}
fn draw_dashed_hline(
painter: &egui::Painter,
x_range: egui::Rangef,
y: f32,
color: Color32,
width: f32,
pattern: &[f32],
) {
let mut x = x_range.min;
let x_end = x_range.max;
let mut pattern_idx = 0;
let mut is_dash = true;
while x < x_end {
let segment_len = pattern[pattern_idx % pattern.len()];
let next_x = (x + segment_len).min(x_end);
if is_dash {
painter.line_segment(
[Pos2::new(x, y), Pos2::new(next_x, y)],
Stroke::new(width, color),
);
}
x = next_x;
pattern_idx += 1;
is_dash = !is_dash;
}
}