1 /*
2     Copyright © 2020, Inochi2D Project
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4     
5     Authors: Luna Nielsen
6 */
7 module creator.panels.parameters;
8 import creator.viewport.model.deform;
9 import creator.panels;
10 import creator.widgets;
11 import creator.windows;
12 import creator.core;
13 import creator;
14 import std.string;
15 import inochi2d;
16 import i18n;
17 import std.uni : toLower;
18 import std.stdio;
19 import std.algorithm;
20 import creator.utils;
21 
22 private {
23     ParameterBinding[][Node] cParamBindingEntries;
24     ParameterBinding[][Node] cParamBindingEntriesAll;
25     Node[] cAllBoundNodes;
26     ParameterBinding[BindTarget] cSelectedBindings;
27     Node[] cCompatibleNodes;
28     vec2u cParamPoint;
29     vec2u cClipboardPoint;
30     ParameterBinding[BindTarget] cClipboardBindings;
31 
32     void refreshBindingList(Parameter param) {
33         // Filter selection to remove anything that went away
34         ParameterBinding[BindTarget] newSelectedBindings;
35 
36         cParamBindingEntriesAll.clear();
37         foreach(ParameterBinding binding; param.bindings) {
38             BindTarget target = binding.getTarget();
39             if (target in cSelectedBindings) newSelectedBindings[target] = binding;
40             cParamBindingEntriesAll[binding.getNode()] ~= binding;
41         }
42         cAllBoundNodes = cParamBindingEntriesAll.keys.dup;
43         cAllBoundNodes.sort!((x, y) => x.name < y.name);
44         cSelectedBindings = newSelectedBindings;
45         paramPointChanged(param);
46     }
47 
48     void paramPointChanged(Parameter param) {
49         cParamBindingEntries.clear();
50 
51         cParamPoint = param.findClosestKeypoint();
52         foreach(ParameterBinding binding; param.bindings) {
53             if (binding.isSet(cParamPoint)) {
54                 cParamBindingEntries[binding.getNode()] ~= binding;
55             }
56         }
57     }
58 
59     void mirrorAll(Parameter param, uint axis) {
60         foreach(ParameterBinding binding; param.bindings) {
61             uint xCount = param.axisPointCount(0);
62             uint yCount = param.axisPointCount(1);
63             foreach(x; 0..xCount) {
64                 foreach(y; 0..yCount) {
65                     vec2u index = vec2u(x, y);
66                     if (binding.isSet(index)) {
67                         binding.scaleValueAt(index, axis, -1);
68                     }
69                 }
70             }
71         }
72     }
73 
74     void mirroredAutofill(Parameter param, uint axis, float min, float max) {
75         foreach(ParameterBinding binding; param.bindings) {
76             uint xCount = param.axisPointCount(0);
77             uint yCount = param.axisPointCount(1);
78             foreach(x; 0..xCount) {
79                 float offX = param.axisPoints[0][x];
80                 if (axis == 0 && (offX < min || offX > max)) continue;
81                 foreach(y; 0..yCount) {
82                     float offY = param.axisPoints[1][x];
83                     if (axis == 1 && (offY < min || offY > max)) continue;
84 
85                     vec2u index = vec2u(x, y);
86                     if (!binding.isSet(index)) binding.extrapolateValueAt(index, axis);
87                 }
88             }
89         }
90     }
91 
92     void fixScales(Parameter param) {
93         foreach(ParameterBinding binding; param.bindings) {
94             switch(binding.getName()) {
95                 case "transform.s.x":
96                 case "transform.s.y":
97                 if (ValueParameterBinding b = cast(ValueParameterBinding)binding) {
98                     uint xCount = param.axisPointCount(0);
99                     uint yCount = param.axisPointCount(1);
100                     foreach(x; 0..xCount) {
101                         foreach(y; 0..yCount) {
102                             vec2u index = vec2u(x, y);
103                             if (b.isSet(index)) {
104                                 b.values[x][y] += 1;
105                             }
106                         }
107                     }
108                     b.reInterpolate();
109                 }
110                 break;
111                 default: break;
112             }
113         }
114     }
115 
116     Node[] getCompatibleNodes() {
117         Node thisNode = null;
118 
119         foreach(binding; cSelectedBindings.byValue()) {
120             if (thisNode is null) thisNode = binding.getNode();
121             else if (!(binding.getNode() is thisNode)) return null;
122         }
123         if (thisNode is null) return null;
124 
125         Node[] compatible;
126         nodeLoop: foreach(otherNode; cParamBindingEntriesAll.byKey()) {
127             if (otherNode is thisNode) continue;
128 
129             foreach(binding; cSelectedBindings.byValue()) {
130                 if (!binding.isCompatibleWithNode(otherNode))
131                     continue nodeLoop;
132             }
133             compatible ~= otherNode;
134         }
135 
136         return compatible;
137     }
138 
139     void copySelectionToNode(Parameter param, Node target) {
140         Node src = cSelectedBindings.keys[0].node;
141 
142         foreach(binding; cSelectedBindings.byValue()) {
143             assert(binding.getNode() is src, "selection mismatch");
144 
145             ParameterBinding b = param.getOrAddBinding(target, binding.getName());
146             binding.copyKeypointToBinding(cParamPoint, b, cParamPoint);
147         }
148 
149         refreshBindingList(param);
150     }
151 
152     void swapSelectionWithNode(Parameter param, Node target) {
153         Node src = cSelectedBindings.keys[0].node;
154 
155         foreach(binding; cSelectedBindings.byValue()) {
156             assert(binding.getNode() is src, "selection mismatch");
157 
158             ParameterBinding b = param.getOrAddBinding(target, binding.getName());
159             binding.swapKeypointWithBinding(cParamPoint, b, cParamPoint);
160         }
161 
162         refreshBindingList(param);
163     }
164 
165     void keypointActions(Parameter param, ParameterBinding[] bindings) {
166         if (igMenuItem(__("Unset"), "", false, true)) {
167             foreach(binding; bindings) {
168                 binding.unset(cParamPoint);
169             }
170             incViewportNodeDeformNotifyParamValueChanged();
171         }
172         if (igMenuItem(__("Set to current"), "", false, true)) {
173             foreach(binding; bindings) {
174                 binding.setCurrent(cParamPoint);
175             }
176             incViewportNodeDeformNotifyParamValueChanged();
177         }
178         if (igMenuItem(__("Reset"), "", false, true)) {
179             foreach(binding; bindings) {
180                 binding.reset(cParamPoint);
181             }
182             incViewportNodeDeformNotifyParamValueChanged();
183         }
184         if (igMenuItem(__("Invert"), "", false, true)) {
185             foreach(binding; bindings) {
186                 binding.scaleValueAt(cParamPoint, -1, -1);
187             }
188             incViewportNodeDeformNotifyParamValueChanged();
189         }
190         if (igBeginMenu(__("Mirror"), true)) {
191             if (igMenuItem(__("Horizontally"), "", false, true)) {
192                 foreach(binding; bindings) {
193                     binding.scaleValueAt(cParamPoint, 0, -1);
194                 }
195                 incViewportNodeDeformNotifyParamValueChanged();
196             }
197             if (igMenuItem(__("Vertically"), "", false, true)) {
198                 foreach(binding; bindings) {
199                     binding.scaleValueAt(cParamPoint, 1, -1);
200                 }
201                 incViewportNodeDeformNotifyParamValueChanged();
202             }
203             igEndMenu();
204         }
205         if (param.isVec2) {
206             if (igBeginMenu(__("Set from mirror"), true)) {
207                 if (igMenuItem(__("Horizontally"), "", false, true)) {
208                     foreach(binding; bindings) {
209                         binding.extrapolateValueAt(cParamPoint, 0);
210                     }
211                     incViewportNodeDeformNotifyParamValueChanged();
212                 }
213                 if (igMenuItem(__("Vertically"), "", false, true)) {
214                     foreach(binding; bindings) {
215                         binding.extrapolateValueAt(cParamPoint, 1);
216                     }
217                     incViewportNodeDeformNotifyParamValueChanged();
218                 }
219                 if (igMenuItem(__("Diagonally"), "", false, true)) {
220                     foreach(binding; bindings) {
221                         binding.extrapolateValueAt(cParamPoint, -1);
222                     }
223                     incViewportNodeDeformNotifyParamValueChanged();
224                 }
225                 igEndMenu();
226             }
227         } else {
228             if (igMenuItem(__("Set from mirror"), "", false, true)) {
229                 foreach(binding; bindings) {
230                     binding.extrapolateValueAt(cParamPoint, 0);
231                 }
232                 incViewportNodeDeformNotifyParamValueChanged();
233             }
234         }
235 
236         if (igMenuItem(__("Copy"), "", false, true)) {
237             cClipboardPoint = cParamPoint;
238             cClipboardBindings.clear();
239             foreach(binding; bindings) {
240                 cClipboardBindings[binding.getTarget()] = binding;
241             }
242         }
243 
244         if (igMenuItem(__("Paste"), "", false,  true)) {
245             if (bindings.length == 1 && cClipboardBindings.length == 1 &&
246                 bindings[0].isCompatibleWithNode(cClipboardBindings.values[0].getNode())) {
247                 cClipboardBindings.values[0].copyKeypointToBinding(cClipboardPoint, bindings[0], cParamPoint);
248             } else {
249                 foreach(binding; bindings) {
250                     if (binding.getTarget() in cClipboardBindings) {
251                         ParameterBinding origBinding = cClipboardBindings[binding.getTarget()];
252                         origBinding.copyKeypointToBinding(cClipboardPoint, binding, cParamPoint);
253                     }
254                 }
255             }
256         }
257 
258     }
259 
260     void bindingList(Parameter param) {
261         if (!igCollapsingHeader(__("Bindings"), ImGuiTreeNodeFlags.DefaultOpen)) return;
262 
263         refreshBindingList(param);
264 
265         auto io = igGetIO();
266         auto style = igGetStyle();
267         ImS32 inactiveColor = igGetColorU32(style.Colors[ImGuiCol.TextDisabled]);
268 
269         igBeginChild("BindingList", ImVec2(0, 256), false);
270             igPushStyleVar(ImGuiStyleVar.CellPadding, ImVec2(4, 1));
271             igPushStyleVar(ImGuiStyleVar.IndentSpacing, 14);
272 
273             foreach(node; cAllBoundNodes) {
274                 ParameterBinding[] allBindings = cParamBindingEntriesAll[node];
275                 ParameterBinding[] *bindings = (node in cParamBindingEntries);
276 
277                 // Figure out if node is selected ( == all bindings selected)
278                 bool nodeSelected = true;
279                 bool someSelected = false;
280                 foreach(binding; allBindings) {
281                     if ((binding.getTarget() in cSelectedBindings) is null)
282                         nodeSelected = false;
283                     else
284                         someSelected = true;
285                 }
286 
287                 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.OpenOnArrow;
288                 if (nodeSelected)
289                     flags |= ImGuiTreeNodeFlags.Selected;
290 
291                 if (bindings is null) igPushStyleColor(ImGuiCol.Text, inactiveColor);
292                 string nodeName = incTypeIdToIconConcat(node.typeId) ~ " " ~ node.name;
293                 if (igTreeNodeEx(cast(void*)node.uuid, flags, nodeName.toStringz)) {
294                     if (bindings is null) igPopStyleColor();
295                     if (igBeginPopup("###BindingPopup")) {
296                         if (igMenuItem(__("Remove"), "", false, true)) {
297                             foreach(binding; cSelectedBindings.byValue()) {
298                                 param.removeBinding(binding);
299                             }
300                             incViewportNodeDeformNotifyParamValueChanged();
301                         }
302 
303                         keypointActions(param, cSelectedBindings.values);
304 
305                         if (igBeginMenu(__("Interpolation Mode"), true)) {
306                             if (igMenuItem(__("Nearest"), "", false, true)) {
307                                 foreach(binding; cSelectedBindings.values) {
308                                     binding.interpolateMode = InterpolateMode.Nearest;
309                                 }
310                                 incViewportNodeDeformNotifyParamValueChanged();
311                             }
312                             if (igMenuItem(__("Linear"), "", false, true)) {
313                                 foreach(binding; cSelectedBindings.values) {
314                                     binding.interpolateMode = InterpolateMode.Linear;
315                                 }
316                                 incViewportNodeDeformNotifyParamValueChanged();
317                             }
318                             igEndMenu();
319                         }
320 
321                         bool haveCompatible = cCompatibleNodes.length > 0;
322                         if (igBeginMenu(__("Copy to"), haveCompatible)) {
323                             foreach(cNode; cCompatibleNodes) {
324                                 if (igMenuItem(cNode.name.toStringz, "", false, true)) {
325                                     copySelectionToNode(param, cNode);
326                                 }
327                             }
328                             igEndMenu();
329                         }
330                         if (igBeginMenu(__("Swap with"), haveCompatible)) {
331                             foreach(cNode; cCompatibleNodes) {
332                                 if (igMenuItem(cNode.name.toStringz, "", false, true)) {
333                                     swapSelectionWithNode(param, cNode);
334                                 }
335                             }
336                             igEndMenu();
337                         }
338 
339                         igEndPopup();
340                     }
341                     if (igIsItemClicked(ImGuiMouseButton.Right)) {
342                         if (!someSelected) {
343                             cSelectedBindings.clear();
344                             foreach(binding; allBindings) {
345                                 cSelectedBindings[binding.getTarget()] = binding;
346                             }
347                         }
348                         cCompatibleNodes = getCompatibleNodes();
349                         igOpenPopup("###BindingPopup");
350                     }
351 
352                     // Node selection logic
353                     if (igIsItemClicked(ImGuiMouseButton.Left) && !igIsItemToggledOpen()) {
354                         
355                         // Select the node you've clicked in the bindings list
356                         if (incNodeInSelection(node)) {
357                             incFocusCamera(node);
358                         } else incSelectNode(node);
359                         
360                         if (!io.KeyCtrl) {
361                             cSelectedBindings.clear();
362                             nodeSelected = false;
363                         }
364                         foreach(binding; allBindings) {
365                             if (nodeSelected) cSelectedBindings.remove(binding.getTarget());
366                             else cSelectedBindings[binding.getTarget()] = binding;
367                         }
368                     }
369                     foreach(binding; allBindings) {
370                         ImGuiTreeNodeFlags flags2 =
371                             ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.OpenOnArrow |
372                             ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen;
373 
374                         bool selected = cast(bool)(binding.getTarget() in cSelectedBindings);
375                         if (selected) flags2 |= ImGuiTreeNodeFlags.Selected;
376 
377                         // Style as inactive if not set at this keypoint
378                         if (!binding.isSet(cParamPoint))
379                             igPushStyleColor(ImGuiCol.Text, inactiveColor);
380 
381                         // Binding entry
382                         auto value = cast(ValueParameterBinding)binding;
383                         string label;
384                         if (value && binding.isSet(cParamPoint)) {
385                             label = format("%s (%.02f)", binding.getName(), value.getValue(cParamPoint));
386                         } else {
387                             label = binding.getName();
388                         }
389                         igTreeNodeEx("binding", flags2, label.toStringz);
390                         if (!binding.isSet(cParamPoint)) igPopStyleColor();
391 
392                         // Binding selection logic
393                         if (igIsItemClicked(ImGuiMouseButton.Right)) {
394                             if (!selected) {
395                                 cSelectedBindings.clear();
396                                 cSelectedBindings[binding.getTarget()] = binding;
397                             }
398                             cCompatibleNodes = getCompatibleNodes();
399                             igOpenPopup("###BindingPopup");
400                         }
401                         if (igIsItemClicked(ImGuiMouseButton.Left)) {
402                             if (!io.KeyCtrl) {
403                                 cSelectedBindings.clear();
404                                 selected = false;
405                             }
406                             if (selected) cSelectedBindings.remove(binding.getTarget());
407                             else cSelectedBindings[binding.getTarget()] = binding;
408                         }
409                     }
410                     igTreePop();
411                 } else if (bindings is null) igPopStyleColor();
412             }
413             igPopStyleVar();
414             igPopStyleVar();
415         igEndChild();
416     }
417 
418 }
419 
420 /**
421     Generates a parameter view
422 */
423 void incParameterView(Parameter param, string* grabParam) {
424     bool open = igCollapsingHeader(param.name.toStringz, ImGuiTreeNodeFlags.DefaultOpen);
425     if(igBeginDragDropSource(ImGuiDragDropFlags.SourceAllowNullID)) {
426         igSetDragDropPayload("_PARAMETER", cast(void*)&param, (&param).sizeof, ImGuiCond.Always);
427         igText(param.name.toStringz);
428         igEndDragDropSource();
429     }
430 
431     if (!open) return;
432     igIndent();
433         igPushID(cast(void*)param);
434 
435             float reqSpace = param.isVec2 ? 144 : 52;
436 
437             // Parameter Control
438             ImVec2 avail = incAvailableSpace();
439 
440             if (igBeginChild("###PARAM", ImVec2(avail.x-24, reqSpace))) {
441                 // Popup for rightclicking the controller
442                 if (igBeginPopup("###ControlPopup")) {
443                     if (incArmedParameter() == param) {
444                         keypointActions(param, param.bindings);
445                     }
446                     igEndPopup();
447                 }
448 
449                 if (param.isVec2) igText("%.2f %.2f", param.value.x, param.value.y);
450                 else igText("%.2f", param.value.x);
451 
452                 if (incController("###CONTROLLER", param, ImVec2(avail.x-18, reqSpace-24), incArmedParameter() == param, *grabParam)) {
453                     if (incArmedParameter() == param) {
454                         incViewportNodeDeformNotifyParamValueChanged();
455                         paramPointChanged(param);
456                     }
457                     if (igIsMouseDown(ImGuiMouseButton.Left)) {
458                         if (*grabParam == null)
459                             *grabParam = param.name;
460                     } else {
461                         *grabParam = "";
462                     }
463                 }
464                 if (igIsItemClicked(ImGuiMouseButton.Right)) {
465                     if (incArmedParameter() == param) incViewportNodeDeformNotifyParamValueChanged();
466                     refreshBindingList(param);
467                     igOpenPopup("###ControlPopup");
468                 }
469             }
470             igEndChild();
471 
472 
473             if (incEditMode == EditMode.ModelEdit) {
474                 igSameLine(0, 0);
475                 // Parameter Setting Buttons
476                 if(igBeginChild("###SETTING", ImVec2(avail.x-24, reqSpace))) {
477                     if (igBeginPopup("###EditParam")) {
478                         if (igMenuItem(__("Edit Properties"), "", false, true)) {
479                             incPushWindowList(new ParamPropWindow(param));
480                         }
481                         
482                         if (igMenuItem(__("Edit Axes Points"), "", false, true)) {
483                             incPushWindowList(new ParamAxesWindow(param));
484                         }
485 
486                         if (param.isVec2) {
487                             if (igMenuItem(__("Flip X"), "", false, true)) {
488                                 param.reverseAxis(0);
489                             }
490                             if (igMenuItem(__("Flip Y"), "", false, true)) {
491                                 param.reverseAxis(1);
492                             }
493                         } else {
494                             if (igMenuItem(__("Flip"), "", false, true)) {
495                                 param.reverseAxis(0);
496                             }
497                         }
498                         if (igBeginMenu(__("Mirror"), true)) {
499                             if (igMenuItem(__("Horizontally"), "", false, true)) {
500                                 mirrorAll(param, 0);
501                                 incViewportNodeDeformNotifyParamValueChanged();
502                             }
503                             if (igMenuItem(__("Vertically"), "", false, true)) {
504                                 mirrorAll(param, 1);
505                                 incViewportNodeDeformNotifyParamValueChanged();
506                             }
507                             igEndMenu();
508                         }
509                         if (igBeginMenu(__("Mirrored Autofill"), true)) {
510                             if (igMenuItem(__(""), "", false, true)) {
511                                 mirroredAutofill(param, 0, 0, 0.4999);
512                                 incViewportNodeDeformNotifyParamValueChanged();
513                             }
514                             if (igMenuItem(__(""), "", false, true)) {
515                                 mirroredAutofill(param, 0, 0.5001, 1);
516                                 incViewportNodeDeformNotifyParamValueChanged();
517                             }
518                             if (param.isVec2) {
519                                 if (igMenuItem(__(""), "", false, true)) {
520                                     mirroredAutofill(param, 1, 0, 0.4999);
521                                     incViewportNodeDeformNotifyParamValueChanged();
522                                 }
523                                 if (igMenuItem(__(""), "", false, true)) {
524                                     mirroredAutofill(param, 1, 0.5001, 1);
525                                     incViewportNodeDeformNotifyParamValueChanged();
526                                 }
527                             }
528                             igEndMenu();
529                         }
530 
531                         igNewLine();
532                         igSeparator();
533 
534                         if (igMenuItem(__("Duplicate"), "", false, true)) {
535                             Parameter newParam = param.dup;
536                             incActivePuppet().parameters ~= newParam;
537                         }
538 
539                         if (igMenuItem(__("Delete"), "", false, true)) {
540                             if (incArmedParameter() == param) {
541                                 incDisarmParameter();
542                             }
543                             incActivePuppet().removeParameter(param);
544                         }
545 
546                         igNewLine();
547                         igSeparator();
548 
549                         // Sets the default value of the param
550                         if (igMenuItem(__("Set Starting Position"), "", false, true)) {
551                             param.defaults = param.value;
552                         }
553                         igEndPopup();
554                     }
555                     
556                     if (igButton("", ImVec2(24, 24))) {
557                         igOpenPopup("###EditParam");
558                     }
559                     
560                     
561                     if (incButtonColored("", ImVec2(24, 24), incArmedParameter() == param ? ImVec4(1f, 0f, 0f, 1f) : *igGetStyleColorVec4(ImGuiCol.Text))) {
562                         if (incArmedParameter() == param) {
563                             incDisarmParameter();
564                         } else {
565                             param.value = param.getClosestKeypointValue();
566                             paramPointChanged(param);
567                             incArmParameter(param);
568                         }
569                     }
570 
571                     // Arms the parameter for recording values.
572                     incTooltip(_("Arm Parameter"));
573                 }
574                 igEndChild();
575             }
576             if (incArmedParameter() == param) {
577                 bindingList(param);
578             }
579         igPopID();
580     igUnindent();
581 }
582 
583 /**
584     The logger frame
585 */
586 class ParametersPanel : Panel {
587 private:
588     string filter;
589     string grabParam = "";
590 protected:
591     override
592     void onUpdate() {
593         auto parameters = incActivePuppet().parameters;
594 
595         if (igBeginPopup("###AddParameter")) {
596             if (igMenuItem(__("Add 1D Parameter (0..1)"), "", false, true)) {
597                 Parameter param = new Parameter(
598                     "Param #%d\0".format(parameters.length),
599                     false
600                 );
601                 incActivePuppet().parameters ~= param;
602             }
603             if (igMenuItem(__("Add 1D Parameter (-1..1)"), "", false, true)) {
604                 Parameter param = new Parameter(
605                     "Param #%d\0".format(parameters.length),
606                     false
607                 );
608                 param.min.x = -1;
609                 param.max.x = 1;
610                 param.insertAxisPoint(0, 0.5);
611                 incActivePuppet().parameters ~= param;
612             }
613             if (igMenuItem(__("Add 2D Parameter (0..1)"), "", false, true)) {
614                 Parameter param = new Parameter(
615                     "Param #%d\0".format(parameters.length),
616                     true
617                 );
618                 incActivePuppet().parameters ~= param;
619             }
620             if (igMenuItem(__("Add 2D Parameter (-1..+1)"), "", false, true)) {
621                 Parameter param = new Parameter(
622                     "Param #%d\0".format(parameters.length),
623                     true
624                 );
625                 param.min = vec2(-1, -1);
626                 param.max = vec2(1, 1);
627                 param.insertAxisPoint(0, 0.5);
628                 param.insertAxisPoint(1, 0.5);
629                 incActivePuppet().parameters ~= param;
630             }
631             if (igMenuItem(__("Add Mouth Shape"), "", false, true)) {
632                 Parameter param = new Parameter(
633                     "Mouth #%d\0".format(parameters.length),
634                     true
635                 );
636                 param.min = vec2(-1, 0);
637                 param.max = vec2(1, 1);
638                 param.insertAxisPoint(0, 0.25);
639                 param.insertAxisPoint(0, 0.5);
640                 param.insertAxisPoint(0, 0.6);
641                 param.insertAxisPoint(1, 0.3);
642                 param.insertAxisPoint(1, 0.5);
643                 param.insertAxisPoint(1, 0.6);
644                 incActivePuppet().parameters ~= param;
645             }
646             igEndPopup();
647         }
648         if (igBeginChild("###FILTER", ImVec2(0, 32))) {
649             if (incInputText("Filter", filter)) {
650                 filter = filter.toLower;
651             }
652             incTooltip(_("Filter, search for specific parameters"));
653         }
654         igEndChild();
655 
656         if (igBeginChild("ParametersList", ImVec2(0, -36))) {
657             
658             // Always render the currently armed parameter on top
659             if (incArmedParameter()) {
660                 incParameterView(incArmedParameter(), &grabParam);
661             }
662 
663             // Render other parameters
664             foreach(ref param; parameters) {
665                 if (incArmedParameter() == param) continue;
666                 import std.algorithm.searching : canFind;
667                 if (filter.length == 0 || param.indexableName.canFind(filter)) {
668                     incParameterView(param, &grabParam);
669                 }
670             }
671         }
672         igEndChild();
673 
674         // Right align add button
675         ImVec2 avail = incAvailableSpace();
676         incDummy(ImVec2(avail.x-32, 32));
677         igSameLine(0, 0);
678 
679         // Add button
680         if (igButton("", ImVec2(32, 32))) {
681             igOpenPopup("###AddParameter");
682         }
683         incTooltip(_("Add Parameter"));
684     }
685 
686 public:
687     this() {
688         super("Parameters", _("Parameters"), false);
689     }
690 }
691 
692 /**
693     Generate logger frame
694 */
695 mixin incPanel!ParametersPanel;
696 
697