/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * 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 3 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 "backends/imgui/IconsMaterialSymbols.h" #define IMGUI_DEFINE_MATH_OPERATORS #include "backends/imgui/imgui.h" #include "backends/imgui/imgui_fonts.h" #include "common/debug.h" #include "common/system.h" #include "graphics/managed_surface.h" #include "wage/wage.h" #include "wage/dt-internal.h" #include "wage/design.h" #include "wage/gui.h" #include "wage/script.h" #include "wage/sound.h" #include "wage/world.h" namespace Wage { ImGuiState *_state = nullptr; ImGuiImage getPatternImage(int fillType) { int index = fillType - 1; if (index >= 0 && index < (int)_state->_patternTextures.size()) { return _state->_patternTextures[index]; } return {0, 0, 0}; } void drawThicknessIcon(int thickness) { ImVec2 pos = ImGui::GetCursorScreenPos(); ImDrawList *dl = ImGui::GetWindowDrawList(); // draw Border dl->AddRect(pos, pos + ImVec2(16, 16), IM_COL32(255, 255, 255, 255)); float half = thickness * 0.5f; // draw Bar dl->AddRectFilled( ImVec2(pos.x, pos.y + 8 - half), ImVec2(pos.x + 16, pos.y + 8 + half), IM_COL32(255, 255, 255, 255)); ImGui::Dummy(ImVec2(16, 16)); ImGui::SetItemTooltip("Line thickness: %d", thickness); } void drawMissingPatternIcon() { ImVec2 pos = ImGui::GetCursorScreenPos(); ImDrawList *dl = ImGui::GetWindowDrawList(); float size = 16.0f; dl->AddLine(pos, pos + ImVec2(size, size), IM_COL32(255, 0, 0, 255)); dl->AddRect(pos, pos + ImVec2(size, size), IM_COL32(255, 255, 255, 255)); ImGui::Dummy(ImVec2(size, size)); } ImGuiImage getImageID(Designed *d, const char *type, int steps = -1) { Common::String key = Common::String::format("%s:%s:%d", d->_name.c_str(), type, steps); if (_state->_images.contains(key)) return _state->_images[key]; if (!d->_design) return { 0, 0, 0 }; // We need to precompute dimensions Graphics::ManagedSurface tmpsurf(1024, 768, Graphics::PixelFormat::createFormatCLUT8()); d->_design->paint(&tmpsurf, *g_wage->_world->_patterns, 0, 0); int sx = d->_design->getBounds()->width(), sy = d->_design->getBounds()->height(); if (!sx || !sy) return { 0, 0, 0 }; Graphics::ManagedSurface *surface = new Graphics::ManagedSurface(); surface->create(sx, sy, Graphics::PixelFormat::createFormatCLUT8()); surface->fillRect(Common::Rect(0, 0, sx, sy), 4); // white background d->_design->paint(surface, *g_wage->_world->_patterns, 0, 0, steps); if (surface->surfacePtr()) { _state->_images[key] = { (ImTextureID)g_system->getImGuiTexture(*surface->surfacePtr(), g_wage->_gui->_wm->getPalette(), g_wage->_gui->_wm->getPaletteSize()), sx, sy }; } delete surface; return _state->_images[key]; } void showImage(const ImGuiImage &image, float scale) { ImVec2 size = { (float)image.width * scale, (float)image.height * scale }; ImGui::BeginGroup(); ImVec2 screenPos = ImGui::GetCursorScreenPos(); ImGui::Image(image.id, size); ImGui::GetWindowDrawList()->AddRect(screenPos, screenPos + ImVec2(size.x, size.y), 0xFFFFFFFF); ImGui::EndGroup(); } void showDesignScriptWindow(Design *d) { if (!_state->_showScriptWindow) return; ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); if (ImGui::Begin("Design Script", &_state->_showScriptWindow)) { for (uint i = 0; i < d->_drawOps.size(); i++) { const DrawOp &op = d->_drawOps[i]; // check if this is the current step bool isCurrent = (_state->_currentStep == i + 1); if (isCurrent) { ImVec2 pos = ImGui::GetCursorScreenPos(); float width = ImGui::GetContentRegionAvail().x; ImDrawList *dl = ImGui::GetWindowDrawList(); ImU32 green = IM_COL32(0, 255, 0, 255); ImU32 greenTransparent = IM_COL32(0, 255, 0, 50); // draw Arrow dl->AddText(pos, green, ICON_MS_ARROW_RIGHT_ALT); // draw highlight dl->AddRectFilled( ImVec2(pos.x + 16.f, pos.y), ImVec2(pos.x + width, pos.y + 16.f), greenTransparent, 0.4f); } // offset text to the right so it doesn't overlap the arrow ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 16.0f); // opcode ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "[%5d]", op.offset); ImGui::SameLine(); // draw current foreground fill pattern if (op.fillType > 0) { ImGuiImage patImg = getPatternImage(op.fillType); if (patImg.id) showImage(patImg, 1.0f); else drawMissingPatternIcon(); ImGui::SetItemTooltip("Foreground pattern: %d", op.fillType); ImGui::SameLine(); } // draw current border fill pattern if (op.borderFillType > 0) { ImGuiImage patImg = getPatternImage(op.borderFillType); if (patImg.id) showImage(patImg, 1.0f); else drawMissingPatternIcon(); ImGui::SetItemTooltip("Border pattern: %d", op.borderFillType); ImGui::SameLine(); } // draw line thickness icon if (op.borderThickness > 0) { drawThicknessIcon(op.borderThickness); ImGui::SameLine(); } Common::String label = Common::String::format("%s##%d", op.opcode.c_str(), i); if (ImGui::Selectable(label.c_str(), isCurrent)) { _state->_currentStep = i + 1; } if (isCurrent) { ImGui::SetItemDefaultFocus(); } } } ImGui::End(); } void showDesignViewer(Designed *item, const char *typeStr) { if (!item) return; Design *d = item->_design; if (d) { if (d->_drawOps.empty()) { // force render to populate ops Graphics::ManagedSurface tmp; tmp.create(1, 1, Graphics::PixelFormat::createFormatCLUT8()); d->paint(&tmp, *g_wage->_world->_patterns, 0, 0, -1); tmp.free(); } uint totalSteps = d->_drawOps.size(); if (ImGui::Button(ICON_MS_SKIP_PREVIOUS)) { if (_state->_currentStep > 1) _state->_currentStep--; else _state->_currentStep = totalSteps; } ImGui::SameLine(); if (ImGui::Button(ICON_MS_SKIP_NEXT)) { if (_state->_currentStep < totalSteps) _state->_currentStep++; else _state->_currentStep = 1; } ImGui::SameLine(); if (ImGui::Button("Script")) { _state->_showScriptWindow = !_state->_showScriptWindow; } ImGui::SameLine(); ImGui::Text("Step: %d / %d", (_state->_currentStep), totalSteps); if (d->getBounds()) { ImGuiImage imgID = getImageID(item, typeStr, _state->_currentStep); showImage(imgID, 1.0); } showDesignScriptWindow(d); } else { ImGui::Text("No Design Data"); } } void resetStepToLast(Designed *d) { if (d && d->_design) { // if drawOps is empty, force a paint to calculate steps if (d->_design->_drawOps.empty()) { Graphics::ManagedSurface tmp(1, 1, Graphics::PixelFormat::createFormatCLUT8()); d->_design->paint(&tmp, *g_wage->_world->_patterns, 0, 0, -1); } _state->_currentStep = d->_design->_drawOps.size(); } else { _state->_currentStep = 1; } } static void showWorld() { if (!_state->_showWorld) return; // Calculate the viewport size ImVec2 viewportSize = ImGui::GetMainViewport()->Size; // Calculate the window size ImVec2 windowSize = ImVec2( viewportSize.x * 0.9f, viewportSize.y * 0.9f ); // Calculate the centered position ImVec2 centeredPosition = ImVec2( (viewportSize.x - windowSize.x) * 0.5f, (viewportSize.y - windowSize.y) * 0.5f ); // Set the next window position and size ImGui::SetNextWindowPos(centeredPosition, ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(windowSize, ImGuiCond_FirstUseEver); if (ImGui::Begin("World", &_state->_showWorld)) { ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_None; if (ImGui::BeginTabBar("MainTabBar", tab_bar_flags)) { if (ImGui::BeginTabItem("Scenes")) { { // Left pane ImGui::BeginChild("ChildL", ImVec2(ImGui::GetContentRegionAvail().x * 0.4f, ImGui::GetContentRegionAvail().y), ImGuiChildFlags_None); if (ImGui::BeginListBox("##listbox scenes", ImVec2(-FLT_MIN, ImGui::GetContentRegionAvail().y))) { for (int n = 0; n < (int)g_wage->_world->_orderedScenes.size(); n++) { const bool is_selected = (_state->_selectedScene == n); Common::String label = Common::String::format("%s##%d", g_wage->_world->_orderedScenes[n]->_name.c_str(), n); if (ImGui::Selectable(label.c_str(), is_selected)) { _state->_selectedScene = n; resetStepToLast(g_wage->_world->_orderedScenes[n]); } // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) if (is_selected) ImGui::SetItemDefaultFocus(); } ImGui::EndListBox(); } ImGui::EndChild(); } ImGui::SameLine(); { // Right pane ImGui::BeginChild("ChildR", ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y), ImGuiChildFlags_Borders); if (ImGui::BeginTabBar("SceneTabBar", tab_bar_flags)) { if (ImGui::BeginTabItem("Script")) { if (g_wage->_world->_orderedScenes[_state->_selectedScene]->_script && g_wage->_world->_orderedScenes[_state->_selectedScene]->_script->_scriptText.size()) { for (auto &t : g_wage->_world->_orderedScenes[_state->_selectedScene]->_script->_scriptText) { ImGui::Text("[%4d]", t->offset); ImGui::SameLine(); ImGui::Text("%s", t->line.c_str()); } } else { ImGui::Text("No script"); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Text")) { ImGui::TextWrapped("%s", g_wage->_world->_orderedScenes[_state->_selectedScene]->_text.c_str()); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Design")) { Designed *d = g_wage->_world->_orderedScenes[_state->_selectedScene]; showDesignViewer(d, "scene"); ImGui::EndTabItem(); } ImGui::EndTabBar(); } ImGui::EndChild(); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Objects")) { { // Left pane ImGui::BeginChild("ChildL", ImVec2(ImGui::GetContentRegionAvail().x * 0.4f, ImGui::GetContentRegionAvail().y), ImGuiChildFlags_None); if (ImGui::BeginListBox("##listbox objects", ImVec2(-FLT_MIN, ImGui::GetContentRegionAvail().y))) { for (int n = 0; n < (int)g_wage->_world->_orderedObjs.size(); n++) { const bool is_selected = (_state->_selectedObj == n); Common::String label = Common::String::format("%s##%d", g_wage->_world->_orderedObjs[n]->_name.c_str(), n); if (ImGui::Selectable(label.c_str(), is_selected)) { _state->_selectedObj = n; resetStepToLast(g_wage->_world->_orderedObjs[n]); } // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) if (is_selected) ImGui::SetItemDefaultFocus(); } ImGui::EndListBox(); } ImGui::EndChild(); } ImGui::SameLine(); { // Right pane ImGui::BeginChild("ChildR", ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y), ImGuiChildFlags_Borders); showDesignViewer(g_wage->_world->_orderedObjs[_state->_selectedObj], "obj"); ImGui::EndChild(); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Characters")) { { // Left pane ImGui::BeginChild("ChildL", ImVec2(ImGui::GetContentRegionAvail().x * 0.4f, ImGui::GetContentRegionAvail().y), ImGuiChildFlags_None); if (ImGui::BeginListBox("##listbox characters", ImVec2(-FLT_MIN, ImGui::GetContentRegionAvail().y))) { for (int n = 0; n < (int)g_wage->_world->_orderedChrs.size(); n++) { const bool is_selected = (_state->_selectedChr == n); Common::String label = Common::String::format("%s##%d", g_wage->_world->_orderedChrs[n]->_name.c_str(), n); if (ImGui::Selectable(label.c_str(), is_selected)) { _state->_selectedChr = n; resetStepToLast(g_wage->_world->_orderedChrs[n]); } // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) if (is_selected) ImGui::SetItemDefaultFocus(); } ImGui::EndListBox(); } ImGui::EndChild(); } ImGui::SameLine(); { // Right pane ImGui::BeginChild("ChildR", ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y), ImGuiChildFlags_Borders); showDesignViewer(g_wage->_world->_orderedChrs[_state->_selectedChr], "chr"); ImGui::EndChild(); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Sounds")) { { // Left pane ImGui::BeginChild("ChildL", ImVec2(ImGui::GetContentRegionAvail().x * 0.4f, ImGui::GetContentRegionAvail().y), ImGuiChildFlags_None); if (ImGui::BeginListBox("##listbox sounds", ImVec2(-FLT_MIN, ImGui::GetContentRegionAvail().y))) { for (int n = 0; n < (int)g_wage->_world->_orderedSounds.size(); n++) { const bool is_selected = (_state->_selectedSound == n); Common::String label = Common::String::format("%s##%d", g_wage->_world->_orderedSounds[n]->_name.c_str(), n); if (ImGui::Selectable(label.c_str(), is_selected)) _state->_selectedSound = n; // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) if (is_selected) ImGui::SetItemDefaultFocus(); } ImGui::EndListBox(); } ImGui::EndChild(); } ImGui::SameLine(); { // Right pane ImGui::BeginChild("ChildR", ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y), ImGuiChildFlags_Borders); ImGui::Text("Sound playback"); ImGui::EndChild(); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Global Script")) { for (auto &t : g_wage->_world->_globalScript->_scriptText) { ImGui::Text("[%4d]", t->offset); ImGui::SameLine(); ImGui::Text("%s", t->line.c_str()); } ImGui::EndTabItem(); } if (ImGui::BeginTabItem("World")) { ImGui::Text("This is the Global Script tab!\nblah blah blah blah blah"); ImGui::EndTabItem(); } ImGui::EndTabBar(); } } ImGui::End(); } void onImGuiInit() { ImGuiIO &io = ImGui::GetIO(); io.Fonts->AddFontDefault(); ImFontConfig icons_config; icons_config.MergeMode = true; icons_config.PixelSnapH = false; icons_config.OversampleH = 3; icons_config.OversampleV = 3; icons_config.GlyphOffset = {0, 4}; static const ImWchar icons_ranges[] = {ICON_MIN_MS, ICON_MAX_MS, 0}; ImGui::addTTFFontFromArchive("MaterialSymbolsSharp.ttf", 16.f, &icons_config, icons_ranges); _state = new ImGuiState(); // pre-load patterns into array Graphics::MacPatterns &patterns = *g_wage->_world->_patterns; for (uint i = 0; i < patterns.size(); ++i) { Graphics::ManagedSurface surface(16, 16, Graphics::PixelFormat::createFormatCLUT8()); Common::Rect rect(16, 16); Design::drawFilledRect(&surface, rect, kColorBlack, patterns, i + 1); if (surface.surfacePtr()) { ImTextureID tex = (ImTextureID)g_system->getImGuiTexture( *surface.surfacePtr(), g_wage->_gui->_wm->getPalette(), g_wage->_gui->_wm->getPaletteSize()); _state->_patternTextures.push_back({tex, 16, 16}); } else { _state->_patternTextures.push_back({0, 0, 0}); } } } void onImGuiRender() { if (!debugChannelSet(-1, kDebugImGui)) { ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange | ImGuiConfigFlags_NoMouse; return; } if (!_state) return; ImGui::GetIO().ConfigFlags &= ~(ImGuiConfigFlags_NoMouseCursorChange | ImGuiConfigFlags_NoMouse); if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("View")) { ImGui::MenuItem("World", NULL, &_state->_showWorld); ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } showWorld(); } void onImGuiCleanup() { delete _state; _state = nullptr; } } // namespace Wage