OXYGENHUB
◆ rrdc-perf enhanced-x16-emulator / src / debug_ui / debug_ui_widgets.h
8.9 KiB · Objective-C · last changed chore(): initial commit 25 days ago source blame history raw
9120e874 Andy McCall 1
// Commander X16 Emulator — shared inline ImGui helpers for the debugger panels.
2
//
3
// Header-only (no CMake entry needed): value tooltips that show a number as hex,
4
// decimal, signed decimal, binary (nibble-grouped) and ASCII. Used by the CPU,
5
// Memory and VERA panels so hovering any value gives a quick multi-base view.
6
#ifndef DEBUG_UI_WIDGETS_H
7
#define DEBUG_UI_WIDGETS_H
8
9
// Font ladder used by dbgui_window_zoom (defined in debug_ui.cpp): zoom picks a
10
// font rasterised at the wanted size rather than scaling one bitmap.
11
ImFont *debug_ui_font_for_size(float size_px, float *out_size);
12
float   debug_ui_font_base_size(void);
13
14
#include "imgui.h"
15
#include <stdint.h>
16
#include <stddef.h>
17
#include <string.h>
18
19
// ── Font-relative sizing ────────────────────────────────────────────────────
20
// Widths must be derived from the font that is actually active, never written
21
// as pixel constants. The interface scale and per-window zoom both change the
22
// font size, so a literal width that fitted at 100% clips its contents once the
23
// UI is scaled up - which is exactly how the breakpoint/watchpoint headers
24
// ended up truncated.
25
//
26
// Pass the widest string the element has to hold ("$FFFF", "00:0000", ...) and
27
// these measure it in the current font.
28
29
// Plain text.
30
static inline float
31
dbgui_text_width(const char *sample)
32
{
33
    return ImGui::CalcTextSize(sample ? sample : "").x;
34
}
35
36
// InputText / InputScalar / InputInt: the text plus the frame's inner padding.
37
static inline float
38
dbgui_field_width(const char *sample)
39
{
40
    return dbgui_text_width(sample) + ImGui::GetStyle().FramePadding.x * 2.0f;
41
}
42
43
// Combo: a field plus the drop-down arrow ImGui draws inside the frame.
44
static inline float
45
dbgui_combo_width(const char *sample)
46
{
47
    return dbgui_field_width(sample) + ImGui::GetFrameHeight();
48
}
49
50
// ── Commit-on-Enter for numeric fields ──────────────────────────────────────
51
// ImGuiInputTextFlags_EnterReturnsTrue is only supported by the InputText
52
// family. InputScalar asserts on it (imgui_widgets.cpp:3666), and InputInt and
53
// friends wrap InputScalar, so a debug build aborts the moment such a field is
54
// drawn. Release builds compile the assert out and the flag happens to work,
55
// because InputScalar passes its flags straight through to InputText -- which
56
// makes this a combination that works only by accident and only where the
57
// assertion is disabled.
58
//
59
// Call this immediately after the field instead. Deactivation-after-edit plus
60
// an Enter in the same frame is the same condition, without the unsupported
61
// flag: ImGui deactivates the item when Enter commits it, and the key state is
62
// still readable that frame.
63
static inline bool
64
dbgui_committed_with_enter()
65
{
66
    return ImGui::IsItemDeactivatedAfterEdit() &&
67
           (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter));
