// // xemu User Interface // // Copyright (C) 2020-2022 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 "common.hh" #include "scene-manager.hh" #include "widgets.hh" #include "main-menu.hh" #include "font-manager.hh" #include "input-manager.hh" #include "snapshot-manager.hh" #include "viewport-manager.hh" #include "xemu-hud.h" #include "misc.hh" #include "gl-helpers.hh" #include "reporting.hh" #include "qapi/error.h" #include "actions.hh" #include "../xemu-input.h" #include "../xemu-notifications.h" #include "../xemu-settings.h" #include "../xemu-monitor.h" #include "../xemu-version.h" #include "../xemu-net.h" #include "../xemu-os-utils.h" #include "../xemu-xbe.h" #include "../thirdparty/fatx/fatx.h" #define DEFAULT_XMU_SIZE 8388608 MainMenuScene g_main_menu; MainMenuTabView::~MainMenuTabView() {} void MainMenuTabView::Draw() {} void MainMenuGeneralView::Draw() { #if defined(_WIN32) SectionTitle("Updates"); Toggle("Check for updates", &g_config.general.updates.check, "Check for updates whenever xemu is opened"); #endif #if defined(__x86_64__) SectionTitle("Performance"); Toggle("Hard FPU emulation", &g_config.perf.hard_fpu, "Use hardware-accelerated floating point emulation (requires restart)"); #endif Toggle("Cache shaders to disk", &g_config.perf.cache_shaders, "Reduce stutter in games by caching previously generated shaders"); SectionTitle("Miscellaneous"); Toggle("Skip startup animation", &g_config.general.skip_boot_anim, "Skip the full Xbox boot animation sequence"); FilePicker("Screenshot output directory", &g_config.general.screenshot_dir, NULL, true); // toggle("Throttle DVD/HDD speeds", &g_config.general.throttle_io, // "Limit DVD/HDD throughput to approximate Xbox load times"); } void MainMenuInputView::Draw() { SectionTitle("Controllers"); ImGui::PushFont(g_font_mgr.m_menu_font_small); 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; // Dimensions of XMU float xmu_x = 0, xmu_x_stride = 256, xmu_y = 0; float xmu_w = 256, xmu_h = 256; // Setup rendering to fbo for controller and port images controller_fbo->Target(); ImTextureID id = (ImTextureID)(intptr_t)controller_fbo->Texture(); // // 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_active(0.50, 0.86, 0.54, 0.12); ImVec4 color_inactive(0, 0, 0, 0); // Begin a 4-column layout to render the ports ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, g_viewport_mgr.Scale(ImVec2(0, 12))); ImGui::Columns(4, "mixed", false); const int port_padding = 8; for (int i = 0; i < 4; i++) { bool is_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 * g_viewport_mgr.m_scale - 2 * port_padding * g_viewport_mgr.m_scale) / 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_selected ? color_active : color_inactive); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, g_viewport_mgr.Scale(ImVec2(port_padding, port_padding))); bool activated = ImGui::ImageButton( "port_image_button", id, ImVec2(b_w * g_viewport_mgr.m_scale, b_h * g_viewport_mgr.m_scale), ImVec2(x / t_w, (b_y + b_h) / t_h), ImVec2((x + b_w) / t_w, b_y / t_h)); ImGui::PopStyleVar(); ImGui::PopStyleColor(); if (activated) { active = i; } uint32_t port_color = 0xafafafff; bool is_hovered = ImGui::IsItemHovered(); if (is_hovered) { port_color = 0xffffffff; } else if (is_selected || port_is_bound) { port_color = 0x81dc8a00; } RenderControllerPort(x, b_y, i, port_color); ImGui::PopID(); ImGui::NextColumn(); } ImGui::PopStyleVar(); // ItemSpacing ImGui::Columns(1); // // Render device driver combo // // List available device drivers const char *driver = bound_drivers[active]; if (strcmp(driver, DRIVER_DUKE) == 0) driver = DRIVER_DUKE_DISPLAY_NAME; else if (strcmp(driver, DRIVER_S) == 0) driver = DRIVER_S_DISPLAY_NAME; ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::BeginCombo("###InputDrivers", driver, ImGuiComboFlags_NoArrowButton)) { const char *available_drivers[] = { DRIVER_DUKE, DRIVER_S }; const char *driver_display_names[] = { DRIVER_DUKE_DISPLAY_NAME, DRIVER_S_DISPLAY_NAME }; bool is_selected = false; int num_drivers = sizeof(driver_display_names) / sizeof(driver_display_names[0]); for (int i = 0; i < num_drivers; i++) { const char *iter = driver_display_names[i]; is_selected = strcmp(driver, iter) == 0; ImGui::PushID(iter); if (ImGui::Selectable(iter, is_selected)) { for (int j = 0; j < num_drivers; j++) { if (iter == driver_display_names[j]) bound_drivers[active] = available_drivers[j]; } xemu_input_bind(active, bound_controllers[active], 1); } if (is_selected) { ImGui::SetItemDefaultFocus(); } ImGui::PopID(); } ImGui::EndCombo(); } DrawComboChevron(); // // Render input device combo // // List available input devices const char *not_connected = "Not Connected"; ControllerState *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; } ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::BeginCombo("###InputDevices", name, ImGuiComboFlags_NoArrowButton)) { // 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 ControllerState *iter; QTAILQ_FOREACH(iter, &available_controllers, entry) { 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); // FIXME: We want to bind the XMU here, but we can't because we // just unbound it and we need to wait for Qemu to release the // file // If we previously had no controller connected, we can rebind // the XMU if (bound_state == NULL) xemu_input_rebind_xmu(active); bound_state = iter; } if (is_selected) { ImGui::SetItemDefaultFocus(); } ImGui::PopID(); } ImGui::EndCombo(); } DrawComboChevron(); ImGui::Columns(1); // // Add a separator between input selection and controller graphic // ImGui::Dummy(ImVec2(0.0f, ImGui::GetStyle().WindowPadding.y / 2)); // // Render controller image // bool device_selected = false; if (bound_state) { device_selected = true; RenderController(0, 0, 0x81dc8a00, 0x0f0f0f00, bound_state); } else { static ControllerState state = { 0 }; RenderController(0, 0, 0x1f1f1f00, 0x0f0f0f00, &state); } ImVec2 cur = ImGui::GetCursorPos(); ImVec2 controller_display_size; if (ImGui::GetContentRegionMax().x < controller_width*g_viewport_mgr.m_scale) { controller_display_size.x = ImGui::GetContentRegionMax().x; controller_display_size.y = controller_display_size.x * controller_height / controller_width; } else { controller_display_size = ImVec2(controller_width * g_viewport_mgr.m_scale, controller_height * g_viewport_mgr.m_scale); } ImGui::SetCursorPosX( ImGui::GetCursorPosX() + (int)((ImGui::GetColumnWidth() - controller_display_size.x) / 2.0)); ImGui::Image(id, controller_display_size, ImVec2(0, controller_height/t_h), ImVec2(controller_width/t_w, 0)); ImVec2 pos = ImGui::GetCursorPos(); if (!device_selected) { const char *msg = "Please select an available input device"; ImVec2 dim = ImGui::CalcTextSize(msg); ImGui::SetCursorPosX(cur.x + (controller_display_size.x-dim.x)/2); ImGui::SetCursorPosY(cur.y + (controller_display_size.y-dim.y)/2); ImGui::Text("%s", msg); } controller_fbo->Restore(); ImGui::PopFont(); ImGui::SetCursorPos(pos); if (bound_state) { SectionTitle("Expansion Slots"); // Begin a 2-column layout to render the expansion slots ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, g_viewport_mgr.Scale(ImVec2(0, 12))); ImGui::Columns(2, "mixed", false); xmu_fbo->Target(); id = (ImTextureID)(intptr_t)xmu_fbo->Texture(); const char *img_file_filters = ".img Files\0*.img\0All Files\0*.*\0"; const char *comboLabels[2] = { "###ExpansionSlotA", "###ExpansionSlotB" }; for (int i = 0; i < 2; i++) { // Display a combo box to allow the user to choose the type of // peripheral they want to use enum peripheral_type selected_type = bound_state->peripheral_types[i]; const char *peripheral_type_names[2] = { "None", "Memory Unit" }; const char *selected_peripheral_type = peripheral_type_names[selected_type]; ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::BeginCombo(comboLabels[i], selected_peripheral_type, ImGuiComboFlags_NoArrowButton)) { // Handle all available peripheral types for (int j = 0; j < 2; j++) { bool is_selected = selected_type == j; ImGui::PushID(j); const char *selectable_label = peripheral_type_names[j]; if (ImGui::Selectable(selectable_label, is_selected)) { // Free any existing peripheral if (bound_state->peripherals[i] != NULL) { if (bound_state->peripheral_types[i] == PERIPHERAL_XMU) { // Another peripheral was already bound. // Unplugging xemu_input_unbind_xmu(active, i); } // Free the existing state g_free((void *)bound_state->peripherals[i]); bound_state->peripherals[i] = NULL; } // Change the peripheral type to the newly selected type bound_state->peripheral_types[i] = (enum peripheral_type)j; // Allocate state for the new peripheral if (j == PERIPHERAL_XMU) { bound_state->peripherals[i] = g_malloc(sizeof(XmuState)); memset(bound_state->peripherals[i], 0, sizeof(XmuState)); } xemu_save_peripheral_settings( active, i, bound_state->peripheral_types[i], NULL); } if (is_selected) { ImGui::SetItemDefaultFocus(); } ImGui::PopID(); } ImGui::EndCombo(); } DrawComboChevron(); // Set an X offset to center the image button within the column ImGui::SetCursorPosX( ImGui::GetCursorPosX() + (int)((ImGui::GetColumnWidth() - xmu_w * g_viewport_mgr.m_scale - 2 * port_padding * g_viewport_mgr.m_scale) / 2)); selected_type = bound_state->peripheral_types[i]; if (selected_type == PERIPHERAL_XMU) { float x = xmu_x + i * xmu_x_stride; float y = xmu_y; XmuState *xmu = (XmuState *)bound_state->peripherals[i]; if (xmu->filename != NULL && strlen(xmu->filename) > 0) { RenderXmu(x, y, 0x81dc8a00, 0x0f0f0f00); } else { RenderXmu(x, y, 0x1f1f1f00, 0x0f0f0f00); } ImVec2 xmu_display_size; if (ImGui::GetContentRegionMax().x < xmu_h * g_viewport_mgr.m_scale) { xmu_display_size.x = ImGui::GetContentRegionMax().x / 2; xmu_display_size.y = xmu_display_size.x * xmu_h / xmu_w; } else { xmu_display_size = ImVec2(xmu_w * g_viewport_mgr.m_scale, xmu_h * g_viewport_mgr.m_scale); } ImGui::SetCursorPosX( ImGui::GetCursorPosX() + (int)((ImGui::GetColumnWidth() - xmu_display_size.x) / 2.0)); ImGui::Image(id, xmu_display_size, ImVec2(0.5f * i, 1), ImVec2(0.5f * (i + 1), 0)); // Button to generate a new XMU ImGui::PushID(i); if (ImGui::Button("New Image", ImVec2(250, 0))) { int flags = NOC_FILE_DIALOG_SAVE | NOC_FILE_DIALOG_OVERWRITE_CONFIRMATION; const char *new_path = PausedFileOpen( flags, img_file_filters, NULL, "xmu.img"); if (new_path) { if (create_fatx_image(new_path, DEFAULT_XMU_SIZE)) { // XMU was created successfully. Bind it xemu_input_bind_xmu(active, i, new_path, false); } else { // Show alert message char *msg = g_strdup_printf( "Unable to create XMU image at %s", new_path); xemu_queue_error_message(msg); g_free(msg); } } } const char *xmu_port_path = NULL; if (xmu->filename == NULL) xmu_port_path = g_strdup(""); else xmu_port_path = g_strdup(xmu->filename); if (FilePicker("Image", &xmu_port_path, img_file_filters)) { if (strlen(xmu_port_path) == 0) { xemu_input_unbind_xmu(active, i); } else { xemu_input_bind_xmu(active, i, xmu_port_path, false); } } g_free((void *)xmu_port_path); ImGui::PopID(); } ImGui::NextColumn(); } xmu_fbo->Restore(); ImGui::PopStyleVar(); // ItemSpacing ImGui::Columns(1); } SectionTitle("Options"); Toggle("Auto-bind controllers", &g_config.input.auto_bind, "Bind newly connected controllers to any open port"); Toggle("Background controller input capture", &g_config.input.background_input_capture, "Capture even if window is unfocused (requires restart)"); } void MainMenuDisplayView::Draw() { SectionTitle("Renderer"); ChevronCombo("Backend", &g_config.display.renderer, "Null\0" "OpenGL\0" #ifdef CONFIG_VULKAN "Vulkan\0" #endif , "Select desired renderer implementation"); int rendering_scale = nv2a_get_surface_scale_factor() - 1; if (ChevronCombo("Internal resolution scale", &rendering_scale, "1x\0" "2x\0" "3x\0" "4x\0" "5x\0" "6x\0" "7x\0" "8x\0" "9x\0" "10x\0", "Increase surface scaling factor for higher quality")) { nv2a_set_surface_scale_factor(rendering_scale+1); } SectionTitle("Window"); bool fs = xemu_is_fullscreen(); if (Toggle("Fullscreen", &fs, "Enable fullscreen now")) { xemu_toggle_fullscreen(); } Toggle("Fullscreen on startup", &g_config.display.window.fullscreen_on_startup, "Start xemu in fullscreen when opened"); if (ChevronCombo("Window size", &g_config.display.window.startup_size, "Last Used\0" "640x480\0" "720x480\0" "1280x720\0" "1280x800\0" "1280x960\0" "1920x1080\0" "2560x1440\0" "2560x1600\0" "2560x1920\0" "3840x2160\0", "Select preferred startup window size")) { } Toggle("Vertical refresh sync", &g_config.display.window.vsync, "Sync to screen vertical refresh to reduce tearing artifacts"); SectionTitle("Interface"); Toggle("Show main menu bar", &g_config.display.ui.show_menubar, "Show main menu bar when mouse is activated"); Toggle("Show notifications", &g_config.display.ui.show_notifications, "Display notifications in upper-right corner"); Toggle("Hide mouse cursor", &g_config.display.ui.hide_cursor, "Hide the mouse cursor when it is not moving"); int ui_scale_idx; if (g_config.display.ui.auto_scale) { ui_scale_idx = 0; } else { ui_scale_idx = g_config.display.ui.scale; if (ui_scale_idx < 0) ui_scale_idx = 0; else if (ui_scale_idx > 2) ui_scale_idx = 2; } if (ChevronCombo("UI scale", &ui_scale_idx, "Auto\0" "1x\0" "2x\0", "Interface element scale")) { if (ui_scale_idx == 0) { g_config.display.ui.auto_scale = true; } else { g_config.display.ui.auto_scale = false; g_config.display.ui.scale = ui_scale_idx; } } Toggle("Animations", &g_config.display.ui.use_animations, "Enable xemu user interface animations"); ChevronCombo("Display mode", &g_config.display.ui.fit, "Center\0" "Scale\0" "Stretch\0", "Select how the framebuffer should fit or scale into the window"); ChevronCombo("Aspect ratio", &g_config.display.ui.aspect_ratio, "Native\0" "Auto (Default)\0" "4:3\0" "16:9\0", "Select the displayed aspect ratio"); } void MainMenuAudioView::Draw() { SectionTitle("Volume"); char buf[32]; snprintf(buf, sizeof(buf), "Limit output volume (%d%%)", (int)(g_config.audio.volume_limit * 100)); Slider("Output volume limit", &g_config.audio.volume_limit, buf); SectionTitle("Quality"); Toggle("Real-time DSP processing", &g_config.audio.use_dsp, "Enable improved audio accuracy (experimental)"); } NetworkInterface::NetworkInterface(pcap_if_t *pcap_desc, char *_friendlyname) { m_pcap_name = pcap_desc->name; m_description = pcap_desc->description ?: pcap_desc->name; if (_friendlyname) { char *tmp = g_strdup_printf("%s (%s)", _friendlyname, m_description.c_str()); m_friendly_name = tmp; g_free((gpointer)tmp); } else { m_friendly_name = m_description; } } NetworkInterfaceManager::NetworkInterfaceManager() { m_current_iface = NULL; m_failed_to_load_lib = false; } void NetworkInterfaceManager::Refresh(void) { pcap_if_t *alldevs, *iter; char err[PCAP_ERRBUF_SIZE]; if (xemu_net_is_enabled()) { return; } #if defined(_WIN32) if (pcap_load_library()) { m_failed_to_load_lib = true; return; } #endif m_ifaces.clear(); m_current_iface = NULL; if (pcap_findalldevs(&alldevs, err)) { return; } for (iter=alldevs; iter != NULL; iter=iter->next) { #if defined(_WIN32) char *friendly_name = get_windows_interface_friendly_name(iter->name); m_ifaces.emplace_back(new NetworkInterface(iter, friendly_name)); if (friendly_name) { g_free((gpointer)friendly_name); } #else m_ifaces.emplace_back(new NetworkInterface(iter)); #endif if (!strcmp(g_config.net.pcap.netif, iter->name)) { m_current_iface = m_ifaces.back().get(); } } pcap_freealldevs(alldevs); } void NetworkInterfaceManager::Select(NetworkInterface &iface) { m_current_iface = &iface; xemu_settings_set_string(&g_config.net.pcap.netif, iface.m_pcap_name.c_str()); } bool NetworkInterfaceManager::IsCurrent(NetworkInterface &iface) { return &iface == m_current_iface; } MainMenuNetworkView::MainMenuNetworkView() { should_refresh = true; } void MainMenuNetworkView::Draw() { SectionTitle("Adapter"); bool enabled = xemu_net_is_enabled(); g_config.net.enable = enabled; if (Toggle("Enable", &g_config.net.enable, enabled ? "Virtual network connected (disable to change network " "settings)" : "Connect virtual network cable to machine")) { if (enabled) { xemu_net_disable(); } else { xemu_net_enable(); } } bool appearing = ImGui::IsWindowAppearing(); if (enabled) ImGui::BeginDisabled(); if (ChevronCombo( "Attached to", &g_config.net.backend, "NAT\0" "UDP Tunnel\0" "Bridged Adapter\0", "Controls what the virtual network controller interfaces with")) { appearing = true; } SectionTitle("Options"); switch (g_config.net.backend) { case CONFIG_NET_BACKEND_PCAP: DrawPcapOptions(appearing); break; case CONFIG_NET_BACKEND_NAT: DrawNatOptions(appearing); break; case CONFIG_NET_BACKEND_UDP: DrawUdpOptions(appearing); break; default: break; } if (enabled) ImGui::EndDisabled(); } void MainMenuNetworkView::DrawPcapOptions(bool appearing) { if (iface_mgr.get() == nullptr) { iface_mgr.reset(new NetworkInterfaceManager()); iface_mgr->Refresh(); } if (iface_mgr->m_failed_to_load_lib) { #if defined(_WIN32) const char *msg = "npcap library could not be loaded.\n" "To use this backend, please install npcap."; ImGui::Text("%s", msg); ImGui::Dummy(ImVec2(0,10*g_viewport_mgr.m_scale)); ImGui::SetCursorPosX((ImGui::GetWindowWidth()-120*g_viewport_mgr.m_scale)/2); if (ImGui::Button("Install npcap", ImVec2(120*g_viewport_mgr.m_scale, 0))) { xemu_open_web_browser("https://nmap.org/npcap/"); } #endif } else { const char *selected_display_name = (iface_mgr->m_current_iface ? iface_mgr->m_current_iface->m_friendly_name.c_str() : g_config.net.pcap.netif); float combo_width = ImGui::GetColumnWidth(); float combo_size_ratio = 0.5; combo_width *= combo_size_ratio; PrepareComboTitleDescription("Network interface", "Host network interface to bridge with", combo_size_ratio); ImGui::SetNextItemWidth(combo_width); ImGui::PushFont(g_font_mgr.m_menu_font_small); if (ImGui::BeginCombo("###network_iface", selected_display_name, ImGuiComboFlags_NoArrowButton)) { if (should_refresh) { iface_mgr->Refresh(); should_refresh = false; } int i = 0; for (auto &iface : iface_mgr->m_ifaces) { bool is_selected = iface_mgr->IsCurrent((*iface)); ImGui::PushID(i++); if (ImGui::Selectable(iface->m_friendly_name.c_str(), is_selected)) { iface_mgr->Select((*iface)); } if (is_selected) ImGui::SetItemDefaultFocus(); ImGui::PopID(); } ImGui::EndCombo(); } else { should_refresh = true; } ImGui::PopFont(); DrawComboChevron(); } } void MainMenuNetworkView::DrawNatOptions(bool appearing) { static ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg; WidgetTitleDescriptionItem( "Port Forwarding", "Configure xemu to forward connections to guest on these ports"); float p = ImGui::GetFrameHeight() * 0.3; ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(p, p)); if (ImGui::BeginTable("port_forward_tbl", 4, flags)) { ImGui::TableSetupColumn("Host Port"); ImGui::TableSetupColumn("Guest Port"); ImGui::TableSetupColumn("Protocol"); ImGui::TableSetupColumn("Action"); ImGui::TableHeadersRow(); for (unsigned int row = 0; row < g_config.net.nat.forward_ports_count; row++) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Text("%d", g_config.net.nat.forward_ports[row].host); ImGui::TableSetColumnIndex(1); ImGui::Text("%d", g_config.net.nat.forward_ports[row].guest); ImGui::TableSetColumnIndex(2); switch (g_config.net.nat.forward_ports[row].protocol) { case CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL_TCP: ImGui::TextUnformatted("TCP"); break; case CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL_UDP: ImGui::TextUnformatted("UDP"); break; default: assert(0); } ImGui::TableSetColumnIndex(3); ImGui::PushID(row); if (ImGui::Button("Remove")) { remove_net_nat_forward_ports(row); } ImGui::PopID(); } ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); static char buf[8] = {"1234"}; ImGui::SetNextItemWidth(ImGui::GetColumnWidth()); ImGui::InputText("###hostport", buf, sizeof(buf)); ImGui::TableSetColumnIndex(1); static char buf2[8] = {"1234"}; ImGui::SetNextItemWidth(ImGui::GetColumnWidth()); ImGui::InputText("###guestport", buf2, sizeof(buf2)); ImGui::TableSetColumnIndex(2); static CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL protocol = CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL_TCP; assert(sizeof(protocol) >= sizeof(int)); ImGui::SetNextItemWidth(ImGui::GetColumnWidth()); ImGui::Combo("###protocol", &protocol, "TCP\0UDP\0"); ImGui::TableSetColumnIndex(3); if (ImGui::Button("Add")) { int host, guest; if (sscanf(buf, "%d", &host) == 1 && sscanf(buf2, "%d", &guest) == 1) { add_net_nat_forward_ports(host, guest, protocol); } } ImGui::EndTable(); } ImGui::PopStyleVar(); } void MainMenuNetworkView::DrawUdpOptions(bool appearing) { if (appearing) { strncpy(remote_addr, g_config.net.udp.remote_addr, sizeof(remote_addr) - 1); strncpy(local_addr, g_config.net.udp.bind_addr, sizeof(local_addr) - 1); } float size_ratio = 0.5; float width = ImGui::GetColumnWidth() * size_ratio; ImGui::PushFont(g_font_mgr.m_menu_font_small); PrepareComboTitleDescription( "Remote Address", "Destination addr:port to forward packets to (1.2.3.4:9968)", size_ratio); ImGui::SetNextItemWidth(width); if (ImGui::InputText("###remote_host", remote_addr, sizeof(remote_addr))) { xemu_settings_set_string(&g_config.net.udp.remote_addr, remote_addr); } PrepareComboTitleDescription( "Bind Address", "Local addr:port to receive packets on (0.0.0.0:9968)", size_ratio); ImGui::SetNextItemWidth(width); if (ImGui::InputText("###local_host", local_addr, sizeof(local_addr))) { xemu_settings_set_string(&g_config.net.udp.bind_addr, local_addr); } ImGui::PopFont(); } MainMenuSnapshotsView::MainMenuSnapshotsView() : MainMenuTabView() { xemu_snapshots_mark_dirty(); m_search_regex = NULL; m_current_title_id = 0; } MainMenuSnapshotsView::~MainMenuSnapshotsView() { g_free(m_search_regex); } bool MainMenuSnapshotsView::BigSnapshotButton(QEMUSnapshotInfo *snapshot, XemuSnapshotData *data, int current_snapshot_binding) { ImGuiStyle &style = ImGui::GetStyle(); ImDrawList *draw_list = ImGui::GetWindowDrawList(); ImGui::PushFont(g_font_mgr.m_menu_font_small); ImVec2 ts_sub = ImGui::CalcTextSize(snapshot->name); ImGui::PopFont(); ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, g_viewport_mgr.Scale(ImVec2(5, 5))); ImGui::PushFont(g_font_mgr.m_menu_font_medium); ImVec2 ts_title = ImGui::CalcTextSize(snapshot->name); ImVec2 thumbnail_size = g_viewport_mgr.Scale( ImVec2(XEMU_SNAPSHOT_THUMBNAIL_WIDTH, XEMU_SNAPSHOT_THUMBNAIL_HEIGHT)); ImVec2 thumbnail_pos(style.FramePadding.x, style.FramePadding.y); ImVec2 name_pos(thumbnail_pos.x + thumbnail_size.x + style.FramePadding.x * 2, thumbnail_pos.y); ImVec2 title_pos(name_pos.x, name_pos.y + ts_title.y + style.FramePadding.x); ImVec2 date_pos(name_pos.x, title_pos.y + ts_title.y + style.FramePadding.x); ImVec2 binding_pos(name_pos.x, date_pos.y + ts_title.y + style.FramePadding.x); ImVec2 button_size(-FLT_MIN, fmax(thumbnail_size.y + style.FramePadding.y * 2, ts_title.y + ts_sub.y + style.FramePadding.y * 3)); bool load = ImGui::Button("###button", button_size); ImGui::PopFont(); const ImVec2 p0 = ImGui::GetItemRectMin(); const ImVec2 p1 = ImGui::GetItemRectMax(); draw_list->PushClipRect(p0, p1, true); // Snapshot thumbnail GLuint thumbnail = data->gl_thumbnail ? data->gl_thumbnail : g_icon_tex; int thumbnail_width, thumbnail_height; glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, thumbnail); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &thumbnail_width); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &thumbnail_height); // Draw black background behind thumbnail ImVec2 thumbnail_min(p0.x + thumbnail_pos.x, p0.y + thumbnail_pos.y); ImVec2 thumbnail_max(thumbnail_min.x + thumbnail_size.x, thumbnail_min.y + thumbnail_size.y); draw_list->AddRectFilled(thumbnail_min, thumbnail_max, IM_COL32_BLACK); // Draw centered thumbnail image int scaled_width, scaled_height; ScaleDimensions(thumbnail_width, thumbnail_height, thumbnail_size.x, thumbnail_size.y, &scaled_width, &scaled_height); ImVec2 img_min = ImVec2(thumbnail_min.x + (thumbnail_size.x - scaled_width) / 2, thumbnail_min.y + (thumbnail_size.y - scaled_height) / 2); ImVec2 img_max = ImVec2(img_min.x + scaled_width, img_min.y + scaled_height); draw_list->AddImage((ImTextureID)(uint64_t)thumbnail, img_min, img_max); // Snapshot title ImGui::PushFont(g_font_mgr.m_menu_font_medium); draw_list->AddText(ImVec2(p0.x + name_pos.x, p0.y + name_pos.y), IM_COL32(255, 255, 255, 255), snapshot->name); ImGui::PopFont(); // Snapshot XBE title name ImGui::PushFont(g_font_mgr.m_menu_font_small); const char *title_name = data->xbe_title_name ? data->xbe_title_name : "(Unknown XBE Title Name)"; draw_list->AddText(ImVec2(p0.x + title_pos.x, p0.y + title_pos.y), IM_COL32(255, 255, 255, 200), title_name); // Snapshot date g_autoptr(GDateTime) date = g_date_time_new_from_unix_local(snapshot->date_sec); char *date_buf = g_date_time_format(date, "%Y-%m-%d %H:%M:%S"); draw_list->AddText(ImVec2(p0.x + date_pos.x, p0.y + date_pos.y), IM_COL32(255, 255, 255, 200), date_buf); g_free(date_buf); // Snapshot keyboard binding if (current_snapshot_binding != -1) { char *binding_text = g_strdup_printf("Bound to F%d", current_snapshot_binding + 5); draw_list->AddText(ImVec2(p0.x + binding_pos.x, p0.y + binding_pos.y), IM_COL32(255, 255, 255, 200), binding_text); g_free(binding_text); } ImGui::PopFont(); draw_list->PopClipRect(); ImGui::PopStyleVar(2); return load; } void MainMenuSnapshotsView::ClearSearch() { m_search_buf.clear(); if (m_search_regex) { g_free(m_search_regex); m_search_regex = NULL; } } int MainMenuSnapshotsView::OnSearchTextUpdate(ImGuiInputTextCallbackData *data) { GError *gerr = NULL; MainMenuSnapshotsView *win = (MainMenuSnapshotsView *)data->UserData; if (win->m_search_regex) { g_free(win->m_search_regex); win->m_search_regex = NULL; } if (data->BufTextLen == 0) { return 0; } char *buf = g_strdup_printf("(.*)%s(.*)", data->Buf); win->m_search_regex = g_regex_new(buf, (GRegexCompileFlags)0, (GRegexMatchFlags)0, &gerr); g_free(buf); if (gerr) { win->m_search_regex = NULL; return 1; } return 0; } void MainMenuSnapshotsView::Draw() { g_snapshot_mgr.Refresh(); SectionTitle("Snapshots"); Toggle("Filter by current title", &g_config.general.snapshots.filter_current_game, "Only display snapshots created while running the currently running " "XBE"); if (g_config.general.snapshots.filter_current_game) { struct xbe *xbe = xemu_get_xbe_info(); if (xbe && xbe->cert) { if (xbe->cert->m_titleid != m_current_title_id) { char *title_name = g_utf16_to_utf8(xbe->cert->m_title_name, 40, NULL, NULL, NULL); if (title_name) { m_current_title_name = title_name; g_free(title_name); } else { m_current_title_name.clear(); } m_current_title_id = xbe->cert->m_titleid; } } else { m_current_title_name.clear(); m_current_title_id = 0; } } ImGui::SetNextItemWidth(ImGui::GetColumnWidth() * 0.8); ImGui::PushFont(g_font_mgr.m_menu_font_small); ImGui::InputTextWithHint("##search", "Search or name new snapshot...", &m_search_buf, ImGuiInputTextFlags_CallbackEdit, &OnSearchTextUpdate, this); bool snapshot_with_create_name_exists = false; for (int i = 0; i < g_snapshot_mgr.m_snapshots_len; ++i) { if (g_strcmp0(m_search_buf.c_str(), g_snapshot_mgr.m_snapshots[i].name) == 0) { snapshot_with_create_name_exists = true; break; } } ImGui::SameLine(); if (snapshot_with_create_name_exists) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8, 0, 0, 1)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1, 0, 0, 1)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1, 0, 0, 1)); } if (ImGui::Button(snapshot_with_create_name_exists ? "Replace" : "Create", ImVec2(-FLT_MIN, 0))) { xemu_snapshots_save(m_search_buf.empty() ? NULL : m_search_buf.c_str(), NULL); ClearSearch(); } if (snapshot_with_create_name_exists) { ImGui::PopStyleColor(3); } if (snapshot_with_create_name_exists && ImGui::IsItemHovered()) { ImGui::SetTooltip("A snapshot with the name \"%s\" already exists. " "This button will overwrite the existing snapshot.", m_search_buf.c_str()); } ImGui::PopFont(); bool at_least_one_snapshot_displayed = false; for (int i = g_snapshot_mgr.m_snapshots_len - 1; i >= 0; i--) { if (g_config.general.snapshots.filter_current_game && g_snapshot_mgr.m_extra_data[i].xbe_title_name && m_current_title_name.size() && strcmp(m_current_title_name.c_str(), g_snapshot_mgr.m_extra_data[i].xbe_title_name)) { continue; } if (m_search_regex) { GMatchInfo *match; bool keep_entry = false; g_regex_match(m_search_regex, g_snapshot_mgr.m_snapshots[i].name, (GRegexMatchFlags)0, &match); keep_entry |= g_match_info_matches(match); g_match_info_free(match); if (g_snapshot_mgr.m_extra_data[i].xbe_title_name) { g_regex_match(m_search_regex, g_snapshot_mgr.m_extra_data[i].xbe_title_name, (GRegexMatchFlags)0, &match); keep_entry |= g_match_info_matches(match); g_free(match); } if (!keep_entry) { continue; } } QEMUSnapshotInfo *snapshot = &g_snapshot_mgr.m_snapshots[i]; XemuSnapshotData *data = &g_snapshot_mgr.m_extra_data[i]; int current_snapshot_binding = -1; for (int j = 0; j < 4; ++j) { if (g_strcmp0(*(g_snapshot_shortcut_index_key_map[j]), snapshot->name) == 0) { assert(current_snapshot_binding == -1); current_snapshot_binding = j; } } ImGui::PushID(i); ImVec2 pos = ImGui::GetCursorScreenPos(); bool load = BigSnapshotButton(snapshot, data, current_snapshot_binding); // FIXME: Provide context menu control annotation if (ImGui::IsItemHovered() && ImGui::IsKeyPressed(ImGuiKey_GamepadFaceLeft)) { ImGui::SetNextWindowPos(pos); ImGui::OpenPopup("Snapshot Options"); } DrawSnapshotContextMenu(snapshot, data, current_snapshot_binding); ImGui::PopID(); if (load) { ActionLoadSnapshotChecked(snapshot->name); } at_least_one_snapshot_displayed = true; } if (!at_least_one_snapshot_displayed) { ImGui::Dummy(g_viewport_mgr.Scale(ImVec2(0, 16))); const char *msg; if (g_snapshot_mgr.m_snapshots_len) { if (!m_search_buf.empty()) { msg = "Press Create to create new snapshot"; } else { msg = "No snapshots match filter criteria"; } } else { msg = "No snapshots to display"; } ImVec2 dim = ImGui::CalcTextSize(msg); ImVec2 cur = ImGui::GetCursorPos(); ImGui::SetCursorPosX(cur.x + (ImGui::GetColumnWidth() - dim.x) / 2); ImGui::TextColored(ImVec4(0.94f, 0.94f, 0.94f, 0.70f), "%s", msg); } } void MainMenuSnapshotsView::DrawSnapshotContextMenu( QEMUSnapshotInfo *snapshot, XemuSnapshotData *data, int current_snapshot_binding) { if (!ImGui::BeginPopupContextItem("Snapshot Options")) { return; } if (ImGui::MenuItem("Load")) { ActionLoadSnapshotChecked(snapshot->name); } if (ImGui::BeginMenu("Keybinding")) { for (int i = 0; i < 4; ++i) { char *item_name = g_strdup_printf("Bind to F%d", i + 5); if (ImGui::MenuItem(item_name)) { if (current_snapshot_binding >= 0) { xemu_settings_set_string(g_snapshot_shortcut_index_key_map [current_snapshot_binding], ""); } xemu_settings_set_string(g_snapshot_shortcut_index_key_map[i], snapshot->name); current_snapshot_binding = i; ImGui::CloseCurrentPopup(); } g_free(item_name); } if (current_snapshot_binding >= 0) { if (ImGui::MenuItem("Unbind")) { xemu_settings_set_string( g_snapshot_shortcut_index_key_map[current_snapshot_binding], ""); current_snapshot_binding = -1; } } ImGui::EndMenu(); } ImGui::Separator(); Error *err = NULL; if (ImGui::MenuItem("Replace")) { xemu_snapshots_save(snapshot->name, &err); } if (ImGui::MenuItem("Delete")) { xemu_snapshots_delete(snapshot->name, &err); } if (err) { xemu_queue_error_message(error_get_pretty(err)); error_free(err); } ImGui::EndPopup(); } MainMenuSystemView::MainMenuSystemView() : m_dirty(false) { } void MainMenuSystemView::Draw() { const char *rom_file_filters = ".bin Files\0*.bin\0.rom Files\0*.rom\0All Files\0*.*\0"; const char *qcow_file_filters = ".qcow2 Files\0*.qcow2\0All Files\0*.*\0"; if (m_dirty) { ImGui::TextColored(ImVec4(1, 0, 0, 1), "Application restart required to apply settings"); } if ((int)g_config.sys.avpack == CONFIG_SYS_AVPACK_NONE) { ImGui::TextColored(ImVec4(1,0,0,1), "Setting AV Pack to NONE disables video output."); } SectionTitle("System Configuration"); if (ChevronCombo( "System Memory", &g_config.sys.mem_limit, "64 MiB (Default)\0" "128 MiB\0", "Increase to 128 MiB for debug or homebrew applications")) { m_dirty = true; } if (ChevronCombo( "AV Pack", &g_config.sys.avpack, "SCART\0HDTV (Default)\0VGA\0RFU\0S-Video\0Composite\0None\0", "Select the attached AV pack")) { m_dirty = true; } SectionTitle("Files"); if (FilePicker("MCPX Boot ROM", &g_config.sys.files.bootrom_path, rom_file_filters)) { m_dirty = true; g_main_menu.UpdateAboutViewConfigInfo(); } if (FilePicker("Flash ROM (BIOS)", &g_config.sys.files.flashrom_path, rom_file_filters)) { m_dirty = true; g_main_menu.UpdateAboutViewConfigInfo(); } if (FilePicker("Hard Disk", &g_config.sys.files.hdd_path, qcow_file_filters)) { m_dirty = true; } if (FilePicker("EEPROM", &g_config.sys.files.eeprom_path, rom_file_filters)) { m_dirty = true; } } MainMenuAboutView::MainMenuAboutView() : m_config_info_text{ NULL } { } void MainMenuAboutView::UpdateConfigInfoText() { if (m_config_info_text) { g_free(m_config_info_text); } gchar *bootrom_checksum = GetFileMD5Checksum(g_config.sys.files.bootrom_path); if (!bootrom_checksum) { bootrom_checksum = g_strdup("None"); } gchar *flash_rom_checksum = GetFileMD5Checksum(g_config.sys.files.flashrom_path); if (!flash_rom_checksum) { flash_rom_checksum = g_strdup("None"); } m_config_info_text = g_strdup_printf("MCPX Boot ROM MD5 Hash: %s\n" "Flash ROM (BIOS) MD5 Hash: %s", bootrom_checksum, flash_rom_checksum); g_free(bootrom_checksum); g_free(flash_rom_checksum); } void MainMenuAboutView::Draw() { static const char *build_info_text = NULL; if (build_info_text == NULL) { build_info_text = g_strdup_printf("Version: %s\nBranch: %s\nCommit: " "%s\nDate: %s", xemu_version, xemu_branch, xemu_commit, xemu_date); } static const char *sys_info_text = NULL; if (sys_info_text == NULL) { const char *gl_shader_version = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION); const char *gl_version = (const char *)glGetString(GL_VERSION); const char *gl_renderer = (const char *)glGetString(GL_RENDERER); const char *gl_vendor = (const char *)glGetString(GL_VENDOR); sys_info_text = g_strdup_printf( "CPU: %s\nOS Platform: %s\nOS Version: " "%s\nManufacturer: %s\n" "GPU Model: %s\nDriver: %s\nShader: %s", xemu_get_cpu_info(), xemu_get_os_platform(), xemu_get_os_info(), gl_vendor, gl_renderer, gl_version, gl_shader_version); } if (m_config_info_text == NULL) { UpdateConfigInfoText(); } Logo(); SectionTitle("Build Information"); ImGui::PushFont(g_font_mgr.m_fixed_width_font); ImGui::InputTextMultiline("##build_info", (char *)build_info_text, strlen(build_info_text) + 1, ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 5), ImGuiInputTextFlags_ReadOnly); ImGui::PopFont(); SectionTitle("System Information"); ImGui::PushFont(g_font_mgr.m_fixed_width_font); ImGui::InputTextMultiline("###systeminformation", (char *)sys_info_text, strlen(sys_info_text) + 1, ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 8), ImGuiInputTextFlags_ReadOnly); ImGui::PopFont(); SectionTitle("Config Information"); ImGui::PushFont(g_font_mgr.m_fixed_width_font); ImGui::InputTextMultiline("##config_info", (char *)m_config_info_text, strlen(build_info_text) + 1, ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 3), ImGuiInputTextFlags_ReadOnly); ImGui::PopFont(); SectionTitle("Community"); ImGui::Text("Visit"); ImGui::SameLine(); if (ImGui::SmallButton("https://xemu.app")) { xemu_open_web_browser("https://xemu.app"); } ImGui::SameLine(); ImGui::Text("for more information"); } MainMenuTabButton::MainMenuTabButton(std::string text, std::string icon) : m_icon(icon), m_text(text) { } bool MainMenuTabButton::Draw(bool selected) { ImGuiStyle &style = ImGui::GetStyle(); ImU32 col = selected ? ImGui::GetColorU32(style.Colors[ImGuiCol_ButtonHovered]) : IM_COL32(0, 0, 0, 0); ImGui::PushStyleColor(ImGuiCol_Button, col); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, selected ? col : IM_COL32(32, 32, 32, 255)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, selected ? col : IM_COL32(32, 32, 32, 255)); int p = ImGui::GetTextLineHeight() * 0.5; ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(p, p)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0); ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0.5)); ImGui::PushFont(g_font_mgr.m_menu_font); ImVec2 button_size = ImVec2(-FLT_MIN, 0); auto text = string_format("%s %s", m_icon.c_str(), m_text.c_str()); ImGui::PushID(this); bool status = ImGui::Button(text.c_str(), button_size); ImGui::PopID(); ImGui::PopFont(); ImGui::PopStyleVar(3); ImGui::PopStyleColor(3); return status; } MainMenuScene::MainMenuScene() : m_animation(0.12, 0.12), m_general_button("General", ICON_FA_GEARS), m_input_button("Input", ICON_FA_GAMEPAD), m_display_button("Display", ICON_FA_TV), m_audio_button("Audio", ICON_FA_VOLUME_HIGH), m_network_button("Network", ICON_FA_NETWORK_WIRED), m_snapshots_button("Snapshots", ICON_FA_CLOCK_ROTATE_LEFT), m_system_button("System", ICON_FA_MICROCHIP), m_about_button("About", ICON_FA_CIRCLE_INFO) { m_had_focus_last_frame = false; m_focus_view = false; m_tabs.push_back(&m_general_button); m_tabs.push_back(&m_input_button); m_tabs.push_back(&m_display_button); m_tabs.push_back(&m_audio_button); m_tabs.push_back(&m_network_button); m_tabs.push_back(&m_snapshots_button); m_tabs.push_back(&m_system_button); m_tabs.push_back(&m_about_button); m_views.push_back(&m_general_view); m_views.push_back(&m_input_view); m_views.push_back(&m_display_view); m_views.push_back(&m_audio_view); m_views.push_back(&m_network_view); m_views.push_back(&m_snapshots_view); m_views.push_back(&m_system_view); m_views.push_back(&m_about_view); m_current_view_index = 0; m_next_view_index = m_current_view_index; } void MainMenuScene::ShowSettings() { SetNextViewIndexWithFocus(g_config.general.last_viewed_menu_index); } void MainMenuScene::ShowSnapshots() { SetNextViewIndexWithFocus(5); } void MainMenuScene::ShowSystem() { SetNextViewIndexWithFocus(6); } void MainMenuScene::ShowAbout() { SetNextViewIndexWithFocus(7); } void MainMenuScene::SetNextViewIndexWithFocus(int i) { m_focus_view = true; SetNextViewIndex(i); if (!g_scene_mgr.IsDisplayingScene()) { g_scene_mgr.PushScene(*this); } } void MainMenuScene::Show() { m_background.Show(); m_nav_control_view.Show(); m_animation.EaseIn(); } void MainMenuScene::Hide() { m_background.Hide(); m_nav_control_view.Hide(); m_animation.EaseOut(); } bool MainMenuScene::IsAnimating() { return m_animation.IsAnimating(); } void MainMenuScene::SetNextViewIndex(int i) { m_next_view_index = i % m_tabs.size(); g_config.general.last_viewed_menu_index = i; } void MainMenuScene::HandleInput() { bool nofocus = !ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow); bool focus = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows | ImGuiFocusedFlags_NoPopupHierarchy); // XXX: Ensure we have focus for two frames. If a user cancels a popup // window, we do not want to cancel main // window as well. if (nofocus || (focus && m_had_focus_last_frame && (ImGui::IsKeyDown(ImGuiKey_GamepadFaceRight) || ImGui::IsKeyDown(ImGuiKey_Escape)))) { Hide(); return; } if (focus && m_had_focus_last_frame) { if (ImGui::IsKeyPressed(ImGuiKey_GamepadL1)) { SetNextViewIndex((m_current_view_index + m_tabs.size() - 1) % m_tabs.size()); } if (ImGui::IsKeyPressed(ImGuiKey_GamepadR1)) { SetNextViewIndex((m_current_view_index + 1) % m_tabs.size()); } } m_had_focus_last_frame = focus; } void MainMenuScene::UpdateAboutViewConfigInfo() { m_about_view.UpdateConfigInfoText(); } bool MainMenuScene::Draw() { m_animation.Step(); m_background.Draw(); m_nav_control_view.Draw(); ImGuiIO &io = ImGui::GetIO(); float t = m_animation.GetSinInterpolatedValue(); float window_alpha = t; ImGui::PushStyleVar(ImGuiStyleVar_Alpha, window_alpha); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0); ImVec4 extents = g_viewport_mgr.GetExtents(); ImVec2 window_pos = ImVec2(io.DisplaySize.x / 2, extents.y); ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, ImVec2(0.5, 0)); ImVec2 max_size = g_viewport_mgr.Scale(ImVec2(800, 0)); float x = fmin(io.DisplaySize.x - extents.x - extents.z, max_size.x); float y = io.DisplaySize.y - extents.y - extents.w; ImGui::SetNextWindowSize(ImVec2(x, y)); if (ImGui::Begin("###MainWindow", NULL, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings)) { // // Nav menu // float width = ImGui::GetWindowWidth(); float nav_width = width * 0.3; float content_width = width - nav_width; ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(26, 26, 26, 255)); ImGui::BeginChild("###MainWindowNav", ImVec2(nav_width, -1), true, ImGuiWindowFlags_NavFlattened); bool move_focus_to_tab = false; if (m_current_view_index != m_next_view_index) { m_current_view_index = m_next_view_index; if (!m_focus_view) { move_focus_to_tab = true; } } int i = 0; for (auto &button : m_tabs) { if (move_focus_to_tab && i == m_current_view_index) { ImGui::SetKeyboardFocusHere(); move_focus_to_tab = false; } if (button->Draw(i == m_current_view_index)) { SetNextViewIndex(i); } if (i == m_current_view_index) { ImGui::SetItemDefaultFocus(); } i++; } ImGui::EndChild(); ImGui::PopStyleColor(); // // Content // ImGui::SameLine(); int s = ImGui::GetTextLineHeight() * 0.75; ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(s, s)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(s, s)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6 * g_viewport_mgr.m_scale); ImGui::PushID(m_current_view_index); ImGui::BeginChild("###MainWindowContent", ImVec2(content_width, -1), true, ImGuiWindowFlags_AlwaysUseWindowPadding | ImGuiWindowFlags_NavFlattened); if (!g_input_mgr.IsNavigatingWithController()) { // Close button ImGui::PushFont(g_font_mgr.m_menu_font); ImGuiStyle &style = ImGui::GetStyle(); ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 128)); ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS); ImVec2 pos = ImGui::GetCursorPos(); ImGui::SetCursorPosX(ImGui::GetContentRegionMax().x - style.FramePadding.x * 2.0f - ImGui::GetTextLineHeight()); if (ImGui::Button(ICON_FA_XMARK)) { Hide(); } ImGui::SetCursorPos(pos); ImGui::PopStyleColor(2); ImGui::PopFont(); } ImGui::PushFont(g_font_mgr.m_default_font); if (m_focus_view) { ImGui::SetKeyboardFocusHere(); m_focus_view = false; } m_views[m_current_view_index]->Draw(); ImGui::PopFont(); ImGui::EndChild(); ImGui::PopID(); ImGui::PopStyleVar(3); HandleInput(); } ImGui::End(); ImGui::PopStyleVar(5); return !m_animation.IsComplete(); }