aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/clay_renderer_SDL3.c319
-rw-r--r--src/clay_renderer_SDL3.h35
-rw-r--r--src/main.c118
-rw-r--r--src/midi_freqs.h143
-rw-r--r--src/sounds.c45
-rw-r--r--src/sounds.h1
-rw-r--r--src/ui.c207
-rw-r--r--src/ui.h29
8 files changed, 820 insertions, 77 deletions
diff --git a/src/clay_renderer_SDL3.c b/src/clay_renderer_SDL3.c
new file mode 100644
index 0000000..147c836
--- /dev/null
+++ b/src/clay_renderer_SDL3.c
@@ -0,0 +1,319 @@
+#include "clay_renderer_SDL3.h"
+
+/* Global for convenience. Even in 4K this is enough for smooth curves (low radius or rect size coupled with
+ * no AA or low resolution might make it appear as jagged curves) */
+static int NUM_CIRCLE_SEGMENTS = 16;
+
+//all rendering is performed by a single SDL call, avoiding multiple RenderRect + plumbing choice for circles.
+static void SDL_Clay_RenderFillRoundedRect(Clay_SDL3RendererData *rendererData, const SDL_FRect rect, const float cornerRadius, const Clay_Color _color) {
+ const SDL_FColor color = { _color.r/255, _color.g/255, _color.b/255, _color.a/255 };
+
+ int indexCount = 0, vertexCount = 0;
+
+ const float minRadius = SDL_min(rect.w, rect.h) / 2.0f;
+ const float clampedRadius = SDL_min(cornerRadius, minRadius);
+
+ const int numCircleSegments = SDL_max(NUM_CIRCLE_SEGMENTS, (int) clampedRadius * 0.5f);
+
+ int totalVertices = 4 + (4 * (numCircleSegments * 2)) + 2*4;
+ int totalIndices = 6 + (4 * (numCircleSegments * 3)) + 6*4;
+
+ SDL_Vertex vertices[totalVertices];
+ int indices[totalIndices];
+
+ //define center rectangle
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y + clampedRadius}, color, {0, 0} }; //0 center TL
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y + clampedRadius}, color, {1, 0} }; //1 center TR
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y + rect.h - clampedRadius}, color, {1, 1} }; //2 center BR
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y + rect.h - clampedRadius}, color, {0, 1} }; //3 center BL
+
+ indices[indexCount++] = 0;
+ indices[indexCount++] = 1;
+ indices[indexCount++] = 3;
+ indices[indexCount++] = 1;
+ indices[indexCount++] = 2;
+ indices[indexCount++] = 3;
+
+ //define rounded corners as triangle fans
+ const float step = (SDL_PI_F/2) / numCircleSegments;
+ for (int i = 0; i < numCircleSegments; i++) {
+ const float angle1 = (float)i * step;
+ const float angle2 = ((float)i + 1.0f) * step;
+
+ for (int j = 0; j < 4; j++) { // Iterate over four corners
+ float cx, cy, signX, signY;
+
+ switch (j) {
+ case 0: cx = rect.x + clampedRadius; cy = rect.y + clampedRadius; signX = -1; signY = -1; break; // Top-left
+ case 1: cx = rect.x + rect.w - clampedRadius; cy = rect.y + clampedRadius; signX = 1; signY = -1; break; // Top-right
+ case 2: cx = rect.x + rect.w - clampedRadius; cy = rect.y + rect.h - clampedRadius; signX = 1; signY = 1; break; // Bottom-right
+ case 3: cx = rect.x + clampedRadius; cy = rect.y + rect.h - clampedRadius; signX = -1; signY = 1; break; // Bottom-left
+ default: return;
+ }
+
+ vertices[vertexCount++] = (SDL_Vertex){ {cx + SDL_cosf(angle1) * clampedRadius * signX, cy + SDL_sinf(angle1) * clampedRadius * signY}, color, {0, 0} };
+ vertices[vertexCount++] = (SDL_Vertex){ {cx + SDL_cosf(angle2) * clampedRadius * signX, cy + SDL_sinf(angle2) * clampedRadius * signY}, color, {0, 0} };
+
+ indices[indexCount++] = j; // Connect to corresponding central rectangle vertex
+ indices[indexCount++] = vertexCount - 2;
+ indices[indexCount++] = vertexCount - 1;
+ }
+ }
+
+ //Define edge rectangles
+ // Top edge
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y}, color, {0, 0} }; //TL
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y}, color, {1, 0} }; //TR
+
+ indices[indexCount++] = 0;
+ indices[indexCount++] = vertexCount - 2; //TL
+ indices[indexCount++] = vertexCount - 1; //TR
+ indices[indexCount++] = 1;
+ indices[indexCount++] = 0;
+ indices[indexCount++] = vertexCount - 1; //TR
+ // Right edge
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w, rect.y + clampedRadius}, color, {1, 0} }; //RT
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w, rect.y + rect.h - clampedRadius}, color, {1, 1} }; //RB
+
+ indices[indexCount++] = 1;
+ indices[indexCount++] = vertexCount - 2; //RT
+ indices[indexCount++] = vertexCount - 1; //RB
+ indices[indexCount++] = 2;
+ indices[indexCount++] = 1;
+ indices[indexCount++] = vertexCount - 1; //RB
+ // Bottom edge
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y + rect.h}, color, {1, 1} }; //BR
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y + rect.h}, color, {0, 1} }; //BL
+
+ indices[indexCount++] = 2;
+ indices[indexCount++] = vertexCount - 2; //BR
+ indices[indexCount++] = vertexCount - 1; //BL
+ indices[indexCount++] = 3;
+ indices[indexCount++] = 2;
+ indices[indexCount++] = vertexCount - 1; //BL
+ // Left edge
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x, rect.y + rect.h - clampedRadius}, color, {0, 1} }; //LB
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x, rect.y + clampedRadius}, color, {0, 0} }; //LT
+
+ indices[indexCount++] = 3;
+ indices[indexCount++] = vertexCount - 2; //LB
+ indices[indexCount++] = vertexCount - 1; //LT
+ indices[indexCount++] = 0;
+ indices[indexCount++] = 3;
+ indices[indexCount++] = vertexCount - 1; //LT
+
+ // Render everything
+ SDL_RenderGeometry(rendererData->renderer, NULL, vertices, vertexCount, indices, indexCount);
+}
+
+static void SDL_Clay_RenderArc(Clay_SDL3RendererData *rendererData, const SDL_FPoint center, const float radius, const float startAngle, const float endAngle, const float thickness, const Clay_Color color) {
+ SDL_SetRenderDrawColor(rendererData->renderer, color.r, color.g, color.b, color.a);
+
+ const float radStart = startAngle * (SDL_PI_F / 180.0f);
+ const float radEnd = endAngle * (SDL_PI_F / 180.0f);
+
+ const int numCircleSegments = SDL_max(NUM_CIRCLE_SEGMENTS, (int)(radius * 1.5f)); //increase circle segments for larger circles, 1.5 is arbitrary.
+
+ const float angleStep = (radEnd - radStart) / (float)numCircleSegments;
+ const float thicknessStep = 0.4f; //arbitrary value to avoid overlapping lines. Changing THICKNESS_STEP or numCircleSegments might cause artifacts.
+
+ for (float t = thicknessStep; t < thickness - thicknessStep; t += thicknessStep) {
+ SDL_FPoint points[numCircleSegments + 1];
+ const float clampedRadius = SDL_max(radius - t, 1.0f);
+
+ for (int i = 0; i <= numCircleSegments; i++) {
+ const float angle = radStart + i * angleStep;
+ points[i] = (SDL_FPoint){
+ SDL_roundf(center.x + SDL_cosf(angle) * clampedRadius),
+ SDL_roundf(center.y + SDL_sinf(angle) * clampedRadius) };
+ }
+ SDL_RenderLines(rendererData->renderer, points, numCircleSegments + 1);
+ }
+}
+
+int SDL_Clay_RenderCircle(SDL_Renderer *renderer, float x, float y, float width, float height, float start_angle, float end_angle, const Clay_Color _color) {
+ const SDL_FColor color = { _color.r/255, _color.g/255, _color.b/255, _color.a/255 };
+ float center_x = x + width / 2;
+ float center_y = y + width / 2;
+ float min_diameter = width > height ? height : width;
+ float radius = min_diameter / 2;
+
+ float rad_start = start_angle * (SDL_PI_F / 180.0f);
+ float rad_end = end_angle * (SDL_PI_F / 180.0f);
+
+ const int segments = SDL_max(16, (int)(radius * 1.5f));
+ SDL_Vertex vertices[segments + 1];
+
+ int vertexCount = 0, indexCount = 0;
+
+ vertices[vertexCount++] = (SDL_Vertex){
+ .position = { center_x, center_y },
+ .color = color
+ };
+
+ float angle_step = (rad_end - rad_start) / ((float)segments - 1);
+
+ for (int i = 0; i < segments; i++) {
+ float angle = rad_start + (float)i * angle_step;
+ vertices[vertexCount++] = (SDL_Vertex){
+ .position = {
+ center_x + radius * SDL_cosf(angle),
+ center_y + radius * SDL_sinf(angle)
+ },
+ .color = color
+ };
+ }
+
+ int indices[segments * 3];
+ for (int j = 0; j < segments - 1; j++) {
+ indices[indexCount++] = 0;
+ indices[indexCount++] = j + 1;
+ indices[indexCount++] = (j + 1) % segments + 1;
+ }
+
+ SDL_RenderGeometry(renderer, NULL, vertices, vertexCount, indices, indexCount);
+ return 0;
+}
+
+SDL_Rect currentClippingRectangle;
+
+void SDL_Clay_RenderClayCommands(Clay_SDL3RendererData *rendererData, Clay_RenderCommandArray *rcommands)
+{
+ for (int32_t i = 0; i < rcommands->length; i++) {
+ Clay_RenderCommand *rcmd = Clay_RenderCommandArray_Get(rcommands, i);
+ const Clay_BoundingBox bounding_box = rcmd->boundingBox;
+ const SDL_FRect rect = { (int)bounding_box.x, (int)bounding_box.y, (int)bounding_box.width, (int)bounding_box.height };
+
+ switch (rcmd->commandType) {
+ case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
+ Clay_RectangleRenderData *config = &rcmd->renderData.rectangle;
+ SDL_SetRenderDrawBlendMode(rendererData->renderer, SDL_BLENDMODE_BLEND);
+ SDL_SetRenderDrawColor(rendererData->renderer, config->backgroundColor.r, config->backgroundColor.g, config->backgroundColor.b, config->backgroundColor.a);
+ if (config->cornerRadius.topLeft > 0) {
+ SDL_Clay_RenderFillRoundedRect(rendererData, rect, config->cornerRadius.topLeft, config->backgroundColor);
+ } else {
+ SDL_RenderFillRect(rendererData->renderer, &rect);
+ }
+ } break;
+ case CLAY_RENDER_COMMAND_TYPE_TEXT: {
+ Clay_TextRenderData *config = &rcmd->renderData.text;
+ TTF_Font *font = rendererData->fonts[config->fontId];
+ TTF_SetFontSize(font, config->fontSize);
+ TTF_Text *text = TTF_CreateText(rendererData->textEngine, font, config->stringContents.chars, config->stringContents.length);
+ TTF_SetTextColor(text, config->textColor.r, config->textColor.g, config->textColor.b, config->textColor.a);
+ TTF_DrawRendererText(text, rect.x, rect.y);
+ TTF_DestroyText(text);
+ } break;
+ case CLAY_RENDER_COMMAND_TYPE_BORDER: {
+ Clay_BorderRenderData *config = &rcmd->renderData.border;
+
+ const float minRadius = SDL_min(rect.w, rect.h) / 2.0f;
+ const Clay_CornerRadius clampedRadii = {
+ .topLeft = SDL_min(config->cornerRadius.topLeft, minRadius),
+ .topRight = SDL_min(config->cornerRadius.topRight, minRadius),
+ .bottomLeft = SDL_min(config->cornerRadius.bottomLeft, minRadius),
+ .bottomRight = SDL_min(config->cornerRadius.bottomRight, minRadius)
+ };
+ //edges
+ SDL_SetRenderDrawColor(rendererData->renderer, config->color.r, config->color.g, config->color.b, config->color.a);
+ if (config->width.left > 0) {
+ const float starting_y = rect.y + clampedRadii.topLeft;
+ const float length = rect.h - clampedRadii.topLeft - clampedRadii.bottomLeft;
+ SDL_FRect line = { rect.x - 1, starting_y, config->width.left, length };
+ SDL_RenderFillRect(rendererData->renderer, &line);
+ }
+ if (config->width.right > 0) {
+ const float starting_x = rect.x + rect.w - (float)config->width.right + 1;
+ const float starting_y = rect.y + clampedRadii.topRight;
+ const float length = rect.h - clampedRadii.topRight - clampedRadii.bottomRight;
+ SDL_FRect line = { starting_x, starting_y, config->width.right, length };
+ SDL_RenderFillRect(rendererData->renderer, &line);
+ }
+ if (config->width.top > 0) {
+ const float starting_x = rect.x + clampedRadii.topLeft;
+ const float length = rect.w - clampedRadii.topLeft - clampedRadii.topRight;
+ SDL_FRect line = { starting_x, rect.y - 1, length, config->width.top };
+ SDL_RenderFillRect(rendererData->renderer, &line);
+ }
+ if (config->width.bottom > 0) {
+ const float starting_x = rect.x + clampedRadii.bottomLeft;
+ const float starting_y = rect.y + rect.h - (float)config->width.bottom + 1;
+ const float length = rect.w - clampedRadii.bottomLeft - clampedRadii.bottomRight;
+ SDL_FRect line = { starting_x, starting_y, length, config->width.bottom };
+ SDL_SetRenderDrawColor(rendererData->renderer, config->color.r, config->color.g, config->color.b, config->color.a);
+ SDL_RenderFillRect(rendererData->renderer, &line);
+ }
+ //corners
+ if (config->cornerRadius.topLeft > 0) {
+ const float centerX = rect.x + clampedRadii.topLeft -1;
+ const float centerY = rect.y + clampedRadii.topLeft - 1;
+ SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.topLeft,
+ 180.0f, 270.0f, config->width.top, config->color);
+ }
+ if (config->cornerRadius.topRight > 0) {
+ const float centerX = rect.x + rect.w - clampedRadii.topRight;
+ const float centerY = rect.y + clampedRadii.topRight - 1;
+ SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.topRight,
+ 270.0f, 360.0f, config->width.top, config->color);
+ }
+ if (config->cornerRadius.bottomLeft > 0) {
+ const float centerX = rect.x + clampedRadii.bottomLeft -1;
+ const float centerY = rect.y + rect.h - clampedRadii.bottomLeft;
+ SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.bottomLeft,
+ 90.0f, 180.0f, config->width.bottom, config->color);
+ }
+ if (config->cornerRadius.bottomRight > 0) {
+ const float centerX = rect.x + rect.w - clampedRadii.bottomRight;
+ const float centerY = rect.y + rect.h - clampedRadii.bottomRight;
+ SDL_Clay_RenderArc(rendererData, (SDL_FPoint){centerX, centerY}, clampedRadii.bottomRight,
+ 0.0f, 90.0f, config->width.bottom, config->color);
+ }
+
+ } break;
+ case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: {
+ Clay_BoundingBox boundingBox = rcmd->boundingBox;
+ currentClippingRectangle = (SDL_Rect) {
+ .x = boundingBox.x,
+ .y = boundingBox.y,
+ .w = boundingBox.width,
+ .h = boundingBox.height,
+ };
+ SDL_SetRenderClipRect(rendererData->renderer, &currentClippingRectangle);
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
+ SDL_SetRenderClipRect(rendererData->renderer, NULL);
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_IMAGE: {
+ SDL_Texture *texture = (SDL_Texture *)rcmd->renderData.image.imageData;
+ const SDL_FRect dest = { rect.x, rect.y, rect.w, rect.h };
+ SDL_RenderTexture(rendererData->renderer, texture, NULL, &dest);
+ break;
+ }
+
+ case CLAY_RENDER_COMMAND_TYPE_CUSTOM: {
+ Clay_BoundingBox bounding_box = rcmd->boundingBox;
+
+ CustomElementData *custom_element = rcmd->renderData.custom.customData;
+ if (!custom_element) continue;
+
+ switch (custom_element->type) {
+ case CUSTOM_ELEMENT_TYPE_CIRCLE: {
+ CircleData config = custom_element->circle;
+
+ float start_angle = config.start_angle;
+ float end_angle = config.value * 360 + start_angle;
+ end_angle = end_angle > start_angle ? end_angle : end_angle + 360;
+
+ SDL_Clay_RenderCircle(rendererData->renderer, bounding_box.x, bounding_box.y, bounding_box.width, bounding_box.height, start_angle, end_angle, config.color);
+ break;
+ }
+ }
+ break;
+ }
+ default:
+ SDL_Log("Unknown render command type: %d", rcmd->commandType);
+ }
+ }
+}
diff --git a/src/clay_renderer_SDL3.h b/src/clay_renderer_SDL3.h
new file mode 100644
index 0000000..6ece289
--- /dev/null
+++ b/src/clay_renderer_SDL3.h
@@ -0,0 +1,35 @@
+#ifndef RENDERER_H_
+#define RENDERER_H_
+
+#include "clay.h"
+#include <SDL3/SDL.h>
+#include <SDL3_ttf/SDL_ttf.h>
+#include <SDL3_image/SDL_image.h>
+
+typedef struct {
+ SDL_Renderer *renderer;
+ TTF_TextEngine *textEngine;
+ TTF_Font **fonts;
+} Clay_SDL3RendererData;
+
+typedef enum {
+ CUSTOM_ELEMENT_TYPE_CIRCLE,
+} CustomElementType;
+
+typedef struct {
+ float start_angle;
+ float value;
+ Clay_Color color;
+} CircleData;
+
+typedef struct {
+ CustomElementType type;
+
+ union {
+ CircleData circle;
+ };
+} CustomElementData;
+
+void SDL_Clay_RenderClayCommands(Clay_SDL3RendererData *rendererData, Clay_RenderCommandArray *rcommands);
+
+#endif // RENDERER_H_
diff --git a/src/main.c b/src/main.c
index e44dd7d..749be94 100644
--- a/src/main.c
+++ b/src/main.c
@@ -12,8 +12,8 @@
#include "sounds.h"
#define CLAY_IMPLEMENTATION
-#include "clay/clay.h"
-#include "clay/renderers/clay_renderer_SDL3.c"
+#include "clay.h"
+#include "clay_renderer_SDL3.h"
#define ARENA_IMPLEMENTATION
#include "arena.h"
@@ -24,12 +24,25 @@ static const int SCREEN_TICKS_PER_FRAME = 1000 / SCREEN_FPS;
static const int FONT_ID = 0;
typedef struct {
+ bool pressed;
+
+ float position_x;
+ float position_y;
+ float pending_scroll_delta_x;
+ float pending_scroll_delta_y;
+} PointerState;
+
+
+typedef struct {
SDL_Window *window;
Clay_SDL3RendererData renderer_data;
Arena frame_arena;
+ PointerState pointer;
KeyState *keys;
size_t keys_amount;
+
+ KnobSettings knob_settings;
snd_pcm_t *sound_device;
message_queue msg_queue;
@@ -40,7 +53,7 @@ int init_sounds(app_state *state) {
int err;
err =
- snd_pcm_open(&state->sound_device, "default", SND_PCM_STREAM_PLAYBACK, 0);
+ snd_pcm_open(&state->sound_device, "default", SND_PCM_STREAM_PLAYBACK, 0);
if (err < 0) {
printf("error: failed to open device: %s", snd_strerror(err));
return err;
@@ -62,30 +75,62 @@ int init_sounds(app_state *state) {
pthread_create(&sound_thread, NULL, sound_thread_start, sound_thread_params);
state->sound_thread = sound_thread;
+ state->knob_settings = (KnobSettings){
+ .volume = {
+ .param_type = PARAM_VOLUME,
+ .value = 0.2,
+ .range_start = 0,
+ .range_end = 1,
+ },
+ .attack = {
+ .param_type = PARAM_ATTACK,
+ .value = 5,
+ .range_start = 0,
+ .range_end = 1000,
+ },
+ .decay = {
+ .param_type = PARAM_DECAY,
+ .value = 10,
+ .range_start = 0,
+ .range_end = 1000,
+ },
+ .sustain = {
+ .param_type = PARAM_SUSTAIN,
+ .value = 0.7,
+ .range_start = 0,
+ .range_end = 1,
+ },
+ .release = {
+ .param_type = PARAM_RELEASE,
+ .value = 100,
+ .range_start = 0,
+ .range_end = 5000,
+ },
+ };
synth_message param_messages[6] = {
{
.type = MSG_PARAM_CHANGE,
.param_change =
- {
- .param_type = PARAM_OSC,
- .value = OSC_SQUARE,
- },
+ {
+ .param_type = PARAM_OSC,
+ .value = OSC_SAW,
+ },
},
{
.type = MSG_PARAM_CHANGE,
.param_change =
- {
- .param_type = PARAM_VOLUME,
- .value = 0.2,
- },
+ {
+ .param_type = PARAM_VOLUME,
+ .value = state->knob_settings.volume.value,
+ },
},
{
.type = MSG_PARAM_CHANGE,
.param_change =
{
.param_type = PARAM_ATTACK,
- .value = 0.005 * SAMPLE_RATE,
+ .value = state->knob_settings.attack.value,
},
},
{
@@ -93,7 +138,7 @@ int init_sounds(app_state *state) {
.param_change =
{
.param_type = PARAM_DECAY,
- .value = 0.0010 * SAMPLE_RATE,
+ .value = state->knob_settings.decay.value,
},
},
{
@@ -101,7 +146,7 @@ int init_sounds(app_state *state) {
.param_change =
{
.param_type = PARAM_SUSTAIN,
- .value = 0.7,
+ .value = state->knob_settings.sustain.value,
},
},
{
@@ -109,7 +154,7 @@ int init_sounds(app_state *state) {
.param_change =
{
.param_type = PARAM_RELEASE,
- .value = 1.000 * SAMPLE_RATE,
+ .value = state->knob_settings.release.value,
},
},
};
@@ -119,20 +164,20 @@ int init_sounds(app_state *state) {
static inline Clay_Dimensions SDL_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData)
{
- TTF_Font **fonts = userData;
- TTF_Font *font = fonts[config->fontId];
- int width, height;
+ TTF_Font **fonts = userData;
+ TTF_Font *font = fonts[config->fontId];
+ int width, height;
- TTF_SetFontSize(font, config->fontSize);
- if (!TTF_GetStringSize(font, text.chars, text.length, &width, &height)) {
- SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to measure text: %s", SDL_GetError());
- }
+ TTF_SetFontSize(font, config->fontSize);
+ if (!TTF_GetStringSize(font, text.chars, text.length, &width, &height)) {
+ SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to measure text: %s", SDL_GetError());
+ }
- return (Clay_Dimensions) { (float) width, (float) height };
+ return (Clay_Dimensions) { (float) width, (float) height };
}
void HandleClayErrors(Clay_ErrorData errorData) {
- printf("%s", errorData.errorText.chars);
+ printf("%s", errorData.errorText.chars);
}
int init_ui(app_state *state) {
@@ -224,7 +269,14 @@ SDL_AppResult SDL_AppIterate(void *appstate) {
ui_data->msg_queue = &state->msg_queue;
ui_data->keys = state->keys;
ui_data->keys_amount = 12;
+ ui_data->arena = arena;
+ ui_data->knob_settings = &state->knob_settings;
+ Clay_SetPointerState((Clay_Vector2){ state->pointer.position_x, state->pointer.position_y }, state->pointer.pressed);
+ Clay_UpdateScrollContainers(true, (Clay_Vector2){ state->pointer.pending_scroll_delta_x, state->pointer.pending_scroll_delta_y }, 0.016f);
+ state->pointer.pending_scroll_delta_x = 0;
+ state->pointer.pending_scroll_delta_y = 0;
+
Clay_BeginLayout();
draw_ui(ui_data);
@@ -293,25 +345,29 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
break;
case SDL_EVENT_MOUSE_MOTION:
- Clay_SetPointerState((Clay_Vector2) { event->motion.x, event->motion.y },
- event->motion.state & SDL_BUTTON_LMASK);
+ state->pointer.pressed = event->motion.state & SDL_BUTTON_LMASK;
+ state->pointer.position_x = event->motion.x;
+ state->pointer.position_y = event->motion.y;
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
- Clay_SetPointerState((Clay_Vector2) { event->button.x, event->button.y },
- event->button.button == SDL_BUTTON_LEFT);
+ state->pointer.pressed = event->button.button == SDL_BUTTON_LEFT;
+ state->pointer.position_x = event->button.x;
+ state->pointer.position_y = event->button.y;
break;
case SDL_EVENT_MOUSE_BUTTON_UP:
if (event->button.button == SDL_BUTTON_LEFT) {
- Clay_SetPointerState((Clay_Vector2) { event->button.x, event->button.y }, false);
+ state->pointer.pressed = false;
+ state->pointer.position_x = event->button.x;
+ state->pointer.position_y = event->button.y;
}
break;
case SDL_EVENT_MOUSE_WHEEL:
- Clay_UpdateScrollContainers(true, (Clay_Vector2) { event->wheel.x, event->wheel.y }, 0.01f);
+ state->pointer.pending_scroll_delta_x += event->wheel.x;
+ state->pointer.pending_scroll_delta_y += event->wheel.y;
break;
-
default:
break;
}
diff --git a/src/midi_freqs.h b/src/midi_freqs.h
new file mode 100644
index 0000000..0261ef2
--- /dev/null
+++ b/src/midi_freqs.h
@@ -0,0 +1,143 @@
+#ifndef MIDI_FREQS_H_
+#define MIDI_FREQS_H_
+
+typedef struct {
+ float freq;
+ char* name;
+} MidiNote;
+
+#ifdef MIDI_FREQS_LIST
+
+MidiNote midi_freqs[128] = {
+ {8.18, ""},
+ {8.66, ""},
+ {9.18, ""},
+ {9.72, ""},
+ {10.30, ""},
+ {10.91, ""},
+ {11.56, ""},
+ {12.25, ""},
+ {12.98, ""},
+ {13.75, ""},
+ {14.57, ""},
+ {15.43, ""},
+ {16.35, ""},
+ {17.32, ""},
+ {18.35, ""},
+ {19.45, ""},
+ {20.60, ""},
+ {21.83, ""},
+ {23.12, ""},
+ {24.50, ""},
+ {25.96, ""},
+ {27.50, "A0"},
+ {29.14, "A#0"},
+ {30.87, "B0"},
+ {32.70, "C1"},
+ {34.65, "C#1"},
+ {36.71, "D1"},
+ {38.89, "D#1"},
+ {41.20, "E1"},
+ {43.65, "F1"},
+ {46.25, "F#1"},
+ {49.00, "G1"},
+ {51.91, "G#1"},
+ {55.00, "A1"},
+ {58.27, "A#1"},
+ {61.74, "B1"},
+ {65.41, "C2"},
+ {69.30, "C#2"},
+ {73.42, "D2"},
+ {77.78, "D#2"},
+ {82.41, "E2"},
+ {87.31, "F2"},
+ {92.50, "F#2"},
+ {98.00, "G2"},
+ {103.83, "G#2"},
+ {110.00, "A2"},
+ {116.54, "A#2"},
+ {123.47, "B2"},
+ {130.81, "C3"},
+ {138.59, "C#3"},
+ {146.83, "D3"},
+ {155.56, "D#3"},
+ {164.81, "E3"},
+ {174.61, "F3"},
+ {185.00, "F#3"},
+ {196.00, "G3"},
+ {207.65, "G#3"},
+ {220.00, "A3"},
+ {233.08, "A#3"},
+ {246.94, "B3"},
+ {261.63, "c’"},
+ {277.18, "C#4"},
+ {293.66, "D4"},
+ {311.13, "D#4"},
+ {329.63, "E4"},
+ {349.23, "F4"},
+ {369.99, "F#4"},
+ {392.00, "G4"},
+ {415.30, "G#4"},
+ {440.00, "a’"},
+ {466.16, "A#4"},
+ {493.88, "B4"},
+ {523.25, "C5"},
+ {554.37, "C#5"},
+ {587.33, "D5"},
+ {622.25, "D#5"},
+ {659.26, "E5"},
+ {698.46, "F5"},
+ {739.99, "F#5"},
+ {783.99, "G5"},
+ {830.61, "G#5"},
+ {880.00, "A5"},
+ {932.33, "A#5"},
+ {987.77, "B5"},
+ {1046.50, "C6"},
+ {1108.73, "C#6"},
+ {1174.66, "D6"},
+ {1244.51, "D#6"},
+ {1318.51, "E6"},
+ {1396.91, "F6"},
+ {1479.98, "F#6"},
+ {1567.98, "G6"},
+ {1661.22, "G#6"},
+ {1760.00, "A6"},
+ {1864.66, "A#6"},
+ {1975.53, "B6"},
+ {2093.00, "C7"},
+ {2217.46, "C#7"},
+ {2349.32, "D7"},
+ {2489.02, "D#7"},
+ {2637.02, "E7"},
+ {2793.83, "F7"},
+ {2959.96, "F#7"},
+ {3135.96, "G7"},
+ {3322.44, "G#7"},
+ {3520.00, "A7"},
+ {3729.31, "A#7"},
+ {3951.07, "B7"},
+ {4186.01, "C8"},
+ {4434.92, "C#8"},
+ {4698.64, "D8"},
+ {4978.03, "D#8"},
+ {5274.04, "E8"},
+ {5587.65, "F8"},
+ {5919.91, "F#8"},
+ {6271.93, "G8"},
+ {6644.88, "G#8"},
+ {7040.00, "A8"},
+ {7458.62, "A#8"},
+ {7902.13, "B8"},
+ {8372.02, "C9"},
+ {8869.84, "C#9"},
+ {9397.27, "D9"},
+ {9956.06, "D#9"},
+ {10548.08, "E9"},
+ {11175.30, "F9"},
+ {11839.82, "F#9"},
+ {12543.85, "G9"},
+};
+#endif // MIDI_FREQS_LIST
+
+#endif // MIDI_FREQS_H_
diff --git a/src/sounds.c b/src/sounds.c
index 9c6711c..692555c 100644
--- a/src/sounds.c
+++ b/src/sounds.c
@@ -1,3 +1,4 @@
+#define MIDI_FREQS_LIST
#include "sounds.h"
float envelope_next(envelope *env) {
@@ -132,11 +133,11 @@ void set_param(synth_params *params, param_type type, float value) {
break;
}
case PARAM_ATTACK: {
- params->envelope_params.attack_time = (int)value;
+ params->envelope_params.attack_time = (int)(value / 1000 * SAMPLE_RATE);
break;
}
case PARAM_DECAY: {
- params->envelope_params.decay_time = (int)value;
+ params->envelope_params.decay_time = (int)(value / 1000 * SAMPLE_RATE);
break;
}
case PARAM_SUSTAIN: {
@@ -144,7 +145,7 @@ void set_param(synth_params *params, param_type type, float value) {
break;
}
case PARAM_RELEASE: {
- params->envelope_params.release_time = (int)value;
+ params->envelope_params.release_time = (int)(value / 1000 * SAMPLE_RATE);
break;
}
}
@@ -192,8 +193,8 @@ void prepare_output(float *scratch_buffer, short *output_buffer,
}
}
-void sound_loop_start(snd_pcm_t *pcm, message_queue *queue,
- synth_voices *voices, synth_params *params) {
+void sound_loop_start(snd_pcm_t *pcm, message_queue *queue, synth_voices *voices) {
+ synth_params params;
short output_buffer[PERIOD_SIZE];
float scratch_buffer[PERIOD_SIZE];
@@ -203,7 +204,7 @@ void sound_loop_start(snd_pcm_t *pcm, message_queue *queue,
switch (msg.type) {
case MSG_NOTE_ON: {
size_t note_id = msg.note.note_id;
- set_note_on(params, voices, note_id);
+ set_note_on(&params, voices, note_id);
break;
}
case MSG_NOTE_OFF: {
@@ -218,7 +219,7 @@ void sound_loop_start(snd_pcm_t *pcm, message_queue *queue,
case MSG_PARAM_CHANGE: {
param_type type = msg.param_change.param_type;
float value = msg.param_change.value;
- set_param(params, type, value);
+ set_param(&params, type, value);
break;
}
case MSG_STOP: {
@@ -230,9 +231,9 @@ void sound_loop_start(snd_pcm_t *pcm, message_queue *queue,
memset(&output_buffer, 0, PERIOD_SIZE * sizeof(short));
memset(&scratch_buffer, 0, PERIOD_SIZE * sizeof(float));
- generate_voices(voices, params, scratch_buffer, PERIOD_SIZE);
+ generate_voices(voices, &params, scratch_buffer, PERIOD_SIZE);
- post_process(params, scratch_buffer, PERIOD_SIZE);
+ post_process(&params, scratch_buffer, PERIOD_SIZE);
prepare_output(scratch_buffer, output_buffer, PERIOD_SIZE);
int period_size = PERIOD_SIZE;
@@ -253,11 +254,11 @@ stop:
return;
}
-void fill_voices(synth_voice *voices, float *freqs, size_t freqs_amount) {
+void fill_voices(synth_voice *voices, MidiNote *freqs, size_t freqs_amount) {
for (size_t i = 0; i < freqs_amount; i++) {
voices[i] = (synth_voice){
.active = false,
- .freq = freqs[i],
+ .freq = freqs[i].freq,
.phase = 0,
.phase_inc = 0,
.envelope = {0},
@@ -268,33 +269,15 @@ void fill_voices(synth_voice *voices, float *freqs, size_t freqs_amount) {
void *sound_thread_start(void *ptr) {
sound_thread_meta *meta = ptr;
- float freqs[12] = {
- 261.63f, // c
- 277.18f, // c#
- 293.66f, // e
- 311.13f, // e#
- 329.63f, // d
- 349.23f, // f
- 369.99f, // f#
- 392, // g
- 415.3f, // g#
- 440, // a
- 466.16, // a#
- 493.88, // b
- };
-
synth_voice buffer[12];
- fill_voices(buffer, freqs, 12);
+ fill_voices(buffer, &midi_freqs[49], 12);
synth_voices voices = {
.buffer = buffer,
.size = 12,
};
- synth_params params = {
- .oscilator_type = OSC_SINE,
- };
- sound_loop_start(meta->pcm, meta->queue, &voices, &params);
+ sound_loop_start(meta->pcm, meta->queue, &voices);
check(snd_pcm_drop(meta->pcm));
return NULL;
diff --git a/src/sounds.h b/src/sounds.h
index d8c8435..290a0b0 100644
--- a/src/sounds.h
+++ b/src/sounds.h
@@ -8,6 +8,7 @@
#include <stdbool.h>
#include "messages.h"
+#include "midi_freqs.h"
#define check(ret) \
do { \
diff --git a/src/ui.c b/src/ui.c
index 12888e5..fcc9877 100644
--- a/src/ui.c
+++ b/src/ui.c
@@ -1,5 +1,91 @@
#include "ui.h"
+bool point_is_inside_circle(Clay_Vector2 point, Clay_BoundingBox circle) {
+ float center_x = circle.x + circle.width / 2;
+ float center_y = circle.y + circle.height / 2;
+ float min_diameter = circle.width > circle.height ? circle.height : circle.width;
+ float radius = min_diameter / 2;
+
+ return (point.x - center_x) * (point.x - center_x) + (point.y - center_y) * (point.y - center_y) <= radius * radius;
+}
+
+float point_angle_on_circle(Clay_Vector2 point, Clay_BoundingBox circle) {
+ float center_x = circle.x + circle.width / 2;
+ float center_y = circle.y + circle.width / 2;
+
+ float point_rad = SDL_atan2f(point.y - center_y, point.x - center_x);
+
+ return point_rad * 180.0f / SDL_PI_F;
+}
+
+float point_value_on_circle(Clay_Vector2 point, Clay_BoundingBox circle, float start_angle) {
+ float point_on_circle = point_angle_on_circle(point, circle);
+ float value = SDL_fmodf((point_on_circle - start_angle + 360), 360) / 360;
+ return value;
+}
+
+
+static inline float normalize_value(float value, float range_start, float range_end)
+{
+ if (range_end == range_start) return 0.0f; // avoid divide-by-zero
+ float normalized = (value - range_start) / (range_end - range_start);
+ if (normalized < 0.0f) normalized = 0.0f;
+ if (normalized > 1.0f) normalized = 1.0f;
+ return normalized;
+}
+
+static inline float denormalize_value(float normalized, float range_start, float range_end)
+{
+ if (normalized < 0.0f) normalized = 0.0f;
+ if (normalized > 1.0f) normalized = 1.0f;
+ return range_start + normalized * (range_end - range_start);
+}
+
+
+bool circle_hovered(void) {
+ if (!Clay_Hovered()) {
+ return false;
+ }
+
+ Clay_PointerData pointer = Clay_GetPointerState();
+
+ Clay_ElementId element_id = Clay_GetCurrentElementId();
+ Clay_ElementData element_data = Clay_GetElementData(element_id);
+
+ return point_is_inside_circle(pointer.position, element_data.boundingBox);
+}
+
+void handle_knob_press(Clay_ElementId element_id, Clay_PointerData pointer_info, intptr_t user_data) {
+ Clay_ElementData element_data = Clay_GetElementData(element_id);
+
+ if (!point_is_inside_circle(pointer_info.position, element_data.boundingBox)) {
+ return;
+ }
+
+ UIKnobData *knob = (UIKnobData *)user_data;
+ UIData *ui_data = knob->ui_data;
+
+ if (pointer_info.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME
+ || pointer_info.state == CLAY_POINTER_DATA_PRESSED) {
+
+ float normalized_value = point_value_on_circle(pointer_info.position,
+ element_data.boundingBox,
+ knob->start_angle);
+ float start = knob->info->range_start;
+ float end = knob->info->range_end;
+ float value = denormalize_value(normalized_value, start, end);
+
+ knob->info->value = value;
+ mqueue_push(ui_data->msg_queue, (synth_message){
+ .type = MSG_PARAM_CHANGE,
+ .param_change = {
+ .param_type = knob->info->param_type,
+ .value = value,
+ },
+ });
+ }
+}
+
void handle_key_press(Clay_ElementId element_id, Clay_PointerData pointer_info, intptr_t user_data) {
UIData *ui_data = (UIData *)user_data;
int idx = element_id.offset;
@@ -67,7 +153,7 @@ void draw_white_key(size_t idx, UIData *ui_data) {
}
CLAY(CLAY_IDI("white_key", idx), {
.layout = {
- .sizing = {CLAY_SIZING_FIXED(40), .height = CLAY_SIZING_FIXED(100)},
+ .sizing = {CLAY_SIZING_FIXED(40), CLAY_SIZING_FIXED(100)},
},
.backgroundColor = fill_color,
.border = { .width = {1, 1, 1, 1, 0}, .color = border_color},
@@ -120,11 +206,11 @@ void draw_black_key(size_t idx, UIData *ui_data) {
fill_color = COLOR_BG;
border_color = COLOR_FG;
}
+
CLAY(CLAY_IDI("black_key", idx), {
.layout = {
- .sizing = {CLAY_SIZING_FIXED(25), .height = CLAY_SIZING_FIXED(65)},
+ .sizing = {CLAY_SIZING_FIXED(25), CLAY_SIZING_FIXED(65)},
},
-
.backgroundColor = fill_color,
.border = { .width = {1, 1, 0, 1, 0}, .color = border_color},
});
@@ -132,9 +218,11 @@ void draw_black_key(size_t idx, UIData *ui_data) {
}
void draw_keyboard(UIData *ui_data) {
- CLAY(CLAY_ID("keyboard"), {
+ CLAY(CLAY_ID("keyboard_container"), {
.layout = {
+ .sizing = {CLAY_SIZING_FIXED(280), CLAY_SIZING_FIXED(100)},
.layoutDirection = CLAY_LEFT_TO_RIGHT,
+ .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER},
},
}) {
for (size_t i = 0; i < ui_data->keys_amount; i++) {
@@ -156,25 +244,116 @@ void draw_keyboard(UIData *ui_data) {
}
}
-void draw_screen() {}
-void draw_nob() {}
+void draw_knob(Clay_ElementId id, UIData *ui_data, KnobInfo* knob_info, Clay_SizingAxis outer_size, Clay_SizingAxis inner_size) {
+ CLAY(id) {
+ bool hovered = circle_hovered();
+ UIKnobData *knob_data = arena_alloc(ui_data->arena, sizeof(UIKnobData));
+ knob_data->ui_data = ui_data;
+ knob_data->start_angle = -90;
+ knob_data->info = knob_info;
+ float value = normalize_value(knob_info->value, knob_info->range_start, knob_info->range_end);
+
+
+ Clay_OnHover(handle_knob_press, (intptr_t)knob_data);
+
+ CustomElementData *knob_element_data = arena_alloc(ui_data->arena, sizeof(CustomElementData));
+ knob_element_data->type = CUSTOM_ELEMENT_TYPE_CIRCLE;
+ knob_element_data->circle = (CircleData){
+ .start_angle = -90,
+ .value = value,
+ .color = hovered ? COLOR_FG_INTER : COLOR_FG,
+ };
+ CLAY_AUTO_ID({
+ .layout = {
+ .sizing = {outer_size, outer_size},
+ .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER},
+ },
+ .custom = {
+ .customData = knob_element_data,
+ },
+ }) {
+ CustomElementData *inner_data = arena_alloc(ui_data->arena, sizeof(CustomElementData));
+ inner_data->type = CUSTOM_ELEMENT_TYPE_CIRCLE;
+ inner_data->circle = (CircleData){
+ .start_angle = 0,
+ .value = 1.0f,
+ .color = COLOR_ACCENT,
+ };
+ CLAY_AUTO_ID({
+ .layout = {
+ .sizing = {inner_size, inner_size},
+ },
+ .custom = {
+ .customData = inner_data,
+ },
+ });
+ };
+ }
+}
+
+void draw_panel(UIData *ui_data) {
+ CLAY(CLAY_ID("panel_container"), {
+ .layout = {
+ .sizing = {CLAY_SIZING_FIXED(450), CLAY_SIZING_FIXED(100)},
+ .childGap = 30,
+ .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER},
+ },
+ }) {
+ CLAY(CLAY_ID("volume_knob_container")) {
+ draw_knob(CLAY_ID("volume_knob"), ui_data, &ui_data->knob_settings->volume, CLAY_SIZING_FIXED(85), CLAY_SIZING_FIXED(45));
+ }
+
+ CLAY(CLAY_ID("wave_screen"), {
+ .layout = {
+ .sizing = {CLAY_SIZING_FIXED(200), CLAY_SIZING_FIXED(100)},
+ },
+ .border = { .width = {1, 1, 1, 1, 0}, .color = COLOR_FG },
+ });
+
+ CLAY(CLAY_ID("envelope_knobs_container"), {
+ .layout = {
+ .layoutDirection = CLAY_TOP_TO_BOTTOM,
+ .childGap = 5,
+ },
+ }) {
+ CLAY(CLAY_ID("envelope_knobs_upper"), {
+ .layout = {
+ .childGap = 5,
+ },
+ }) {
+ draw_knob(CLAY_ID("attack_knob"), ui_data, &ui_data->knob_settings->attack, CLAY_SIZING_FIXED(40), CLAY_SIZING_FIXED(21));
+ draw_knob(CLAY_ID("decay_knob"), ui_data, &ui_data->knob_settings->decay, CLAY_SIZING_FIXED(40), CLAY_SIZING_FIXED(21));
+ }
+ CLAY(CLAY_ID("envelope_knobs_lower"), {
+ .layout = {
+ .childGap = 5,
+ },
+ }) {
+ draw_knob(CLAY_ID("sustain_knob"), ui_data, &ui_data->knob_settings->sustain, CLAY_SIZING_FIXED(40), CLAY_SIZING_FIXED(21));
+ draw_knob(CLAY_ID("release_knob"), ui_data, &ui_data->knob_settings->release, CLAY_SIZING_FIXED(40), CLAY_SIZING_FIXED(21));
+ }
+ }
+ };
+}
void draw_ui(UIData *ui_data) {
CLAY(CLAY_ID("outer_container"), {
.layout = {
.sizing = {CLAY_SIZING_GROW(0), CLAY_SIZING_GROW(0)},
- .padding = CLAY_PADDING_ALL(16),
- .childGap = 16,
.childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER},
},
.backgroundColor = COLOR_BG,
+ }) {
+ CLAY(CLAY_ID("ui_container"), {
+ .layout = {
+ .sizing = {CLAY_SIZING_FIT(0), CLAY_SIZING_FIT(0)},
+ .layoutDirection = CLAY_TOP_TO_BOTTOM,
+ .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER},
+ .childGap = 30,
+ },
}) {
- /* CLAY(CLAY_ID("app_container"), { */
- /* .layout = { */
- /* .sizing = {CLAY_SIZING_PERCENT(0.75), CLAY_SIZING_PERCENT(0.75)} */
- /* }, */
- /* }) { */
+ draw_panel(ui_data);
draw_keyboard(ui_data);
- /* }; */
+ };
};
}
diff --git a/src/ui.h b/src/ui.h
index 9651c41..3c89d3e 100644
--- a/src/ui.h
+++ b/src/ui.h
@@ -8,13 +8,17 @@
#include <SDL3/SDL_scancode.h>
#include <SDL3/SDL_keycode.h>
-#include "clay/clay.h"
+#include "arena.h"
+
+#include "clay_renderer_SDL3.h"
+#include "clay.h"
#include "messages.h"
static const Clay_Color COLOR_BG = (Clay_Color){45, 53, 59, 255};
static const Clay_Color COLOR_BG_INTER = (Clay_Color){52, 63, 68, 255};
static const Clay_Color COLOR_FG = (Clay_Color){211, 198, 170, 255};
static const Clay_Color COLOR_FG_INTER = (Clay_Color){227, 212, 181, 255};
+static const Clay_Color COLOR_ACCENT = (Clay_Color){133, 146, 137, 255};
typedef struct {
char letter;
@@ -24,11 +28,34 @@ typedef struct {
} KeyState;
typedef struct {
+ float value;
+ param_type param_type;
+ float range_start;
+ float range_end;
+} KnobInfo;
+
+typedef struct {
+ KnobInfo volume;
+ KnobInfo attack;
+ KnobInfo decay;
+ KnobInfo sustain;
+ KnobInfo release;
+} KnobSettings;
+
+typedef struct {
message_queue *msg_queue;
KeyState *keys;
size_t keys_amount;
+ KnobSettings *knob_settings;
+ Arena *arena;
} UIData;
+typedef struct {
+ UIData *ui_data;
+ float start_angle;
+ KnobInfo *info;
+} UIKnobData;
+
void draw_ui(UIData *ui_data);
#endif // UI_H_