nodui 0.2.1

An egui-based visual graph editor
Documentation
//! Node rendering.

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;

/* -------------------------------------------------------------------------- */

/// This is what you use to render a node.
///
/// See [`GraphUi::node`].
pub struct NodeUi<S> {
    /// The header of the node.
    header: Header,
    /// The background color of the node.
    ///
    /// Note: [`Color32::PLACEHOLDER`] will be replace by [`egui::Visuals::extreme_bg_color`].
    background_color: Color32,
    /// The layout of the sockets.
    layout: NodeLayout,
    /// The sockets.
    sockets: Vec<Socket<S>>,
    /// The outline.
    outline: Option<egui::Stroke>,
}

/// What [`GraphUi::node`] returns.
pub struct NodeResponse<'a, R, S> {
    /// The result of the callback.
    pub inner: R,
    /// The [`Response`] of the node.
    pub response: Response,
    /// The rendered socket of the node.
    pub sockets: &'a [RenderedSocket<S>],
}

/* -------------------------------------------------------------------------- */

impl<S> GraphUi<S> {
    /// Render a node.
    ///
    /// `id_salt` must be a unique id for the node.
    #[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();
            // node.set_pos(viewport.grid.canvas_to_graph_nearest(new_pos));
            *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> {
    /// Creates a new [`NodeUi<S>`].
    fn new() -> NodeUi<S> {
        NodeUi {
            header: Header::None,
            background_color: Color32::PLACEHOLDER,
            layout: NodeLayout::Double,
            sockets: Vec::new(),
            outline: None,
        }
    }

    /// Do the computations required to render the node.
    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> {
    /// Sets the header of the node.
    #[inline]
    pub fn header(&mut self, header: impl Into<Header>) {
        self.header = header.into();
    }

    /// The background color of the node.
    #[inline]
    pub fn background_color(&mut self, color: impl Into<Color32>) {
        self.background_color = color.into();
    }

    /// Sets the layout for the sockets.
    #[inline]
    pub fn layout(&mut self, layout: NodeLayout) {
        self.layout = layout;
    }

    /// Use two columns for the sockets.
    #[inline]
    pub fn double_column_layout(&mut self) {
        self.layout = NodeLayout::Double;
    }

    /// Use a single column for the socket.
    #[inline]
    pub fn single_column_layout(&mut self) {
        self.layout = NodeLayout::Single;
    }

    /// Add a socket to the node.
    #[inline]
    pub fn socket(&mut self, socket: Socket<S>) {
        self.sockets.push(socket);
    }

    /// Sets the outline of the node.
    #[inline]
    pub fn outline(&mut self, outline: impl Into<egui::Stroke>) {
        self.outline = Some(outline.into());
    }
}

/* -------------------------------------------------------------------------- */

/// Computed data to render the node.
pub(super) struct PreparedNode<S> {
    /// Computed  data to render the header.
    header: PreparedHeader,
    /// Computed data to render the body.
    body: PreparedBody<S>,
    /// The outline of the node.
    outline: egui::Stroke,
}

impl<S> PreparedNode<S> {
    /// The space occupied by the node.
    pub(super) fn size(&self) -> Vec2 {
        layout::stack_vertically([self.header.size(), self.body.size()])
    }

    /// Render the node to the [`egui::Ui`].
    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);

        // Add a stroke around the node to make it easier to see.
        ui.painter().add(RectShape::stroke(
            Rect::from_min_size(pos, size),
            corner_radius,
            outline,
            StrokeKind::Inside,
        ));
    }
}

/* -------------------------------------------------------------------------- */

/// Split the node rounding to the different parts of the node.
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)
    }
}

/* -------------------------------------------------------------------------- */