use crate::chart::coords::ChartMapping;
use crate::chart::hit_test::{HIT_TOLERANCE, point_to_segment_distance};
use crate::studies::{Indicator, IndicatorValue};
use egui::{Pos2, Rect};
#[derive(Clone, Debug)]
pub struct IndicatorHitResult {
pub indicator_idx: usize,
pub bar_idx: usize,
}
pub fn hit_test_indicator<F>(
click_pos: Pos2,
indicator: &dyn Indicator,
indicator_idx: usize,
visible_range: std::ops::Range<usize>,
coords: &ChartMapping,
price_to_y: F,
) -> Option<IndicatorHitResult>
where
F: Fn(f64) -> f32,
{
if !indicator.is_overlay() || !indicator.is_visible() {
return None;
}
if !coords.rect.contains(click_pos) {
return None;
}
let values = indicator.values();
let line_cnt = indicator.line_cnt();
for line_idx in 0..line_cnt {
if let Some(hit) = hit_test_indicator_line(
click_pos,
values,
line_idx,
indicator_idx,
&visible_range,
coords,
&price_to_y,
) {
return Some(hit);
}
}
None
}
fn hit_test_indicator_line<F>(
click_pos: Pos2,
values: &[IndicatorValue],
line_idx: usize,
indicator_idx: usize,
visible_range: &std::ops::Range<usize>,
coords: &ChartMapping,
price_to_y: F,
) -> Option<IndicatorHitResult>
where
F: Fn(f64) -> f32,
{
let line_values: Vec<Option<f64>> = values
.iter()
.map(|v| match v {
IndicatorValue::Single(val) => Some(*val),
IndicatorValue::Multiple(vals) => vals.get(line_idx).copied(),
IndicatorValue::None => None,
})
.collect();
for i in visible_range.start..visible_range.end.saturating_sub(1) {
let v1 = line_values.get(i).and_then(|v| *v);
let v2 = line_values.get(i + 1).and_then(|v| *v);
let (Some(v1), Some(v2)) = (v1, v2) else {
continue;
};
let x1 = coords.idx_to_x(i);
let x2 = coords.idx_to_x(i + 1);
if x1 > coords.rect.max.x && x2 > coords.rect.max.x {
continue;
}
if x1 < coords.rect.min.x && x2 < coords.rect.min.x {
continue;
}
let min_x = x1.min(x2) - HIT_TOLERANCE;
let max_x = x1.max(x2) + HIT_TOLERANCE;
if click_pos.x < min_x || click_pos.x > max_x {
continue;
}
let y1 = price_to_y(v1);
let y2 = price_to_y(v2);
let dist = point_to_segment_distance(click_pos, Pos2::new(x1, y1), Pos2::new(x2, y2));
if dist <= HIT_TOLERANCE {
return Some(IndicatorHitResult {
indicator_idx,
bar_idx: i,
});
}
}
None
}
pub fn hit_test_pane_indicator(
click_pos: Pos2,
indicator: &dyn Indicator,
indicator_idx: usize,
visible_range: std::ops::Range<usize>,
chart_rect: Rect,
y_min: f64,
y_max: f64,
coords: &ChartMapping,
) -> Option<IndicatorHitResult> {
if indicator.is_overlay() || !indicator.is_visible() {
return None;
}
if !chart_rect.contains(click_pos) {
return None;
}
let values = indicator.values();
let line_cnt = indicator.line_cnt();
let value_to_y = |value: f64| -> f32 {
let y_range = y_max - y_min;
if y_range.abs() < f64::EPSILON {
return chart_rect.center().y;
}
let normalized = (value - y_min) / y_range;
chart_rect.max.y - (normalized as f32 * chart_rect.height())
};
for line_idx in 0..line_cnt {
if let Some(hit) = hit_test_pane_indicator_line(
click_pos,
values,
line_idx,
indicator_idx,
&visible_range,
chart_rect,
coords,
value_to_y,
) {
return Some(hit);
}
}
None
}
fn hit_test_pane_indicator_line<F>(
click_pos: Pos2,
values: &[IndicatorValue],
line_idx: usize,
indicator_idx: usize,
visible_range: &std::ops::Range<usize>,
chart_rect: Rect,
coords: &ChartMapping,
value_to_y: F,
) -> Option<IndicatorHitResult>
where
F: Fn(f64) -> f32,
{
let line_values: Vec<Option<f64>> = values
.iter()
.map(|v| match v {
IndicatorValue::Single(val) => Some(*val),
IndicatorValue::Multiple(vals) => vals.get(line_idx).copied(),
IndicatorValue::None => None,
})
.collect();
for i in visible_range.start..visible_range.end.saturating_sub(1) {
let v1 = line_values.get(i).and_then(|v| *v);
let v2 = line_values.get(i + 1).and_then(|v| *v);
let (Some(v1), Some(v2)) = (v1, v2) else {
continue;
};
let x1 = coords.idx_to_x(i);
let x2 = coords.idx_to_x(i + 1);
if x1 > chart_rect.max.x && x2 > chart_rect.max.x {
continue;
}
if x1 < chart_rect.min.x && x2 < chart_rect.min.x {
continue;
}
let min_x = x1.min(x2) - HIT_TOLERANCE;
let max_x = x1.max(x2) + HIT_TOLERANCE;
if click_pos.x < min_x || click_pos.x > max_x {
continue;
}
let y1 = value_to_y(v1);
let y2 = value_to_y(v2);
let dist = point_to_segment_distance(click_pos, Pos2::new(x1, y1), Pos2::new(x2, y2));
if dist <= HIT_TOLERANCE {
return Some(IndicatorHitResult {
indicator_idx,
bar_idx: i,
});
}
}
None
}