OXYGENHUB
◆ main openmsx / src / imgui / ImGuiUtils.hh
17.0 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#ifndef IMGUI_UTILS_HH
2
#define IMGUI_UTILS_HH
3
4
#include "ImGuiCpp.hh"
5
6
#include "Reactor.hh"
7
8
#include "Math.hh"
9
#include "StringOp.hh"
10
#include "circular_buffer.hh"
11
#include "fast_log2.hh"
12
#include "function_ref.hh"
13
#include "strCat.hh"
14
15
#include <imgui.h>
16
#include <imgui_internal.h> // ImTextCharToUtf8
17
18
#include <algorithm>
19
#include <concepts>
20
#include <functional>
21
#include <span>
22
#include <string>
23
#include <string_view>
24
#include <utility>
25
26
using namespace std::literals;
27
28
namespace ImGui {
29
30
inline void TextUnformatted(const std::string& str)
31
{
32
	const char* begin = str.data();
33
	const char* end = begin + str.size();
34
	ImGui::TextUnformatted(begin, end);
35
}
36
inline void TextUnformatted(std::string_view str)
37
{
38
	const char* begin = str.data();
39
	const char* end = begin + str.size();
40
	ImGui::TextUnformatted(begin, end);
41
}
42
43
inline void TextDisabledUnformatted(std::string_view str)
44
{
45
	im::StyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled), [&]{
46
		ImGui::TextUnformatted(str);
47
	});
48
}
49
50
inline auto CalcTextSize(std::string_view str)
51
{
52
	return ImGui::CalcTextSize(str.data(), str.data() + str.size());
53
}
54
55
inline std::string leftClip(std::string_view s, float maxWidth)
56
{
57
	auto fullWidth = ImGui::CalcTextSize(s).x;
58
	if (fullWidth <= maxWidth) return std::string(s);
59
60
	maxWidth -= ImGui::CalcTextSize("..."sv).x;
61
	if (maxWidth <= 0.0f) return "...";
62
63
	auto len = s.size();
64
	auto num = *std::ranges::lower_bound(std::views::iota(0uz, len), maxWidth, {},
65
		[&](size_t n) { return ImGui::CalcTextSize(s.substr(len - n)).x; });
66
	return strCat("...", s.substr(len - num));
67
}
68
69
// Mirror of leftClip(): keep the start of the string and replace the part that
70
// doesn't fit with an ellipsis.
71
inline std::string rightClip(std::string_view s, float maxWidth)
72
{
73
	auto fullWidth = ImGui::CalcTextSize(s).x;
74
	if (fullWidth <= maxWidth) return std::string(s);
75
76
	maxWidth -= ImGui::CalcTextSize("..."sv).x;
77
	if (maxWidth <= 0.0f) return "...";
78
79
	// The first length that is too wide, minus one, is the longest prefix that
80
	// still fits. Length 0 always fits, so that first length is never 0.
81
	auto lengths = std::views::iota(0uz, s.size() + 1);
82
	auto it = std::ranges::upper_bound(lengths, maxWidth, {},
83
		[&](size_t n) { return ImGui::CalcTextSize(s.substr(0, n)).x; });
84
	auto num = (it == lengths.end()) ? s.size() : (*it - 1);
85
	return strCat(s.substr(0, num), "...");
86
}
87
88
template<typename... Ts>
89
void StrCat(Ts&& ...ts)
90
{
91
	auto s = tmpStrCat(std::forward<Ts>(ts)...);
92
	TextUnformatted(std::string_view(s));
93
}
94
95
inline void RightAlignText(std::string_view text, float maxWidth)
96
{
97
	auto actualWidth = ImGui::CalcTextSize(text).x;
98
	if (auto spacing = maxWidth - actualWidth; spacing > 0.0f) {
99
		auto pos = ImGui::GetCursorPosX();
100
		ImGui::SetCursorPosX(pos + spacing);
101
	}
102
	ImGui::TextUnformatted(text);
103
}
104
105
inline void RightAlignText(std::string_view text, std::string_view maxWidthText)
106
{
107
	RightAlignText(text, ImGui::CalcTextSize(maxWidthText).x);
108
}
109
110
inline void AddImageRectMultiColor(ImDrawList* draw_list, ImTextureRef tex_id,
111
                                   const ImVec2& p_min, const ImVec2& p_max,
112
                                   const ImVec2& uv_min, const ImVec2& uv_max,
113
                                   ImU32 col_tl, ImU32 col_tr, ImU32 col_br,
114
                                   ImU32 col_bl)
