-
-
Notifications
You must be signed in to change notification settings - Fork 588
Expand file tree
/
Copy pathCustomDropDown.cs
More file actions
322 lines (270 loc) · 10.3 KB
/
CustomDropDown.cs
File metadata and controls
322 lines (270 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
using System.Collections.Generic;
using Godot;
/// <summary>
/// A custom dropdown implemented through MenuButton, with extra popup menu functionality
/// such as some slide down animation.
/// </summary>
/// <remarks>
/// <para>
/// In Godot 4 it is possible to set dropdown icon size now so this just has the animation and some data helpers.
/// </para>
/// </remarks>
public partial class CustomDropDown : MenuButton
{
#pragma warning disable CA2213
/// <summary>
/// The MenuButton's popup menu
/// </summary>
public readonly PopupMenu Popup;
#pragma warning restore CA2213
private readonly StringName vSeparationReference = new("v_separation");
private readonly NodePath themeVSeparationReference = new("theme_override_constants/v_separation");
private readonly float cachedPopupVSeparation;
private readonly float fontHeight;
private readonly float contentMarginTop;
private readonly Dictionary<string, List<Item>> items = new();
/// <summary>
/// All item icon sizes will be adjusted according to this. Currently, it's automatically
/// set according to the PopupMenu's check icon size (with a bit smaller result)
/// </summary>
private int iconMaxWidth;
public CustomDropDown()
{
Popup = GetPopup();
cachedPopupVSeparation = Popup.GetThemeConstant(vSeparationReference);
fontHeight = Popup.GetThemeFont("font").GetHeight(Popup.GetThemeFontSize("font_size"));
contentMarginTop = Popup.GetThemeStylebox("panel").ContentMarginTop;
var checkSize = Popup.GetThemeIcon("checked").GetSize();
// Set the custom icon size
iconMaxWidth = (int)(checkSize.X - 2);
ClipContents = true;
Connect(MenuButton.SignalName.AboutToPopup, new Callable(this, nameof(OnPopupAboutToShow)));
}
public override void _Draw()
{
ReadjustRectSizes();
}
public void AddItemSection(string name)
{
if (!items.ContainsKey(name))
items.Add(name, new List<Item>());
}
/// <summary>
/// Helper for adding an item into the items dictionary. This does not add the item into the PopupMenu,
/// for that see <see cref="CreateElements"/>.
/// </summary>
/// <returns>
/// The CustomDropDown's own Item class. All custom operations relating to the dropdown uses this.
/// </returns>
public Item AddItem(string text, bool checkable, Color color, Texture2D? icon = null,
string section = "default")
{
if (!items.ContainsKey(section))
{
AddItemSection(section);
items[section].Add(new Item { Text = section, Separator = true });
}
var item = new Item
{
Text = text,
Icon = icon,
Color = color,
Checkable = checkable,
};
items[section].Add(item);
return item;
}
/// <summary>
/// Helper for clearing the items in the dictionary.
/// </summary>
/// <remarks>
/// <para>
/// This method doesn't cause rebuild of the popup.
/// </para>
/// </remarks>
public void ClearAllItems()
{
items.Clear();
}
/// <summary>
/// Returns the index of an item containing the given name/text in a section.
/// </summary>
/// <param name="name">The item text</param>
/// <param name="section">The item section where to search the item index for</param>
/// <returns>Item's index. -1 if not found</returns>
public int GetItemIndex(string name, string section)
{
if (!items.TryGetValue(section, out var sectionItems))
{
GD.PrintErr("No section found with name ", section);
return -1;
}
foreach (var item in sectionItems)
{
if (item.Text == name)
return Popup.GetItemIndex(item.Id) + 1;
}
return -1;
}
/// <summary>
/// Returns the index of an item containing the given name/text in all section.
/// </summary>
/// <param name="name">The item text</param>
/// <returns>
/// List of item's index as this takes into account all exact name occurrences in all sections.
/// Empty list if not found.
/// </returns>
public List<int> GetItemIndex(string name)
{
var result = new List<int>();
foreach (var section in items)
{
foreach (var item in section.Value)
{
if (item.Text == name)
result.Add(Popup.GetItemIndex(item.Id) + 1);
}
}
return result;
}
/// <summary>
/// Retrieves all items from dictionary and instantiates them into <see cref="Popup"/>.
/// </summary>
public void CreateElements()
{
Popup.Clear();
var id = 0;
foreach (var section in items)
{
foreach (var item in section.Value)
{
if (item.Text == "default" && item.Separator)
continue;
item.Id = id++;
if (item.Icon != null)
{
if (item.Checkable)
{
// Use a pre-tinted icon so the species color is applied only to the custom icon
// while the radio/checkbox indicator (selection control) keeps theme colors
var tintedIcon = item.PreTintedIcon ??= CreateTintedIcon(item.Icon, item.Color);
Popup.AddIconCheckItem(tintedIcon, item.Text, id);
var index = Popup.GetItemIndex(id);
Popup.SetItemIconMaxWidth(index, iconMaxWidth);
// Don't apply color modulation to checkable items as it affects the radio button icon
// The species color should only affect the custom icon, not the selection radio button
Popup.SetItemChecked(index, item.Checked);
}
else
{
Popup.AddIconItem(item.Icon, item.Text, id);
var index = Popup.GetItemIndex(id);
Popup.SetItemIconMaxWidth(index, iconMaxWidth);
Popup.SetItemIconModulate(index, item.Color);
Popup.SetItemAsSeparator(index, item.Separator);
}
}
else
{
if (item.Checkable)
{
Popup.AddCheckItem(item.Text, id);
Popup.SetItemChecked(Popup.GetItemIndex(id), item.Checked);
}
else
{
Popup.AddItem(item.Text, id);
Popup.SetItemAsSeparator(Popup.GetItemIndex(id), item.Separator);
}
}
}
}
ReadjustRectSizes();
}
/// <summary>
/// Creates a tinted copy of the provided icon by multiplying its pixels with the given color.
/// This bakes the color into the texture so later UI modulation won't affect selection indicators.
/// </summary>
/// <param name="icon">Base icon texture to tint</param>
/// <param name="tint">Tint color to apply</param>
/// <returns>New texture containing the tinted icon</returns>
private static Texture2D CreateTintedIcon(Texture2D icon, Color tint)
{
// Get a mutable image copy of the original texture
var image = icon.GetImage();
if (image == null)
return icon;
// Ensure a format with alpha channel to retain transparency
if (image.GetFormat() != Image.Format.Rgba8 && image.GetFormat() != Image.Format.Rgb8)
{
image.Convert(Image.Format.Rgba8);
}
var width = image.GetWidth();
var height = image.GetHeight();
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var px = image.GetPixel(x, y);
// Multiply RGB by tint, alpha by tint alpha to allow dimming if needed
var tinted = new Color(
px.R * tint.R,
px.G * tint.G,
px.B * tint.B,
px.A * tint.A);
image.SetPixel(x, y, tinted);
}
}
// Create a texture from the modified image
return ImageTexture.CreateFromImage(image);
}
/// <summary>
/// This re-adjust the rect size of this MenuButton and its PopupMenu. Called when they are updated.
/// </summary>
private void ReadjustRectSizes()
{
// Adjust the menu button to have the same length as the popup
CustomMinimumSize = new Vector2(Popup.GetContentsMinimumSize().X + iconMaxWidth + 6, CustomMinimumSize.Y);
// Set popup to minimum length
Popup.Size = new Vector2I(MathUtils.RoundToInt(Size.X), 0);
}
private void OnPopupAboutToShow()
{
Popup.AddThemeConstantOverride(vSeparationReference, -14);
// Animate slide down
var tween = CreateTween();
tween.SetTrans(Tween.TransitionType.Cubic);
tween.SetEase(Tween.EaseType.Out);
tween.TweenProperty(Popup, themeVSeparationReference, cachedPopupVSeparation, 0.1).From(-14);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
vSeparationReference.Dispose();
themeVSeparationReference.Dispose();
}
base.Dispose(disposing);
}
/// <summary>
/// Helper data regarding the popup menu item. All custom operations relating to the dropdown uses this,
/// we can't utilize PopupMenu's internal item class since it's not exposed to the user.
/// </summary>
/// <remarks>
/// <para>
/// NOTE: Fields may not always be updated, especially if the user bypass custom methods and directly
/// change the internal items by using methods in <see cref="Popup"/>.
/// </para>
/// </remarks>
public class Item
{
public string Text = string.Empty;
public Texture2D? Icon;
public Color Color;
public bool Checkable;
public bool Checked;
public int Id;
public bool Separator;
public Texture2D? PreTintedIcon;
}
}