Skip to content

Commit

Permalink
Merge pull request #12 from kang-sw/feature/10-popup-graph-menu-on-drop
Browse files Browse the repository at this point in the history
Implement #10
  • Loading branch information
zakarumych authored Mar 28, 2024
2 parents c32782c + 49a42c5 commit 1d1c4b7
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 40 deletions.
129 changes: 127 additions & 2 deletions examples/demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::collections::HashMap;
use eframe::{App, CreationContext};
use egui::{Color32, Ui};
use egui_snarl::{
ui::{PinInfo, SnarlStyle, SnarlViewer},
InPin, InPinId, NodeId, OutPin, Snarl,
ui::{AnyPins, PinInfo, SnarlStyle, SnarlViewer},
InPin, InPinId, NodeId, OutPin, OutPinId, Snarl,
};

const STRING_COLOR: Color32 = Color32::from_rgb(0x00, 0xb0, 0x00);
Expand Down Expand Up @@ -485,6 +485,127 @@ impl SnarlViewer<DemoNode> for DemoViewer {
}
}

fn graph_menu_for_dropped_wire(
&mut self,
pos: egui::Pos2,
ui: &mut Ui,
_scale: f32,
src_pins: AnyPins,
snarl: &mut Snarl<DemoNode>,
) {
// In this demo, we create a context-aware node graph menu, and connect a wire
// dropped on the fly based on user input to a new node created.
//
// In your implementation, you may want to define specifications for each node's
// pin inputs and outputs and compatibility to make this easier.

ui.label("Add node");

type PinCompat = usize;
const PIN_NUM: PinCompat = 1;
const PIN_STR: PinCompat = 2;
const PIN_IMG: PinCompat = 4;
const PIN_SINK: PinCompat = PIN_NUM | PIN_STR | PIN_IMG;

fn pin_out_compat(node: &DemoNode) -> PinCompat {
match node {
DemoNode::Sink => 0,
DemoNode::Number(_) => PIN_NUM,
DemoNode::String(_) => PIN_STR,
DemoNode::ShowImage(_) => PIN_IMG,
DemoNode::ExprNode(_) => PIN_NUM,
}
}

fn pin_in_compat(node: &DemoNode) -> PinCompat {
match node {
DemoNode::Sink => PIN_SINK,
DemoNode::Number(_) => 0,
DemoNode::String(_) => 0,
DemoNode::ShowImage(_) => PIN_STR,
DemoNode::ExprNode(_) => PIN_STR,
}
}

match src_pins {
AnyPins::Out(src_pins) => {
assert!(
src_pins.len() == 1,
"There's no concept of multi-input nodes in this demo"
);

let src_pin = src_pins[0];
let src_out_ty = pin_out_compat(snarl.get_node(src_pin.node).unwrap());
let dst_in_candidates = [
("Sink", (|| DemoNode::Sink) as fn() -> DemoNode, PIN_SINK),
("Show Image", || DemoNode::ShowImage("".to_owned()), PIN_STR),
("Expr", || DemoNode::ExprNode(ExprNode::new()), PIN_STR),
];

for (name, ctor, in_ty) in dst_in_candidates {
if src_out_ty & in_ty != 0 {
if ui.button(name).clicked() {
// Create new node.
let new_node = snarl.insert_node(pos, ctor());
let dst_pin = InPinId {
node: new_node,
input: 0,
};

// Connect the wire.
snarl.connect(src_pin, dst_pin);
ui.close_menu();
}
}
}
}
AnyPins::In(pins) => {
let all_src_types = pins.iter().fold(0, |acc, pin| {
acc | pin_in_compat(snarl.get_node(pin.node).unwrap())
});

let dst_out_candidates = [
(
"Number",
(|| DemoNode::Number(0.)) as fn() -> DemoNode,
PIN_NUM,
),
("String", || DemoNode::String("".to_owned()), PIN_STR),
("Expr", || DemoNode::ExprNode(ExprNode::new()), PIN_NUM),
("Show Image", || DemoNode::ShowImage("".to_owned()), PIN_IMG),
];

for (name, ctor, out_ty) in dst_out_candidates {
if all_src_types & out_ty != 0 {
if ui.button(name).clicked() {
// Create new node.
let new_node = ctor();
let dst_ty = pin_out_compat(&new_node);

let new_node = snarl.insert_node(pos, new_node);
let dst_pin = OutPinId {
node: new_node,
output: 0,
};

// Connect the wire.
for src_pin in pins {
let src_ty = pin_in_compat(snarl.get_node(src_pin.node).unwrap());
if src_ty & dst_ty != 0 {
// In this demo, input pin MUST be unique ...
// Therefore here we drop inputs of source input pin.
snarl.drop_inputs(*src_pin);
snarl.connect(dst_pin, *src_pin);
ui.close_menu();
}
}
}
}
}
}
};
}

