imnodes.cpp 97.9 KB
Newer Older
1
2
3
4
5
// the structure of this file:
//
// [SECTION] internal data structures
// [SECTION] global struct
// [SECTION] editor context definition
6
7
// [SECTION] draw list helper
// [SECTION] ObjectPool implementation
8
9
10
11
12
13
// [SECTION] ui state logic
// [SECTION] render helpers
// [SECTION] API implementation

#include "imnodes.hpp"

Gauthier Quesnel's avatar
Gauthier Quesnel committed
14
#include <imgui.h>
15
#define IMGUI_DEFINE_MATH_OPERATORS
Gauthier Quesnel's avatar
Gauthier Quesnel committed
16
#include <imgui_internal.h>
17
18

// Check minimum ImGui version
Gauthier Quesnel's avatar
Gauthier Quesnel committed
19
#define MINIMUM_COMPATIBLE_IMGUI_VERSION 17400
20
21
#if IMGUI_VERSION_NUM < MINIMUM_COMPATIBLE_IMGUI_VERSION
#error                                                                         \
Gauthier Quesnel's avatar
Gauthier Quesnel committed
22
  "Minimum ImGui version requirement not met -- please use a newer version!"
23
24
25
#endif

#include <assert.h>
Gauthier Quesnel's avatar
Gauthier Quesnel committed
26
#include <limits.h>
27
28
29
#include <math.h>
#include <new>
#include <stdint.h>
Gauthier Quesnel's avatar
Gauthier Quesnel committed
30
#include <stdio.h> // for fwrite, ssprintf, sscanf
31
#include <stdlib.h>
Gauthier Quesnel's avatar
Gauthier Quesnel committed
32
#include <string.h> // strlen, strncmp
33

Gauthier Quesnel's avatar
Gauthier Quesnel committed
34
35
namespace imnodes {
namespace {
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
enum ScopeFlags
{
    Scope_None = 1,
    Scope_Editor = 1 << 1,
    Scope_Node = 1 << 2,
    Scope_Attribute = 1 << 3
};

enum AttributeType
{
    AttributeType_None,
    AttributeType_Input,
    AttributeType_Output
};

Gauthier Quesnel's avatar
Gauthier Quesnel committed
51
52
53
54
55
56
57
58
enum ElementStateChange
{
    ElementStateChange_None = 0,
    ElementStateChange_LinkStarted = 1 << 0,
    ElementStateChange_LinkDropped = 1 << 1,
    ElementStateChange_LinkCreated = 1 << 2
};

59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// [SECTION] internal data structures

// The object T must have the following interface:
//
// struct T
// {
//     T();
//
//     int id;
// };
template<typename T>
struct ObjectPool
{
    ImVector<T> pool;
    ImVector<bool> in_use;
    ImVector<int> free_list;
    ImGuiStorage id_map;

Gauthier Quesnel's avatar
Gauthier Quesnel committed
77
78
79
80
81
82
    ObjectPool()
      : pool()
      , in_use()
      , free_list()
      , id_map()
    {}
83
84
};

Gauthier Quesnel's avatar
Gauthier Quesnel committed
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// Emulates std::optional<int> using the sentinel value `invalid_index`.
struct OptionalIndex
{
    OptionalIndex()
      : m_index(invalid_index)
    {}
    OptionalIndex(const int value)
      : m_index(value)
    {}

    // Observers

    inline bool has_value() const
    {
        return m_index != invalid_index;
    }

    inline int value() const
    {
        assert(has_value());
        return m_index;
    }

    // Modifiers

    inline OptionalIndex& operator=(const int value)
    {
        m_index = value;
        return *this;
    }

    inline void reset()
    {
        m_index = invalid_index;
    }

    inline bool operator==(const OptionalIndex& rhs) const
    {
        return m_index == rhs.m_index;
    }

    inline bool operator==(const int rhs) const
    {
        return m_index == rhs;
    }

Gauthier Quesnel's avatar
Gauthier Quesnel committed
131
132
133
134
135
136
137
138
139
140
    inline bool operator!=(const OptionalIndex& rhs) const
    {
        return m_index != rhs.m_index;
    }

    inline bool operator!=(const int rhs) const
    {
        return m_index != rhs;
    }

Gauthier Quesnel's avatar
Gauthier Quesnel committed
141
142
143
144
145
    static const int invalid_index = -1;

private:
    int m_index;
};
146
147
148
149
150
151
152
153
154
155
156

struct NodeData
{
    int id;
    ImVec2 origin; // The node origin is in editor space
    ImRect title_bar_content_rect;
    ImRect rect;

    struct
    {
        ImU32 background, background_hovered, background_selected, outline,
Gauthier Quesnel's avatar
Gauthier Quesnel committed
157
          titlebar, titlebar_hovered, titlebar_selected;
158
159
160
161
162
163
    } color_style;

    struct
    {
        float corner_rounding;
        ImVec2 padding;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
164
        float border_thickness;
165
166
    } layout_style;

Gauthier Quesnel's avatar
Gauthier Quesnel committed
167
    ImVector<int> pin_indices;
168
169
    bool draggable;

170
171
    NodeData(const int node_id)
      : id(node_id)
Gauthier Quesnel's avatar
Gauthier Quesnel committed
172
173
174
175
176
177
178
179
      , origin(100.0f, 100.0f)
      , title_bar_content_rect()
      , rect(ImVec2(0.0f, 0.0f), ImVec2(0.0f, 0.0f))
      , color_style()
      , layout_style()
      , pin_indices()
      , draggable(true)
    {}
Gauthier Quesnel's avatar
Gauthier Quesnel committed
180
181
182
183
184

    ~NodeData()
    {
        id = INT_MIN;
    }
185
186
187
188
189
};

struct PinData
{
    int id;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
190
    int parent_node_idx;
191
192
193
    ImRect attribute_rect;
    AttributeType type;
    PinShape shape;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
194
195
    ImVec2 pos; // screen-space coordinates
    int flags;
196
197
198
199
200
201

    struct
    {
        ImU32 background, hovered;
    } color_style;

202
203
    PinData(const int pin_id)
      : id(pin_id)
Gauthier Quesnel's avatar
Gauthier Quesnel committed
204
205
206
207
208
209
210
211
      , parent_node_idx()
      , attribute_rect()
      , type(AttributeType_None)
      , shape(PinShape_CircleFilled)
      , pos()
      , flags(AttributeFlags_None)
      , color_style()
    {}
212
213
214
215
216
};

struct LinkData
{
    int id;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
217
    int start_pin_idx, end_pin_idx;
218
219
220
221
222
223