68
}
69
70
// Table column sized to fit `content_w`, and its header text, plus the cell
71
// padding on both sides. Pass content_w from one of the helpers above (or
72
// ImGui::GetFrameHeight() for a lone checkbox).
73
static inline float
74
dbgui_col_fit(const char *header, float content_w)
75
{
76
    const float hw = dbgui_text_width(header);
77
    if (hw > content_w) {
78
        content_w = hw;
79
    }
80
    return content_w + ImGui::GetStyle().CellPadding.x * 2.0f;
81
}
82
83
// Column holding plain text, e.g. an address or a count.
84
static inline float
85
dbgui_col_width(const char *header, const char *sample)
86
{
87
    return dbgui_col_fit(header, dbgui_text_width(sample));
88
}
89
90
// Flags every data table should carry: let the user drag column borders (and
91
// get ImGui's header context menu, which offers "size all columns to fit").
92
// Default widths still track the font; an explicit resize overrides them.
93
#define DBGUI_TABLE_FLAGS_RESIZABLE (ImGuiTableFlags_Resizable | ImGuiTableFlags_ContextMenuInBody)
94
95
// Format the low `bits` of v as binary, grouped in nibbles: e.g. "1010 0101".
96
static inline void
97
dbgui_format_binary(char *out, size_t outsz, uint32_t v, int bits)
98
{
99
    size_t o = 0;
100
    for (int i = bits - 1; i >= 0 && o + 2 < outsz; i--) {
101
        out[o++] = ((v >> i) & 1u) ? '1' : '0';
102
        if (i != 0 && (i % 4) == 0 && o + 1 < outsz)
103
            out[o++] = ' ';
104
    }
105
    out[o] = '\0';
106
}
107
108
// Render the value lines (hex / decimal / signed / binary / ASCII). Does NOT
109
// open a tooltip window itself — call between BeginTooltip()/EndTooltip() or
110
// inline. `bytes` is the value width (1..4).
111
static inline void
112
dbgui_value_lines(uint32_t v, int bytes)
113
{
114
    if (bytes < 1)
115
        bytes = 1;
116
    if (bytes > 4)
117
        bytes = 4;
118
    int      bits = bytes * 8;
119
    uint32_t mask = (bits >= 32) ? 0xFFFFFFFFu : ((1u << bits) - 1u);
120
    v &= mask;
121
122
    int32_t s = (int32_t)v; // sign-extend from the value's width
123
    if (bits < 32 && (v & (1u << (bits - 1))))
124
        s = (int32_t)(v | ~mask);
125
126
    char bin[48];
127
    dbgui_format_binary(bin, sizeof bin, v, bits);
128
129
    ImGui::Text("Hex   : $%0*X", bytes * 2, (unsigned)v);
130
    ImGui::Text("Dec   : %u", (unsigned)v);
131
    ImGui::Text("Signed: %d", (int)s);
132
    ImGui::Text("Bin   : %s", bin);
133
    if (bytes == 1) {
134
        unsigned char c = (unsigned char)(v & 0xFF);
135
        if (c >= 0x20 && c < 0x7f)
136
            ImGui::Text("Char  : '%c'", c);
137
        else
138
            ImGui::Text("Char  : .");
139
    }
140
}
141
142
// If the LAST-submitted item is hovered, show a value tooltip, optionally
143
// prefixed with a name/description line. Call immediately after the widget.
144
static inline void
145
dbgui_hover_value_tooltip(const char *desc, uint32_t v, int bytes)
146
{
147
    if (!ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
148
        return;
149
    ImGui::BeginTooltip();
150
    if (desc && desc[0]) {
151
        ImGui::TextUnformatted(desc);
152
        ImGui::Separator();
153
    }
154
    dbgui_value_lines(v, bytes);
155
    ImGui::EndTooltip();
156
}
157
158
// Per-window font zoom. Call once immediately after Begin() (inside the window)
159
// and pass a stable key for that window. Ctrl+mouse-wheel over the window zooms
160
// its text only, leaving every other panel alone; Ctrl+middle-click resets to
161
// 100%. The wheel event is consumed while Ctrl is held so the window does not
162
// also scroll. Returns the active scale.
163
//
164
// Zoom selects a font that was rasterised at the wanted size rather than
165
// scaling one bitmap — which is what SetWindowFontScale does, and why zoomed
166
// text used to smear — so the requested scale snaps to the nearest built size.
167
// Frame padding is not scaled; that trade-off is fine for source/hex views.
168
//
169
// IMPORTANT: the pushed font must be popped before ImGui::End(), and panels have
170
// several early-exit paths, so panels call dbgui_window_end() instead of
171
// ImGui::End() and the pop is handled there.
172
static int dbgui_zoom_pushed = 0;
173
174
static inline float
175
dbgui_window_zoom(const char *key)
176
{
177
    // Small fixed table — a handful of debugger panels, no allocation needed.
178
    struct Entry { const char *key; float scale; };
179
    static Entry  entries[16];
180
    static int    count = 0;
181
182
    Entry *e = nullptr;
183
    for (int i = 0; i < count; i++) {
184
        if (entries[i].key == key || (entries[i].key && key && !strcmp(entries[i].key, key))) {
185
            e = &entries[i];
186
            break;
187
        }
188
    }
189
    if (!e && count < (int)(sizeof entries / sizeof entries[0])) {
190
        entries[count].key   = key;
191
        entries[count].scale = 1.0f;
192
        e = &entries[count++];
193
    }
194
    if (!e)
195
        return 1.0f;
196
197
    ImGuiIO &io = ImGui::GetIO();
198
    if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows |
199
                               ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) {
200
        if (io.KeyCtrl && io.MouseWheel != 0.0f) {
201
            e->scale += io.MouseWheel * 0.1f;
202
            if (e->scale < 0.5f) e->scale = 0.5f;
203
            if (e->scale > 3.0f) e->scale = 3.0f;
204
            io.MouseWheel = 0.0f; // consume: don't scroll the window too
205
        }
206
        if (io.KeyCtrl && ImGui::IsMouseClicked(ImGuiMouseButton_Middle))
207
            e->scale = 1.0f;
208
    }
209
210
    const float base = debug_ui_font_base_size();
211
    float       got  = base;
212
    ImFont     *font = debug_ui_font_for_size(base * e->scale, &got);
213
    if (font) {
214
        ImGui::PushFont(font);
215
        dbgui_zoom_pushed++;
216
        e->scale = got / base; // report what was actually applied
217
    }
218
    return e->scale;
219
}
220
221
// End a panel window. Pops any font dbgui_window_zoom() pushed, then calls
222
// ImGui::End(). Panels use this at EVERY exit path (including the one taken
223
// when Begin() returns false, where nothing was pushed).
224
static inline void
225
dbgui_window_end(void)
226
{
227
    while (dbgui_zoom_pushed > 0) {
228
        ImGui::PopFont();
229
        dbgui_zoom_pushed--;
230
    }
231
    ImGui::End();
232
}
233
234
// Show the current zoom as a small right-aligned hint (only when not 100%).
235
static inline void
236
dbgui_zoom_hint(float scale)
237
{
238
    if (scale == 1.0f)
239
        return;
240
    ImGui::SameLine();
241
    ImGui::TextDisabled("(%.0f%%)", scale * 100.0f);
242
}
243
244
#endif // DEBUG_UI_WIDGETS_H