~jpsamaroo/Adjutant.jl

678ebaa628b7201ad8ce2ba793d5b71d7441ef92 — Julian P Samaroo 2 years ago c93000a master
Add terminal menu for low-level configuration

Trigger terminal menu on keypress
Track module loading
Remove MLJ code
Add TiledLayout
Add copy-paste
Add node deletion
6 files changed, 173 insertions(+), 277 deletions(-)

M .gitignore
M Project.toml
M src/Adjutant.jl
D src/mlj.jl
M src/node_editor.jl
M src/widgets.jl
M .gitignore => .gitignore +1 -0
@@ 1,1 1,2 @@
Manifest.toml
imgui.ini

M Project.toml => Project.toml +1 -0
@@ 8,4 8,5 @@ CImGui = "5d785b6c-b76f-510e-a07c-3070796c7e87"
CSyntax = "ea656a56-6ca6-5dda-bba5-7b6963a5f74c"
Observables = "510215fc-4207-5dde-b226-833fc4488ee2"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
TerminalMenus = "dc548174-15c3-5faf-af27-7997cfbde655"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

M src/Adjutant.jl => src/Adjutant.jl +62 -0
@@ 10,6 10,7 @@ using CImGui.OpenGLBackend.ModernGL

using Observables, UUIDs
using Pkg
using TerminalMenus

const DEBUG = get(ENV, "ADJUTANT_DEBUG", "0") == "1"



@@ 17,6 18,8 @@ include("processing.jl")
include("widgets.jl")
include("node_editor.jl")

const TRACKED_MODULES = Dict()

Base.@kwdef mutable struct AdjutantState
    widgets = WidgetContainer()
end


@@ 53,6 56,64 @@ CImGui.StyleColorsDark()
ImGui_ImplGlfw_InitForOpenGL(window, true)
ImGui_ImplOpenGL3_Init(glsl_version)

# Interactive menu to control low-level functionality
mainmenuoptions = ["Load package", "Reload package"]
mainmenu = RadioMenu(mainmenuoptions)
function adj_menu()
    while true
        choice = request("Main menu:", mainmenu)
        choice == -1 && continue
        if mainmenuoptions[choice] == "Load package"
            while true
                print("Package name: ")
                pkg = readline()
                pkg == "" && continue
                if !(pkg in keys(Pkg.installed()))
                    print("Package path: ")
                    pkgpath = readline()
                    if !ispath(pkgpath)
                        println("Invalid package path: $pkgpath")
                        continue
                    end
                    Pkg.API.activate(pkgpath)
                end
                try
                    mod = Base.require(Main, Symbol(pkg))
                    TRACKED_MODULES[mod] = pkg
                    break
                catch err
                    showerror(stderr, err)
                    println(stderr)
                end
            end
        elseif mainmenuoptions[choice] == "Reload package"
            mods = collect(keys(TRACKED_MODULES))
            reloadmenuoptions = convert(Vector{String}, string.(mods))
            reloadmenu = RadioMenu(reloadmenuoptions)
            reloadchoice = request("Module to reload:", reloadmenu)
            reloadchoice == -1 && continue

            # Clear out old module
            oldmod = mods[reloadchoice]
            oldpkg = TRACKED_MODULES[oldmod]
            delete!(TRACKED_MODULES, oldmod)
            deregister_widgets!(oldmod)

            # Load new module
            mod = include(Base.pathof(mods[reloadchoice]))
            TRACKED_MODULES[mod] = oldpkg
            @show keys(TRACKED_MODULES)
            @show keys(KNOWN_WIDGETS)
            for mod in keys(TRACKED_MODULES)
                println("Module $mod:")
                @show KNOWN_WIDGETS[mod]
            end
        else
            println("Missed command \"$(mainmenuoptions[choice])\"!")
        end
    end
end

binds = Dict(
    #name == "c" && mods == 2 && exit(0)
    "Q" => () -> exit(0),


@@ 65,6 126,7 @@ binds = Dict(
        "c" => () -> clear_model!(adj),
        "f" => () -> fit!(adj),
    ),
    "L" => () -> adj_menu(),
)
current_dict = Ref(binds)
function dispatch_keybinding(key, scancode, actions, mods)

D src/mlj.jl => src/mlj.jl +0 -229
@@ 1,229 0,0 @@
### Tasks ###

const known_tasks = []