    struct
    {
        ImU32 base, hovered, selected;
    } color_style;

224
225
    LinkData(const int link_id)
      : id(link_id)
Gauthier Quesnel's avatar
Gauthier Quesnel committed
226
227
228
229
      , start_pin_idx()
      , end_pin_idx()
      , color_style()
    {}
230
231
232
233
234
235
236
237
238
};

struct LinkPredicate
{
    bool operator()(const LinkData& lhs, const LinkData& rhs) const
    {
        // Do a unique compare by sorting the pins' addresses.
        // This catches duplicate links, whether they are in the
        // same direction or not.
Gauthier Quesnel's avatar
Gauthier Quesnel committed
239
240
        // Sorting by pin index should have the uniqueness guarantees as sorting
        // by id -- each unique id will get one slot in the link pool array.
241

Gauthier Quesnel's avatar
Gauthier Quesnel committed
242
243
244
245
        int lhs_start = lhs.start_pin_idx;
        int lhs_end = lhs.end_pin_idx;
        int rhs_start = rhs.start_pin_idx;
        int rhs_end = rhs.end_pin_idx;
246

Gauthier Quesnel's avatar
Gauthier Quesnel committed
247
        if (lhs_start > lhs_end) {
248
249
250
            ImSwap(lhs_start, lhs_end);
        }

Gauthier Quesnel's avatar
Gauthier Quesnel committed
251
        if (rhs_start > rhs_end) {
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
            ImSwap(rhs_start, rhs_end);
        }

        return lhs_start == rhs_start && lhs_end == rhs_end;
    }
};

struct BezierCurve
{
    // the curve control points
    ImVec2 p0, p1, p2, p3;
};

struct LinkBezierData
{
    BezierCurve bezier;
    int num_segments;
};

Gauthier Quesnel's avatar
Gauthier Quesnel committed
271
enum ClickInteractionType
272
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
273
274
275
276
277
278
    ClickInteractionType_Node,
    ClickInteractionType_Link,
    ClickInteractionType_LinkCreation,
    ClickInteractionType_Panning,
    ClickInteractionType_BoxSelection,
    ClickInteractionType_None
279
280
};

Gauthier Quesnel's avatar
Gauthier Quesnel committed
281
282
283
284
285
286
enum LinkCreationType
{
    LinkCreationType_Standard,
    LinkCreationType_FromDetach
};

Gauthier Quesnel's avatar
Gauthier Quesnel committed
287
struct ClickInteractionState
288
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
289
290
291
    struct
    {
        int start_pin_idx;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
292
293
        OptionalIndex end_pin_idx;
        LinkCreationType link_creation_type;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
294
    } link_creation;
295

Gauthier Quesnel's avatar
Gauthier Quesnel committed
296
297
298
299
    struct
    {
        ImRect rect;
    } box_selector;
300
301
302
303
304
305
306
};

struct ColorStyleElement
{
    ImU32 color;
    ColorStyle item;

Gauthier Quesnel's avatar
Gauthier Quesnel committed
307
308
309
310
    ColorStyleElement(const ImU32 c, const ColorStyle s)
      : color(c)
      , item(s)
    {}
311
312
313
314
315
316
317
318
};

struct StyleElement
{
    StyleVar item;
    float value;

    StyleElement(const float value, const StyleVar variable)
Gauthier Quesnel's avatar
Gauthier Quesnel committed
319
320
321
      : item(variable)
      , value(value)
    {}
322
};
Gauthier Quesnel's avatar
Gauthier Quesnel committed
323
} // namespace
324
325
// [SECTION] global struct
// this stores data which only lives for one frame
Gauthier Quesnel's avatar
Gauthier Quesnel committed
326
struct Context
327
328
329
{
    EditorContext* default_editor_ctx;
    EditorContext* editor_ctx;
330
331

    // Canvas draw list and helper state
332
    ImDrawList* canvas_draw_list;
333
334
335
    ImGuiStorage node_idx_to_submission_idx;
    ImVector<int> node_idx_submission_order;
    ImVector<int> node_indices_overlapping_with_mouse;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
336
    ImVector<int> occluded_pin_indices;
337
338

    // Canvas extents
339
340
    ImVec2 canvas_origin_screen_space;
    ImRect canvas_rect_screen_space;
341
342

    // Debug helpers
343
344
    ScopeFlags current_scope;

345
    // Configuration state
Gauthier Quesnel's avatar
Gauthier Quesnel committed
346
    IO io;
347
348
349
350
351
    Style style;
    ImVector<ColorStyleElement> color_modifier_stack;
    ImVector<StyleElement> style_modifier_stack;
    ImGuiTextBuffer text_buffer;

Gauthier Quesnel's avatar
Gauthier Quesnel committed
352
353
354
    int current_attribute_flags;
    ImVector<int> attribute_flag_stack;

355
    // UI element state
Gauthier Quesnel's avatar
Gauthier Quesnel committed
356
357
    int current_node_idx;
    int current_pin_idx;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
358
    int current_attribute_id;
359

Gauthier Quesnel's avatar
Gauthier Quesnel committed
360
    OptionalIndex hovered_node_idx;
361
    OptionalIndex interactive_node_idx;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
362
363
364
    OptionalIndex hovered_link_idx;
    OptionalIndex hovered_pin_idx;
    int hovered_pin_flags;
365

Gauthier Quesnel's avatar
Gauthier Quesnel committed
366
    OptionalIndex deleted_link_idx;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
367
    OptionalIndex snap_link_idx;
368

369
    // Event helper state
Gauthier Quesnel's avatar
Gauthier Quesnel committed
370
    int element_state_change;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
371
372
373
374

    int active_attribute_id;
    bool active_attribute;

375
376
377
378
    // ImGui::IO cache

    ImVec2 mouse_pos;

Gauthier Quesnel's avatar
Gauthier Quesnel committed
379
380
    bool left_mouse_clicked;
    bool left_mouse_released;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
381
    bool alt_mouse_clicked;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
382
    bool left_mouse_dragging;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
383
384
385
386
387
388
    bool alt_mouse_dragging;
};

Context* g = NULL;

