Skip to content

UI Internals

Lunae Mons Research edited this page Sep 1, 2025 · 1 revision

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.

MainWindow::draw() - The UI Orchestrator

The MainWindow::draw() function is called at 60 FPS and orchestrates the entire UI rendering process:

Execution Flow

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();
}

State Synchronization Logic

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);
    }
}

Window and Layout Setup

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();
}

Top Bar Implementation

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();
    }
}

Menu System Architecture

Dynamic Menu Construction

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 Configuration

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}
    ]
}

Waterfall Widget Deep Dive

Architecture Overview

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;
};

Custom Rendering Pipeline

FFT Display Rendering

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);
    }
}

Waterfall Texture Management

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));
}

Input Processing State Machine

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;
}

WaterfallVFO UI Model

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);
};

VFO Update Flow

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);
    }
}

Performance Optimizations

Texture Management

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]);
        }
    }
};

VFO Rendering Optimization

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));
    }
}

Memory Usage Optimization

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);
    }
};

Advanced Features

Raw FFT Access for Modules

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));
                }
            }
        });
    }
};

Module Drawing Extensions

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());
        }
    }
};

Custom Colormap Support

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);
            }
        }
    }
};

Debugging UI Performance

Frame Rate Analysis

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;
    }
};

Memory Leak Detection

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.

Clone this wiki locally