-
Notifications
You must be signed in to change notification settings - Fork 13
UI Internals
This page provides deep implementation details of the UI system, focusing on the MainWindow orchestrator, the complex waterfall widget, and advanced rendering techniques.
The MainWindow::draw() function is called at 60 FPS and orchestrates the entire UI rendering process:
void MainWindow::draw() {
// 1. STATE SYNCHRONIZATION - Critical UI-to-DSP bridge
handleStateSync();
// 2. WINDOW SETUP - Create fullscreen canvas
setupMainWindow();
// 3. TOP BAR - Menu, controls, frequency selector
drawTopBar();
// 4. LAYOUT MANAGEMENT - Menu/waterfall split
setupLayout();
// 5. CONDITIONAL MENU - Side panel if enabled
if (showMenu) drawSideMenu();
// 6. WATERFALL WIDGET - Main display
gui::waterfall.draw();
// 7. GLOBAL HOTKEYS - Keyboard shortcuts
processGlobalInput();
}Critical: This is where UI events become DSP actions:
void MainWindow::handleStateSync() {
// VFO frequency changes from waterfall dragging
if (gui::waterfall.centerFreqMoved) {
gui::waterfall.centerFreqMoved = false;
double newFreq = gui::waterfall.getCenterFrequency();
if (vfo != nullptr) {
// Convert UI event to DSP command
tuner::tune(tuningMode, selectedVFO, newFreq);
}
// Persist to configuration
core::configManager.acquire();
core::configManager.conf["frequency"] = newFreq;
core::configManager.release(true);
}
// Volume changes from UI slider
if (volumeChanged) {
volumeChanged = false;
sigpath::sinkManager.setVolume(volume);
core::configManager.acquire();
core::configManager.conf["volume"] = volume;
core::configManager.release(true);
}
// Source selection changes
if (sourceChanged) {
sourceChanged = false;
sigpath::sourceManager.selectSource(selectedSource);
core::configManager.acquire();
core::configManager.conf["source"] = selectedSource;
core::configManager.release(true);
}
}void MainWindow::setupMainWindow() {
// Create invisible fullscreen window as UI canvas
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->Pos);
ImGui::SetNextWindowSize(viewport->Size);
ImGui::SetNextWindowViewport(viewport->ID);
// Window flags for fullscreen overlay
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoBringToFrontOnFocus;
ImGui::Begin("SDR++CE", nullptr, flags);
}
void MainWindow::setupLayout() {
// Calculate layout dimensions
float totalWidth = ImGui::GetContentRegionAvail().x;
float menuWidth = showMenu ? menuWidth : 0;
float waterfallWidth = totalWidth - menuWidth;
// Create resizable layout
if (showMenu) {
ImGui::BeginChild("MenuPanel", ImVec2(menuWidth, 0), true);
drawSideMenu();
ImGui::EndChild();
ImGui::SameLine();
// Draggable divider
ImGui::Button("##divider", ImVec2(4, -1));
if (ImGui::IsItemActive()) {
menuWidth += ImGui::GetIO().MouseDelta.x;
menuWidth = std::clamp(menuWidth, 200.0f, totalWidth - 200.0f);
}
ImGui::SameLine();
}
ImGui::BeginChild("WaterfallPanel", ImVec2(waterfallWidth, 0), false);
gui::waterfall.draw();
ImGui::EndChild();
}void MainWindow::drawTopBar() {
// Menu toggle button (hamburger icon)
if (SmGui::Button(ICON_FA_BARS "##menu")) {
showMenu = !showMenu;
}
ImGui::SameLine();
// Play/Stop button with conditional styling
if (playing) {
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(255, 100, 100, 255));
if (SmGui::Button(ICON_FA_STOP "##play_stop")) {
setPlaying(false);
}
ImGui::PopStyleColor();
} else {
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(100, 255, 100, 255));
if (SmGui::Button(ICON_FA_PLAY "##play_stop")) {
setPlaying(true);
}
ImGui::PopStyleColor();
}
ImGui::SameLine();
// Volume control with dB display
float displayVolume = 20.0f * log10f(volume); // Convert to dB
if (SmGui::SliderFloat("##volume", &displayVolume, -60.0f, 0.0f, "%.1f dB")) {
volume = powf(10.0f, displayVolume / 20.0f); // Convert back to linear
volumeChanged = true;
}
ImGui::SameLine();
// Frequency selector (complex custom widget)
gui::freqSelect.draw();
// SNR meter (if enabled)
if (showSNR) {
ImGui::SameLine();
drawSNRMeter();
}
}The side menu is built dynamically from registered components:
class MenuManager {
struct MenuEntry {
std::string name;
void (*handler)(void* ctx);
void* ctx;
bool visible = true;
};
std::vector<MenuEntry> entries;
std::vector<std::string> order; // From config
public:
void registerEntry(const std::string& name, void (*handler)(void*), void* ctx) {
entries.push_back({name, handler, ctx, true});
}
void draw(bool& firstRender) {
// Iterate through configured order
for (const std::string& name : order) {
auto it = std::find_if(entries.begin(), entries.end(),
[&name](const MenuEntry& e) { return e.name == name; });
if (it != entries.end() && it->visible) {
it->handler(it->ctx); // Call module's draw function
}
}
// Handle first render for modules that need it
if (firstRender) {
firstRender = false;
}
}
};Menu order and visibility are controlled by config.json:
{
"menuElements": [
{"name": "Source", "open": true},
{"name": "Radio", "open": true},
{"name": "Audio", "open": false},
{"name": "Display", "open": false},
{"name": "Band Plan", "open": false},
{"name": "Frequency Manager", "open": false}
]
}class WaterFall {
// OpenGL Resources
GLuint textureId; // Main waterfall texture
GLuint fftTextureId; // FFT line texture (optional)
// Display Buffers
float* latestFFT; // Current FFT for line display
uint32_t* waterfallFb; // Waterfall pixel framebuffer
float* rawFFTs; // Circular buffer of raw FFT data
// Buffer Management
std::mutex bufMtx; // Protects rawFFTs access
std::recursive_mutex latestFFTMtx; // Protects display data
// State Variables
int currentFFTLine; // Current position in circular buffer
int fftLines; // Number of valid FFT lines
int dataWidth; // Display width in pixels
int waterfallHeight; // Height in pixels
double viewBandwidth; // Currently displayed bandwidth
double viewOffset; // Frequency offset from center
// UI Models for VFOs
std::map<std::string, WaterfallVFO> vfos;
};void WaterFall::drawFFT() {
if (!latestFFT) return;
// Get ImGui drawing context
ImGuiWindow* window = ImGui::GetCurrentWindow();
ImDrawList* drawList = window->DrawList;
// Calculate display area
ImVec2 fftMin = ImVec2(widgetPos.x, widgetPos.y);
ImVec2 fftMax = ImVec2(widgetPos.x + dataWidth, widgetPos.y + fftHeight);
// Draw frequency grid
drawFrequencyGrid(drawList, fftMin, fftMax);
// Draw dB scale
drawDBScale(drawList, fftMin, fftMax);
// Convert FFT data to screen coordinates
std::vector<ImVec2> fftPoints;
fftPoints.reserve(dataWidth);
for (int i = 0; i < dataWidth; i++) {
float dbValue = latestFFT[i]; // Already in dB
float x = fftMin.x + i;
float y = fftMax.y - ((dbValue - fftMin) / (fftMax - fftMin)) * fftHeight;
y = std::clamp(y, fftMin.y, fftMax.y);
fftPoints.emplace_back(x, y);
}
// Draw FFT trace as connected lines
if (fftPoints.size() > 1) {
drawList->AddPolyline(fftPoints.data(), fftPoints.size(),
fftColor, false, 2.0f);
}
// Draw filled area under FFT
if (fftFill && fftPoints.size() > 1) {
// Add bottom corners to close the polygon
fftPoints.emplace_back(fftMax.x, fftMax.y);
fftPoints.emplace_back(fftMin.x, fftMax.y);
drawList->AddConvexPolyFilled(fftPoints.data(), fftPoints.size(),
fftFillColor);
}
}void WaterFall::updateWaterfallTexture() {
if (!waterfallFb || !textureId) return;
// Move existing data down by one line
memmove(&waterfallFb[dataWidth], waterfallFb,
dataWidth * (waterfallHeight - 1) * sizeof(uint32_t));
// Convert new FFT line to pixels using colormap
for (int i = 0; i < dataWidth; i++) {
float dbValue = latestFFT[i];
// Normalize to colormap range
float normalized = (dbValue - waterfallMin) / (waterfallMax - waterfallMin);
normalized = std::clamp(normalized, 0.0f, 1.0f);
// Map to colormap
int colorIndex = (int)(normalized * (colorMapSize - 1));
waterfallFb[i] = colorMap[colorIndex];
}
// Upload to OpenGL texture
glBindTexture(GL_TEXTURE_2D, textureId);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, dataWidth, waterfallHeight,
GL_RGBA, GL_UNSIGNED_BYTE, waterfallFb);
}
void WaterFall::drawWaterfall() {
if (!textureId) return;
ImGuiWindow* window = ImGui::GetCurrentWindow();
ImDrawList* drawList = window->DrawList;
// Calculate waterfall display area
ImVec2 waterfallMin = ImVec2(widgetPos.x, widgetPos.y + fftHeight);
ImVec2 waterfallMax = ImVec2(widgetPos.x + dataWidth,
widgetPos.y + fftHeight + waterfallHeight);
// Draw textured quad
drawList->AddImage((ImTextureID)(intptr_t)textureId,
waterfallMin, waterfallMax,
ImVec2(0, 0), ImVec2(1, 1));
}Complex mouse interaction handling for VFO manipulation:
enum DragMode {
DRAG_NONE,
DRAG_VFO_FREQ, // Dragging VFO center frequency
DRAG_VFO_BW_LEFT, // Dragging left bandwidth edge
DRAG_VFO_BW_RIGHT, // Dragging right bandwidth edge
DRAG_PAN, // Panning waterfall view
DRAG_ZOOM, // Zooming waterfall
DRAG_FFT_RESIZE // Resizing FFT height
};
void WaterFall::processInputs() {
ImGuiIO& io = ImGui::GetIO();
ImVec2 mousePos = io.MousePos;
bool mouseDown = io.MouseDown[0];
// Convert mouse position to frequency
double mouseFreq = pixelToFreq(mousePos.x);
if (mouseDown && !lastMouseDown) {
// Mouse pressed - determine interaction type
dragMode = DRAG_NONE;
if (isInWaterfallArea(mousePos)) {
// Check VFO hit testing first (highest priority)
for (auto& [name, vfo] : vfos) {
if (hitTestVFO(vfo, mousePos)) {
if (hitTestBandwidthHandle(vfo, mousePos, true)) {
dragMode = DRAG_VFO_BW_LEFT;
selectedVFO = name;
} else if (hitTestBandwidthHandle(vfo, mousePos, false)) {
dragMode = DRAG_VFO_BW_RIGHT;
selectedVFO = name;
} else {
dragMode = DRAG_VFO_FREQ;
selectedVFO = name;
}
break;
}
}
// If no VFO hit, check other interactions
if (dragMode == DRAG_NONE) {
if (isOnFrequencyScale(mousePos)) {
// Clicked on frequency scale
setCenterFrequency(mouseFreq);
centerFreqMoved = true;
} else if (io.KeyCtrl) {
dragMode = DRAG_ZOOM;
} else {
dragMode = DRAG_PAN;
}
}
} else if (isOnFFTResizeHandle(mousePos)) {
dragMode = DRAG_FFT_RESIZE;
}
dragStartPos = mousePos;
dragStartFreq = mouseFreq;
}
if (mouseDown && lastMouseDown && dragMode != DRAG_NONE) {
// Mouse dragged - update based on current mode
ImVec2 delta = mousePos - dragStartPos;
double freqDelta = pixelToFreq(mousePos.x) - dragStartFreq;
switch (dragMode) {
case DRAG_VFO_FREQ:
updateVFOFrequency(selectedVFO, freqDelta);
break;
case DRAG_VFO_BW_LEFT:
updateVFOBandwidth(selectedVFO, -freqDelta, true);
break;
case DRAG_VFO_BW_RIGHT:
updateVFOBandwidth(selectedVFO, freqDelta, false);
break;
case DRAG_PAN:
updateViewOffset(-freqDelta);
break;
case DRAG_ZOOM:
updateZoom(delta.y);
break;
case DRAG_FFT_RESIZE:
updateFFTHeight(delta.y);
break;
}
}
if (!mouseDown && lastMouseDown) {
// Mouse released - end drag and save state
if (dragMode != DRAG_NONE) {
saveDragState();
dragMode = DRAG_NONE;
selectedVFO = "";
}
}
// Handle scroll wheel for zooming
if (io.MouseWheel != 0 && isInWaterfallArea(mousePos)) {
double zoomFactor = pow(1.1, io.MouseWheel);
zoomAroundMouse(mousePos, zoomFactor);
}
lastMouseDown = mouseDown;
}Separate UI representation optimized for rendering:
class WaterfallVFO {
// Frequency domain properties
double generalOffset; // VFO center frequency offset
double centerOffset; // For REF_CENTER mode
double lowerOffset, upperOffset; // For asymmetric modes
double bandwidth; // Filter bandwidth
int reference; // REF_CENTER, REF_LOWER, REF_UPPER
// Screen space coordinates (updated each frame)
ImVec2 rectMin, rectMax; // Main VFO body
ImVec2 lineMin, lineMax; // Center frequency line
ImVec2 lbwSelMin, lbwSelMax; // Left bandwidth handle
ImVec2 rbwSelMin, rbwSelMax; // Right bandwidth handle
ImVec2 wfRectMin, wfRectMax; // Waterfall area
// Visual state
ImU32 color; // VFO display color
bool lineVisible; // Show center line
bool leftClamped, rightClamped; // Clipped at edges
// Change tracking
bool centerOffsetChanged;
bool bandwidthChanged;
bool redrawRequired;
// Constraints
double minBandwidth, maxBandwidth;
bool bandwidthLocked;
// Events
Event<double> onUserChangedBandwidth;
Event<double> onUserChangedNotch;
public:
void updateDrawingVars(double viewBandwidth, float dataWidth,
double viewOffset, ImVec2 widgetPos, int fftHeight);
void draw(ImGuiWindow* window, bool selected);
void setOffset(double offset);
void setBandwidth(double bw);
void setReference(int ref);
};Thread-safe synchronization between UI and DSP:
void WaterfallVFO::setOffset(double offset) {
// Update UI state immediately
generalOffset = offset;
updateOffsetsFromGeneral();
redrawRequired = true;
// Mark for DSP sync (handled by VFOManager)
centerOffsetChanged = true;
}
void VFOManager::updateVFOFromWaterfall(const std::string& name) {
auto vfoIt = vfos.find(name);
if (vfoIt == vfos.end()) return;
VFO* vfo = vfoIt->second;
WaterfallVFO* wtfVFO = &gui::waterfall.vfos[name];
// Check if UI changed
if (wtfVFO->centerOffsetChanged) {
wtfVFO->centerOffsetChanged = false;
// Update DSP VFO (thread-safe)
double newFreq = gui::waterfall.getCenterFrequency() + wtfVFO->generalOffset;
vfo->dspVFO->setOffset(wtfVFO->generalOffset);
// Update UI frequency display
gui::freqSelect.setFrequency(newFreq);
// Save to config
core::configManager.acquire();
core::configManager.conf["vfos"][name]["frequency"] = newFreq;
core::configManager.release(true);
}
if (wtfVFO->bandwidthChanged) {
wtfVFO->bandwidthChanged = false;
// Update DSP filter
vfo->dspVFO->setBandwidth(wtfVFO->bandwidth);
// Save to config
core::configManager.acquire();
core::configManager.conf["vfos"][name]["bandwidth"] = wtfVFO->bandwidth;
core::configManager.release(true);
}
}class WaterFall {
// Texture caching strategy
bool textureNeedsUpdate = false;
uint32_t lastUpdateFrame = 0;
public:
void pushFFT() {
// Mark texture for update
textureNeedsUpdate = true;
// Rate limit texture updates
uint32_t currentFrame = ImGui::GetFrameCount();
if (currentFrame - lastUpdateFrame < 2) {
return; // Skip this update
}
lastUpdateFrame = currentFrame;
updateWaterfallTexture();
textureNeedsUpdate = false;
}
private:
void optimizedTextureUpdate() {
// Only update visible portion
int visibleLines = std::min(waterfallHeight, newLinesAvailable);
if (visibleLines > 0) {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,
dataWidth, visibleLines,
GL_RGBA, GL_UNSIGNED_BYTE,
&waterfallFb[0]);
}
}
};void WaterFall::drawVFOs() {
// Early exit if no VFOs
if (vfos.empty()) return;
// Batch VFO drawing
ImDrawList* drawList = ImGui::GetCurrentWindow()->DrawList;
// Reserve space for all VFO draw commands
int estimatedCommands = vfos.size() * 6; // ~6 draw calls per VFO
drawList->PrimReserve(estimatedCommands * 6, estimatedCommands * 4);
for (auto& [name, vfo] : vfos) {
// Skip VFOs outside visible range
if (vfo.rectMax.x < widgetPos.x ||
vfo.rectMin.x > widgetPos.x + dataWidth) {
continue;
}
// Draw VFO efficiently
drawVFOOptimized(drawList, vfo, name == selectedVFO);
}
}
void WaterFall::drawVFOOptimized(ImDrawList* drawList, WaterfallVFO& vfo, bool selected) {
// Use primitive drawing for performance
ImU32 bodyColor = selected ? vfo.color | 0xFF000000 : vfo.color;
ImU32 borderColor = selected ? IM_COL32(255, 255, 255, 255) : bodyColor;
// VFO body (filled rectangle)
drawList->AddRectFilled(vfo.rectMin, vfo.rectMax, bodyColor);
// VFO border
if (selected) {
drawList->AddRect(vfo.rectMin, vfo.rectMax, borderColor, 0.0f, 0, 2.0f);
}
// Center line (if visible)
if (vfo.lineVisible) {
drawList->AddLine(vfo.lineMin, vfo.lineMax, borderColor, 1.0f);
}
// Bandwidth handles (only if selected)
if (selected) {
drawList->AddRectFilled(vfo.lbwSelMin, vfo.lbwSelMax,
IM_COL32(255, 255, 255, 200));
drawList->AddRectFilled(vfo.rbwSelMin, vfo.rbwSelMax,
IM_COL32(255, 255, 255, 200));
}
}class WaterFall {
// Memory pools for temporary data
std::vector<ImVec2> pointPool;
std::vector<uint32_t> colorPool;
public:
WaterFall() {
// Pre-allocate pools
pointPool.reserve(MAX_DISPLAY_WIDTH);
colorPool.reserve(MAX_DISPLAY_WIDTH);
}
private:
void efficientFFTDraw() {
// Reuse pre-allocated vectors
pointPool.clear(); // No deallocation
colorPool.clear();
// Build point list
for (int i = 0; i < dataWidth; i++) {
float x = widgetPos.x + i;
float y = dbToPixel(latestFFT[i]);
pointPool.emplace_back(x, y);
}
// Single draw call
ImGui::GetWindowDrawList()->AddPolyline(
pointPool.data(), pointPool.size(),
fftColor, false, 2.0f);
}
};Modules can access full-bandwidth FFT data:
class WaterFall {
public:
// Thread-safe raw FFT access
float* acquireRawFFT(int& width) {
bufMtx.lock();
if (!rawFFTs) {
bufMtx.unlock();
return nullptr;
}
width = rawFFTSize;
// Return current FFT line
return &rawFFTs[currentFFTLine * rawFFTSize];
}
void releaseRawFFT() {
bufMtx.unlock();
}
// High-level access for modules
void withRawFFT(std::function<void(const float*, int)> callback) {
int width;
float* fft = acquireRawFFT(width);
if (fft) {
callback(fft, width);
releaseRawFFT();
}
}
};
// Usage in modules
class ScannerModule {
void analyzeSpectrum() {
gui::waterfall.withRawFFT([this](const float* fft, int size) {
// Analyze full-bandwidth FFT data
for (int i = 0; i < size; i++) {
if (fft[i] > threshold) {
detectSignal(binToFrequency(i));
}
}
});
}
};Modules can extend waterfall rendering:
class WaterFall {
// Extension points for modules
Event<ImDrawList*> onFFTRedraw;
Event<ImDrawList*> onWaterfallRedraw;
Event<ImVec2> onInputProcess;
public:
void draw() {
// ... standard drawing ...
// Allow modules to draw overlays
ImDrawList* drawList = ImGui::GetCurrentWindow()->DrawList;
onFFTRedraw.emit(drawList);
onWaterfallRedraw.emit(drawList);
}
void processInputs() {
// ... standard input processing ...
// Allow modules to handle custom input
onInputProcess.emit(ImGui::GetIO().MousePos);
}
};
// Module usage
class FrequencyManagerModule {
void init() {
// Register for waterfall extension
gui::waterfall.onFFTRedraw.bindHandler(&fftDrawHandler);
}
static void fftDrawHandler(ImDrawList* drawList, void* ctx) {
FrequencyManagerModule* module = (FrequencyManagerModule*)ctx;
module->drawFrequencyMarkers(drawList);
}
void drawFrequencyMarkers(ImDrawList* drawList) {
for (auto& marker : frequencyMarkers) {
float x = freqToPixel(marker.frequency);
ImVec2 start(x, fftArea.y);
ImVec2 end(x, fftArea.y + fftArea.height);
drawList->AddLine(start, end, marker.color, 2.0f);
// Label
drawList->AddText(ImVec2(x + 5, start.y),
marker.color, marker.label.c_str());
}
}
};class ColorMap {
struct ColorMapEntry {
std::string name;
std::vector<uint32_t> colors;
std::string author;
};
std::map<std::string, ColorMapEntry> maps;
public:
void addColorMap(const std::string& name, const float colors[][3],
int count, const std::string& author) {
ColorMapEntry entry;
entry.name = name;
entry.author = author;
entry.colors.reserve(count);
for (int i = 0; i < count; i++) {
uint8_t r = (uint8_t)(colors[i][0] * 255);
uint8_t g = (uint8_t)(colors[i][1] * 255);
uint8_t b = (uint8_t)(colors[i][2] * 255);
entry.colors.push_back(IM_COL32(r, g, b, 255));
}
maps[name] = entry;
}
void interpolateColorMap(const std::string& name, uint32_t* output, int size) {
auto it = maps.find(name);
if (it == maps.end()) return;
const auto& colors = it->second.colors;
float step = (float)(colors.size() - 1) / (size - 1);
for (int i = 0; i < size; i++) {
float pos = i * step;
int idx = (int)pos;
float t = pos - idx;
if (idx >= colors.size() - 1) {
output[i] = colors.back();
} else {
// Linear interpolation between colors
uint32_t c1 = colors[idx];
uint32_t c2 = colors[idx + 1];
output[i] = interpolateColor(c1, c2, t);
}
}
}
};class UIPerformanceMonitor {
std::deque<float> frameTimeHistory;
float maxHistorySeconds = 5.0f;
public:
void update() {
ImGuiIO& io = ImGui::GetIO();
float currentTime = io.DeltaTime;
frameTimeHistory.push_back(currentTime);
// Remove old entries
float totalTime = 0;
for (float time : frameTimeHistory) totalTime += time;
while (totalTime > maxHistorySeconds && frameTimeHistory.size() > 1) {
totalTime -= frameTimeHistory.front();
frameTimeHistory.pop_front();
}
}
void drawMetrics() {
if (ImGui::Begin("Performance")) {
ImGuiIO& io = ImGui::GetIO();
ImGui::Text("FPS: %.1f", io.Framerate);
ImGui::Text("Frame Time: %.3f ms", io.DeltaTime * 1000);
// Frame time graph
std::vector<float> times(frameTimeHistory.begin(), frameTimeHistory.end());
ImGui::PlotLines("Frame Time", times.data(), times.size(),
0, nullptr, 0.0f, 0.033f, ImVec2(300, 100));
// Memory usage
ImGui::Text("Draw Calls: %d", ImGui::GetDrawData()->CmdListsCount);
ImGui::Text("Vertices: %d", getTotalVertexCount());
}
ImGui::End();
}
private:
int getTotalVertexCount() {
int total = 0;
ImDrawData* drawData = ImGui::GetDrawData();
for (int i = 0; i < drawData->CmdListsCount; i++) {
total += drawData->CmdLists[i]->VtxBuffer.Size;
}
return total;
}
};class UIMemoryTracker {
static size_t totalAllocations;
static size_t totalDeallocations;
static std::map<void*, size_t> activeAllocations;
public:
static void* trackedAlloc(size_t size) {
void* ptr = malloc(size);
activeAllocations[ptr] = size;
totalAllocations++;
return ptr;
}
static void trackedFree(void* ptr) {
auto it = activeAllocations.find(ptr);
if (it != activeAllocations.end()) {
activeAllocations.erase(it);
totalDeallocations++;
}
free(ptr);
}
static void reportLeaks() {
size_t leakedBytes = 0;
for (auto& [ptr, size] : activeAllocations) {
leakedBytes += size;
}
if (leakedBytes > 0) {
flog::warn("UI Memory leaks detected: {} bytes in {} allocations",
leakedBytes, activeAllocations.size());
}
}
};This comprehensive UI Internals page covers the deep implementation details needed for advanced UI development in SDR++CE. You can copy this content directly into your GitHub Wiki as the "UI Internals" page.