use crate::drawings::domain::{Drawing, DrawingToolType, HandlePos};
use crate::drawings::services::{
DrawingInteraction, HandleConfig, HandleService, HistoryService, SelectionService, SnapOptions,
SnapService,
};
use crate::tokens::DESIGN_TOKENS;
use egui::{Pos2, Rect};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
pub enum DrawingSyncAction {
SaveDrawing(usize),
UpdateDrawing(usize),
Delete(String),
RestoreDrawings(Vec<crate::drawings::persistence::StoredDrawing>),
}
#[derive(Debug, Clone, Default)]
pub struct DrawingState {
pub active_tool: Option<DrawingToolType>,
pub sel_drawing: Option<usize>,
pub magnet_mode: bool,
pub stay_in_drawing_mode: bool,
pub curr_color: [u8; 4],
pub eraser_mode: bool,
}
#[derive(Clone, Debug)]
pub struct DrawingManagerOptions {
pub default_color: [u8; 4],
pub snap_to_price: bool,
pub snap_to_time: bool,
pub snap_distance: f32,
pub magnet_mode: bool,
pub magnet_distance: f32,
pub handle_config: HandleConfig,
}
impl Default for DrawingManagerOptions {
fn default() -> Self {
Self {
default_color: [41, 98, 255, 255],
snap_to_price: true,
snap_to_time: true,
snap_distance: DESIGN_TOKENS.spacing.lg + DESIGN_TOKENS.spacing.xs,
magnet_mode: false,
magnet_distance: DESIGN_TOKENS.sizing.drawing.magnet_distance,
handle_config: HandleConfig::default(),
}
}
}
pub struct DrawingManager {
pub drawings: Vec<Drawing>,
pub active_tool: Option<DrawingToolType>,
pub curr_drawing: Option<Drawing>,
next_id: usize,
selection: SelectionService,
history: HistoryService,
snap_service: SnapService,
pub options: DrawingManagerOptions,
pub dragging_handle: Option<(usize, HandlePos)>,
drag_old_state: Option<Drawing>,
pub stay_in_drawing_mode: bool,
pub curr_timeframe: String,
pub eraser_mode: bool,
pub eraser_hover_drawing: Option<usize>,
pub pending_sync: Arc<Mutex<Vec<DrawingSyncAction>>>,
}
impl Default for DrawingManager {
fn default() -> Self {
Self::new()
}
}
impl DrawingManager {
pub fn new() -> Self {
Self {
drawings: Vec::new(),
active_tool: None,
curr_drawing: None,
next_id: 0,
selection: SelectionService::new(),
history: HistoryService::new(),
snap_service: SnapService::new(),
options: DrawingManagerOptions::default(),
dragging_handle: None,
drag_old_state: None,
stay_in_drawing_mode: false,
curr_timeframe: String::from("1D"),
eraser_mode: false,
eraser_hover_drawing: None,
pending_sync: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn sync_from_app_state(&mut self, drawing_state: &DrawingState) {
self.active_tool = drawing_state.active_tool;
self.options.magnet_mode = drawing_state.magnet_mode;
self.options.default_color = drawing_state.curr_color;
self.stay_in_drawing_mode = drawing_state.stay_in_drawing_mode;
self.eraser_mode = drawing_state.eraser_mode;
match (drawing_state.sel_drawing, self.selection.primary()) {
(Some(id), current) if current != Some(id) => {
self.selection.select(id);
}
(None, Some(_)) => {
self.selection.deselect_all();
}
_ => {} }
}
pub fn sel_drawing(&self) -> Option<usize> {
self.selection.primary()
}
pub fn select(&mut self, id: usize) {
self.selection.select(id);
}
pub fn deselect(&mut self) {
self.selection.deselect_all();
}
pub fn select_at(&mut self, point: Pos2) -> bool {
if let Some(id) = self.hit_test(point) {
self.selection.select(id);
true
} else {
self.selection.deselect_all();
false
}
}
pub fn undo(&mut self) -> bool {
self.history.undo(&mut self.drawings).is_some()
}
pub fn redo(&mut self) -> bool {
self.history.redo(&mut self.drawings).is_some()
}
pub fn can_undo(&self) -> bool {
self.history.can_undo()
}
pub fn can_redo(&self) -> bool {
self.history.can_redo()
}
pub fn update_snap_options(&mut self) {
self.snap_service = SnapService::with_options(SnapOptions {
snap_to_price: self.options.snap_to_price,
snap_to_time: self.options.snap_to_time,
snap_distance: self.options.snap_distance,
magnet_mode: self.options.magnet_mode,
magnet_distance: self.options.magnet_distance,
});
}
pub fn snap_point(&self, point: Pos2, drawings: &[Drawing]) -> Pos2 {
self.snap_service.snap_point_with_drawing_points(
point,
&[],
&[],
drawings.iter().flat_map(|d| d.points.iter().copied()),
)
}
pub fn get_handles(&self, drawing: &Drawing) -> Vec<(HandlePos, Pos2)> {
HandleService::get_handles(drawing)
}
pub fn hit_test_handle(&self, drawing: &Drawing, point: Pos2) -> Option<HandlePos> {
HandleService::hit_test_handle(drawing, point, self.options.handle_config.size)
}
pub fn start_drag_handle(&mut self, drawing_id: usize, handle: HandlePos) {
if let Some(drawing) = self.drawings.iter().find(|d| d.id == drawing_id) {
self.dragging_handle = Some((drawing_id, handle));
self.drag_old_state = Some(drawing.clone());
}
}
pub fn update_drag_handle<F, G>(&mut self, new_pos: Pos2, x_to_bar: F, y_to_price: G)
where
F: Fn(f32) -> f32,
G: Fn(f32) -> f64,
{
if let Some((id, handle)) = self.dragging_handle {
let snapped = self.snap_point(new_pos, &[]);
if let Some(drawing) = self.drawings.iter_mut().find(|d| d.id == id) {
HandleService::update_handle(drawing, handle, snapped, x_to_bar, y_to_price);
}
}
}
pub fn end_drag_handle(&mut self) {
if let Some(old_state) = self.drag_old_state.take() {
let drawing_id = old_state.id;
self.history.push_modify(old_state.id, old_state);
if let Ok(mut sync) = self.pending_sync.lock() {
sync.push(DrawingSyncAction::UpdateDrawing(drawing_id));
}
}
self.dragging_handle = None;
}
pub fn render_handles(&self, painter: &egui::Painter, drawing: &Drawing) {
let dragging = self
.dragging_handle
.filter(|(id, _)| *id == drawing.id)
.map(|(_, h)| h);
HandleService::render_handles(painter, drawing, &self.options.handle_config, dragging);
}
pub fn set_active_tool(&mut self, tool: Option<DrawingToolType>) {
self.active_tool = tool;
if tool.is_some() {
self.curr_drawing = None;
}
}
pub fn start_text_annotation(&mut self) {
self.set_active_tool(Some(DrawingToolType::Note));
}
pub fn start_icon_insertion(&mut self, icon_name: String) {
self.set_active_tool(Some(DrawingToolType::FontIcon));
let mut drawing = Drawing::with_color(
self.next_id,
DrawingToolType::FontIcon,
self.options.default_color,
);
self.next_id += 1;
drawing.text = Some(icon_name);
drawing.font_size = 32.0; drawing.completed = false;
self.curr_drawing = Some(drawing);
}
pub fn start_drawing_with_coords<F, G>(
&mut self,
tool_type: DrawingToolType,
point: Pos2,
x_to_bar: F,
y_to_price: G,
) where
F: Fn(f32) -> f32,
G: Fn(f32) -> f64,
{
let mut drawing = Drawing::with_color(self.next_id, tool_type, self.options.default_color);
let drawing_id = self.next_id;
self.next_id += 1;
drawing.add_point_with_chart_coords(point, x_to_bar, y_to_price);
if drawing.completed {
self.history.push_add(drawing.clone());
self.drawings.push(drawing);
if let Ok(mut sync) = self.pending_sync.lock() {
sync.push(DrawingSyncAction::SaveDrawing(drawing_id));
}
self.curr_drawing = None;
if !self.stay_in_drawing_mode {
self.active_tool = None;
}
} else {
self.curr_drawing = Some(drawing);
}
}
pub fn add_point_with_coords<F, G>(&mut self, point: Pos2, x_to_bar: F, y_to_price: G)
where
F: Fn(f32) -> f32,
G: Fn(f32) -> f64,
{
if let Some(ref mut drawing) = self.curr_drawing {
let drawing_id = drawing.id;
drawing.add_point_with_chart_coords(point, &x_to_bar, &y_to_price);
if drawing.completed {
self.history.push_add(drawing.clone());
self.drawings.push(drawing.clone());
if let Ok(mut sync) = self.pending_sync.lock() {
sync.push(DrawingSyncAction::SaveDrawing(drawing_id));
}
self.curr_drawing = None;
if !self.stay_in_drawing_mode {
self.active_tool = None;
}
}
}
}
pub fn update_last_point_with_coords<F, G>(&mut self, point: Pos2, x_to_bar: F, y_to_price: G)
where
F: Fn(f32) -> f32,
G: Fn(f32) -> f64,
{
if let Some(ref mut drawing) = self.curr_drawing
&& !drawing.points.is_empty()
&& !drawing.completed
{
if drawing.points.len() == 1 {
drawing.add_point_with_chart_coords(point, &x_to_bar, &y_to_price);
drawing.completed = false;
} else if drawing.points.len() >= 2 {
drawing.update_last_point_with_chart_coords(point, x_to_bar, y_to_price);
}
}
}
pub fn complete_curr_drawing(&mut self) {
if let Some(ref mut drawing) = self.curr_drawing
&& drawing.points.len() >= 2
{
drawing.completed = true;
self.history.push_add(drawing.clone());
self.drawings.push(drawing.clone());
self.curr_drawing = None;
if !self.stay_in_drawing_mode {
self.active_tool = None;
}
}
}
pub fn cancel_curr_drawing(&mut self) {
self.curr_drawing = None;
}
pub fn delete_drawing(&mut self, id: usize) {
if let Some(drawing) = self.drawings.iter().find(|d| d.id == id).cloned() {
self.drawings.retain(|d| d.id != id);
self.history.push_delete(drawing);
if let Ok(mut sync) = self.pending_sync.lock() {
sync.push(DrawingSyncAction::Delete(id.to_string()));
}
}
if self.selection.primary() == Some(id) {
self.selection.deselect_all();
}
}
pub fn delete_selected(&mut self) {
if let Some(id) = self.selection.primary() {
self.delete_drawing(id);
}
}
pub fn clear_all(&mut self) {
self.drawings.clear();
self.curr_drawing = None;
self.selection.deselect_all();
}
pub fn bring_to_front(&mut self, id: usize) {
if let Some(idx) = self.drawings.iter().position(|d| d.id == id) {
let drawing = self.drawings.remove(idx);
self.drawings.push(drawing);
}
}
pub fn send_to_back(&mut self, id: usize) {
if let Some(idx) = self.drawings.iter().position(|d| d.id == id) {
let drawing = self.drawings.remove(idx);
self.drawings.insert(0, drawing);
}
}
pub fn update_all_screen_coords<F, G>(&mut self, bar_to_x: F, price_to_y: G)
where
F: Fn(f32) -> f32 + Copy,
G: Fn(f64) -> f32 + Copy,
{
for drawing in &mut self.drawings {
drawing.update_screen_coords(bar_to_x, price_to_y);
}
if let Some(ref mut drawing) = self.curr_drawing {
drawing.update_screen_coords(bar_to_x, price_to_y);
}
}
pub fn shift_bar_indices(&mut self, shift: f32) {
if shift.abs() < 0.001 {
return;
}
for drawing in &mut self.drawings {
for cp in &mut drawing.chart_points {
cp.bar_idx += shift;
}
}
if let Some(ref mut curr) = self.curr_drawing {
for cp in &mut curr.chart_points {
cp.bar_idx += shift;
}
}
self.history.shift_bar_indices(shift);
}
pub fn hit_test(&self, point: Pos2) -> Option<usize> {
DrawingInteraction::new().hit_test(point, &self.drawings, &self.curr_timeframe)
}
pub fn toggle_visibility_all(&mut self) {
let all_visible = self.drawings.iter().all(|d| d.visible);
for drawing in &mut self.drawings {
drawing.visible = !all_visible;
}
}
pub fn toggle_lock_all(&mut self) {
let all_locked = self.drawings.iter().all(|d| d.locked);
for drawing in &mut self.drawings {
drawing.locked = !all_locked;
}
}
pub fn complete_drag_drawing(&mut self) {
self.complete_curr_drawing();
}
pub fn update_drag_handle_with_coords<F, G>(
&mut self,
new_pos: Pos2,
x_to_bar: F,
y_to_price: G,
) where
F: Fn(f32) -> f32,
G: Fn(f32) -> f64,
{
self.update_drag_handle(new_pos, x_to_bar, y_to_price);
}
pub fn hit_test_handle_by_id(&self, point: Pos2, drawing_id: usize) -> Option<HandlePos> {
if let Some(drawing) = self.drawings.iter().find(|d| d.id == drawing_id) {
self.hit_test_handle(drawing, point)
} else {
None
}
}
pub fn update_pos_prices(&mut self, price: f64) {
for drawing in &mut self.drawings {
drawing.curr_price = Some(price);
}
}
pub fn add_synced_drawing(&mut self, mut drawing: Drawing) {
drawing.id = self.next_id;
self.next_id += 1;
self.drawings.push(drawing);
}
pub fn render_all(&self, painter: &egui::Painter, price_rect: Rect) {
for drawing in &self.drawings {
if drawing.visible {
drawing.render(painter, price_rect);
}
}
if let Some(ref drawing) = self.curr_drawing {
drawing.render(painter, price_rect);
}
if let Some(sel_id) = self.selection.primary()
&& let Some(drawing) = self.drawings.iter().find(|d| d.id == sel_id)
{
self.render_handles(painter, drawing);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_manager() {
let manager = DrawingManager::new();
assert!(manager.drawings.is_empty());
assert!(manager.active_tool.is_none());
}
#[test]
fn test_selection() {
let mut manager = DrawingManager::new();
manager.select(1);
assert_eq!(manager.sel_drawing(), Some(1));
manager.deselect();
assert_eq!(manager.sel_drawing(), None);
}
#[test]
fn test_hit_test_fibonacci_level_off_control_point() {
let mut manager = DrawingManager::new();
let mut fib = Drawing::new(7, DrawingToolType::FibonacciRetracement);
fib.points = vec![Pos2::new(100.0, 100.0), Pos2::new(300.0, 200.0)];
fib.completed = true;
manager.drawings.push(fib);
let click = Pos2::new(180.0, 123.6);
for handle in [
Pos2::new(100.0, 100.0),
Pos2::new(300.0, 200.0),
Pos2::new(200.0, 150.0),
] {
let d = ((click.x - handle.x).powi(2) + (click.y - handle.y).powi(2)).sqrt();
assert!(d > 20.0, "click must be off control points, was {d}px away");
}
assert_eq!(manager.hit_test(click), Some(7));
assert!(manager.select_at(click));
assert_eq!(manager.sel_drawing(), Some(7));
}
#[test]
fn test_hit_test_channel_segment_off_control_point() {
let mut manager = DrawingManager::new();
let mut channel = Drawing::new(11, DrawingToolType::ParallelChannel);
channel.points = vec![
Pos2::new(100.0, 100.0),
Pos2::new(300.0, 100.0),
Pos2::new(200.0, 200.0),
];
channel.completed = true;
manager.drawings.push(channel);
let click = Pos2::new(150.0, 100.0);
for handle in [
Pos2::new(100.0, 100.0),
Pos2::new(300.0, 100.0),
Pos2::new(200.0, 200.0),
] {
let d = ((click.x - handle.x).powi(2) + (click.y - handle.y).powi(2)).sqrt();
assert!(d > 20.0, "click must be off control points, was {d}px away");
}
assert_eq!(manager.hit_test(click), Some(11));
}
#[test]
fn test_snap_point_borrows_magnet_targets() {
let mut manager = DrawingManager::new();
manager.options.magnet_mode = true;
manager.update_snap_options();
let mut d = Drawing::new(1, DrawingToolType::TrendLine);
d.points = vec![Pos2::new(100.0, 100.0), Pos2::new(200.0, 200.0)];
let drawings = vec![d];
let snapped = manager.snap_point(Pos2::new(103.0, 102.0), &drawings);
assert_eq!(snapped, Pos2::new(100.0, 100.0));
}
}