MetaImGUI 1.0.0
ImGui Application Template for C++20
Loading...
Searching...
No Matches
UIRenderer.cpp
Go to the documentation of this file.
1/*
2 MetaImGUI
3 Copyright (C) 2026 A P Nicholson
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>.
17*/
18
19#include "UIRenderer.h"
20
21#include "ISSTracker.h"
22#include "Localization.h"
23#include "Logger.h"
24#include "ThemeManager.h"
25#include "UpdateChecker.h"
26#include "version.h"
27
28#include <GLFW/glfw3.h>
29#include <imgui.h>
30#include <imgui_impl_glfw.h>
31#include <imgui_impl_opengl3.h>
32#include <implot.h>
33
34#include <array>
35#include <cstdlib>
36#include <ctime>
37
38namespace MetaImGUI {
39
40// UI Layout Constants
41namespace UILayout {
42// Margins and spacing
43constexpr float LEFT_MARGIN = 50.0f;
44constexpr float TOP_MARGIN = 100.0f;
45constexpr float LINE_SPACING = 30.0f;
46constexpr float BUTTON_SPACING = 40.0f;
47constexpr float STATUS_BAR_HEIGHT = 28.0f;
48
49// Window sizes
50constexpr float ABOUT_WINDOW_WIDTH = 450.0f;
51constexpr float ABOUT_WINDOW_HEIGHT = 350.0f;
52constexpr float UPDATE_WINDOW_WIDTH = 450.0f;
53constexpr float UPDATE_WINDOW_HEIGHT = 300.0f;
54constexpr float RELEASE_NOTES_HEIGHT = 120.0f;
55
56// Button sizes
57constexpr float BUTTON_OPEN_RELEASE_WIDTH = 200.0f;
58constexpr float BUTTON_REMIND_LATER_WIDTH = 150.0f;
59constexpr float BUTTON_CLOSE_WIDTH = 75.0f;
60constexpr float BUTTON_HEIGHT = 30.0f;
61
62// Status bar
63constexpr float STATUS_CIRCLE_RADIUS = 5.0f;
64constexpr float STATUS_CIRCLE_PADDING = 6.0f;
65constexpr float STATUS_RIGHT_SIDE_WIDTH = 200.0f;
66
67// Padding and style
68constexpr float WINDOW_PADDING_X = 8.0f;
69constexpr float WINDOW_PADDING_Y = 4.0f;
70constexpr float ITEM_SPACING_X = 12.0f;
71constexpr float ITEM_SPACING_Y = 0.0f;
72constexpr float VERTICAL_SPACING_SMALL = 10.0f;
73constexpr float TEXT_WRAP_POS_MULTIPLIER = 35.0f;
74} // namespace UILayout
75
76UIRenderer::UIRenderer() = default;
77
81
82bool UIRenderer::Initialize(GLFWwindow* window) {
83 if (m_initialized) {
84 return true;
85 }
86
87 // Setup ImGui context
88 IMGUI_CHECKVERSION();
89 ImGui::CreateContext();
90 ImGuiIO& io = ImGui::GetIO();
91 io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
92
93 // Setup ImPlot context
94 ImPlot::CreateContext();
95
96 // Setup ImGui style
98
99 // Setup Platform/Renderer backends
100 ImGui_ImplGlfw_InitForOpenGL(window, true);
101 ImGui_ImplOpenGL3_Init("#version 330");
102
103 m_initialized = true;
104 return true;
105}
106
108 if (m_initialized) {
109 ImGui_ImplOpenGL3_Shutdown();
110 ImGui_ImplGlfw_Shutdown();
111 ImPlot::DestroyContext();
112 ImGui::DestroyContext();
113 m_initialized = false;
114 }
115}
116
118 ImGui_ImplOpenGL3_NewFrame();
119 ImGui_ImplGlfw_NewFrame();
120 ImGui::NewFrame();
121}
122
124 ImGui::Render();
125 ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
126}
127
128void UIRenderer::RenderMainWindow(std::function<void()> onShowAbout, std::function<void()> onShowDemo,
129 std::function<void()> onShowInputDialog) {
130 auto& loc = Localization::Instance();
131
132 // Calculate heights
133 const float contentHeight = ImGui::GetContentRegionAvail().y - UILayout::STATUS_BAR_HEIGHT;
134
135 // Main content area
136 if (ImGui::BeginChild("MainContent", ImVec2(0, contentHeight), ImGuiChildFlags_None, ImGuiWindowFlags_None)) {
137 ImGui::SetCursorPos(ImVec2(UILayout::LEFT_MARGIN, UILayout::TOP_MARGIN));
138 ImGui::Text("Welcome to MetaImGUI!");
139
141 ImGui::Text("This is a template for creating ImGui-based applications.");
142
143 ImGui::SetCursorPos(ImVec2(UILayout::LEFT_MARGIN, UILayout::TOP_MARGIN + (UILayout::LINE_SPACING * 2)));
144 ImGui::Text("Use the menu bar above to access the About dialog.");
145
146 ImGui::SetCursorPos(ImVec2(UILayout::LEFT_MARGIN,
148 if (ImGui::Button(loc.Tr("button.show_about").c_str())) {
149 if (onShowAbout) {
150 onShowAbout();
151 }
152 }
153
154 ImGui::SetCursorPos(ImVec2(UILayout::LEFT_MARGIN, UILayout::TOP_MARGIN + (UILayout::LINE_SPACING * 2) +
156 if (ImGui::Button(loc.Tr("button.show_demo").c_str())) {
157 if (onShowDemo) {
158 onShowDemo();
159 }
160 }
161
162 ImGui::SetCursorPos(ImVec2(UILayout::LEFT_MARGIN, UILayout::TOP_MARGIN + (UILayout::LINE_SPACING * 2) +
164 if (ImGui::Button(loc.Tr("button.show_input").c_str())) {
165 if (onShowInputDialog) {
166 onShowInputDialog();
167 }
168 }
169 }
170 ImGui::EndChild();
171}
172
173void UIRenderer::RenderMenuBar(std::function<void()> onExit, std::function<void()> onToggleDemo,
174 std::function<void()> onCheckUpdates, std::function<void()> onShowAbout,
175 bool showDemoWindow, std::function<void()> onToggleISSTracker, bool showISSTracker) {
176 auto& loc = Localization::Instance();
177
178 if (ImGui::BeginMenuBar()) {
179 if (ImGui::BeginMenu(loc.Tr("menu.file").c_str())) {
180 if (ImGui::MenuItem(loc.Tr("menu.exit").c_str(), "Alt+F4")) {
181 if (onExit) {
182 onExit();
183 }
184 }
185 ImGui::EndMenu();
186 }
187
188 if (ImGui::BeginMenu(loc.Tr("menu.view").c_str())) {
189 if (ImGui::MenuItem(loc.Tr("menu.demo_window").c_str(), nullptr, showDemoWindow)) {
190 if (onToggleDemo) {
191 onToggleDemo();
192 }
193 }
194
195 if (ImGui::MenuItem("ISS Tracker", nullptr, showISSTracker)) {
196 if (onToggleISSTracker) {
197 onToggleISSTracker();
198 }
199 }
200
201 ImGui::Separator();
202
203 if (ImGui::BeginMenu(loc.Tr("menu.theme").c_str())) {
204 if (ImGui::MenuItem("Dark", nullptr, ThemeManager::GetCurrent() == ThemeManager::Theme::Dark)) {
206 }
207 if (ImGui::MenuItem("Light", nullptr, ThemeManager::GetCurrent() == ThemeManager::Theme::Light)) {
209 }
210 if (ImGui::MenuItem("Classic", nullptr, ThemeManager::GetCurrent() == ThemeManager::Theme::Classic)) {
212 }
213 if (ImGui::MenuItem("Modern", nullptr, ThemeManager::GetCurrent() == ThemeManager::Theme::Modern)) {
215 }
216 ImGui::EndMenu();
217 }
218
219 ImGui::Separator();
220
221 if (ImGui::BeginMenu(loc.Tr("menu.language").c_str())) {
222 if (ImGui::MenuItem("English", nullptr, loc.GetCurrentLanguage() == "en")) {
223 loc.SetLanguage("en");
224 }
225 if (ImGui::MenuItem("Español", nullptr, loc.GetCurrentLanguage() == "es")) {
226 loc.SetLanguage("es");
227 }
228 if (ImGui::MenuItem("Français", nullptr, loc.GetCurrentLanguage() == "fr")) {
229 loc.SetLanguage("fr");
230 }
231 if (ImGui::MenuItem("Deutsch", nullptr, loc.GetCurrentLanguage() == "de")) {
232 loc.SetLanguage("de");
233 }
234 ImGui::EndMenu();
235 }
236 ImGui::EndMenu();
237 }
238
239 if (ImGui::BeginMenu(loc.Tr("menu.help").c_str())) {
240 if (ImGui::MenuItem(loc.Tr("menu.check_updates").c_str())) {
241 if (onCheckUpdates) {
242 onCheckUpdates();
243 }
244 }
245 ImGui::Separator();
246 if (ImGui::MenuItem(loc.Tr("menu.about").c_str(), "Ctrl+A")) {
247 if (onShowAbout) {
248 onShowAbout();
249 }
250 }
251 ImGui::EndMenu();
252 }
253
254 ImGui::EndMenuBar();
255 }
256}
257
258void UIRenderer::RenderStatusBar(const std::string& statusMessage, float fps, const char* version,
259 bool updateInProgress) {
260 // Status bar styling - theme-aware background
261 const ImVec4 windowBg = ImGui::GetStyle().Colors[ImGuiCol_WindowBg];
262 const ImVec4 statusBarBg = ImVec4(windowBg.x * 0.85f, // Slightly darker/lighter than window
263 windowBg.y * 0.85f, windowBg.z * 0.85f, 1.0f);
264
265 ImGui::PushStyleColor(ImGuiCol_ChildBg, statusBarBg);
266 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(UILayout::WINDOW_PADDING_X, UILayout::WINDOW_PADDING_Y));
267 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(UILayout::ITEM_SPACING_X, UILayout::ITEM_SPACING_Y));
268
269 // Draw subtle top separator line - theme-aware
270 ImDrawList* drawList = ImGui::GetWindowDrawList();
271 const ImVec2 statusBarPos = ImGui::GetCursorScreenPos();
272 const ImVec2 lineEnd = ImVec2(statusBarPos.x + ImGui::GetContentRegionAvail().x, statusBarPos.y);
273 const ImVec4 separatorColor = ImGui::GetStyle().Colors[ImGuiCol_Separator];
274 drawList->AddLine(statusBarPos, lineEnd, ImGui::ColorConvertFloat4ToU32(separatorColor), 1.0f);
275
276 if (ImGui::BeginChild("StatusBar", ImVec2(0, UILayout::STATUS_BAR_HEIGHT), ImGuiChildFlags_None,
277 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
278 // Left side - Status message with indicator
279
280 // Reserve space for the circle and get aligned position
281 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (UILayout::STATUS_CIRCLE_RADIUS * 2) +
283 ImGui::AlignTextToFramePadding();
284
285 // Draw status indicator circle aligned with text
286 drawList = ImGui::GetWindowDrawList();
287 const ImVec2 textPos = ImGui::GetCursorScreenPos();
288 const ImVec2 circleCenter =
290 textPos.y + (ImGui::GetFrameHeight() * 0.5f));
291
292 ImU32 indicatorColor = IM_COL32(50, 200, 50, 255); // Green by default
293 if (updateInProgress) {
294 indicatorColor = IM_COL32(255, 200, 50, 255); // Yellow for in-progress
295 } else {
296 indicatorColor = IM_COL32(50, 200, 50, 255); // Green for ready
297 }
298
299 drawList->AddCircleFilled(circleCenter, UILayout::STATUS_CIRCLE_RADIUS, indicatorColor);
300
301 // Draw text
302 ImGui::Text("%s", statusMessage.c_str());
303
304 // Right side - Version and FPS
305 ImGui::SameLine();
306 ImGui::SetCursorPosX(ImGui::GetWindowWidth() - UILayout::STATUS_RIGHT_SIDE_WIDTH);
307
308 // Version
309 ImGui::TextDisabled("v%s", version);
310
311 // Separator
312 ImGui::SameLine();
313 ImGui::TextDisabled("|");
314
315 // FPS counter
316 ImGui::SameLine();
317 ImGui::TextDisabled("%.0f FPS", fps);
318 }
319 ImGui::EndChild();
320
321 ImGui::PopStyleVar(2);
322 ImGui::PopStyleColor();
323}
324
325void UIRenderer::RenderAboutWindow(bool& showAboutWindow) {
326 if (!showAboutWindow) {
327 return;
328 }
329
330 auto& loc = Localization::Instance();
331
332 ImGui::SetNextWindowSize(ImVec2(UILayout::ABOUT_WINDOW_WIDTH, UILayout::ABOUT_WINDOW_HEIGHT),
333 ImGuiCond_FirstUseEver);
334 if (ImGui::Begin("About MetaImGUI", &showAboutWindow, ImGuiWindowFlags_AlwaysAutoResize)) {
335 ImGui::Text("MetaImGUI v%s", Version::VERSION);
336 ImGui::TextDisabled("Build: %s", Version::VERSION_FULL);
337 ImGui::Separator();
338
339 ImGui::Text("A template for creating ImGui-based applications");
340 ImGui::Spacing();
341
342 ImGui::Text("Built with:");
343 ImGui::BulletText("ImGui v1.92.4");
344 ImGui::BulletText("ImPlot v0.17");
345 ImGui::BulletText("GLFW");
346 ImGui::BulletText("OpenGL 4.6 (4.1 on macOS)");
347 ImGui::BulletText("C++20");
348 ImGui::Separator();
349
350 ImGui::Text("This template provides:");
351 ImGui::BulletText("Basic ImGui application structure");
352 ImGui::BulletText("Cross-platform build system");
353 ImGui::BulletText("Dependency management");
354 ImGui::BulletText("Automated CI/CD and releases");
355 ImGui::BulletText("Version management from git");
356 ImGui::BulletText("Modern C++20 codebase");
357 ImGui::Spacing();
358 ImGui::TextWrapped("Use this as a starting point for your own ImGui applications!");
359
360 ImGui::Separator();
361 ImGui::TextDisabled("Git: %s (%s)", Version::COMMIT, Version::BRANCH);
362 ImGui::TextDisabled("Config: %s", Version::BUILD_CONFIG);
363
364 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + UILayout::VERTICAL_SPACING_SMALL);
365 if (ImGui::Button(loc.Tr("button.close").c_str())) {
366 showAboutWindow = false;
367 }
368 }
369 ImGui::End();
370}
371
372void UIRenderer::RenderUpdateNotification(bool& showUpdateNotification, UpdateInfo* updateInfo) {
373 if (updateInfo == nullptr) {
374 showUpdateNotification = false;
375 return;
376 }
377
378 ImGui::SetNextWindowSize(ImVec2(UILayout::UPDATE_WINDOW_WIDTH, UILayout::UPDATE_WINDOW_HEIGHT),
379 ImGuiCond_FirstUseEver);
380 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
381
382 const char* windowTitle = updateInfo->updateAvailable ? "Update Available" : "No Updates Available";
383 if (ImGui::Begin(windowTitle, &showUpdateNotification, ImGuiWindowFlags_NoCollapse)) {
384 if (updateInfo->updateAvailable) {
385 // Update available UI
386 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 0.8f, 0.2f, 1.0f));
387 ImGui::Text("A new version is available!");
388 ImGui::PopStyleColor();
389
390 ImGui::Separator();
391 ImGui::Spacing();
392
393 ImGui::Text("Current version: v%s", updateInfo->currentVersion.c_str());
394 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 1.0f, 1.0f));
395 ImGui::Text("Latest version: v%s", updateInfo->latestVersion.c_str());
396 ImGui::PopStyleColor();
397
398 ImGui::Spacing();
399 ImGui::Separator();
400 ImGui::Spacing();
401
402 if (!updateInfo->releaseNotes.empty()) {
403 ImGui::Text("Release Notes:");
404 ImGui::BeginChild("ReleaseNotes", ImVec2(0, UILayout::RELEASE_NOTES_HEIGHT), ImGuiChildFlags_Border);
405 ImGui::TextWrapped("%s", updateInfo->releaseNotes.c_str());
406 ImGui::EndChild();
407 }
408
409 ImGui::Spacing();
410 ImGui::Separator();
411 ImGui::Spacing();
412
413 ImGui::Text("Visit the release page to download:");
414 ImGui::Spacing();
415
416 // Buttons for update available
417 if (ImGui::Button("Open Release Page",
419 // Open URL in browser
420 const std::string url = updateInfo->releaseUrl;
421
422 // Security: Validate URL before opening to prevent command injection
423 // Only allow https:// URLs from github.com
424 bool isValidUrl = false;
425 if (url.starts_with("https://")) {
426 // Check if it contains github.com (the expected domain)
427 if (url.find("github.com") != std::string::npos) {
428 // Check for shell metacharacters that could be used for injection
429 const std::string dangerousChars = ";|&$`\n<>(){}[]'\"\\";
430 bool hasDangerousChars = false;
431 for (const char c : dangerousChars) {
432 if (url.find(c) != std::string::npos) {
433 hasDangerousChars = true;
434 break;
435 }
436 }
437 isValidUrl = !hasDangerousChars;
438 }
439 }
440
441 if (isValidUrl) {
442#ifdef _WIN32
443 // Windows: Use system() with start command
444 const std::string cmd = "start \"\" \"" + url + "\"";
445 [[maybe_unused]] const int result = std::system(cmd.c_str());
446#elif __APPLE__
447 // macOS: Use system() but with validated/escaped URL
448 const std::string cmd = "open \"" + url + "\"";
449 [[maybe_unused]] const int result = std::system(cmd.c_str());
450#else
451 // Linux: Use system() but with validated/escaped URL
452 const std::string cmd = "xdg-open \"" + url + "\"";
453 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: URL validated, single-threaded UI context
454 [[maybe_unused]] const int result = std::system(cmd.c_str());
455#endif
456 } else {
457 LOG_ERROR("Rejected potentially malicious URL: {}", url);
458 }
459 }
460
461 ImGui::SameLine();
462
463 if (ImGui::Button("Remind Me Later",
465 showUpdateNotification = false;
466 }
467
468 ImGui::SameLine();
469
470 if (ImGui::Button("Close", ImVec2(UILayout::BUTTON_CLOSE_WIDTH, UILayout::BUTTON_HEIGHT))) {
471 showUpdateNotification = false;
472 }
473 } else {
474 // No update available UI
475 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 0.4f, 1.0f));
476 ImGui::Text("You're up to date!");
477 ImGui::PopStyleColor();
478
479 ImGui::Separator();
480 ImGui::Spacing();
481
482 ImGui::Text("Current version: v%s", updateInfo->currentVersion.c_str());
483 if (!updateInfo->latestVersion.empty()) {
484 ImGui::Text("Latest version: v%s", updateInfo->latestVersion.c_str());
485 }
486
487 ImGui::Spacing();
488 ImGui::Separator();
489 ImGui::Spacing();
490
491 ImGui::TextWrapped("You are running the latest version of MetaImGUI.");
492 ImGui::TextWrapped("Check back later for updates!");
493
494 ImGui::Spacing();
495 ImGui::Separator();
496 ImGui::Spacing();
497
498 // Center the OK button
499 const float buttonWidth = 100.0f;
500 const float windowWidth = ImGui::GetWindowSize().x;
501 ImGui::SetCursorPosX((windowWidth - buttonWidth) * 0.5f);
502
503 if (ImGui::Button("OK", ImVec2(buttonWidth, UILayout::BUTTON_HEIGHT))) {
504 showUpdateNotification = false;
505 }
506 }
507 }
508 ImGui::End();
509}
510
511void UIRenderer::ShowDemoWindow(bool& showDemoWindow) {
512 if (showDemoWindow) {
513 ImGui::ShowDemoWindow(&showDemoWindow);
514 }
515}
516
517void UIRenderer::RenderISSTrackerWindow(bool& showISSTracker, ISSTracker* issTracker) {
518 if (!showISSTracker || issTracker == nullptr) {
519 return;
520 }
521
522 ImGui::SetNextWindowSize(ImVec2(900, 700), ImGuiCond_FirstUseEver);
523 if (ImGui::Begin("ISS Tracker", &showISSTracker)) {
524 // Get current position
525 const ISSPosition currentPos = issTracker->GetCurrentPosition();
526
527 // Control panel
528 ImGui::BeginGroup();
529 {
530 ImGui::Text("ISS Position Tracker");
531 ImGui::Separator();
532
533 // Control buttons
534 if (issTracker->IsTracking()) {
535 if (ImGui::Button("Stop Tracking")) {
536 issTracker->StopTracking();
537 }
538 } else {
539 if (ImGui::Button("Start Tracking")) {
540 issTracker->StartTracking();
541 }
542 }
543
544 ImGui::Separator();
545
546 // Display current position info
547 if (currentPos.valid) {
548 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 0.4f, 1.0f));
549 ImGui::Text("Status: Active");
550 ImGui::PopStyleColor();
551
552 ImGui::Spacing();
553 ImGui::Text("Latitude: %.4f°", currentPos.latitude);
554 ImGui::Text("Longitude: %.4f°", currentPos.longitude);
555 ImGui::Text("Altitude: %.2f km", currentPos.altitude);
556 ImGui::Text("Velocity: %.2f km/h", currentPos.velocity);
557
558 // Convert Unix timestamp to readable time
559 if (currentPos.timestamp > 0) {
560 const auto time = static_cast<time_t>(currentPos.timestamp);
561 struct tm timeinfo = {};
562 std::array<char, 80> buffer{};
563
564 // Use thread-safe versions of gmtime
565#ifdef _WIN32
566 if (gmtime_s(&timeinfo, &time) == 0) {
567 strftime(buffer.data(), buffer.size(), "%Y-%m-%d %H:%M:%S UTC", &timeinfo);
568 ImGui::Text("Time: %s", buffer.data());
569 } else {
570 ImGui::Text("Time: (error converting timestamp)");
571 }
572#else
573 if (gmtime_r(&time, &timeinfo) != nullptr) {
574 strftime(buffer.data(), buffer.size(), "%Y-%m-%d %H:%M:%S UTC", &timeinfo);
575 ImGui::Text("Time: %s", buffer.data());
576 } else {
577 ImGui::Text("Time: (error converting timestamp)");
578 }
579#endif
580 }
581 } else {
582 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.4f, 0.4f, 1.0f));
583 ImGui::Text("Status: No data");
584 ImGui::PopStyleColor();
585 ImGui::TextWrapped("Click 'Start Tracking' or 'Fetch Now' to get ISS position data.");
586 }
587 }
588 ImGui::EndGroup();
589
590 ImGui::Separator();
591
592 // Get position history
593 std::vector<double> latitudes, longitudes;
594 issTracker->GetPositionHistory(latitudes, longitudes);
595
596 // Plot area
597 if (ImPlot::BeginPlot("ISS Orbit", ImVec2(-1, -1))) {
598 // Set axis limits for Earth coordinates
599 ImPlot::SetupAxes("Longitude (°)", "Latitude (°)");
600 ImPlot::SetupAxisLimits(ImAxis_X1, -180, 180, ImGuiCond_Always);
601 ImPlot::SetupAxisLimits(ImAxis_Y1, -90, 90, ImGuiCond_Always);
602
603 // Plot orbit trail if we have data
604 if (!latitudes.empty() && !longitudes.empty()) {
605 ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle, 2.0f);
606 ImPlot::PlotLine("Orbit Trail", longitudes.data(), latitudes.data(),
607 static_cast<int>(longitudes.size()));
608 }
609
610 // Plot current position as a larger marker
611 if (currentPos.valid) {
612 ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle, 5.0f, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
613 ImPlot::PlotScatter("Current Position", &currentPos.longitude, &currentPos.latitude, 1);
614 }
615
616 // Add grid reference lines
617 ImPlot::SetNextLineStyle(ImVec4(0.5f, 0.5f, 0.5f, 0.3f));
618 std::array<double, 2> xRange = {-180.0, 180.0};
619 std::array<double, 2> yZero = {0.0, 0.0};
620 ImPlot::PlotLine("Equator", xRange.data(), yZero.data(), 2);
621
622 std::array<double, 2> xZero = {0.0, 0.0};
623 std::array<double, 2> yRange = {-90.0, 90.0};
624 ImPlot::PlotLine("Prime Meridian", xZero.data(), yRange.data(), 2);
625
626 ImPlot::EndPlot();
627 }
628 }
629 ImGui::End();
630
631 // If window was closed, stop tracking
632 if (!showISSTracker && issTracker->IsTracking()) {
633 issTracker->StopTracking();
634 }
635}
636
637void UIRenderer::HelpMarker(const char* desc) {
638 ImGui::TextDisabled("(?)");
639 if (ImGui::IsItemHovered()) {
640 ImGui::BeginTooltip();
641 ImGui::PushTextWrapPos(ImGui::GetFontSize() * UILayout::TEXT_WRAP_POS_MULTIPLIER);
642 ImGui::TextUnformatted(desc);
643 ImGui::PopTextWrapPos();
644 ImGui::EndTooltip();
645 }
646}
647
648} // namespace MetaImGUI
#define LOG_ERROR(...)
Definition Logger.h:220
ISS Tracker that fetches ISS position data asynchronously.
Definition ISSTracker.h:51
void GetPositionHistory(std::vector< double > &latitudes, std::vector< double > &longitudes) const
Get position history for orbit trail (thread-safe)
ISSPosition GetCurrentPosition() const
Get the current ISS position (thread-safe)
void StopTracking()
Stop tracking.
void StartTracking(std::function< void(const ISSPosition &)> callback=nullptr)
Start tracking ISS position asynchronously.
bool IsTracking() const
Check if tracking is active.
static Localization & Instance()
Get singleton instance.
static void Apply(Theme theme)
Apply a theme to the current ImGui context.
static Theme GetCurrent()
Get the currently active theme.
@ Light
ImGui's default light theme.
@ Dark
ImGui's default dark theme.
@ Modern
Custom light theme with rounded corners and borders.
@ Classic
ImGui's classic theme.
void ShowDemoWindow(bool &showDemoWindow)
Show ImGui demo window.
void RenderUpdateNotification(bool &showUpdateNotification, UpdateInfo *updateInfo)
Render the update notification dialog.
void RenderAboutWindow(bool &showAboutWindow)
Render the about dialog.
void RenderMainWindow(std::function< void()> onShowAbout, std::function< void()> onShowDemo, std::function< void()> onShowInputDialog=nullptr)
Render the main application window.
void RenderStatusBar(const std::string &statusMessage, float fps, const char *version, bool updateInProgress)
Render the status bar.
void Shutdown()
Shutdown ImGui context.
void RenderMenuBar(std::function< void()> onExit, std::function< void()> onToggleDemo, std::function< void()> onCheckUpdates, std::function< void()> onShowAbout, bool showDemoWindow, std::function< void()> onToggleISSTracker=nullptr, bool showISSTracker=false)
Render the menu bar.
void BeginFrame()
Begin a new ImGui frame.
static void HelpMarker(const char *desc)
Helper to show tooltip with question mark.
void RenderISSTrackerWindow(bool &showISSTracker, ISSTracker *issTracker)
Render the ISS tracker window.
bool Initialize(GLFWwindow *window)
Initialize ImGui context and backends.
void EndFrame()
End the current ImGui frame and render.
constexpr float STATUS_RIGHT_SIDE_WIDTH
constexpr float BUTTON_REMIND_LATER_WIDTH
constexpr float ABOUT_WINDOW_HEIGHT
constexpr float RELEASE_NOTES_HEIGHT
constexpr float BUTTON_HEIGHT
constexpr float ABOUT_WINDOW_WIDTH
constexpr float BUTTON_OPEN_RELEASE_WIDTH
constexpr float STATUS_CIRCLE_RADIUS
constexpr float VERTICAL_SPACING_SMALL
constexpr float BUTTON_CLOSE_WIDTH
constexpr float WINDOW_PADDING_Y
constexpr float TOP_MARGIN
constexpr float STATUS_BAR_HEIGHT
constexpr float BUTTON_SPACING
constexpr float LEFT_MARGIN
constexpr float WINDOW_PADDING_X
constexpr float STATUS_CIRCLE_PADDING
constexpr float ITEM_SPACING_Y
constexpr float UPDATE_WINDOW_HEIGHT
constexpr float UPDATE_WINDOW_WIDTH
constexpr float TEXT_WRAP_POS_MULTIPLIER
constexpr float LINE_SPACING
constexpr float ITEM_SPACING_X
std::string latestVersion
std::string currentVersion