1use egui::{Pos2, Rect, Response, Ui};
18use web_time::Instant;
19
20use super::helpers::{apply_price_zoom, y_to_price};
21use super::state::BoxZoomMode;
22use crate::widget::Chart;
23
24impl Chart {
25 pub fn handle_mouse_wheel(
30 &mut self,
31 ui: &Ui,
32 response: &Response,
33 chart_width: f32,
34 chart_rect_min_x: f32,
35 price_axis_rect: Rect,
36 ) -> Option<f32> {
37 let mut pending_price_zoom = None;
38
39 if !response.hovered()
40 || (!self.chart_options.handle_scale.mouse_wheel
41 && !self.chart_options.handle_scroll.mouse_wheel)
42 {
43 return pending_price_zoom;
44 }
45
46 let scroll_input = ui.input(|i| {
47 let mut dx = if i.smooth_scroll_delta.x.abs() > 0.0 {
48 i.smooth_scroll_delta.x
49 } else {
50 i.raw_scroll_delta.x
51 };
52 let mut dy = if i.smooth_scroll_delta.y.abs() > 0.0 {
53 i.smooth_scroll_delta.y
54 } else {
55 i.raw_scroll_delta.y
56 };
57
58 if i.modifiers.shift && dx.abs() < 0.1 && dy.abs() > 0.1 {
60 dx = dy;
61 dy = 0.0;
62 }
63 (
64 dy,
65 dx,
66 i.pointer.hover_pos(),
67 i.modifiers.command || i.modifiers.ctrl,
68 )
69 });
70 let (delta_y, delta_x, hover_pos, _is_modifier_held) = scroll_input;
71
72 let mut did_zoom = false;
73 if delta_y.abs() > 0.1 {
74 let shift_held = ui.input(|i| i.modifiers.shift);
75
76 if shift_held && self.chart_options.handle_scroll.mouse_wheel {
77 let pan_amount = -delta_y * 2.0;
78 self.state.time_scale_mut().scroll_pixels(pan_amount);
79 } else if self.chart_options.handle_scale.mouse_wheel
80 && let Some(hp) = hover_pos.or_else(|| response.hover_pos())
81 {
82 if price_axis_rect.contains(hp) {
83 pending_price_zoom = Some(delta_y);
84 } else {
85 let zoom_scale = (-delta_y / 100.0).clamp(-0.5, 0.5);
86 let zoom_point_x = hp.x - chart_rect_min_x; log::debug!(
91 "[ZOOM INPUT] mouse_x={:.1}, chart_min_x={:.1}, anchor_x={:.1}, chart_width={:.1}",
92 hp.x,
93 chart_rect_min_x,
94 zoom_point_x,
95 chart_width
96 );
97
98 self.state.time_scale_mut().zoom(
99 zoom_scale,
100 zoom_point_x,
101 chart_rect_min_x,
102 chart_width,
103 );
104 did_zoom = true;
105
106 if self.scroll_start_offset.is_some() {
109 self.scroll_start_offset = Some(self.state.time_scale().right_offset());
110 self.scroll_start_pos = response.interact_pointer_pos();
111 }
112 }
113 }
114 }
115
116 if !did_zoom
117 && delta_y.abs() < 0.1
118 && delta_x.abs() > 0.1
119 && self.chart_options.handle_scroll.mouse_wheel
120 {
121 self.state.time_scale_mut().scroll_pixels(delta_x);
122 }
123
124 pending_price_zoom
125 }
126
127 pub fn handle_pinch_zoom(
132 &mut self,
133 ui: &Ui,
134 response: &Response,
135 chart_width: f32,
136 chart_rect_min_x: f32,
137 ) {
138 if !response.hovered() || !self.chart_options.handle_scale.pinch {
139 return;
140 }
141
142 let zoom_info = ui.input(|i| {
144 i.multi_touch()
145 .map(|mt| (mt.zoom_delta, mt.translation_delta, i.pointer.hover_pos()))
146 });
147
148 if let Some((zoom_delta, translation_delta, hover_pos)) = zoom_info {
149 if (zoom_delta - 1.0).abs() > 0.001 {
151 let zoom_scale = (zoom_delta - 1.0) * 2.0;
155
156 let zoom_point_x = hover_pos
158 .map(|p| p.x - chart_rect_min_x)
159 .unwrap_or(chart_width / 2.0);
160
161 log::debug!(
162 "[PINCH ZOOM] zoom_delta={zoom_delta:.4}, zoom_scale={zoom_scale:.4}, anchor_x={zoom_point_x:.1}"
163 );
164
165 self.state.time_scale_mut().zoom(
166 zoom_scale,
167 zoom_point_x,
168 chart_rect_min_x,
169 chart_width,
170 );
171
172 if self.scroll_start_offset.is_some() {
174 self.scroll_start_offset = Some(self.state.time_scale().right_offset());
175 self.scroll_start_pos = response.interact_pointer_pos();
176 }
177 }
178
179 if translation_delta.x.abs() > 0.5 {
181 self.state
182 .time_scale_mut()
183 .scroll_pixels(translation_delta.x);
184 }
185 }
186 }
187
188 pub fn handle_drag_pan(
195 &mut self,
196 ui: &Ui,
197 response: &Response,
198 price_axis_rect: Rect,
199 time_axis_rect: Rect,
200 chart_rect_min_x: f32,
201 has_active_drawing_tool: bool,
202 ) {
203 let box_zoom_active = self.box_zoom.active;
205
206 if !response.dragged()
207 || !self.chart_options.handle_scroll.pressed_mouse_move
208 || has_active_drawing_tool
209 || box_zoom_active
210 {
211 if !has_active_drawing_tool
213 && self.chart_options.kinetic_scroll.enabled
214 && self.scroll_start_pos.is_some()
215 {
216 let is_trackpad_gesture =
217 ui.input(|i| i.multi_touch().is_some() || i.any_touches());
218
219 if is_trackpad_gesture {
220 let move_distance = self
221 .kinetic_scroll
222 .last_pos
223 .and_then(|last| {
224 self.scroll_start_pos.map(|start| (last.x - start.x).abs())
225 })
226 .unwrap_or(0.0);
227
228 let min_v_px_s = self.chart_options.kinetic_scroll.min_scroll_speed * 60.0;
229 let max_v_px_s = self.chart_options.kinetic_scroll.max_scroll_speed * 60.0;
230
231 if move_distance >= self.chart_options.kinetic_scroll.scroll_min_move
232 && self.kinetic_scroll.velocity.abs() >= min_v_px_s
233 {
234 self.kinetic_scroll.is_active = true;
235 self.kinetic_scroll.velocity =
236 self.kinetic_scroll.velocity.clamp(-max_v_px_s, max_v_px_s);
237 self.kinetic_scroll.anim_last_time = Some(Instant::now());
238 }
239 }
240 }
241
242 self.scroll_start_pos = None;
243 self.scroll_start_offset = None;
244 self.kinetic_scroll.last_pos = None;
245 self.kinetic_scroll.last_time = None;
246 return;
247 }
248
249 self.kinetic_scroll.is_active = false;
251
252 if self.scroll_start_pos.is_none() {
253 self.scroll_start_pos = response.interact_pointer_pos();
254 self.scroll_start_offset = Some(self.state.time_scale().right_offset());
255 }
256
257 if let (Some(start_pos), Some(start_offset), Some(curr_pos)) = (
258 self.scroll_start_pos,
259 self.scroll_start_offset,
260 response.interact_pointer_pos(),
261 ) {
262 if price_axis_rect.contains(start_pos)
263 && self
264 .chart_options
265 .handle_scale
266 .axis_pressed_mouse_move
267 .price
268 {
269 self.price_scale_drag_start = Some(start_pos);
270 } else if time_axis_rect.contains(start_pos)
271 && self.chart_options.handle_scale.axis_pressed_mouse_move.time
272 {
273 let dx = curr_pos.x - start_pos.x;
274 if dx.abs() > 0.1 {
275 let zoom_point_x = curr_pos.x - chart_rect_min_x;
276 let zoom_scale = (dx / 100.0).signum() * (dx / 100.0).abs().min(1.0);
277 let chart_width = time_axis_rect.width();
278 self.state.time_scale_mut().zoom(
279 zoom_scale,
280 zoom_point_x,
281 chart_rect_min_x,
282 chart_width,
283 );
284 self.scroll_start_pos = Some(curr_pos);
285 }
286 } else {
287 let drag_delta_x = curr_pos.x - start_pos.x;
288 let bar_spacing = self.state.time_scale().bar_spacing();
289 let drag_in_bars = drag_delta_x / bar_spacing;
290 let new_offset = start_offset - drag_in_bars;
291
292 self.state.time_scale_mut().set_right_offset(new_offset);
293 }
294
295 if self.chart_options.kinetic_scroll.enabled {
297 let now = Instant::now();
298 if let (Some(last_pos), Some(last_time)) =
299 (self.kinetic_scroll.last_pos, self.kinetic_scroll.last_time)
300 {
301 let dt = now.duration_since(last_time).as_secs_f32();
302 if dt > 0.0 {
303 let dx = curr_pos.x - last_pos.x;
304 self.kinetic_scroll.velocity = dx / dt;
305 }
306 }
307 self.kinetic_scroll.last_pos = Some(curr_pos);
308 self.kinetic_scroll.last_time = Some(now);
309 }
310 }
311 }
312
313 pub fn apply_kinetic_scroll(&mut self, ui: &Ui) {
319 if !self.chart_options.kinetic_scroll.enabled || !self.kinetic_scroll.is_active {
320 return;
321 }
322
323 let now = Instant::now();
324 let dt = if let Some(t0) = self.kinetic_scroll.anim_last_time {
325 now.duration_since(t0).as_secs_f32().max(0.0)
326 } else {
327 self.kinetic_scroll.anim_last_time = Some(now);
328 0.0
329 };
330 self.kinetic_scroll.anim_last_time = Some(now);
331
332 if dt > 0.0 {
333 let velocity_in_bars_per_s =
334 self.kinetic_scroll.velocity / self.state.time_scale().bar_spacing();
335 let delta_offset = velocity_in_bars_per_s * dt;
336 let curr_offset = self.state.time_scale().right_offset();
337 let new_offset = curr_offset - delta_offset;
338
339 self.state.time_scale_mut().set_right_offset(new_offset);
340
341 let frames = (dt * 60.0).max(0.0);
342 let damping = self.chart_options.kinetic_scroll.dumping_coeff.powf(frames);
343 self.kinetic_scroll.velocity *= damping;
344
345 let min_v_px_s = self.chart_options.kinetic_scroll.min_scroll_speed * 60.0;
346 if self.kinetic_scroll.velocity.abs() < min_v_px_s {
347 self.kinetic_scroll.is_active = false;
348 self.kinetic_scroll.velocity = 0.0;
349 self.kinetic_scroll.anim_last_time = None;
350 }
351 }
352
353 ui.ctx().request_repaint();
354 }
355
356 pub fn handle_double_click(
358 &mut self,
359 response: &Response,
360 price_axis_rect: Rect,
361 time_axis_rect: Rect,
362 ) {
363 if !response.double_clicked() {
364 return;
365 }
366
367 if let Some(pos) = response.interact_pointer_pos() {
368 if self.chart_options.handle_scale.axis_double_click_reset.time
369 && time_axis_rect.contains(pos)
370 {
371 self.state.time_scale_mut().jump_to_latest();
372 self.state
373 .time_scale_mut()
374 .set_bar_spacing(self.chart_options.time_scale.bar_spacing);
375 }
376 if self
377 .chart_options
378 .handle_scale
379 .axis_double_click_reset
380 .price
381 && price_axis_rect.contains(pos)
382 {
383 self.state.set_price_auto_scale(true);
384 }
385 }
386 }
387
388 pub fn handle_box_zoom(
394 &mut self,
395 ui: &Ui,
396 response: &Response,
397 chart_rect: Rect,
398 chart_width: f32,
399 zoom_mode_active: bool,
400 ) -> bool {
401 let btn_pressed = zoom_mode_active && ui.input(|i| i.pointer.primary_pressed());
403 let btn_down = zoom_mode_active && ui.input(|i| i.pointer.primary_down());
404 let btn_released = zoom_mode_active && ui.input(|i| i.pointer.primary_released());
405
406 if btn_pressed
407 && let Some(pos) = response.interact_pointer_pos()
408 && chart_rect.contains(pos)
409 {
410 self.box_zoom.active = true;
411 self.box_zoom.start_pos = Some(pos);
412 self.box_zoom.curr_pos = Some(pos);
413 }
414
415 if btn_down
416 && self.box_zoom.active
417 && let Some(pos) = response.interact_pointer_pos()
418 {
419 self.box_zoom.curr_pos = Some(pos);
420 }
421
422 let mut zoom_applied = false;
423 if btn_released && self.box_zoom.active {
424 if let (Some(start), Some(end)) = (self.box_zoom.start_pos, self.box_zoom.curr_pos)
425 && (chart_rect.contains(start) || chart_rect.contains(end))
426 {
427 match self.box_zoom.mode {
428 BoxZoomMode::Zoom => {
429 zoom_applied = self.execute_box_zoom(start, end, chart_rect, chart_width);
430 }
431 BoxZoomMode::Measure => {
432 }
437 }
438 }
439 self.box_zoom.reset();
440 }
441
442 zoom_applied
443 }
444
445 pub fn execute_box_zoom(
450 &mut self,
451 start: Pos2,
452 end: Pos2,
453 chart_rect: Rect,
454 chart_width: f32,
455 ) -> bool {
456 let min_x = start.x.min(end.x);
457 let max_x = start.x.max(end.x);
458 let min_y = start.y.min(end.y);
459 let max_y = start.y.max(end.y);
460
461 if (max_x - min_x).abs() <= 20.0 || (max_y - min_y).abs() <= 20.0 {
462 return false; }
464
465 self.state.push_zoom_state();
467
468 let left_x_relative = min_x - chart_rect.min.x;
469 let right_x_relative = max_x - chart_rect.min.x;
470
471 let left_idx = self
472 .state
473 .time_scale()
474 .coord_to_idx(min_x, chart_rect.min.x, chart_width);
475 let right_idx = self
476 .state
477 .time_scale()
478 .coord_to_idx(max_x, chart_rect.min.x, chart_width);
479
480 let num_bars = right_idx.max(left_idx) - right_idx.min(left_idx) + 1.0;
481 if num_bars > 0.0 {
482 let new_spacing = (right_x_relative - left_x_relative) / num_bars;
483
484 let min_spacing = self.chart_options.time_scale.min_bar_spacing;
485 let max_spacing = self.chart_options.time_scale.max_bar_spacing;
486 let clamped_spacing = if max_spacing > 0.0 {
487 new_spacing.clamp(min_spacing, max_spacing)
488 } else {
489 new_spacing.max(min_spacing)
490 };
491
492 self.state.time_scale_mut().set_bar_spacing(clamped_spacing);
493
494 let center_idx = (left_idx + right_idx) / 2.0;
495 let base_idx = self.state.time_scale().base_idx() as f32;
496 let visible_bars = chart_width / clamped_spacing;
497 let target_right_offset = center_idx + (visible_bars / 2.0) - base_idx;
498 self.state
499 .time_scale_mut()
500 .set_right_offset(target_right_offset);
501 }
502
503 let price_min_y = max_y;
505 let price_max_y = min_y;
506
507 let price_range_height = chart_rect.height();
508 let price_min_ratio = (chart_rect.max.y - price_min_y) / price_range_height;
509 let price_max_ratio = (chart_rect.max.y - price_max_y) / price_range_height;
510
511 let (curr_min, curr_max) = self.state.price_range();
512 let curr_range = curr_max - curr_min;
513
514 let sel_min_price = curr_min + (price_min_ratio as f64 * curr_range);
515 let sel_max_price = curr_min + (price_max_ratio as f64 * curr_range);
516
517 self.state.set_price_range(sel_min_price, sel_max_price);
518
519 true }
521
522 pub fn apply_price_zoom(
527 &mut self,
528 pending_price_zoom: Option<f32>,
529 response: &Response,
530 chart_rect: Rect,
531 adjusted_min: f64,
532 adjusted_max: f64,
533 ) -> (f64, f64) {
534 let mut new_min = adjusted_min;
535 let mut new_max = adjusted_max;
536
537 if let Some(delta_y) = pending_price_zoom
538 && let Some(hp) = response.hover_pos()
539 {
540 let anchor_price = y_to_price(hp.y, adjusted_min, adjusted_max, chart_rect);
541 let (min, max) = apply_price_zoom(
542 (adjusted_min, adjusted_max),
543 anchor_price,
544 delta_y,
545 chart_rect.height(),
546 );
547 self.state.set_price_range(min, max);
548 new_min = min;
549 new_max = max;
550 }
551
552 if let Some(start_pos) = self.price_scale_drag_start.take()
553 && let Some(curr_pos) = response.interact_pointer_pos()
554 {
555 let dy = curr_pos.y - start_pos.y;
556 let anchor_price = y_to_price(start_pos.y, new_min, new_max, chart_rect);
557 let (min, max) =
558 apply_price_zoom((new_min, new_max), anchor_price, dy, chart_rect.height());
559 self.state.set_price_range(min, max);
560 new_min = min;
561 new_max = max;
562 }
563
564 (new_min, new_max)
565 }
566
567 pub fn apply_timescale_config(&mut self, chart_width: f32) {
570 self.state.time_scale_mut().set_width(chart_width);
571
572 if self.apply_visible_bars_once {
573 if let Some(desired) = self.desired_visible_bars
574 && desired > 0
575 {
576 let mut spacing = self.calculate_bar_spacing(chart_width, desired);
577 let min = self.chart_options.time_scale.min_bar_spacing;
578 let max = self.chart_options.time_scale.max_bar_spacing;
579 spacing = if max > 0.0 {
580 spacing.clamp(min, max)
581 } else {
582 spacing.max(min)
583 };
584 self.state.time_scale_mut().set_bar_spacing(spacing);
585 }
586 self.apply_visible_bars_once = false;
587 }
588
589 if self
591 .chart_options
592 .time_scale
593 .lock_visible_time_range_on_resize
594 {
595 if let Some(prev_width) = self.prev_width
596 && (chart_width - prev_width).abs() > 1.0
597 {
598 let prev_visible_bars = self.calculate_visible_bars(prev_width);
599 if prev_visible_bars > 0 {
600 let mut spacing = chart_width / prev_visible_bars as f32;
601 let min = self.chart_options.time_scale.min_bar_spacing;
602 let max = self.chart_options.time_scale.max_bar_spacing;
603 spacing = if max > 0.0 {
604 spacing.clamp(min, max)
605 } else {
606 spacing.max(min)
607 };
608 self.state.time_scale_mut().set_bar_spacing(spacing);
609 }
610 }
611 self.prev_width = Some(chart_width);
612 }
613
614 if let Some(target_start) = self.pending_start_idx.take() {
616 let bar_spacing = self.state.time_scale().bar_spacing();
617 if bar_spacing > 0.0 {
618 let visible_len = chart_width / bar_spacing;
619 let base_idx = self.state.time_scale().base_idx() as f32;
620 let desired_right_border = (target_start as f32) + visible_len - 1.0;
621 let desired_offset = desired_right_border - base_idx;
622 self.state.time_scale_mut().set_right_offset(desired_offset);
623 }
624 }
625 }
626}