use std::{collections::HashMap, hash::Hash};
use egui::{
collapsing_header::paint_default_icon, epaint::Shadow, pos2, vec2, Align, Color32, Frame, Id,
Layout, Margin, Modifiers, PointerButton, Pos2, Rect, Rounding, Sense, Shape, Stroke, Ui,
UiBuilder, Vec2,
};
use crate::{InPin, InPinId, Node, NodeId, OutPin, OutPinId, Snarl};
mod background_pattern;
mod pin;
mod state;
mod viewer;
mod wire;
mod zoom;
use self::{
pin::{draw_pin, AnyPin},
state::{NewWires, NodeState, SnarlState},
wire::{draw_wire, hit_wire, pick_wire_style},
zoom::Zoom,
};
pub use self::{
background_pattern::{BackgroundPattern, CustomBackground, Grid, Viewport},
pin::{AnyPins, BasicPinShape, CustomPinShape, PinInfo, PinShape},
viewer::SnarlViewer,
wire::{WireLayer, WireStyle},
};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))]
pub struct SelectionStyle {
pub margin: Margin,
pub rounding: Rounding,
pub fill: Color32,
pub stroke: Stroke,
}
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "egui-probe", derive(egui_probe::EguiProbe))]
pub struct SnarlStyle {
#[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..))]
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub pin_size: Option<f32>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub pin_fill: Option<Color32>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub pin_stroke: Option<Stroke>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub pin_shape: Option<BasicPinShape>,
#[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..))]
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub wire_width: Option<f32>,
#[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..))]
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub wire_frame_size: Option<f32>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub downscale_wire_frame: Option<bool>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub upscale_wire_frame: Option<bool>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub wire_style: Option<WireStyle>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub wire_layer: Option<WireLayer>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub header_drag_space: Option<Vec2>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub collapsible: Option<bool>,
#[cfg_attr(
feature = "serde",
serde(
skip_serializing_if = "Option::is_none",
default,
with = "serde_frame_option"
)
)]
pub bg_frame: Option<Frame>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub bg_pattern: Option<BackgroundPattern>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub bg_pattern_stroke: Option<Stroke>,
#[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..=1.0))]
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub min_scale: Option<f32>,
#[cfg_attr(feature = "egui-probe", egui_probe(range = 1.0..))]
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub max_scale: Option<f32>,
#[cfg_attr(feature = "egui-probe", egui_probe(range = 0.0..))]
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub scale_velocity: Option<f32>,
#[cfg_attr(
feature = "serde",
serde(
skip_serializing_if = "Option::is_none",
default,
with = "serde_frame_option"
)
)]
pub node_frame: Option<Frame>,
#[cfg_attr(
feature = "serde",
serde(
skip_serializing_if = "Option::is_none",
default,
with = "serde_frame_option"
)
)]
pub header_frame: Option<Frame>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub centering: Option<bool>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub select_stoke: Option<Stroke>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub select_fill: Option<Color32>,
pub select_rect_contained: Option<bool>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub select_style: Option<SelectionStyle>,
#[doc(hidden)]
#[cfg_attr(feature = "egui-probe", egui_probe(skip))]
#[cfg_attr(feature = "serde", serde(skip_serializing, default))]
pub _non_exhaustive: (),
}
impl SnarlStyle {
fn get_pin_size(&self, scale: f32, ui: &Ui) -> f32 {
self.pin_size
.zoomed(scale)
.unwrap_or_else(|| ui.spacing().interact_size.y * 0.5)
}
fn get_pin_fill(&self, ui: &Ui) -> Color32 {
self.pin_fill.unwrap_or(ui.visuals().widgets.active.bg_fill)
}
fn get_pin_stoke(&self, scale: f32, ui: &Ui) -> Stroke {
self.pin_stroke.zoomed(scale).unwrap_or(Stroke::new(
ui.visuals().widgets.active.bg_stroke.width,
ui.visuals().widgets.active.bg_stroke.color,
))
}
fn get_pin_shape(&self) -> PinShape {
self.pin_shape.unwrap_or(BasicPinShape::Circle).into()
}
fn get_wire_width(&self, scale: f32, ui: &Ui) -> f32 {
self.wire_width
.zoomed(scale)
.unwrap_or(self.get_pin_size(scale, ui) * 0.2)
}
fn get_wire_frame_size(&self, scale: f32, ui: &Ui) -> f32 {
self.wire_frame_size
.zoomed(scale)
.unwrap_or(self.get_pin_size(scale, ui) * 5.0)
}
fn get_downscale_wire_frame(&self) -> bool {
self.downscale_wire_frame.unwrap_or(true)
}
fn get_upscale_wire_frame(&self) -> bool {
self.upscale_wire_frame.unwrap_or(false)
}
fn get_wire_style(&self, scale: f32) -> WireStyle {
self.wire_style.zoomed(scale).unwrap_or(WireStyle::Bezier5)
}
fn get_wire_layer(&self) -> WireLayer {
self.wire_layer.unwrap_or(WireLayer::BehindNodes)
}
fn get_header_drag_space(&self, scale: f32, ui: &Ui) -> Vec2 {
self.header_drag_space
.zoomed(scale)
.unwrap_or(vec2(ui.spacing().icon_width, ui.spacing().icon_width))
}
fn get_collapsible(&self) -> bool {
self.collapsible.unwrap_or(true)
}
fn get_bg_frame(&self, ui: &Ui) -> Frame {
self.bg_frame.unwrap_or(Frame::canvas(ui.style()))
}
fn get_draw_bg_pattern(&self, style: &SnarlStyle, viewport: &Viewport, ui: &mut Ui) {
match &self.bg_pattern {
None => BackgroundPattern::new().draw(style, viewport, ui),
Some(pattern) => pattern.draw(style, viewport, ui),
}
}
fn get_bg_pattern_stroke(&self, scale: f32, ui: &Ui) -> Stroke {
self.bg_pattern_stroke
.zoomed(scale)
.unwrap_or(ui.visuals().widgets.noninteractive.bg_stroke)
}
fn get_min_scale(&self) -> f32 {
self.min_scale.unwrap_or(0.2)
}
fn get_max_scale(&self) -> f32 {
self.max_scale.unwrap_or(5.0)
}
fn get_scale_velocity(&self) -> f32 {
self.scale_velocity.unwrap_or(0.005)
}
fn get_node_frame(&self, scale: f32, ui: &Ui) -> Frame {
self.node_frame
.zoomed(scale)
.unwrap_or_else(|| Frame::window(ui.style()))
}
fn get_header_frame(&self, scale: f32, ui: &Ui) -> Frame {
self.header_frame
.zoomed(scale)
.unwrap_or_else(|| self.get_node_frame(scale, ui).shadow(Shadow::NONE))
}
fn get_centering(&self) -> bool {
self.centering.unwrap_or(true)
}
fn get_select_stroke(&self, scale: f32, ui: &Ui) -> Stroke {
self.select_stoke.zoomed(scale).unwrap_or(Stroke::new(
ui.visuals().selection.stroke.width,
ui.visuals().selection.stroke.color.gamma_multiply(0.5),
))
}
fn get_select_fill(&self, ui: &Ui) -> Color32 {
self.select_fill
.unwrap_or(ui.visuals().selection.bg_fill.gamma_multiply(0.3))
}
fn get_select_rect_contained(&self) -> bool {
self.select_rect_contained.unwrap_or(false)
}
fn get_select_style(&self, scale: f32, ui: &Ui) -> SelectionStyle {
self.select_style.zoomed(scale).unwrap_or(SelectionStyle {
margin: ui.spacing().window_margin,
rounding: ui.visuals().window_rounding,
fill: self.get_select_fill(ui),
stroke: self.get_select_stroke(scale, ui),
})
}
}
#[cfg(feature = "serde")]
mod serde_frame_option {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Serialize, Deserialize)]
pub struct Frame {
pub inner_margin: egui::Margin,
pub outer_margin: egui::Margin,
pub rounding: egui::Rounding,
pub shadow: egui::epaint::Shadow,
pub fill: egui::Color32,
pub stroke: egui::Stroke,
}
pub fn serialize<S>(frame: &Option<egui::Frame>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match frame {
Some(frame) => Frame {
inner_margin: frame.inner_margin,
outer_margin: frame.outer_margin,
rounding: frame.rounding,
shadow: frame.shadow,
fill: frame.fill,
stroke: frame.stroke,
}
.serialize(serializer),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<egui::Frame>, D::Error>
where
D: Deserializer<'de>,
{
let frame_opt = Option::<Frame>::deserialize(deserializer)?;
Ok(frame_opt.map(|frame| egui::Frame {
inner_margin: frame.inner_margin,
outer_margin: frame.outer_margin,
rounding: frame.rounding,
shadow: frame.shadow,
fill: frame.fill,
stroke: frame.stroke,
}))
}
}
impl SnarlStyle {
#[must_use]
pub const fn new() -> Self {
SnarlStyle {
pin_size: None,
pin_fill: None,
pin_stroke: None,
pin_shape: None,
wire_width: None,
wire_frame_size: None,
downscale_wire_frame: None,
upscale_wire_frame: None,
wire_style: None,
wire_layer: None,
header_drag_space: None,
collapsible: None,
bg_frame: None,
bg_pattern: None,
bg_pattern_stroke: None,
min_scale: None,
max_scale: None,
scale_velocity: None,
node_frame: None,
header_frame: None,
centering: None,
select_stoke: None,
select_fill: None,
select_rect_contained: None,
select_style: None,
_non_exhaustive: (),
}
}
}
impl Default for SnarlStyle {
#[inline]
fn default() -> Self {
Self::new()
}
}
struct Input {
hover_pos: Option<Pos2>,
interact_pos: Option<Pos2>,
scroll_delta: f32,
secondary_pressed: bool,
modifiers: Modifiers,
}
struct DrawNodeResponse {
node_moved: Option<(NodeId, Vec2)>,
node_to_top: Option<NodeId>,
drag_released: bool,
pin_hovered: Option<AnyPin>,
rect: Rect,
}
struct PinResponse {
pos: Pos2,
pin_fill: Color32,
wire_style: Option<WireStyle>,
}
impl<T> Snarl<T> {
fn draw_background(style: &SnarlStyle, snarl_state: &SnarlState, viewport: &Rect, ui: &mut Ui) {
let viewport = Viewport {
rect: *viewport,
scale: snarl_state.scale(),
offset: snarl_state.offset(),
};
style.get_draw_bg_pattern(style, &viewport, ui);
}
pub fn show<V>(&mut self, viewer: &mut V, style: &SnarlStyle, id_salt: impl Hash, ui: &mut Ui)
where
V: SnarlViewer<T>,
{
#![allow(clippy::too_many_lines)]
let snarl_id = ui.make_persistent_id(id_salt);
let bg_frame = style.get_bg_frame(ui);
let input = ui.ctx().input(|i| Input {
scroll_delta: i.raw_scroll_delta.y,
hover_pos: i.pointer.hover_pos(),
interact_pos: i.pointer.interact_pos(),
modifiers: i.modifiers,
secondary_pressed: i.pointer.secondary_pressed(),
});
bg_frame.show(ui, |ui| {
let mut node_moved = None;
let mut node_to_top = None;
let mut bg_r = ui.allocate_rect(ui.max_rect(), Sense::click_and_drag());
let viewport = bg_r.rect;
ui.set_clip_rect(viewport);
let pivot = input.hover_pos.unwrap_or_else(|| viewport.center());
let mut snarl_state =
SnarlState::load(ui.ctx(), snarl_id, pivot, viewport, self, style);
ui.style_mut().zoom(snarl_state.scale());
Self::draw_background(style, &snarl_state, &viewport, ui);
let wire_frame_size = style.get_wire_frame_size(snarl_state.scale(), ui);
let wire_width = style.get_wire_width(snarl_state.scale(), ui);
let node_frame = style.get_node_frame(snarl_state.scale(), ui);
let header_frame = style.get_header_frame(snarl_state.scale(), ui);
let wire_shape_idx = match style.get_wire_layer() {
WireLayer::BehindNodes => Some(ui.painter().add(Shape::Noop)),
WireLayer::AboveNodes => None,
};
match input.hover_pos {
Some(hover_pos)
if viewport.contains(hover_pos) && ui.rect_contains_pointer(viewport) =>
{
if input.scroll_delta != 0.0 {
let new_scale = (snarl_state.scale()
* (1.0 + input.scroll_delta * style.get_scale_velocity()))
.clamp(style.get_min_scale(), style.get_max_scale());
snarl_state.set_scale(new_scale);
}
}
_ => {}
}
let mut input_info = HashMap::new();
let mut output_info = HashMap::new();
let mut pin_hovered = None;
let draw_order = snarl_state.update_draw_order(self);
let mut drag_released = false;
let mut centers_sum = vec2(0.0, 0.0);
let mut centers_weight = 0;
let mut node_rects = Vec::new();
for node_idx in draw_order {
if !self.nodes.contains(node_idx.0) {
continue;
}
let response = self.draw_node(
ui,
node_idx,
viewer,
&mut snarl_state,
style,
snarl_id,
&node_frame,
&header_frame,
&mut input_info,
&input,
&mut output_info,
);
if let Some(response) = response {
if let Some(v) = response.node_to_top {
node_to_top = Some(v);
}
if let Some(v) = response.node_moved {
node_moved = Some(v);
}
if let Some(v) = response.pin_hovered {
pin_hovered = Some(v);
}
drag_released |= response.drag_released;
centers_sum += response.rect.center().to_vec2();
centers_weight += 1;
if snarl_state.is_rect_selection() {
node_rects.push((node_idx, response.rect));
}
}
}
let mut hovered_wire = None;
let mut hovered_wire_disconnect = false;
let mut wire_shapes = Vec::new();
let mut wire_hit = false;
for wire in self.wires.iter() {
let from_r = &output_info[&wire.out_pin];
let to_r = &input_info[&wire.in_pin];
if !wire_hit && !snarl_state.has_new_wires() && bg_r.hovered() && !bg_r.dragged() {
if let Some(interact_pos) = input.interact_pos {
wire_hit = hit_wire(
interact_pos,
wire_frame_size,
style.get_upscale_wire_frame(),
style.get_downscale_wire_frame(),
from_r.pos,
to_r.pos,
wire_width.max(1.5),
pick_wire_style(
style.get_wire_style(snarl_state.scale()),
from_r.wire_style,
to_r.wire_style,
)
.zoomed(snarl_state.scale()),
);
if wire_hit {
hovered_wire = Some(wire);
hovered_wire_disconnect |= bg_r.clicked_by(PointerButton::Secondary);
bg_r.hovered = false;
bg_r.clicked = false;
}
}
}
let color = mix_colors(from_r.pin_fill, to_r.pin_fill);
let mut draw_width = wire_width;
if hovered_wire == Some(wire) {
draw_width *= 1.5;
}
draw_wire(
ui,
&mut wire_shapes,
wire_frame_size,
style.get_upscale_wire_frame(),
style.get_downscale_wire_frame(),
from_r.pos,
to_r.pos,
Stroke::new(draw_width, color),
pick_wire_style(
style.get_wire_style(snarl_state.scale()),
from_r.wire_style.zoomed(snarl_state.scale()),
to_r.wire_style.zoomed(snarl_state.scale()),
),
);
}
if hovered_wire_disconnect {
if let Some(wire) = hovered_wire {
let out_pin = OutPin::new(self, wire.out_pin);
let in_pin = InPin::new(self, wire.in_pin);
viewer.disconnect(&out_pin, &in_pin, self);
}
}
if bg_r.drag_started_by(PointerButton::Primary) && input.modifiers.shift {
let screen_pos = input.interact_pos.unwrap_or(viewport.center());
let graph_pos = snarl_state.screen_pos_to_graph(screen_pos, viewport);
snarl_state.start_rect_selection(graph_pos);
}
if bg_r.dragged_by(PointerButton::Primary) {
if snarl_state.is_rect_selection() && input.hover_pos.is_some() {
let screen_pos = input.hover_pos.unwrap();
let graph_pos = snarl_state.screen_pos_to_graph(screen_pos, viewport);
snarl_state.update_rect_selection(graph_pos);
} else {
snarl_state.pan(-bg_r.drag_delta());
}
}
if bg_r.drag_stopped_by(PointerButton::Primary) {
if let Some(select_rect) = snarl_state.rect_selection() {
let select_nodes = node_rects.into_iter().filter_map(|(id, rect)| {
let select = match style.get_select_rect_contained() {
true => select_rect.contains_rect(rect),
false => select_rect.intersects(rect),
};
if select {
Some(id)
} else {
None
}
});
if input.modifiers.command {
snarl_state.deselect_many_nodes(select_nodes);
} else {
snarl_state.select_many_nodes(!input.modifiers.shift, select_nodes);
}
snarl_state.stop_rect_selection();
}
}
if let Some(select_rect) = snarl_state.rect_selection() {
ui.painter().rect(
snarl_state.graph_rect_to_screen(select_rect, viewport),
0.0,
style.get_select_fill(ui),
style.get_select_stroke(snarl_state.scale(), ui),
);
}
if snarl_state.has_new_wires()
&& ui.input(|x| x.pointer.button_down(PointerButton::Secondary))
{
let _ = snarl_state.take_wires();
bg_r.clicked = false;
}
if style.get_centering() && bg_r.double_clicked() {
centers_sum /= centers_weight as f32;
snarl_state.set_offset(centers_sum * snarl_state.scale());
}
if input.modifiers.command && bg_r.clicked_by(PointerButton::Primary) {
snarl_state.deselect_all_nodes();
}
let mut wire_end_pos = input.hover_pos.unwrap_or_default();
if drag_released {
let new_wires = snarl_state.take_wires();
if new_wires.is_some() {
ui.ctx().request_repaint();
}
match (new_wires, pin_hovered) {
(Some(NewWires::In(in_pins)), Some(AnyPin::Out(out_pin))) => {
for in_pin in in_pins {
viewer.connect(
&OutPin::new(self, out_pin),
&InPin::new(self, in_pin),
self,
);
}
}
(Some(NewWires::Out(out_pins)), Some(AnyPin::In(in_pin))) => {
for out_pin in out_pins {
viewer.connect(
&OutPin::new(self, out_pin),
&InPin::new(self, in_pin),
self,
);
}
}
(Some(new_wires), None) if bg_r.hovered() => {
snarl_state.revert_take_wires(new_wires);
bg_r.long_touched = true;
}
_ => {}
}
}
let mut is_menu_visible = false;
if let Some(interact_pos) = ui.ctx().input(|i| i.pointer.interact_pos()) {
if snarl_state.has_new_wires() {
let pins = match snarl_state.new_wires().unwrap() {
NewWires::In(x) => AnyPins::In(x),
NewWires::Out(x) => AnyPins::Out(x),
};
if viewer.has_dropped_wire_menu(pins, self) {
bg_r.context_menu(|ui| {
is_menu_visible = true;
if !snarl_state.is_link_menu_open() {
snarl_state.open_link_menu();
}
let pins = match snarl_state.new_wires().unwrap() {
NewWires::In(x) => AnyPins::In(x),
NewWires::Out(x) => AnyPins::Out(x),
};
wire_end_pos = ui.cursor().min;
viewer.show_dropped_wire_menu(
snarl_state.screen_pos_to_graph(ui.cursor().min, viewport),
ui,
snarl_state.scale(),
pins,
self,
);
});
}
} else if snarl_state.is_link_menu_open()
|| viewer.has_graph_menu(interact_pos, self)
{
bg_r.context_menu(|ui| {
is_menu_visible = true;
if !snarl_state.is_link_menu_open() {
snarl_state.open_link_menu();
}
viewer.show_graph_menu(
snarl_state.screen_pos_to_graph(ui.cursor().min, viewport),
ui,
snarl_state.scale(),
self,
);
});
}
}
if !is_menu_visible && snarl_state.is_link_menu_open() {
snarl_state.close_link_menu();
}
match snarl_state.new_wires() {
None => {}
Some(NewWires::In(pins)) => {
for pin in pins {
let from_pos = wire_end_pos;
let to_r = &input_info[pin];
draw_wire(
ui,
&mut wire_shapes,
wire_frame_size,
style.get_upscale_wire_frame(),
style.get_downscale_wire_frame(),
from_pos,
to_r.pos,
Stroke::new(wire_width, to_r.pin_fill),
to_r.wire_style
.zoomed(snarl_state.scale())
.unwrap_or(style.get_wire_style(snarl_state.scale())),
);
}
}
Some(NewWires::Out(pins)) => {
for pin in pins {
let from_r = &output_info[pin];
let to_pos = wire_end_pos;
draw_wire(
ui,
&mut wire_shapes,
wire_frame_size,
style.get_upscale_wire_frame(),
style.get_downscale_wire_frame(),
from_r.pos,
to_pos,
Stroke::new(wire_width, from_r.pin_fill),
from_r
.wire_style
.zoomed(snarl_state.scale())
.unwrap_or(style.get_wire_style(snarl_state.scale())),
);
}
}
}
match wire_shape_idx {
None => {
ui.painter().add(Shape::Vec(wire_shapes));
}
Some(idx) => {
ui.painter().set(idx, Shape::Vec(wire_shapes));
}
}
ui.advance_cursor_after_rect(Rect::from_min_size(viewport.min, Vec2::ZERO));
if let Some(node) = node_to_top {
if self.nodes.contains(node.0) {
ui.ctx().request_repaint();
snarl_state.node_to_top(node);
}
}
if let Some((node, delta)) = node_moved {
if self.nodes.contains(node.0) {
ui.ctx().request_repaint();
if snarl_state.selected_nodes().contains(&node) {
for node in snarl_state.selected_nodes() {
let node = &mut self.nodes[node.0];
node.pos += delta;
}
} else {
let node = &mut self.nodes[node.0];
node.pos += delta;
}
}
}
snarl_state.store(self, ui.ctx());
});
}
#[inline]
#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_arguments)]
fn draw_node<V>(
&mut self,
ui: &mut Ui,
node: NodeId,
viewer: &mut V,
snarl_state: &mut SnarlState,
style: &SnarlStyle,
snarl_id: Id,
node_frame: &Frame,
header_frame: &Frame,
input_positions: &mut HashMap<InPinId, PinResponse>,
input: &Input,
output_positions: &mut HashMap<OutPinId, PinResponse>,
) -> Option<DrawNodeResponse>
where
V: SnarlViewer<T>,
{
let Node {
pos,
open,
ref value,
} = self.nodes[node.0];
let viewport = ui.max_rect();
let inputs_count = viewer.inputs(value);
let outputs_count = viewer.outputs(value);
let node_pos = snarl_state.graph_pos_to_screen(pos, viewport);
let node_id = snarl_id.with(("snarl-node", node));
let openness = ui.ctx().animate_bool(node_id, open);
let mut node_state = NodeState::load(ui.ctx(), node_id, ui.spacing(), snarl_state.scale());
let node_rect = node_state.node_rect(node_pos, openness);
let mut node_to_top = None;
let mut node_moved = None;
let mut drag_released = false;
let mut pin_hovered = None;
let node_frame_rect = node_rect + node_frame.total_margin();
if snarl_state.selected_nodes().contains(&node) {
let select_style = style.get_select_style(snarl_state.scale(), ui);
let select_rect = node_frame_rect + select_style.margin;
ui.painter().rect(
select_rect,
select_style.rounding,
select_style.fill,
select_style.stroke,
);
}
let pin_size = style.get_pin_size(snarl_state.scale(), ui).max(0.0);
let header_drag_space = style
.get_header_drag_space(snarl_state.scale(), ui)
.max(Vec2::ZERO);
let inputs = (0..inputs_count)
.map(|idx| InPin::new(self, InPinId { node, input: idx }))
.collect::<Vec<_>>();
let outputs = (0..outputs_count)
.map(|idx| OutPin::new(self, OutPinId { node, output: idx }))
.collect::<Vec<_>>();
let r = ui.interact(
node_frame_rect,
node_id.with("frame"),
Sense::click_and_drag(),
);
if !input.modifiers.shift
&& !input.modifiers.command
&& r.dragged_by(PointerButton::Primary)
{
node_moved = Some((node, snarl_state.screen_vec_to_graph(r.drag_delta())));
}
if r.clicked_by(PointerButton::Primary) || r.dragged_by(PointerButton::Primary) {
if input.modifiers.shift {
snarl_state.select_one_node(input.modifiers.command, node);
} else if input.modifiers.command {
snarl_state.deselect_one_node(node);
}
}
if r.clicked() || r.dragged() {
node_to_top = Some(node);
}
if viewer.has_node_menu(&self.nodes[node.0].value) {
r.context_menu(|ui| {
viewer.show_node_menu(node, &inputs, &outputs, ui, snarl_state.scale(), self);
});
}
if !self.nodes.contains(node.0) {
node_state.clear(ui.ctx());
return None;
}
if viewer.has_on_hover_popup(&self.nodes[node.0].value) {
r.on_hover_ui_at_pointer(|ui| {
viewer.show_on_hover_popup(node, &inputs, &outputs, ui, snarl_state.scale(), self);
});
}
if !self.nodes.contains(node.0) {
node_state.clear(ui.ctx());
return None;
}
let node_ui = &mut ui.new_child(
UiBuilder::new()
.max_rect(node_frame_rect)
.layout(Layout::top_down(Align::Center))
.id_salt(node_id),
);
let mut new_pins_size = Vec2::ZERO;
let r = node_frame.show(node_ui, |ui| {
let min_pin_y = node_rect.min.y + node_state.header_height() * 0.5;
let input_x = node_frame_rect.left() + node_frame.inner_margin.left + pin_size;
let output_x = node_frame_rect.right() - node_frame.inner_margin.right - pin_size;
if (openness < 1.0 && open) || (openness > 0.0 && !open) {
ui.ctx().request_repaint();
}
let payload_rect = Rect::from_min_max(
pos2(
node_rect.min.x,
node_rect.min.y
+ node_state.header_height()
+ header_frame.total_margin().bottom
+ ui.spacing().item_spacing.y
- node_state.payload_offset(openness),
),
node_rect.max,
);
let payload_clip_rect =
Rect::from_min_max(node_rect.min, pos2(node_rect.max.x, f32::INFINITY));
let inputs_ui = &mut ui.new_child(
UiBuilder::new()
.max_rect(payload_rect)
.layout(Layout::top_down(Align::Min))
.id_salt("inputs"),
);
inputs_ui.set_clip_rect(payload_clip_rect.intersect(viewport));
for in_pin in &inputs {
inputs_ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
let (pin_id, _) = ui.allocate_space(vec2(pin_size * 1.5, pin_size * 1.5));
let y0 = ui.cursor().min.y;
let pin_info = viewer.show_input(in_pin, ui, snarl_state.scale(), self);
if !self.nodes.contains(node.0) {
return;
}
let y1 = ui.min_rect().max.y;
let y = min_pin_y.max((y0 + y1) * 0.5);
let pin_pos = pos2(input_x, y);
let r = ui.interact(
Rect::from_center_size(pin_pos, vec2(pin_size, pin_size)),
pin_id,
Sense::click_and_drag(),
);
if r.clicked_by(PointerButton::Secondary) {
if snarl_state.has_new_wires() {
snarl_state.remove_new_wire_in(in_pin.id);
} else {
viewer.drop_inputs(in_pin, self);
if !self.nodes.contains(node.0) {
return;
}
}
}
if r.drag_started_by(PointerButton::Primary) {
if input.modifiers.command {
snarl_state.start_new_wires_out(&in_pin.remotes);
if !input.modifiers.shift {
self.drop_inputs(in_pin.id);
if !self.nodes.contains(node.0) {
return;
}
}
} else {
snarl_state.start_new_wire_in(in_pin.id);
}
}
if r.drag_stopped() {
drag_released = true;
}
let mut pin_size = pin_size;
match input.hover_pos {
Some(hover_pos) if r.rect.contains(hover_pos) => {
if input.modifiers.shift {
snarl_state.add_new_wire_in(in_pin.id);
} else if input.secondary_pressed {
snarl_state.remove_new_wire_in(in_pin.id);
}
pin_hovered = Some(AnyPin::In(in_pin.id));
pin_size *= 1.2;
}
_ => {}
}
let pin_fill = pin_info.fill.unwrap_or(style.get_pin_fill(ui));
draw_pin(
ui.painter(),
pin_info.shape.as_ref().unwrap_or(&style.get_pin_shape()),
pin_fill,
pin_info
.stroke
.zoomed(snarl_state.scale())
.unwrap_or(style.get_pin_stoke(snarl_state.scale(), ui)),
pin_pos,
pin_size,
);
input_positions.insert(
in_pin.id,
PinResponse {
pos: pin_pos,
pin_fill,
wire_style: pin_info.wire_style,
},
);
});
}
let inputs_rect = inputs_ui.min_rect();
ui.expand_to_include_rect(inputs_rect.intersect(payload_clip_rect));
let inputs_size = inputs_rect.size();
if !self.nodes.contains(node.0) {
return;
}
let outputs_ui = &mut ui.new_child(
UiBuilder::new()
.max_rect(payload_rect)
.layout(Layout::top_down(Align::Max))
.id_salt("outputs"),
);
outputs_ui.set_clip_rect(payload_clip_rect.intersect(viewport));
for out_pin in &outputs {
outputs_ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
let (pin_id, _) = ui.allocate_space(vec2(pin_size * 1.5, pin_size * 1.5));
let y0 = ui.cursor().min.y;
let pin_info = viewer.show_output(out_pin, ui, snarl_state.scale(), self);
if !self.nodes.contains(node.0) {
return;
}
let y1 = ui.min_rect().max.y;
let y = min_pin_y.max((y0 + y1) * 0.5);
let pin_pos = pos2(output_x, y);
let r = ui.interact(
Rect::from_center_size(pin_pos, vec2(pin_size, pin_size)),
pin_id,
Sense::click_and_drag(),
);
if r.clicked_by(PointerButton::Secondary) {
if snarl_state.has_new_wires() {
snarl_state.remove_new_wire_out(out_pin.id);
} else {
viewer.drop_outputs(out_pin, self);
if !self.nodes.contains(node.0) {
return;
}
}
}
if r.drag_started_by(PointerButton::Primary) {
if input.modifiers.command {
snarl_state.start_new_wires_in(&out_pin.remotes);
if !input.modifiers.shift {
self.drop_outputs(out_pin.id);
if !self.nodes.contains(node.0) {
return;
}
}
} else {
snarl_state.start_new_wire_out(out_pin.id);
}
}
if r.drag_stopped() {
drag_released = true;
}
let mut pin_size = pin_size;
match input.hover_pos {
Some(hover_pos) if r.rect.contains(hover_pos) => {
if input.modifiers.shift {
snarl_state.add_new_wire_out(out_pin.id);
} else if input.secondary_pressed {
snarl_state.remove_new_wire_out(out_pin.id);
}
pin_hovered = Some(AnyPin::Out(out_pin.id));
pin_size *= 1.2;
}
_ => {}
}
let pin_fill = pin_info.fill.unwrap_or(style.get_pin_fill(ui));
draw_pin(
ui.painter(),
pin_info.shape.as_ref().unwrap_or(&style.get_pin_shape()),
pin_fill,
pin_info
.stroke
.zoomed(snarl_state.scale())
.unwrap_or(style.get_pin_stoke(snarl_state.scale(), ui)),
pin_pos,
pin_size,
);
output_positions.insert(
out_pin.id,
PinResponse {
pos: pin_pos,
pin_fill,
wire_style: pin_info.wire_style,
},
);
});
}
let outputs_rect = outputs_ui.min_rect();
ui.expand_to_include_rect(outputs_rect.intersect(payload_clip_rect));
let outputs_size = outputs_rect.size();
if !self.nodes.contains(node.0) {
return;
}
new_pins_size = vec2(
inputs_size.x + outputs_size.x + ui.spacing().item_spacing.x,
f32::max(inputs_size.y, outputs_size.y),
);
let mut pins_bottom = f32::max(inputs_rect.bottom(), outputs_rect.bottom());
if viewer.has_body(&self.nodes.get(node.0).unwrap().value) {
let body_left = inputs_rect.right() + ui.spacing().item_spacing.x;
let body_right = outputs_rect.left() - ui.spacing().item_spacing.x;
let body_top = payload_rect.top();
let body_bottom = payload_rect.bottom();
let mut body_rect =
Rect::from_min_max(pos2(body_left, body_top), pos2(body_right, body_bottom));
body_rect = node_state.align_body(body_rect);
let mut body_ui = ui.new_child(
UiBuilder::new()
.max_rect(body_rect)
.layout(Layout::left_to_right(Align::Min))
.id_salt("body"),
);
body_ui.set_clip_rect(payload_clip_rect.intersect(viewport));
viewer.show_body(
node,
&inputs,
&outputs,
&mut body_ui,
snarl_state.scale(),
self,
);
body_rect = body_ui.min_rect();
ui.expand_to_include_rect(body_rect.intersect(payload_clip_rect));
let body_size = body_rect.size();
node_state.set_body_width(body_size.x);
new_pins_size.x += body_size.x + ui.spacing().item_spacing.x;
new_pins_size.y = f32::max(new_pins_size.y, body_size.y);
pins_bottom = f32::max(pins_bottom, body_rect.bottom());
if !self.nodes.contains(node.0) {
return;
}
}
if viewer.has_footer(&self.nodes[node.0].value) {
let footer_left = node_rect.left();
let footer_right = node_rect.right();
let footer_top = pins_bottom + ui.spacing().item_spacing.y;
let footer_bottom = node_rect.bottom();
let mut footer_rect = Rect::from_min_max(
pos2(footer_left, footer_top),
pos2(footer_right, footer_bottom),
);
footer_rect = node_state.align_footer(footer_rect);
let mut footer_ui = ui.new_child(
UiBuilder::new()
.max_rect(footer_rect)
.layout(Layout::left_to_right(Align::Min))
.id_salt("footer"),
);
footer_ui.set_clip_rect(payload_clip_rect.intersect(viewport));
viewer.show_footer(
node,
&inputs,
&outputs,
&mut footer_ui,
snarl_state.scale(),
self,
);
footer_rect = footer_ui.min_rect();
ui.expand_to_include_rect(footer_rect.intersect(payload_clip_rect));
let footer_size = footer_rect.size();
node_state.set_footer_width(footer_size.x);
new_pins_size.x = f32::max(new_pins_size.x, footer_size.x);
new_pins_size.y += footer_size.y + ui.spacing().item_spacing.y;
if !self.nodes.contains(node.0) {
return;
}
}
let mut header_rect = Rect::NAN;
let mut header_frame_rect = Rect::NAN;
let header_ui: &mut Ui = &mut ui.new_child(
UiBuilder::new()
.max_rect(node_rect + header_frame.total_margin())
.layout(Layout::top_down(Align::Center))
.id_salt("header"),
);
header_frame.show(header_ui, |ui: &mut Ui| {
ui.with_layout(Layout::left_to_right(Align::Min), |ui| {
if style.get_collapsible() {
let (_, r) = ui.allocate_exact_size(
vec2(ui.spacing().icon_width, ui.spacing().icon_width),
Sense::click(),
);
paint_default_icon(ui, openness, &r);
if r.clicked_by(PointerButton::Primary) {
self.open_node(node, !open);
}
}
ui.allocate_exact_size(header_drag_space, Sense::hover());
viewer.show_header(node, &inputs, &outputs, ui, snarl_state.scale(), self);
header_rect = ui.min_rect();
});
header_frame_rect = header_rect + header_frame.total_margin();
ui.advance_cursor_after_rect(Rect::from_min_max(
header_rect.min,
pos2(
f32::max(header_rect.max.x, node_rect.max.x),
header_rect.min.y,
),
));
});
ui.expand_to_include_rect(header_rect);
let header_size = header_rect.size();
node_state.set_header_height(header_size.y);
node_state.set_size(vec2(
f32::max(header_size.x, new_pins_size.x),
header_size.y
+ header_frame.total_margin().bottom
+ ui.spacing().item_spacing.y
+ new_pins_size.y,
));
});
if !self.nodes.contains(node.0) {
ui.ctx().request_repaint();
node_state.clear(ui.ctx());
return None;
}
let final_rect = snarl_state.screen_rect_to_graph(r.response.rect, viewport);
viewer.final_node_rect(
node,
r.response.rect,
final_rect,
ui,
snarl_state.scale(),
self,
);
node_state.store(ui.ctx());
ui.ctx().request_repaint();
Some(DrawNodeResponse {
node_moved,
node_to_top,
drag_released,
pin_hovered,
rect: final_rect,
})
}
}
fn mix_colors(a: Color32, b: Color32) -> Color32 {
Color32::from_rgba_premultiplied(
((a.r() as u32 + b.r() as u32) / 2) as u8,
((a.g() as u32 + b.g() as u32) / 2) as u8,
((a.b() as u32 + b.b() as u32) / 2) as u8,
((a.a() as u32 + b.a() as u32) / 2) as u8,
)
}