285 lines
12 KiB
C++
285 lines
12 KiB
C++
/**
|
|
* @file main.cpp
|
|
* @brief StreamHub ImGui desktop client entry point.
|
|
*
|
|
* Usage: StreamHubClient [-host <ip>] [-port <N>]
|
|
*
|
|
* Defaults: host=127.0.0.1 port=8090
|
|
*/
|
|
|
|
#include "App.h"
|
|
#include "Icons.h"
|
|
|
|
#include "imgui.h"
|
|
#include "imgui_impl_sdl2.h"
|
|
#include "imgui_impl_opengl3.h"
|
|
#include "implot.h"
|
|
|
|
#include <SDL.h>
|
|
#include <SDL_opengl.h>
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
#include <unistd.h>
|
|
|
|
int main(int argc, char** argv) {
|
|
/* ── Parse args ─────────────────────────────────────────────────── */
|
|
std::string host = "127.0.0.1";
|
|
uint16_t port = 8090u;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "-host") == 0 && i + 1 < argc) {
|
|
host = argv[++i];
|
|
} else if (strcmp(argv[i], "-port") == 0 && i + 1 < argc) {
|
|
port = static_cast<uint16_t>(atoi(argv[++i]));
|
|
} else if (strcmp(argv[i], "-h") == 0 ||
|
|
strcmp(argv[i], "--help") == 0) {
|
|
printf("Usage: StreamHubClient [-host <ip>] [-port <N>]\n");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* ── SDL2 + OpenGL ──────────────────────────────────────────────── */
|
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
|
fprintf(stderr, "SDL_Init error: %s\n", SDL_GetError());
|
|
return 1;
|
|
}
|
|
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
|
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
|
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
|
|
|
SDL_WindowFlags wf = static_cast<SDL_WindowFlags>(
|
|
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
|
|
SDL_Window* window = SDL_CreateWindow(
|
|
"StreamHub Client",
|
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
|
1280, 800, wf);
|
|
if (!window) {
|
|
fprintf(stderr, "SDL_CreateWindow error: %s\n", SDL_GetError());
|
|
return 1;
|
|
}
|
|
|
|
SDL_GLContext glCtx = SDL_GL_CreateContext(window);
|
|
SDL_GL_MakeCurrent(window, glCtx);
|
|
SDL_GL_SetSwapInterval(1); /* vsync */
|
|
|
|
/* ── ImGui setup ─────────────────────────────────────────────────── */
|
|
IMGUI_CHECKVERSION();
|
|
ImGui::CreateContext();
|
|
ImPlot::CreateContext();
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
|
|
|
/* ── Fonts: bundled resources (Fira Sans + Font Awesome) ─────────────── *
|
|
* The font dir is resolved relative to the executable so both build-tree
|
|
* and installed layouts work; the source tree is the last fallback. */
|
|
const float kFontSizePx = 17.0f;
|
|
{
|
|
std::string exeDir;
|
|
{
|
|
char buf[4096];
|
|
ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
|
|
if (n > 0) {
|
|
buf[n] = '\0';
|
|
if (char* slash = strrchr(buf, '/')) { *slash = '\0'; }
|
|
exeDir = buf;
|
|
}
|
|
}
|
|
const std::string fontDirCandidates[] = {
|
|
exeDir + "/../share/streamhub-client/fonts", /* installed */
|
|
exeDir + "/resources/fonts", /* build tree */
|
|
std::string(APP_RESOURCE_DIR) + "/fonts", /* source tree */
|
|
};
|
|
auto findFont = [&](const char* file) -> std::string {
|
|
for (const auto& d : fontDirCandidates) {
|
|
std::string p = d + "/" + file;
|
|
if (access(p.c_str(), R_OK) == 0) { return p; }
|
|
}
|
|
return "";
|
|
};
|
|
|
|
std::string uiFont = findFont("FiraSans-Regular.ttf");
|
|
bool loaded = !uiFont.empty() &&
|
|
io.Fonts->AddFontFromFileTTF(uiFont.c_str(), kFontSizePx);
|
|
if (loaded) {
|
|
fprintf(stderr, "Font: loaded %s\n", uiFont.c_str());
|
|
} else {
|
|
io.Fonts->AddFontDefault();
|
|
fprintf(stderr, "Font: bundled font not found, using built-in.\n");
|
|
}
|
|
|
|
#ifdef HAVE_FONT_AWESOME
|
|
/* Merge Font Awesome 6 solid glyphs into the base font */
|
|
std::string faFont = findFont("fa-solid-900.ttf");
|
|
static const ImWchar faRanges[] = { ICON_MIN_FA, ICON_MAX_16_FA, 0 };
|
|
ImFontConfig faCfg;
|
|
faCfg.MergeMode = true;
|
|
faCfg.PixelSnapH = true;
|
|
faCfg.GlyphMinAdvanceX = kFontSizePx * 0.9f; /* monospace-ish icons */
|
|
faCfg.GlyphOffset = ImVec2(0.f, 1.f);
|
|
if (faFont.empty() ||
|
|
!io.Fonts->AddFontFromFileTTF(faFont.c_str(), kFontSizePx * 0.80f,
|
|
&faCfg, faRanges)) {
|
|
fprintf(stderr, "Font: failed to merge Font Awesome glyphs\n");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
ImGui::StyleColorsDark();
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
style.WindowRounding = 10.f;
|
|
style.ChildRounding = 8.f;
|
|
style.FrameRounding = 7.f; /* rounded buttons/inputs */
|
|
style.PopupRounding = 8.f;
|
|
style.GrabRounding = 7.f;
|
|
style.ScrollbarRounding = 12.f;
|
|
style.TabRounding = 7.f;
|
|
style.FramePadding = ImVec2(9.f, 5.f);
|
|
style.ItemSpacing = ImVec2(8.f, 6.f);
|
|
style.ItemInnerSpacing = ImVec2(6.f, 4.f);
|
|
style.CellPadding = ImVec2(8.f, 4.f);
|
|
style.ScrollbarSize = 11.f;
|
|
style.WindowPadding = ImVec2(10.f, 10.f);
|
|
style.PopupBorderSize = 1.f;
|
|
style.FrameBorderSize = 0.f;
|
|
style.WindowBorderSize = 1.f;
|
|
style.SeparatorTextBorderSize = 2.f;
|
|
style.WindowTitleAlign = ImVec2(0.5f, 0.5f);
|
|
|
|
/* ── Catppuccin Mocha palette ─────────────────────────────────────── */
|
|
const ImVec4 kCrust (0.067f,0.067f,0.106f,1.f); /* #11111b */
|
|
const ImVec4 kMantle (0.094f,0.094f,0.145f,1.f); /* #181825 */
|
|
const ImVec4 kBase (0.118f,0.118f,0.180f,1.f); /* #1e1e2e */
|
|
const ImVec4 kSurface0 (0.192f,0.196f,0.267f,1.f); /* #313244 */
|
|
const ImVec4 kSurface1 (0.271f,0.278f,0.353f,1.f); /* #45475a */
|
|
const ImVec4 kSurface2 (0.345f,0.357f,0.439f,1.f); /* #585b70 */
|
|
const ImVec4 kOverlay0 (0.424f,0.439f,0.525f,1.f); /* #6c7086 */
|
|
const ImVec4 kText (0.804f,0.839f,0.957f,1.f); /* #cdd6f4 */
|
|
const ImVec4 kBlue (0.537f,0.706f,0.980f,1.f); /* #89b4fa */
|
|
const ImVec4 kLavender (0.706f,0.745f,0.996f,1.f); /* #b4befe */
|
|
const ImVec4 kMauve (0.796f,0.651f,0.969f,1.f); /* #cba6f7 */
|
|
const ImVec4 kPeach (0.980f,0.702f,0.529f,1.f); /* #fab387 */
|
|
|
|
ImVec4* c = style.Colors;
|
|
c[ImGuiCol_WindowBg] = kBase;
|
|
c[ImGuiCol_ChildBg] = kMantle;
|
|
c[ImGuiCol_PopupBg] = kMantle;
|
|
c[ImGuiCol_Border] = kSurface0;
|
|
c[ImGuiCol_BorderShadow] = ImVec4(0.f,0.f,0.f,0.f);
|
|
c[ImGuiCol_FrameBg] = kSurface0;
|
|
c[ImGuiCol_FrameBgHovered] = kSurface1;
|
|
c[ImGuiCol_FrameBgActive] = kSurface1;
|
|
c[ImGuiCol_TitleBg] = kCrust;
|
|
c[ImGuiCol_TitleBgActive] = kMantle;
|
|
c[ImGuiCol_TitleBgCollapsed] = kCrust;
|
|
c[ImGuiCol_MenuBarBg] = kCrust;
|
|
c[ImGuiCol_ScrollbarBg] = kCrust;
|
|
c[ImGuiCol_ScrollbarGrab] = kSurface1;
|
|
c[ImGuiCol_ScrollbarGrabHovered] = kSurface2;
|
|
c[ImGuiCol_ScrollbarGrabActive] = kBlue;
|
|
c[ImGuiCol_CheckMark] = kBlue;
|
|
c[ImGuiCol_SliderGrab] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.8f);
|
|
c[ImGuiCol_SliderGrabActive] = kBlue;
|
|
c[ImGuiCol_Button] = kSurface0;
|
|
c[ImGuiCol_ButtonHovered] = kSurface1;
|
|
c[ImGuiCol_ButtonActive] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.35f);
|
|
c[ImGuiCol_Header] = kSurface0;
|
|
c[ImGuiCol_HeaderHovered] = kSurface1;
|
|
c[ImGuiCol_HeaderActive] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.4f);
|
|
c[ImGuiCol_Separator] = kSurface0;
|
|
c[ImGuiCol_SeparatorHovered] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.6f);
|
|
c[ImGuiCol_SeparatorActive] = kBlue;
|
|
c[ImGuiCol_ResizeGrip] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.2f);
|
|
c[ImGuiCol_ResizeGripHovered] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.6f);
|
|
c[ImGuiCol_ResizeGripActive] = kBlue;
|
|
c[ImGuiCol_Tab] = kSurface0;
|
|
c[ImGuiCol_TabHovered] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.3f);
|
|
c[ImGuiCol_TabActive] = kSurface1;
|
|
c[ImGuiCol_TabUnfocused] = kMantle;
|
|
c[ImGuiCol_TabUnfocusedActive]= kSurface0;
|
|
c[ImGuiCol_TableHeaderBg] = kSurface0;
|
|
c[ImGuiCol_TableBorderStrong] = kSurface1;
|
|
c[ImGuiCol_TableBorderLight] = kSurface0;
|
|
c[ImGuiCol_TableRowBg] = ImVec4(0.f,0.f,0.f,0.f);
|
|
c[ImGuiCol_TableRowBgAlt] = ImVec4(1.f,1.f,1.f,0.025f);
|
|
c[ImGuiCol_Text] = kText;
|
|
c[ImGuiCol_TextDisabled] = kOverlay0;
|
|
c[ImGuiCol_TextSelectedBg] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.35f);
|
|
c[ImGuiCol_DragDropTarget] = kPeach;
|
|
c[ImGuiCol_NavHighlight] = kLavender;
|
|
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.f,0.f,0.f,0.55f);
|
|
(void)kMauve;
|
|
|
|
// ImPlot style
|
|
ImPlotStyle& ps = ImPlot::GetStyle();
|
|
ps.PlotPadding = ImVec2(8.f,8.f);
|
|
ps.LabelPadding = ImVec2(5.f,3.f);
|
|
ps.LegendPadding = ImVec2(8.f,8.f);
|
|
ps.MinorAlpha = 0.15f;
|
|
ps.Colors[ImPlotCol_PlotBg] = kCrust;
|
|
ps.Colors[ImPlotCol_PlotBorder]= kSurface0;
|
|
ps.Colors[ImPlotCol_AxisGrid] = ImVec4(kSurface0.x,kSurface0.y,kSurface0.z,0.6f);
|
|
ps.Colors[ImPlotCol_AxisTick] = kSurface1;
|
|
ps.Colors[ImPlotCol_AxisText] = ImVec4(0.651f,0.678f,0.784f,1.f);
|
|
ps.Colors[ImPlotCol_LegendBg] = ImVec4(kMantle.x,kMantle.y,kMantle.z,0.9f);
|
|
ps.Colors[ImPlotCol_LegendBorder] = kSurface0;
|
|
ps.Colors[ImPlotCol_Crosshairs] = ImVec4(kText.x,kText.y,kText.z,0.5f);
|
|
|
|
ImGui_ImplSDL2_InitForOpenGL(window, glCtx);
|
|
ImGui_ImplOpenGL3_Init("#version 330 core");
|
|
|
|
/* ── Application ─────────────────────────────────────────────────── */
|
|
StreamHubClient::App app;
|
|
app.init(host, port);
|
|
|
|
bool quit = false;
|
|
while (!quit) {
|
|
SDL_Event event;
|
|
while (SDL_PollEvent(&event)) {
|
|
ImGui_ImplSDL2_ProcessEvent(&event);
|
|
if (event.type == SDL_QUIT) { quit = true; }
|
|
if (event.type == SDL_KEYDOWN &&
|
|
event.key.keysym.sym == SDLK_q &&
|
|
(event.key.keysym.mod & KMOD_ALT)) {
|
|
quit = true;
|
|
}
|
|
}
|
|
|
|
/* ── New frame ───────────────────────────────────────────────── */
|
|
ImGui_ImplOpenGL3_NewFrame();
|
|
ImGui_ImplSDL2_NewFrame();
|
|
ImGui::NewFrame();
|
|
|
|
app.update();
|
|
|
|
/* ── Render ──────────────────────────────────────────────────── */
|
|
ImGui::Render();
|
|
glViewport(0, 0, static_cast<int>(io.DisplaySize.x),
|
|
static_cast<int>(io.DisplaySize.y));
|
|
glClearColor(0.118f, 0.118f, 0.180f, 1.f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
|
SDL_GL_SwapWindow(window);
|
|
}
|
|
|
|
/* ── Cleanup ─────────────────────────────────────────────────────── */
|
|
app.shutdown();
|
|
ImGui_ImplOpenGL3_Shutdown();
|
|
ImGui_ImplSDL2_Shutdown();
|
|
ImPlot::DestroyContext();
|
|
ImGui::DestroyContext();
|
|
SDL_GL_DeleteContext(glCtx);
|
|
SDL_DestroyWindow(window);
|
|
SDL_Quit();
|
|
|
|
return 0;
|
|
}
|