use crate::chart::coords::ChartMapping;
use crate::tokens::DESIGN_TOKENS;
use egui::{Color32, Painter, Pos2};
pub const SELECTION_DOT_OUTER_RADIUS: f32 = 4.5;
pub const SELECTION_DOT_INNER_RADIUS: f32 = 2.5;
pub const SELECTION_DOT_TARGET_GAP: f32 = 70.0;
pub const SELECTION_DOT_MIN_INTERVAL: usize = 3;
pub const SELECTION_DOT_MAX_INTERVAL: usize = 50;
pub const SELECTION_DOT_DEFAULT_INTERVAL: usize = 5;
#[derive(Clone, Debug)]
pub struct SelectionHandleConfig {
pub outer_radius: f32,
pub inner_radius: f32,
pub ring_color: Color32,
pub inner_color: Color32,
pub dot_interval: usize,
}
impl Default for SelectionHandleConfig {
fn default() -> Self {
Self {
outer_radius: SELECTION_DOT_OUTER_RADIUS,
inner_radius: SELECTION_DOT_INNER_RADIUS,
ring_color: DESIGN_TOKENS.semantic.extended.accent,
inner_color: DESIGN_TOKENS.semantic.chart.bg,
dot_interval: SELECTION_DOT_DEFAULT_INTERVAL,
}
}
}
pub fn calculate_dot_interval(bar_spacing: f32) -> usize {
let interval = (SELECTION_DOT_TARGET_GAP / bar_spacing).round() as usize;
interval.clamp(SELECTION_DOT_MIN_INTERVAL, SELECTION_DOT_MAX_INTERVAL)
}
fn draw_selection_dot(painter: &Painter, pos: Pos2, config: &SelectionHandleConfig) {
painter.circle_filled(pos, config.outer_radius, config.ring_color);
painter.circle_filled(pos, config.inner_radius, config.inner_color);
}
pub fn render_series_selection_on_points(
painter: &Painter,
points: &[Pos2],
config: &SelectionHandleConfig,
) {
if points.is_empty() {
return;
}
for (i, &point) in points.iter().enumerate() {
if i % config.dot_interval == 0 {
draw_selection_dot(painter, point, config);
}
}
}
#[inline]
fn close_for_abs_idx(closes: &[f64], start_idx: usize, abs_idx: usize) -> Option<f64> {
let local = abs_idx.checked_sub(start_idx)?;
closes.get(local).copied()
}
pub fn render_candle_selection_dots<F>(
painter: &Painter,
visible_range: std::ops::Range<usize>,
coords: &ChartMapping,
closes: &[f64],
price_to_y: F,
config: &SelectionHandleConfig,
) where
F: Fn(f64) -> f32,
{
let start_idx = visible_range.start;
for i in visible_range {
if i % config.dot_interval != 0 {
continue;
}
let Some(close) = close_for_abs_idx(closes, start_idx, i) else {
continue;
};
let x = coords.idx_to_x(i);
let y = price_to_y(close);
if coords.rect.contains(Pos2::new(x, y)) {
draw_selection_dot(painter, Pos2::new(x, y), config);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn close_lookup_resolves_for_nonzero_start_index() {
let start_idx = 400;
let closes: Vec<f64> = (0..100).map(|local| 10.0 + local as f64).collect();
let visible_range = start_idx..start_idx + closes.len();
for abs_idx in visible_range.clone() {
let resolved = close_for_abs_idx(&closes, start_idx, abs_idx)
.expect("close must resolve for an in-window absolute index");
assert_eq!(resolved, closes[abs_idx - start_idx]);
}
assert_eq!(close_for_abs_idx(&closes, start_idx, start_idx), Some(10.0));
assert_eq!(
close_for_abs_idx(&closes, start_idx, visible_range.end - 1),
Some(10.0 + 99.0)
);
}
#[test]
fn close_lookup_rejects_out_of_window_indices() {
let start_idx = 400;
let closes: Vec<f64> = vec![1.0, 2.0, 3.0];
assert_eq!(close_for_abs_idx(&closes, start_idx, start_idx - 1), None);
assert_eq!(close_for_abs_idx(&closes, start_idx, start_idx + 3), None);
}
}