xemu/ui/xui/main-menu.cc
Matt Borgerson 9c06980275 ui: Redesign user interface
Introduces a new user interface that looks much nicer, is easier to
navigate with controllers, provides more context to users, and is
scalable. Some additional features are included.

* Adds 'popup menu' with actions that can be used easily from controller
* Adds 'main menu', unifying other configuration dialogs
* Adds port-forwarding user interface
* Adds screenshot feature
* Adds volume control feature
* Adds gamepad auto-bind option
* Adds vsync configuration option
* Adds auto UI scaling
* Adds preferred window size selection
* Adds AV pack selection
* Exposes some existing config items in GUI
2022-05-07 16:09:34 -07:00

1143 lines
38 KiB
C++

//
// 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 <http://www.gnu.org/licenses/>.
//
#include "common.hh"
#include "scene-manager.hh"
#include "widgets.hh"
#include "main-menu.hh"
#include "font-manager.hh"
#include "input-manager.hh"
#include "viewport-manager.hh"
#include "xemu-hud.h"
#include "misc.hh"
#include "gl-helpers.hh"
#include "reporting.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"
MainMenuTabView::~MainMenuTabView() {}
void MainMenuTabView::Draw() {}
void MainMenuGeneralView::Draw()
{
SectionTitle("Updates");
Toggle("Check for updates", &g_config.general.updates.check,
"Check for updates whenever xemu is opened");
SectionTitle("Performance");
Toggle("Hard FPU emulation", &g_config.perf.hard_fpu,
"Use hardware-accelerated floating point emulation (requires restart)");
// 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;
// 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);
bool activated = ImGui::ImageButton(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),
port_padding);
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 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);
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);
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("Quality");
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"
"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");
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"
"Scale (Widescreen 16:9)\0"
"Scale (4:3)\0"
"Stretch\0",
"Select how the framebuffer should fit or scale into the window");
}
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();
}
#if 0
class MainMenuSnapshotsView : public virtual MainMenuTabView
{
protected:
GLuint screenshot;
public:
void initScreenshot()
{
if (screenshot == 0) {
glGenTextures(1, &screenshot);
int w, h, n;
stbi_set_flip_vertically_on_load(0);
unsigned char *data = stbi_load("./data/screenshot.png", &w, &h, &n, 4);
assert(n == 4);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, screenshot);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
stbi_image_free(data);
}
}
void snapshotBigButton(const char *name, const char *title_name, GLuint screenshot)
{
ImGuiStyle &style = ImGui::GetStyle();
ImVec2 pos = ImGui::GetCursorPos();
ImDrawList *draw_list = ImGui::GetWindowDrawList();
ImGui::PushFont(g_font_mgr.m_menuFont);
const char *icon = ICON_FA_CIRCLE_XMARK;
ImVec2 ts_icon = ImGui::CalcTextSize(icon);
ts_icon.x += 2*style.FramePadding.x;
ImGui::PopFont();
ImGui::PushFont(g_font_mgr.m_menuFontSmall);
ImVec2 ts_sub = ImGui::CalcTextSize(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_menuFontMedium);
ImVec2 ts_title = ImGui::CalcTextSize(name);
ImVec2 thumbnail_size = g_viewport_mgr.scale(ImVec2(160, 120));
ImVec2 thumbnail_pos(style.FramePadding.x, style.FramePadding.y);
ImVec2 text_pos(thumbnail_pos.x + thumbnail_size.x + style.FramePadding.x * 2, thumbnail_pos.y);
ImVec2 subtext_pos(text_pos.x, text_pos.y + ts_title.y + style.FramePadding.x);
ImGui::Button("###button", ImVec2(ImGui::GetContentRegionAvail().x, fmax(thumbnail_size.y + style.FramePadding.y * 2,
ts_title.y + ts_sub.y + style.FramePadding.y * 3)));
ImGui::PopFont();
const ImVec2 sz = ImGui::GetItemRectSize();
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
ts_icon.y = sz.y;
// Snapshot thumbnail
ImGui::SetItemAllowOverlap();
ImGui::SameLine();
ImGui::SetCursorPosX(pos.x + thumbnail_pos.x);
ImGui::SetCursorPosY(pos.y + thumbnail_pos.y);
ImGui::Image((ImTextureID)screenshot, thumbnail_size, ImVec2(0,0), ImVec2(1,1));
draw_list->PushClipRect(p0, p1, true);
// Snapshot title
ImGui::PushFont(g_font_mgr.m_menuFontMedium);
draw_list->AddText(ImVec2(p0.x + text_pos.x, p0.y + text_pos.y), IM_COL32(255, 255, 255, 255), name);
ImGui::PopFont();
// Snapshot subtitle
ImGui::PushFont(g_font_mgr.m_menuFontSmall);
draw_list->AddText(ImVec2(p0.x + subtext_pos.x, p0.y + subtext_pos.y), IM_COL32(255, 255, 255, 200), title_name);
ImGui::PopFont();
draw_list->PopClipRect();
// Delete button
ImGui::SameLine();
ImGui::SetCursorPosY(pos.y);
ImGui::SetCursorPosX(pos.x + sz.x - ts_icon.x);
ImGui::PushFont(g_font_mgr.m_menuFont);
ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0));
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS);
ImGui::Button(icon, ts_icon);
ImGui::PopStyleColor();
ImGui::PopStyleVar(1);
ImGui::PopFont();
ImGui::PopStyleVar(2);
}
void Draw()
{
initScreenshot();
for (int i = 0; i < 15; i++) {
char buf[64];
snprintf(buf, sizeof(buf), "%s", "Apr 9 2022 19:44");
ImGui::PushID(i);
snapshotBigButton(buf, "Halo: Combat Evolved", screenshot);
ImGui::PopID();
}
}
};
#endif
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");
}
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("Boot ROM", &g_config.sys.files.bootrom_path,
rom_file_filters)) {
m_dirty = true;
}
if (FilePicker("Flash ROM", &g_config.sys.files.flashrom_path,
rom_file_filters)) {
m_dirty = true;
}
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;
}
}
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);
}
static uint32_t time_start = 0;
if (ImGui::IsWindowAppearing()) {
time_start = SDL_GetTicks();
}
uint32_t now = SDL_GetTicks() - time_start;
ImGui::SetCursorPosY(ImGui::GetCursorPosY()-50*g_viewport_mgr.m_scale);
ImGui::SetCursorPosX((ImGui::GetWindowWidth()-256*g_viewport_mgr.m_scale)/2);
logo_fbo->Target();
ImTextureID id = (ImTextureID)(intptr_t)logo_fbo->Texture();
float t_w = 256.0;
float t_h = 256.0;
float x_off = 0;
ImGui::Image(id,
ImVec2((t_w-x_off)*g_viewport_mgr.m_scale, t_h*g_viewport_mgr.m_scale),
ImVec2(x_off/t_w, t_h/t_h),
ImVec2(t_w/t_w, 0));
if (ImGui::IsItemClicked()) {
time_start = SDL_GetTicks();
}
RenderLogo(now, 0x42e335ff, 0x42e335ff, 0x00000000);
logo_fbo->Restore();
ImGui::SetCursorPosY(ImGui::GetCursorPosY()-75*g_viewport_mgr.m_scale);
SectionTitle("Build Information");
ImGui::PushFont(g_font_mgr.m_fixed_width_font);
ImGui::InputTextMultiline("##build_info", (char *)build_info_text,
strlen(build_info_text),
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),
ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 8),
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, MainMenuTabView *view)
: m_icon(icon), m_text(text), m_view(view)
{
}
MainMenuTabView *MainMenuTabButton::view()
{
return m_view;
}
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_general_view),
m_input_button("Input", ICON_FA_GAMEPAD, &m_input_view),
m_display_button("Display", ICON_FA_TV, &m_display_view),
m_audio_button("Audio", ICON_FA_VOLUME_HIGH, &m_audio_view),
m_network_button("Network", ICON_FA_NETWORK_WIRED, &m_network_view),
// m_snapshots_button("Snapshots", ICON_FA_CLOCK_ROTATE_LEFT, &m_snapshots_view),
m_system_button("System", ICON_FA_MICROCHIP, &m_system_view),
m_about_button("About", ICON_FA_CIRCLE_INFO, &m_about_view)
{
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_current_view_index = 0;
m_next_view_index = m_current_view_index;
}
void MainMenuScene::ShowGeneral()
{
SetNextViewIndexWithFocus(0);
}
void MainMenuScene::ShowInput()
{
SetNextViewIndexWithFocus(1);
}
void MainMenuScene::ShowDisplay()
{
SetNextViewIndexWithFocus(2);
}
void MainMenuScene::ShowAudio()
{
SetNextViewIndexWithFocus(3);
}
void MainMenuScene::ShowNetwork()
{
SetNextViewIndexWithFocus(4);
}
// void MainMenuScene::showSnapshots() { SetNextViewIndexWithFocus(5); }
void MainMenuScene::ShowSystem()
{
SetNextViewIndexWithFocus(5);
}
void MainMenuScene::ShowAbout()
{
SetNextViewIndexWithFocus(6);
}
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::IsNavInputTest(ImGuiNavInput_Cancel,
ImGuiInputReadMode_Pressed))) {
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;
}
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_tabs[m_current_view_index]->view()->Draw();
ImGui::PopFont();
ImGui::EndChild();
ImGui::PopID();
ImGui::PopStyleVar(3);
HandleInput();
}
ImGui::End();
ImGui::PopStyleVar(5);
return !m_animation.IsComplete();
}
MainMenuScene g_main_menu;