115
{
116
	const bool push_texture_id = tex_id != draw_list->_CmdHeader.TexRef;
117
	if (push_texture_id) draw_list->PushTexture(tex_id);
118
119
	auto idx = ImDrawIdx(draw_list->_VtxCurrentIdx);
120
	draw_list->PrimReserve(6, 4);
121
122
	draw_list->PrimWriteIdx(ImDrawIdx(idx + 0));
123
	draw_list->PrimWriteIdx(ImDrawIdx(idx + 1));
124
	draw_list->PrimWriteIdx(ImDrawIdx(idx + 2));
125
126
	draw_list->PrimWriteIdx(ImDrawIdx(idx + 0));
127
	draw_list->PrimWriteIdx(ImDrawIdx(idx + 2));
128
	draw_list->PrimWriteIdx(ImDrawIdx(idx + 3));
129
130
	draw_list->PrimWriteVtx(p_min,                    uv_min,                     col_tl);
131
	draw_list->PrimWriteVtx(ImVec2(p_max.x, p_min.y), ImVec2(uv_max.x, uv_min.y), col_tr);
132
	draw_list->PrimWriteVtx(p_max,                    uv_max,                     col_br);
133
	draw_list->PrimWriteVtx(ImVec2(p_min.x, p_max.y), ImVec2(uv_min.x, uv_max.y), col_bl);
134
135
	if (push_texture_id) draw_list->PopTexture();
136
}
137
138
} // namespace ImGui
139
140
namespace openmsx {
141
142
class BooleanSetting;
143
class FloatSetting;
144
class HotKey;
145
class ImGuiManager;
146
class IntegerSetting;
147
class Setting;
148
class VideoSourceSetting;
149
150
struct EnumToolTip {
151
	std::string_view value;
152
	std::string_view tip;
153
};
154
using EnumToolTips = std::span<const EnumToolTip>;
155
156
inline void simpleToolTip(std::string_view desc)
157
{
158
	if (desc.empty()) return;
159
	im::ItemTooltip([&]{
160
		im::TextWrapPos(ImGui::GetFontSize() * 35.0f, [&]{
161
			ImGui::TextUnformatted(desc);
162
		});
163
	});
164
}
165
166
void simpleToolTip(std::invocable<> auto descFunc)
167
{
168
	if (!ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) return;
169
	auto desc = std::invoke(descFunc);
170
	if (desc.empty()) return;
171
	im::Tooltip([&]{
172
		im::TextWrapPos(ImGui::GetFontSize() * 35.0f, [&]{
173
			ImGui::TextUnformatted(desc);
174
		});
175
	});
176
}
177
178
void HelpMarkerNewLine(std::string_view desc);
179
void HelpMarker(std::string_view desc, float spacing = -1.0f);
180
181
// Generate a distinct color for each given index.
182
// Obviously the more colors you request, the less distinct each color pair will be.
183
// The initial color (for index=0) can be tuned with the 'h0' and 'v0' parameters.
184
[[nodiscard]] inline std::tuple<float, float, float> generateDistinctColor(
185
	unsigned index,
186
	float h0 = 0.0f, // Hue start offset [0, 1)
187
	unsigned v0 = 0) // Brightness phase offset [0, 2]
188
{
189
	// Hue: low-discrepancy sequence, wrapped to [0, 1)
190
	constexpr float phi = 0.6180339887498948482f; // golden ratio conjugate  (sqrt(5) - 1) / 2
191
	auto h = Math::fract(h0 + float(index) * phi);
192
193
	// Saturation
194
	constexpr float s = 0.65f;
195
196
	// Value (brightness) levels
197
	constexpr std::array v_levels = {0.90f, 0.75f, 0.60f};
198
	auto v = v_levels[(index + v0) % v_levels.size()];
199
200
	float r, g, b;
201
	ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b);
202
	return {r, g, b};
203
}
204
205
inline bool ButtonWithCustomRendering(
206
	const char* label, gl::vec2 size, bool pressed,
207
	std::invocable<gl::vec2 /*center*/, ImDrawList*> auto render)