namespace {
389

Gauthier Quesnel's avatar
Gauthier Quesnel committed
390
391
EditorContext&
editor_context_get()
392
{
393
    // No editor context was set! Did you forget to call imnodes::Initialize?
Gauthier Quesnel's avatar
Gauthier Quesnel committed
394
395
    assert(g->editor_ctx != NULL);
    return *g->editor_ctx;
396
397
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
398
399
inline ImVec2
eval_bezier(float t, const BezierCurve& bezier)
400
401
{
    // B(t) = (1-t)**3 p0 + 3(1 - t)**2 t P1 + 3(1-t)t**2 P2 + t**3 P3
Gauthier Quesnel's avatar
Gauthier Quesnel committed
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
    return ImVec2((1 - t) * (1 - t) * (1 - t) * bezier.p0.x +
                    3 * (1 - t) * (1 - t) * t * bezier.p1.x +
                    3 * (1 - t) * t * t * bezier.p2.x + t * t * t * bezier.p3.x,
                  (1 - t) * (1 - t) * (1 - t) * bezier.p0.y +
                    3 * (1 - t) * (1 - t) * t * bezier.p1.y +
                    3 * (1 - t) * t * t * bezier.p2.y +
                    t * t * t * bezier.p3.y);
}

// Calculates the closest point along each bezier curve segment.
ImVec2
get_closest_point_on_cubic_bezier(const int num_segments,
                                  const ImVec2& p,
                                  const BezierCurve& bezier)
{
    IM_ASSERT(num_segments > 0);
    ImVec2 p_last = bezier.p0;
    ImVec2 p_closest;
    float p_closest_dist = FLT_MAX;
    float t_step = 1.0f / (float)num_segments;
    for (int i = 1; i <= num_segments; ++i) {
        ImVec2 p_current = eval_bezier(t_step * i, bezier);
        ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p);
        float dist = ImLengthSqr(p - p_line);
        if (dist < p_closest_dist) {
            p_closest = p_line;
            p_closest_dist = dist;
429
        }
Gauthier Quesnel's avatar
Gauthier Quesnel committed
430
        p_last = p_current;
431
    }
Gauthier Quesnel's avatar
Gauthier Quesnel committed
432
    return p_closest;
433
434
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
435
436
437
438
inline float
get_distance_to_cubic_bezier(const ImVec2& pos,
                             const BezierCurve& bezier,
                             const int num_segments)
439
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
440
441
    const ImVec2 point_on_curve =
      get_closest_point_on_cubic_bezier(num_segments, pos, bezier);
442
443
444
445
446

    const ImVec2 to_curve = point_on_curve - pos;
    return ImSqrt(ImLengthSqr(to_curve));
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
447
448
inline ImRect
get_containing_rect_for_bezier_curve(const BezierCurve& bezier)
449
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
450
451
452
453
454
    const ImVec2 min =
      ImVec2(ImMin(bezier.p0.x, bezier.p3.x), ImMin(bezier.p0.y, bezier.p3.y));
    const ImVec2 max =
      ImVec2(ImMax(bezier.p0.x, bezier.p3.x), ImMax(bezier.p0.y, bezier.p3.y));

Gauthier Quesnel's avatar
Gauthier Quesnel committed
455
    const float hover_distance = g->style.link_hover_distance;
456
457
458
459

    ImRect rect(min, max);
    rect.Add(bezier.p1);
    rect.Add(bezier.p2);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
460
    rect.Expand(ImVec2(hover_distance, hover_distance));
461
462
463
464

    return rect;
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
465
466
467
468
469
inline LinkBezierData
get_link_renderable(ImVec2 start,
                    ImVec2 end,
                    const AttributeType start_type,
                    const float line_segments_per_length)
470
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
471
472
473
    assert((start_type == AttributeType_Input) ||
           (start_type == AttributeType_Output));
    if (start_type == AttributeType_Input) {
474
475
        ImSwap(start, end);
    }
Gauthier Quesnel's avatar
Gauthier Quesnel committed
476
477

    const float link_length = ImSqrt(ImLengthSqr(end - start));
478
479
480
481
482
483
484
    const ImVec2 offset = ImVec2(0.25f * link_length, 0.f);
    LinkBezierData link_data;
    link_data.bezier.p0 = start;
    link_data.bezier.p1 = start + offset;
    link_data.bezier.p2 = end - offset;
    link_data.bezier.p3 = end;
    link_data.num_segments =
Gauthier Quesnel's avatar
Gauthier Quesnel committed
485
      ImMax(static_cast<int>(link_length * line_segments_per_length), 1);
486
487
488
    return link_data;
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
489
490
inline float
eval_implicit_line_eq(const ImVec2& p1, const ImVec2& p2, const ImVec2& p)
491
492
493
494
495
{
    return (p2.y - p1.y) * p.x + (p1.x - p2.x) * p.y +
           (p2.x * p1.y - p1.x * p2.y);
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
496
497
498
499
500
inline int
sign(float val)
{
    return int(val > 0.0f) - int(val < 0.0f);
}
501

Gauthier Quesnel's avatar
Gauthier Quesnel committed
502
503
504
505
inline bool
rectangle_overlaps_line_segment(const ImRect& rect,
                                const ImVec2& p1,
                                const ImVec2& p2)
506
{
507
508
    // Trivial case: rectangle contains an endpoint
    if (rect.Contains(p1) || rect.Contains(p2)) {
509
510
511
        return true;
    }

512
513
    // Flip rectangle if necessary
    ImRect flip_rect = rect;
514

515
516
517
    if (flip_rect.Min.x > flip_rect.Max.x) {
        ImSwap(flip_rect.Min.x, flip_rect.Max.x);
    }
518

519
520
    if (flip_rect.Min.y > flip_rect.Max.y) {
        ImSwap(flip_rect.Min.y, flip_rect.Max.y);
521
522
    }

523
524
525
526
527
528
529
    // Trivial case: line segment lies to one particular side of rectangle
    if ((p1.x < flip_rect.Min.x && p2.x < flip_rect.Min.x) ||
        (p1.x > flip_rect.Max.x && p2.x > flip_rect.Max.x) ||
        (p1.y < flip_rect.Min.y && p2.y < flip_rect.Min.y) ||
        (p1.y > flip_rect.Max.y && p2.y > flip_rect.Max.y)) {
        return false;
    }
530

531
532
533
534
535
536
537
538
    const int corner_signs[4] = {
        sign(eval_implicit_line_eq(p1, p2, flip_rect.Min)),
        sign(eval_implicit_line_eq(
          p1, p2, ImVec2(flip_rect.Max.x, flip_rect.Min.y))),
        sign(eval_implicit_line_eq(
          p1, p2, ImVec2(flip_rect.Min.x, flip_rect.Max.y))),
        sign(eval_implicit_line_eq(p1, p2, flip_rect.Max))
    };
539

540
541
    int sum = 0;
    int sum_abs = 0;
542

543
544
545
    for (int i = 0; i < 4; ++i) {
        sum += corner_signs[i];
        sum_abs += abs(corner_signs[i]);
546
547
    }

548
549
    // At least one corner of rectangle lies on a different side of line segment
    return abs(sum) != sum_abs;
550
551
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
552
553
554
inline bool
rectangle_overlaps_bezier(const ImRect& rectangle,
                          const LinkBezierData& link_data)
555
556
557
{
    ImVec2 current = eval_bezier(0.f, link_data.bezier);
    const float dt = 1.0f / link_data.num_segments;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
558
    for (int s = 0; s < link_data.num_segments; ++s) {
559
        ImVec2 next =
Gauthier Quesnel's avatar
Gauthier Quesnel committed
560
561
          eval_bezier(static_cast<float>((s + 1) * dt), link_data.bezier);
        if (rectangle_overlaps_line_segment(rectangle, current, next)) {
562
563
564
565
566
567
568
            return true;
        }
        current = next;
    }
    return false;
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
569
570
571
572
573
inline bool
rectangle_overlaps_link(const ImRect& rectangle,
                        const ImVec2& start,
                        const ImVec2& end,
                        const AttributeType start_type)
574
575
576
577
{
    // First level: simple rejection test via rectangle overlap:

    ImRect lrect = ImRect(start, end);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
578
    if (lrect.Min.x > lrect.Max.x) {
579
580
581
        ImSwap(lrect.Min.x, lrect.Max.x);
    }

Gauthier Quesnel's avatar
Gauthier Quesnel committed
582
    if (lrect.Min.y > lrect.Max.y) {
583
584
585
        ImSwap(lrect.Min.y, lrect.Max.y);
    }

Gauthier Quesnel's avatar
Gauthier Quesnel committed
586
    if (rectangle.Overlaps(lrect)) {
587
588
589
        // First, check if either one or both endpoinds are trivially contained
        // in the rectangle

Gauthier Quesnel's avatar
Gauthier Quesnel committed
590
        if (rectangle.Contains(start) || rectangle.Contains(end)) {
591
592
593
594
595
596
597
            return true;
        }

        // Second level of refinement: do a more expensive test against the
        // link

        const LinkBezierData link_data = get_link_renderable(
Gauthier Quesnel's avatar
Gauthier Quesnel committed
598
          start, end, start_type, g->style.link_line_segments_per_length);
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
        return rectangle_overlaps_bezier(rectangle, link_data);
    }

    return false;
}
} // namespace

// [SECTION] editor context definition

struct EditorContext
{
    ObjectPool<NodeData> nodes;
    ObjectPool<PinData> pins;
    ObjectPool<LinkData> links;

614
615
    ImVector<int> node_depth_order;

616
617
618
    // ui related fields
    ImVec2 panning;

Gauthier Quesnel's avatar
Gauthier Quesnel committed
619
620
    ImVector<int> selected_node_indices;
    ImVector<int> selected_link_indices;
621

Gauthier Quesnel's avatar
Gauthier Quesnel committed
622
623
    ClickInteractionType click_interaction_type;
    ClickInteractionState click_interaction_state;
624
625

    EditorContext()
Gauthier Quesnel's avatar
Gauthier Quesnel committed
626
627
628
629
630
631
632
633
634
      : nodes()
      , pins()
      , links()
      , panning(0.f, 0.f)
      , selected_node_indices()
      , selected_link_indices()
      , click_interaction_type(ClickInteractionType_None)
      , click_interaction_state()
    {}
635
636
};

Gauthier Quesnel's avatar
Gauthier Quesnel committed
637
namespace {
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
// [SECTION] draw list helper

void
ImDrawList_grow_channels(ImDrawList* draw_list, const int num_channels)
{
    ImDrawListSplitter& splitter = draw_list->_Splitter;

    if (splitter._Count == 1) {
        splitter.Split(draw_list, num_channels + 1);
        return;
    }

    // NOTE: this logic has been lifted from ImDrawListSplitter::Split with
    // slight modifications to allow nested splits. The main modification is
    // that we only create new ImDrawChannel instances after splitter._Count,
    // instead of over the whole splitter._Channels array like the regular
    // ImDrawListSplitter::Split method does.

    const int old_channel_capacity = splitter._Channels.Size;
    // NOTE: _Channels is not resized down, and therefore _Count <=
    // _Channels.size()!
    const int old_channel_count = splitter._Count;
    const int requested_channel_count = old_channel_count + num_channels;
    if (old_channel_capacity < old_channel_count + num_channels) {
        splitter._Channels.resize(requested_channel_count);
    }

    splitter._Count = requested_channel_count;

    for (int i = old_channel_count; i < requested_channel_count; ++i) {
        ImDrawChannel& channel = splitter._Channels[i];

        // If we're inside the old capacity region of the array, we need to
        // reuse the existing memory of the command and index buffers.
        if (i < old_channel_capacity) {
            channel._CmdBuffer.resize(0);
            channel._IdxBuffer.resize(0);
        }
        // Else, we need to construct new draw channels.
        else {
            IM_PLACEMENT_NEW(&channel) ImDrawChannel();
        }

        {
            ImDrawCmd draw_cmd;
            draw_cmd.ClipRect = draw_list->_ClipRectStack.back();
            draw_cmd.TextureId = draw_list->_TextureIdStack.back();
            channel._CmdBuffer.push_back(draw_cmd);
        }
    }
}

void
ImDrawListSplitter_swap_channels(ImDrawListSplitter& splitter,
                                 const int lhs_idx,
                                 const int rhs_idx)
{
    if (lhs_idx == rhs_idx) {
        return;
    }

    assert(lhs_idx >= 0 && lhs_idx < splitter._Count);
    assert(rhs_idx >= 0 && rhs_idx < splitter._Count);

    ImDrawChannel& lhs_channel = splitter._Channels[lhs_idx];
    ImDrawChannel& rhs_channel = splitter._Channels[rhs_idx];
    lhs_channel._CmdBuffer.swap(rhs_channel._CmdBuffer);
    lhs_channel._IdxBuffer.swap(rhs_channel._IdxBuffer);

    const int current_channel = splitter._Current;

    if (current_channel == lhs_idx) {
        splitter._Current = rhs_idx;
    } else if (current_channel == rhs_idx) {
        splitter._Current = lhs_idx;
    }
}

void
draw_list_set(ImDrawList* window_draw_list)
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
719
720
721
    g->canvas_draw_list = window_draw_list;
    g->node_idx_to_submission_idx.Clear();
    g->node_idx_submission_order.clear();
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
}

// The draw list channels are structured as follows. First we have our base
// channel, the canvas grid on which we render the grid lines in
// BeginNodeEditor(). The base channel is the reason
// draw_list_submission_idx_to_background_channel_idx offsets the index by one.
// Each BeginNode() call appends two new draw channels, for the node background
// and foreground. The node foreground is the channel into which the node's
// ImGui content is rendered. Finally, in EndNodeEditor() we append one last
// draw channel for rendering the selection box and the incomplete link on top
// of everything else.
//
// +----------+----------+----------+----------+----------+----------+
// |          |          |          |          |          |          |
// |canvas    |node      |node      |...       |...       |click     |
// |grid      |background|foreground|          |          |interaction
// |          |          |          |          |          |          |
// +----------+----------+----------+----------+----------+----------+
//            |                     |
//            |   submission idx    |
//            |                     |
//            -----------------------

void
draw_list_add_node(const int node_idx)
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
748
749
750
751
    g->node_idx_to_submission_idx.SetInt(static_cast<ImGuiID>(node_idx),
                                         g->node_idx_submission_order.Size);
    g->node_idx_submission_order.push_back(node_idx);
    ImDrawList_grow_channels(g->canvas_draw_list, 2);
752
753
754
755
756
757
758
}

void
draw_list_append_click_interaction_channel()
{
    // NOTE: don't use this function outside of EndNodeEditor. Using this before
    // all nodes have been added will screw up the node draw order.
Gauthier Quesnel's avatar
Gauthier Quesnel committed
759
    ImDrawList_grow_channels(g->canvas_draw_list, 1);
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
}

int
draw_list_submission_idx_to_background_channel_idx(const int submission_idx)
{
    // NOTE: the first channel is the canvas background, i.e. the grid
    return 1 + 2 * submission_idx;
}

int
draw_list_submission_idx_to_foreground_channel_idx(const int submission_idx)
{
    return draw_list_submission_idx_to_background_channel_idx(submission_idx) +
           1;
}

void
draw_list_activate_click_interaction_channel()
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
779
780
    g->canvas_draw_list->_Splitter.SetCurrentChannel(
      g->canvas_draw_list, g->canvas_draw_list->_Splitter._Count - 1);
781
782
783
784
785
786
787
}

void
draw_list_activate_current_node_foreground()
{
    const int foreground_channel_idx =
      draw_list_submission_idx_to_foreground_channel_idx(
Gauthier Quesnel's avatar
Gauthier Quesnel committed
788
789
790
        g->node_idx_submission_order.Size - 1);
    g->canvas_draw_list->_Splitter.SetCurrentChannel(g->canvas_draw_list,
                                                     foreground_channel_idx);
791
792
793
794
795
796
}

void
draw_list_activate_node_background(const int node_idx)
{
    const int submission_idx =
Gauthier Quesnel's avatar
Gauthier Quesnel committed
797
      g->node_idx_to_submission_idx.GetInt(static_cast<ImGuiID>(node_idx), -1);
798
799
800
801
802
803
804
805
806
807
    // There is a discrepancy in the submitted node count and the rendered node
    // count! Did you call one of the following functions
    // * EditorContextMoveToNode
    // * SetNodeScreenSpacePos
    // * SetNodeGridSpacePos
    // * SetNodeDraggable
    // after the BeginNode/EndNode function calls?
    assert(submission_idx != -1);
    const int background_channel_idx =
      draw_list_submission_idx_to_background_channel_idx(submission_idx);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
808
809
    g->canvas_draw_list->_Splitter.SetCurrentChannel(g->canvas_draw_list,
                                                     background_channel_idx);
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
}

void
draw_list_swap_submission_indices(const int lhs_idx, const int rhs_idx)
{
    assert(lhs_idx != rhs_idx);

    const int lhs_foreground_channel_idx =
      draw_list_submission_idx_to_foreground_channel_idx(lhs_idx);
    const int lhs_background_channel_idx =
      draw_list_submission_idx_to_background_channel_idx(lhs_idx);
    const int rhs_foreground_channel_idx =
      draw_list_submission_idx_to_foreground_channel_idx(rhs_idx);
    const int rhs_background_channel_idx =
      draw_list_submission_idx_to_background_channel_idx(rhs_idx);

Gauthier Quesnel's avatar
Gauthier Quesnel committed
826
    ImDrawListSplitter_swap_channels(g->canvas_draw_list->_Splitter,
827
828
                                     lhs_background_channel_idx,
                                     rhs_background_channel_idx);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
829
    ImDrawListSplitter_swap_channels(g->canvas_draw_list->_Splitter,
830
831
832
833
834
835
836
                                     lhs_foreground_channel_idx,
                                     rhs_foreground_channel_idx);
}

void
draw_list_sort_channels_by_depth(const ImVector<int>& node_idx_depth_order)
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
837
    if (g->node_idx_to_submission_idx.Data.Size < 2) {
838
839
840
        return;
    }

Gauthier Quesnel's avatar
Gauthier Quesnel committed
841
    assert(node_idx_depth_order.Size == g->node_idx_submission_order.Size);
842
843
844
845

    int start_idx = node_idx_depth_order.Size - 1;

    while (node_idx_depth_order[start_idx] ==
Gauthier Quesnel's avatar
Gauthier Quesnel committed
846
           g->node_idx_submission_order[start_idx]) {
847
848
849
850
851
852
853
854
855
856
857
858
859
860
        if (--start_idx == 0) {
            // early out if submission order and depth order are the same
            return;
        }
    }

    // TODO: this is an O(N^2) algorithm. It might be worthwhile revisiting this
    // to see if the time complexity can be reduced.

    for (int depth_idx = start_idx; depth_idx > 0; --depth_idx) {
        const int node_idx = node_idx_depth_order[depth_idx];

        // Find the current index of the node_idx in the submission order array
        int submission_idx = -1;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
861
862
        for (int i = 0; i < g->node_idx_submission_order.Size; ++i) {
            if (g->node_idx_submission_order[i] == node_idx) {
863
864
865
866
867
868
869
870
871
872
873
874
                submission_idx = i;
                break;
            }
        }
        assert(submission_idx >= 0);

        if (submission_idx == depth_idx) {
            continue;
        }

        for (int j = submission_idx; j < depth_idx; ++j) {
            draw_list_swap_submission_indices(j, j + 1);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
875
876
            ImSwap(g->node_idx_submission_order[j],
                   g->node_idx_submission_order[j + 1]);
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
        }
    }
}

// [SECTION] ObjectPool implementation

template<typename T>
int
object_pool_find(ObjectPool<T>& objects, const int id)
{
    const int index = objects.id_map.GetInt(static_cast<ImGuiID>(id), -1);
    return index;
}

template<typename T>
void
object_pool_update(ObjectPool<T>& objects)
{
    objects.free_list.clear();
    for (int i = 0; i < objects.in_use.size(); ++i) {
        if (!objects.in_use[i]) {
            objects.id_map.SetInt(objects.pool[i].id, -1);
            objects.free_list.push_back(i);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
900
            (objects.pool.Data + i)->~T();
901
902
903
904
905
906
907
908
909
910
        }
    }
}

template<>
void
object_pool_update(ObjectPool<NodeData>& nodes)
{
    nodes.free_list.clear();
    for (int i = 0; i < nodes.in_use.size(); ++i) {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
911
912
913
        if (nodes.in_use[i]) {
            nodes.pool[i].pin_indices.clear();
        } else {
914
915
            const int previous_id = nodes.pool[i].id;
            const int previous_idx = nodes.id_map.GetInt(previous_id, -1);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
916

917
            if (previous_idx != -1) {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
918
                assert(previous_idx == i);
919
920
921
922
923
924
925
926
927
928
929
                // Remove node idx form depth stack the first time we detect
                // that this idx slot is unused
                ImVector<int>& depth_stack =
                  editor_context_get().node_depth_order;
                const int* const elem = depth_stack.find(i);
                assert(elem != depth_stack.end());
                depth_stack.erase(elem);
            }

            nodes.id_map.SetInt(previous_id, -1);
            nodes.free_list.push_back(i);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
930
            (nodes.pool.Data + i)->~NodeData();
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
        }
    }
}

template<typename T>
void
object_pool_reset(ObjectPool<T>& objects)
{
    if (!objects.in_use.empty()) {
        memset(objects.in_use.Data, 0, objects.in_use.size_in_bytes());
    }
}

template<typename T>
int
object_pool_find_or_create_index(ObjectPool<T>& objects, const int id)
{
    int index = objects.id_map.GetInt(static_cast<ImGuiID>(id), -1);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
949
950

    // Construct new object
951
952
953
    if (index == -1) {
        if (objects.free_list.empty()) {
            index = objects.pool.size();
Gauthier Quesnel's avatar
Gauthier Quesnel committed
954
955
956
957
            IM_ASSERT(objects.pool.size() == objects.in_use.size());
            const int new_size = objects.pool.size() + 1;
            objects.pool.resize(new_size);
            objects.in_use.resize(new_size);
958
959
960
961
        } else {
            index = objects.free_list.back();
            objects.free_list.pop_back();
        }
Gauthier Quesnel's avatar
Gauthier Quesnel committed
962
        IM_PLACEMENT_NEW(objects.pool.Data + index) T(id);
963
964
        objects.id_map.SetInt(static_cast<ImGuiID>(id), index);
    }
Gauthier Quesnel's avatar
Gauthier Quesnel committed
965
966

    // Flag it as used
967
    objects.in_use[index] = true;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
968

969
970
971
972
973
974
975
976
    return index;
}

template<>
int
object_pool_find_or_create_index(ObjectPool<NodeData>& nodes, const int node_id)
{
    int node_idx = nodes.id_map.GetInt(static_cast<ImGuiID>(node_id), -1);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
977
978

    // Construct new node
979
980
981
    if (node_idx == -1) {
        if (nodes.free_list.empty()) {
            node_idx = nodes.pool.size();
Gauthier Quesnel's avatar
Gauthier Quesnel committed
982
983
984
985
            IM_ASSERT(nodes.pool.size() == nodes.in_use.size());
            const int new_size = nodes.pool.size() + 1;
            nodes.pool.resize(new_size);
            nodes.in_use.resize(new_size);
986
987
988
989
        } else {
            node_idx = nodes.free_list.back();
            nodes.free_list.pop_back();
        }
Gauthier Quesnel's avatar
Gauthier Quesnel committed
990
        IM_PLACEMENT_NEW(nodes.pool.Data + node_idx) NodeData(node_id);
991
992
993
994
995
        nodes.id_map.SetInt(static_cast<ImGuiID>(node_id), node_idx);

        EditorContext& editor = editor_context_get();
        editor.node_depth_order.push_back(node_idx);
    }
Gauthier Quesnel's avatar
Gauthier Quesnel committed
996
997

    // Flag node as used
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
    nodes.in_use[node_idx] = true;

    return node_idx;
}

template<typename T>
T&
object_pool_find_or_create_object(ObjectPool<T>& objects, const int id)
{
    const int index = object_pool_find_or_create_index(objects, id);
    return objects.pool[index];
}

1011
1012
// [SECTION] ui state logic

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1013
1014
1015
1016
1017
1018
1019
ImVec2
get_screen_space_pin_coordinates(const ImRect& node_rect,
                                 const ImRect& attribute_rect,
                                 const AttributeType type)
{
    assert(type == AttributeType_Input || type == AttributeType_Output);
    const float x = type == AttributeType_Input
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1020
1021
                      ? (node_rect.Min.x - g->style.pin_offset)
                      : (node_rect.Max.x + g->style.pin_offset);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1022
1023
1024
1025
1026
1027
    return ImVec2(x, 0.5f * (attribute_rect.Min.y + attribute_rect.Max.y));
}

ImVec2
get_screen_space_pin_coordinates(const EditorContext& editor,
                                 const PinData& pin)
1028
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1029
1030
1031
1032
    const ImRect& parent_node_rect =
      editor.nodes.pool[pin.parent_node_idx].rect;
    return get_screen_space_pin_coordinates(
      parent_node_rect, pin.attribute_rect, pin.type);
1033
1034
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
bool
mouse_in_canvas()
{
    // This flag should be true either when hovering or clicking something in
    // the canvas.
    const bool is_window_hovered_or_focused =
      ImGui::IsWindowHovered() || ImGui::IsWindowFocused();

    return is_window_hovered_or_focused &&
           g->canvas_rect_screen_space.Contains(ImGui::GetMousePos());
}
1046

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
void
begin_node_selection(EditorContext& editor, const int node_idx)
{
    // Don't start selecting a node if we are e.g. already creating and dragging
    // a new link! New link creation can happen when the mouse is clicked over
    // a node, but within the hover radius of a pin.
    if (editor.click_interaction_type != ClickInteractionType_None) {
        return;
    }

    editor.click_interaction_type = ClickInteractionType_Node;
    // If the node is not already contained in the selection, then we want only
    // the interaction node to be selected, effective immediately.
    //
    // Otherwise, we want to allow for the possibility of multiple nodes to be
    // moved at once.
    if (!editor.selected_node_indices.contains(node_idx)) {
        editor.selected_node_indices.clear();
        editor.selected_link_indices.clear();
        editor.selected_node_indices.push_back(node_idx);
1067
1068
1069
1070
1071
1072
1073

        // Ensure that individually selected nodes get rendered on top
        ImVector<int>& depth_stack = editor.node_depth_order;
        const int* const elem = depth_stack.find(node_idx);
        assert(elem != depth_stack.end());
        depth_stack.erase(elem);
        depth_stack.push_back(node_idx);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1074
1075
1076
1077
1078
    }
}

void
begin_link_selection(EditorContext& editor, const int link_idx)
1079
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1080
1081
1082
1083
1084
1085
    editor.click_interaction_type = ClickInteractionType_Link;
    // When a link is selected, clear all other selections, and insert the link
    // as the sole selection.
    editor.selected_node_indices.clear();
    editor.selected_link_indices.clear();
    editor.selected_link_indices.push_back(link_idx);
1086
1087
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1088
1089
1090
1091
void
begin_link_detach(EditorContext& editor,
                  const int link_idx,
                  const int detach_pin_idx)
1092
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1093
1094
    const LinkData& link = editor.links.pool[link_idx];
    ClickInteractionState& state = editor.click_interaction_state;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1095
    state.link_creation.end_pin_idx.reset();
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1096
1097
1098
    state.link_creation.start_pin_idx = detach_pin_idx == link.start_pin_idx
                                          ? link.end_pin_idx
                                          : link.start_pin_idx;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1099
    g->deleted_link_idx = link_idx;
1100
1101
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1102
1103
void
begin_link_interaction(EditorContext& editor, const int link_idx)
1104
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1105
1106
1107
    // First check if we are clicking a link in the vicinity of a pin.
    // This may result in a link detach via click and drag.
    if (editor.click_interaction_type == ClickInteractionType_LinkCreation) {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1108
        if ((g->hovered_pin_flags &
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1109
             AttributeFlags_EnableLinkDetachWithDragClick) != 0) {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1110
            begin_link_detach(editor, link_idx, g->hovered_pin_idx.value());
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1111
1112
            editor.click_interaction_state.link_creation.link_creation_type =
              LinkCreationType_FromDetach;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1113
1114
1115
1116
1117
1118
        }
    }
    // If we aren't near a pin, check if we are clicking the link with the
    // modifier pressed. This may also result in a link detach via clicking.
    else {
        const bool modifier_pressed =
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1119
          g->io.link_detach_with_modifier_click.modifier == NULL
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1120
            ? false
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1121
            : *g->io.link_detach_with_modifier_click.modifier;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1122
1123
1124
1125
1126

        if (modifier_pressed) {
            const LinkData& link = editor.links.pool[link_idx];
            const PinData& start_pin = editor.pins.pool[link.start_pin_idx];
            const PinData& end_pin = editor.pins.pool[link.end_pin_idx];
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1127
            const ImVec2& mouse_pos = g->mouse_pos;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1128
1129
1130
1131
1132
1133
1134
1135
            const float dist_to_start = ImLengthSqr(start_pin.pos - mouse_pos);
            const float dist_to_end = ImLengthSqr(end_pin.pos - mouse_pos);
            const int closest_pin_idx = dist_to_start < dist_to_end
                                          ? link.start_pin_idx
                                          : link.end_pin_idx;

            editor.click_interaction_type = ClickInteractionType_LinkCreation;
            begin_link_detach(editor, link_idx, closest_pin_idx);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1136
1137
            editor.click_interaction_state.link_creation.link_creation_type =
              LinkCreationType_FromDetach;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
        } else {
            begin_link_selection(editor, link_idx);
        }
    }
}

