1 module creator.widgets.category;
2 import creator.core;
3 import creator.widgets;
4 import bindbc.imgui;
5 
6 private {
7     struct CategoryData {
8         bool open;
9         bool badColor;
10         bool isDark;
11         ImVec4 contentBounds;
12         IncCategoryFlags flags;
13     }
14 
15     void incGetCategoryColors(ImVec4 color, out ImVec4 hoverColor, out ImVec4 activeColor, out ImVec4 bgColor, out ImVec4 shadowColor, ref ImVec4 textColor, ref bool isDark) {
16         
17         // First get HSV version of color
18         float h, s, v;
19         hoverColor = color;
20         activeColor = color;
21         shadowColor = color;
22         bgColor = color;
23 
24         // Assume all colors are dark and invert them if we're in light mode
25         igColorConvertRGBtoHSV(color.x, color.y, color.z, &h, &s, &v);
26         isDark = v <= 0.5;
27         if (!isDark) textColor = ImVec4(0, 0, 0, 1);
28         else textColor = ImVec4(1, 1, 1, 1);
29 
30         // Then darken/lighten it the first time for active color
31         s -= 0.075*s;
32         if (v > 0.5) v -= 0.05;
33         else v += 0.15;
34         igColorConvertHSVtoRGB(h, s, v, &activeColor.x, &activeColor.y, &activeColor.z);
35 
36         // Then darken/lighten it the second time for hover color
37         s -= 0.075*s;
38         if (v > 0.5) v -= 0.05;
39         else v += 0.15;
40         igColorConvertHSVtoRGB(h, s, v, &hoverColor.x, &hoverColor.y, &hoverColor.z);
41         
42         // Then darken it for bg color
43         igColorConvertRGBtoHSV(color.x, color.y, color.z, &h, &s, &v);
44         s *= 0.90;
45         if (v <= 0.15) v = 0.15;
46         else v *= 0.80;
47         igColorConvertHSVtoRGB(h, s, v, &bgColor.x, &bgColor.y, &bgColor.z);
48     
49         // Finally the shadow color
50         igColorConvertRGBtoHSV(color.x, color.y, color.z, &h, &s, &v);
51         s *= 0.85;
52         v *= 0.75;
53         igColorConvertHSVtoRGB(h, s, v, &shadowColor.x, &shadowColor.y, &shadowColor.z);
54         shadowColor.w *= 2;
55     }
56 }
57 
58 enum IncCategoryFlags {
59     None = 0,
60     NoCollapse = 1
61 }
62 
63 /**
64     Begins a category using slightly darkened versions of the main UI colors
65     
66     Remember to call incEndCategory after!
67 */
68 bool incBeginCategory(const(char)* title, IncCategoryFlags flags = IncCategoryFlags.None) {
69     ImVec4 col = igGetStyle().Colors[ImGuiCol.WindowBg];
70     col = ImVec4(col.x-0.025, col.y-0.025, col.z-0.025, col.w);
71     return incBeginCategory(title, col, flags);
72 }
73 
74 /**
75     Begins a category using the defined color
76 
77     Remember to call incEndCategory after!
78 */
79 bool incBeginCategory(const(char)* title, ImVec4 color, IncCategoryFlags flags = IncCategoryFlags.None) {
80     import inmath : clamp;
81 
82     // We do not support transparency
83     color.w = 1;
84     color.x = clamp(color.x, 0.15, 1);
85     color.y = clamp(color.y, 0.15, 1);
86     color.z = clamp(color.z, 0.15, 1);
87     
88     // Calculate colors for category
89     ImVec4 hoverColor;
90     ImVec4 activeColor;
91     ImVec4 shadowColor;
92     ImVec4 bgColor;
93     ImVec4 textColor;
94     bool isDark;
95     incGetCategoryColors(color, hoverColor, activeColor, bgColor, shadowColor, textColor, isDark);
96 
97     // Push ID of our category
98     igPushID(title);
99     auto storage = igGetStateStorage();
100     auto id = igGetID("CATEGORYDATA");
101     auto window = igGetCurrentWindow();    
102 
103     // The fun stuff starts
104     igPushStyleColor(ImGuiCol.HeaderHovered, hoverColor);
105     igPushStyleColor(ImGuiCol.HeaderActive, activeColor);
106     igPushStyleColor(ImGuiCol.Text, textColor);
107     CategoryData* data = cast(CategoryData*)ImGuiStorage_GetVoidPtr(storage, id);
108     if (!data) {
109         data = cast(CategoryData*)igMemAlloc(CategoryData.sizeof);
110         data.open = false;
111         data.contentBounds = ImVec4(
112             0, 0, 0, 0
113         );
114         ImGuiStorage_SetVoidPtr(storage, id, data);
115     }
116 
117     // Calculate some values for drawing our background color.
118     data.flags = flags;
119     data.badColor = isDark != incGetDarkMode();
120     data.isDark = isDark;
121     data.contentBounds.x = igGetCursorPosX();
122     data.contentBounds.y = igGetCursorPosY();
123     data.contentBounds.z = incAvailableSpace().x;
124 
125     ImVec2 cursor;
126     igGetCursorScreenPos(&cursor);
127 
128     // Draw background color
129     ImDrawList_AddRectFilled(
130         igGetWindowDrawList(),
131         ImVec2(cursor.x, cursor.y),
132         ImVec2(cursor.x+data.contentBounds.z, cursor.y+data.contentBounds.w+1),
133         igGetColorU32(bgColor)
134     );
135     
136     // Draw "shadow" underneath
137     ImDrawList_AddRectFilled(
138         igGetWindowDrawList(),
139         ImVec2(cursor.x, cursor.y+data.contentBounds.w-1),
140         ImVec2(cursor.x+data.contentBounds.z, cursor.y+data.contentBounds.w+1),
141         igGetColorU32(shadowColor)
142     );
143 
144     // Our fancy tree node which will be used to open/close the category.
145     incDummy(ImVec2(0, 2));
146 
147     float paddingX = igGetStyle().WindowPadding.x/2;
148     window.ContentRegionRect.Min.x -= paddingX;
149     window.WorkRect.Min.x -= paddingX;
150     igSetCursorPosX(igGetCursorPosX()+paddingX);
151 
152     if ((data.flags & IncCategoryFlags.NoCollapse) == IncCategoryFlags.NoCollapse) {
153         data.open = true;
154         igIndent();
155             igText(title);
156         igUnindent();
157     } else data.open = igTreeNodeEx(title, ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.SpanAvailWidth);
158 
159     window.ContentRegionRect.Min.x += paddingX;
160     window.WorkRect.Min.x += paddingX;
161 
162     if (data.open) {
163         ImVec2 newCursor;
164         igGetCursorScreenPos(&newCursor);
165 
166         float diffY = newCursor.y-cursor.y;
167 
168         // Draw lighter fill color for contents when open
169         ImDrawList_AddRectFilled(
170             igGetWindowDrawList(),
171             ImVec2(newCursor.x+2, newCursor.y),
172             ImVec2(newCursor.x+data.contentBounds.z-2, newCursor.y+data.contentBounds.w-(diffY+1)),
173             igGetColorU32(color)
174         );
175     }
176     
177     // NOTE: We use these instead of incDummy since otherwise you can't drag
178     // the tree node via drag/drop.
179     igSetCursorPosY(igGetCursorPosY()+2);
180 
181     if (data.open) {
182         igSetCursorPosY(igGetCursorPosY()+2);
183         igIndent();
184     }
185 
186 
187     igPopStyleColor(3);
188 
189     // We force our childrens' content to fit better within ourselves
190     // This gets undone after incEndCategory is called.
191     float indentSpacing = igGetStyle().IndentSpacing;
192     window.ContentRegionRect.Min.x -= indentSpacing;
193     window.WorkRect.Min.x -= indentSpacing;
194 
195     if (data.badColor && data.isDark) incPushDarkColorScheme();
196     else if (data.badColor && !data.isDark) incPushLightColorScheme();
197     
198 
199     return data.open;
200 }
201 
202 /**
203     Ends a category
204 */
205 void incEndCategory() {
206     auto window = igGetCurrentWindow();
207 
208     // Undo our fancy content fit math.
209     // This needs to be here otherwise the usable size of the child
210     // window will continue to shrink
211     window.ContentRegionRect.Min.x += igGetStyle().IndentSpacing;
212     window.WorkRect.Min.x += igGetStyle().IndentSpacing;
213 
214     auto storage = igGetStateStorage();
215     auto id = igGetID("CATEGORYDATA");
216     if (CategoryData* data = cast(CategoryData*)ImGuiStorage_GetVoidPtr(storage, id)) {
217         if (data.badColor) incPopColorScheme();
218         if (data.open) {
219             igUnindent();
220             incDummy(ImVec2(0, 2));
221         }
222 
223         data.contentBounds.w = igGetCursorPosY()-data.contentBounds.y;
224 
225         igSpacing();
226         igSpacing();
227     }
228 
229     igPopID();
230 }