208
{
209
	bool result = false;
210
	im::StyleColor(pressed, ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_ButtonActive), [&]{
211
		gl::vec2 topLeft = ImGui::GetCursorScreenPos();
212
		gl::vec2 center = topLeft + size * 0.5f;
213
		result = ImGui::Button(label, size);
214
		render(center, ImGui::GetWindowDrawList());
215
	});
216
	return result;
217
}
218
219
inline bool ButtonWithCustomRendering(
220
	const char* label, gl::vec2 size,
221
	std::invocable<gl::vec2 /*center*/, ImDrawList*> auto render)
222
{
223
	return ButtonWithCustomRendering(label, size, false, render);
224
}
225
226
inline bool ButtonWithCenteredGlyph(ImWchar glyph, gl::vec2 maxGlyphSize)
227
{
228
	std::array<char, 2 + 5> label = {'#', '#'};
229
	ImTextCharToUtf8(&label[2], glyph);
230
231
	const auto& style = ImGui::GetStyle();
232
	auto buttonSize = maxGlyphSize + 2.0f* gl::vec2(style.FramePadding);
233
234
	return ButtonWithCustomRendering(label.data(), buttonSize, [&](gl::vec2 center, ImDrawList* drawList) {
235
		auto* font = ImGui::GetFont();
236
		auto texId = font->OwnerAtlas->TexRef;
237
		const auto* g = font->GetFontBaked(ImGui::GetFontSize())->FindGlyph(glyph);
238
		auto halfSize = gl::vec2{g->X1 - g->X0, g->Y1 - g->Y0} * 0.5f;
239
		drawList->AddImage(texId, center - halfSize, center + halfSize, {g->U0, g->V0}, {g->U1, g->V1});
240
	});
241
};
242
243
inline void centerNextWindowOverCurrent()
244
{
245
	static constexpr gl::vec2 center{0.5f, 0.5f};
246
	gl::vec2 windowPos = ImGui::GetWindowPos();
247
	gl::vec2 windowSize = ImGui::GetWindowSize();
248
	auto windowCenter = windowPos + center * windowSize;
249
	ImGui::SetNextWindowPos(windowCenter, ImGuiCond_Appearing, center);
250
}
251
252
class ConfirmDialog {
253
public:
254
	explicit ConfirmDialog(std::string title_) : title(std::move(title_)) {}
255
256
	void open(std::string text_, std::function<void()> action_) {
257
		text = std::move(text_);
258
		action = std::move(action_);
259
		doOpen = true;
260
	}
261
262
	void execute();
263
264
private:
265
	std::string title;
266
	std::string text;
267
	std::function<void()> action;
268
	bool doOpen = false;
269
};
270
271
class ConfirmDialogTclCommand : public ConfirmDialog {
272
public:
273
	ConfirmDialogTclCommand(ImGuiManager& manager_, std::string title_)
274
		: ConfirmDialog(std::move(title_)), manager(&manager_) {}
275
276
	void open(std::string text_, TclObject cmd_);
277
278
private:
279
	ImGuiManager* manager;
280
};
281
282
struct GetSettingDescription {
283
	std::string operator()(const Setting& setting) const;
284
};
285
286
bool Checkbox(const HotKey& hotkey, BooleanSetting& setting);
287
bool Checkbox(const HotKey& hotkey, const char* label, BooleanSetting& setting, function_ref<std::string(const Setting&)> getTooltip = GetSettingDescription{});
288
bool SliderInt(IntegerSetting& setting, ImGuiSliderFlags flags = 0);
289
bool SliderInt(const char* label, IntegerSetting& setting, ImGuiSliderFlags flags = 0);
290
bool SliderFloat(FloatSetting& setting, const char* format = "%.3f", ImGuiSliderFlags flags = 0);
291
bool SliderFloat(const char* label, FloatSetting& setting, const char* format = "%.3f", ImGuiSliderFlags flags = 0);
292
bool InputText(Setting& setting);
293
bool InputText(const char* label, Setting& setting);
294
void ComboBox(Setting& setting, EnumToolTips toolTips = {}); // must be an EnumSetting
295
void ComboBox(const char* label, Setting& setting, EnumToolTips toolTips = {}); // must be an EnumSetting
296
void ComboBox(const char* label, Setting& setting, function_ref<std::string(const std::string&)> displayValue, EnumToolTips toolTips = {});
297
void ComboBox(VideoSourceSetting& setting);
298
void ComboBox(const char* label, VideoSourceSetting& setting);
299
300
const char* getComboString(int item, const char* itemsSeparatedByZeros);
301
302
std::string formatTime(std::optional<double> time);
303
float calculateFade(float current, float target, float period);
304
305
template<int HexDigits>
306
void comboHexSequence(const char* label, int* value, int mult, int max, int offset) {
307
	assert(offset < mult);
308
	*value &= ~(mult - 1);
309
	// only apply offset in display, not in the actual value
310
	auto preview = tmpStrCat("0x", hex_string<HexDigits>(*value | offset));
311
	im::Combo(label, preview.c_str(), [&]{
312
		for (int addr = 0; addr < max; addr += mult) {
313
			if (auto str = tmpStrCat("0x", hex_string<HexDigits>(addr | offset));
314
			    ImGui::Selectable(str.c_str(), *value == addr)) {
315
				*value = addr;
316
			}
317
			if (*value == addr) {
318
				ImGui::SetItemDefaultFocus();
319
			}
320
		}
321
	});
322
};
323
324
template<typename Range, typename Projection>
325
void sortUpDown_T(Range& range, const ImGuiTableSortSpecs* sortSpecs, Projection proj) {
326
	if (sortSpecs->Specs->SortDirection == ImGuiSortDirection_Descending) {
327
		std::ranges::stable_sort(range, std::greater<>{}, proj);
328
	} else {
329
		std::ranges::stable_sort(range, std::less<>{}, proj);
330
	}
331
};
332
template<typename Range, typename Projection>
333
void sortUpDown_String(Range& range, const ImGuiTableSortSpecs* sortSpecs, Projection proj) {
334
	if (sortSpecs->Specs->SortDirection == ImGuiSortDirection_Descending) {
335
		std::ranges::stable_sort(range, StringOp::inv_caseless{}, proj);
336
	} else {
337
		std::ranges::stable_sort(range, StringOp::caseless{}, proj);
338
	}
339
};
340
341
342
[[nodiscard]] inline const std::string* getOptionalDictValue(
343
	const std::vector<std::pair<std::string, std::string>>& info,
344
	std::string_view key)