void
begin_link_creation(EditorContext& editor, const int hovered_pin_idx)
{
    editor.click_interaction_type = ClickInteractionType_LinkCreation;
    editor.click_interaction_state.link_creation.start_pin_idx =
      hovered_pin_idx;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1150
1151
1152
    editor.click_interaction_state.link_creation.end_pin_idx.reset();
    editor.click_interaction_state.link_creation.link_creation_type =
      LinkCreationType_Standard;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1153
    g->element_state_change |= ElementStateChange_LinkStarted;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1154
1155
1156
1157
1158
1159
}

void
begin_canvas_interaction(EditorContext& editor)
{
    const bool any_ui_element_hovered =
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1160
1161
      g->hovered_node_idx.has_value() || g->hovered_link_idx.has_value() ||
      g->hovered_pin_idx.has_value() || ImGui::IsAnyItemHovered();
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1162

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1163
    const bool mouse_not_in_canvas = !mouse_in_canvas();
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1164

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1165
1166
    if (editor.click_interaction_type != ClickInteractionType_None ||
        any_ui_element_hovered || mouse_not_in_canvas) {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1167
1168
1169
        return;
    }

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1170
    const bool started_panning = g->alt_mouse_clicked;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1171

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1172
1173
1174
1175
1176
    if (started_panning) {
        editor.click_interaction_type = ClickInteractionType_Panning;
    } else if (g->left_mouse_clicked) {
        editor.click_interaction_type = ClickInteractionType_BoxSelection;
        editor.click_interaction_state.box_selector.rect.Min = g->mouse_pos;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1177
1178
1179
1180
1181
1182
    }
}