fn node_menu(
&mut self,
node: NodeId,
Expand Down Expand Up @@ -881,6 +1002,10 @@ impl App for DemoApp {
}

egui::widgets::global_dark_light_mode_switch(ui);

if ui.button("Clear All").clicked() {
self.snarl = Default::default();
}
});
});

Expand Down
129 changes: 92 additions & 37 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use self::{

pub use self::{
background_pattern::{BackgroundPattern, CustomBackground, Grid, Viewport},
pin::{CustomPinShape, PinInfo, PinShape},
pin::{AnyPins, CustomPinShape, PinInfo, PinShape},
viewer::SnarlViewer,
wire::WireLayer,
};
Expand Down Expand Up @@ -420,20 +420,103 @@ impl<T> Snarl<T> {
if bg_r.dragged_by(PointerButton::Primary) {
snarl_state.pan(-bg_r.drag_delta());
}

// If right button is clicked while new wire is being dragged, cancel it.
// This is to provide way to 'not open' the link graph node menu, but just
// releasing the new wire to empty space.
//
// This uses `button_down` directly, instead of `clicked_by` to improve
// responsiveness of the cancel action.
if snarl_state.has_new_wires()
&& ui.input(|x| x.pointer.button_down(PointerButton::Secondary))
{
let _ = snarl_state.take_wires();
bg_r.clicked = [false; egui::NUM_POINTER_BUTTONS];
}

// Wire end position will be overrided when link graph menu is opened.
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() => {
// A new pin is dropped without connecting it anywhere. This
// will open a pop-up window for creating a new node.
snarl_state.revert_take_wires(new_wires);

// Force open context menu.
bg_r.clicked[PointerButton::Secondary as usize] = true;
}
_ => {}
}
}

// Open graph menu when right-clicking on empty space.
let mut is_menu_visible = false;
bg_r.context_menu(|ui| {
viewer.graph_menu(
snarl_state.screen_pos_to_graph(ui.cursor().min, viewport),
ui,
snarl_state.scale(),
self,
);
let graph_pos = snarl_state.screen_pos_to_graph(ui.cursor().min, viewport);

is_menu_visible = true;

if snarl_state.has_new_wires() {
if !snarl_state.is_link_menu_open() {
// Mark link menu is now visible.
snarl_state.open_link_menu();
}

// Let the wires be drawn on ui position
wire_end_pos = ui.cursor().min;

let pins = match snarl_state.new_wires().unwrap() {
NewWires::In(x) => AnyPins::In(x),
NewWires::Out(x) => AnyPins::Out(x),
};

// The context menu is opened as *link* graph menu.
viewer.graph_menu_for_dropped_wire(
graph_pos,
ui,
snarl_state.scale(),
pins,
self,
);
} else {
viewer.graph_menu(graph_pos, ui, snarl_state.scale(), self);
}
});

if !is_menu_visible && snarl_state.is_link_menu_open() {
// It seems that the context menu was closed. Remove new wires.
snarl_state.close_link_menu();
}

match snarl_state.new_wires() {
None => {}
Some(NewWires::In(pins)) => {
for pin in pins {
let from = input.hover_pos.unwrap_or(Pos2::ZERO);
let from = wire_end_pos;
let (to, color) = input_info[pin];

draw_wire(
Expand All @@ -451,7 +534,7 @@ impl<T> Snarl<T> {
Some(NewWires::Out(pins)) => {
for pin in pins {
let (from, color) = output_info[pin];
let to = input.hover_pos.unwrap_or(Pos2::ZERO);
let to = wire_end_pos;

draw_wire(
ui,
Expand All @@ -476,34 +559,6 @@ impl<T> Snarl<T> {
}
}

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,
);
}
}
_ => {}
}
}

ui.advance_cursor_after_rect(Rect::from_min_size(viewport.min, Vec2::ZERO));

snarl_state.store(ui.ctx());
Expand Down
10 changes: 10 additions & 0 deletions src/ui/pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ pub enum AnyPin {
In(InPinId),
}

/// In the current context, these are the I/O pins of the 'source' node that the newly
/// created node's I/O pins will connect to.
#[derive(Debug)]
pub enum AnyPins<'a> {
/// Output pins.
Out(&'a [OutPinId]),
/// Input pins
In(&'a [InPinId]),
}

tiny_fn::tiny_fn! {
/// Custom pin shape drawing function with signature
/// `Fn(painter: &Painter, rect: Rect, fill: Color32, stroke: Stroke)`
Expand Down
Loading

0 comments on commit 1d1c4b7

Please sign in to comment.