use egui::{Color32, Painter, Rect};
pub use crate::chart::coords::ChartMapping;
pub struct RenderContext<'a> {
pub painter: &'a Painter,
pub rect: Rect,
}
impl<'a> RenderContext<'a> {
pub fn new(painter: &'a Painter, rect: Rect) -> Self {
Self { painter, rect }
}
}
#[derive(Debug, Copy, Clone)]
pub struct LinearPriceMap {
pub min_price: f64,
pub max_price: f64,
}
impl LinearPriceMap {
pub fn new(min_price: f64, max_price: f64) -> Self {
Self {
min_price,
max_price,
}
}
#[inline]
pub fn price_range(&self) -> f64 {
self.max_price - self.min_price
}
pub fn price_to_y(&self, price: f64, rect: Rect) -> f32 {
let range = self.price_range();
if range.abs() < f64::EPSILON {
return rect.center().y;
}
let ratio = ((price - self.min_price) / range) as f32;
rect.max.y - ratio * rect.height()
}
}
#[derive(Debug, Copy, Clone)]
pub struct StyleColors {
pub bullish: Color32,
pub bearish: Color32,
pub text: Color32,
pub bullish_border: Option<Color32>,
pub bearish_border: Option<Color32>,
pub bullish_wick: Option<Color32>,
pub bearish_wick: Option<Color32>,
pub candle_border_width: f32,
}
impl StyleColors {
pub fn bar_color(&self, is_bullish: bool) -> Color32 {
if is_bullish {
self.bullish
} else {
self.bearish
}
}
pub fn wick_color(&self, is_bullish: bool) -> Color32 {
if is_bullish {
self.bullish_wick
.or(self.bullish_border)
.unwrap_or(self.bullish)
} else {
self.bearish_wick
.or(self.bearish_border)
.unwrap_or(self.bearish)
}
}
pub fn border_color(&self, is_bullish: bool) -> Color32 {
if is_bullish {
self.bullish_border.unwrap_or(self.bullish)
} else {
self.bearish_border.unwrap_or(self.bearish)
}
}
pub fn has_border(&self) -> bool {
self.candle_border_width > 0.0
}
}
#[derive(Debug, Copy, Clone)]
pub struct BarRenderParams {
pub x: f32,
pub width: f32,
pub wick_width: f32,
}
impl BarRenderParams {
pub fn new(x: f32, width: f32, wick_width: f32) -> Self {
Self {
x,
width,
wick_width,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use egui::{Pos2, Vec2};
fn rect() -> Rect {
Rect::from_min_size(Pos2::new(0.0, 0.0), Vec2::new(100.0, 200.0))
}
#[test]
fn price_range_is_derived_from_bounds() {
let map = LinearPriceMap::new(10.0, 30.0);
assert_eq!(map.price_range(), 20.0);
}
#[test]
fn price_to_y_is_finite_in_normal_window() {
let map = LinearPriceMap::new(100.0, 200.0);
let r = rect();
let y = map.price_to_y(150.0, r);
assert!(y.is_finite());
assert!((r.min.y..=r.max.y).contains(&y));
}
#[test]
fn price_to_y_on_flat_window_is_finite_and_in_rect() {
let map = LinearPriceMap::new(100.0, 100.0);
let r = rect();
let y = map.price_to_y(100.0, r);
assert!(y.is_finite(), "y must be finite on a flat window");
assert!(
(r.min.y..=r.max.y).contains(&y),
"y={y} must lie within the rect"
);
assert_eq!(y, r.center().y);
}
}