clin-rs 0.4.4

Encrypted terminal note-taking app
use std::sync::{Arc, RwLock};
use std::time::Instant;

use crossterm::event::{KeyEvent, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;

use super::GraphState;

pub fn handle_graph_keys(app: &mut crate::app::App, key: KeyEvent) -> bool {
    let keybinds = &app.keybinds;

    if keybinds.matches_graph(crate::keybinds::GraphAction::Quit, &key) {
        app.close_graph_view();
        return false;
    }
    if keybinds.matches_graph(crate::keybinds::GraphAction::Help, &key) {
        app.open_help_page();
        return false;
    }

    if app.graph_state.is_none() {
        return false;
    }

    if keybinds.matches_graph(crate::keybinds::GraphAction::PanUp, &key) {
        let state = app.graph_state.as_ref().unwrap();
        let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
        guard.viewport.pan_up();
    } else if keybinds.matches_graph(crate::keybinds::GraphAction::PanDown, &key) {
        let state = app.graph_state.as_ref().unwrap();
        let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
        guard.viewport.pan_down();
    } else if keybinds.matches_graph(crate::keybinds::GraphAction::PanLeft, &key) {
        let state = app.graph_state.as_ref().unwrap();
        let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
        guard.viewport.pan_left();
    } else if keybinds.matches_graph(crate::keybinds::GraphAction::PanRight, &key) {
        let state = app.graph_state.as_ref().unwrap();
        let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
        guard.viewport.pan_right();
    } else if keybinds.matches_graph(crate::keybinds::GraphAction::ZoomIn, &key) {
        let state = app.graph_state.as_ref().unwrap();
        let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
        guard.viewport.zoom_in();
    } else if keybinds.matches_graph(crate::keybinds::GraphAction::ZoomOut, &key) {
        let state = app.graph_state.as_ref().unwrap();
        let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
        guard.viewport.zoom_out();
    } else if keybinds.matches_graph(crate::keybinds::GraphAction::OpenNote, &key) {
        let note_id = {
            let state = app.graph_state.as_ref().unwrap();
            let guard = state.read().unwrap_or_else(|e| e.into_inner());
            guard.selected_node.and_then(|idx| {
                guard
                    .simulation
                    .get_graph()
                    .node_weight(idx)
                    .map(|n| n.data.note_id.clone())
            })
        };
        if let Some(id) = note_id {
            app.open_note_from_graph(&id);
        }
    } else if keybinds.matches_graph(crate::keybinds::GraphAction::AutoFit, &key) {
        let state = app.graph_state.as_ref().unwrap();
        let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
        let viewport = guard.viewport.clone();
        let vp = viewport.auto_fit_from_graph(guard.simulation.get_graph());
        guard.viewport = vp;
    }

    false
}

#[derive(Default)]
pub struct GraphMouseState {
    pub drag_origin: Option<(u16, u16)>,
    pub is_panning: bool,
    pub last_click_time: Option<Instant>,
    pub last_clicked_node: Option<fdg_sim::petgraph::graph::NodeIndex>,
}

pub fn handle_graph_mouse(
    state: &Arc<RwLock<GraphState>>,
    mouse_event: MouseEvent,
    area: Rect,
    mouse_state: &mut GraphMouseState,
) {
    match mouse_event.kind {
        MouseEventKind::ScrollUp => {
            let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
            guard.viewport.zoom_in();
        }
        MouseEventKind::ScrollDown => {
            let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
            guard.viewport.zoom_out();
        }
        MouseEventKind::Down(MouseButton::Left) => {
            let (wx, wy) = {
                let guard = state.read().unwrap_or_else(|e| e.into_inner());
                guard
                    .viewport
                    .screen_to_world(mouse_event.column, mouse_event.row, area)
            };

            let hit = {
                let guard = state.read().unwrap_or_else(|e| e.into_inner());
                guard.viewport.hit_test(wx, wy, &guard)
            };

            let is_double_click = mouse_state
                .last_click_time
                .is_some_and(|t| t.elapsed().as_millis() < 300);

            if let Some(node_idx) = hit {
                let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
                guard.selected_node = Some(node_idx);
                guard.dragging_node = Some(node_idx);
                mouse_state.drag_origin = Some((mouse_event.column, mouse_event.row));
                mouse_state.is_panning = false;
                mouse_state.last_clicked_node = Some(node_idx);
            } else {
                let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
                if is_double_click {
                    guard.selected_node = None;
                }
                guard.dragging_node = None;
                mouse_state.drag_origin = Some((mouse_event.column, mouse_event.row));
                mouse_state.is_panning = true;
                mouse_state.last_clicked_node = None;
            }
        }
        MouseEventKind::Drag(MouseButton::Left) => {
            let Some((orig_col, orig_row)) = mouse_state.drag_origin else {
                return;
            };

            if mouse_state.is_panning {
                let dx = -(mouse_event.column as f64 - orig_col as f64) * 0.2;
                let dy = (mouse_event.row as f64 - orig_row as f64) * 0.2;
                let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
                guard.viewport.pan(dx, dy);
                mouse_state.drag_origin = Some((mouse_event.column, mouse_event.row));
            } else {
                let (wx, wy) = {
                    let guard = state.read().unwrap_or_else(|e| e.into_inner());
                    guard
                        .viewport
                        .screen_to_world(mouse_event.column, mouse_event.row, area)
                };

                let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
                if let Some(node_idx) = guard.dragging_node {
                    let graph = guard.simulation.get_graph_mut();
                    if let Some(node) = graph.node_weight_mut(node_idx) {
                        node.location.x = wx as f32;
                        node.location.y = wy as f32;
                        node.velocity = fdg_sim::glam::Vec3::ZERO;
                    }
                    guard.is_settled = false;
                }
                mouse_state.drag_origin = Some((mouse_event.column, mouse_event.row));
            }
        }
        MouseEventKind::Up(MouseButton::Left) => {
            {
                let mut guard = state.write().unwrap_or_else(|e| e.into_inner());
                guard.dragging_node = None;
            }
            mouse_state.drag_origin = None;
            mouse_state.is_panning = false;
            mouse_state.last_click_time = Some(Instant::now());
        }
        _ => {}
    }
}