#[derive(Debug, Clone, Copy)]
pub struct LogicalRange {
pub left: f32,
pub right: f32,
}
impl LogicalRange {
pub fn new(left: f32, right: f32) -> Self {
Self { left, right }
}
pub fn to_strict_range(&self) -> (usize, usize) {
let start = self.left.floor().max(0.0) as usize;
let end = self.right.ceil().max(0.0) as usize;
(start, end)
}
pub fn length(&self) -> f32 {
self.right - self.left
}
}
#[derive(Debug, Clone)]
pub struct TimeScale {
bar_spacing: f32,
right_offset: f32,
width: f32,
bar_cnt: usize,
min_bar_spacing: f32,
max_bar_spacing: f32,
fix_left_edge: bool,
fix_right_edge: bool,
}
impl TimeScale {
pub fn new() -> Self {
Self {
bar_spacing: 8.0,
right_offset: 5.0, width: 800.0,
bar_cnt: 0,
min_bar_spacing: 0.5,
max_bar_spacing: 0.0, fix_left_edge: true,
fix_right_edge: false, }
}
pub fn apply_options(
&mut self,
bar_spacing: f32,
min_bar_spacing: f32,
max_bar_spacing: f32,
fix_left_edge: bool,
fix_right_edge: bool,
right_offset_bars: f32,
right_offset_pixels: Option<f32>,
) {
self.min_bar_spacing = min_bar_spacing;
self.max_bar_spacing = max_bar_spacing;
self.fix_left_edge = fix_left_edge;
self.fix_right_edge = fix_right_edge;
self.set_bar_spacing(bar_spacing);
let right_offset = if let Some(px) = right_offset_pixels {
px / self.bar_spacing
} else {
right_offset_bars
};
self.set_right_offset(right_offset);
}
pub fn set_width(&mut self, width: f32) {
self.width = width;
self.apply_constraints();
}
pub fn set_bar_cnt(&mut self, count: usize) {
self.bar_cnt = count;
self.apply_constraints();
}
pub fn set_bar_spacing(&mut self, spacing: f32) {
let clamped = if self.max_bar_spacing > 0.0 {
spacing.clamp(self.min_bar_spacing, self.max_bar_spacing)
} else {
spacing.max(self.min_bar_spacing)
};
self.bar_spacing = clamped;
self.apply_constraints();
}
pub fn set_min_bar_spacing(&mut self, min: f32) {
self.min_bar_spacing = min.max(0.0);
self.set_bar_spacing(self.bar_spacing);
}
pub fn set_max_bar_spacing(&mut self, max: f32) {
self.max_bar_spacing = max.max(0.0);
self.set_bar_spacing(self.bar_spacing);
}
pub fn set_fix_left_edge(&mut self, fix: bool) {
self.fix_left_edge = fix;
self.apply_constraints();
}
pub fn set_fix_right_edge(&mut self, fix: bool) {
self.fix_right_edge = fix;
self.apply_constraints();
}
pub fn set_right_offset(&mut self, offset: f32) {
self.right_offset = offset;
self.apply_constraints();
}
pub fn jump_to_latest(&mut self) {
const DEFAULT_RIGHT_OFFSET: f32 = 2.5;
self.right_offset = DEFAULT_RIGHT_OFFSET;
}
pub fn bar_spacing(&self) -> f32 {
self.bar_spacing
}
pub fn right_offset(&self) -> f32 {
self.right_offset
}
pub fn width(&self) -> f32 {
self.width
}
pub fn base_idx(&self) -> usize {
self.bar_cnt.saturating_sub(1)
}
pub fn visible_logical_range(&self) -> LogicalRange {
let base_idx = self.base_idx() as f32;
let bars_len = self.width / self.bar_spacing;
let right_border = base_idx + self.right_offset;
let left_border = right_border - bars_len + 1.0;
LogicalRange::new(left_border, right_border)
}
pub fn idx_to_coord(&self, index: usize, rect_min_x: f32, rect_width: f32) -> f32 {
let base_idx = self.base_idx();
let delta_from_right = base_idx as f32 + self.right_offset - index as f32;
let relative_x = rect_width - (delta_from_right + 0.5) * self.bar_spacing - 1.0;
rect_min_x + relative_x
}
pub fn idx_to_coord_precise(&self, index: f32, rect_min_x: f32, rect_width: f32) -> f32 {
let base_idx = self.base_idx() as f32;
let delta_from_right = base_idx + self.right_offset - index;
let relative_x = rect_width - (delta_from_right + 0.5) * self.bar_spacing - 1.0;
rect_min_x + relative_x
}
pub fn coord_to_idx(&self, x: f32, rect_min_x: f32, rect_width: f32) -> f32 {
let base_idx = self.base_idx() as f32;
let relative_x = x - rect_min_x;
let delta_from_right = (rect_width - relative_x - 1.0) / self.bar_spacing - 0.5;
base_idx + self.right_offset - delta_from_right
}
pub fn scroll_bars(&mut self, bars: f32) {
self.right_offset -= bars;
self.apply_constraints();
}
pub fn scroll_pixels(&mut self, pixels: f32) {
let bars = pixels / self.bar_spacing;
self.scroll_bars(bars);
}
pub fn zoom(&mut self, delta: f32, anchor_x: f32, rect_min_x: f32, rect_width: f32) {
let old_spacing = self.bar_spacing;
let _old_right_offset = self.right_offset;
let anchor_bar_idx = self.coord_to_idx(rect_min_x + anchor_x, rect_min_x, rect_width);
let zoom_scale = delta;
let new_spacing = old_spacing + zoom_scale * (old_spacing / 10.0);
let clamped = if self.max_bar_spacing > 0.0 {
new_spacing.clamp(self.min_bar_spacing, self.max_bar_spacing)
} else {
new_spacing.max(self.min_bar_spacing)
};
self.bar_spacing = clamped;
let base_idx = self.base_idx() as f32;
let relative_anchor = anchor_x; let delta_from_right = (rect_width - relative_anchor - 1.0) / self.bar_spacing - 0.5;
let calculated_offset = anchor_bar_idx - base_idx + delta_from_right;
self.right_offset = calculated_offset;
if (rect_width - self.width).abs() > 0.1 {
log::warn!(
"[ZOOM WIDTH MISMATCH] rect_width={:.1}, self.width={:.1}",
rect_width,
self.width
);
}
let before_constraint = self.right_offset;
self.apply_constraints();
if (self.right_offset - before_constraint).abs() > 0.01 {
log::debug!(
"[ZOOM CONSTRAINT] right_offset changed: {:.3} -> {:.3} (delta={:.3})",
before_constraint,
self.right_offset,
self.right_offset - before_constraint
);
}
}
pub fn fit_content(&mut self) {
if self.bar_cnt > 0 {
let spacing = self.width / self.bar_cnt as f32;
self.set_bar_spacing(spacing);
self.right_offset = 0.0;
}
}
pub fn scroll_to_realtime(&mut self) {
const REALTIME_OFFSET: f32 = 2.5;
self.right_offset = REALTIME_OFFSET;
}
fn apply_constraints(&mut self) {
if self.width <= 0.0 || self.bar_spacing <= 0.0 || self.bar_cnt == 0 {
return;
}
let min_right = self.calculate_min_right_offset();
let max_right = self.calculate_max_right_offset();
let (min_right_offset, max_right_offset) = if let Some(min_val) = min_right {
if min_val <= max_right {
(min_val, max_right)
} else {
(max_right, min_val)
}
} else {
(f32::NEG_INFINITY, max_right)
};
self.right_offset = self.right_offset.clamp(min_right_offset, max_right_offset);
if self.fix_right_edge && self.right_offset.abs() < 1e-6 {
self.right_offset = 0.0;
}
}
fn calculate_min_right_offset(&self) -> Option<f32> {
if self.bar_cnt == 0 || self.bar_spacing <= 0.0 || self.width <= 0.0 {
return None;
}
Some(-(self.bar_cnt as f32 + 100.0))
}
fn calculate_max_right_offset(&self) -> f32 {
if self.bar_cnt == 0 {
return 0.0;
}
if self.fix_right_edge {
0.0
} else {
self.width / self.min_bar_spacing
}
}
pub fn visible_candles(&self) -> usize {
(self.width / self.bar_spacing).floor() as usize
}
}
impl Default for TimeScale {
fn default() -> Self {
Self::new()
}
}