#= FIXME: Preload built-in MLJ tasks
for t in (
    MLJ.load_ames,
    MLJ.load_boston,
    MLJ.load_crabs,
    MLJ.load_iris,
    MLJ.load_reduced_ames,
)
    push!(known_tasks, t)
end
=#

function fake_task1()
    x = rand(64)
    y = 2x.^2 .+ 3x .- 4
    df = DataFrame(:x=>x, :y=>y)
    return supervised(data=df,
                  target=:y,
                  is_probabilistic=false,
                  types=Dict(:x=>Continuous,
                             :y=>Continuous)
    )
end
push!(known_tasks, fake_task1)

### Models ###

const known_models = Pair{String,String}[]

Base.@kwdef mutable struct MLJ_Model
    model = nothing
end
Base.@kwdef mutable struct MLJ_Task
    task = nothing
end

clear_task!(adj::Adjutant) = (adj.active_task = nothing)
function clear_model!(adj::Adjutant)
    adj.active_model = nothing
    adj.fitness = nothing
end
function clear_all!(adj::Adjutant)
    clear_task!(adj)
    clear_model!(adj)
end

function fit!(adj::Adjutant)
    if adj.active_model !== nothing
        print("Fitting model $(adj.active_model[1][2])... ")
        MLJ.fit!(adj.active_model[2]; verbosity=0)
        println("Done!")
    end
end
function fitness!(adj::Adjutant)
    if adj.active_model !== nothing && adj.active_task !== nothing
        mach = adj.active_model[2]
        task = adj.active_task[2]
        if task isa MLJ.SupervisedTask
            adj.fitness = sum((predict(mach) .- task.y) .^ 2)
        else
            println(stderr, "Non-supervised fitness not implemented")
        end
    end
end
function create_model!(adj::Adjutant, selected)
    if adj.active_task !== nothing
        pkgname, modelname = known_models[selected]
        create_model!(adj, modelname, pkgname)
    end
end
function create_model!(adj::Adjutant, modelname, pkgname)
    if adj.active_task !== nothing
        if haskey(Pkg.installed(), pkgname)
            if isdefined(Main, Symbol(pkgname))
                pkg = getfield(Main, Symbol(pkgname))
            else
                print("Loading package $pkgname... ")
                pkg = Base.require(Main, Symbol(pkgname))
                println("Done!")
            end
            print("Loading model $modelname from package $pkgname... ")
            current_module = pkg
            model = nothing
            for field in split(modelname, ".")
                model = getfield(current_module, Symbol(field))
            end
            @assert model !== nothing
            mach = machine(model(), adj.active_task[2])
            clear_model!(adj)
            adj.active_model = (pkgname,modelname)=>mach
            println("Done!")
        else
            println(stderr, "Failure: Package $pkgname not in project")
        end
    end
end
function query_models!(adj::Adjutant)
    if adj.active_task !== nothing
        print("Querying models for $(adj.active_task[1])... ")
        empty!(known_models)
        for (pkgname, ms) in models(adj.active_task[2])
            for m in ms
                push!(known_models, pkgname=>m)
            end
        end
        println("Done!")
    end
end

