aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/animated_image.cpp207
-rw-r--r--src/ui/animated_image.h39
-rw-r--r--src/ui/image_renderer.cpp186
-rw-r--r--src/ui/image_renderer.h33
-rw-r--r--src/ui/ui.cpp65
-rw-r--r--src/ui/ui.h16
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;