use std::collections::HashMap;
use dioxus::prelude::*;
use crate::anim::{bump_epoch, tween};
use crate::layout::{compute_layout, LayoutNode, LayoutOptions};
use crate::types::{
side_point, Edge, HandleGeom, HandleKey, HandleKind, Id, NodeGeom, Point, Rect, Side, Viewport,
};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Interaction {
#[default]
None,
Pan,
DragNode,
Connect,
PanePressed,
Pressed,
}
#[derive(Clone, Debug, Default)]
pub struct DragState {
pub pointer_id: Option<i32>,
pub origin_client: Point,
pub last_client: Point,
pub moved: bool,
pub suppress_click: bool,
pub grabs: Vec<(Id, Point)>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct SnapTarget {
pub key: HandleKey,
pub point: Point,
pub side: Side,
}
#[derive(Clone, PartialEq, Debug)]
pub struct ConnectionState {
pub from: HandleKey,
pub cursor: Point,
pub snap: Option<SnapTarget>,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct FlowConfig {
pub min_zoom: f64,
pub max_zoom: f64,
pub pan_on_drag: bool,
pub zoom_on_scroll: bool,
pub pan_on_scroll: bool,
pub nodes_draggable: bool,
pub drag_threshold: f64,
pub connection_radius: f64,
pub fit_view_padding: f64,
}
impl Default for FlowConfig {
fn default() -> Self {
Self {
min_zoom: 0.25,
max_zoom: 4.0,
pan_on_drag: true,
zoom_on_scroll: true,
pan_on_scroll: true,
nodes_draggable: true,
drag_threshold: 0.0,
connection_radius: 28.0,
fit_view_padding: 0.12,
}
}
}
#[derive(Clone, Copy)]
pub struct FlowCore {
pub iid: usize,
pub viewport: Signal<Viewport>,
pub container: Signal<Rect>,
pub interaction: Signal<Interaction>,
pub connection: Signal<Option<ConnectionState>>,
pub handles: Signal<HashMap<HandleKey, HandleGeom>>,
pub edges: Signal<Vec<Edge>>,
pub geoms: Memo<Vec<NodeGeom>>,
pub config: Signal<FlowConfig>,
pub(crate) drag: Signal<DragState>,
pub(crate) epoch: Signal<u64>,
pub(crate) snap_key: Memo<Option<HandleKey>>,
pub(crate) connect_from: Memo<Option<HandleKey>>,
pub(crate) deselect_nodes: Callback<()>,
pub(crate) overlay_insets: Signal<HashMap<usize, (Side, f64)>>,
pub(crate) pending_sizes: Signal<Vec<(Id, crate::types::Size)>>,
pub(crate) size_flush_queued: Signal<bool>,
pub(crate) pending_handles: Signal<Vec<(HandleKey, Option<HandleGeom>)>>,
pub(crate) handle_flush_queued: Signal<bool>,
pub(crate) on_connect_start: Option<EventHandler<HandleKey>>,
pub(crate) valid_connection: Option<Callback<crate::types::Connection, bool>>,
}
impl PartialEq for FlowCore {
fn eq(&self, other: &Self) -> bool {
self.iid == other.iid
}
}
pub fn use_flow() -> FlowCore {
use_context::<FlowCore>()
}
static NEXT_OVERLAY_KEY: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
pub fn use_overlay_inset(side: Side, thickness: f64) {
let core = use_context::<FlowCore>();
let key = use_hook(|| NEXT_OVERLAY_KEY.fetch_add(1, std::sync::atomic::Ordering::Relaxed));
let mut insets = core.overlay_insets;
if insets.peek().get(&key) != Some(&(side, thickness)) {
insets.write().insert(key, (side, thickness));
}
use_drop(move || {
core.overlay_insets.clone().write().remove(&key);
});
}
impl FlowCore {
pub(crate) fn queue_handle_write(&self, key: HandleKey, geom: Option<HandleGeom>) {
self.pending_handles.clone().write().push((key, geom));
let mut queued = self.handle_flush_queued;
if *queued.peek() {
return;
}
queued.set(true);
let core = *self;
dioxus::core::spawn_forever(async move {
crate::anim::sleep_ms(0).await;
let mut queued_signal = core.handle_flush_queued;
let Ok(mut queued) = queued_signal.try_write() else {
return;
};
*queued = false;
drop(queued);
let mut pending_signal = core.pending_handles;
let Ok(mut pending_ref) = pending_signal.try_write() else {
return;
};
let pending = std::mem::take(&mut *pending_ref);
drop(pending_ref);
if pending.is_empty() {
return;
}
let mut handles = core.handles;
let changed = match handles.try_peek() {
Ok(current) => pending.iter().any(|(key, geom)| match geom {
Some(geom) => current.get(key) != Some(geom),
None => current.contains_key(key),
}),
Err(_) => return,
};
if !changed {
return;
}
let Ok(mut current) = handles.try_write() else {
return;
};
for (key, geom) in pending {
match geom {
Some(geom) => {
current.insert(key, geom);
}
None => {
current.remove(&key);
}
}
}
});
}
pub fn claim_pointer(&self) -> bool {
let mut interaction = self.interaction;
if *interaction.peek() != Interaction::None {
return false;
}
interaction.set(Interaction::Pressed);
true
}
pub fn release_pointer(&self) {
let mut interaction = self.interaction;
if *interaction.peek() == Interaction::Pressed {
interaction.set(Interaction::None);
}
}
pub fn begin_pan(&self, pointer_id: i32, client: Point) -> bool {
let mut interaction = self.interaction;
if *interaction.peek() != Interaction::None {
return false;
}
self.cancel_animations();
{
let mut drag = self.drag;
let mut state = drag.write();
*state = DragState {
pointer_id: Some(pointer_id),
origin_client: client,
last_client: client,
moved: false,
suppress_click: true,
grabs: Vec::new(),
};
}
interaction.set(Interaction::Pan);
true
}
pub fn client_to_flow(&self, client: Point) -> Point {
let rect = *self.container.peek();
self.viewport.peek().screen_to_flow(client - rect.origin())
}
pub fn flow_to_client(&self, flow: Point) -> Point {
let rect = *self.container.peek();
self.viewport.peek().flow_to_screen(flow) + rect.origin()
}
pub fn cancel_animations(&self) {
bump_epoch(self.epoch);
}
pub fn nodes_bounds(&self) -> Option<Rect> {
let geoms = self.geoms.peek();
let mut iter = geoms.iter();
let first = iter.next()?.rect;
Some(iter.fold(first, |acc, geom| acc.union(&geom.rect)))
}
pub fn set_viewport(&self, target: Viewport, duration_ms: u64) {
let mut viewport = self.viewport;
if duration_ms == 0 {
self.cancel_animations();
viewport.set(target);
return;
}
let from = *viewport.peek();
tween(self.epoch, duration_ms, move |t| {
viewport.set(from.lerp(&target, t));
});
}
pub fn zoom_by(&self, factor: f64, anchor_client: Option<Point>, duration_ms: u64) {
let config = *self.config.peek();
let rect = *self.container.peek();
let vp = *self.viewport.peek();
let anchor = anchor_client
.map(|c| c - rect.origin())
.unwrap_or_else(|| Point::new(rect.width / 2.0, rect.height / 2.0));
let target = vp.zoom_about(vp.zoom * factor, anchor, config.min_zoom, config.max_zoom);
self.set_viewport(target, duration_ms);
}
pub fn zoom_in(&self, duration_ms: u64) {
self.zoom_by(1.25, None, duration_ms);
}
pub fn zoom_out(&self, duration_ms: u64) {
self.zoom_by(0.8, None, duration_ms);
}
pub fn fit_bounds(&self, bounds: Rect, padding: f64, duration_ms: u64) {
if let Some(target) = fit_viewport(self, bounds, padding) {
self.set_viewport(target, duration_ms);
}
}
pub fn fit_view(&self, duration_ms: u64) {
let padding = self.config.peek().fit_view_padding;
if let Some(bounds) = self.nodes_bounds() {
self.fit_bounds(bounds, padding, duration_ms);
}
}
pub fn center_on(&self, flow: Point, duration_ms: u64) {
let rect = *self.container.peek();
let zoom = self.viewport.peek().zoom;
let target = Viewport::new(
rect.width / 2.0 - flow.x * zoom,
rect.height / 2.0 - flow.y * zoom,
zoom,
);
self.set_viewport(target, duration_ms);
}
pub(crate) fn resolve_anchor(
&self,
handles: &HashMap<HandleKey, HandleGeom>,
geom: &NodeGeom,
kind: HandleKind,
handle_id: &Option<Id>,
) -> (Point, Side, bool) {
let key = HandleKey {
node: geom.id.clone(),
kind,
id: handle_id.clone().unwrap_or_default(),
};
anchor_from_geom(handles.get(&key), geom, kind)
}
pub(crate) fn anchor_of(&self, key: &HandleKey) -> Option<(Point, Side)> {
let handles = self.handles.peek();
let geoms = self.geoms.peek();
let geom = geoms.iter().find(|geom| geom.id == key.node)?;
let id = (!key.id.is_empty()).then(|| key.id.clone());
let (point, side, _) = self.resolve_anchor(&handles, geom, key.kind, &id);
Some((point, side))
}
pub(crate) fn find_snap(&self, from: &HandleKey, cursor: Point) -> Option<SnapTarget> {
let radius = self.config.peek().connection_radius / self.viewport.peek().zoom.max(1e-6);
let handles = self.handles.peek();
let geoms = self.geoms.peek();
let geom_by_id: HashMap<&str, &NodeGeom> =
geoms.iter().map(|geom| (geom.id.as_str(), geom)).collect();
let mut best: Option<(f64, SnapTarget)> = None;
for (key, hg) in handles.iter() {
if key.kind == from.kind || key.node == from.node {
continue;
}
let Some(geom) = geom_by_id.get(key.node.as_str()) else {
continue;
};
if let Some(valid) = &self.valid_connection {
if !valid.call(orient_connection(from, key)) {
continue;
}
}
let point = side_point(&geom.rect, hg.side, hg.offset);
let d2 = point.distance_sq(cursor);
if d2 <= radius * radius && best.as_ref().map(|(bd, _)| d2 < *bd).unwrap_or(true) {
best = Some((
d2,
SnapTarget {
key: key.clone(),
point,
side: hg.side,
},
));
}
}
best.map(|(_, target)| target)
}
}
pub(crate) fn anchor_from_geom(
handle: Option<&HandleGeom>,
geom: &NodeGeom,
kind: HandleKind,
) -> (Point, Side, bool) {
if let Some(hg) = handle {
return (side_point(&geom.rect, hg.side, hg.offset), hg.side, true);
}
let side = match kind {
HandleKind::Source => geom.source_side,
HandleKind::Target => geom.target_side,
};
(side_point(&geom.rect, side, 0.5), side, false)
}
pub(crate) fn orient_connection(from: &HandleKey, to: &HandleKey) -> crate::types::Connection {
let (source, target) = match from.kind {
HandleKind::Source => (from, to),
HandleKind::Target => (to, from),
};
crate::types::Connection {
source: source.node.clone(),
target: target.node.clone(),
source_handle: (!source.id.is_empty()).then(|| source.id.clone()),
target_handle: (!target.id.is_empty()).then(|| target.id.clone()),
}
}
pub struct FlowApi<T: 'static> {
pub core: FlowCore,
pub nodes: Signal<Vec<crate::types::Node<T>>>,
}
impl<T> Clone for FlowApi<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for FlowApi<T> {}
pub struct FlowHandle<T: 'static = ()> {
pub(crate) inner: Signal<Option<FlowApi<T>>>,
}
impl<T> Clone for FlowHandle<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for FlowHandle<T> {}
impl<T> PartialEq for FlowHandle<T> {
fn eq(&self, _other: &Self) -> bool {
true
}
}
pub fn use_flow_handle<T: 'static>() -> FlowHandle<T> {
FlowHandle {
inner: use_signal(|| None),
}
}
impl<T: Clone + PartialEq + 'static> FlowHandle<T> {
fn api(&self) -> Option<FlowApi<T>> {
*self.inner.peek()
}
pub fn core(&self) -> Option<FlowCore> {
self.api().map(|api| api.core)
}
pub fn viewport(&self) -> Option<Viewport> {
self.api().map(|api| *api.core.viewport.peek())
}
pub fn set_viewport(&self, viewport: Viewport, duration_ms: u64) {
if let Some(api) = self.api() {
api.core.set_viewport(viewport, duration_ms);
}
}
pub fn fit_view(&self, duration_ms: u64) {
if let Some(api) = self.api() {
api.core.fit_view(duration_ms);
}
}
pub fn zoom_in(&self, duration_ms: u64) {
if let Some(api) = self.api() {
api.core.zoom_in(duration_ms);
}
}
pub fn zoom_out(&self, duration_ms: u64) {
if let Some(api) = self.api() {
api.core.zoom_out(duration_ms);
}
}
pub fn client_to_flow(&self, client: Point) -> Option<Point> {
self.api().map(|api| api.core.client_to_flow(client))
}
pub fn delete_selected(&self) {
if let Some(api) = self.api() {
crate::flow::delete_selected(api.nodes, api.core.edges);
}
}
pub fn auto_layout(&self, opts: &LayoutOptions) {
let Some(api) = self.api() else { return };
let mut nodes = api.nodes;
let core = api.core;
let layout_nodes: Vec<LayoutNode> = nodes
.peek()
.iter()
.map(|node| LayoutNode {
id: node.id.clone(),
size: node.rect().size(),
})
.collect();
let edge_pairs: Vec<(Id, Id)> = core
.edges
.peek()
.iter()
.map(|edge| (edge.source.clone(), edge.target.clone()))
.collect();
let targets = compute_layout(&layout_nodes, &edge_pairs, opts);
if opts.update_handle_sides {
let (target_side, source_side) = opts.direction.handle_sides();
nodes.with_mut(|nodes| {
for node in nodes.iter_mut() {
node.target_side = target_side;
node.source_side = source_side;
}
});
}
let starts: HashMap<Id, Point> = nodes
.peek()
.iter()
.map(|node| (node.id.clone(), node.position))
.collect();
let mut bounds: Option<Rect> = None;
for layout_node in &layout_nodes {
if let Some(pos) = targets.get(&layout_node.id) {
let rect = Rect::from_points(*pos, layout_node.size);
bounds = Some(bounds.map(|b| b.union(&rect)).unwrap_or(rect));
}
}
tween(core.epoch, 420, move |t| {
nodes.with_mut(|nodes| {
for node in nodes.iter_mut() {
if let (Some(start), Some(end)) = (starts.get(&node.id), targets.get(&node.id))
{
node.position = start.lerp(*end, t);
}
}
});
});
if let Some(bounds) = bounds {
let padding = core.config.peek().fit_view_padding;
fit_bounds_without_cancel(core, bounds, padding);
}
}
}
fn fit_viewport(core: &FlowCore, bounds: Rect, padding: f64) -> Option<Viewport> {
let rect = *core.container.peek();
if rect.width <= 0.0 || rect.height <= 0.0 || (bounds.width <= 0.0 && bounds.height <= 0.0) {
return None;
}
let (mut left, mut right, mut top, mut bottom) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
for (side, thickness) in core.overlay_insets.peek().values() {
match side {
Side::Left => left = left.max(*thickness),
Side::Right => right = right.max(*thickness),
Side::Top => top = top.max(*thickness),
Side::Bottom => bottom = bottom.max(*thickness),
}
}
let cap_x = rect.width * 0.35;
let cap_y = rect.height * 0.35;
let (left, right) = (left.min(cap_x), right.min(cap_x));
let (top, bottom) = (top.min(cap_y), bottom.min(cap_y));
let free_w = rect.width - left - right;
let free_h = rect.height - top - bottom;
let config = *core.config.peek();
let zoom_x = free_w / bounds.width.max(1.0);
let zoom_y = free_h / bounds.height.max(1.0);
let zoom =
(zoom_x.min(zoom_y) * (1.0 - padding).max(0.05)).clamp(config.min_zoom, config.max_zoom);
let center = bounds.center();
Some(Viewport::new(
left + free_w / 2.0 - center.x * zoom,
top + free_h / 2.0 - center.y * zoom,
zoom,
))
}
fn fit_bounds_without_cancel(core: FlowCore, bounds: Rect, padding: f64) {
let Some(target) = fit_viewport(&core, bounds, padding) else {
return;
};
let mut viewport = core.viewport;
let from = *viewport.peek();
let epoch = core.epoch;
let my_epoch = *epoch.peek();
spawn(async move {
let start = web_time::Instant::now();
loop {
crate::anim::sleep_ms(16).await;
if *epoch.peek() != my_epoch {
return;
}
let t = (start.elapsed().as_secs_f64() * 1000.0 / 420.0).min(1.0);
viewport.set(from.lerp(&target, crate::anim::ease_in_out_cubic(t)));
if t >= 1.0 {
return;
}
}
});
}