summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-08 13:47:55 +0900
committerAdam Malczewski <[email protected]>2026-06-08 13:47:55 +0900
commit60c084ce1f9b1ff237b275ecbc89634468660077 (patch)
treed7cbabd27c6352f25770c00a677397825bc1f29a
parent2a31f14a71b207e910c932f1328ce119f7cd6981 (diff)
downloadstudy-player-60c084ce1f9b1ff237b275ecbc89634468660077.tar.gz
study-player-60c084ce1f9b1ff237b275ecbc89634468660077.zip
ui: add tab bar, layout editor with drag-to-reposition
- GuiTabBar from raygui v5.0 for Player/Layout tab switching - Layout tab: 5 draggable element placeholders (Title, Bar, Status, Play button, Help) — press+hold+drag to reposition - Auto-save config file on tab switch from Layout back to Player - config_save() added to config module - raygui include path added to Makefile, warnings suppressed via #pragma for third-party header
-rw-r--r--Makefile2
-rw-r--r--src/config.c30
-rw-r--r--src/config.h1
-rw-r--r--src/layout_editor.c206
-rw-r--r--src/layout_editor.h5
-rw-r--r--src/main.c38
6 files changed, 280 insertions, 2 deletions
diff --git a/Makefile b/Makefile
index be802da..71a058d 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,7 @@ RAYLIB_SRCS := $(RAYLIB_SRC)/rcore.c $(RAYLIB_SRC)/rshapes.c $(RAYLIB_SRC)/rtext
RAYLIB_OBJS := $(patsubst $(RAYLIB_SRC)/%.c,$(BUILD_DIR)/raylib/%.o,$(RAYLIB_SRCS))
# Common include paths
-COMMON_INCS := -I$(BUILD_DIR) -I$(RAYLIB_SRC) -I$(RAYLIB_SRC)/external/glfw/include
+COMMON_INCS := -I$(BUILD_DIR) -I$(RAYLIB_SRC) -I$(RAYLIB_SRC)/external/glfw/include -Ideps/raygui/src
# ---------------------------------------------------------------------------
# Linux native configuration (default target)
diff --git a/src/config.c b/src/config.c
index 1b3ecc2..1399de8 100644
--- a/src/config.c
+++ b/src/config.c
@@ -104,6 +104,10 @@ static int parse_config(const char *path, UILayout *layout)
}
fclose(fp);
+
+ layout->barX = (1920.0f - layout->barWidth) / 2.0f;
+ layout->statusY = layout->barY + layout->barHeight + 30.0f;
+
return loaded;
}
@@ -114,5 +118,29 @@ int config_load(const char *exePath, UILayout *layout)
char cfgPath[1024];
build_config_path(exePath, cfgPath, sizeof(cfgPath));
- return parse_config(cfgPath, layout);
+ int loaded = parse_config(cfgPath, layout);
+ layout->barX = (1920.0f - layout->barWidth) / 2.0f;
+ layout->statusY = layout->barY + layout->barHeight + 30.0f;
+ return loaded;
+}
+
+int config_save(const char *exePath, const UILayout *layout)
+{
+ char cfgPath[1024];
+ build_config_path(exePath, cfgPath, sizeof(cfgPath));
+
+ FILE *fp = fopen(cfgPath, "w");
+ if (!fp) return 0;
+
+ fprintf(fp, "# Study Player layout config\n");
+ fprintf(fp, "title_y=%.2f\n", layout->titleY);
+ fprintf(fp, "bar_y=%.2f\n", layout->barY);
+ fprintf(fp, "bar_height=%.2f\n", layout->barHeight);
+ fprintf(fp, "bar_width=%.2f\n", layout->barWidth);
+ fprintf(fp, "btn_radius=%.2f\n", layout->btnRadius);
+ fprintf(fp, "help_y=%.2f\n", layout->helpY);
+ fprintf(fp, "btn_y=%.2f\n", layout->btnY);
+ fprintf(fp, "btn_center_x=%.2f\n", layout->btnCenterX);
+ fclose(fp);
+ return 1;
}
diff --git a/src/config.h b/src/config.h
index e2eb65e..ce96a5b 100644
--- a/src/config.h
+++ b/src/config.h
@@ -14,3 +14,4 @@ typedef struct {
} UILayout;
int config_load(const char *exePath, UILayout *layout);
+int config_save(const char *exePath, const UILayout *layout);
diff --git a/src/layout_editor.c b/src/layout_editor.c
new file mode 100644
index 0000000..a728548
--- /dev/null
+++ b/src/layout_editor.c
@@ -0,0 +1,206 @@
+#include "layout_editor.h"
+#include "raylib.h"
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#define RAYGUI_IMPLEMENTATION
+#include "raygui.h"
+#pragma GCC diagnostic pop
+
+#define TITLE_W 400.0f
+#define TITLE_H 70.0f
+#define TIME_W 80.0f
+#define PCT_W 60.0f
+#define PCT_H 30.0f
+#define STATUS_W 120.0f
+#define STATUS_H 40.0f
+#define HELP_W 600.0f
+#define HELP_H 30.0f
+
+static int dragIndex = -1;
+static float dragOffsetX = 0.0f;
+static float dragOffsetY = 0.0f;
+
+void layout_editor_init(void)
+{
+ GuiLoadStyleDefault();
+}
+
+static Color fillColor = { 60, 60, 80, 100 };
+static Color borderColor = { 180, 180, 200, 200 };
+static Color highlightColor = { 233, 69, 96, 150 };
+static Color labelColor = { 234, 234, 234, 255 };
+static const int labelFontSize = 20;
+
+static void draw_label(const char *text, Rectangle r, Color fill, Color border)
+{
+ DrawRectangleRec(r, fill);
+ DrawRectangleLinesEx(r, 2.0f, border);
+ int textY = (int)(r.y + r.height / 2.0f - (float)labelFontSize / 2.0f);
+ DrawText(text, (int)r.x + 10, textY, labelFontSize, labelColor);
+}
+
+void layout_editor_draw(const char *exePath, UILayout *layout)
+{
+ (void)exePath;
+
+ int screenW = GetScreenWidth();
+ Vector2 mouse = GetMousePosition();
+
+ /* --- Hit detection on press --- */
+ if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
+ dragIndex = -1;
+
+ /* Check element 4 first (Help), then 3, 2, 1, 0.
+ Later checks overwrite earlier ones if they overlap —
+ priority goes to elements drawn last / on top. */
+
+ /* 4: Help */
+ {
+ Rectangle r = { 40.0f, layout->helpY, HELP_W, HELP_H };
+ if (CheckCollisionPointRec(mouse, r)) {
+ dragIndex = 4;
+ dragOffsetY = mouse.y - layout->helpY;
+ }
+ }
+
+ /* 3: Play button (circle) */
+ {
+ float dx = mouse.x - layout->btnCenterX;
+ float dy = mouse.y - layout->btnY;
+ if (dx * dx + dy * dy <= layout->btnRadius * layout->btnRadius) {
+ dragIndex = 3;
+ dragOffsetX = mouse.x - layout->btnCenterX;
+ dragOffsetY = mouse.y - layout->btnY;
+ }
+ }
+
+ /* 2: Status */
+ {
+ Rectangle r = { (float)screenW / 2.0f - STATUS_W / 2.0f,
+ layout->statusY, STATUS_W, STATUS_H };
+ if (CheckCollisionPointRec(mouse, r)) {
+ dragIndex = 2;
+ dragOffsetY = mouse.y - layout->statusY;
+ }
+ }
+
+ /* 1: Bar group (bar + time labels + percentage) */
+ {
+ float gx = layout->barX - 90.0f;
+ float gw = layout->barWidth + 10.0f + 80.0f + 90.0f;
+ float gy = layout->barY - 40.0f;
+ float gh = layout->barHeight + 40.0f;
+ Rectangle group = { gx, gy, gw, gh };
+ if (CheckCollisionPointRec(mouse, group)) {
+ dragIndex = 1;
+ dragOffsetX = mouse.x - layout->barX;
+ dragOffsetY = mouse.y - layout->barY;
+ }
+ }
+
+ /* 0: Title */
+ {
+ Rectangle r = { (float)screenW / 2.0f - TITLE_W / 2.0f,
+ layout->titleY, TITLE_W, TITLE_H };
+ if (CheckCollisionPointRec(mouse, r)) {
+ dragIndex = 0;
+ dragOffsetY = mouse.y - layout->titleY;
+ }
+ }
+ }
+
+ /* --- Drag update --- */
+ if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && dragIndex >= 0) {
+ switch (dragIndex) {
+ case 0: /* Title */
+ layout->titleY = mouse.y - dragOffsetY;
+ break;
+ case 1: /* Bar group */
+ layout->barX = mouse.x - dragOffsetX;
+ layout->barY = mouse.y - dragOffsetY;
+ break;
+ case 2: /* Status */
+ layout->statusY = mouse.y - dragOffsetY;
+ break;
+ case 3: /* Play button */
+ layout->btnCenterX = mouse.x - dragOffsetX;
+ layout->btnY = mouse.y - dragOffsetY;
+ break;
+ case 4: /* Help */
+ layout->helpY = mouse.y - dragOffsetY;
+ break;
+ }
+ }
+
+ /* --- Release --- */
+ if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
+ dragIndex = -1;
+ }
+
+ Color barFill = (dragIndex == 1) ? highlightColor : fillColor;
+
+ /* --- 0: Title --- */
+ {
+ Color f = (dragIndex == 0) ? highlightColor : fillColor;
+ Rectangle r = { (float)screenW / 2.0f - TITLE_W / 2.0f,
+ layout->titleY, TITLE_W, TITLE_H };
+ draw_label("Title", r, f, borderColor);
+ }
+
+ /* --- 1: Bar group --- */
+ /* Percentage above bar (centered on bar's X) */
+ {
+ Rectangle r = { layout->barX + layout->barWidth / 2.0f - PCT_W / 2.0f,
+ layout->barY - 40.0f, PCT_W, PCT_H };
+ draw_label("%", r, barFill, borderColor);
+ }
+ /* Elapsed time */
+ {
+ Rectangle r = { layout->barX - 90.0f, layout->barY,
+ TIME_W, layout->barHeight };
+ draw_label("Time", r, barFill, borderColor);
+ }
+ /* Bar */
+ {
+ Rectangle r = { layout->barX, layout->barY,
+ layout->barWidth, layout->barHeight };
+ draw_label("Bar", r, barFill, borderColor);
+ }
+ /* Remaining time */
+ {
+ Rectangle r = { layout->barX + layout->barWidth + 10.0f,
+ layout->barY, TIME_W, layout->barHeight };
+ draw_label("Time", r, barFill, borderColor);
+ }
+
+ /* --- 2: Status --- */
+ {
+ Color f = (dragIndex == 2) ? highlightColor : fillColor;
+ Rectangle r = { (float)screenW / 2.0f - STATUS_W / 2.0f,
+ layout->statusY, STATUS_W, STATUS_H };
+ draw_label("Status", r, f, borderColor);
+ }
+
+ /* --- 3: Play button --- */
+ {
+ float cx = layout->btnCenterX;
+ float cy = layout->btnY;
+ float r = layout->btnRadius;
+ Color f = (dragIndex == 3) ? highlightColor : fillColor;
+ DrawCircle((int)cx, (int)cy, r, f);
+ DrawCircleLines((int)cx, (int)cy, r, borderColor);
+ float half = r * 0.5f;
+ Vector2 v1 = { cx - half * 0.7f, cy - half };
+ Vector2 v2 = { cx - half * 0.7f, cy + half };
+ Vector2 v3 = { cx + half * 0.8f, cy };
+ DrawTriangle(v1, v2, v3, labelColor);
+ }
+
+ /* --- 4: Help --- */
+ {
+ Color f = (dragIndex == 4) ? highlightColor : fillColor;
+ Rectangle r = { 40.0f, layout->helpY, HELP_W, HELP_H };
+ draw_label("Help", r, f, borderColor);
+ }
+}
diff --git a/src/layout_editor.h b/src/layout_editor.h
new file mode 100644
index 0000000..a05cca7
--- /dev/null
+++ b/src/layout_editor.h
@@ -0,0 +1,5 @@
+#pragma once
+#include "config.h"
+
+void layout_editor_init(void);
+void layout_editor_draw(const char *exePath, UILayout *layout);
diff --git a/src/main.c b/src/main.c
index 7f3d1d8..f07b9a0 100644
--- a/src/main.c
+++ b/src/main.c
@@ -4,6 +4,8 @@
#include <ctype.h>
#include <stdio.h>
#include "config.h"
+#include "layout_editor.h"
+#include "raygui.h"
#include "font_data.h"
#ifdef PLATFORM_LINUX
@@ -54,6 +56,10 @@ static Color btnHoverColor;
static UILayout layout;
+static int activeTab = 0;
+static int prevTab = 0;
+static char exeDir[512];
+
static int strcasecmp_ext(const char *a, const char *b)
{
while (*a && *b) {
@@ -352,6 +358,8 @@ static void update_frame(void)
}
#endif
+ if (activeTab == 0)
+ {
/* --- Button clicks --- */
if (state.loaded)
{
@@ -506,6 +514,7 @@ static void update_frame(void)
}
}
}
+ }
/* --- Music stream update --- */
if (state.loaded)
@@ -555,10 +564,20 @@ static void update_frame(void)
}
}
+ /* --- Save layout on tab switch --- */
+ if (prevTab == 1 && activeTab == 0) {
+ config_save(exeDir, &layout);
+ }
+ prevTab = activeTab;
+
/* --- Drawing --- */
BeginDrawing();
ClearBackground(bgColor);
+ char *tabNames[] = { "Player", "Layout" };
+ GuiTabBar((Rectangle){ 0, 10, SCREEN_W, 32 }, tabNames, 2, &activeTab);
+
+ if (activeTab == 0) {
if (state.loaded)
{
draw_text_centered(font, state.filename, layout.titleY, szFont, mutedColor);
@@ -693,6 +712,10 @@ static void update_frame(void)
}
}
+ } else {
+ layout_editor_draw(exeDir, &layout);
+ }
+
EndDrawing();
}
@@ -744,6 +767,21 @@ int main(void)
readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
#endif
config_load(exePath, &layout);
+
+ {
+ const char *lastSlash = strrchr(exePath, '/');
+ if (lastSlash) {
+ size_t len = (size_t)(lastSlash - exePath);
+ if (len >= sizeof(exeDir)) len = sizeof(exeDir) - 1;
+ memcpy(exeDir, exePath, len);
+ exeDir[len] = '\0';
+ } else {
+ exeDir[0] = '.';
+ exeDir[1] = '\0';
+ }
+ }
+
+ layout_editor_init();
}
#ifdef PLATFORM_WEB