use super::GraphInfo;
use super::camera::Camera2D;
use super::euclid::{IntoEuclid, LayoutPoint, WorldPoint, WorldRect};
use super::focus::{Easing, FocusOptions};
use super::{DEFAULT_CORE_SIZE, DEFAULT_ORBIT_OFFSET, DEFAULT_ORBIT_SPACING};
use crate::style::EdgeCurve;
use iced_widget::core::{Layout, Padding, Point, Size, keyboard, touch};
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use web_time::Instant;
#[derive(Debug, Clone, Copy)]
pub(super) struct CameraTween {
pub(super) start_center: WorldPoint,
pub(super) start_zoom: f32,
pub(super) end_center: WorldPoint,
pub(super) end_zoom: f32,
pub(super) viewport: Size,
pub(super) padding: Padding,
pub(super) elapsed: f32,
pub(super) duration: f32,
pub(super) easing: Easing,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) enum Dragging {
#[default]
None,
Graph(WorldPoint),
Node {
node: usize,
origin: LayoutPoint,
followers: Vec<usize>,
},
GroupMove {
origin: LayoutPoint,
anchor: usize,
followers: Vec<usize>,
},
Resize {
node: usize,
origin: LayoutPoint,
start: Size,
},
Edge {
from_node: usize,
from_pin: usize,
origin: LayoutPoint,
},
EdgeOver {
from_node: usize,
from_pin: usize,
to_node: usize,
to_pin: usize,
},
SelectionBox(LayoutPoint, LayoutPoint),
EdgeCutting {
trail: Vec<LayoutPoint>,
pending_cuts: HashSet<usize>,
},
Anchor { anchor: usize, origin: WorldPoint },
Route {
edge: usize,
detached: Option<usize>,
},
RouteOver {
edge: usize,
anchor: usize,
detached: Option<usize>,
},
PressPending {
origin_world: WorldPoint,
origin_screen: Point,
target: PressTarget,
},
Minimap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PressTarget {
AnchorCore { anchor: usize },
Wrap { edge: usize, anchor: usize },
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct AnchorGeometry {
pub(super) core_size: f32,
pub(super) orbit_offset: f32,
pub(super) orbit_spacing: f32,
}
impl Default for AnchorGeometry {
fn default() -> Self {
Self {
core_size: DEFAULT_CORE_SIZE,
orbit_offset: DEFAULT_ORBIT_OFFSET,
orbit_spacing: DEFAULT_ORBIT_SPACING,
}
}
}
impl AnchorGeometry {
pub(super) fn core_half(&self) -> f32 {
self.core_size * 0.5
}
pub(super) fn orbit_radius(&self, orbit: u8) -> f32 {
self.orbit_offset + orbit as f32 * self.orbit_spacing
}
}
#[derive(Debug)]
pub(super) struct NodeGraphState {
pub(super) camera: Camera2D,
pub(super) dragging: Dragging,
pub(super) time: f32,
pub(super) last_update: Option<Instant>,
pub(super) pending_selection: Option<HashSet<usize>>,
pub(super) selection_baseline: Option<HashSet<usize>>,
pub(super) modifiers: keyboard::Modifiers,
pub(super) valid_drop_targets: HashSet<(usize, usize)>,
pub(super) anchor_geometry: RefCell<Vec<AnchorGeometry>>,
pub(super) edge_curves: RefCell<Vec<EdgeCurve>>,
pub(super) last_synced_camera: Option<(Point, f32)>,
pub(super) camera_tween: Option<CameraTween>,
pub(super) pending_focus: Option<(WorldRect, FocusOptions)>,
pub(super) last_redraw: Option<Instant>,
pub(super) sdf_animated: Cell<bool>,
pub(super) hover_zone: Cell<bool>,
pub(super) last_info: RefCell<Option<GraphInfo>>,
pub(super) node_z: HashMap<usize, u64>,
pub(super) z_counter: u64,
pub(super) fingers: Vec<(touch::Finger, Point)>,
pub(super) touch_tap: Option<(touch::Finger, Point, f32)>,
}
impl Default for NodeGraphState {
fn default() -> Self {
Self {
camera: Camera2D::new(),
dragging: Default::default(),
time: 0.0,
last_update: None,
pending_selection: None,
selection_baseline: None,
modifiers: keyboard::Modifiers::default(),
valid_drop_targets: HashSet::new(),
anchor_geometry: RefCell::new(Vec::new()),
edge_curves: RefCell::new(Vec::new()),
last_synced_camera: None,
camera_tween: None,
pending_focus: None,
last_redraw: None,
sdf_animated: Cell::new(false),
hover_zone: Cell::new(false),
last_info: RefCell::new(None),
node_z: HashMap::new(),
z_counter: 0,
fingers: Vec::new(),
touch_tap: None,
}
}
}
impl NodeGraphState {
pub(super) fn camera_for(&self, layout: Layout<'_>) -> Camera2D {
self.camera
.with_viewport_origin(layout.bounds().position().into_euclid().to_vector())
}
pub(super) fn animation_time(&self, now: Instant) -> f32 {
match self.last_update {
Some(last_update) => self.time + now.duration_since(last_update).as_secs_f32().min(0.1),
None => self.time,
}
}
pub(super) fn ensure_z_entries(&mut self, node_count: usize) {
for idx in 0..node_count {
if let std::collections::hash_map::Entry::Vacant(e) = self.node_z.entry(idx) {
e.insert(self.z_counter);
self.z_counter = self.z_counter.wrapping_add(1);
}
}
}
pub(super) fn promote_z(&mut self, idx: usize) {
self.node_z.insert(idx, self.z_counter);
self.z_counter = self.z_counter.wrapping_add(1);
}
pub(super) fn promote_z_many(&mut self, indices: &[usize]) {
let mut sorted: Vec<usize> = indices.to_vec();
sorted.sort_by_key(|i| self.node_z.get(i).copied().unwrap_or(0));
for idx in sorted {
self.promote_z(idx);
}
}
}
pub(super) fn z_render_indices(
state: &NodeGraphState,
node_count: usize,
is_selected: impl Fn(usize) -> bool,
is_frame: impl Fn(usize) -> bool,
) -> Vec<usize> {
let mut indices: Vec<usize> = (0..node_count).collect();
indices.sort_by_key(|&i| {
let z = state.node_z.get(&i).copied().unwrap_or(0);
(!is_frame(i), is_selected(i), z)
});
indices
}
#[cfg(test)]
mod tests {
use super::*;
use euclid::Point2D;
#[test]
fn test_dragging_default_is_none() {
let dragging: Dragging = Default::default();
assert_eq!(dragging, Dragging::None);
}
#[test]
fn test_dragging_states_not_equal() {
let world = WorldPoint::new(10.0, 20.0);
let origin = LayoutPoint::new(10.0, 20.0);
assert_ne!(Dragging::None, Dragging::Graph(world));
let node_drag = Dragging::Node {
node: 0,
origin,
followers: Vec::new(),
};
assert_ne!(Dragging::Graph(world), node_drag);
assert_ne!(
node_drag,
Dragging::Edge {
from_node: 0,
from_pin: 0,
origin
}
);
}
#[test]
fn test_dragging_graph_stores_origin() {
let origin = Point2D::new(100.0, 200.0);
let dragging = Dragging::Graph(origin);
if let Dragging::Graph(stored) = dragging {
assert_eq!(stored.x, 100.0);
assert_eq!(stored.y, 200.0);
} else {
panic!("Expected Dragging::Graph");
}
}
#[test]
fn test_dragging_node_stores_index_and_origin() {
let origin = Point2D::new(50.0, 75.0);
let dragging = Dragging::Node {
node: 5,
origin,
followers: vec![7],
};
if let Dragging::Node {
node: idx,
origin: stored,
followers,
} = dragging
{
assert_eq!(idx, 5);
assert_eq!(stored.x, 50.0);
assert_eq!(stored.y, 75.0);
assert_eq!(followers, vec![7]);
} else {
panic!("Expected Dragging::Node");
}
}
#[test]
fn test_dragging_edge_stores_node_pin_and_cursor() {
let cursor = Point2D::new(300.0, 400.0);
let dragging = Dragging::Edge {
from_node: 2,
from_pin: 1,
origin: cursor,
};
if let Dragging::Edge {
from_node: node,
from_pin: pin,
origin: stored,
} = dragging
{
assert_eq!(node, 2);
assert_eq!(pin, 1);
assert_eq!(stored.x, 300.0);
assert_eq!(stored.y, 400.0);
} else {
panic!("Expected Dragging::Edge");
}
}
#[test]
fn selection_box_stores_two_points() {
let start = Point2D::new(0.0, 0.0);
let current = Point2D::new(100.0, 100.0);
let dragging = Dragging::SelectionBox(start, current);
if let Dragging::SelectionBox(s, c) = dragging {
assert_eq!(s.x, 0.0);
assert_eq!(s.y, 0.0);
assert_eq!(c.x, 100.0);
assert_eq!(c.y, 100.0);
} else {
panic!("Expected Dragging::SelectionBox");
}
}
#[test]
fn test_group_move_stores_origin() {
let origin = Point2D::new(250.0, 350.0);
let dragging = Dragging::GroupMove {
origin,
anchor: 3,
followers: Vec::new(),
};
if let Dragging::GroupMove {
origin: stored,
anchor,
..
} = dragging
{
assert_eq!(stored.x, 250.0);
assert_eq!(stored.y, 350.0);
assert_eq!(anchor, 3);
} else {
panic!("Expected Dragging::GroupMove");
}
}
#[test]
fn test_edge_cutting_trail() {
let trail = vec![
Point2D::new(0.0, 0.0),
Point2D::new(10.0, 10.0),
Point2D::new(20.0, 20.0),
];
let mut pending_cuts = HashSet::new();
pending_cuts.insert(1);
pending_cuts.insert(3);
let dragging = Dragging::EdgeCutting {
trail: trail.clone(),
pending_cuts: pending_cuts.clone(),
};
if let Dragging::EdgeCutting {
trail: stored,
pending_cuts: cuts,
} = dragging
{
assert_eq!(stored.len(), 3);
assert_eq!(stored[0].x, 0.0);
assert_eq!(stored[2].x, 20.0);
assert!(cuts.contains(&1));
assert!(cuts.contains(&3));
assert!(!cuts.contains(&2));
} else {
panic!("Expected Dragging::EdgeCutting");
}
}
#[test]
fn test_node_graph_state_default() {
let state = NodeGraphState::default();
assert_eq!(state.dragging, Dragging::None);
assert_eq!(state.time, 0.0);
assert!(state.last_update.is_none());
assert!(state.valid_drop_targets.is_empty());
assert!(state.node_z.is_empty());
assert_eq!(state.z_counter, 0);
}
#[test]
fn test_ensure_z_entries_assigns_new_indices() {
let mut state = NodeGraphState::default();
state.ensure_z_entries(3);
assert_eq!(state.node_z.get(&0), Some(&0));
assert_eq!(state.node_z.get(&1), Some(&1));
assert_eq!(state.node_z.get(&2), Some(&2));
assert_eq!(state.z_counter, 3);
state.ensure_z_entries(3);
assert_eq!(state.z_counter, 3);
state.ensure_z_entries(5);
assert_eq!(state.node_z.get(&3), Some(&3));
assert_eq!(state.node_z.get(&4), Some(&4));
}
#[test]
fn test_promote_z_puts_node_on_top() {
let mut state = NodeGraphState::default();
state.ensure_z_entries(3);
state.promote_z(0);
let z0 = state.node_z[&0];
let z1 = state.node_z[&1];
let z2 = state.node_z[&2];
assert!(z0 > z1);
assert!(z0 > z2);
}
#[test]
fn test_promote_z_many_preserves_relative_order() {
let mut state = NodeGraphState::default();
state.ensure_z_entries(4);
state.promote_z_many(&[0, 2]);
assert!(state.node_z[&0] > state.node_z[&1]);
assert!(state.node_z[&0] > state.node_z[&3]);
assert!(state.node_z[&2] > state.node_z[&0]);
}
#[test]
fn test_z_render_indices_unselected_then_selected() {
let mut state = NodeGraphState::default();
state.ensure_z_entries(4);
state.promote_z(1);
let order = z_render_indices(&state, 4, |i| i == 3, |_| false);
assert_eq!(order.last(), Some(&3));
let one_pos = order.iter().position(|&i| i == 1).unwrap();
assert_eq!(one_pos, 2);
}
#[test]
fn test_z_render_indices_selected_sorted_by_z() {
let mut state = NodeGraphState::default();
state.ensure_z_entries(3);
let order = z_render_indices(&state, 3, |i| i == 0 || i == 2, |_| false);
assert_eq!(order, vec![1, 0, 2]);
}
#[test]
fn test_z_render_indices_frames_first() {
let mut state = NodeGraphState::default();
state.ensure_z_entries(3);
state.promote_z(2);
let order = z_render_indices(&state, 3, |i| i == 2, |i| i == 2);
assert_eq!(order, vec![2, 0, 1]);
}
}