345
{
346
	auto it = std::ranges::find_if(info, [&](const auto& p) { return p.first == key; });
347
	if (it == info.end()) return {};
348
	return &it->second;
349
}
350
351
template<typename T> // 'MachineInfo' or 'ExtensionInfo', both have a 'configInfo' member
352
[[nodiscard]] std::vector<std::string> getAllValuesFor(std::string_view key, const std::vector<T>& items)
353
{
354
	std::vector<std::string> result;
355
	for (const auto& item : items) {
356
		if (const auto* value = getOptionalDictValue(item.configInfo, key)) {
357
			if (!contains(result, *value)) { // O(N^2), but that's fine
358
				result.emplace_back(*value);
359
			}
360
		}
361
	}
362
	std::ranges::sort(result, StringOp::caseless{});
363
	return result;
364
}
365
366
template<typename T>
367
void displayFilterCombo(std::string& selection, zstring_view key, const std::vector<T>& items)
368
{
369
	im::Combo(key.c_str(), selection.empty() ? "--all--" : selection.c_str(), [&]{
370
		if (ImGui::Selectable("--all--")) {
371
			selection.clear();
372
		}
373
		for (const auto& type : getAllValuesFor(key, items)) {
374
			if (ImGui::Selectable(type.c_str())) {
375
				selection = type;
376
			}
377
		}
378
	});
379
}
380
381
template<typename T>
382
void applyComboFilter(std::string_view key, std::string_view value, const std::vector<T>& items, std::vector<size_t>& indices)
383
{
384
	if (value.empty()) return;
385
	std::erase_if(indices, [&](auto idx) {
386
		const auto& info = items[idx].configInfo;
387
		const auto* val = getOptionalDictValue(info, key);
388
		if (!val) return true; // remove items that don't have the key
389
		return *val != value;
390
	});
391
}
392
393
template<std::invocable<size_t> GetName>
394
void filterIndices(std::string_view filterString, GetName getName, std::vector<size_t>& indices)
395
{
396
	if (filterString.empty()) return;
397
	std::erase_if(indices, [&](auto idx) {
398
		const auto& name = getName(idx);
399
		return !std::ranges::all_of(StringOp::split_view<StringOp::EmptyParts::REMOVE>(filterString, ' '),
400
			[&](auto part) { return StringOp::containsCaseInsensitive(name, part); });
401
	});
402
}
403
404
template<typename T>
405
void applyDisplayNameFilter(std::string_view filterString, const std::vector<T>& items, std::vector<size_t>& indices)
406
{
407
	filterIndices(filterString, [&](size_t idx) { return items[idx].displayName; }, indices);
408
}
409
410
template<typename T>
411
void addRecentItem(circular_buffer<T>& recentItems, const T& item)
412
{
413
	if (auto it = std::ranges::find(recentItems, item); it != recentItems.end()) {
414
		// was already present, move to front
415
		std::rotate(recentItems.begin(), it, it + 1);
416
	} else {
417
		// new entry, add it, but possibly remove oldest entry
418
		if (recentItems.full()) recentItems.pop_back();
419
		recentItems.push_front(item);
420
	}
421
}
422
423
// Similar to c++23 chunk_by(). Main difference is internal vs external iteration.
424
template<typename Range, typename BinaryPred, typename Action>
425
static void chunk_by(Range&& range, BinaryPred pred, Action action)
426
{
427
	auto it = std::begin(range);
428
	auto last = std::end(range);
429
	while (it != last) {
430
		auto start = it;
431
		auto prev = it++;
432
		while (it != last && pred(*prev, *it)) {
433
			prev = it++;
434
		}
435
		action(start, it);
436
	}
437
}
438
439
TemporaryString getShortCutForCommand(const HotKey& hotkey, std::string_view command);
440
441
std::string getKeyChordName(ImGuiKeyChord keyChord);
442
std::optional<ImGuiKeyChord> parseKeyChord(std::string_view name);
443
444
[[nodiscard]] std::string formatToString(function_ref<uint8_t(unsigned)> fetch, unsigned begin, unsigned end, std::string_view prefix,
445
	unsigned columns, std::string_view suffix, std::string_view separator, std::string_view formatStr, Interpreter& interp);