void
box_selector_update_selection(EditorContext& editor, ImRect box_rect)
{
1183
1184
    // Invert box selector coordinates as needed

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1185
    if (box_rect.Min.x > box_rect.Max.x) {
1186
1187
1188
        ImSwap(box_rect.Min.x, box_rect.Max.x);
    }

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1189
    if (box_rect.Min.y > box_rect.Max.y) {
1190
1191
1192
        ImSwap(box_rect.Min.y, box_rect.Max.y);
    }

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1193
1194
1195
    // Update node selection

    editor.selected_node_indices.clear();
1196
1197
1198

    // Test for overlap against node rectangles

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1199
1200
1201
1202
1203
    for (int node_idx = 0; node_idx < editor.nodes.pool.size(); ++node_idx) {
        if (editor.nodes.in_use[node_idx]) {
            NodeData& node = editor.nodes.pool[node_idx];
            if (box_rect.Overlaps(node.rect)) {
                editor.selected_node_indices.push_back(node_idx);
1204
1205
1206
1207
            }
        }
    }

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1208
1209
1210
    // Update link selection

    editor.selected_link_indices.clear();
1211
1212
1213

    // Test for overlap against links

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1214
1215
1216
    for (int link_idx = 0; link_idx < editor.links.pool.size(); ++link_idx) {
        if (editor.links.in_use[link_idx]) {
            const LinkData& link = editor.links.pool[link_idx];
1217

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1218
1219
1220
1221
1222
1223
            const PinData& pin_start = editor.pins.pool[link.start_pin_idx];
            const PinData& pin_end = editor.pins.pool[link.end_pin_idx];
            const ImRect& node_start_rect =
              editor.nodes.pool[pin_start.parent_node_idx].rect;
            const ImRect& node_end_rect =
              editor.nodes.pool[pin_end.parent_node_idx].rect;
1224

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1225
1226
1227
1228
            const ImVec2 start = get_screen_space_pin_coordinates(
              node_start_rect, pin_start.attribute_rect, pin_start.type);
            const ImVec2 end = get_screen_space_pin_coordinates(
              node_end_rect, pin_end.attribute_rect, pin_end.type);
1229
1230

            // Test
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1231
1232
            if (rectangle_overlaps_link(box_rect, start, end, pin_start.type)) {
                editor.selected_link_indices.push_back(link_idx);
1233
1234
1235
1236
1237
            }
        }
    }
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1238
1239
void
translate_selected_nodes(EditorContext& editor)
1240
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1241
    if (g->left_mouse_dragging) {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1242
1243
1244
1245
1246
1247
1248
        const ImGuiIO& io = ImGui::GetIO();
        for (int i = 0; i < editor.selected_node_indices.size(); ++i) {
            const int node_idx = editor.selected_node_indices[i];
            NodeData& node = editor.nodes.pool[node_idx];
            if (node.draggable) {
                node.origin += io.MouseDelta;
            }
1249
1250
1251
1252
        }
    }
}

