// // 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 "widgets.hh" #include "misc.hh" #include "font-manager.hh" #include "viewport-manager.hh" #include "ui/xemu-os-utils.h" void Separator() { // XXX: IDK. Maybe there's a better way to draw a separator ( ImGui::Separator() ) that cuts through window // padding... Just grab the draw list and draw the line with outer clip rect float thickness = 1 * g_viewport_mgr.m_scale; ImGuiWindow *window = ImGui::GetCurrentWindow(); ImDrawList *draw_list = ImGui::GetWindowDrawList(); ImRect window_rect = window->Rect(); ImVec2 size = ImVec2(window_rect.GetWidth(), thickness); ImVec2 p0(window_rect.Min.x, ImGui::GetCursorScreenPos().y); ImVec2 p1(p0.x + size.x, p0.y); ImGui::PushClipRect(window_rect.Min, window_rect.Max, false); draw_list->AddLine(p0, p1, ImGui::GetColorU32(ImGuiCol_Separator), thickness); ImGui::PopClipRect(); ImGui::Dummy(size); } void SectionTitle(const char *title) { ImGui::Spacing(); ImGui::PushFont(g_font_mgr.m_menu_font_medium); ImGui::Text("%s", title); ImGui::PopFont(); Separator(); } float GetWidgetTitleDescriptionHeight(const char *title, const char *description) { ImGui::PushFont(g_font_mgr.m_menu_font_medium); float h = ImGui::GetFrameHeight(); ImGui::PopFont(); if (description) { ImGuiStyle &style = ImGui::GetStyle(); h += style.ItemInnerSpacing.y; ImGui::PushFont(g_font_mgr.m_default_font); h += ImGui::GetTextLineHeight(); ImGui::PopFont(); } return h; } void WidgetTitleDescription(const char *title, const char *description, ImVec2 pos) { ImDrawList *draw_list = ImGui::GetWindowDrawList(); ImGuiStyle &style = ImGui::GetStyle(); ImVec2 text_pos = pos; text_pos.x += style.FramePadding.x; text_pos.y += style.FramePadding.y; ImGui::PushFont(g_font_mgr.m_menu_font_medium); float title_height = ImGui::GetTextLineHeight(); draw_list->AddText(text_pos, ImGui::GetColorU32(ImGuiCol_Text), title); ImGui::PopFont(); if (description) { text_pos.y += title_height + style.ItemInnerSpacing.y; ImGui::PushFont(g_font_mgr.m_default_font); draw_list->AddText(text_pos, ImGui::GetColorU32(ImVec4(0.94f, 0.94f, 0.94f, 0.70f)), description); ImGui::PopFont(); } } void WidgetTitleDescriptionItem(const char *str_id, const char *description) { ImVec2 p = ImGui::GetCursorScreenPos(); ImVec2 size(ImGui::GetColumnWidth(), GetWidgetTitleDescriptionHeight(str_id, description)); WidgetTitleDescription(str_id, description, p); // XXX: Internal API ImRect bb(p, ImVec2(p.x + size.x, p.y + size.y)); ImGui::ItemSize(size, 0.0f); ImGui::ItemAdd(bb, 0); } float GetSliderRadius(ImVec2 size) { return size.y * 0.5; } float GetSliderTrackXOffset(ImVec2 size) { return GetSliderRadius(size); } float GetSliderTrackWidth(ImVec2 size) { return size.x - GetSliderRadius(size) * 2; } float GetSliderValueForMousePos(ImVec2 mouse, ImVec2 pos, ImVec2 size) { return (mouse.x - pos.x - GetSliderTrackXOffset(size)) / GetSliderTrackWidth(size); } void DrawSlider(float v, bool hovered, ImVec2 pos, ImVec2 size) { ImDrawList *draw_list = ImGui::GetWindowDrawList(); float radius = GetSliderRadius(size); float rounding = size.y * 0.25; float slot_half_height = size.y * 0.125; const bool circular_grab = false; ImU32 bg = hovered ? ImGui::GetColorU32(ImGuiCol_FrameBgActive) : ImGui::GetColorU32(ImGuiCol_CheckMark); ImVec2 pmid(pos.x + radius + v*(size.x - radius*2), pos.y + size.y / 2); ImVec2 smin(pos.x + rounding, pmid.y - slot_half_height); ImVec2 smax(pmid.x, pmid.y + slot_half_height); draw_list->AddRectFilled(smin, smax, bg, rounding); bg = hovered ? ImGui::GetColorU32(ImGuiCol_FrameBgHovered) : ImGui::GetColorU32(ImGuiCol_FrameBg); smin.x = pmid.x; smax.x = pos.x + size.x - rounding; draw_list->AddRectFilled(smin, smax, bg, rounding); if (circular_grab) { draw_list->AddCircleFilled(pmid, radius * 0.8, ImGui::GetColorU32(ImGuiCol_SliderGrab)); } else { ImVec2 offs(radius*0.8, radius*0.8); draw_list->AddRectFilled(pmid - offs, pmid + offs, ImGui::GetColorU32(ImGuiCol_SliderGrab), rounding); } } void DrawToggle(bool enabled, bool hovered, ImVec2 pos, ImVec2 size) { ImDrawList *draw_list = ImGui::GetWindowDrawList(); float radius = size.y * 0.5; float rounding = size.y * 0.25; float slot_half_height = size.y * 0.5; const bool circular_grab = false; ImU32 bg = hovered ? ImGui::GetColorU32(enabled ? ImGuiCol_FrameBgActive : ImGuiCol_FrameBgHovered) : ImGui::GetColorU32(enabled ? ImGuiCol_CheckMark : ImGuiCol_FrameBg); ImVec2 pmid(pos.x + radius + (int)enabled * (size.x - radius * 2), pos.y + size.y / 2); ImVec2 smin(pos.x, pmid.y - slot_half_height); ImVec2 smax(pos.x + size.x, pmid.y + slot_half_height); draw_list->AddRectFilled(smin, smax, bg, rounding); if (circular_grab) { draw_list->AddCircleFilled(pmid, radius * 0.8, ImGui::GetColorU32(ImGuiCol_SliderGrab)); } else { ImVec2 offs(radius*0.8, radius*0.8); draw_list->AddRectFilled(pmid - offs, pmid + offs, ImGui::GetColorU32(ImGuiCol_SliderGrab), rounding); } } bool Toggle(const char *str_id, bool *v, const char *description) { ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS); ImGuiStyle &style = ImGui::GetStyle(); ImGui::PushFont(g_font_mgr.m_menu_font_medium); float title_height = ImGui::GetTextLineHeight(); ImGui::PopFont(); ImVec2 p = ImGui::GetCursorScreenPos(); ImVec2 bb(ImGui::GetColumnWidth(), GetWidgetTitleDescriptionHeight(str_id, description)); ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); ImGui::PushID(str_id); bool status = ImGui::Button("###toggle_button", bb); if (status) { *v = !*v; } ImGui::PopID(); ImGui::PopStyleVar(); const ImVec2 p_min = ImGui::GetItemRectMin(); const ImVec2 p_max = ImGui::GetItemRectMax(); WidgetTitleDescription(str_id, description, p); float toggle_height = title_height * 0.9; ImVec2 toggle_size(toggle_height * 1.75, toggle_height); ImVec2 toggle_pos(p_max.x - toggle_size.x - style.FramePadding.x, p_min.y + (title_height - toggle_size.y)/2 + style.FramePadding.y); DrawToggle(*v, ImGui::IsItemHovered(), toggle_pos, toggle_size); ImGui::PopStyleColor(); return status; } void Slider(const char *str_id, float *v, const char *description) { ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS); ImGuiStyle &style = ImGui::GetStyle(); ImGuiWindow *window = ImGui::GetCurrentWindow(); ImGui::PushFont(g_font_mgr.m_menu_font_medium); float title_height = ImGui::GetTextLineHeight(); ImGui::PopFont(); ImVec2 p = ImGui::GetCursorScreenPos(); ImVec2 size(ImGui::GetColumnWidth(), GetWidgetTitleDescriptionHeight(str_id, description)); WidgetTitleDescription(str_id, description, p); // XXX: Internal API ImVec2 wpos = ImGui::GetCursorPos(); ImRect bb(p, ImVec2(p.x + size.x, p.y + size.y)); ImGui::ItemSize(size, 0.0f); ImGui::ItemAdd(bb, 0); ImGui::SetItemAllowOverlap(); ImGui::SameLine(0, 0); ImVec2 slider_size(size.x * 0.4, title_height * 0.9); ImVec2 slider_pos(bb.Max.x - slider_size.x - style.FramePadding.x, p.y + (title_height - slider_size.y)/2 + style.FramePadding.y); ImGui::SetCursorPos(ImVec2(wpos.x + size.x - slider_size.x - style.FramePadding.x, wpos.y)); ImGui::InvisibleButton("###slider", slider_size, 0); if (ImGui::IsItemHovered()) { if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) || ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft) || ImGui::IsKeyPressed(ImGuiKey_GamepadLStickLeft) || ImGui::IsKeyPressed(ImGuiKey_GamepadRStickLeft)) { *v -= 0.05; } if (ImGui::IsKeyPressed(ImGuiKey_RightArrow) || ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight) || ImGui::IsKeyPressed(ImGuiKey_GamepadLStickRight) || ImGui::IsKeyPressed(ImGuiKey_GamepadRStickRight)) { *v += 0.05; } if ( ImGui::IsKeyDown(ImGuiKey_LeftArrow) || ImGui::IsKeyDown(ImGuiKey_GamepadDpadLeft) || ImGui::IsKeyDown(ImGuiKey_GamepadLStickLeft) || ImGui::IsKeyDown(ImGuiKey_GamepadRStickLeft) || ImGui::IsKeyDown(ImGuiKey_RightArrow) || ImGui::IsKeyDown(ImGuiKey_GamepadDpadRight) || ImGui::IsKeyDown(ImGuiKey_GamepadLStickRight) || ImGui::IsKeyDown(ImGuiKey_GamepadRStickRight) ) { ImGui::NavMoveRequestCancel(); } } if (ImGui::IsItemActive()) { ImVec2 mouse = ImGui::GetMousePos(); *v = GetSliderValueForMousePos(mouse, slider_pos, slider_size); } *v = fmax(0, fmin(*v, 1)); DrawSlider(*v, ImGui::IsItemHovered() || ImGui::IsItemActive(), slider_pos, slider_size); ImVec2 slider_max = ImVec2(slider_pos.x + slider_size.x, slider_pos.y + slider_size.y); ImGui::RenderNavHighlight(ImRect(slider_pos, slider_max), window->GetID("###slider")); ImGui::PopStyleColor(); } bool FilePicker(const char *str_id, const char **buf, const char *filters, bool dir) { bool changed = false; ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32_BLACK_TRANS); ImGuiStyle &style = ImGui::GetStyle(); ImVec2 p = ImGui::GetCursorScreenPos(); const char *desc = strlen(*buf) ? *buf : "(None Selected)"; ImVec2 bb(ImGui::GetColumnWidth(), GetWidgetTitleDescriptionHeight(str_id, desc)); ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); ImGui::PushID(str_id); bool status = ImGui::Button("###file_button", bb); if (status) { int flags = NOC_FILE_DIALOG_OPEN; if (dir) flags |= NOC_FILE_DIALOG_DIR; const char *new_path = PausedFileOpen(flags, filters, *buf, NULL); if (new_path) { free((void*)*buf); *buf = strdup(new_path); changed = true; } } ImGui::PopID(); ImGui::PopStyleVar(); WidgetTitleDescription(str_id, desc, p); const ImVec2 p0 = ImGui::GetItemRectMin(); const ImVec2 p1 = ImGui::GetItemRectMax(); ImDrawList *draw_list = ImGui::GetWindowDrawList(); ImGui::PushFont(g_font_mgr.m_menu_font); const char *icon = dir ? ICON_FA_FOLDER : ICON_FA_FILE; ImVec2 ts_icon = ImGui::CalcTextSize(icon); draw_list->AddText(ImVec2(p1.x - style.FramePadding.x - ts_icon.x, p0.y + (p1.y - p0.y - ts_icon.y) / 2), ImGui::GetColorU32(ImGuiCol_Text), icon); ImGui::PopFont(); ImGui::PopStyleColor(); return changed; } void DrawComboChevron() { ImGui::PushFont(g_font_mgr.m_menu_font); const ImVec2 p0 = ImGui::GetItemRectMin(); const ImVec2 p1 = ImGui::GetItemRectMax(); const char *icon = ICON_FA_CHEVRON_DOWN; ImVec2 ts_icon = ImGui::CalcTextSize(icon); ImGuiStyle &style = ImGui::GetStyle(); ImDrawList *draw_list = ImGui::GetWindowDrawList(); draw_list->AddText(ImVec2(p1.x - style.FramePadding.x - ts_icon.x, p0.y + (p1.y - p0.y - ts_icon.y) / 2), ImGui::GetColorU32(ImGuiCol_Text), icon); ImGui::PopFont(); } void PrepareComboTitleDescription(const char *label, const char *description, float combo_size_ratio) { float width = ImGui::GetColumnWidth(); ImVec2 pos = ImGui::GetCursorScreenPos(); ImVec2 size(width, GetWidgetTitleDescriptionHeight(label, description)); WidgetTitleDescription(label, description, pos); ImVec2 wpos = ImGui::GetCursorPos(); ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y)); ImGui::ItemSize(size, 0.0f); ImGui::ItemAdd(bb, 0); ImGui::SetItemAllowOverlap(); ImGui::SameLine(0, 0); float combo_width = width * combo_size_ratio; ImGui::SetCursorPos(ImVec2(wpos.x + width - combo_width, wpos.y)); } bool ChevronCombo(const char *label, int *current_item, bool (*items_getter)(void *, int, const char **), void *data, int items_count, const char *description) { bool value_changed = false; float combo_width = ImGui::GetColumnWidth(); if (*label != '#') { float combo_size_ratio = 0.4; PrepareComboTitleDescription(label, description, combo_size_ratio); combo_width *= combo_size_ratio; } ImGuiContext& g = *GImGui; ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1, 0)); // Call the getter to obtain the preview string which is a parameter to BeginCombo() const char* preview_value = NULL; if (*current_item >= 0 && *current_item < items_count) items_getter(data, *current_item, &preview_value); ImGui::SetNextItemWidth(combo_width); ImGui::PushFont(g_font_mgr.m_menu_font_small); ImGui::PushID(label); if (ImGui::BeginCombo("###chevron_combo", preview_value, ImGuiComboFlags_NoArrowButton)) { // Display items // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed) for (int i = 0; i < items_count; i++) { ImGui::PushID(i); const bool item_selected = (i == *current_item); const char* item_text; if (!items_getter(data, i, &item_text)) item_text = "*Unknown item*"; if (ImGui::Selectable(item_text, item_selected)) { value_changed = true; *current_item = i; } if (item_selected) ImGui::SetItemDefaultFocus(); ImGui::PopID(); } ImGui::EndCombo(); if (value_changed) ImGui::MarkItemEdited(g.LastItemData.ID); } ImGui::PopID(); ImGui::PopFont(); DrawComboChevron(); ImGui::PopStyleVar(); return value_changed; } // Getter for the old Combo() API: "item1\0item2\0item3\0" static bool Items_SingleStringGetter(void* data, int idx, const char** out_text) { // FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited. const char* items_separated_by_zeros = (const char*)data; int items_count = 0; const char* p = items_separated_by_zeros; while (*p) { if (idx == items_count) break; p += strlen(p) + 1; items_count++; } if (!*p) return false; if (out_text) *out_text = p; return true; } // Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0" bool ChevronCombo(const char* label, int* current_item, const char* items_separated_by_zeros, const char *description) { int items_count = 0; const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open while (*p) { p += strlen(p) + 1; items_count++; } bool value_changed = ChevronCombo( label, current_item, Items_SingleStringGetter, (void *)items_separated_by_zeros, items_count, description); return value_changed; } void Hyperlink(const char *text, const char *url) { ImColor col; ImGui::Text("%s", text); if (ImGui::IsItemHovered()) { col = IM_COL32_WHITE; ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } else { col = ImColor(127, 127, 127, 255); } ImVec2 max = ImGui::GetItemRectMax(); ImVec2 min = ImGui::GetItemRectMin(); min.x -= 1 * g_viewport_mgr.m_scale; min.y = max.y; max.x -= 1 * g_viewport_mgr.m_scale; ImGui::GetWindowDrawList()->AddLine(min, max, col, 1.0 * g_viewport_mgr.m_scale); if (ImGui::IsItemClicked()) { xemu_open_web_browser(url); } } 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(); } }