push!(adj.widgets, ClosureWidget() do outer
    CImGui.Text("Status: $(adj.status)")
    active_task = adj.active_task !== nothing ? adj.active_task[1] : "nothing"
    CImGui.Text("Active Task: $active_task")
    active_model = adj.active_model !== nothing ? adj.active_model[1][2] : "nothing"
    CImGui.Text("Active Model: $active_model")
end)
push!(adj.widgets, ClosureWidget() do outer
    CImGui.Text("Tasks")
    CImGui.Separator()
    # task list
    selected = @cstatic selected=1 begin
        if CImGui.Button("Load")
            task_func = known_tasks[selected]
            print("Loading $task_func... ")
            adj.active_task = repr(task_func)=>task_func()
            empty!(known_models)
            println("Done!")
        end
        CImGui.SameLine()
        if CImGui.Button("Clear")
            clear_task!(adj)
        end
        # left
        CImGui.BeginChild("task pane", (150, 200), true)
        for (idx,task) in enumerate(known_tasks)
            CImGui.Selectable(repr(task), selected == idx) && (selected = idx;)
        end
        CImGui.EndChild()
        CImGui.SameLine()
    end

    # task description
    CImGui.BeginGroup()
        # leave room for 1 line below us
        CImGui.BeginChild("item view", (150, 200))
            if adj.active_task !== nothing
                CImGui.Text("Active Task: $(adj.active_task[1]) ($(typeof(adj.active_task[2])))")
                task = adj.active_task[2]
                #= FIXME
                CImGui.Separator()
                CImGui.TextWrapped("Target: $(task.target) ($(task.target_scitype_union))")
                =#
                # FIXME: List of inputs
            else
                CImGui.Text("Active Task: nothing")
            end
        CImGui.EndChild()
    CImGui.EndGroup()
end)
push!(adj.widgets, ClosureWidget() do outer
    CImGui.Text("Models")
    CImGui.Separator()
    # model list
    selected = @cstatic selected=1 begin
        if CImGui.Button("Query")
            query_models!(adj)
        end
        CImGui.SameLine()
        if CImGui.Button("Create")
            create_model!(adj, selected)
        end
        CImGui.SameLine()
        if CImGui.Button("Clear")
            clear_model!(adj)
        end
        # left
        CImGui.BeginChild("model pane", (150, 0), true)
        for (idx,(pkgname,modelname)) in enumerate(known_models)
            if CImGui.Selectable(pkgname*"."*modelname, selected == idx)
                selected = idx
            end
        end
        CImGui.EndChild()
    end
    # manual model entry
    @cstatic name="\0"^128 pkg="\0"^128 begin
        CImGui.Text("Manual Model Selection")
        CImGui.InputText("Model Name", name, length(name))
        CImGui.InputText("Package Name", pkg, length(pkg))
        if CImGui.Button("Find and Create")
            create_model!(adj, name, pkg)
        end
    end
end)
push!(adj.widgets, ClosureWidget() do outer
    CImGui.Text("Training")
    CImGui.Separator()
    if adj.active_model !== nothing
        if CImGui.Button("Fit")
            fit!(adj)
        end
        # TODO: Some graphs!
    end
end)
push!(adj.widgets, ClosureWidget() do outer
    CImGui.Text("Testing")
    CImGui.Separator()
    if adj.active_model !== nothing
        if CImGui.Button("Calculate Fitness")
            fitness!(adj)
        end
        if adj.fitness !== nothing
            CImGui.Text("Fitness: $(adj.fitness)")
        end
        # TODO: Some graphs!
    end
end)

struct ResamplingWidget
    method
end
push!(known_widgets, ResamplingWidget)
function (rw::Widget{ResamplingWidget})(outer)
    # FIXME
end

M src/node_editor.jl => src/node_editor.jl +93 -43
@@ 1,4 1,4 @@
export NodeEditor, Node, NodeLink
export NodeEditor, Node, NodeLink, addnode!, delnode!

using CImGui.LibCImGui
import CImGui.LibCImGui: ImVec2, ImColor


@@ 24,12 24,12 @@ const SLOT_CONNECTED_COLOR = CImGui.IM_COL32(100, 200, 100, 150)

# Dummy
mutable struct Node
    ID::Int
    id::Int
    widget::Widget
    Pos::ImVec2
    Size::ImVec2
    Color::ImColor
    pos::ImVec2
    last_size::ImVec2
end
Node(id, widget, pos) = Node(id, widget, pos, ImVec2(1f0, 1f0))
(node::Node)(outer) = node.widget(outer)
name(node::Node) = name(node.widget)
processor(node::Node) = processor(node.widget)


@@ 47,8 47,8 @@ struct NodeLink
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)))
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.last_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


@@ 59,21 59,62 @@ Base.@kwdef mutable struct NodeEditor
    node_selected::Int = -1
    node_targeted::Int = 0
    slot_targeted::Int = 0
    copybuf = nothing
    del_list::Vector{Int} = Int[]
end

findlink(ne::Widget{NodeEditor}, n1, s1, n2, s2) =
findlink(ne::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) =
isconnected(ne::NodeEditor, n1, s1, n2, s2) =
    findlink(ne, n1, s1, n2, s2) !== nothing
isinputconnected(ne::Widget{NodeEditor}, node, slot) =
isinputconnected(ne::NodeEditor, node, slot) =
    findfirst(l->l.InputIdx==node && l.InputSlot==slot, ne.links) !== nothing
