From 7c05b7c6a75ee884149d85780cd1b4e0c15bc5c6 Mon Sep 17 00:00:00 2001 From: Matt Borgerson Date: Thu, 12 Mar 2020 02:21:50 -0700 Subject: [PATCH] ui: Add primary xemu user interface --- ui/xemu-hud.cc | 1232 ++++++++++++++++++++++++++++++++++++++++++++++++ ui/xemu-hud.h | 49 ++ 2 files changed, 1281 insertions(+) create mode 100644 ui/xemu-hud.cc create mode 100644 ui/xemu-hud.h diff --git a/ui/xemu-hud.cc b/ui/xemu-hud.cc new file mode 100644 index 0000000000..d1c25f71e0 --- /dev/null +++ b/ui/xemu-hud.cc @@ -0,0 +1,1232 @@ +/* + * xemu User Interface + * + * Copyright (C) 2020 Matt Borgerson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "xemu-hud.h" +#include "xemu-input.h" +#include "xemu-notifications.h" +#include "xemu-settings.h" +#include "xemu-shaders.h" +#include "xemu-custom-widgets.h" +#include "xemu-monitor.h" +#include "xemu-version.h" + +#include "imgui/imgui.h" +#include "ImGuiFileDialog/ImGuiFileDialog.h" +#include "imgui/examples/imgui_impl_sdl.h" +#include "imgui/examples/imgui_impl_opengl3.h" + +extern "C" { +// Include necessary QEMU headers +#include "qemu/osdep.h" +#include "qemu-common.h" +#include "ui/input.h" +#include "qapi/qapi-commands-misc.h" +#include "qapi/qapi-types-run-state.h" +#include "qom/object.h" +#include "sysemu/sysemu.h" +#include "sysemu/runstate.h" +#define typename c_typename +#define typeof decltype +#include "hw/qdev-core.h" +#include "hw/qdev-properties.h" +#include "qapi/error.h" +#include "monitor/qdev.h" +#include "qapi/qmp/qdict.h" +#include "qemu/option.h" +#include "qemu/config-file.h" +#undef typename +} + +uint32_t c = 0x81dc8a21; // FIXME: Use existing theme colors here +#define COL(color, c) (float)(((color)>>((c)*8)) & 0xff)/255.0 +ImVec4 color_active = ImVec4(COL(c, 3), COL(c, 2), COL(c, 1), COL(c, 0)); +#undef COL + +using namespace std; + +static bool show_notifications = true; +const int notification_duration = 4000; +static void render_notification(bool* p_open, float t, const char *msg); + +ImFont *fixed_width_font; +bool show_main_menu = true; +float main_menu_height; +static void ShowMainMenu(); + +bool show_first_boot_window = false; +static void ShowFirstBootWindow(bool* p_open); + +bool show_monitor_window = false; +static void ShowMonitorConsole(bool* p_open); + +bool show_input_window = false; +static void ShowInputWindow(bool* p_open); + +bool show_settings_window = false; +static void ShowSettingsWindow(bool* p_open); + +bool show_about_window = false; +static void ShowAboutWindow(bool* p_open); + +bool show_demo_window = false; + +void xemu_hud_init(SDL_Window* window, void* sdl_gl_context) +{ + xemu_monitor_init(); + + initialize_custom_ui_rendering(); + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + io.IniFilename = NULL; + + // Load fonts + io.Fonts->AddFontFromFileTTF("./data/Roboto-Medium.ttf", 16); + fixed_width_font = io.Fonts->AddFontDefault(); + + // Setup Platform/Renderer bindings + ImGui_ImplSDL2_InitForOpenGL(window, sdl_gl_context); + const char *glsl_version = "#version 150"; + ImGui_ImplOpenGL3_Init(glsl_version); + + // Set default theme, override + ImGui::StyleColorsDark(); + + ImGuiStyle& style = ImGui::GetStyle(); + style.FrameRounding = 8.0; + style.GrabRounding = 12.0; + style.PopupRounding = 5.0; + style.ScrollbarRounding = 12.0; + style.FramePadding.x = 10; + style.FramePadding.y = 4; + style.WindowBorderSize = 0; + style.PopupBorderSize = 0; + style.FrameBorderSize = 0; + style.TabBorderSize = 0; + + ImVec4* colors = ImGui::GetStyle().Colors; + colors[ImGuiCol_Text] = ImVec4(0.86f, 0.93f, 0.89f, 0.78f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.86f, 0.93f, 0.89f, 0.28f); + colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.06f, 250.0/255.0); + colors[ImGuiCol_ChildBg] = ImVec4(0.16f, 0.16f, 0.16f, 0.58f); + colors[ImGuiCol_PopupBg] = ImVec4(0.16f, 0.16f, 0.16f, 0.90f); + colors[ImGuiCol_Border] = ImVec4(0.11f, 0.11f, 0.11f, 0.60f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.16f, 0.16f, 0.16f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.16f, 0.16f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.28f, 0.71f, 0.25f, 0.78f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f); + colors[ImGuiCol_TitleBg] = ImVec4(0.20f, 0.51f, 0.18f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.16f, 0.16f, 0.16f, 0.75f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 0.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.16f, 0.16f, 0.16f, 1.00f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.20f, 0.51f, 0.18f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.28f, 0.71f, 0.25f, 0.78f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.26f, 0.26f, 0.26f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.36f, 0.36f, 0.36f, 1.00f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.28f, 0.71f, 0.25f, 0.76f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.71f, 0.25f, 0.86f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.11f, 0.11f, 0.11f, 0.60f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.13f, 0.87f, 0.16f, 0.78f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.25f, 0.75f, 0.10f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.47f, 0.83f, 0.49f, 0.04f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.28f, 0.71f, 0.25f, 0.78f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f); + colors[ImGuiCol_Tab] = ImVec4(0.26f, 0.67f, 0.23f, 0.95f); + colors[ImGuiCol_TabHovered] = ImVec4(0.28f, 0.71f, 0.25f, 0.86f); + colors[ImGuiCol_TabActive] = ImVec4(0.26f, 0.66f, 0.23f, 1.00f); + colors[ImGuiCol_TabUnfocused] = ImVec4(0.21f, 0.54f, 0.19f, 0.99f); + colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.24f, 0.60f, 0.21f, 1.00f); + colors[ImGuiCol_PlotLines] = ImVec4(0.86f, 0.93f, 0.89f, 0.63f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.86f, 0.93f, 0.89f, 0.63f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(0.28f, 0.71f, 0.25f, 1.00f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.28f, 0.71f, 0.25f, 0.43f); + colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); + colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.16f, 0.16f, 0.16f, 0.73f); + + show_first_boot_window = xemu_settings_did_fail_to_load(); + if (show_first_boot_window) { + show_main_menu = false; + } +} + +void xemu_hud_process_sdl_events(SDL_Event *event) +{ + ImGui_ImplSDL2_ProcessEvent(event); +} + +void xemu_hud_should_capture_kbd_mouse(int *kbd, int *mouse) +{ + ImGuiIO& io = ImGui::GetIO(); + if (kbd) *kbd = io.WantCaptureKeyboard; + if (mouse) *mouse = io.WantCaptureMouse; +} + +static void HelpMarker(const char* desc) +{ + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +static void ShowMainMenu() +{ + bool running = runstate_is_running(); + + if (ImGui::BeginMainMenuBar()) + { + if (ImGui::BeginMenu("Machine")) + { + ImGui::MenuItem("Input", NULL, &show_input_window); + ImGui::MenuItem("Settings", NULL, &show_settings_window); + ImGui::Separator(); + if (ImGui::MenuItem(running ? "Pause" : "Run")) { + if (running) { + vm_stop(RUN_STATE_PAUSED); + } else { + vm_start(); + } + } + // FIXME: Disabled for now because nv2a crashes during resets. This + // will be fixed shortly. + #if 0 + if (ImGui::MenuItem("Restart")) { + qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET); + } + #endif + if (ImGui::MenuItem("Shutdown")) { + qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI); + } + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("View")) + { + if (ImGui::Combo("Scaling Mode", &scaling_mode, "Center\0Scale\0Stretch\0\0")) { + xemu_settings_set_enum(XEMU_SETTINGS_DISPLAY_SCALE, scaling_mode); + xemu_settings_save(); + } + ImGui::SameLine(); HelpMarker("Controls how the rendered content should be scaled into the window"); + if (ImGui::MenuItem("Fullscreen", NULL, xemu_is_fullscreen(), true)) { + xemu_toggle_fullscreen(); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Debug")) + { + ImGui::MenuItem("Monitor", NULL, &show_monitor_window); + ImGui::Separator(); + ImGui::MenuItem("ImGUI Demo", NULL, &show_demo_window); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Help")) + { + ImGui::MenuItem("About", NULL, &show_about_window); + ImGui::EndMenu(); + } + + ImGui::SetCursorPosX(ImGui::GetWindowWidth()-100.0); + extern float fps; + ImGui::Text("%.3f", fps); + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetWindowWidth()-300.0); + ImGui::Text("%.3f", 1000.0/fps); + + main_menu_height = ImGui::GetWindowHeight(); + ImGui::EndMainMenuBar(); + } +} + +// Tracker for current notification +struct notification_display_state +{ + bool active; + uint32_t notification_end_ts; + const char *msg; +} notification; + +#include +std::deque notifications; +std::deque errors; + +void xemu_queue_notification(const char *msg) +{ + notifications.push_back(strdup(msg)); +} + +void xemu_queue_error_message(const char *msg) +{ + errors.push_back(strdup(msg)); +} + +static void render_notification(bool* p_open, float t, const char *msg) +{ + const float DISTANCE = 10.0f; + static int corner = 1; + ImGuiIO& io = ImGui::GetIO(); + if (corner != -1) + { + ImVec2 window_pos = ImVec2((corner & 1) ? io.DisplaySize.x - DISTANCE : DISTANCE, (corner & 2) ? io.DisplaySize.y - DISTANCE : DISTANCE); + window_pos.y = main_menu_height + DISTANCE; + ImVec2 window_pos_pivot = ImVec2((corner & 1) ? 1.0f : 0.0f, (corner & 2) ? 1.0f : 0.0f); + // window_pos_pivot = ImVec2(0.5f, 1.0f); + // window_pos = ImVec2(io.DisplaySize.x/2, io.DisplaySize.y - DISTANCE); + ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot); + } + + const float fade_in = 0.1; + const float fade_out = 0.9; + float fade = 0; + + if (t < fade_in) { + // Linear fade in + fade = t/fade_in; + } else if (t >= fade_out) { + // Linear fade out + fade = 1-(t-fade_out)/(1-fade_out); + } else { + // Constant + fade = 1.0; + } + + ImVec4 color = ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]; + color.w *= fade; + ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 1); + ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0,0,0,fade*0.9f)); + ImGui::PushStyleColor(ImGuiCol_Border, color); + ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::SetNextWindowBgAlpha(0.90f * fade); + if (ImGui::Begin("Notification", p_open, + ImGuiWindowFlags_Tooltip | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoInputs + )) + { + ImGui::Text("%s", msg); + } + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + ImGui::End(); +} + +void xemu_hud_render(SDL_Window *window) +{ + uint32_t now = SDL_GetTicks(); + bool ui_wakeup = false; + struct controller_state *iter; + + // Combine all controller states to allow any controller to navigate + uint32_t buttons = 0; + int16_t axis[CONTROLLER_AXIS__COUNT] = {0}; + for (iter=available_controllers; iter != NULL; iter=iter->next) { + if (iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) continue; + buttons |= iter->buttons; + // We simply take any axis that is >10 % activation + for (int i = 0; i < CONTROLLER_AXIS__COUNT; i++) { + if ((iter->axis[i] > 3276) || (iter->axis[i] < -3276)) { + axis[i] = iter->axis[i]; + } + } + } + + // If the guide button is pressed, wake the up + bool menu_button = false; + if (buttons & CONTROLLER_BUTTON_GUIDE) { + ui_wakeup = true; + menu_button = true; + } + + // Allow controllers without a guide button to also work + if ((buttons & CONTROLLER_BUTTON_BACK) && + (buttons & CONTROLLER_BUTTON_START)) { + ui_wakeup = true; + menu_button = true; + } + + // If the mouse is moved, wake the ui + static ImVec2 last_mouse_pos = ImVec2(); + ImVec2 current_mouse_pos = ImGui::GetMousePos(); + if ((current_mouse_pos.x != last_mouse_pos.x) || + (current_mouse_pos.y != last_mouse_pos.y)) { + last_mouse_pos = current_mouse_pos; + ui_wakeup = true; + } + + // If mouse capturing is enabled (we are in a dialog), ensure the UI is alive + bool controller_focus_capture = false; + ImGuiIO& io = ImGui::GetIO(); + if (io.NavActive) { + ui_wakeup = true; + controller_focus_capture = true; + } + + // Prevent controller events from going to the guest if they are being used + // to navigate the HUD + xemu_input_set_test_mode(controller_focus_capture); + + // Start the Dear ImGui frame + ImGui_ImplOpenGL3_NewFrame(); + + // Override SDL2 implementation gamecontroller interface + io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad; + ImGui_ImplSDL2_NewFrame(window); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + + // Update gamepad inputs (from imgui_impl_sdl.cpp) + memset(io.NavInputs, 0, sizeof(io.NavInputs)); + #define MAP_BUTTON(NAV_NO, BUTTON_NO) { io.NavInputs[NAV_NO] = (buttons & BUTTON_NO) ? 1.0f : 0.0f; } + #define MAP_ANALOG(NAV_NO, AXIS_NO, V0, V1) { float vn = (float)(axis[AXIS_NO] - V0) / (float)(V1 - V0); if (vn > 1.0f) vn = 1.0f; if (vn > 0.0f && io.NavInputs[NAV_NO] < vn) io.NavInputs[NAV_NO] = vn; } + const int thumb_dead_zone = 8000; // SDL_gamecontroller.h suggests using this value. + MAP_BUTTON(ImGuiNavInput_Activate, CONTROLLER_BUTTON_A); // Cross / A + MAP_BUTTON(ImGuiNavInput_Cancel, CONTROLLER_BUTTON_B); // Circle / B + MAP_BUTTON(ImGuiNavInput_Menu, CONTROLLER_BUTTON_X); // Square / X + MAP_BUTTON(ImGuiNavInput_Input, CONTROLLER_BUTTON_Y); // Triangle / Y + MAP_BUTTON(ImGuiNavInput_DpadLeft, CONTROLLER_BUTTON_DPAD_LEFT); // D-Pad Left + MAP_BUTTON(ImGuiNavInput_DpadRight, CONTROLLER_BUTTON_DPAD_RIGHT); // D-Pad Right + MAP_BUTTON(ImGuiNavInput_DpadUp, CONTROLLER_BUTTON_DPAD_UP); // D-Pad Up + MAP_BUTTON(ImGuiNavInput_DpadDown, CONTROLLER_BUTTON_DPAD_DOWN); // D-Pad Down + MAP_BUTTON(ImGuiNavInput_FocusPrev, CONTROLLER_BUTTON_WHITE); // L1 / LB + MAP_BUTTON(ImGuiNavInput_FocusNext, CONTROLLER_BUTTON_BLACK); // R1 / RB + MAP_BUTTON(ImGuiNavInput_TweakSlow, CONTROLLER_BUTTON_WHITE); // L1 / LB + MAP_BUTTON(ImGuiNavInput_TweakFast, CONTROLLER_BUTTON_BLACK); // R1 / RB + + // Allow Guide and "Back+Start" buttons to also act as Menu buttons + if (menu_button) { + io.NavInputs[ImGuiNavInput_Menu] = 1.0; + } + + MAP_ANALOG(ImGuiNavInput_LStickLeft, CONTROLLER_AXIS_LSTICK_X, -thumb_dead_zone, -32768); + MAP_ANALOG(ImGuiNavInput_LStickRight, CONTROLLER_AXIS_LSTICK_X, +thumb_dead_zone, +32767); + MAP_ANALOG(ImGuiNavInput_LStickUp, CONTROLLER_AXIS_LSTICK_Y, +thumb_dead_zone, +32767); + MAP_ANALOG(ImGuiNavInput_LStickDown, CONTROLLER_AXIS_LSTICK_Y, -thumb_dead_zone, -32767); + + ImGui::NewFrame(); + + if (show_main_menu) { + // Auto-hide main menu after 5s of inactivity + static uint32_t last_check = 0; + float alpha = 1.0; + const uint32_t timeout = 5000; + const float fade_duration = 1000.0; + if (ui_wakeup) { + last_check = now; + } + if ((now-last_check) > timeout) { + float t = fmin((float)((now-last_check)-timeout)/fade_duration, 1.0); + alpha = 1.0-t; + if (t >= 1.0) { + alpha = 0.0; + } + ImGui::SetNextWindowBgAlpha(alpha); + } + if (alpha > 0.0) { + ImVec4 tc = ImGui::GetStyle().Colors[ImGuiCol_Text]; + tc.w = alpha; + ImGui::PushStyleColor(ImGuiCol_Text, tc); + ShowMainMenu(); + ImGui::PopStyleColor(); + } else { + main_menu_height = 0; + } + } + + if (show_first_boot_window) ShowFirstBootWindow(&show_first_boot_window); + if (show_input_window) ShowInputWindow(&show_input_window); + if (show_settings_window) ShowSettingsWindow(&show_settings_window); + if (show_monitor_window) ShowMonitorConsole(&show_monitor_window); + if (show_about_window) ShowAboutWindow(&show_about_window); + if (show_demo_window) ImGui::ShowDemoWindow(&show_demo_window); + + if (notification.active) { + // Currently displaying a notification + float t = (notification.notification_end_ts - now)/(float)notification_duration; + if (t > 1.0) { + // Notification delivered, free it + free((void*)notification.msg); + notification.active = 0; + } else { + // Notification should be displayed + render_notification(&show_notifications, t, notification.msg); + } + } else { + // Check to see if a notification is pending + if (notifications.size() > 0) { + notification.msg = notifications[0]; + notification.active = 1; + notification.notification_end_ts = now+notification_duration; + notifications.pop_front(); + } + } + + // Very rudimentary error notification API + if (errors.size() > 0) { + ImGui::OpenPopup("Error"); + } + if (ImGui::BeginPopupModal("Error", NULL, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::Text("%s", errors[0]); + ImGui::Dummy(ImVec2(0,16)); + ImGui::SetItemDefaultFocus(); + ImGui::SetCursorPosX(ImGui::GetWindowWidth()-(120+10)); + if (ImGui::Button("Ok", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + free((void*)errors[0]); + errors.pop_front(); + } + ImGui::EndPopup(); + } + + // Rendering + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); +} + +void xemu_hud_cleanup(void) +{ + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplSDL2_Shutdown(); + ImGui::DestroyContext(); +} + +struct MonitorConsole +{ + char InputBuf[256]; + ImVector Items; + ImVector Commands; + ImVector History; + int HistoryPos; // -1: new line, 0..History.Size-1 browsing history. + ImGuiTextFilter Filter; + bool AutoScroll; + bool ScrollToBottom; + + MonitorConsole() + { + memset(InputBuf, 0, sizeof(InputBuf)); + HistoryPos = -1; + AutoScroll = true; + ScrollToBottom = false; + } + ~MonitorConsole() + { + } + + // Portable helpers + static int Stricmp(const char* str1, const char* str2) { int d; while ((d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; } return d; } + static int Strnicmp(const char* str1, const char* str2, int n) { int d = 0; while (n > 0 && (d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; n--; } return d; } + static char* Strdup(const char *str) { size_t len = strlen(str) + 1; void* buf = malloc(len); IM_ASSERT(buf); return (char*)memcpy(buf, (const void*)str, len); } + static void Strtrim(char* str) { char* str_end = str + strlen(str); while (str_end > str && str_end[-1] == ' ') str_end--; *str_end = 0; } + + void Draw(const char* title, bool* p_open) + { + ImGui::SetNextWindowSize(ImVec2(520,600), ImGuiCond_FirstUseEver); + if (!ImGui::Begin(title, p_open)) + { + ImGui::End(); + return; + } + + const float footer_height_to_reserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); // 1 separator, 1 input text + ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), false, ImGuiWindowFlags_HorizontalScrollbar); // Leave room for 1 separator + 1 InputText + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,1)); // Tighten spacing + ImGui::PushFont(fixed_width_font); + ImGui::TextUnformatted(xemu_get_monitor_buffer()); + ImGui::PopFont(); + + if (ScrollToBottom || (AutoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) + ImGui::SetScrollHereY(1.0f); + ScrollToBottom = false; + + ImGui::PopStyleVar(); + ImGui::EndChild(); + ImGui::Separator(); + + // Command-line + bool reclaim_focus = false; + ImGui::SetNextItemWidth(-1); + ImGui::PushFont(fixed_width_font); + if (ImGui::InputText("", InputBuf, IM_ARRAYSIZE(InputBuf), ImGuiInputTextFlags_EnterReturnsTrue|ImGuiInputTextFlags_CallbackCompletion|ImGuiInputTextFlags_CallbackHistory, &TextEditCallbackStub, (void*)this)) + { + char* s = InputBuf; + Strtrim(s); + if (s[0]) + ExecCommand(s); + strcpy(s, ""); + reclaim_focus = true; + } + ImGui::PopFont(); + + // Auto-focus on window apparition + ImGui::SetItemDefaultFocus(); + if (reclaim_focus) + ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget + + ImGui::End(); + } + + void ExecCommand(const char* command_line) + { + xemu_run_monitor_command(command_line); + + // Insert into history. First find match and delete it so it can be pushed to the back. This isn't trying to be smart or optimal. + HistoryPos = -1; + for (int i = History.Size-1; i >= 0; i--) + if (Stricmp(History[i], command_line) == 0) + { + free(History[i]); + History.erase(History.begin() + i); + break; + } + History.push_back(Strdup(command_line)); + + // On commad input, we scroll to bottom even if AutoScroll==false + ScrollToBottom = true; + } + + static int TextEditCallbackStub(ImGuiInputTextCallbackData* data) // In C++11 you are better off using lambdas for this sort of forwarding callbacks + { + MonitorConsole* console = (MonitorConsole*)data->UserData; + return console->TextEditCallback(data); + } + + int TextEditCallback(ImGuiInputTextCallbackData* data) + { + switch (data->EventFlag) + { + case ImGuiInputTextFlags_CallbackHistory: + { + // Example of HISTORY + const int prev_history_pos = HistoryPos; + if (data->EventKey == ImGuiKey_UpArrow) + { + if (HistoryPos == -1) + HistoryPos = History.Size - 1; + else if (HistoryPos > 0) + HistoryPos--; + } + else if (data->EventKey == ImGuiKey_DownArrow) + { + if (HistoryPos != -1) + if (++HistoryPos >= History.Size) + HistoryPos = -1; + } + + // A better implementation would preserve the data on the current input line along with cursor position. + if (prev_history_pos != HistoryPos) + { + const char* history_str = (HistoryPos >= 0) ? History[HistoryPos] : ""; + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, history_str); + } + } + } + return 0; + } +}; + +static void ShowMonitorConsole(bool* p_open) +{ + static MonitorConsole console; + console.Draw("Monitor", p_open); +} + +struct InputWindow +{ + InputWindow() + { + } + ~InputWindow() + { + } + + void Draw(const char* title, bool* p_open) + { + ImGui::SetNextWindowSize(ImVec2(500,620), ImGuiCond_Appearing); + + // Remove window X padding for this window to easily center stuff + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0,ImGui::GetStyle().WindowPadding.y)); + if (!ImGui::Begin(title, p_open, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + ImGui::End(); + ImGui::PopStyleVar(); + return; + } + + static int active = 0; + + // Output dimensions of texture + float t_w = 512, t_h = 512; + // Dimensions of (port+label)s + float b_x = 0, b_x_stride = 100, b_y = 400; + float b_w = 68, b_h = 81; + // Dimensions of controller (rendered at origin) + float controller_width = 477.0f; + float controller_height = 395.0f; + + // Setup rendering to fbo for controller and port images + ImTextureID id = (ImTextureID)render_to_fbo(controller_fbo); + + // + // Render buttons with icons of the Xbox style port sockets with + // circular numbers above them. These buttons can be activated to + // configure the associated port, like a tabbed interface. + // + ImVec4 color_inactive = ImVec4(0,0,0,0); + + // Begin a 4-column layout to render the ports + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0,12)); + ImGui::Columns(4, "mixed", false); + + const int port_padding = 8; + for (int i = 0; i < 4; i++) { + bool is_currently_selected = (i == active); + bool port_is_bound = (xemu_input_get_bound(i) != NULL); + + // Set an X offset to center the image button within the column + ImGui::SetCursorPosX(ImGui::GetCursorPosX()+(int)((ImGui::GetColumnWidth()-b_w-2*port_padding)/2)); + + // We are using the same texture for all buttons, but ImageButton + // uses the texture as a unique ID. Push a new ID now to resolve + // the conflict. + ImGui::PushID(i); + float x = b_x+i*b_x_stride; + ImGui::PushStyleColor(ImGuiCol_Button, is_currently_selected ? color_active : color_inactive); + bool activated = ImGui::ImageButton(id, + ImVec2(b_w,b_h), + ImVec2(x/t_w, (b_y+b_h)/t_h), + ImVec2((x+b_w)/t_w, b_y/t_h), + port_padding); + ImGui::PopStyleColor(); + + if (activated) { + active = i; + } + + uint32_t port_color = 0xafafafff; + bool is_hovered = ImGui::IsItemHovered(); + if (is_currently_selected || port_is_bound) { + port_color = 0x81dc8a00; + } else if (is_hovered) { + port_color = 0x000000ff; + } + + render_controller_port(x, b_y, i, port_color); + + ImGui::PopID(); + ImGui::NextColumn(); + } + ImGui::PopStyleVar(); // ItemSpacing + ImGui::Columns(1); + + // + // Render input device combo + // + + // Center the combo above the controller with the same width + ImGui::SetCursorPosX(ImGui::GetCursorPosX()+(int)((ImGui::GetColumnWidth()-controller_width)/2.0)); + + // Note: SetNextItemWidth applies only to the combo element, but not the + // associated label which follows, so scale back a bit to make space for + // the label. + ImGui::SetNextItemWidth(controller_width*0.75); + + // List available input devices + const char *not_connected = "Not Connected"; + struct controller_state *bound_state = xemu_input_get_bound(active); + + // Get current controller name + const char *name; + if (bound_state == NULL) { + name = not_connected; + } else { + name = bound_state->name; + } + + if (ImGui::BeginCombo("Input Devices", name)) + { + // Handle "Not connected" + bool is_selected = bound_state == NULL; + if (ImGui::Selectable(not_connected, is_selected)) { + xemu_input_bind(active, NULL, 1); + bound_state = NULL; + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + + // Handle all available input devices + struct controller_state *iter; + for (iter=available_controllers; iter != NULL; iter=iter->next) { + is_selected = bound_state == iter; + ImGui::PushID(iter); + const char *selectable_label = iter->name; + char buf[128]; + if (iter->bound >= 0) { + snprintf(buf, sizeof(buf), "%s (Port %d)", iter->name, iter->bound+1); + selectable_label = buf; + } + if (ImGui::Selectable(selectable_label, is_selected)) { + xemu_input_bind(active, iter, 1); + bound_state = iter; + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + ImGui::PopID(); + } + + ImGui::EndCombo(); + } + + // + // Add a separator between input selection and controller graphic + // + ImGui::Columns(1); + ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y)); + ImGui::Separator(); + ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y)); + + // + // Render controller image + // + bool device_selected = false; + + if (bound_state) { + device_selected = true; + render_controller(0, 0, 0x81dc8a00, 0x0f0f0f00, bound_state); + } else { + static struct controller_state state = { 0 }; + render_controller(0, 0, 0x1f1f1f00, 0x0f0f0f00, &state); + } + + // update_sdl_controller_state(&state); + // update_sdl_kbd_controller_state(&state); + ImVec2 cur = ImGui::GetCursorPos(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX()+(int)((ImGui::GetColumnWidth()-controller_width)/2.0)); + ImGui::Image(id, + ImVec2(controller_width, controller_height), + ImVec2(0, controller_height/t_h), + ImVec2(controller_width/t_w, 0)); + + if (!device_selected) { + // ImGui::SameLine(); + const char *msg = "Please select an available input device"; + ImVec2 dim = ImGui::CalcTextSize(msg); + ImGui::SetCursorPosX(cur.x + (controller_width-dim.x)/2); + ImGui::SetCursorPosY(cur.y +(controller_height-dim.y)/2); + ImGui::Text("%s", msg); + ImGui::SameLine(); + } + + ImGui::End(); + ImGui::PopStyleVar(); // Window padding + + // Restore original framebuffer target + render_to_default_fb(); + } +}; + +static void ShowInputWindow(bool* p_open) +{ + static InputWindow console; + console.Draw("Input", p_open); +} + +#define MAX_STRING_LEN 2048 // FIXME: Completely arbitrary and only used here + // to give a buffer to ImGui for each field + +struct SettingsWindow +{ + char flash_path[MAX_STRING_LEN]; + char bootrom_path[MAX_STRING_LEN]; + char hdd_path[MAX_STRING_LEN]; + char dvd_path[MAX_STRING_LEN]; + char eeprom_path[MAX_STRING_LEN]; + int memory_idx; + bool short_animation; + bool dirty; + bool pending_restart; + + SettingsWindow() + { + Load(); // Note: This does not catch updates made elsewhere! That isn't + // a problem yet, but in the future might need to be changed to + // get most recent data. I put it here so we don't need to sync + // settings back to these temporary buffers on every frame. + // Please don't do this. If you need it, consider adding an "on + // settings updated" callback to sync the updates, if necessary. + pending_restart = false; + } + + ~SettingsWindow() + { + } + + void Load(void) + { + const char *tmp; + int tmp_int; + size_t len; + + xemu_settings_get_string(XEMU_SETTINGS_SYSTEM_FLASH_PATH, &tmp); + len = strlen(tmp); + assert(len < (MAX_STRING_LEN-1)); + strncpy(flash_path, tmp, sizeof(flash_path)); + + xemu_settings_get_string(XEMU_SETTINGS_SYSTEM_BOOTROM_PATH, &tmp); + len = strlen(tmp); + assert(len < (MAX_STRING_LEN-1)); + strncpy(bootrom_path, tmp, sizeof(bootrom_path)); + + xemu_settings_get_string(XEMU_SETTINGS_SYSTEM_HDD_PATH, &tmp); + len = strlen(tmp); + assert(len < (MAX_STRING_LEN-1)); + strncpy(hdd_path, tmp, sizeof(hdd_path)); + + xemu_settings_get_string(XEMU_SETTINGS_SYSTEM_DVD_PATH, &tmp); + len = strlen(tmp); + assert(len < (MAX_STRING_LEN-1)); + strncpy(dvd_path, tmp, sizeof(dvd_path)); + + xemu_settings_get_string(XEMU_SETTINGS_SYSTEM_EEPROM_PATH, &tmp); + len = strlen(tmp); + assert(len < (MAX_STRING_LEN-1)); + strncpy(eeprom_path, tmp, sizeof(eeprom_path)); + + xemu_settings_get_int(XEMU_SETTINGS_SYSTEM_MEMORY, &tmp_int); + memory_idx = (tmp_int-64)/64; + + xemu_settings_get_bool(XEMU_SETTINGS_SYSTEM_SHORTANIM, &tmp_int); + short_animation = !!tmp_int; + + dirty = false; + } + + void Save(void) + { + xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_FLASH_PATH, flash_path); + xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_BOOTROM_PATH, bootrom_path); + xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_HDD_PATH, hdd_path); + xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_DVD_PATH, dvd_path); + xemu_settings_set_string(XEMU_SETTINGS_SYSTEM_EEPROM_PATH, eeprom_path); + xemu_settings_set_int(XEMU_SETTINGS_SYSTEM_MEMORY, 64+memory_idx*64); + xemu_settings_set_bool(XEMU_SETTINGS_SYSTEM_SHORTANIM, short_animation); + xemu_settings_save(); + xemu_queue_notification("Settings saved! Restart to apply updates."); + pending_restart = true; + } + + void FilePicker(const char *name, char *buf, size_t len, const char *filters) + { + ImGui::PushID(name); + if (ImGui::InputText("", buf, len)) { + dirty = true; + } + ImGui::SameLine(); + if (ImGui::Button("Browse...", ImVec2(100, 0))) { + ImGuiFileDialog::Instance()->OpenDialog(name, name, filters, strlen(buf) > 0 ? buf : "."); + } + ImGui::PopID(); + + if (ImGuiFileDialog::Instance()->FileDialog(name)) { + if (ImGuiFileDialog::Instance()->IsOk == true) { + std::string filePathName = ImGuiFileDialog::Instance()->GetFilepathName(); + std::string filePath = ImGuiFileDialog::Instance()->GetCurrentPath(); + strcpy(buf, filePathName.c_str()); + dirty = true; + } + + ImGuiFileDialog::Instance()->CloseDialog(name); + } + } + + void Draw(const char* title, bool* p_open) + { + ImGui::SetNextWindowSize(ImVec2(550, 300), ImGuiCond_Appearing); + if (!ImGui::Begin(title, p_open, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + ImGui::End(); + return; + } + + if (ImGui::IsWindowAppearing()) { + Load(); + } + + const char *rom_file_filters = ".bin\0.rom\0\0"; + const char *iso_file_filters = ".iso\0\0"; + const char *qcow_file_filters = ".qcow2\0\0"; + + ImGui::Columns(2, "", false); + ImGui::SetColumnWidth(0, ImGui::GetWindowWidth()*0.25); + + ImGui::Text("Flash (BIOS) File"); + ImGui::NextColumn(); + float picker_width = ImGui::GetColumnWidth()-120; + ImGui::SetNextItemWidth(picker_width); + FilePicker("###Flash", flash_path, MAX_STRING_LEN, rom_file_filters); + ImGui::NextColumn(); + + ImGui::Text("BootROM File"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(picker_width); + FilePicker("###BootROM", bootrom_path, MAX_STRING_LEN, rom_file_filters); + ImGui::NextColumn(); + + ImGui::Text("Hard Disk Image File"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(picker_width); + FilePicker("###HDD", hdd_path, MAX_STRING_LEN, qcow_file_filters); + ImGui::NextColumn(); + + ImGui::Text("DVD Image File"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(picker_width); + FilePicker("###DVD", dvd_path, MAX_STRING_LEN, iso_file_filters); + ImGui::NextColumn(); + + ImGui::Text("EEPROM File"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(picker_width); + FilePicker("###EEPROM", eeprom_path, MAX_STRING_LEN, rom_file_filters); + ImGui::NextColumn(); + + ImGui::Text("System Memory"); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(ImGui::GetColumnWidth()*0.5); + if (ImGui::Combo("###mem", &memory_idx, "64 MiB\0" "128 MiB\0" "\0")) { + dirty = true; + } + ImGui::NextColumn(); + + ImGui::Dummy(ImVec2(0,0)); + ImGui::NextColumn(); + if (ImGui::Checkbox("Skip startup animation", &short_animation)) { + dirty = true; + } + ImGui::NextColumn(); + + ImGui::Columns(1); + + ImGui::SetCursorPosY(ImGui::GetWindowHeight()-(10+20)); + if (dirty) { + ImGui::Text("Warning: Unsaved changes!"); + ImGui::SameLine(); + } else if (pending_restart) { + ImGui::Text("Restart to apply updates"); + ImGui::SameLine(); + } + + ImGui::SetCursorPosY(ImGui::GetWindowHeight()-(10+25)); + ImGui::SetCursorPosX(ImGui::GetWindowWidth()-(120+10)); + + ImGui::SetItemDefaultFocus(); + if (ImGui::Button("Save", ImVec2(120, 0))) { + Save(); + dirty = false; + pending_restart = true; + } + + ImGui::End(); + } +}; + +static void ShowSettingsWindow(bool* p_open) +{ + static SettingsWindow console; + console.Draw("Settings", p_open); +} + +struct AboutWindow +{ + char build_info_text[256]; + + AboutWindow() + { + snprintf(build_info_text, sizeof(build_info_text), + "Verson: %s\n" "Branch: %s\n" "Commit: %s\n" "Date: %s\n", + xemu_version, xemu_branch, xemu_commit, xemu_date); + // FIXME: Show platform + // FIXME: Show driver + // FIXME: Show BIOS/BootROM hash + } + + ~AboutWindow() + { + } + + void Draw(const char* title, bool* p_open) + { + ImGui::SetNextWindowSize(ImVec2(400, 350), ImGuiCond_Appearing); + if (!ImGui::Begin(title, p_open, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + ImGui::End(); + return; + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY()-50); + ImGui::SetCursorPosX((ImGui::GetWindowWidth()-256)/2); + + ImTextureID id = (ImTextureID)render_to_fbo(logo_fbo); + float t_w = 256.0; + float t_h = 256.0; + float x_off = 0; + ImGui::Image(id, + ImVec2(t_w-x_off, t_h), + ImVec2(x_off/t_w, t_h/t_h), + ImVec2(t_w/t_w, 0)); + render_logo(0x42e335ff, 0x42e335ff, 0x00000000); + render_to_default_fb(); + ImGui::SetCursorPosX(10); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY()-100); + + ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(xemu_version).x)/2); + ImGui::Text("%s", xemu_version); + + ImGui::SetCursorPosX(10); + ImGui::Dummy(ImVec2(0,35)); + + const char *msg = "Visit https://xemu.app for more information"; + ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(msg).x)/2); + ImGui::Text("%s", msg); + + ImGui::Dummy(ImVec2(0,35)); + + ImGui::PushFont(fixed_width_font); + ImGui::InputTextMultiline("##build_info", build_info_text, IM_ARRAYSIZE(build_info_text), ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 6), ImGuiInputTextFlags_ReadOnly); + ImGui::PopFont(); + if (ImGui::BeginPopupContextItem("##build_info_context", 1)) + { + if (ImGui::MenuItem("Copy to Clipboard")) { + SDL_SetClipboardText(build_info_text); + } + ImGui::EndPopup(); + } + + ImGui::End(); + } +}; + +static void ShowAboutWindow(bool* p_open) +{ + static AboutWindow console; + console.Draw("About", p_open); +} + +struct FirstBootWindow +{ + FirstBootWindow() + { + } + + ~FirstBootWindow() + { + } + + void Draw(const char* title, bool* p_open) + { + ImVec2 size(400, 300); + ImGuiIO& io = ImGui::GetIO(); + + ImVec2 window_pos = ImVec2((io.DisplaySize.x - size.x)/2, (io.DisplaySize.y - size.y)/2); + ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always); + + ImGui::SetNextWindowSize(size, ImGuiCond_Appearing); + if (!ImGui::Begin(title, p_open, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDecoration)) { + ImGui::End(); + return; + } + + ImGui::SetCursorPosY(ImGui::GetCursorPosY()-50); + ImGui::SetCursorPosX((ImGui::GetWindowWidth()-256)/2); + + ImTextureID id = (ImTextureID)render_to_fbo(logo_fbo); + float t_w = 256.0; + float t_h = 256.0; + float x_off = 0; + ImGui::Image(id, + ImVec2(t_w-x_off, t_h), + ImVec2(x_off/t_w, t_h/t_h), + ImVec2(t_w/t_w, 0)); + render_logo(0x42e335ff, 0x42e335ff, 0x00000000); + render_to_default_fb(); + ImGui::SetCursorPosX(10); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY()-75); + + const char *msg = "To get started, please configure machine settings."; + ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(msg).x)/2); + ImGui::Text("%s", msg); + + ImGui::Dummy(ImVec2(0,20)); + ImGui::SetCursorPosX((ImGui::GetWindowWidth()-120)/2); + if (ImGui::Button("Settings", ImVec2(120, 0))) { + show_settings_window = true; + } + ImGui::Dummy(ImVec2(0,20)); + + ImGui::SetCursorPosX(10); + + msg = "Visit https://xemu.app for more information"; + ImGui::SetCursorPosY(ImGui::GetWindowHeight()-ImGui::CalcTextSize(msg).y-10); + ImGui::SetCursorPosX((ImGui::GetWindowWidth()-ImGui::CalcTextSize(msg).x)/2); + ImGui::Text("%s", msg); + + ImGui::End(); + } +}; + +static void ShowFirstBootWindow(bool* p_open) +{ + static FirstBootWindow console; + console.Draw("First Boot", p_open); +} diff --git a/ui/xemu-hud.h b/ui/xemu-hud.h new file mode 100644 index 0000000000..3d14e8668d --- /dev/null +++ b/ui/xemu-hud.h @@ -0,0 +1,49 @@ +/* + * xemu User Interface + * + * Subsystem handling primary graphical user interface, which can be controlled + * via mouse and keyboard or through any attached gamepad. + * + * Copyright (C) 2020 Matt Borgerson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef XEMU_HUD_H +#define XEMU_HUD_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Implemented in xemu.c +extern int scaling_mode; +int xemu_is_fullscreen(void); +void xemu_monitor_init(void); +void xemu_toggle_fullscreen(void); + +// Implemented in xemu_hud.cc +void xemu_hud_init(SDL_Window *window, void *sdl_gl_context); +void xemu_hud_cleanup(void); +void xemu_hud_render(SDL_Window *window); +void xemu_hud_process_sdl_events(SDL_Event *event); +void xemu_hud_should_capture_kbd_mouse(int *kbd, int *mouse); + +#ifdef __cplusplus +} +#endif + +#endif