use egui::{
epaint::RectShape, vec2, Color32, CornerRadius, Pos2, Rect, Response, StrokeKind, UiBuilder,
Vec2,
};
use crate::{
misc::{collector::Collector, layout},
Header, NodeLayout, Pos, RenderedSocket, Socket,
};
use super::render::{self, body::PreparedBody, header::PreparedHeader};
use super::GraphUi;
pub struct NodeUi<S> {
header: Header,
background_color: Color32,
layout: NodeLayout,
sockets: Vec<Socket<S>>,
outline: Option<egui::Stroke>,
}
pub struct NodeResponse<'a, R, S> {
pub inner: R,
pub response: Response,
pub sockets: &'a [RenderedSocket<S>],
}
impl<S> GraphUi<S> {
#[inline]
pub fn node<'a, R>(
&mut self,
id_salt: impl core::hash::Hash,
pos: &mut Pos,
build_fn: impl FnOnce(&mut NodeUi<S>) -> R + 'a,
) -> NodeResponse<'_, R, S>
where
S: core::hash::Hash,
{
let mut node_ui = NodeUi::new();
let inner = build_fn(&mut node_ui);
let node = node_ui.prepare(&self.ui);
let id = self.graph_id.with(id_salt);
let canvas_pos = {
let delta_pos = match self.dragged_node {
Some((dragged_id, delta_pos)) if dragged_id == id => delta_pos,
_ => Vec2::ZERO,
};
self.viewport.grid.graph_to_canvas(*pos) + delta_pos
};
let ui_pos = self.viewport.canvas_to_viewport(canvas_pos);
let node_size = node.size();
let layer_id = egui::LayerId::new(egui::Order::Middle, id);
let (response, sockets) = self
.ui
.scope_builder(UiBuilder::new().layer_id(layer_id), |ui| {
let response = ui.interact(
Rect::from_min_size(ui_pos, node_size),
id,
egui::Sense::click_and_drag(),
);
let (sockets, ()) = self.rendered_sockets.watch(|rendered_sockets| {
node.show(ui, ui_pos, rendered_sockets);
});
(response, sockets)
})
.inner;
if response.drag_stopped() {
self.dragged_node = None;
let new_pos = canvas_pos + response.drag_delta();
*pos = self.viewport.grid.canvas_to_graph_nearest(new_pos);
} else if response.drag_started() {
self.dragged_node = Some((id, response.drag_delta()));
} else if response.dragged() {
if let Some(dragged_node) = self.dragged_node.as_mut() {
dragged_node.1 += response.drag_delta();
}
}
if response.flags.contains(egui::response::Flags::CLICKED)
|| response
.flags
.contains(egui::response::Flags::FAKE_PRIMARY_CLICKED)
|| response.dragged()
{
self.ui.ctx().move_to_top(layer_id);
response.request_focus();
}
NodeResponse {
inner,
response,
sockets,
}
}
}
impl<S> NodeUi<S> {
fn new() -> NodeUi<S> {
NodeUi {
header: Header::None,
background_color: Color32::PLACEHOLDER,
layout: NodeLayout::Double,
sockets: Vec::new(),
outline: None,
}
}
fn prepare(self, ui: &egui::Ui) -> PreparedNode<S> {
let Self {
header,
mut background_color,
layout,
sockets,
outline,
} = self;
if background_color == Color32::PLACEHOLDER {
background_color = ui.visuals().extreme_bg_color;
}
let outline = outline.unwrap_or(ui.visuals().window_stroke);
let header = render::header::prepare(ui, header, background_color);
let sockets = sockets
.into_iter()
.map(|s| render::socket::prepare(ui, s))
.collect();
let body = render::body::prepare(ui.spacing(), background_color, layout, sockets);
PreparedNode {
header,
body,
outline,
}
}
}
impl<S> NodeUi<S> {
#[inline]
pub fn header(&mut self, header: impl Into<Header>) {
self.header = header.into();
}
#[inline]
pub fn background_color(&mut self, color: impl Into<Color32>) {
self.background_color = color.into();
}
#[inline]
pub fn layout(&mut self, layout: NodeLayout) {
self.layout = layout;
}
#[inline]
pub fn double_column_layout(&mut self) {
self.layout = NodeLayout::Double;
}
#[inline]
pub fn single_column_layout(&mut self) {
self.layout = NodeLayout::Single;
}
#[inline]
pub fn socket(&mut self, socket: Socket<S>) {
self.sockets.push(socket);
}
#[inline]
pub fn outline(&mut self, outline: impl Into<egui::Stroke>) {
self.outline = Some(outline.into());
}
}
pub(super) struct PreparedNode<S> {
header: PreparedHeader,
body: PreparedBody<S>,
outline: egui::Stroke,
}
impl<S> PreparedNode<S> {
pub(super) fn size(&self) -> Vec2 {
layout::stack_vertically([self.header.size(), self.body.size()])
}
pub(super) fn show(
self,
ui: &mut egui::Ui,
pos: Pos2,
rendered_sockets: &mut Collector<RenderedSocket<S>>,
) where
S: core::hash::Hash,
{
let size = self.size();
let Self {
header,
body,
outline,
} = self;
let header_pos = pos;
let body_pos = pos + vec2(0.0, header.size().y);
let corner_radius = ui.visuals().window_corner_radius;
let (header_rounding, body_rounding) =
split_corner_radius(corner_radius, header.has_content());
header.show(ui, header_pos, size, header_rounding);
body.show(ui, body_pos, size, body_rounding, rendered_sockets);
ui.painter().add(RectShape::stroke(
Rect::from_min_size(pos, size),
corner_radius,
outline,
StrokeKind::Inside,
));
}
}
fn split_corner_radius(
node_corner_radius: CornerRadius,
has_header: bool,
) -> (CornerRadius, CornerRadius) {
let CornerRadius { nw, ne, sw, se } = node_corner_radius;
let top = CornerRadius {
nw,
ne,
..Default::default()
};
let bottom = CornerRadius {
sw,
se,
..Default::default()
};
if has_header {
(top, bottom)
} else {
(CornerRadius::ZERO, node_corner_radius)
}
}