isoutputconnected(ne::Widget{NodeEditor}, node, slot) =
isoutputconnected(ne::NodeEditor, node, slot) =
    findfirst(l->l.OutputIdx==node && l.OutputSlot==slot, ne.links) !== nothing

mutable struct TiledLayout
    ne::NodeEditor
    width::Int
    height::Int
    increment::ImVec2
    pos::ImVec2
end
TiledLayout(ne, width, height, increment) =
    TiledLayout(ne, width, height, increment, ImVec2(increment.x, increment.y))

function addnode!(tl::TiledLayout, widget)
    pos = tl.pos
    addnode!(tl.ne, widget, pos)
    pos = ImVec2(pos.x, pos.y + tl.increment.y)
    if pos.y >= tl.height
        pos = ImVec2(tl.increment.x + pos.x, tl.increment.y)
    end
    if pos.x >= tl.width
        pos = ImVec2(tl.increment.x, tl.increment.y)
    end
    tl.pos = pos
end

# FIXME: Actually find a free position
findfreepos(ne::NodeEditor) = ImVec2(20 .* rand(1:10), 20 .* rand(1:10))

function addnode!(ne::NodeEditor, widget, pos=nothing)
    if pos === nothing
        pos = findfreepos(ne)
    end
    nextid = length(ne.nodes)>0 ? maximum(map(n->n.id, ne.nodes))+1 : 1
    push!(ne.nodes, Node(nextid, widget, pos))
end
delnode!(ne::NodeEditor, id::Int) = push!(ne.del_list, id)
delnode!(ne::NodeEditor, node::Node) = push!(ne.del_list, node.id)

getmouseclicks() = [(c=CImGui.IsMouseClicked(i), d=CImGui.IsMouseDoubleClicked(i)) for i in 0:1:2]

function interact_slot(ne::Widget{NodeEditor}, kind::Symbol, node_idx, slot_idx)
    _ne = core(ne)
    if kind == :input
        kind_str = "input"
        other_str = "output"


@@ 113,7 154,8 @@ function interact_slot(ne::Widget{NodeEditor}, kind::Symbol, node_idx, slot_idx)
    end
    is_targeted = node_idx == ne.node_targeted &&
                  slot_idx == modifier*ne.slot_targeted
    if CImGui.IsMouseClicked(0)
    mouseclicks = getmouseclicks()
    if mouseclicks[1].c
        if ne.node_targeted == 0
            DEBUG && @info "Select $kind_str $(slot_idx) from $(name(node.widget))"
            ne.node_targeted = node_idx


@@ 123,25 165,26 @@ function interact_slot(ne::Widget{NodeEditor}, kind::Symbol, node_idx, slot_idx)
            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)
            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)
                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]))"
                # FIXME: Detach previously-connected links on input
                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)
                attach!(inp_proc, inputnames(inp_proc)[inp_slot_idx], out_proc, out_slot_name) #, linkmode)
            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)
    elseif mouseclicks[2].c
        iscon = kind == :input ? isinputconnected : isoutputconnected
        if iscon(ne, node_idx, slot_idx)
            link_idxs = findlinks(ne, node_idx, slot_idx)
        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)


@@ 154,6 197,7 @@ 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)
    _ne = core(ne)
    nodes = ne.nodes
    links = ne.links



@@ 166,12 210,12 @@ function (ne::Widget{NodeEditor})(outer)
    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
        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
            node_hovered_in_list = node.id
            open_context_menu |= CImGui.IsMouseClicked(1)
        end
        CImGui.PopID()


@@ 226,8 270,8 @@ function (ne::Widget{NodeEditor})(outer)
    # Display nodes
    for node_idx = 1:length(nodes)
        node = nodes[node_idx]
        CImGui.PushID(node.ID)
        node_rect_min = offset + node.Pos
        CImGui.PushID(node.id)
        node_rect_min = offset + node.pos

        # Display node contents first
        CImGui.ChannelsSetCurrent(draw_list, 1) # Foreground


