#include "ui.h" #include "synth.h" #include "sprites_data.h" #include "font_data.h" #include #include #include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif #define SPRITE_SCALE 2 #define TILE_SIZE 8 #define KNOB_SIZE 16 #define TITLE_BAR_H (TILE_SIZE * SPRITE_SCALE + 4) /* fits 16-px sprite buttons */ // File-local text helpers — ui must be in scope at every call site #define DRAW_TEXT(txt, x, y, sz, col) \ DrawTextEx(ui->font, (txt), (Vector2){(float)(x),(float)(y)}, (float)(sz), 0, (col)) #define MEASURE_TEXT(txt, sz) \ ((int)MeasureTextEx(ui->font, (txt), (float)(sz), 0).x) // ------------------------------------------------------------------ CONTROL REGISTRY static void registerControl(UIState *ui, int id, float *ptr, float min, float max) { for (int i = 0; i < ui->controlRegistryCount; i++) { if (ui->controlRegistry[i].id == id) return; // already registered } if (ui->controlRegistryCount >= 128) return; ui->controlRegistry[ui->controlRegistryCount++] = (UIControlEntry){id, ptr, min, max}; // Resolve any pending CC mappings that were waiting for this control for (int i = 0; i < ui->pendingCount; i++) { if (ui->pendingMappings[i].controlId == id) { int cc = ui->pendingMappings[i].cc; if (cc >= 0 && cc < 128) { ui->midi->ccMappings[cc].active = 1; ui->midi->ccMappings[cc].valuePtr = ptr; ui->midi->ccMappings[cc].min = ui->pendingMappings[i].min; ui->midi->ccMappings[cc].max = ui->pendingMappings[i].max; ui->midi->ccMappings[cc].controlId = id; } // Shift-compact the pending list for (int j = i; j < ui->pendingCount - 1; j++) ui->pendingMappings[j] = ui->pendingMappings[j + 1]; ui->pendingCount--; i--; } } } // ------------------------------------------------------------------ INTERNAL static Rectangle getTile(SpriteIndex index) { if (index == SPRITE_KNOB) return (Rectangle){ 72, 0, KNOB_SIZE, KNOB_SIZE }; return (Rectangle){ index * TILE_SIZE, 0, TILE_SIZE, TILE_SIZE }; } static void drawTile( UIState *ui, SpriteIndex index, float x, float y, Color tint) { Rectangle src = getTile(index); Rectangle dest = { x, y, src.width * SPRITE_SCALE, src.height * SPRITE_SCALE }; DrawTexturePro(ui->sprites, src, dest, (Vector2){0, 0}, 0.0f, tint); } static void drawTileRotated( UIState *ui, SpriteIndex index, float x, float y, float rotation, Color tint) { Rectangle src = getTile(index); float w = src.width * SPRITE_SCALE; float h = src.height * SPRITE_SCALE; Rectangle dest = { x + w * 0.5f, y + h * 0.5f, w, h }; DrawTexturePro(ui->sprites, src, dest, (Vector2){ w * 0.5f, h * 0.5f }, rotation, tint); } // ------------------------------------------------------------------ LIFECYCLE static Font loadEmbeddedFont(void) { const int glyphCount = 224, glyphW = 8, glyphH = 8, cols = 16; Image img = LoadImageFromMemory(".png", font_png, (int)font_png_len); Font f = { 0 }; f.baseSize = glyphH; f.glyphCount = glyphCount; f.glyphs = (GlyphInfo *)MemAlloc(glyphCount * sizeof(GlyphInfo)); f.recs = (Rectangle *)MemAlloc(glyphCount * sizeof(Rectangle)); for (int i = 0; i < glyphCount; i++) { f.recs[i] = (Rectangle){ (float)((i % cols) * glyphW), (float)((i / cols) * glyphH), glyphW, glyphH }; f.glyphs[i].value = 32 + i; f.glyphs[i].offsetX = 0; f.glyphs[i].offsetY = 0; f.glyphs[i].advanceX = glyphW; } f.texture = LoadTextureFromImage(img); SetTextureFilter(f.texture, TEXTURE_FILTER_POINT); UnloadImage(img); return f; } void uiInit(UIState *ui, MidiState *midi, Camera2D *camera) { Image spriteImg = LoadImageFromMemory(".png", sprites_png, (int)sprites_png_len); ui->sprites = LoadTextureFromImage(spriteImg); UnloadImage(spriteImg); SetTextureFilter(ui->sprites, TEXTURE_FILTER_POINT); ui->font = loadEmbeddedFont(); ui->midi = midi; ui->aboutMenuOpen = 0; ui->midiMenuOpen = 0; ui->midiMenuSelected = -1; ui->draggingKnob = -1; ui->draggingSlider = -1; ui->dragStartValue = 0.0f; ui->camera = camera; ui->midiLearnActive = 0; ui->midiLearnTargetId = -1; ui->midiLearnValuePtr = NULL; ui->midiLearnMin = 0.0f; ui->midiLearnMax = 1.0f; ui->patchMenuOpen = 0; ui->focusedTextInput = -1; ui->patchConfirmOverwrite = 0; ui->patchConfirmClear = 0; ui->patchSaveName[0] = '\0'; ui->patchCurrentName[0] = '\0'; ui->patchFileCount = 0; ui->patchScrollOffset = 0; ui->patchShowFavsOnly = 0; ui->patchFavCount = patchLoadFavs(ui->patchFavs, PATCH_MAX_FILES); ui->patchCurrentDir[0] = '\0'; ui->patchSubDirCount = 0; ui->patchVCursor = -1; ui->controlRegistryCount = 0; ui->pendingCount = 0; } void uiClose( UIState *ui) { UnloadTexture(ui->sprites); UnloadFont(ui->font); } // Window helpers: void recomputeViewPort(Camera2D* camera, int x_logical_resolution, int y_logical_resolution, int border_width){ float x_scale = (GetScreenWidth() / (float)(x_logical_resolution + 2*border_width)); float y_scale = (GetScreenHeight() / (float)(y_logical_resolution + 2*border_width)); // Set the scale. if (x_scale > y_scale) { camera->zoom = y_scale; } else { camera->zoom= x_scale; } // Center the active drawing space in the window. camera->target = (Vector2){ ((x_logical_resolution * camera->zoom - GetScreenWidth()) / (2.0f * camera->zoom)), ((y_logical_resolution * camera->zoom - GetScreenHeight()) / (2.0f * camera->zoom)) }; } // ------------------------------------------------------------------ PRIMITIVES float uiKnob(UIState *ui, int id, float x, float y, float *ptr, float min, float max, const char *label) { registerControl(ui, id, ptr, min, max); float value = *ptr; float scaledSize = KNOB_SIZE * SPRITE_SCALE; int fontSize = 8; // Highlight ring when this knob is the selected MIDI learn target if (ui->midiLearnActive == 2 && id == ui->midiLearnTargetId) { int pulse = (int)(GetTime() * 4.0) % 2; Color ring = pulse ? ORANGE : (Color){180, 90, 0, 255}; DrawRectangleLines((int)(x - 3), (int)(y - 3), (int)(scaledSize + 6), (int)(scaledSize + 6), ring); } // Map value to rotation angle float t = (value - min) / (max - min); float rotation = -135.0f + t * 270.0f - 225.0f; drawTileRotated(ui, SPRITE_KNOB, x, y, rotation, WHITE); int textWidth = MEASURE_TEXT(label, fontSize); DRAW_TEXT(label, (int)(x + scaledSize * 0.5f - textWidth * 0.5f), (int)(y + scaledSize + 2), fontSize, RAYWHITE); Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); float cx = x + scaledSize * 0.5f; float cy = y + scaledSize * 0.5f; float dist = sqrtf((mouse.x - cx) * (mouse.x - cx) + (mouse.y - cy) * (mouse.y - cy)); // MIDI learn: intercept click in select mode if (ui->midiLearnActive == 1 && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && dist < scaledSize * 0.5f) { ui->midiLearnValuePtr = ptr; ui->midiLearnMin = min; ui->midiLearnMax = max; ui->midiLearnTargetId = id; ui->midiLearnActive = 2; atomic_store(&ui->midi->midiLearnMode, 1); atomic_store(&ui->midi->midiLearnCC, -1); return value; } if (ui->midiLearnActive == 0) { if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && dist < scaledSize * 0.5f) { ui->draggingKnob = id; ui->dragStart = mouse; ui->dragStartValue = value; } if (ui->draggingKnob == id) { if (IsKeyDown(KEY_LEFT_CONTROL)) { value = min + (max - min) * 0.5f; ui->dragStartValue = value; ui->dragStart = mouse; } else if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { float delta = (ui->dragStart.y - mouse.y) * ((max - min) / 200.0f); value = ui->dragStartValue + delta; if (value < min) value = min; if (value > max) value = max; } else { ui->draggingKnob = -1; } } } *ptr = value; return value; } float uiSlider(UIState *ui, int id, float x, float y, float width, float *ptr, float min, float max, const char *label) { registerControl(ui, id, ptr, min, max); float value = *ptr; float scaledTile = TILE_SIZE * SPRITE_SCALE; int fontSize = 8; float trackHeight = scaledTile * 0.25f; float trackY = y + scaledTile * 0.5f - trackHeight * 0.5f; // Highlight when selected MIDI learn target if (ui->midiLearnActive == 2 && id == ui->midiLearnTargetId) { int pulse = (int)(GetTime() * 4.0) % 2; Color ring = pulse ? ORANGE : (Color){180, 90, 0, 255}; DrawRectangleLines((int)(x - 2), (int)(y - 2), (int)(width + 4), (int)(scaledTile + 4), ring); } DrawRectangle((int)x, (int)trackY, (int)width, (int)trackHeight, DARKGRAY); float t = (value - min) / (max - min); float handleX = x + t * (width - scaledTile); drawTile(ui, SPRITE_SLIDER, handleX, y, WHITE); int textWidth = MEASURE_TEXT(label, fontSize); DRAW_TEXT(label, (int)(x + width * 0.5f - textWidth * 0.5f), (int)(y + scaledTile + 2), fontSize, RAYWHITE); Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); int onTrack = (mouse.x >= x && mouse.x <= x + width && mouse.y >= y && mouse.y <= y + scaledTile); // MIDI learn: intercept click in select mode if (ui->midiLearnActive == 1 && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && onTrack) { ui->midiLearnValuePtr = ptr; ui->midiLearnMin = min; ui->midiLearnMax = max; ui->midiLearnTargetId = id; ui->midiLearnActive = 2; atomic_store(&ui->midi->midiLearnMode, 1); atomic_store(&ui->midi->midiLearnCC, -1); return value; } if (ui->midiLearnActive == 0) { if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && onTrack) { ui->draggingSlider = id; ui->dragStart = mouse; ui->dragStartValue = value; } if (ui->draggingSlider == id) { if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { float delta = (mouse.x - ui->dragStart.x) * ((max - min) / width); value = ui->dragStartValue + delta; if (value < min) value = min; if (value > max) value = max; } else { ui->draggingSlider = -1; } } } *ptr = value; return value; } int uiWaveformSelector( UIState *ui, float x, float y, int currentWaveform) { float scaledTile = TILE_SIZE * SPRITE_SCALE; int result = currentWaveform; for (int i = 0; i < 6; i++) { float tx = x + i * (scaledTile + 2); bool active = (i == currentWaveform); Color tint = active ? GREEN : GRAY; Rectangle dest = { tx, y, scaledTile, scaledTile }; Rectangle src = getTile((SpriteIndex)i); DrawTexturePro(ui->sprites, src, dest, (Vector2){0, 0}, 0.0f, tint); // Click to select Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= tx && mouse.x <= tx + scaledTile && mouse.y >= y && mouse.y <= y + scaledTile) { result = i; } } return result; } void uiADSRShape(float x, float y, float width, float height, float attack, float decay, float sustain, float release, Color color) { // Normalize times to fractions of the display width float total = attack + decay + release + 0.001f; float aFrac = attack / total; float dFrac = decay / total; float rFrac = release / total; float sFrac = 1.0f - aFrac - dFrac - rFrac; // Key points Vector2 p0 = { x, y + height }; Vector2 p1 = { x + aFrac * width, y }; Vector2 p2 = { x + (aFrac + dFrac) * width, y + (1.0f - sustain) * height }; Vector2 p3 = { x + (aFrac + dFrac + sFrac) * width, y + (1.0f - sustain) * height }; Vector2 p4 = { x + width, y + height }; DrawLineV(p0, p1, color); DrawLineV(p1, p2, color); DrawLineV(p2, p3, color); DrawLineV(p3, p4, color); } void uiVoiceMeter( UIState *ui, float x, float y, Voice *voices, int voiceCount) { float scaledTile = TILE_SIZE * SPRITE_SCALE; for (int i = 0; i < voiceCount; i++) { float tx = x + i * (scaledTile + 2); Color tint = voices[i].active ? GREEN : GRAY; drawTile(ui, SPRITE_BTN_GREEN, tx, y, tint); } } void uiOscillatorPanel( UIState *ui, int baseId, float x, float y, float width, float height, Oscillator *osc, const char *title) { float scaledTile = TILE_SIZE * SPRITE_SCALE; float padding = 6.0f; int fontSize = 8; // Panel background and border DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); DrawRectangleLines((int)x, (int)y, (int)width, (int)height, DARKGRAY); // Title bar DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); DRAW_TEXT(title, (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); // Active button float activeBtnX = x + width - scaledTile - padding; float activeBtnY = y + 2; drawTile(ui, osc->active ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, activeBtnX, activeBtnY, WHITE); DRAW_TEXT("Active", (int)(activeBtnX - MEASURE_TEXT("Active", fontSize) - 4), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= activeBtnX && mouse.x <= activeBtnX + scaledTile && mouse.y >= activeBtnY && mouse.y <= activeBtnY + scaledTile) { osc->active = !osc->active; } float contentY = y + TITLE_BAR_H + padding; // Waveform selector DRAW_TEXT("Wave", (int)(x + padding), (int)contentY, fontSize, GRAY); contentY += fontSize + 4; osc->waveform = uiWaveformSelector(ui, x + padding, contentY, osc->waveform); contentY += scaledTile + padding * 2; // Divider DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += padding; // Knobs float knobSize = KNOB_SIZE * SPRITE_SCALE; float knobSpacing = (width - padding * 2) / 3.0f; float knobY = contentY; float knobOffset = knobSpacing * 0.5f - knobSize * 0.5f; uiKnob(ui, baseId + 0, x + padding + knobSpacing * 0.0f + knobOffset, knobY, &osc->dutyCycle, 0.05f, 0.95f, "PWidth"); uiKnob(ui, baseId + 1, x + padding + knobSpacing * 1.0f + knobOffset, knobY, &osc->detune, -100.0f, 100.0f, "Tune"); uiKnob(ui, baseId + 2, x + padding + knobSpacing * 2.0f + knobOffset, knobY, &osc->gain, 0.0f, OSC_MAX_GAIN, "Gain"); contentY += knobSize + fontSize + padding * 2; // Divider DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += padding; // Modulation section — octave buttons share this header row DRAW_TEXT("Modulation", (int)(x + padding), (int)contentY, fontSize, GRAY); { const char *octLabels[] = { "-2", "-1", "0", "+1", "+2" }; float octStep = scaledTile + 1; float octX = x + width - padding - 5.0f * octStep + 1; int lblFont = 8; for (int o = 0; o < 5; o++) { float bx = octX + o * octStep; int oval = o - 2; int sel = (osc->octave == oval); drawTile(ui, sel ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, bx, contentY, sel ? GREEN : GRAY); int lw = MEASURE_TEXT(octLabels[o], lblFont); DRAW_TEXT(octLabels[o], (int)(bx + (scaledTile - lw) * 0.5f), (int)(contentY + (scaledTile - lblFont) * 0.5f), lblFont, RAYWHITE); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= bx && mouse.x <= bx + scaledTile && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { osc->octave = oval; } } } contentY += fontSize + 24; const char *modParams[] = { "PWidth", "Tune", "Gain" }; for (int i = 0; i < 3; i++) { DRAW_TEXT(modParams[i], (int)(x + padding * 2), (int)contentY, fontSize, RAYWHITE); // Depth knob — sits to the right of the label, left of the buttons float knobSize = KNOB_SIZE * SPRITE_SCALE; float knobX = x + padding * 2 + MEASURE_TEXT("PWidth", fontSize) + 8; float knobY = contentY - knobSize * 0.25f; // osc->modDepth[i] = uiKnob(ui, baseId + 10 + i, // knobX, knobY, // osc->modDepth[i], -1.0f, 1.0f, ""); float depthMin, depthMax; switch (i) { case 1: // Detune, the only weird case. depthMin = -1200.0f; depthMax = 1200.0f; break; case 2: depthMin = -OSC_MAX_GAIN; depthMax = OSC_MAX_GAIN; break; default: depthMin = -1.0f; depthMax = 1.0f; } uiKnob(ui, baseId + 10 + i, knobX, knobY, &osc->modDepth[i], depthMin, depthMax, ""); // Routing buttons — E0, E1, L0, L1 float btnX = x + width - (scaledTile + 2) * 4 - padding; const char *modLabels[] = { "E0", "E1", "L0", "L1" }; ModSource sources[] = { MOD_SOURCE_AMP_ENV, MOD_SOURCE_MOD_ENV, MOD_SOURCE_LFO, MOD_SOURCE_LFO2 }; for (int b = 0; b < 4; b++) { float bx = btnX + b * (scaledTile + 2); bool enabled = (osc->modRouting[i] == (int)sources[b]); Color tint = enabled ? GREEN : GRAY; drawTile(ui, enabled ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, bx, contentY, tint); DRAW_TEXT(modLabels[b], (int)(bx + 1), (int)(contentY + (scaledTile - 8) * 0.5f), 8, RAYWHITE); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= bx && mouse.x <= bx + scaledTile && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { osc->modRouting[i] = enabled ? MOD_SOURCE_NONE : sources[b]; } } contentY += scaledTile + 24; } } void uiEnvelopePanel( UIState *ui, int baseId, float x, float y, float width, float height, Envelope *env, const char *title) { float scaledTile = TILE_SIZE * SPRITE_SCALE; float padding = 6.0f; int fontSize = 8; // Panel background and border DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); DrawRectangleLines((int)x, (int)y, (int)width, (int)height, DARKGRAY); // Title bar DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); DRAW_TEXT(title, (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); float contentY = y + TITLE_BAR_H + padding; // ADSR shape visualizer float visWidth = width - padding * 2; float visHeight = 40.0f; DrawRectangle((int)(x + padding), (int)contentY, (int)visWidth, (int)visHeight, (Color){20, 20, 20, 255}); DrawRectangleLines((int)(x + padding), (int)contentY, (int)visWidth, (int)visHeight, DARKGRAY); uiADSRShape(x + padding, contentY, visWidth, visHeight, env->attackSec, env->decaySec, env->sustainLevel, env->releaseSec, GREEN); contentY += visHeight + padding; // Divider DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += padding; // ADSR sliders float sliderWidth = width - padding * 2; // Attack DRAW_TEXT("A", (int)(x + padding), (int)contentY, fontSize, RAYWHITE); uiSlider(ui, baseId + 0, x + padding + fontSize + 4, contentY, sliderWidth - fontSize - 4, &env->attackSec, 0.001f, 2.0f, ""); contentY += scaledTile + fontSize + padding; // Decay DRAW_TEXT("D", (int)(x + padding), (int)contentY, fontSize, RAYWHITE); uiSlider(ui, baseId + 1, x + padding + fontSize + 4, contentY, sliderWidth - fontSize - 4, &env->decaySec, 0.001f, 2.0f, ""); contentY += scaledTile + fontSize + padding; // Sustain DRAW_TEXT("S", (int)(x + padding), (int)contentY, fontSize, RAYWHITE); uiSlider(ui, baseId + 2, x + padding + fontSize + 4, contentY, sliderWidth - fontSize - 4, &env->sustainLevel, 0.0f, 1.0f, ""); contentY += scaledTile + fontSize + padding; // Release DRAW_TEXT("R", (int)(x + padding), (int)contentY, fontSize, RAYWHITE); uiSlider(ui, baseId + 3, x + padding + fontSize + 4, contentY, sliderWidth - fontSize - 4, &env->releaseSec, 0.001f, 2.0f, ""); contentY += scaledTile + fontSize + padding; // Divider DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += padding; // Current stage indicator const char *stageNames[] = { "IDLE", "ATTACK", "DECAY", "SUSTAIN", "RELEASE" }; Color stageColors[] = { GRAY, GREEN, YELLOW, SKYBLUE, RED }; DRAW_TEXT("Stage:", (int)(x + padding), (int)contentY, fontSize, GRAY); DRAW_TEXT(stageNames[env->stage], (int)(x + padding + MEASURE_TEXT("Stage: ", fontSize)), (int)contentY, fontSize, stageColors[env->stage]); } static int uiTextInput(UIState *ui, int id, float x, float y, float width, char *buf, int bufSize) { float scaledTile = TILE_SIZE * SPRITE_SCALE; int fontSize = 8; int focused = (ui->focusedTextInput == id); Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (mouse.x >= x && mouse.x <= x + width && mouse.y >= y && mouse.y <= y + scaledTile) { ui->focusedTextInput = id; focused = 1; } else if (focused) { ui->focusedTextInput = -1; focused = 0; } } if (focused) { int len = (int)strlen(buf); int ch; while ((ch = GetCharPressed()) > 0) { if (ch >= 32 && ch < 127 && len + 1 < bufSize) { buf[len++] = (char)ch; buf[len] = '\0'; } } if (IsKeyPressed(KEY_BACKSPACE) && len > 0) buf[--len] = '\0'; } Color borderCol = focused ? BLUE : DARKGRAY; DrawRectangle((int)x, (int)y, (int)width, (int)scaledTile, (Color){20, 20, 30, 255}); DrawRectangleLines((int)x, (int)y, (int)width, (int)scaledTile, borderCol); DRAW_TEXT(buf, (int)(x + 4), (int)(y + 2), fontSize, RAYWHITE); if (focused && (int)(GetTime() * 2.0) % 2 == 0) { int tw = MEASURE_TEXT(buf, fontSize); DRAW_TEXT("|", (int)(x + 4 + tw), (int)(y + 2), fontSize, RAYWHITE); } return focused && IsKeyPressed(KEY_ENTER); } static void uiSaveCcMappings(UIState *ui) { const char *devName = NULL; for (int i = 0; i < ui->midi->inputCount; i++) { if (ui->midi->inputs[i].client == ui->midi->connectedClient && ui->midi->inputs[i].port == ui->midi->connectedPort) { devName = ui->midi->inputs[i].clientName; break; } } if (!devName) return; ConfigCcEntry entries[128]; int count = 0; for (int cc = 0; cc < 128 && count < 128; cc++) { if (ui->midi->ccMappings[cc].active) { entries[count].cc = cc; entries[count].controlId = ui->midi->ccMappings[cc].controlId; entries[count].min = ui->midi->ccMappings[cc].min; entries[count].max = ui->midi->ccMappings[cc].max; count++; } } configSaveCcMappings(devName, entries, count); } void uiLoadCcMappingsForDevice(UIState *ui, const char *deviceName) { for (int i = 0; i < 128; i++) ui->midi->ccMappings[i].active = 0; ui->pendingCount = 0; ConfigCcEntry entries[128]; int count = configLoadCcMappings(deviceName, entries, 128); for (int i = 0; i < count; i++) { int resolved = 0; for (int j = 0; j < ui->controlRegistryCount; j++) { if (ui->controlRegistry[j].id == entries[i].controlId) { int cc = entries[i].cc; ui->midi->ccMappings[cc].active = 1; ui->midi->ccMappings[cc].valuePtr = ui->controlRegistry[j].valuePtr; ui->midi->ccMappings[cc].min = entries[i].min; ui->midi->ccMappings[cc].max = entries[i].max; ui->midi->ccMappings[cc].controlId = entries[i].controlId; resolved = 1; break; } } if (!resolved && ui->pendingCount < 128) ui->pendingMappings[ui->pendingCount++] = entries[i]; } } void uiMidiMenu(UIState *ui, float x, float y, float width, MidiState *midi) { if (!ui->midiMenuOpen) return; float padding = 6.0f; int fontSize = 8; float scaledTile = TILE_SIZE * SPRITE_SCALE; float rowH = (float)(fontSize + 8); float btnW = width - padding * 2; int visRows = midi->inputCount > 0 ? midi->inputCount : 1; if (visRows > 10) visRows = 10; float height = (float)TITLE_BAR_H + padding // title bar + scaledTile + padding // rescan button + visRows * rowH + padding // device rows + scaledTile + padding; // close button // Dim the rest of the UI { float invZ = 1.0f / ui->camera->zoom; Vector2 org = GetScreenToWorld2D((Vector2){0.0f, 0.0f}, *ui->camera); DrawRectangle((int)org.x, (int)org.y, (int)(GetScreenWidth() * invZ), (int)(GetScreenHeight() * invZ), (Color){0, 0, 0, 140}); } // Panel background DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); DrawRectangleLines((int)x, (int)y, (int)width, (int)height, LIGHTGRAY); // Title bar DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); DRAW_TEXT("MIDI Devices", (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); // Close button in title bar float closeBtnX = x + width - scaledTile - padding; float closeBtnY = y + 2; drawTile(ui, SPRITE_BTN_RED, closeBtnX, closeBtnY, WHITE); DRAW_TEXT("X", (int)(closeBtnX + (scaledTile - MEASURE_TEXT("X", fontSize)) * 0.5f), (int)(closeBtnY + (scaledTile - fontSize) * 0.5f), fontSize, RAYWHITE); Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= closeBtnX && mouse.x <= closeBtnX + scaledTile && mouse.y >= closeBtnY && mouse.y <= closeBtnY + scaledTile) { ui->midiMenuOpen = 0; } float contentY = y + TITLE_BAR_H + padding; // Rescan button DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, (Color){55, 55, 75, 255}); DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, GRAY); int rescanTextW = MEASURE_TEXT("Rescan", fontSize); DRAW_TEXT("Rescan", (int)(x + padding + btnW * 0.5f - rescanTextW * 0.5f), (int)(contentY + 2), fontSize, RAYWHITE); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= x + padding && mouse.x <= x + padding + btnW && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { midiScanInputs(midi); } contentY += scaledTile + padding; // Device rows if (midi->inputCount == 0) { DRAW_TEXT("No MIDI inputs found.", (int)(x + padding), (int)(contentY + 2), fontSize, GRAY); contentY += rowH; } for (int i = 0; i < midi->inputCount && i < 10; i++) { MidiInputInfo *info = &midi->inputs[i]; bool active = (midi->connectedClient == info->client && midi->connectedPort == info->port); Color rowBg = active ? (Color){20, 60, 20, 255} : (Color){45, 45, 45, 255}; Color rowEdge= active ? GREEN : DARKGRAY; Color textCol= active ? GREEN : RAYWHITE; DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), rowBg); DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), rowEdge); DRAW_TEXT(TextFormat("[%d:%d] %s", info->client, info->port, info->clientName), (int)(x + padding + 4), (int)(contentY + 2), fontSize, textCol); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= x + padding && mouse.x <= x + padding + btnW && mouse.y >= contentY && mouse.y <= contentY + rowH - 2) { if (active) { midiDevDisconnect(midi); for (int j = 0; j < 128; j++) ui->midi->ccMappings[j].active = 0; ui->pendingCount = 0; } else { midiDevConnect(midi, info->client, info->port); configSaveLastDevice(info->clientName); uiLoadCcMappingsForDevice(ui, info->clientName); } } contentY += rowH; } contentY += padding; // Close button at bottom DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, (Color){70, 40, 40, 255}); DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, DARKGRAY); int closeTextW = MEASURE_TEXT("Close", fontSize); DRAW_TEXT("Close", (int)(x + padding + btnW * 0.5f - closeTextW * 0.5f), (int)(contentY + 2), fontSize, RAYWHITE); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= x + padding && mouse.x <= x + padding + btnW && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { ui->midiMenuOpen = 0; } } static void buildPatchPath(char *out, size_t sz, const char *dir, const char *name) { if (dir[0]) snprintf(out, sz, "patches/%s/%s.json", dir, name); else snprintf(out, sz, "patches/%s.json", name); } static void patchRescan(UIState *ui) { ui->patchFileCount = patchScanDir( ui->patchCurrentDir, ui->patchFiles, PATCH_MAX_FILES, ui->patchSubDirs, PATCH_MAX_DIRS, &ui->patchSubDirCount); ui->patchScrollOffset = 0; ui->patchVCursor = -1; } void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s) { if (!ui->patchMenuOpen) return; float padding = 6.0f; int fontSize = 8; float scaledTile = TILE_SIZE * SPRITE_SCALE; float rowH = (float)(fontSize + 8); float saveW = 60.0f; float btnW = width - padding * 2; float inputW = btnW - 4 - saveW; const int MAX_VIS = 8; float height = (float)TITLE_BAR_H + padding + scaledTile + padding + 1 + padding + scaledTile + padding // filter toggle row + MAX_VIS * rowH + padding + 1 + padding + scaledTile + padding; { float invZ = 1.0f / ui->camera->zoom; Vector2 org = GetScreenToWorld2D((Vector2){0.0f, 0.0f}, *ui->camera); DrawRectangle((int)org.x, (int)org.y, (int)(GetScreenWidth() * invZ), (int)(GetScreenHeight() * invZ), (Color){0, 0, 0, 140}); } DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); DrawRectangleLines((int)x, (int)y, (int)width, (int)height, LIGHTGRAY); DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); { char titleBuf[280]; if (ui->patchCurrentDir[0]) snprintf(titleBuf, sizeof(titleBuf), "Patches / %s", ui->patchCurrentDir); else snprintf(titleBuf, sizeof(titleBuf), "Patches"); DRAW_TEXT(titleBuf, (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); } float closeBtnX = x + width - scaledTile - padding; float closeBtnY = y + 2; drawTile(ui, SPRITE_BTN_RED, closeBtnX, closeBtnY, WHITE); DRAW_TEXT("X", (int)(closeBtnX + (scaledTile - MEASURE_TEXT("X", fontSize)) * 0.5f), (int)(closeBtnY + (scaledTile - fontSize) * 0.5f), fontSize, RAYWHITE); Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= closeBtnX && mouse.x <= closeBtnX + scaledTile && mouse.y >= closeBtnY && mouse.y <= closeBtnY + scaledTile) { ui->patchMenuOpen = 0; ui->focusedTextInput = -1; ui->patchConfirmOverwrite = 0; ui->patchConfirmClear = 0; return; } float contentY = y + TITLE_BAR_H + padding; if (!ui->patchConfirmOverwrite) { // Normal: text input + Save button int enterPressed = uiTextInput(ui, 900, x + padding, contentY, inputW, ui->patchSaveName, PATCH_NAME_LEN); float saveX = x + padding + inputW + 4; int canSave = (ui->patchSaveName[0] != '\0'); Color saveBg = canSave ? (Color){20, 60, 20, 255} : (Color){45, 45, 45, 255}; DrawRectangle((int)saveX, (int)contentY, (int)saveW, (int)scaledTile, saveBg); DrawRectangleLines((int)saveX, (int)contentY, (int)saveW, (int)scaledTile, canSave ? GREEN : DARKGRAY); int saveTW = MEASURE_TEXT("Save", fontSize); DRAW_TEXT("Save", (int)(saveX + saveW * 0.5f - saveTW * 0.5f), (int)(contentY + 2), fontSize, canSave ? RAYWHITE : GRAY); int clickSave = IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= saveX && mouse.x <= saveX + saveW && mouse.y >= contentY && mouse.y <= contentY + scaledTile; if (canSave && (enterPressed || clickSave)) { int exists = 0; for (int i = 0; i < ui->patchFileCount; i++) { if (strcmp(ui->patchFiles[i], ui->patchSaveName) == 0) { exists = 1; break; } } if (exists) { ui->patchConfirmOverwrite = 1; ui->focusedTextInput = -1; } else { char path[512]; buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchSaveName); if (patchSave(s, path)) { snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchSaveName); patchRescan(ui); } } } } else { // Confirm overwrite float yesW = 44.0f, noW = 44.0f; char prompt[PATCH_NAME_LEN + 16]; snprintf(prompt, sizeof(prompt), "Overwrite '%s'?", ui->patchSaveName); DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, (Color){50, 35, 15, 255}); DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)scaledTile, ORANGE); DRAW_TEXT(prompt, (int)(x + padding + 4), (int)(contentY + 2), fontSize, ORANGE); float yesX = x + width - padding - noW - 4 - yesW; float noX = x + width - padding - noW; DrawRectangle((int)yesX, (int)contentY, (int)yesW, (int)scaledTile, (Color){20, 60, 20, 255}); DrawRectangleLines((int)yesX, (int)contentY, (int)yesW, (int)scaledTile, GREEN); int yesTW = MEASURE_TEXT("Yes", fontSize); DRAW_TEXT("Yes", (int)(yesX + yesW * 0.5f - yesTW * 0.5f), (int)(contentY + 2), fontSize, RAYWHITE); DrawRectangle((int)noX, (int)contentY, (int)noW, (int)scaledTile, (Color){70, 40, 40, 255}); DrawRectangleLines((int)noX, (int)contentY, (int)noW, (int)scaledTile, DARKGRAY); int noTW = MEASURE_TEXT("No", fontSize); DRAW_TEXT("No", (int)(noX + noW * 0.5f - noTW * 0.5f), (int)(contentY + 2), fontSize, RAYWHITE); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (mouse.x >= yesX && mouse.x <= yesX + yesW && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { char path[512]; buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchSaveName); if (patchSave(s, path)) { snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchSaveName); patchRescan(ui); } ui->patchConfirmOverwrite = 0; } else if (mouse.x >= noX && mouse.x <= noX + noW && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { ui->patchConfirmOverwrite = 0; } } } contentY += scaledTile + padding; DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += 1 + padding; // Filter toggle: [All] [* Favs] float filterBtnW = (btnW - 4) * 0.5f; float allBtnX = x + padding; float favBtnX = allBtnX + filterBtnW + 4; Color allBg = !ui->patchShowFavsOnly ? (Color){40,40,60,255} : (Color){25,25,35,255}; Color favBg = ui->patchShowFavsOnly ? (Color){60,55,15,255} : (Color){25,25,35,255}; Color allEdge = !ui->patchShowFavsOnly ? LIGHTGRAY : DARKGRAY; Color favEdge = ui->patchShowFavsOnly ? YELLOW : DARKGRAY; Color allText = !ui->patchShowFavsOnly ? RAYWHITE : GRAY; Color favText = ui->patchShowFavsOnly ? YELLOW : GRAY; DrawRectangle((int)allBtnX, (int)contentY, (int)filterBtnW, (int)scaledTile, allBg); DrawRectangleLines((int)allBtnX, (int)contentY, (int)filterBtnW, (int)scaledTile, allEdge); { int tw = MEASURE_TEXT("All", fontSize); DRAW_TEXT("All", (int)(allBtnX + filterBtnW*0.5f - tw*0.5f), (int)(contentY+2), fontSize, allText); } DrawRectangle((int)favBtnX, (int)contentY, (int)filterBtnW, (int)scaledTile, favBg); DrawRectangleLines((int)favBtnX, (int)contentY, (int)filterBtnW, (int)scaledTile, favEdge); { int tw = MEASURE_TEXT("* Favs", fontSize); DRAW_TEXT("* Favs", (int)(favBtnX + filterBtnW*0.5f - tw*0.5f), (int)(contentY+2), fontSize, favText); } if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (mouse.x >= allBtnX && mouse.x <= allBtnX + filterBtnW && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { ui->patchShowFavsOnly = 0; ui->patchScrollOffset = 0; } if (mouse.x >= favBtnX && mouse.x <= favBtnX + filterBtnW && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { ui->patchShowFavsOnly = 1; ui->patchScrollOffset = 0; } } contentY += scaledTile + padding; // Build filtered patch index list int filteredIdx[PATCH_MAX_FILES]; int filteredCount = 0; for (int i = 0; i < ui->patchFileCount; i++) { if (!ui->patchShowFavsOnly) { filteredIdx[filteredCount++] = i; } else { for (int j = 0; j < ui->patchFavCount; j++) { if (strcmp(ui->patchFavs[j], ui->patchFiles[i]) == 0) { filteredIdx[filteredCount++] = i; break; } } } } // Virtual row layout: [..] + subdirs + patches int isRoot = (ui->patchCurrentDir[0] == '\0'); int backRows = isRoot ? 0 : 1; int totalVirtual = backRows + ui->patchSubDirCount + filteredCount; // Mouse-wheel scroll when hovering over list float listY0 = contentY; if (mouse.y >= listY0 && mouse.y <= listY0 + MAX_VIS * rowH) { ui->patchScrollOffset -= (int)GetMouseWheelMove(); } // Arrow key navigation over all virtual rows: [..], subdirs, patches if (ui->focusedTextInput == -1 && !ui->patchConfirmOverwrite && !ui->patchConfirmClear && totalVirtual > 0) { // Sync cursor from currently loaded patch if unset if (ui->patchVCursor < 0 && ui->patchCurrentName[0]) { for (int fi = 0; fi < filteredCount; fi++) { if (strcmp(ui->patchFiles[filteredIdx[fi]], ui->patchCurrentName) == 0) { ui->patchVCursor = fi + backRows + ui->patchSubDirCount; break; } } } int next = ui->patchVCursor; if (IsKeyPressed(KEY_DOWN)) next = (next < 0) ? 0 : (next < totalVirtual - 1 ? next + 1 : next); if (IsKeyPressed(KEY_UP)) next = (next < 0) ? totalVirtual - 1 : (next > 0 ? next - 1 : next); if (next != ui->patchVCursor) { ui->patchVCursor = next; // Auto-load when landing on a patch row if (next >= backRows + ui->patchSubDirCount && filteredCount > 0) { int fi = next - backRows - ui->patchSubDirCount; int i = filteredIdx[fi]; char path[512]; buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchFiles[i]); if (patchLoad(s, path)) { snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); snprintf(ui->patchSaveName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); } } } // Enter activates navigation rows (.. and subdirs) if (IsKeyPressed(KEY_ENTER) && ui->patchVCursor >= 0) { int vc = ui->patchVCursor; if (!isRoot && vc == 0) { char *lastSlash = strrchr(ui->patchCurrentDir, '/'); if (lastSlash) *lastSlash = '\0'; else ui->patchCurrentDir[0] = '\0'; ui->patchCurrentName[0] = '\0'; patchRescan(ui); return; } else if (vc >= backRows && vc < backRows + ui->patchSubDirCount) { int d = vc - backRows; if (ui->patchCurrentDir[0]) { size_t curLen = strlen(ui->patchCurrentDir); snprintf(ui->patchCurrentDir + curLen, sizeof(ui->patchCurrentDir) - curLen, "/%s", ui->patchSubDirs[d]); } else { snprintf(ui->patchCurrentDir, sizeof(ui->patchCurrentDir), "%s", ui->patchSubDirs[d]); } ui->patchCurrentName[0] = '\0'; patchRescan(ui); return; } } // Scroll to keep cursor visible if (ui->patchVCursor >= 0) { if (ui->patchVCursor < ui->patchScrollOffset) ui->patchScrollOffset = ui->patchVCursor; if (ui->patchVCursor >= ui->patchScrollOffset + MAX_VIS) ui->patchScrollOffset = ui->patchVCursor - MAX_VIS + 1; } } int maxScroll = totalVirtual - MAX_VIS; if (maxScroll < 0) maxScroll = 0; if (ui->patchScrollOffset < 0) ui->patchScrollOffset = 0; if (ui->patchScrollOffset > maxScroll) ui->patchScrollOffset = maxScroll; // Scrollbar (only when list overflows) if (totalVirtual > MAX_VIS) { float trackH = MAX_VIS * rowH; float trackX = x + width - 4; float thumbH = trackH * MAX_VIS / (float)totalVirtual; float thumbY = listY0 + (ui->patchScrollOffset / (float)totalVirtual) * trackH; DrawRectangle((int)trackX, (int)listY0, 3, (int)trackH, (Color){40,40,40,255}); DrawRectangle((int)trackX, (int)thumbY, 3, (int)thumbH, LIGHTGRAY); } // Combined row list: [..], subdirs, patches float starW = scaledTile; float nameBtnW = btnW - starW - 2; if (totalVirtual == 0) { DRAW_TEXT("(No patches found.)", (int)(x + padding), (int)(contentY + 2), fontSize, GRAY); } for (int v = 0; v < MAX_VIS; v++) { int r = v + ui->patchScrollOffset; if (r >= totalVirtual) break; if (!isRoot && r == 0) { // ".." — navigate to parent Color dotBg = (ui->patchVCursor == r) ? (Color){40, 60, 100, 255} : (Color){20, 30, 50, 255}; Color dotEdge = (ui->patchVCursor == r) ? SKYBLUE : (Color){80, 120, 180, 255}; DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dotBg); DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dotEdge); DRAW_TEXT("..", (int)(x + padding + 4), (int)(contentY + 2), fontSize, SKYBLUE); if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= x + padding && mouse.x <= x + padding + btnW && mouse.y >= contentY && mouse.y <= contentY + rowH - 2) { char *lastSlash = strrchr(ui->patchCurrentDir, '/'); if (lastSlash) *lastSlash = '\0'; else ui->patchCurrentDir[0] = '\0'; ui->patchCurrentName[0] = '\0'; patchRescan(ui); return; } } else if (r - backRows < ui->patchSubDirCount) { // Subdirectory row int d = r - backRows; char label[PATCH_NAME_LEN + 4]; snprintf(label, sizeof(label), "> %s", ui->patchSubDirs[d]); Color dirBg = (ui->patchVCursor == r) ? (Color){40, 60, 100, 255} : (Color){20, 30, 50, 255}; Color dirEdge = (ui->patchVCursor == r) ? SKYBLUE : (Color){80, 120, 180, 255}; DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dirBg); DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dirEdge); DRAW_TEXT(label, (int)(x + padding + 4), (int)(contentY + 2), fontSize, SKYBLUE); if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= x + padding && mouse.x <= x + padding + btnW && mouse.y >= contentY && mouse.y <= contentY + rowH - 2) { if (ui->patchCurrentDir[0]) { size_t curLen = strlen(ui->patchCurrentDir); snprintf(ui->patchCurrentDir + curLen, sizeof(ui->patchCurrentDir) - curLen, "/%s", ui->patchSubDirs[d]); } else { snprintf(ui->patchCurrentDir, sizeof(ui->patchCurrentDir), "%s", ui->patchSubDirs[d]); } ui->patchCurrentName[0] = '\0'; patchRescan(ui); return; } } else { // Patch row int fi = r - backRows - ui->patchSubDirCount; int i = filteredIdx[fi]; int current = (strcmp(ui->patchFiles[i], ui->patchCurrentName) == 0); int fav = 0; for (int j = 0; j < ui->patchFavCount; j++) if (strcmp(ui->patchFavs[j], ui->patchFiles[i]) == 0) { fav = 1; break; } Color rowBg = current ? (Color){20, 60, 20, 255} : (Color){45, 45, 45, 255}; Color rowEdge = current ? GREEN : DARKGRAY; Color textCol = current ? GREEN : RAYWHITE; DrawRectangle((int)(x + padding), (int)contentY, (int)nameBtnW, (int)(rowH - 2), rowBg); DrawRectangleLines((int)(x + padding), (int)contentY, (int)nameBtnW, (int)(rowH - 2), rowEdge); DRAW_TEXT(ui->patchFiles[i], (int)(x + padding + 4), (int)(contentY + 2), fontSize, textCol); float starX = x + padding + nameBtnW + 2; Color starBg = fav ? (Color){60,55,10,255} : (Color){35,35,35,255}; Color starEdge = fav ? YELLOW : DARKGRAY; DrawRectangle((int)starX, (int)contentY, (int)starW, (int)(rowH - 2), starBg); DrawRectangleLines((int)starX, (int)contentY, (int)starW, (int)(rowH - 2), starEdge); { int tw = MEASURE_TEXT("*", fontSize); DRAW_TEXT("*", (int)(starX + starW*0.5f - tw*0.5f), (int)(contentY + 2), fontSize, fav ? YELLOW : GRAY); } if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (mouse.x >= starX && mouse.x <= starX + starW && mouse.y >= contentY && mouse.y <= contentY + rowH - 2) { int found = -1; for (int j = 0; j < ui->patchFavCount; j++) { if (strcmp(ui->patchFavs[j], ui->patchFiles[i]) == 0) { found = j; break; } } if (found >= 0) { for (int j = found; j < ui->patchFavCount - 1; j++) memcpy(ui->patchFavs[j], ui->patchFavs[j+1], PATCH_NAME_LEN); ui->patchFavCount--; } else if (ui->patchFavCount < PATCH_MAX_FILES) { memcpy(ui->patchFavs[ui->patchFavCount], ui->patchFiles[i], PATCH_NAME_LEN); ui->patchFavCount++; } patchSaveFavs(ui->patchFavs, ui->patchFavCount); } else if (mouse.x >= x + padding && mouse.x <= x + padding + nameBtnW && mouse.y >= contentY && mouse.y <= contentY + rowH - 2) { char path[512]; buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchFiles[i]); if (patchLoad(s, path)) { snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); snprintf(ui->patchSaveName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]); ui->patchVCursor = r; } } } } contentY += rowH; } contentY += padding; DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += 1 + padding; // Bottom row: Rescan | Init | Close float thirdW = (btnW - 8) * (1.0f / 3.0f); float rescanX = x + padding; float initX = rescanX + thirdW + 4; float closeX = initX + thirdW + 4; DrawRectangle((int)rescanX, (int)contentY, (int)thirdW, (int)scaledTile, (Color){55, 55, 75, 255}); DrawRectangleLines((int)rescanX, (int)contentY, (int)thirdW, (int)scaledTile, GRAY); int rescanTW = MEASURE_TEXT("Rescan", fontSize); DRAW_TEXT("Rescan", (int)(rescanX + thirdW * 0.5f - rescanTW * 0.5f), (int)(contentY + 2), fontSize, RAYWHITE); if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= rescanX && mouse.x <= rescanX + thirdW && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { patchRescan(ui); } DrawRectangle((int)initX, (int)contentY, (int)thirdW, (int)scaledTile, (Color){60, 45, 10, 255}); DrawRectangleLines((int)initX, (int)contentY, (int)thirdW, (int)scaledTile, ORANGE); int initTW = MEASURE_TEXT("Init", fontSize); DRAW_TEXT("Init", (int)(initX + thirdW * 0.5f - initTW * 0.5f), (int)(contentY + 2), fontSize, ORANGE); if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= initX && mouse.x <= initX + thirdW && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { ui->patchConfirmClear = 1; ui->focusedTextInput = -1; ui->patchConfirmOverwrite = 0; } DrawRectangle((int)closeX, (int)contentY, (int)thirdW, (int)scaledTile, (Color){70, 40, 40, 255}); DrawRectangleLines((int)closeX, (int)contentY, (int)thirdW, (int)scaledTile, DARKGRAY); int closeTW2 = MEASURE_TEXT("Close", fontSize); DRAW_TEXT("Close", (int)(closeX + thirdW * 0.5f - closeTW2 * 0.5f), (int)(contentY + 2), fontSize, RAYWHITE); if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= closeX && mouse.x <= closeX + thirdW && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { ui->patchMenuOpen = 0; ui->focusedTextInput = -1; ui->patchConfirmOverwrite = 0; ui->patchConfirmClear = 0; } // Clear-to-init confirmation overlay — drawn last, on top of panel content if (ui->patchConfirmClear) { DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){0, 0, 0, 160}); float dlgW = width - padding * 4; float dlgH = padding + (fontSize + 4) * 2 + padding + scaledTile + padding; float dlgX = x + padding * 2; float dlgY = y + (height - dlgH) * 0.5f; DrawRectangle((int)dlgX, (int)dlgY, (int)dlgW, (int)dlgH, (Color){20, 15, 10, 255}); DrawRectangleLines((int)dlgX, (int)dlgY, (int)dlgW, (int)dlgH, ORANGE); DRAW_TEXT("Clear to init?", (int)(dlgX + padding), (int)(dlgY + padding), fontSize, ORANGE); DRAW_TEXT("Unsaved changes will be lost.", (int)(dlgX + padding), (int)(dlgY + padding + fontSize + 4), fontSize, GRAY); float dlgBtnY = dlgY + padding + (fontSize + 4) * 2 + padding; float dlgBtnW = (dlgW - padding * 3) * 0.5f; float dlgYesX = dlgX + padding; float dlgNoX = dlgX + padding * 2 + dlgBtnW; DrawRectangle((int)dlgYesX, (int)dlgBtnY, (int)dlgBtnW, (int)scaledTile, (Color){20, 60, 20, 255}); DrawRectangleLines((int)dlgYesX, (int)dlgBtnY, (int)dlgBtnW, (int)scaledTile, GREEN); int dlgYesTW = MEASURE_TEXT("Yes", fontSize); DRAW_TEXT("Yes", (int)(dlgYesX + dlgBtnW * 0.5f - dlgYesTW * 0.5f), (int)(dlgBtnY + 2), fontSize, RAYWHITE); DrawRectangle((int)dlgNoX, (int)dlgBtnY, (int)dlgBtnW, (int)scaledTile, (Color){70, 40, 40, 255}); DrawRectangleLines((int)dlgNoX, (int)dlgBtnY, (int)dlgBtnW, (int)scaledTile, DARKGRAY); int dlgNoTW = MEASURE_TEXT("No", fontSize); DRAW_TEXT("No", (int)(dlgNoX + dlgBtnW * 0.5f - dlgNoTW * 0.5f), (int)(dlgBtnY + 2), fontSize, RAYWHITE); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (mouse.x >= dlgYesX && mouse.x <= dlgYesX + dlgBtnW && mouse.y >= dlgBtnY && mouse.y <= dlgBtnY + scaledTile) { synthResetPatch(s); ui->patchCurrentName[0] = '\0'; ui->patchSaveName[0] = '\0'; ui->patchConfirmClear = 0; } else if (mouse.x >= dlgNoX && mouse.x <= dlgNoX + dlgBtnW && mouse.y >= dlgBtnY && mouse.y <= dlgBtnY + scaledTile) { ui->patchConfirmClear = 0; } } } } void uiLfoPanel(UIState *ui, int baseId, float x, float y, float width, float height, LFO *lfo, const char *title) { float scaledTile = TILE_SIZE * SPRITE_SCALE; float padding = 6.0f; int fontSize = 8; DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); DrawRectangleLines((int)x, (int)y, (int)width, (int)height, DARKGRAY); DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); DRAW_TEXT(title, (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); float activeBtnX = x + width - scaledTile - padding; float activeBtnY = y + 2; drawTile(ui, lfo->active ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, activeBtnX, activeBtnY, WHITE); DRAW_TEXT("Active", (int)(activeBtnX - MEASURE_TEXT("Active", fontSize) - 4), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= activeBtnX && mouse.x <= activeBtnX + scaledTile && mouse.y >= activeBtnY && mouse.y <= activeBtnY + scaledTile) { lfo->active = !lfo->active; } float contentY = y + TITLE_BAR_H + padding; DRAW_TEXT("Wave", (int)(x + padding), (int)contentY, fontSize, GRAY); contentY += fontSize + 4; lfo->waveform = (Waveform)uiWaveformSelector(ui, x + padding, contentY, lfo->waveform); contentY += scaledTile + padding * 2; DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += padding; float knobSize = KNOB_SIZE * SPRITE_SCALE; float centerX = x + width * 0.5f; uiKnob(ui, baseId, centerX - knobSize * 0.5f, contentY, &lfo->rate, 0.01f, 20.0f, "Rate Hz"); } void uiFilterPanel(UIState *ui, float x, float y, float width, float height, Filter *filter, const char *title) { float scaledTile = TILE_SIZE * SPRITE_SCALE; float padding = 6.0f; int fontSize = 8; DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); DrawRectangleLines((int)x, (int)y, (int)width, (int)height, DARKGRAY); DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); DRAW_TEXT(title, (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); float activeBtnX = x + width - scaledTile - padding; float activeBtnY = y + 2; drawTile(ui, filter->active ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, activeBtnX, activeBtnY, WHITE); DRAW_TEXT("Active", (int)(activeBtnX - MEASURE_TEXT("Active", fontSize) - 4), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= activeBtnX && mouse.x <= activeBtnX + scaledTile && mouse.y >= activeBtnY && mouse.y <= activeBtnY + scaledTile) { filter->active = !filter->active; } float contentY = y + TITLE_BAR_H + padding; // Filter type selector DRAW_TEXT("Type", (int)(x + padding), (int)contentY, fontSize, GRAY); contentY += fontSize + 4; const char *typeLabels[] = { "LP", "HP", "BP" }; for (int t = 0; t < FILTER_COUNT; t++) { float bx = x + padding + t * (scaledTile + 2); bool active = (filter->type == (FilterType)t); drawTile(ui, active ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, bx, contentY, active ? GREEN : GRAY); DRAW_TEXT(typeLabels[t], (int)(bx + 1), (int)(contentY + (scaledTile - 8) * 0.5f), 8, RAYWHITE); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= bx && mouse.x <= bx + scaledTile && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { filter->type = (FilterType)t; } } contentY += scaledTile + padding; // Cutoff and resonance knobs float knobSize = KNOB_SIZE * SPRITE_SCALE; float knobSpacing = (width - padding * 2) / 2.0f; float knobOffset = knobSpacing * 0.5f - knobSize * 0.5f; uiKnob(ui, 400, x + padding + knobSpacing * 0.0f + knobOffset, contentY, &filter->cutoff, 20.0f, 20000.0f, "Cutoff"); uiKnob(ui, 401, x + padding + knobSpacing * 1.0f + knobOffset, contentY, &filter->resonance, 0.0f, 0.99f, "Res"); contentY += knobSize + fontSize + padding * 2; DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += padding; // Cutoff mod routing DRAW_TEXT("Cutoff Mod", (int)(x + padding), (int)contentY, fontSize, GRAY); contentY += fontSize + 24; float knobX = x + padding + MEASURE_TEXT("PWidth", fontSize) + 8; float knobYm = contentY - knobSize * 0.25f; uiKnob(ui, 402, knobX, knobYm, &filter->modDepth, -20000.0f, 20000.0f, ""); float btnX = x + width - (scaledTile + 2) * 4 - padding; const char *modLabels[] = { "E0", "E1", "L0", "L1" }; ModSource modSources[] = { MOD_SOURCE_AMP_ENV, MOD_SOURCE_MOD_ENV, MOD_SOURCE_LFO, MOD_SOURCE_LFO2 }; for (int b = 0; b < 4; b++) { float bx = btnX + b * (scaledTile + 2); bool enabled = (filter->modRouting == (int)modSources[b]); drawTile(ui, enabled ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, bx, contentY, enabled ? GREEN : GRAY); DRAW_TEXT(modLabels[b], (int)(bx + 1), (int)(contentY + (scaledTile - 8) * 0.5f), 8, RAYWHITE); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= bx && mouse.x <= bx + scaledTile && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { filter->modRouting = enabled ? MOD_SOURCE_NONE : (int)modSources[b]; } } contentY += scaledTile + 8; // Resonance mod routing DRAW_TEXT("Res Mod", (int)(x + padding), (int)contentY, fontSize, GRAY); contentY += fontSize + 24; float resKnobY = contentY - knobSize * 0.25f; uiKnob(ui, 403, knobX, resKnobY, &filter->resModDepth, -0.99f, 0.99f, ""); for (int b = 0; b < 4; b++) { float bx = btnX + b * (scaledTile + 2); bool enabled = (filter->resModRouting == (int)modSources[b]); drawTile(ui, enabled ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, bx, contentY, enabled ? GREEN : GRAY); DRAW_TEXT(modLabels[b], (int)(bx + 1), (int)(contentY + (scaledTile - 8) * 0.5f), 8, RAYWHITE); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= bx && mouse.x <= bx + scaledTile && mouse.y >= contentY && mouse.y <= contentY + scaledTile) { filter->resModRouting = enabled ? MOD_SOURCE_NONE : (int)modSources[b]; } } } void uiMasterPanel(UIState *ui, float x, float y, float width, float height, Synth *s) { float scaledTile = TILE_SIZE * SPRITE_SCALE; float padding = 6.0f; int fontSize = 8; // Check every frame: has MIDI thread reported a CC for learn? if (ui->midiLearnActive == 2) { int cc = atomic_load(&ui->midi->midiLearnCC); if (cc >= 0) { ui->midi->ccMappings[cc].active = 1; ui->midi->ccMappings[cc].valuePtr = ui->midiLearnValuePtr; ui->midi->ccMappings[cc].min = ui->midiLearnMin; ui->midi->ccMappings[cc].max = ui->midiLearnMax; ui->midi->ccMappings[cc].controlId = ui->midiLearnTargetId; atomic_store(&ui->midi->midiLearnMode, 0); atomic_store(&ui->midi->midiLearnCC, -1); ui->midiLearnActive = 0; ui->midiLearnTargetId = -1; ui->midiLearnValuePtr = NULL; uiSaveCcMappings(ui); } } // Panel background and border DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); DrawRectangleLines((int)x, (int)y, (int)width, (int)height, DARKGRAY); // Title bar DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); DRAW_TEXT("Master", (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); float contentY = y + TITLE_BAR_H + padding; float centerX = x + width * 0.5f; float knobSize = KNOB_SIZE * SPRITE_SCALE; // Volume knob centered in panel float knobX = centerX - knobSize * 0.5f; uiKnob(ui, 300, knobX, contentY, &s->volume, 0.0f, 1.0f, "Volume"); contentY += knobSize + fontSize + padding * 2; // Divider DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += padding; // Pitch bend range knob — centered in left half of the row float knobSpacing = (width - padding * 2) / 2.0f; uiKnob(ui, 301, x + padding + knobSpacing * 0.5f - knobSize * 0.5f, contentY, &s->pitchBendRange, 1.0f, 24.0f, "PB Range"); // Voice meter float meterX = x + padding + knobSpacing; float meterY = contentY; DRAW_TEXT("Voices", (int)meterX, (int)meterY, fontSize, GRAY); meterY += fontSize + 4; // Two rows of 4 voices each for (int i = 0; i < VOICE_COUNT; i++) { float bx = meterX + (i % 4) * (scaledTile + 2); float by = meterY + (i / 4) * (scaledTile + 2); Color tint = s->voices[i].active ? GREEN : GRAY; drawTile(ui, s->voices[i].active ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, bx, by, tint); } contentY += knobSize + fontSize + padding * 2; // Divider DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += padding; // MIDI section — button opens device picker DRAW_TEXT("MIDI", (int)(x + padding), (int)contentY, fontSize, GRAY); bool connected = (ui->midi->connectedClient >= 0); float midiBtnX = x + width - scaledTile - padding; drawTile(ui, connected ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, midiBtnX, contentY, WHITE); Vector2 midiMouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && midiMouse.x >= midiBtnX && midiMouse.x <= midiBtnX + scaledTile && midiMouse.y >= contentY && midiMouse.y <= contentY + scaledTile) { midiScanInputs(ui->midi); ui->midiMenuOpen = 1; } contentY += fontSize + 4; if (connected) { const char *devName = "..."; for (int i = 0; i < ui->midi->inputCount; i++) { if (ui->midi->inputs[i].client == ui->midi->connectedClient && ui->midi->inputs[i].port == ui->midi->connectedPort) { devName = ui->midi->inputs[i].clientName; break; } } DRAW_TEXT(devName, (int)(x + padding), (int)contentY, fontSize, GREEN); } else { DRAW_TEXT("Not connected", (int)(x + padding), (int)contentY, fontSize, GRAY); } contentY += fontSize + 4; // Divider DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += 1 + padding; // Patches section DRAW_TEXT("Patches", (int)(x + padding), (int)contentY, fontSize, GRAY); float patchBtnX = x + width - scaledTile - padding; drawTile(ui, SPRITE_BTN_GREEN, patchBtnX, contentY, WHITE); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && midiMouse.x >= patchBtnX && midiMouse.x <= patchBtnX + scaledTile && midiMouse.y >= contentY && midiMouse.y <= contentY + scaledTile) { patchRescan(ui); ui->patchMenuOpen = 1; } contentY += fontSize + 4; if (ui->patchCurrentName[0]) { DRAW_TEXT(ui->patchCurrentName, (int)(x + padding), (int)contentY, fontSize, GREEN); } else { DRAW_TEXT("No patch", (int)(x + padding), (int)contentY, fontSize, GRAY); } contentY += fontSize + 4; // Divider DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += 1 + padding; // MIDI Learn section DRAW_TEXT("MIDI Lrn", (int)(x + padding), (int)contentY, fontSize, GRAY); int learnOn = (ui->midiLearnActive > 0); int pulse = (int)(GetTime() * 4.0) % 2; Color learnColor = learnOn ? (pulse ? YELLOW : (Color){180, 180, 0, 255}) : GRAY; float learnBtnX = x + width - scaledTile - padding; drawTile(ui, learnOn ? SPRITE_BTN_GREEN : SPRITE_BTN_RED, learnBtnX, contentY, learnColor); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && midiMouse.x >= learnBtnX && midiMouse.x <= learnBtnX + scaledTile && midiMouse.y >= contentY && midiMouse.y <= contentY + scaledTile) { if (ui->midiLearnActive > 0) { // Cancel learn atomic_store(&ui->midi->midiLearnMode, 0); atomic_store(&ui->midi->midiLearnCC, -1); ui->midiLearnActive = 0; ui->midiLearnTargetId = -1; ui->midiLearnValuePtr = NULL; } else { ui->midiLearnActive = 1; ui->midiLearnTargetId = -1; } } contentY += fontSize + 4; const char *learnStatus = ""; if (ui->midiLearnActive == 1) learnStatus = "Click a ctrl"; else if (ui->midiLearnActive == 2) learnStatus = "Wiggle CC..."; DRAW_TEXT(learnStatus, (int)(x + padding), (int)contentY, fontSize, YELLOW); contentY += fontSize + 4; // Divider DrawLine((int)(x + padding), (int)contentY, (int)(x + width - padding), (int)contentY, DARKGRAY); contentY += 1 + padding; // About button DRAW_TEXT("About", (int)(x + padding), (int)contentY, fontSize, GRAY); float aboutBtnX = x + width - scaledTile - padding; drawTile(ui, SPRITE_BTN_GREEN, aboutBtnX, contentY, WHITE); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && midiMouse.x >= aboutBtnX && midiMouse.x <= aboutBtnX + scaledTile && midiMouse.y >= contentY && midiMouse.y <= contentY + scaledTile) { ui->aboutMenuOpen = 1; } } // ------------------------------------------------------------------ ABOUT MENU void uiAboutMenu(UIState *ui, float x, float y, float width) { if (!ui->aboutMenuOpen) return; /* Edit these strings to update the about screen. */ static const char *lines[] = { "SoundThing v0.9.5", "", "Polyphonic subtractive synthesizer", "Built with Raylib", "", "by Jake the Anachronaut", "", "", /* Add a project URL here: */ "github.com/RealBusinessAccount", }; const int lineCount = (int)(sizeof(lines) / sizeof(lines[0])); int fontSize = 8; float padding = 8.0f; float scaledTile = TILE_SIZE * SPRITE_SCALE; float lineH = (float)(fontSize + 6); float height = (float)TITLE_BAR_H + padding + lineCount * lineH + padding; /* Full-screen dim */ { float invZ = 1.0f / ui->camera->zoom; Vector2 org = GetScreenToWorld2D((Vector2){0.0f, 0.0f}, *ui->camera); DrawRectangle((int)org.x, (int)org.y, (int)(GetScreenWidth() * invZ), (int)(GetScreenHeight() * invZ), (Color){0, 0, 0, 140}); } /* Panel background */ DrawRectangle((int)x, (int)y, (int)width, (int)height, (Color){30, 30, 30, 255}); DrawRectangleLines((int)x, (int)y, (int)width, (int)height, LIGHTGRAY); /* Title bar */ DrawRectangle((int)x, (int)y, (int)width, TITLE_BAR_H, (Color){50, 50, 50, 255}); DRAW_TEXT("About SoundThing", (int)(x + padding), (int)(y + (TITLE_BAR_H - fontSize) / 2), fontSize, RAYWHITE); /* Close button */ float closeBtnX = x + width - scaledTile - padding; float closeBtnY = y + (TITLE_BAR_H - scaledTile) / 2.0f; drawTile(ui, SPRITE_BTN_RED, closeBtnX, closeBtnY, WHITE); DRAW_TEXT("X", (int)(closeBtnX + (scaledTile - MEASURE_TEXT("X", fontSize)) * 0.5f), (int)(closeBtnY + (scaledTile - fontSize) * 0.5f), fontSize, RAYWHITE); Vector2 mouse = GetScreenToWorld2D(GetMousePosition(), *ui->camera); if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && mouse.x >= closeBtnX && mouse.x <= closeBtnX + scaledTile && mouse.y >= closeBtnY && mouse.y <= closeBtnY + scaledTile) { ui->aboutMenuOpen = 0; } /* Content lines */ float contentY = y + TITLE_BAR_H + padding; for (int i = 0; i < lineCount; i++) { if (lines[i][0] != '\0') DRAW_TEXT(lines[i], (int)(x + padding), (int)contentY, fontSize, RAYWHITE); contentY += lineH; } }