use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use super::{
transform::WorldPos,
viewer::{Link, NodeId, PortAddr, StackId},
};
pub const RIGHT_CLICK_MAX_SECS: f64 = 0.35;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CanvasItem {
Node(NodeId),
Stack(StackId),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct GridConfig {
pub enabled: bool,
pub snap: bool,
pub spacing: f64,
pub major_every: u32,
}
impl Default for GridConfig {
fn default() -> Self {
Self {
enabled: true,
snap: true,
spacing: 24.0,
major_every: 5,
}
}
}
impl GridConfig {
pub fn snap_pos(&self, p: WorldPos) -> WorldPos {
let s = self.spacing.max(f64::EPSILON);
(p / s).round() * s
}
}
#[derive(Debug, Clone, Copy)]
pub struct ReorderDrag {
pub stack: StackId,
pub node: NodeId,
pub from_index: usize,
pub target_index: usize,
pub grab_offset: WorldPos,
}
#[derive(Debug, Clone, Copy)]
pub enum DragItem {
Node(NodeId),
Stack(StackId),
}
#[derive(Debug, Clone)]
pub struct CanvasDrag {
pub primary_origin: WorldPos,
pub grab_offset: WorldPos,
pub nodes: Vec<(NodeId, WorldPos)>,
pub stacks: Vec<(StackId, WorldPos)>,
}
#[derive(Debug, Clone, Default)]
pub struct Interaction {
pub canvas_drag: Option<CanvasDrag>,
pub reordering: Option<ReorderDrag>,
pub pending_link_from: Option<PortAddr>,
pub pending_from_input: bool,
pub detaching_link: Option<Link>,
pub box_select_start: Option<WorldPos>,
pub secondary_press: Option<(egui::Pos2, f64)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphView {
pub pan: WorldPos,
pub zoom: f64,
pub grid: GridConfig,
pub positions: HashMap<NodeId, WorldPos>,
pub stack_positions: HashMap<StackId, WorldPos>,
#[serde(skip)]
pub selection: HashSet<NodeId>,
#[serde(skip)]
pub selected_stacks: HashSet<StackId>,
#[serde(skip)]
pub selected_links: HashSet<Link>,
#[serde(skip)]
pub collapsed: HashSet<NodeId>,
#[serde(default)]
pub z_order: Vec<CanvasItem>,
#[serde(skip)]
pub interaction: Interaction,
#[serde(skip)]
last_viewport_size: Option<egui::Vec2>,
}
impl Default for GraphView {
fn default() -> Self {
Self {
pan: WorldPos::ZERO,
zoom: 1.0,
grid: GridConfig::default(),
positions: HashMap::new(),
stack_positions: HashMap::new(),
selection: HashSet::new(),
selected_stacks: HashSet::new(),
selected_links: HashSet::new(),
collapsed: HashSet::new(),
z_order: Vec::new(),
interaction: Interaction::default(),
last_viewport_size: None,
}
}
}
pub const MIN_ZOOM: f64 = 0.05;
pub const MAX_ZOOM: f64 = 2.0;
impl GraphView {
pub fn update_viewport_size(&mut self, size: egui::Vec2) {
if size.x <= 0.0 || size.y <= 0.0 {
return;
}
let Some(previous) = self.last_viewport_size.replace(size) else {
return;
};
let delta = previous - size;
self.pan += WorldPos::new(delta.x as f64, delta.y as f64) / (2.0 * self.zoom);
}
pub fn position(&self, id: NodeId) -> WorldPos {
self.positions.get(&id).copied().unwrap_or(WorldPos::ZERO)
}
pub fn ensure_position(&mut self, id: NodeId, default: WorldPos) -> WorldPos {
*self.positions.entry(id).or_insert(default)
}
pub fn stack_position(&self, id: StackId) -> WorldPos {
self.stack_positions
.get(&id)
.copied()
.unwrap_or(WorldPos::ZERO)
}
#[allow(dead_code)]
pub fn ensure_stack_position(&mut self, id: StackId, default: WorldPos) -> WorldPos {
*self.stack_positions.entry(id).or_insert(default)
}
pub fn set_zoom_clamped(&mut self, zoom: f64) {
self.zoom = zoom.clamp(MIN_ZOOM, MAX_ZOOM);
}
pub fn is_collapsed(&self, id: NodeId) -> bool {
self.collapsed.contains(&id)
}
pub fn toggle_collapsed(&mut self, id: NodeId) {
if !self.collapsed.remove(&id) {
self.collapsed.insert(id);
}
}
pub fn raise(&mut self, item: CanvasItem) {
if self.z_order.last() == Some(&item) {
return;
}
self.z_order.retain(|i| *i != item);
self.z_order.push(item);
}
pub fn z_key(&self, item: CanvasItem) -> (u8, usize) {
match self.z_order.iter().position(|i| *i == item) {
Some(pos) => (1, pos),
None => (0, 0),
}
}
pub fn clear_selection(&mut self) -> bool {
let had = !self.selection.is_empty()
|| !self.selected_stacks.is_empty()
|| !self.selected_links.is_empty();
self.selection.clear();
self.selected_stacks.clear();
self.selected_links.clear();
had
}
}