1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
OptionalIndex
find_duplicate_link(const EditorContext& editor,
                    const int start_pin_idx,
                    const int end_pin_idx)
{
    LinkData test_link(0);
    test_link.start_pin_idx = start_pin_idx;
    test_link.end_pin_idx = end_pin_idx;
    for (int link_idx = 0; link_idx < editor.links.pool.size(); ++link_idx) {
        const LinkData& link = editor.links.pool[link_idx];
        if (LinkPredicate()(test_link, link) && editor.links.in_use[link_idx]) {
            return OptionalIndex(link_idx);
        }
    }

    return OptionalIndex();
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1271
bool
1272
1273
1274
1275
should_link_snap_to_pin(const EditorContext& editor,
                        const PinData& start_pin,
                        const int hovered_pin_idx,
                        const OptionalIndex duplicate_link)
1276
{
1277
1278
1279
1280
    const PinData& end_pin = editor.pins.pool[hovered_pin_idx];

    // The end pin must be in a different node
    if (start_pin.parent_node_idx == end_pin.parent_node_idx) {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1281
1282
1283
        return false;
    }

1284
1285
    // The end pin must be of a different type
    if (start_pin.type == end_pin.type) {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1286
        return false;
1287
    }
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1288

1289
1290
1291
    // The link to be created must not be a duplicate, unless it is the link
    // which was created on snap. In that case we want to snap, since we want it
    // to appear visually as if the created link remains snapped to the pin.
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1292
    if (duplicate_link.has_value() && !(duplicate_link == g->snap_link_idx)) {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1293
1294
1295
1296
        return false;
    }

    return true;
1297
1298
}

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1299
1300
void
click_interaction_update(EditorContext& editor)
1301
{
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1302
1303
1304
    switch (editor.click_interaction_type) {
    case ClickInteractionType_BoxSelection: {
        ImRect& box_rect = editor.click_interaction_state.box_selector.rect;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1305
        box_rect.Max = g->mouse_pos;
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1306
1307
1308

        box_selector_update_selection(editor, box_rect);

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1309
1310
        const ImU32 box_selector_color =
          g->style.colors[ColorStyle_BoxSelector];
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1311
        const ImU32 box_selector_outline =
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1312
1313
          g->style.colors[ColorStyle_BoxSelectorOutline];
        g->canvas_draw_list->AddRectFilled(
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1314
          box_rect.Min, box_rect.Max, box_selector_color);
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1315
        g->canvas_draw_list->AddRect(
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1316
1317
          box_rect.Min, box_rect.Max, box_selector_outline);

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1318
        if (g->left_mouse_released) {
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
            ImVector<int>& depth_stack = editor.node_depth_order;
            const ImVector<int>& selected_idxs = editor.selected_node_indices;

            // Bump the selected node indices, in order, to the top of the depth
            // stack. NOTE: this algorithm has worst case time complexity of
            // O(N^2), if the node selection is ~ N (due to
            // selected_idxs.contains()).

            if ((selected_idxs.Size > 0) &&
                (selected_idxs.Size < depth_stack.Size)) {
                int num_moved = 0; // The number of indices moved. Stop after
                                   // selected_idxs.Size
                for (int i = 0; i < depth_stack.Size - selected_idxs.Size;
                     ++i) {
                    for (int node_idx = depth_stack[i];
                         selected_idxs.contains(node_idx);
                         node_idx = depth_stack[i]) {
                        depth_stack.erase(depth_stack.begin() +
                                          static_cast<size_t>(i));
                        depth_stack.push_back(node_idx);
                        ++num_moved;
                    }

                    if (num_moved == selected_idxs.Size) {
                        break;
                    }
                }
            }

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1348
            editor.click_interaction_type = ClickInteractionType_None;
1349
        }
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1350
1351
1352
    } break;
    case ClickInteractionType_Node: {
        translate_selected_nodes(editor);
1353

Gauthier Quesnel's avatar
Gauthier Quesnel committed
1354
        if (g->left_mouse_released) {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1355
            editor.click_interaction_type = ClickInteractionType_None;
1356
        }
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1357
1358
    } break;
    case ClickInteractionType_Link: {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1359
        if (g->left_mouse_released) {
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1360
1361
1362
1363
            editor.click_interaction_type = ClickInteractionType_None;
        }
    } break;
    case ClickInteractionType_LinkCreation: {
1364
        const PinData& start_pin =
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1365
1366
1367
          editor.pins
            .pool[editor.click_interaction_state.link_creation.start_pin_idx];

1368
        const OptionalIndex maybe_duplicate_link_idx =
Gauthier Quesnel's avatar
Gauthier Quesnel committed
1369
          g->hovered_pin_idx.has_value<