@@ 240,33 284,33 @@ function (ne::Widget{NodeEditor})(outer)

        # 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
        node.last_size = node_min_size(node, CImGui.GetItemRectSize() + NODE_WINDOW_PADDING + NODE_WINDOW_PADDING)
        node_rect_max = node_rect_min + node.last_size

        # Display node box
        CImGui.ChannelsSetCurrent(draw_list, 0) # Background
        CImGui.SetCursorScreenPos(node_rect_min)
        CImGui.InvisibleButton("node", node.Size)
        CImGui.InvisibleButton("node", node.last_size)
        if (CImGui.IsItemHovered())
            node_hovered_in_scene = node.ID
            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
            ne.node_selected = node.id
        end
        if (node_moving_active && CImGui.IsMouseDragging(0))
            node.Pos = node.Pos + CImGui.GetIO().MouseDelta
            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)
        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)
            elseif isinputconnected(_ne, node_idx, slot_idx)
                color = SLOT_CONNECTED_COLOR
            else
                color = SLOT_NORMAL_COLOR


@@ 286,7 330,7 @@ function (ne::Widget{NodeEditor})(outer)
            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)
            elseif isoutputconnected(_ne, node_idx, slot_idx)
                color = SLOT_CONNECTED_COLOR
            else
                color = SLOT_NORMAL_COLOR


@@ 330,20 374,19 @@ function (ne::Widget{NodeEditor})(outer)
        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"
            if CImGui.MenuItem("Delete", C_NULL, false)
                delnode!(_ne, node)
            end
            if (CImGui.MenuItem("Copy", C_NULL, false))
                @info "TODO: Copy"
            if CImGui.MenuItem("Copy", C_NULL, false)
                ne.copybuf = node
            end
        else
            if (CImGui.MenuItem("Add"))
                push!(nodes, Node(length(nodes)+1, "New node", scene_pos, 0f5, ImColor(100, 100, 200), 2, 2))
            if CImGui.MenuItem("Add")
                addnode!(_ne, AddWidgetNode)
            end
            if CImGui.MenuItem("Paste", C_NULL, false)
                addnode!(_ne, clone(ne.copybuf.widget))
            end
            if (CImGui.MenuItem("Paste", C_NULL, false, false)) end
        end
        CImGui.EndPopup()
    end


@@ 359,4 402,11 @@ function (ne::Widget{NodeEditor})(outer)
    CImGui.PopStyleColor()
    CImGui.PopStyleVar(2)
    CImGui.EndGroup()

    for node_id in sort(unique(ne.del_list); rev=true)
        node_idx = findfirst(n->n.id==node_id, ne.nodes)
        node_idx === nothing && continue
        deleteat!(ne.nodes, node_idx)
    end
    empty!(ne.del_list)
end

M src/widgets.jl => src/widgets.jl +16 -5
@@ 1,22 1,31 @@
export Widget, AddWidgetButton, ClosureWidget
export known_widgets, processor, get_processor
export core, processor, get_processor

const KNOWN_WIDGETS = Dict()
const known_widgets = Any[]

register_widgets!(mod::Module, widgets) = (KNOWN_WIDGETS[mod] = widgets;)
deregister_widgets!(mod::Module) = delete!(KNOWN_WIDGETS, mod)

### Core ###

mutable struct Widget{T}
    x::T
    p::Processor
end
Base.getproperty(w::Widget, sym::Symbol) = getproperty(getfield(w, :x), sym)
Base.setproperty!(w::Widget, sym::Symbol, x) = setproperty!(getfield(w, :x), sym, x)
Base.getproperty(w::Widget, sym::Symbol) = getproperty(core(w), sym)
Base.setproperty!(w::Widget, sym::Symbol, x) = setproperty!(core(w), sym, x)
Base.convert(::Type{W} where W<:Widget, x) = Widget(x, get_processor(x))
Base.convert(::Type{W} where W<:Widget, w::Widget) = w
name(w::Widget{T}) where T = string(T)
# FIXME: Unique name for IDs
core(w::Widget) = getfield(w, :x)
processor(w::Widget{T}) where T = getfield(w, :p)

### Overloadable functions ###

name(w::Widget{T}) where T = string(T) # FIXME: Unique name for IDs
get_processor(x) = Processor()
clone(w::Widget) = Widget(copy(core(w)))
save(w::Widget) = w

Base.@kwdef mutable struct WidgetContainer
    widgets::Vector{Widget} = Widget[]


@@ 106,3 115,5 @@ struct TestWidget end
(tw::Widget{TestWidget})(outer) = CImGui.Text("TestWidget")
get_processor(tw::TestWidget) = Processor(["a","b","c"],["d","e"]) do p
end

register_widgets!(Adjutant, known_widgets)