use egui::epaint::{CircleShape, PathShape, RectShape};
use egui::{vec2, Color32, CornerRadius, Pos2, Rect, Response, Shape, Stroke, Vec2, WidgetText};
#[derive(Clone)]
pub struct Socket<S> {
pub id: S,
pub side: NodeSide,
pub text: WidgetText,
pub filled: bool,
pub shape: SocketShape,
pub color: Color32,
}
impl<S> Socket<S> {
#[inline]
pub fn new(id: S, side: NodeSide) -> Self {
Self {
id,
side,
text: WidgetText::default(),
filled: false,
shape: SocketShape::default(),
color: Color32::PLACEHOLDER,
}
}
#[must_use]
#[inline]
pub fn text(mut self, text: impl Into<WidgetText>) -> Self {
self.text = text.into();
self
}
#[must_use]
#[inline]
pub fn filled(mut self, filled: bool) -> Self {
self.filled = filled;
self
}
#[must_use]
#[inline]
pub fn shape(mut self, shape: SocketShape) -> Self {
self.shape = shape;
self
}
#[must_use]
#[inline]
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.color = color.into();
self
}
}
#[derive(Debug, Clone)]
pub struct RenderedSocket<S> {
pub id: S,
pub response: Response,
pub side: NodeSide,
pub color: Color32,
}
impl<S> RenderedSocket<S> {
#[inline]
#[must_use]
pub fn pos(&self) -> Pos2 {
self.response.rect.center()
}
}
#[expect(clippy::large_enum_variant, reason = "require test on performance")]
pub(crate) enum SocketInteraction<S> {
None,
Connect(S, S),
InProgress(ConnectionInProgress<S>),
}
pub struct ConnectionInProgress<S> {
pub source: RenderedSocket<S>,
pub target: Option<RenderedSocket<S>>,
pub pointer_pos: Pos2,
}
pub(crate) fn handle_socket_responses<S>(
dragged_socket_id: &mut Option<S>,
rendered_sockets: &[RenderedSocket<S>],
) -> SocketInteraction<S>
where
S: Clone + PartialEq,
{
let mut interaction = SocketInteraction::None;
if let Some(socket_id) = dragged_socket_id.as_ref() {
let dragged_socket = rendered_sockets.iter().find(|s| &s.id == socket_id);
if let Some(socket) = dragged_socket {
if socket.response.drag_stopped() {
let hovered = rendered_sockets.iter().find(|s| s.response.hovered());
if let Some(hovered_socket) = hovered {
interaction =
SocketInteraction::Connect(socket_id.clone(), hovered_socket.id.clone());
} else {
}
*dragged_socket_id = None;
} else {
let hovered = rendered_sockets
.iter()
.find(|s| s.response.contains_pointer());
if let Some(pointer_pos) = socket.response.interact_pointer_pos() {
interaction = SocketInteraction::InProgress(ConnectionInProgress {
source: socket.clone(),
target: hovered.cloned(),
pointer_pos,
});
}
}
} else {
*dragged_socket_id = None;
}
} else if let Some(socket) = rendered_sockets.iter().find(|s| s.response.drag_started()) {
*dragged_socket_id = Some(socket.id.clone());
}
interaction
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum SocketShape {
#[default]
Circle,
Square,
Triangle,
}
impl SocketShape {
#[inline]
pub fn to_shape(&self, center: Pos2, width: f32, color: Color32, filled: bool) -> Shape {
use std::f32::consts::{FRAC_1_SQRT_2, FRAC_PI_3};
let fill = if filled { color } else { Color32::default() };
let stroke = Stroke::new(1.0, color);
match self {
SocketShape::Circle => Shape::Circle(CircleShape {
center,
radius: width / 2.0,
fill,
stroke,
}),
SocketShape::Square => Shape::Rect(RectShape {
rect: Rect::from_center_size(center, Vec2::splat(width * FRAC_1_SQRT_2)),
fill,
stroke,
corner_radius: CornerRadius::default(),
round_to_pixels: None,
brush: None,
stroke_kind: egui::StrokeKind::Inside,
blur_width: 0.0,
}),
SocketShape::Triangle => Shape::Path(PathShape {
points: vec![
center + (width / 2.0) * vec2(f32::cos(0.0), f32::sin(0.0)),
center
+ (width / 2.0)
* vec2(f32::cos(2.0 * FRAC_PI_3), f32::sin(2.0 * FRAC_PI_3)),
center
+ (width / 2.0)
* vec2(f32::cos(4.0 * FRAC_PI_3), f32::sin(4.0 * FRAC_PI_3)),
],
closed: true,
fill,
stroke: stroke.into(),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum NodeSide {
Left,
Right,
}