446
447
[[nodiscard]] std::string formatToString(function_ref<uint8_t(unsigned)> fetch, unsigned begin, unsigned end, std::string_view prefix,
448
	unsigned columns, std::string_view suffix, std::string_view separator, std::function<std::string(uint8_t)> formatFunc);
449
450
[[nodiscard]] std::string rawToString(function_ref<uint8_t(unsigned)> fetch, unsigned begin, unsigned end);
451
452
// Read from VRAM-table, including mirroring behavior
453
//  shared between ImGuiCharacter, ImGuiSpriteViewer
454
class VramTable {
455
public:
456
	explicit VramTable(std::span<const uint8_t> vram_, bool planar_ = false)
457
		: vram(vram_), planar(planar_) {}
458
459
	void setRegister(unsigned value, unsigned extraLsbBits) {
460
		registerMask = (value << extraLsbBits) | ~(~0u << extraLsbBits);
461
	}
462
	void setIndexSize(unsigned bits) {
463
		indexMask = ~0u << bits;
464
	}
465
466
	[[nodiscard]] auto getAddress(unsigned index) const {
467
		return registerMask & (indexMask | index);
468
	}
469
	[[nodiscard]] uint8_t operator[](unsigned index) const {
470
		auto addr = getAddress(index);
471
		if (planar) {
472
			addr = ((addr << 16) | (addr >> 1)) & 0x1'FFFF;
473
		}
474
		return vram[addr];
475
	}
476
private:
477
	std::span<const uint8_t> vram;
478
	unsigned registerMask = 0;
479
	unsigned indexMask = 0;
480
	bool planar = false;
481
};
482
483
[[nodiscard]] inline int freq2noteNum(float freq)
484
{
485
	static constexpr auto a4_freq = 440.0f;
486
	return int(std::lround(12.0f * fast_log2(freq / a4_freq))) + 9 + 4 * 12;
487
}
488
[[nodiscard]] inline std::string_view noteNum2str(int n, std::span<char, 3> buf)
489
{
490
	using N = std::array<char, 2>;
491
	static constexpr std::array<N, 12> names = {
492
		N{'C'}, N{'C','#'}, N{'D'}, N{'D','#'}, N{'E'},
493
		N{'F'}, N{'F','#'}, N{'G'}, N{'G','#'}, N{'A'}, N{'A','#'}, N{'B'}
494
	};
495
496
	if (n < 0) return {}; // these are below 20Hz, so inaudible
497
	auto note = n % 12;
498
	auto octave = n / 12;
499
	const auto& name = names[note];
500
	buf[0] = name[0];
501
	if (name[1]) {
502
		buf[1] = name[1];
503
		buf[2] = char(octave + '0');
504
		return std::string_view{buf.data(), 3};
505
	} else {
506
		buf[1] = char(octave + '0');
507
		return std::string_view{buf.data(), 2};
508
	}
509
}
510
[[nodiscard]] inline auto freq2note(float freq, std::span<char, 3> buf)
511
{
512
	return noteNum2str(freq2noteNum(freq), buf);
513
}
514
515
enum class imColor : uint8_t {
516
	TRANSPARENT,
517
	BLACK,
518
	WHITE,
519
	GRAY,
520
	YELLOW,
521
	RED_BG,    // red    background (transparent)
522
	YELLOW_BG, // yellow background (transparent)
523
524
	TEXT,
525
	TEXT_DISABLED,
526
527
	ERROR,
528
	WARNING,
529
530
	COMMENT, // syntax highlighting in the console
531
	VARIABLE,
532
	LITERAL,
533
	PROC,
534
	OPERATOR,
535
536
	KEY_ACTIVE,     // virtual keyboard
537
	KEY_NOT_ACTIVE,
538
539
	NUM_COLORS
540
};
541
inline std::array<ImU32, size_t(imColor::NUM_COLORS)> imColors;
542
543
void setColors(int style);
544
545
inline ImU32 getColor(imColor col) {
546
	assert(col < imColor::NUM_COLORS);
547
	return imColors[size_t(col)];
548
}
549
550
} // namespace openmsx
551
552
#endif