mirror of
https://github.com/WerWolv/ImHex.git
synced 2026-03-28 07:47:03 -05:00
feat: Support for building ImHex for the web (#1328)
Co-authored-by: WerWolv <werwolv98@gmail.com> Co-authored-by: AnnsAnn <git@annsann.eu>
This commit is contained in:
@@ -8,11 +8,13 @@ add_executable(main ${APPLICATION_TYPE}
|
||||
source/window/win_window.cpp
|
||||
source/window/macos_window.cpp
|
||||
source/window/linux_window.cpp
|
||||
source/window/web_window.cpp
|
||||
|
||||
source/messaging/common.cpp
|
||||
source/messaging/linux.cpp
|
||||
source/messaging/macos.cpp
|
||||
source/messaging/win.cpp
|
||||
source/messaging/web.cpp
|
||||
|
||||
source/init/splash_window.cpp
|
||||
source/init/tasks.cpp
|
||||
@@ -29,6 +31,18 @@ add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../lib/external/libromfs ${CMAKE
|
||||
set_target_properties(${LIBROMFS_LIBRARY} PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
add_dependencies(imhex_all main)
|
||||
|
||||
if (EMSCRIPTEN)
|
||||
target_link_options(main PRIVATE -sUSE_GLFW=3 -sUSE_PTHREADS=1 -sALLOW_MEMORY_GROWTH=1)
|
||||
target_link_options(main PRIVATE -sTOTAL_MEMORY=134217728)
|
||||
target_link_options(main PRIVATE -sMAX_WEBGL_VERSION=2)
|
||||
target_link_options(main PRIVATE -sEXPORTED_RUNTIME_METHODS=ccall)
|
||||
target_link_options(main PRIVATE -sFETCH)
|
||||
target_link_options(main PRIVATE -sWASM_BIGINT)
|
||||
target_link_options(main PRIVATE -O1)
|
||||
target_link_options(main PRIVATE -sLEGACY_GL_EMULATION)
|
||||
target_link_libraries(main PRIVATE idbfs.js)
|
||||
endif ()
|
||||
|
||||
set_target_properties(main PROPERTIES
|
||||
OUTPUT_NAME ${IMHEX_APPLICATION_NAME}
|
||||
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../..
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace hex {
|
||||
|
||||
static void initNative();
|
||||
|
||||
void resize(i32 width, i32 height);
|
||||
|
||||
private:
|
||||
void setupNativeWindow();
|
||||
void beginNativeWindowFrame();
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <hex/ui/imgui_imhex_extensions.h>
|
||||
#include <imgui_impl_glfw.h>
|
||||
#include <imgui_impl_opengl3.h>
|
||||
#include <imgui_impl_opengl3_loader.h>
|
||||
#include <fonts/fontawesome_font.h>
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
@@ -29,6 +28,14 @@
|
||||
#include <numeric>
|
||||
#include <random>
|
||||
|
||||
#if defined(OS_WEB)
|
||||
#define GLFW_INCLUDE_ES3
|
||||
#include <GLES3/gl3.h>
|
||||
#include <emscripten/html5.h>
|
||||
#else
|
||||
#include <imgui_impl_opengl3_loader.h>
|
||||
#endif
|
||||
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
namespace hex::init {
|
||||
@@ -324,6 +331,8 @@ namespace hex::init {
|
||||
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);
|
||||
glfwWindowHint(GLFW_DECORATED, GLFW_FALSE);
|
||||
glfwWindowHint(GLFW_FLOATING, GLFW_FALSE);
|
||||
glfwWindowHint(GLFW_SAMPLES, 1);
|
||||
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
|
||||
|
||||
// Create the splash screen window
|
||||
this->m_window = glfwCreateWindow(1, 400, "Starting ImHex...", nullptr, nullptr);
|
||||
@@ -373,6 +382,8 @@ namespace hex::init {
|
||||
|
||||
#if defined(OS_MACOS)
|
||||
ImGui_ImplOpenGL3_Init("#version 150");
|
||||
#elif defined(OS_WEB)
|
||||
ImGui_ImplOpenGL3_Init();
|
||||
#else
|
||||
ImGui_ImplOpenGL3_Init("#version 130");
|
||||
#endif
|
||||
|
||||
@@ -80,6 +80,13 @@ namespace hex::init {
|
||||
}
|
||||
|
||||
TaskManager::createBackgroundTask("Sending statistics...", [uuid, versionString](auto&) {
|
||||
// To avoid potentially flooding our database with lots of dead users
|
||||
// from people just visiting the website, don't send telemetry data from
|
||||
// the web version
|
||||
#if defined(OS_WEB)
|
||||
return;
|
||||
#endif
|
||||
|
||||
// Make telemetry request
|
||||
nlohmann::json telemetry = {
|
||||
{ "uuid", uuid },
|
||||
@@ -145,7 +152,7 @@ namespace hex::init {
|
||||
wolv::io::File newConfigFile(newConfigPath / "settings.json", wolv::io::File::Mode::Read);
|
||||
if (!newConfigFile.isValid()) {
|
||||
|
||||
// find an old config
|
||||
// Find an old config
|
||||
std::fs::path oldConfigPath;
|
||||
for (const auto &dir : hex::fs::appendPath(hex::fs::getDataPaths(), "config")) {
|
||||
wolv::io::File oldConfigFile(dir / "settings.json", wolv::io::File::Mode::Read);
|
||||
@@ -493,15 +500,17 @@ namespace hex::init {
|
||||
|
||||
// ImHex requires exactly one built-in plugin
|
||||
// If no built-in plugin or more than one was found, something's wrong and we can't continue
|
||||
if (builtinPlugins == 0) {
|
||||
log::error("Built-in plugin not found!");
|
||||
ImHexApi::System::impl::addInitArgument("no-builtin-plugin");
|
||||
return false;
|
||||
} else if (builtinPlugins > 1) {
|
||||
log::error("Found more than one built-in plugin!");
|
||||
ImHexApi::System::impl::addInitArgument("multiple-builtin-plugins");
|
||||
return false;
|
||||
}
|
||||
#if !defined(EMSCRIPTEN)
|
||||
if (builtinPlugins == 0) {
|
||||
log::error("Built-in plugin not found!");
|
||||
ImHexApi::System::impl::addInitArgument("no-builtin-plugin");
|
||||
return false;
|
||||
} else if (builtinPlugins > 1) {
|
||||
log::error("Found more than one built-in plugin!");
|
||||
ImHexApi::System::impl::addInitArgument("multiple-builtin-plugins");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
#include <wolv/io/fs.hpp>
|
||||
#include <wolv/utils/guards.hpp>
|
||||
|
||||
#if defined(OS_WEB)
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
#endif
|
||||
|
||||
using namespace hex;
|
||||
|
||||
namespace {
|
||||
@@ -66,6 +71,7 @@ namespace {
|
||||
/**
|
||||
* @brief Displays ImHex's splash screen and runs all initialization tasks. The splash screen will be displayed until all tasks have finished.
|
||||
*/
|
||||
[[maybe_unused]]
|
||||
void initializeImHex() {
|
||||
init::WindowSplash splashWindow;
|
||||
|
||||
@@ -105,6 +111,103 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if defined(OS_WEB)
|
||||
using namespace hex::init;
|
||||
|
||||
void saveFsData() {
|
||||
EM_ASM({
|
||||
FS.syncfs(function (err) {
|
||||
if (!err)
|
||||
return;
|
||||
alert("Failed to save permanent file system: "+err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
int runImHex() {
|
||||
auto splashWindow = new WindowSplash();
|
||||
|
||||
log::info("Using '{}' GPU", ImHexApi::System::getGPUVendor());
|
||||
|
||||
// Add initialization tasks to run
|
||||
TaskManager::init();
|
||||
for (const auto &[name, task, async] : init::getInitTasks())
|
||||
splashWindow->addStartupTask(name, task, async);
|
||||
|
||||
splashWindow->startStartupTasks();
|
||||
|
||||
// Draw the splash window while tasks are running
|
||||
emscripten_set_main_loop_arg([](void *arg) {
|
||||
auto splashWindow = reinterpret_cast<WindowSplash*>(arg);
|
||||
|
||||
FrameResult res = splashWindow->fullFrame();
|
||||
if (res == FrameResult::success) {
|
||||
handleFileOpenRequest();
|
||||
|
||||
// Clean up everything after the main window is closed
|
||||
emscripten_set_beforeunload_callback(nullptr, [](int eventType, const void *reserved, void *userData) {
|
||||
hex::unused(eventType, reserved, userData);
|
||||
|
||||
try {
|
||||
saveFsData();
|
||||
deinitializeImHex();
|
||||
return "";
|
||||
} catch (const std::exception &ex) {
|
||||
std::string *msg = new std::string("Failed to deinitialize ImHex. This is just a message warning you of this, the application has already closed, you probably can't do anything about it. Message: ");
|
||||
msg->append(std::string(ex.what()));
|
||||
log::fatal("{}", *msg);
|
||||
return msg->c_str();
|
||||
}
|
||||
});
|
||||
|
||||
// Delete splash window (do it before creating the main window so glfw destroys the window)
|
||||
delete splashWindow;
|
||||
|
||||
emscripten_cancel_main_loop();
|
||||
|
||||
// Main window
|
||||
static Window window;
|
||||
emscripten_set_main_loop([]() {
|
||||
window.fullFrame();
|
||||
}, 60, 0);
|
||||
}
|
||||
}, splashWindow, 60, 0);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
int runImHex() {
|
||||
|
||||
bool shouldRestart = false;
|
||||
do {
|
||||
// Register an event handler that will make ImHex restart when requested
|
||||
shouldRestart = false;
|
||||
EventManager::subscribe<RequestRestartImHex>([&] {
|
||||
shouldRestart = true;
|
||||
});
|
||||
|
||||
initializeImHex();
|
||||
handleFileOpenRequest();
|
||||
|
||||
// Clean up everything after the main window is closed
|
||||
ON_SCOPE_EXIT {
|
||||
deinitializeImHex();
|
||||
};
|
||||
|
||||
// Main window
|
||||
Window window;
|
||||
window.loop();
|
||||
|
||||
} while (shouldRestart);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,27 +230,5 @@ int main(int argc, char **argv) {
|
||||
|
||||
ImHexApi::System::impl::setPortableVersion(isPortableVersion());
|
||||
|
||||
bool shouldRestart = false;
|
||||
do {
|
||||
// Register an event handler that will make ImHex restart when requested
|
||||
shouldRestart = false;
|
||||
EventManager::subscribe<RequestRestartImHex>([&] {
|
||||
shouldRestart = true;
|
||||
});
|
||||
|
||||
initializeImHex();
|
||||
handleFileOpenRequest();
|
||||
|
||||
// Clean up everything after the main window is closed
|
||||
ON_SCOPE_EXIT {
|
||||
deinitializeImHex();
|
||||
};
|
||||
|
||||
// Main window
|
||||
Window window;
|
||||
window.loop();
|
||||
|
||||
} while (shouldRestart);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
return runImHex();
|
||||
};
|
||||
|
||||
24
main/gui/source/messaging/web.cpp
Normal file
24
main/gui/source/messaging/web.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#if defined(OS_WEB)
|
||||
|
||||
#include<stdexcept>
|
||||
|
||||
#include <hex/helpers/intrinsics.hpp>
|
||||
#include <hex/helpers/logger.hpp>
|
||||
|
||||
#include "messaging.hpp"
|
||||
|
||||
namespace hex::messaging {
|
||||
|
||||
void sendToOtherInstance(const std::string &eventName, const std::vector<u8> &args) {
|
||||
hex::unused(eventName);
|
||||
hex::unused(args);
|
||||
log::error("Unimplemented function 'sendToOtherInstance()' called");
|
||||
}
|
||||
|
||||
// Not implemented, so lets say we are the main instance every time so events are forwarded to ourselves
|
||||
bool setupNative() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -22,13 +22,13 @@ namespace hex::messaging {
|
||||
// Check if the window is visible and if it's an ImHex window
|
||||
if (::IsWindowVisible(hWnd) == TRUE && length != 0) {
|
||||
if (windowName.starts_with("ImHex")) {
|
||||
// it's our window, return it and stop iteration
|
||||
// It's our window, return it and stop iteration
|
||||
*reinterpret_cast<HWND*>(ret) = hWnd;
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
// continue iteration
|
||||
// Continue iteration
|
||||
return TRUE;
|
||||
|
||||
}, reinterpret_cast<LPARAM>(&imhexWindow));
|
||||
@@ -69,7 +69,7 @@ namespace hex::messaging {
|
||||
|
||||
constexpr static auto UniqueMutexId = "ImHex/a477ea68-e334-4d07-a439-4f159c683763";
|
||||
|
||||
// check if an ImHex instance is already running by opening a global mutex
|
||||
// Check if an ImHex instance is already running by opening a global mutex
|
||||
HANDLE globalMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, UniqueMutexId);
|
||||
if (globalMutex == nullptr) {
|
||||
// If no ImHex instance is running, create a new global mutex
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace hex {
|
||||
executeCmd({"zenity", "--error", "--text", message});
|
||||
} else if(isFileInPath("notify-send")) {
|
||||
executeCmd({"notify-send", "-i", "script-error", "Error", message});
|
||||
} // hopefully one of these commands is installed
|
||||
} // Hopefully one of these commands is installed
|
||||
}
|
||||
|
||||
void Window::initNative() {
|
||||
|
||||
71
main/gui/source/window/web_window.cpp
Normal file
71
main/gui/source/window/web_window.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#include "window.hpp"
|
||||
|
||||
#if defined(OS_WEB)
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
|
||||
// Function used by c++ to get the size of the html canvas
|
||||
EM_JS(int, canvas_get_width, (), {
|
||||
return Module.canvas.width;
|
||||
});
|
||||
|
||||
// Function used by c++ to get the size of the html canvas
|
||||
EM_JS(int, canvas_get_height, (), {
|
||||
return Module.canvas.height;
|
||||
});
|
||||
|
||||
// Function called by javascript
|
||||
EM_JS(void, resizeCanvas, (), {
|
||||
js_resizeCanvas();
|
||||
});
|
||||
|
||||
namespace hex {
|
||||
|
||||
void nativeErrorMessage(const std::string &message) {
|
||||
log::fatal(message);
|
||||
EM_ASM({
|
||||
alert(UTF8ToString($0));
|
||||
}, message.c_str());
|
||||
}
|
||||
|
||||
void Window::initNative() {
|
||||
EM_ASM({
|
||||
// Save data directory
|
||||
FS.mkdir("/home/web_user/.local");
|
||||
FS.mount(IDBFS, {}, '/home/web_user/.local');
|
||||
|
||||
FS.syncfs(true, function (err) {
|
||||
if (!err)
|
||||
return;
|
||||
alert("Failed to load permanent file system: "+err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void Window::setupNativeWindow() {
|
||||
resizeCanvas();
|
||||
}
|
||||
|
||||
void Window::beginNativeWindowFrame() {
|
||||
static i32 prevWidth = 0;
|
||||
static i32 prevHeight = 0;
|
||||
|
||||
auto width = canvas_get_width();
|
||||
auto height = canvas_get_height();
|
||||
|
||||
if (prevWidth != width || prevHeight != height) {
|
||||
// Size has changed
|
||||
|
||||
prevWidth = width;
|
||||
prevHeight = height;
|
||||
this->resize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
void Window::endNativeWindowFrame() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -156,6 +156,8 @@ namespace hex {
|
||||
}
|
||||
|
||||
void Window::fullFrame() {
|
||||
this->m_lastFrameTime = glfwGetTime();
|
||||
|
||||
glfwPollEvents();
|
||||
|
||||
// Render frame
|
||||
@@ -255,7 +257,7 @@ namespace hex {
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetColorU32(ImGuiCol_ScrollbarGrabActive));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetColorU32(ImGuiCol_ScrollbarGrabHovered));
|
||||
|
||||
// custom titlebar buttons implementation for borderless window mode
|
||||
// Custom titlebar buttons implementation for borderless window mode
|
||||
auto &titleBarButtons = ContentRegistry::Interface::impl::getTitleBarButtons();
|
||||
|
||||
// Draw custom title bar buttons
|
||||
@@ -812,6 +814,8 @@ namespace hex {
|
||||
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
|
||||
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);
|
||||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
|
||||
glfwWindowHint(GLFW_SAMPLES, 1);
|
||||
|
||||
if (restoreWindowPos) {
|
||||
int maximized = ContentRegistry::Settings::read("hex.builtin.setting.interface", "hex.builtin.setting.interface.window.maximized", GLFW_FALSE);
|
||||
@@ -924,30 +928,32 @@ namespace hex {
|
||||
win->processEvent();
|
||||
});
|
||||
|
||||
// Register key press callback
|
||||
glfwSetKeyCallback(this->m_window, [](GLFWwindow *window, int key, int scancode, int action, int mods) {
|
||||
hex::unused(mods);
|
||||
#if !defined(OS_WEB)
|
||||
// Register key press callback
|
||||
glfwSetKeyCallback(this->m_window, [](GLFWwindow *window, int key, int scancode, int action, int mods) {
|
||||
hex::unused(mods);
|
||||
|
||||
auto win = static_cast<Window *>(glfwGetWindowUserPointer(window));
|
||||
auto win = static_cast<Window *>(glfwGetWindowUserPointer(window));
|
||||
|
||||
if (action == GLFW_RELEASE) {
|
||||
win->m_buttonDown = false;
|
||||
} else {
|
||||
win->m_buttonDown = true;
|
||||
}
|
||||
if (action == GLFW_RELEASE) {
|
||||
win->m_buttonDown = false;
|
||||
} else {
|
||||
win->m_buttonDown = true;
|
||||
}
|
||||
|
||||
if (key == GLFW_KEY_UNKNOWN) return;
|
||||
if (key == GLFW_KEY_UNKNOWN) return;
|
||||
|
||||
auto keyName = glfwGetKeyName(key, scancode);
|
||||
if (keyName != nullptr)
|
||||
key = std::toupper(keyName[0]);
|
||||
auto keyName = glfwGetKeyName(key, scancode);
|
||||
if (keyName != nullptr)
|
||||
key = std::toupper(keyName[0]);
|
||||
|
||||
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
|
||||
win->m_pressedKeys.push_back(key);
|
||||
}
|
||||
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
|
||||
win->m_pressedKeys.push_back(key);
|
||||
}
|
||||
|
||||
win->processEvent();
|
||||
});
|
||||
win->processEvent();
|
||||
});
|
||||
#endif
|
||||
|
||||
// Register cursor position callback
|
||||
glfwSetCursorPosCallback(this->m_window, [](GLFWwindow *window, double x, double y) {
|
||||
@@ -995,6 +1001,10 @@ namespace hex {
|
||||
glfwShowWindow(this->m_window);
|
||||
}
|
||||
|
||||
void Window::resize(i32 width, i32 height) {
|
||||
glfwSetWindowSize(this->m_window, width, height);
|
||||
}
|
||||
|
||||
void Window::initImGui() {
|
||||
IMGUI_CHECKVERSION();
|
||||
|
||||
@@ -1094,10 +1104,13 @@ namespace hex {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ImGui_ImplGlfw_InitForOpenGL(this->m_window, true);
|
||||
|
||||
#if defined(OS_MACOS)
|
||||
ImGui_ImplOpenGL3_Init("#version 150");
|
||||
#elif defined(OS_WEB)
|
||||
ImGui_ImplOpenGL3_Init();
|
||||
#else
|
||||
ImGui_ImplOpenGL3_Init("#version 130");
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user