using CImGui.LibCImGui
import CImGui.LibCImGui: ImVec2, ImColor
import Random: randstring
#=
# Creating a node graph editor for ImGui
# Quick demo, not production code! This is more of a demo of how to use ImGui to create custom stuff.
# Better version by @daniel_collin here https://gist.github.com/emoon/b8ff4b4ce4f1b43e79f2
# See https://github.com/ocornut/imgui/issues/306
# v0.03: fixed grid offset issue, inverted sign of 'scrolling'
# Animated gif: https://cloud.githubusercontent.com/assets/8225057/9472357/c0263c04-4b4c-11e5-9fdf-2cd4f33f6582.gif
=#
# TODO: Glorious piracy!
Base.:+(lhs::ImVec2, rhs::ImVec2) = ImVec2(lhs.x + rhs.x, lhs.y + rhs.y)
Base.:-(lhs::ImVec2, rhs::ImVec2) = ImVec2(lhs.x - rhs.x, lhs.y - rhs.y)
ImColor(r, g, b) = ImColor(ImVec4(r, g, b, 1))
const SLOT_RADIUS = 8f0
const SLOT_NORMAL_COLOR = CImGui.IM_COL32(150, 150, 150, 150)
const SLOT_SELECTED_COLOR = CImGui.IM_COL32(200, 100, 100, 150)
const SLOT_CONNECTED_COLOR = CImGui.IM_COL32(100, 200, 100, 150)
# Dummy
mutable struct Node
ID::Int
widget::Widget
Pos::ImVec2
Size::ImVec2
Color::ImColor
end
(node::Node)(outer) = node.widget(outer)
name(node::Node) = name(node.widget)
processor(node::Node) = processor(node.widget)
# FIXME: Make Processor retain desired order instead
inputnames(node::Node) = inputnames(processor(node))
outputnames(node::Node) = outputnames(processor(node))
inputscount(node::Node) = length(inputnames(processor(node)))
outputscount(node::Node) = length(outputnames(processor(node)))
struct NodeLink
OutputIdx::Int
OutputSlot::Int
InputIdx::Int
InputSlot::Int
end
node_min_size(node::Node, cur_size::ImVec2) = ImVec2(max(2SLOT_RADIUS * inputscount(node), cur_size.x), max(2SLOT_RADIUS * outputscount(node), cur_size.y))
GetInputSlotPos(node::Node, slot_no::Int) = ImVec2(node.Pos.x, node.Pos.y + SLOT_RADIUS + (2SLOT_RADIUS * (slot_no - 1)))
GetOutputSlotPos(node::Node, slot_no::Int) = ImVec2(node.Pos.x + node.Size.x, node.Pos.y + SLOT_RADIUS + (2SLOT_RADIUS * (slot_no - 1)))
distance(pos1::ImVec2, pos2::ImVec2) = sqrt((pos1.x-pos2.x)^2 + (pos1.y-pos2.y)^2)
Base.@kwdef mutable struct NodeEditor
nodes::Vector{Node} = Node[]
links::Vector{NodeLink} = NodeLink[]
scrolling::ImVec2 = ImVec2(0f0, 0f0)
show_grid::Bool = true
node_selected::Int = -1
node_targeted::Int = 0
slot_targeted::Int = 0
end
findlink(ne::Widget{NodeEditor}, n1, s1, n2, s2) =
findfirst(l->l.OutputIdx==n1 &&
l.OutputSlot==s1 &&
l.InputIdx==n2 &&
l.InputSlot==s2, ne.links)
isconnected(ne::Widget{NodeEditor}, n1, s1, n2, s2) =
findlink(ne, n1, s1, n2, s2) !== nothing
isinputconnected(ne::Widget{NodeEditor}, node, slot) =
findfirst(l->l.InputIdx==node && l.InputSlot==slot, ne.links) !== nothing
isoutputconnected(ne::Widget{NodeEditor}, node, slot) =
findfirst(l->l.OutputIdx==node && l.OutputSlot==slot, ne.links) !== nothing
function interact_slot(ne::Widget{NodeEditor}, kind::Symbol, node_idx, slot_idx)
if kind == :input
kind_str = "input"
other_str = "output"
inp_node_idx = node_idx
inp_slot_idx = slot_idx
out_node_idx = ne.node_targeted
out_slot_idx = ne.slot_targeted
can_connect = ne.slot_targeted > 0
modifier = -1
inp_node = ne.nodes[inp_node_idx]
node = inp_node
inp_proc = processor(inp_node)
inp_slot_name = inputnames(inp_proc)[inp_slot_idx]
if ne.node_targeted != 0 && can_connect
out_node = ne.nodes[out_node_idx]
out_proc = processor(out_node)
out_slot_name = outputnames(out_proc)[out_slot_idx]
end
elseif kind == :output
kind_str = "output"
other_str = "input"
inp_node_idx = ne.node_targeted
inp_slot_idx = -ne.slot_targeted
out_node_idx = node_idx
out_slot_idx = slot_idx
can_connect = ne.slot_targeted < 0
modifier = 1
out_node = ne.nodes[out_node_idx]
node = out_node
out_proc = processor(out_node)
out_slot_name = outputnames(out_proc)[out_slot_idx]
if ne.node_targeted != 0 && can_connect
inp_node = ne.nodes[inp_node_idx]
inp_proc = processor(inp_node)
inp_slot_name = inputnames(inp_proc)[inp_slot_idx]
end
end
is_targeted = node_idx == ne.node_targeted &&
slot_idx == modifier*ne.slot_targeted
if CImGui.IsMouseClicked(0)
if ne.node_targeted == 0
DEBUG && @info "Select $kind_str $(slot_idx) from $(name(node.widget))"
ne.node_targeted = node_idx
ne.slot_targeted = modifier*slot_idx
elseif is_targeted
DEBUG && @info "Deselect $kind_str $(slot_idx) from $(name(node.widget))"
ne.node_targeted = 0
ne.slot_targeted = 0
elseif can_connect
if isconnected(ne, out_node_idx, out_slot_idx, inp_node_idx, inp_slot_idx)
DEBUG && @info "Disconnect input $(inp_slot_idx) of $(name(ne.nodes[inp_node_idx])) from output $(out_slot_idx) of $(name(ne.nodes[out_node_idx]))"
link_idx = findlink(ne, out_node_idx, out_slot_idx, inp_node_idx, inp_slot_idx)
deleteat!(ne.links, link_idx)
detach!(inp_proc, inputnames(inp_proc)[inp_slot_idx])
else
DEBUG && @info "Connect input $(inp_slot_idx) of $(name(ne.nodes[inp_node_idx])) to output $(out_slot_idx) of $(name(ne.nodes[out_node_idx]))"
push!(ne.links, NodeLink(out_node_idx, out_slot_idx, inp_node_idx, inp_slot_idx))
attach!(inp_proc, inputnames(inp_proc)[inp_slot_idx], out_proc, out_slot_name)
end
ne.node_targeted = 0
ne.slot_targeted = 0
else
DEBUG && @info "Skip on $kind_str $slot_idx of $(name(node.widget))"
end
elseif CImGui.IsMouseClicked(1)
iscon = kind == :input ? isinputconnected : isoutputconnected
if iscon(ne, node_idx, slot_idx)
link_idxs = findlinks(ne, node_idx, slot_idx)
for link_idx in reverse(link_idxs)
link_inp_slot_idx = ne.links[link_idx].InputSlot
deleteat!(ne.links, link_idx)
detach!(inp_proc, inputnames(inp_proc)[link_inp_slot_idx])
end
end
end
end
# Really dumb data structure provided for the example.
# Note that we storing links are INDICES (not ID) to make example code shorter, obviously a bad idea for any general purpose code.
function (ne::Widget{NodeEditor})(outer)
nodes = ne.nodes
links = ne.links
# Draw a list of nodes on the left side
open_context_menu = false
node_hovered_in_list = -1
node_hovered_in_scene = -1
CImGui.BeginChild("node_list", ImVec2(100, 0))
CImGui.Text("Nodes")
CImGui.Separator()
for node_idx in 1:length(nodes)
node = nodes[node_idx]
CImGui.PushID(node.ID)
if (CImGui.Selectable(name(node), node.ID == ne.node_selected))
ne.node_selected = node.ID
end
if (CImGui.IsItemHovered())
node_hovered_in_list = node.ID
open_context_menu |= CImGui.IsMouseClicked(1)
end
CImGui.PopID()
end
CImGui.EndChild()
CImGui.SameLine()
CImGui.BeginGroup()
NODE_WINDOW_PADDING = ImVec2(8f0, 8f0)
# Create our child canvas
CImGui.Text("Hold middle mouse button to scroll ($(ne.scrolling.x), $(ne.scrolling.y))")
CImGui.SameLine(CImGui.GetWindowWidth() - 100)
show_grid = ne.show_grid
@c CImGui.Checkbox("Show grid", &show_grid)
ne.show_grid = show_grid
CImGui.PushStyleVar(LibCImGui.ImGuiStyleVar_FramePadding, ImVec2(1, 1))
CImGui.PushStyleVar(LibCImGui.ImGuiStyleVar_WindowPadding, ImVec2(0, 0))
CImGui.PushStyleColor(LibCImGui.ImGuiCol_ChildBg, CImGui.IM_COL32(60, 60, 70, 200))
CImGui.BeginChild("scrolling_region", ImVec2(0, 0), true, LibCImGui.ImGuiWindowFlags_NoScrollbar | LibCImGui.ImGuiWindowFlags_NoMove)
CImGui.PushItemWidth(120f0)
offset = CImGui.GetCursorScreenPos() + ne.scrolling
draw_list = CImGui.GetWindowDrawList()
# Display grid
if (ne.show_grid)
GRID_COLOR = CImGui.IM_COL32(200, 200, 200, 40)
GRID_SZ = 64f0
win_pos = CImGui.GetCursorScreenPos()
canvas_sz = CImGui.GetWindowSize()
for x = rem(ne.scrolling.x, GRID_SZ):GRID_SZ:canvas_sz.x
CImGui.AddLine(draw_list, ImVec2(x, 0f0) + win_pos, ImVec2(x, canvas_sz.y) + win_pos, GRID_COLOR)
end
for y = rem(ne.scrolling.y, GRID_SZ):GRID_SZ:canvas_sz.y
CImGui.AddLine(draw_list, ImVec2(0f0, y) + win_pos, ImVec2(canvas_sz.x, y) + win_pos, GRID_COLOR)
end
end
# Display links
CImGui.ChannelsSplit(draw_list, 2)
CImGui.ChannelsSetCurrent(draw_list, 0) # Background
for link_idx = 1:length(links)
link = links[link_idx]
node_out = nodes[link.OutputIdx]
node_inp = nodes[link.InputIdx]
pout = offset + GetOutputSlotPos(node_out, link.OutputSlot)
pinp = offset + GetInputSlotPos(node_inp, link.InputSlot)
CImGui.AddBezierCurve(draw_list, pout, pout + ImVec2(+50, 0), pinp + ImVec2(-50, 0), pinp, CImGui.IM_COL32(200, 200, 100, 255), 3f0)
end
# Display nodes
for node_idx = 1:length(nodes)
node = nodes[node_idx]
CImGui.PushID(node.ID)
node_rect_min = offset + node.Pos
# Display node contents first
CImGui.ChannelsSetCurrent(draw_list, 1) # Foreground
old_any_active = CImGui.IsAnyItemActive()
CImGui.SetCursorScreenPos(node_rect_min + NODE_WINDOW_PADDING)
CImGui.BeginGroup() # Lock horizontal position
CImGui.Text(name(node))
node(ne)
CImGui.EndGroup()
# Save the size of what we have emitted and whether any of the widgets are being used
node_widgets_active = (!old_any_active && CImGui.IsAnyItemActive())
node.Size = node_min_size(node, CImGui.GetItemRectSize() + NODE_WINDOW_PADDING + NODE_WINDOW_PADDING)
node_rect_max = node_rect_min + node.Size
# Display node box
CImGui.ChannelsSetCurrent(draw_list, 0) # Background
CImGui.SetCursorScreenPos(node_rect_min)
CImGui.InvisibleButton("node", node.Size)
if (CImGui.IsItemHovered())
node_hovered_in_scene = node.ID
open_context_menu |= CImGui.IsMouseClicked(1)
end
node_moving_active = CImGui.IsItemActive()
if (node_widgets_active || node_moving_active)
ne.node_selected = node.ID
end
if (node_moving_active && CImGui.IsMouseDragging(0))
node.Pos = node.Pos + CImGui.GetIO().MouseDelta
end
node_bg_color = (node_hovered_in_list == node.ID || node_hovered_in_scene == node.ID || (node_hovered_in_list == -1 && ne.node_selected == node.ID)) ? CImGui.IM_COL32(75, 75, 75, 255) : CImGui.IM_COL32(60, 60, 60, 255)
CImGui.AddRectFilled(draw_list, node_rect_min, node_rect_max, node_bg_color, 4f0)
CImGui.AddRect(draw_list, node_rect_min, node_rect_max, CImGui.IM_COL32(100, 100, 100, 255), 4f0)
for slot_idx = 1:inputscount(node)
slot_pos = offset + GetInputSlotPos(node, slot_idx)
if node_idx == ne.node_targeted && slot_idx == -ne.slot_targeted
color = SLOT_SELECTED_COLOR
elseif isinputconnected(ne, node_idx, slot_idx)
color = SLOT_CONNECTED_COLOR
else
color = SLOT_NORMAL_COLOR
end
CImGui.AddCircleFilled(draw_list, slot_pos, SLOT_RADIUS, color)
slot_name = inputnames(node)[slot_idx]
text_size = CImGui.CalcTextSize(slot_name)
text_size = ImVec2(text_size.x, text_size.y / 2)
CImGui.SetCursorScreenPos(slot_pos - ImVec2(SLOT_RADIUS, 0) - text_size)
CImGui.Text(slot_name)
mouse_pos = CImGui.GetMousePos()
if distance(mouse_pos, slot_pos) <= SLOT_RADIUS
interact_slot(ne, :input, node_idx, slot_idx)
end
end
for slot_idx = 1:outputscount(node)
slot_pos = offset + GetOutputSlotPos(node, slot_idx)
if node_idx == ne.node_targeted && slot_idx == ne.slot_targeted
color = SLOT_SELECTED_COLOR
elseif isoutputconnected(ne, node_idx, slot_idx)
color = SLOT_CONNECTED_COLOR
else
color = SLOT_NORMAL_COLOR
end
CImGui.AddCircleFilled(draw_list, slot_pos, SLOT_RADIUS, color)
slot_name = outputnames(node)[slot_idx]
text_size = CImGui.CalcTextSize(slot_name)
text_size = ImVec2(0, text_size.y / 2)
CImGui.SetCursorScreenPos(slot_pos + ImVec2(SLOT_RADIUS, 0) - text_size)
CImGui.Text(slot_name)
mouse_pos = CImGui.GetMousePos()
if distance(mouse_pos, slot_pos) <= SLOT_RADIUS
interact_slot(ne, :output, node_idx, slot_idx)
end
end
CImGui.PopID()
end
CImGui.ChannelsMerge(draw_list)
# Open context menu
if (!CImGui.IsAnyItemHovered() && #=FIXME: CImGui.IsMouseHoveringWindow() && =#CImGui.IsMouseClicked(1))
ne.node_selected = node_hovered_in_list = node_hovered_in_scene = -1
open_context_menu = true
end
if (open_context_menu)
CImGui.OpenPopup("context_menu")
if (node_hovered_in_list != -1)
ne.node_selected = node_hovered_in_list
end
if (node_hovered_in_scene != -1)
ne.node_selected = node_hovered_in_scene
end
end
# Draw context menu
CImGui.PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8))
if (CImGui.BeginPopup("context_menu"))
node = ne.node_selected != -1 ? nodes[ne.node_selected] : nothing
scene_pos = CImGui.GetMousePosOnOpeningCurrentPopup() - offset
if (node !== nothing)
CImGui.Text("Node '$(name(node))'")
CImGui.Separator()
if (CImGui.MenuItem("Rename..", C_NULL, false))
@info "TODO: Rename"
end
if (CImGui.MenuItem("Delete", C_NULL, false))
@info "TODO: Delete"
end
if (CImGui.MenuItem("Copy", C_NULL, false))
@info "TODO: Copy"
end
else
if (CImGui.MenuItem("Add"))
push!(nodes, Node(length(nodes)+1, "New node", scene_pos, 0f5, ImColor(100, 100, 200), 2, 2))
end
if (CImGui.MenuItem("Paste", C_NULL, false, false)) end
end
CImGui.EndPopup()
end
CImGui.PopStyleVar()
# Scrolling
if (CImGui.IsWindowHovered() && !CImGui.IsAnyItemActive() && CImGui.IsMouseDragging(2, 0f0))
ne.scrolling += CImGui.GetIO().MouseDelta
end
CImGui.PopItemWidth()
CImGui.EndChild()
CImGui.PopStyleColor()
CImGui.PopStyleVar(2)
CImGui.EndGroup()
end