diff options
| author | hachem <im@hachem.wtf> | 2026-08-30 17:59:14 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-08-30 17:59:14 +0200 |
| commit | f099a82eb2b2a6bb64cd51d98ab027f4ce45078c (patch) | |
| tree | ac83cedd80afb500b63ff8e517f6e24939742f9c /src/ui | |
| parent | 5074ae48a95f96ec3370e464bb3c7242a3455411 (diff) | |
feat: little pepe
Diffstat (limited to 'src/ui')
| -rw-r--r-- | src/ui/animated_image.cpp | 207 | ||||
| -rw-r--r-- | src/ui/animated_image.h | 39 | ||||
| -rw-r--r-- | src/ui/image_renderer.cpp | 186 | ||||
| -rw-r--r-- | src/ui/image_renderer.h | 33 | ||||
| -rw-r--r-- | src/ui/ui.cpp | 65 | ||||
| -rw-r--r-- | src/ui/ui.h | 16 |
6 files changed, 544 insertions, 2 deletions
diff --git a/src/ui/animated_image.cpp b/src/ui/animated_image.cpp new file mode 100644 index 0000000..d7c6536 --- /dev/null +++ b/src/ui/animated_image.cpp @@ -0,0 +1,207 @@ +#include "ui/animated_image.h" +#include "core/log.h" + +#include <glad/gl.h> + +#define STB_IMAGE_IMPLEMENTATION +#define STBI_ONLY_GIF +#define STBI_ONLY_PNG +#define STBI_ONLY_JPEG +#define STBI_ONLY_BMP +#include <stb_image.h> + +#include <algorithm> +#include <cstdlib> +#include <fstream> +#include <iterator> + +namespace Hsdbg +{ + namespace + { + constexpr float MIN_FRAME_SECONDS = 0.02f; + + auto bleed_edges(unsigned char* rgba, int width, int height) -> void + { + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + unsigned char* pixel = rgba + (static_cast<std::size_t>(y) * width + x) * 4; + + if (pixel[3] != 0) + continue; + + int red = 0; + int green = 0; + int blue = 0; + int count = 0; + + for (int dy = -1; dy <= 1; ++dy) + { + for (int dx = -1; dx <= 1; ++dx) + { + const int nx = x + dx; + const int ny = y + dy; + + if (nx < 0 || ny < 0 || nx >= width || ny >= height) + continue; + + const unsigned char* near = + rgba + (static_cast<std::size_t>(ny) * width + nx) * 4; + + if (near[3] == 0) + continue; + + red += near[0]; + green += near[1]; + blue += near[2]; + ++count; + } + } + + if (count == 0) + continue; + + pixel[0] = static_cast<unsigned char>(red / count); + pixel[1] = static_cast<unsigned char>(green / count); + pixel[2] = static_cast<unsigned char>(blue / count); + } + } + } + + auto upload(const unsigned char* rgba, int width, int height) -> unsigned int + { + unsigned int texture = 0; + + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba); + glBindTexture(GL_TEXTURE_2D, 0); + + return texture; + } + + auto read_file(const std::filesystem::path& path) -> std::vector<unsigned char> + { + std::ifstream file(path, std::ios::binary); + + if (!file.is_open()) + return {}; + + return std::vector<unsigned char>(std::istreambuf_iterator<char>(file), + std::istreambuf_iterator<char>()); + } + } + + AnimatedImage::~AnimatedImage() + { + unload(); + } + + auto AnimatedImage::unload() -> void + { + if (!m_frames.empty()) + glDeleteTextures(static_cast<int>(m_frames.size()), m_frames.data()); + + m_frames.clear(); + m_delays.clear(); + m_width = 0; + m_height = 0; + m_current = 0; + m_accumulator = 0.0f; + } + + auto AnimatedImage::load(const std::filesystem::path& path) -> bool + { + unload(); + + const std::vector<unsigned char> bytes = read_file(path); + + if (bytes.empty()) + { + Log::warn("image: could not read '{}'", path.string()); + return false; + } + + const auto length = static_cast<int>(bytes.size()); + int width = 0; + int height = 0; + int comp = 0; + + int frames = 0; + int* delays = nullptr; + + if (stbi_uc* data = stbi_load_gif_from_memory(bytes.data(), length, &delays, + &width, &height, &frames, &comp, 4)) + { + m_width = width; + m_height = height; + + for (int index = 0; index < frames; ++index) + { + stbi_uc* frame = data + static_cast<std::size_t>(index) * width * height * 4; + + bleed_edges(frame, width, height); + m_frames.push_back(upload(frame, width, height)); + + const float seconds = delays != nullptr + ? static_cast<float>(delays[index]) / 1000.0f + : 0.1f; + + m_delays.push_back(std::max(seconds, MIN_FRAME_SECONDS)); + } + + stbi_image_free(data); + std::free(delays); + } + else if (stbi_uc* still = stbi_load_from_memory(bytes.data(), length, + &width, &height, &comp, 4)) + { + m_width = width; + m_height = height; + bleed_edges(still, width, height); + m_frames.push_back(upload(still, width, height)); + m_delays.push_back(MIN_FRAME_SECONDS); + stbi_image_free(still); + } + else + { + Log::warn("image: could not decode '{}': {}", path.string(), stbi_failure_reason()); + return false; + } + + Log::info("image: loaded '{}' ({}x{}, {} frame{})", + path.string(), + m_width, + m_height, + m_frames.size(), + m_frames.size() == 1 ? "" : "s"); + + return true; + } + + auto AnimatedImage::advance(float delta_seconds) -> void + { + if (m_frames.size() < 2) + return; + + m_accumulator += delta_seconds; + + for (int step = 0; step < 1000 && m_accumulator >= m_delays[m_current]; ++step) + { + m_accumulator -= m_delays[m_current]; + m_current = (m_current + 1) % m_frames.size(); + } + } + + auto AnimatedImage::texture() const -> unsigned int + { + return m_frames.empty() ? 0 : m_frames[m_current]; + } +} diff --git a/src/ui/animated_image.h b/src/ui/animated_image.h new file mode 100644 index 0000000..4987d30 --- /dev/null +++ b/src/ui/animated_image.h @@ -0,0 +1,39 @@ +#pragma once + +#include <cstddef> +#include <filesystem> +#include <vector> + +namespace Hsdbg +{ + class AnimatedImage + { + public: + AnimatedImage() = default; + ~AnimatedImage(); + + AnimatedImage(const AnimatedImage&) = delete; + AnimatedImage(AnimatedImage&&) = delete; + auto operator=(const AnimatedImage&) -> AnimatedImage& = delete; + auto operator=(AnimatedImage&&) -> AnimatedImage& = delete; + + auto load(const std::filesystem::path& path) -> bool; + auto unload() -> void; + + auto advance(float delta_seconds) -> void; + + auto valid() const -> bool { return !m_frames.empty(); } + + auto texture() const -> unsigned int; + auto width() const -> float { return static_cast<float>(m_width); } + auto height() const -> float { return static_cast<float>(m_height); } + + private: + std::vector<unsigned int> m_frames; + std::vector<float> m_delays; + int m_width = 0; + int m_height = 0; + std::size_t m_current = 0; + float m_accumulator = 0.0f; + }; +} diff --git a/src/ui/image_renderer.cpp b/src/ui/image_renderer.cpp new file mode 100644 index 0000000..8cca305 --- /dev/null +++ b/src/ui/image_renderer.cpp @@ -0,0 +1,186 @@ +#include "ui/image_renderer.h" + +#include "core/log.h" + +#include <glad/gl.h> + +#include <filesystem> +#include <fstream> +#include <iterator> +#include <string> + +#ifndef HSDBG_ASSET_DIR + #define HSDBG_ASSET_DIR "" +#endif + +namespace Hsdbg +{ + namespace + { + auto read_file(const std::filesystem::path& path) -> std::string + { + std::ifstream file(path); + + if (!file.is_open()) + { + Log::error("image_renderer: could not open '{}'", path.string()); + return {}; + } + + return std::string(std::istreambuf_iterator<char>(file), + std::istreambuf_iterator<char>()); + } + + auto compile(unsigned int type, const char* source) -> unsigned int + { + const unsigned int shader = glCreateShader(type); + glShaderSource(shader, 1, &source, nullptr); + glCompileShader(shader); + + int ok = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); + + if (ok == 0) + { + char log[512]; + glGetShaderInfoLog(shader, sizeof(log), nullptr, log); + Log::error("image_renderer: shader compile failed: {}", log); + glDeleteShader(shader); + return 0; + } + + return shader; + } + } + + ImageRenderer::~ImageRenderer() + { + shutdown(); + } + + auto ImageRenderer::init() -> bool + { + const std::filesystem::path assets(HSDBG_ASSET_DIR); + const std::string vertex_source = read_file(assets / "texture.vert"); + const std::string fragment_source = read_file(assets / "texture.glsl"); + + if (vertex_source.empty() || fragment_source.empty()) + return false; + + const unsigned int vertex = compile(GL_VERTEX_SHADER, vertex_source.c_str()); + const unsigned int fragment = compile(GL_FRAGMENT_SHADER, fragment_source.c_str()); + + if (vertex == 0 || fragment == 0) + return false; + + m_program = glCreateProgram(); + glAttachShader(m_program, vertex); + glAttachShader(m_program, fragment); + glBindAttribLocation(m_program, 0, "a_position"); + glBindAttribLocation(m_program, 1, "a_uv"); + glLinkProgram(m_program); + glDeleteShader(vertex); + glDeleteShader(fragment); + + int ok = 0; + glGetProgramiv(m_program, GL_LINK_STATUS, &ok); + + if (ok == 0) + { + char log[512]; + glGetProgramInfoLog(m_program, sizeof(log), nullptr, log); + Log::error("image_renderer: program link failed: {}", log); + glDeleteProgram(m_program); + m_program = 0; + return false; + } + + m_texture_uniform = glGetUniformLocation(m_program, "u_texture"); + + glGenVertexArrays(1, &m_vao); + glGenBuffers(1, &m_vbo); + + glBindVertexArray(m_vao); + glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 24, nullptr, GL_DYNAMIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 4, + reinterpret_cast<void*>(0)); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 4, + reinterpret_cast<void*>(sizeof(float) * 2)); + glBindVertexArray(0); + + return true; + } + + auto ImageRenderer::shutdown() -> void + { + if (m_vbo != 0) + glDeleteBuffers(1, &m_vbo); + + if (m_vao != 0) + glDeleteVertexArrays(1, &m_vao); + + if (m_program != 0) + glDeleteProgram(m_program); + + m_vbo = 0; + m_vao = 0; + m_program = 0; + } + + auto ImageRenderer::draw(unsigned int texture, + float x0, float y0, float x1, float y1, + int framebuffer_width, int framebuffer_height) -> void + { + if (m_program == 0 || texture == 0 || framebuffer_width <= 0 || framebuffer_height <= 0) + return; + + const auto to_ndc_x = [&](float pixel) { + return pixel / static_cast<float>(framebuffer_width) * 2.0f - 1.0f; + }; + const auto to_ndc_y = [&](float pixel) { + return 1.0f - pixel / static_cast<float>(framebuffer_height) * 2.0f; + }; + + const float left = to_ndc_x(x0); + const float right = to_ndc_x(x1); + const float top = to_ndc_y(y0); + const float bottom = to_ndc_y(y1); + + // two triangles; the u coordinate runs 1 -> 0 left-to-right so the image + // is mirrored horizontally, and v (0,0) stays at the top to match how the + // texture was uploaded (stb hands back the first row as the top) + const float vertices[24] = { + left, top, 1.0f, 0.0f, + left, bottom, 1.0f, 1.0f, + right, bottom, 0.0f, 1.0f, + left, top, 1.0f, 0.0f, + right, bottom, 0.0f, 1.0f, + right, top, 0.0f, 0.0f, + }; + + glViewport(0, 0, framebuffer_width, framebuffer_height); + glDisable(GL_DEPTH_TEST); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_CULL_FACE); + glEnable(GL_BLEND); + glBlendEquation(GL_FUNC_ADD); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + glUseProgram(m_program); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, texture); + glUniform1i(m_texture_uniform, 0); + + glBindVertexArray(m_vao); + glBindBuffer(GL_ARRAY_BUFFER, m_vbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + glDrawArrays(GL_TRIANGLES, 0, 6); + + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + glUseProgram(0); + } +} diff --git a/src/ui/image_renderer.h b/src/ui/image_renderer.h new file mode 100644 index 0000000..3f4c129 --- /dev/null +++ b/src/ui/image_renderer.h @@ -0,0 +1,33 @@ +#pragma once + +namespace Hsdbg +{ + class ImageRenderer + { + public: + ImageRenderer() = default; + ~ImageRenderer(); + + ImageRenderer(const ImageRenderer&) = delete; + ImageRenderer(ImageRenderer&&) = delete; + auto operator=(const ImageRenderer&) -> ImageRenderer& = delete; + auto operator=(ImageRenderer&&) -> ImageRenderer& = delete; + + auto init() -> bool; + auto shutdown() -> void; + + auto ready() const -> bool { return m_program != 0; } + + // draws the texture into the given rectangle, in framebuffer pixels with + // the origin at the top-left, blended over whatever is already there + auto draw(unsigned int texture, + float x0, float y0, float x1, float y1, + int framebuffer_width, int framebuffer_height) -> void; + + private: + unsigned int m_program = 0; + unsigned int m_vao = 0; + unsigned int m_vbo = 0; + int m_texture_uniform = -1; + }; +} diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index c152ad1..de273ff 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -18,6 +18,10 @@ #include <span> #include <vector> +#ifndef HSDBG_ASSET_DIR + #define HSDBG_ASSET_DIR "" +#endif + namespace Hsdbg { namespace @@ -443,6 +447,26 @@ namespace Hsdbg { ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + if (m_mascot_pending && m_mascot_texture != 0) + { + if (!m_image_renderer_ready) + m_image_renderer_ready = m_image_renderer.init(); + + if (m_image_renderer_ready) + { + const ImVec2 scale = ImGui::GetIO().DisplayFramebufferScale; + const int width = static_cast<int>(m_window->framebuffer_width()); + const int height = static_cast<int>(m_window->framebuffer_height()); + + m_image_renderer.draw(m_mascot_texture, + m_mascot_x0 * scale.x, m_mascot_y0 * scale.y, + m_mascot_x1 * scale.x, m_mascot_y1 * scale.y, + width, height); + } + } + + m_mascot_pending = false; } auto Ui::open_source(const std::filesystem::path& path) -> void @@ -735,10 +759,23 @@ namespace Hsdbg const bool has_target = debugger.has_target(); const bool running = debugger.is_running(); + if (!m_mascot_loaded) + { + m_mascot_loaded = true; + m_mascot.load(std::filesystem::path(HSDBG_ASSET_DIR) / "peepocry.gif"); + } + + const float button_height = ImGui::GetFrameHeight(); + const bool has_mascot = m_mascot.valid() && m_mascot.height() > 0.0f; + const float mascot_height = has_mascot ? button_height * 1.6f : 0.0f; + const float row_height = std::max(button_height, mascot_height); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 6.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, TOOLBAR_PADDING); - ImGui::BeginChild("##toolbar", - ImVec2(0.0f, ImGui::GetFrameHeight() + TOOLBAR_PADDING.y * 2.0f)); + ImGui::BeginChild("##toolbar", ImVec2(0.0f, row_height + TOOLBAR_PADDING.y * 2.0f)); + + const float row_top = ImGui::GetCursorPosY(); + ImGui::SetCursorPosY(row_top + (row_height - button_height) * 0.5f); ImGui::BeginDisabled(!has_target || running); if (ImGui::Button("run")) @@ -788,6 +825,30 @@ namespace Hsdbg ImGui::AlignTextToFramePadding(); ImGui::TextColored(state_color(debugger.state()), "%s", to_string(debugger.state()).data()); + if (has_mascot) + { + const ImVec2 size(mascot_height * (m_mascot.width() / m_mascot.height()), mascot_height); + + ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - size.x); + ImGui::SetCursorPosY(row_top); + + // reserve the spot but let imgui draw nothing here; the mascot is + // rendered by our own shader after imgui, straight to the framebuffer + const ImVec2 origin = ImGui::GetCursorScreenPos(); + ImGui::Dummy(size); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("it's going to be okay"); + + const ImVec2 viewport = ImGui::GetMainViewport()->Pos; + m_mascot_pending = true; + m_mascot_texture = m_mascot.texture(); + m_mascot_x0 = origin.x - viewport.x; + m_mascot_y0 = origin.y - viewport.y; + m_mascot_x1 = m_mascot_x0 + size.x; + m_mascot_y1 = m_mascot_y0 + size.y; + } + ImGui::EndChild(); ImGui::PopStyleVar(2); } diff --git a/src/ui/ui.h b/src/ui/ui.h index 8cc573e..edd343f 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -1,6 +1,8 @@ #pragma once #include "core/result.h" +#include "ui/animated_image.h" +#include "ui/image_renderer.h" #include "ui/source_view.h" #include <cstdint> @@ -79,6 +81,20 @@ namespace Hsdbg SourceView m_source_view; PanelVisibility m_visible; + // the crying-pepe that lives in the toolbar; loaded on the first frame + // once there is a gl context to upload its textures to, and drawn with a + // dedicated shader after imgui rather than through ImGui::Image + AnimatedImage m_mascot; + ImageRenderer m_image_renderer; + bool m_mascot_loaded = false; + bool m_image_renderer_ready = false; + bool m_mascot_pending = false; + unsigned int m_mascot_texture = 0; + float m_mascot_x0 = 0.0f; + float m_mascot_y0 = 0.0f; + float m_mascot_x1 = 0.0f; + float m_mascot_y1 = 0.0f; + std::vector<std::string> m_console_lines; std::string m_console_input; std::string m_target_input; |
