cyborg/ramen_egui/src/lib.rs

473 lines
16 KiB
Rust

use egui::{Color32, Frame, Stroke};
use glam::Vec2;
use ramen::command::CommandHistory;
use ramen::node::{Edge, SlotIndex};
use ramen::Graph;
use std::collections::HashMap;
use std::sync::Arc;
pub struct NodeEditor {
graph: Graph,
cmd_history: CommandHistory,
pointer_state: PointerState,
}
impl NodeEditor {
pub fn from_graph(graph: Graph) -> Self {
Self {
graph,
cmd_history: CommandHistory::new(),
pointer_state: PointerState::new(),
}
}
pub fn undo(&mut self) {
self.cmd_history.undo(&mut self.graph).unwrap();
}
pub fn redo(&mut self) {
self.cmd_history.redo(&mut self.graph).unwrap();
}
pub fn show(&mut self, ui: &mut egui::Ui) {
let style = ui.style();
let style = GraphStyle::from_ui_style(style);
let layout = GraphLayout::layout(ui, &style, &self.pointer_state, &self.graph);
let rect = ui.available_rect_before_wrap();
let sense = egui::Sense::click_and_drag();
let response = ui.allocate_rect(rect, sense);
let pointer_pos = response.interact_pointer_pos();
let pointer_target = pointer_pos.map(|pos| layout.pointer_target(pos));
let painter = ui.painter_at(rect);
layout.paint(rect, &painter);
if let Some(pointer_pos) = pointer_pos {
self.pointer_state.position = pointer_pos;
self.pointer_state.delta = response.drag_delta();
if let PointerAction::Idle(idle) = &mut self.pointer_state.action {
*idle = pointer_target.clone();
}
}
if response.drag_started() && response.dragged_by(egui::PointerButton::Primary) {
let pointer_target = pointer_target.expect("Drag started but no target");
self.pointer_state.start = self.pointer_state.position;
let next_state = match pointer_target {
PointerTarget::Grid => None, // TODO grid navigation
PointerTarget::Node(idx) => Some(PointerAction::MovingNode(idx)),
PointerTarget::Input(idx) => Some(PointerAction::SettingInput(idx)),
PointerTarget::Output(idx) => Some(PointerAction::SettingOutput(idx)),
};
if let Some(next_state) = next_state {
self.pointer_state.action = next_state;
}
} else if response.drag_released() {
use ramen::command::*;
let cmd: Option<DynCommand> = match self.pointer_state.action {
PointerAction::Idle(_) => None,
PointerAction::MovingNode(idx) => {
let delta = self.pointer_state.position - self.pointer_state.start;
Some(Box::new(MoveNode {
target: idx,
to: Vec2::new(delta.x, delta.y),
relative: true,
}))
}
PointerAction::SettingInput(output) => {
if let Some(PointerTarget::Output(input)) = pointer_target {
Some(Box::new(SetEdge {
edge: Edge { input, output },
}))
} else if self.graph.nodes[output.node].inputs[output.slot].is_some() {
Some(Box::new(DeleteEdge { input: output }))
} else {
None
}
}
PointerAction::SettingOutput(input) => {
if let Some(PointerTarget::Input(output)) = pointer_target {
Some(Box::new(SetEdge {
edge: Edge { input, output },
}))
} else {
None
}
}
};
if let Some(cmd) = cmd {
let cmd_debug = format!("{:#?}", cmd);
match self.cmd_history.push(&mut self.graph, cmd) {
Ok(_) => {}
Err(e) => eprintln!("{}:\n {:#?}", cmd_debug, e),
}
}
self.pointer_state.action = PointerAction::Idle(None); // TODO idle pointer targeting
}
}
}
pub struct PointerState {
pub start: egui::Pos2,
pub position: egui::Pos2,
pub delta: egui::Vec2,
pub action: PointerAction,
}
impl PointerState {
pub fn new() -> Self {
Self {
start: egui::Pos2::ZERO,
position: egui::Pos2::ZERO,
delta: egui::Vec2::ZERO,
action: PointerAction::Idle(None),
}
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum PointerAction {
Idle(Option<PointerTarget>),
MovingNode(usize),
SettingInput(SlotIndex),
SettingOutput(SlotIndex),
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum PointerTarget {
Grid,
Node(usize),
Input(SlotIndex),
Output(SlotIndex),
}
#[derive(Debug)]
pub struct EdgeLayout {
pub points: [Vec2; 4],
}
#[derive(Clone, Debug)]
pub struct GraphStyle {
pub grid_spacing: f32,
pub grid_stroke: Stroke,
pub node_frame: Frame,
pub edge_stroke: Stroke,
pub edge_control_offset: f32,
pub slot_radius: f32,
pub slot_spacing: f32,
pub slot_label_margin: f32,
pub slot_fill: Color32,
pub slot_stroke: Stroke,
}
impl GraphStyle {
pub fn from_ui_style(style: &egui::Style) -> Self {
let node_alpha = 192;
let node_bg = if style.visuals.dark_mode {
Color32::from_black_alpha(node_alpha)
} else {
Color32::from_white_alpha(node_alpha)
};
Self {
grid_spacing: 25.0,
grid_stroke: style.visuals.widgets.noninteractive.bg_stroke,
node_frame: Frame::window(style).fill(node_bg),
edge_stroke: style.visuals.widgets.active.fg_stroke,
edge_control_offset: 200.0,
slot_radius: 7.0,
slot_spacing: 40.0,
slot_label_margin: 5.0,
slot_fill: style.visuals.widgets.noninteractive.bg_fill,
slot_stroke: style.visuals.widgets.noninteractive.bg_stroke,
}
}
}
#[derive(Debug)]
pub struct TextLayout {
pub pos: egui::Pos2,
pub galley: Arc<egui::Galley>,
}
#[derive(Debug)]
pub struct GraphLayout {
pub style: GraphStyle,
pub node_rects: HashMap<usize, egui::Rect>,
pub input_positions: HashMap<SlotIndex, Vec2>,
pub output_positions: HashMap<SlotIndex, Vec2>,
pub edges: HashMap<Edge, EdgeLayout>,
pub active_edge: Option<(Vec2, Vec2)>,
pub texts: Vec<TextLayout>,
}
impl GraphLayout {
pub fn layout(
ui: &egui::Ui,
style: &GraphStyle,
pointer: &PointerState,
graph: &Graph,
) -> Self {
let mut layout = Self {
style: style.clone(),
node_rects: Default::default(),
input_positions: Default::default(),
output_positions: Default::default(),
edges: Default::default(),
active_edge: None,
texts: Default::default(),
};
let mut edges = Vec::new();
let kind_font = egui::FontSelection::Style(egui::TextStyle::Heading);
let kind_font = kind_font.resolve(ui.style());
let kind_spacing = ui.style().spacing.item_spacing.y;
let kind_color = ui.visuals().text_color();
let label_font = egui::FontSelection::Style(egui::TextStyle::Body);
let label_font = label_font.resolve(ui.style());
let label_color = ui.visuals().text_color();
for (node_index, node) in graph.nodes.iter() {
let node_kind = &graph.node_kinds.kinds[node.kind];
let mut node_pos = egui::pos2(node.pos.x, node.pos.y);
if pointer.action == PointerAction::MovingNode(node_index) {
node_pos += pointer.position - pointer.start;
}
let node_size = egui::vec2(150., 150.);
let node_rect = egui::Rect::from_min_size(node_pos, node_size);
layout.node_rects.insert(node_index, node_rect);
let text = node_kind.title.to_string();
let kind_font = kind_font.clone();
let wrap_width = f32::MAX;
let galley = ui.fonts().layout(text, kind_font, kind_color, wrap_width);
let kind_height = galley.rect.height() + kind_spacing * 2.0;
layout.texts.push(TextLayout {
pos: egui::pos2(
node_rect.center().x - galley.rect.center().x,
node_pos.y + kind_height - galley.rect.height(),
),
galley,
});
let slot_top = node_pos.y + kind_height + style.slot_spacing;
let slot_left = node_rect.left();
let slot_right = node_rect.right();
for (slot_index, input) in node.inputs.iter().enumerate() {
let slot_y = slot_index as f32 * style.slot_spacing + slot_top;
let position = glam::Vec2::new(slot_left, slot_y);
let slot_index = SlotIndex {
node: node_index,
slot: slot_index,
};
layout.input_positions.insert(slot_index, position);
let text = node_kind.inputs[slot_index.slot].label.to_owned();
let label_font = label_font.clone();
let wrap_width = f32::MAX;
let galley = ui.fonts().layout(text, label_font, label_color, wrap_width);
layout.texts.push(TextLayout {
pos: egui::pos2(
position.x - galley.rect.left()
+ style.slot_radius
+ style.slot_label_margin,
position.y - galley.rect.bottom() + style.slot_radius,
),
galley,
});
if pointer.action == PointerAction::SettingInput(slot_index) {
let pointer = Vec2::new(pointer.position.x, pointer.position.y);
layout.active_edge = Some((position, pointer));
} else if let Some(input) = input {
edges.push(Edge {
input: *input,
output: slot_index,
});
}
}
for (slot_index, output) in node_kind.outputs.iter().enumerate() {
let slot_y = slot_index as f32 * style.slot_spacing + slot_top;
let position = glam::Vec2::new(slot_right, slot_y);
let slot_index = SlotIndex {
node: node_index,
slot: slot_index,
};
layout.output_positions.insert(slot_index, position);
let text = output.label.to_owned();
let label_font = label_font.clone();
let wrap_width = f32::MAX;
let galley = ui.fonts().layout(text, label_font, label_color, wrap_width);
layout.texts.push(TextLayout {
pos: egui::pos2(
position.x
- galley.rect.right()
- style.slot_radius
- style.slot_label_margin,
position.y - galley.rect.bottom() + style.slot_radius,
),
galley,
});
if pointer.action == PointerAction::SettingOutput(slot_index) {
let pointer = Vec2::new(pointer.position.x, pointer.position.y);
layout.active_edge = Some((pointer, position));
}
}
}
for edge in edges.into_iter() {
let input = *layout.input_positions.get(&edge.output).unwrap();
let output = *layout.output_positions.get(&edge.input).unwrap();
let edge_layout = layout.layout_edge(input, output);
layout.edges.insert(edge, edge_layout);
}
layout
}
pub fn layout_edge(&self, input: Vec2, output: Vec2) -> EdgeLayout {
let control_offset = Vec2::new(self.style.edge_control_offset, 0.0);
let mut input_control = input - control_offset;
let mut output_control = output + control_offset;
if output_control.x > input_control.x && output.x < input.x {
let mid = (output_control.x + input_control.x) / 2.0;
output_control.x = mid;
input_control.x = mid;
}
EdgeLayout {
points: [input, input_control, output_control, output],
}
}
pub fn pointer_target(&self, pointer: egui::Pos2) -> PointerTarget {
let test_slots = |positions: &HashMap<SlotIndex, Vec2>| -> Option<SlotIndex> {
for (index, position) in positions.iter() {
let dx = pointer.x - position.x;
let dy = pointer.y - position.y;
let d2 = (dx * dx) + (dy * dy);
let slot_radius2 = self.style.slot_radius.powi(2);
if d2 < slot_radius2 {
return Some(*index);
}
}
None
};
if let Some(index) = test_slots(&self.input_positions) {
return PointerTarget::Input(index);
}
if let Some(index) = test_slots(&self.output_positions) {
return PointerTarget::Output(index);
}
for (node_index, node_rect) in self.node_rects.iter() {
if node_rect.contains(pointer) {
return PointerTarget::Node(*node_index);
}
}
PointerTarget::Grid
}
pub fn paint(&self, rect: egui::Rect, painter: &egui::Painter) {
let style = &self.style;
let grid_spacing = style.grid_spacing;
let grid_pos = rect.left_top().to_vec2() / egui::vec2(grid_spacing, grid_spacing);
let mut grid_pos = grid_pos.floor() * grid_spacing;
while grid_pos.x < rect.right() {
grid_pos.x += grid_spacing;
let start = egui::pos2(grid_pos.x, rect.top());
let end = egui::pos2(grid_pos.x, rect.bottom());
painter.line_segment([start, end], style.grid_stroke);
}
while grid_pos.y < rect.bottom() {
grid_pos.y += grid_spacing;
let start = egui::pos2(rect.left(), grid_pos.y);
let end = egui::pos2(rect.right(), grid_pos.y);
painter.line_segment([start, end], style.grid_stroke);
}
let paint_edge = |layout: &EdgeLayout| {
let points = layout.points.map(|p| p.to_array().into());
let shape = egui::epaint::CubicBezierShape {
points,
closed: false,
fill: egui::Color32::TRANSPARENT,
stroke: style.edge_stroke,
};
painter.add(shape);
};
for (_edge, edge_layout) in self.edges.iter() {
paint_edge(edge_layout);
}
if let Some(active_edge) = self.active_edge {
let layout = self.layout_edge(active_edge.0, active_edge.1);
paint_edge(&layout);
}
for (_node_index, node_rect) in self.node_rects.iter() {
let shape = style.node_frame.paint(*node_rect);
painter.add(shape);
}
let draw_slots = |positions: &HashMap<SlotIndex, Vec2>| {
for (_index, position) in positions.iter() {
let shape = egui::epaint::CircleShape {
center: egui::pos2(position.x, position.y),
radius: style.slot_radius,
fill: style.slot_fill,
stroke: style.slot_stroke,
};
painter.add(shape);
}
};
draw_slots(&self.input_positions);
draw_slots(&self.output_positions);
for text in self.texts.iter() {
painter.add(egui::epaint::TextShape {
pos: text.pos,
galley: text.galley.to_owned(),
underline: Stroke::default(),
override_text_color: None,
angle: 0.0,
});
}
}
}