blob: 955b3f52e7718cebde112adabb90cf325a805e89 [file] [log] [blame]
Austin Schuh1e69f942020-11-14 15:06:14 -08001/*----------------------------------------------------------------------------*/
2/* Copyright (c) 2019-2020 FIRST. All Rights Reserved. */
3/* Open Source Software - may be modified and shared by FRC teams. The code */
4/* must be accompanied by the FIRST BSD license file in the root directory of */
5/* the project. */
6/*----------------------------------------------------------------------------*/
7
8#include "wpigui.h"
9
10#include <algorithm>
11#include <cstdio>
12#include <cstring>
13
14#include <GLFW/glfw3.h>
15#include <imgui.h>
16#include <imgui_ProggyDotted.h>
17#include <imgui_impl_glfw.h>
18#include <imgui_internal.h>
19#include <implot.h>
20#include <stb_image.h>
21
22#include "wpigui_internal.h"
23
24using namespace wpi::gui;
25
26namespace wpi {
27
28Context* gui::gContext;
29
30static void ErrorCallback(int error, const char* description) {
31 std::fprintf(stderr, "GLFW Error %d: %s\n", error, description);
32}
33
34static void WindowSizeCallback(GLFWwindow* window, int width, int height) {
35 if (!gContext->maximized) {
36 gContext->width = width;
37 gContext->height = height;
38 }
39 PlatformRenderFrame();
40}
41
42static void FramebufferSizeCallback(GLFWwindow* window, int width, int height) {
43 PlatformFramebufferSizeChanged(width, height);
44}
45
46static void WindowMaximizeCallback(GLFWwindow* window, int maximized) {
47 gContext->maximized = maximized;
48}
49
50static void WindowPosCallback(GLFWwindow* window, int xpos, int ypos) {
51 if (!gContext->maximized) {
52 gContext->xPos = xpos;
53 gContext->yPos = ypos;
54 }
55}
56
57static void* IniReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
58 const char* name) {
59 if (std::strcmp(name, "GLOBAL") != 0) return nullptr;
60 return static_cast<SavedSettings*>(gContext);
61}
62
63static void IniReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
64 void* entry, const char* lineStr) {
65 auto impl = static_cast<SavedSettings*>(entry);
66 const char* value = std::strchr(lineStr, '=');
67 if (!value) return;
68 ++value;
69 int num = std::atoi(value);
70 if (std::strncmp(lineStr, "width=", 6) == 0) {
71 impl->width = num;
72 impl->loadedWidthHeight = true;
73 } else if (std::strncmp(lineStr, "height=", 7) == 0) {
74 impl->height = num;
75 impl->loadedWidthHeight = true;
76 } else if (std::strncmp(lineStr, "maximized=", 10) == 0) {
77 impl->maximized = num;
78 } else if (std::strncmp(lineStr, "xpos=", 5) == 0) {
79 impl->xPos = num;
80 } else if (std::strncmp(lineStr, "ypos=", 5) == 0) {
81 impl->yPos = num;
82 } else if (std::strncmp(lineStr, "userScale=", 10) == 0) {
83 impl->userScale = num;
84 } else if (std::strncmp(lineStr, "style=", 6) == 0) {
85 impl->style = num;
86 }
87}
88
89static void IniWriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler,
90 ImGuiTextBuffer* out_buf) {
91 if (!gContext) return;
92 out_buf->appendf(
93 "[MainWindow][GLOBAL]\nwidth=%d\nheight=%d\nmaximized=%d\n"
94 "xpos=%d\nypos=%d\nuserScale=%d\nstyle=%d\n\n",
95 gContext->width, gContext->height, gContext->maximized, gContext->xPos,
96 gContext->yPos, gContext->userScale, gContext->style);
97}
98
99void gui::CreateContext() {
100 gContext = new Context;
101 AddFont("ProggyDotted", [](ImGuiIO& io, float size, const ImFontConfig* cfg) {
102 return ImGui::AddFontProggyDotted(io, size, cfg);
103 });
104 PlatformCreateContext();
105}
106
107void gui::DestroyContext() {
108 PlatformDestroyContext();
109 delete gContext;
110 gContext = nullptr;
111}
112
113bool gui::Initialize(const char* title, int width, int height) {
114 gContext->title = title;
115 gContext->width = width;
116 gContext->height = height;
117 gContext->defaultWidth = width;
118 gContext->defaultHeight = height;
119
120 // Setup window
121 glfwSetErrorCallback(ErrorCallback);
122 glfwInitHint(GLFW_JOYSTICK_HAT_BUTTONS, GLFW_FALSE);
123 PlatformGlfwInitHints();
124 if (!glfwInit()) return false;
125
126 PlatformGlfwWindowHints();
127
128 // Setup Dear ImGui context
129 IMGUI_CHECKVERSION();
130 ImGui::CreateContext();
131 ImPlot::CreateContext();
132 ImGuiIO& io = ImGui::GetIO();
133
134 // Hook ini handler to save settings
135 ImGuiSettingsHandler iniHandler;
136 iniHandler.TypeName = "MainWindow";
137 iniHandler.TypeHash = ImHashStr(iniHandler.TypeName);
138 iniHandler.ReadOpenFn = IniReadOpen;
139 iniHandler.ReadLineFn = IniReadLine;
140 iniHandler.WriteAllFn = IniWriteAll;
141 ImGui::GetCurrentContext()->SettingsHandlers.push_back(iniHandler);
142
143 for (auto&& initialize : gContext->initializers) {
144 if (initialize) initialize();
145 }
146
147 // Load INI file
148 ImGui::LoadIniSettingsFromDisk(io.IniFilename);
149
150 // Set initial window settings
151 glfwWindowHint(GLFW_MAXIMIZED, gContext->maximized ? GLFW_TRUE : GLFW_FALSE);
152
153 if (gContext->width == 0 || gContext->height == 0) {
154 gContext->width = gContext->defaultWidth;
155 gContext->height = gContext->defaultHeight;
156 gContext->loadedWidthHeight = false;
157 }
158
159 float windowScale = 1.0;
160 if (!gContext->loadedWidthHeight) {
161 glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);
162 // get the primary monitor work area to see if we have a reasonable initial
163 // window size; if not, maximize, and default scaling to smaller
164 if (GLFWmonitor* primary = glfwGetPrimaryMonitor()) {
165 int monWidth, monHeight;
166 glfwGetMonitorWorkarea(primary, nullptr, nullptr, &monWidth, &monHeight);
167 if (monWidth < gContext->width || monHeight < gContext->height) {
168 glfwWindowHint(GLFW_MAXIMIZED, GLFW_TRUE);
169 windowScale = (std::min)(monWidth * 1.0 / gContext->width,
170 monHeight * 1.0 / gContext->height);
171 }
172 }
173 }
174 if (gContext->xPos != -1 && gContext->yPos != -1)
175 glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
176
177 // Create window with graphics context
178 gContext->window =
179 glfwCreateWindow(gContext->width, gContext->height,
180 gContext->title.c_str(), nullptr, nullptr);
181 if (!gContext->window) return false;
182
183 if (!gContext->loadedWidthHeight) {
184 if (windowScale == 1.0)
185 glfwGetWindowContentScale(gContext->window, &windowScale, nullptr);
186 // force user scale if window scale is smaller
187 if (windowScale <= 0.5)
188 gContext->userScale = 0;
189 else if (windowScale <= 0.75)
190 gContext->userScale = 1;
191 if (windowScale != 1.0) {
192 for (auto&& func : gContext->windowScalers) func(windowScale);
193 }
194 }
195
196 // Update window settings
197 if (gContext->xPos != -1 && gContext->yPos != -1) {
198 glfwSetWindowPos(gContext->window, gContext->xPos, gContext->yPos);
199 glfwShowWindow(gContext->window);
200 }
201
202 // Set window callbacks
203 glfwGetWindowSize(gContext->window, &gContext->width, &gContext->height);
204 glfwSetWindowSizeCallback(gContext->window, WindowSizeCallback);
205 glfwSetFramebufferSizeCallback(gContext->window, FramebufferSizeCallback);
206 glfwSetWindowMaximizeCallback(gContext->window, WindowMaximizeCallback);
207 glfwSetWindowPosCallback(gContext->window, WindowPosCallback);
208
209 // Setup Dear ImGui style
210 SetStyle(static_cast<Style>(gContext->style));
211
212 // Load Fonts
213 // this range is based on 13px being the "nominal" 100% size and going from
214 // ~0.5x (7px) to ~2.0x (25px)
215 for (auto&& makeFont : gContext->makeFonts) {
216 if (makeFont.second) {
217 auto& font = gContext->fonts.emplace_back();
218 for (int i = 0; i < Font::kScaledLevels; ++i) {
219 float size = 7.0f + i * 3.0f;
220 ImFontConfig cfg;
221 std::snprintf(cfg.Name, sizeof(cfg.Name), "%s-%d", makeFont.first,
222 static_cast<int>(size));
223 font.scaled[i] = makeFont.second(io, size, &cfg);
224 }
225 }
226 }
227
228 if (!PlatformInitRenderer()) return false;
229
230 return true;
231}
232
233void gui::Main() {
234 // Main loop
235 while (!glfwWindowShouldClose(gContext->window) && !gContext->exit) {
236 // Poll and handle events (inputs, window resize, etc.)
237 glfwPollEvents();
238 PlatformRenderFrame();
239 }
240
241 // Cleanup
242 PlatformShutdown();
243 ImGui_ImplGlfw_Shutdown();
244 ImPlot::DestroyContext();
245 ImGui::DestroyContext();
246
247 glfwDestroyWindow(gContext->window);
248 glfwTerminate();
249}
250
251void gui::CommonRenderFrame() {
252 ImGui_ImplGlfw_NewFrame();
253
254 // Start the Dear ImGui frame
255 ImGui::NewFrame();
256
257 // Scale based on OS window content scaling
258 float windowScale = 1.0;
259 glfwGetWindowContentScale(gContext->window, &windowScale, nullptr);
260 // map to closest font size: 0 = 0.5x, 1 = 0.75x, 2 = 1.0x, 3 = 1.25x,
261 // 4 = 1.5x, 5 = 1.75x, 6 = 2x
262 gContext->fontScale = std::clamp(
263 gContext->userScale + static_cast<int>((windowScale - 1.0) * 4), 0,
264 Font::kScaledLevels - 1);
265 ImGui::GetIO().FontDefault = gContext->fonts[0].scaled[gContext->fontScale];
266
267 for (size_t i = 0; i < gContext->earlyExecutors.size(); ++i) {
268 auto& execute = gContext->earlyExecutors[i];
269 if (execute) execute();
270 }
271
272 for (size_t i = 0; i < gContext->lateExecutors.size(); ++i) {
273 auto& execute = gContext->lateExecutors[i];
274 if (execute) execute();
275 }
276
277 // Rendering
278 ImGui::Render();
279}
280
281void gui::Exit() {
282 if (!gContext) return;
283 gContext->exit = true;
284}
285
286void gui::AddInit(std::function<void()> initialize) {
287 if (initialize) gContext->initializers.emplace_back(std::move(initialize));
288}
289
290void gui::AddWindowScaler(std::function<void(float scale)> windowScaler) {
291 if (windowScaler)
292 gContext->windowScalers.emplace_back(std::move(windowScaler));
293}
294
295void gui::AddEarlyExecute(std::function<void()> execute) {
296 if (execute) gContext->earlyExecutors.emplace_back(std::move(execute));
297}
298
299void gui::AddLateExecute(std::function<void()> execute) {
300 if (execute) gContext->lateExecutors.emplace_back(std::move(execute));
301}
302
303GLFWwindow* gui::GetSystemWindow() { return gContext->window; }
304
305int gui::AddFont(
306 const char* name,
307 std::function<ImFont*(ImGuiIO& io, float size, const ImFontConfig* cfg)>
308 makeFont) {
309 if (makeFont) gContext->makeFonts.emplace_back(name, std::move(makeFont));
310 return gContext->makeFonts.size() - 1;
311}
312
313ImFont* gui::GetFont(int font) {
314 return gContext->fonts[font].scaled[gContext->fontScale];
315}
316
317void gui::SetStyle(Style style) {
318 gContext->style = static_cast<int>(style);
319 switch (style) {
320 case kStyleClassic:
321 ImGui::StyleColorsClassic();
322 break;
323 case kStyleDark:
324 ImGui::StyleColorsDark();
325 break;
326 case kStyleLight:
327 ImGui::StyleColorsLight();
328 break;
329 }
330}
331
332void gui::SetClearColor(ImVec4 color) { gContext->clearColor = color; }
333
334void gui::EmitViewMenu() {
335 if (ImGui::BeginMenu("View")) {
336 if (ImGui::BeginMenu("Style")) {
337 bool selected;
338 selected = gContext->style == kStyleClassic;
339 if (ImGui::MenuItem("Classic", nullptr, &selected, true))
340 SetStyle(kStyleClassic);
341 selected = gContext->style == kStyleDark;
342 if (ImGui::MenuItem("Dark", nullptr, &selected, true))
343 SetStyle(kStyleDark);
344 selected = gContext->style == kStyleLight;
345 if (ImGui::MenuItem("Light", nullptr, &selected, true))
346 SetStyle(kStyleLight);
347 ImGui::EndMenu();
348 }
349
350 if (ImGui::BeginMenu("Zoom")) {
351 for (int i = 0; i < Font::kScaledLevels && (25 * (i + 2)) <= 200; ++i) {
352 char label[20];
353 std::snprintf(label, sizeof(label), "%d%%", 25 * (i + 2));
354 bool selected = gContext->userScale == i;
355 bool enabled = (gContext->fontScale - gContext->userScale + i) >= 0 &&
356 (gContext->fontScale - gContext->userScale + i) <
357 Font::kScaledLevels;
358 if (ImGui::MenuItem(label, nullptr, &selected, enabled))
359 gContext->userScale = i;
360 }
361 ImGui::EndMenu();
362 }
363 ImGui::EndMenu();
364 }
365}
366
367bool gui::UpdateTextureFromImage(ImTextureID* texture, int width, int height,
368 const unsigned char* data, int len) {
369 // Load from memory
370 int width2 = 0;
371 int height2 = 0;
372 unsigned char* imgData =
373 stbi_load_from_memory(data, len, &width2, &height2, nullptr, 4);
374 if (!data) return false;
375
376 if (width2 == width && height2 == height)
377 UpdateTexture(texture, kPixelRGBA, width2, height2, imgData);
378 else
379 *texture = CreateTexture(kPixelRGBA, width2, height2, imgData);
380
381 stbi_image_free(imgData);
382
383 return true;
384}
385
386bool gui::CreateTextureFromFile(const char* filename, ImTextureID* out_texture,
387 int* out_width, int* out_height) {
388 // Load from file
389 int width = 0;
390 int height = 0;
391 unsigned char* data = stbi_load(filename, &width, &height, nullptr, 4);
392 if (!data) return false;
393
394 *out_texture = CreateTexture(kPixelRGBA, width, height, data);
395 if (out_width) *out_width = width;
396 if (out_height) *out_height = height;
397
398 stbi_image_free(data);
399
400 return true;
401}
402
403bool gui::CreateTextureFromImage(const unsigned char* data, int len,
404 ImTextureID* out_texture, int* out_width,
405 int* out_height) {
406 // Load from memory
407 int width = 0;
408 int height = 0;
409 unsigned char* imgData =
410 stbi_load_from_memory(data, len, &width, &height, nullptr, 4);
411 if (!imgData) return false;
412
413 *out_texture = CreateTexture(kPixelRGBA, width, height, imgData);
414 if (out_width) *out_width = width;
415 if (out_height) *out_height = height;
416
417 stbi_image_free(imgData);
418
419 return true;
420}
421
422void gui::MaxFit(ImVec2* min, ImVec2* max, float width, float height) {
423 float destWidth = max->x - min->x;
424 float destHeight = max->y - min->y;
425 if (width == 0 || height == 0) return;
426 if (destWidth * height > destHeight * width) {
427 float outputWidth = width * destHeight / height;
428 min->x += (destWidth - outputWidth) / 2;
429 max->x -= (destWidth - outputWidth) / 2;
430 } else {
431 float outputHeight = height * destWidth / width;
432 min->y += (destHeight - outputHeight) / 2;
433 max->y -= (destHeight - outputHeight) / 2;
434 }
435}
436
437} // namespace wpi