#include #include #include #include #include #include #include #include #include "engine.h" #include "gui.h" #include "dispatcher.h" #include "carla.h" /* microui includes */ #define MU_IMPLEMENTATION #include "microui.h" /* --------------------------------------------------------------------------- * microui callbacks * ------------------------------------------------------------------------- */ static int text_width(mu_Font font, const char *text, int len) { (void)font; if (len == -1) { len = strlen(text); } return len * 8; } static int text_height(mu_Font font) { (void)font; return 18; } /* --------------------------------------------------------------------------- * global state * ------------------------------------------------------------------------- */ static mu_Context *ctx = NULL; static int running = 1; /* engine state */ static Engine *g_engine = NULL; static int active = 0; static float bpm = 120.0f; static int loop_length = 8; /* beats */ static int current_beat = 0; /* Carla host and plugin URI input */ /* CarlaHost is now stored in Engine struct */ static char plugin_uri_input[256]; static int plugin_uri_input_len = 0; static int selected_channel = 0; /* --------------------------------------------------------------------------- * zoom mode state * ------------------------------------------------------------------------- */ static int zoom_enabled = 0; /* 0 = normal, 1 = zoomed */ static int zoom_level = 0; /* 0 = 1x, 1 = 2x, 2 = 4x */ static int zoom_focus_scene = 0; /* which scene is focused when zoomed */ static int zoom_focus_channel = 0; /* which channel is focused when zoomed */ static const int zoom_levels[] = {1, 2, 4}; static const char *zoom_labels[] = {"1x", "2x", "4x"}; /* --------------------------------------------------------------------------- * drawing helpers (stubs for microui) * ------------------------------------------------------------------------- */ static void draw_frame(mu_Context *ctx, mu_Rect rect, int colorid) { /* stub: we rely on ncurses for actual rendering */ (void)ctx; (void)rect; (void)colorid; } /* --------------------------------------------------------------------------- * GUI update * ------------------------------------------------------------------------- */ static void gui_update(void) { mu_begin(ctx); /* main window */ if (mu_begin_window(ctx, "Jack Looper", mu_rect(10, 10, 400, 300))) { /* transport controls */ mu_layout_row(ctx, 3, (int[]) { 80, 80, 80 }, 0); if (mu_button(ctx, active ? "Stop" : "Play")) { active = !active; if (active) { Action action = { .type = ACTION_TRANSPORT_PLAY }; g_engine->dispatch(action); } else { Action action = { .type = ACTION_TRANSPORT_PAUSE }; g_engine->dispatch(action); } } if (mu_button(ctx, "Reset")) { current_beat = 0; } if (mu_button(ctx, "Quit")) { running = 0; } /* BPM slider */ mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0); mu_label(ctx, "BPM:"); mu_slider_ex(ctx, &bpm, 20.0f, 300.0f, 0, "%.0f", 0); /* loop length */ mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0); mu_label(ctx, "Length:"); mu_slider_ex(ctx, (float*)&loop_length, 1.0f, 64.0f, 0, "%.0f", 0); /* beat indicator */ mu_layout_row(ctx, 1, (int[]) { -1 }, 0); { char buf[64]; snprintf(buf, sizeof(buf), "Beat: %d / %d", current_beat + 1, loop_length); mu_label(ctx, buf); } /* progress bar */ mu_layout_row(ctx, 1, (int[]) { -1 }, 0); { float progress = (loop_length > 0) ? (float)current_beat / (float)loop_length : 0.0f; mu_Rect r = mu_layout_next(ctx); mu_draw_control_frame(ctx, mu_get_id(ctx, &progress, sizeof(progress)), r, MU_COLOR_BASE, 0); if (progress > 0.0f) { mu_Rect fill = r; fill.w = (int)(r.w * progress); mu_draw_rect(ctx, fill, mu_color(100, 200, 100, 255)); } } /* zoom indicator */ mu_layout_row(ctx, 1, (int[]) { -1 }, 0); { char buf[64]; if (zoom_enabled) { snprintf(buf, sizeof(buf), "Zoom: %s Scene:%d Channel:%d", zoom_labels[zoom_level], zoom_focus_scene + 1, zoom_focus_channel + 1); } else { snprintf(buf, sizeof(buf), "Zoom: Off"); } mu_label(ctx, buf); } /* Zoom mode controls */ mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0); mu_label(ctx, "Zoom:"); if (mu_button(ctx, zoom_enabled ? "Disable" : "Enable")) { zoom_enabled = !zoom_enabled; if (!zoom_enabled) { zoom_focus_scene = 0; zoom_focus_channel = 0; } } if (zoom_enabled) { mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0); mu_label(ctx, "Level:"); if (mu_button(ctx, zoom_labels[zoom_level])) { zoom_level = (zoom_level + 1) % 3; } mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0); mu_label(ctx, "Scene:"); { char buf[32]; snprintf(buf, sizeof(buf), "%d", zoom_focus_scene + 1); if (mu_button(ctx, buf)) { /* click to reset? no action needed */ } } mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0); mu_label(ctx, "Channel:"); { char buf[32]; snprintf(buf, sizeof(buf), "%d", zoom_focus_channel + 1); if (mu_button(ctx, buf)) { /* click to reset? no action needed */ } } /* Navigation hint */ mu_layout_row(ctx, 1, (int[]) { -1 }, 0); mu_label(ctx, "Use arrow keys to navigate when zoomed"); } /* Plugin URI input */ mu_layout_row(ctx, 2, (int[]) { 60, -1 }, 0); mu_label(ctx, "URI:"); { char display[256]; if (plugin_uri_input_len > 0) { snprintf(display, sizeof(display), "%s_", plugin_uri_input); } else { snprintf(display, sizeof(display), "type URI here..."); } mu_label(ctx, display); } mu_layout_row(ctx, 1, (int[]) { -1 }, 0); if (mu_button(ctx, "Add Plugin")) { if (plugin_uri_input_len > 0) { carla_add_plugin(&g_engine->carla_host, selected_channel, plugin_uri_input, PLUGIN_TYPE_INTERNAL); plugin_uri_input_len = 0; plugin_uri_input[0] = '\0'; } } mu_end_window(ctx); } mu_end(ctx); } /* --------------------------------------------------------------------------- * main loop * ------------------------------------------------------------------------- */ int gui_main(Engine *engine) { g_engine = engine; /* Carla host is now initialized in engine_init */ /* initialise microui */ ctx = malloc(sizeof(mu_Context)); if (!ctx) return -1; mu_init(ctx); ctx->text_width = text_width; ctx->text_height = text_height; ctx->draw_frame = draw_frame; /* ncurses setup */ initscr(); cbreak(); noecho(); keypad(stdscr, TRUE); nodelay(stdscr, TRUE); curs_set(0); /* main loop */ while (running) { /* handle input */ int ch = getch(); if (ch != ERR) { /* Handle text input for plugin URI */ if (ch >= 32 && ch <= 126 && plugin_uri_input_len < 255) { plugin_uri_input[plugin_uri_input_len++] = (char)ch; plugin_uri_input[plugin_uri_input_len] = '\0'; } else if (ch == 127 || ch == KEY_BACKSPACE) { if (plugin_uri_input_len > 0) { plugin_uri_input[--plugin_uri_input_len] = '\0'; } } else if (ch == '\n' || ch == '\r') { /* Submit plugin URI */ if (plugin_uri_input_len > 0) { carla_add_plugin(&g_engine->carla_host, selected_channel, plugin_uri_input, PLUGIN_TYPE_INTERNAL); plugin_uri_input_len = 0; plugin_uri_input[0] = '\0'; } } switch (ch) { case 'q': case 'Q': running = 0; break; case ' ': active = !active; if (active) { Action action = { .type = ACTION_TRANSPORT_PLAY }; g_engine->dispatch(action); } else { Action action = { .type = ACTION_TRANSPORT_PAUSE }; g_engine->dispatch(action); } break; case 'z': case 'Z': zoom_enabled = !zoom_enabled; if (!zoom_enabled) { zoom_focus_scene = 0; zoom_focus_channel = 0; } break; case KEY_UP: if (zoom_enabled) { int step = zoom_levels[zoom_level]; zoom_focus_scene = (zoom_focus_scene - step + MAX_SCENES) % MAX_SCENES; } else { bpm = fminf(bpm + 5.0f, 300.0f); } break; case KEY_DOWN: if (zoom_enabled) { int step = zoom_levels[zoom_level]; zoom_focus_scene = (zoom_focus_scene + step) % MAX_SCENES; } else { bpm = fmaxf(bpm - 5.0f, 20.0f); } break; case KEY_LEFT: if (zoom_enabled) { int step = zoom_levels[zoom_level]; zoom_focus_channel = (zoom_focus_channel - step + MAX_CHANNELS) % MAX_CHANNELS; } else { loop_length = (loop_length > 1) ? loop_length - 1 : 1; } break; case KEY_RIGHT: if (zoom_enabled) { int step = zoom_levels[zoom_level]; zoom_focus_channel = (zoom_focus_channel + step) % MAX_CHANNELS; } else { loop_length = (loop_length < 64) ? loop_length + 1 : 64; } break; default: break; } } /* update engine state */ AppState state = dispatcher_get_state(); current_beat = state.bar_position * 4 + state.beat_position; /* render GUI */ gui_update(); /* draw commands */ mu_Command *cmd = NULL; while (mu_next_command(ctx, &cmd)) { if (cmd->type == MU_COMMAND_TEXT) { mvprintw(cmd->text.pos.y / 18, cmd->text.pos.x / 8, "%.*s", (int)strlen(cmd->text.str), cmd->text.str); } if (cmd->type == MU_COMMAND_RECT) { /* simple rectangle rendering using ncurses */ int x = cmd->rect.rect.x / 8; int y = cmd->rect.rect.y / 18; int w = cmd->rect.rect.w / 8; int h = cmd->rect.rect.h / 18; for (int i = 0; i < h && y + i < LINES; i++) { mvhline(y + i, x, ' ', w); } } } refresh(); struct timespec ts = {0, 16666000}; /* 16666 us = 16666000 ns */ nanosleep(&ts, NULL); } /* cleanup */ carla_cleanup(&g_engine->carla_host); endwin(); free(ctx); return 0; }