tedit/main.cpp
2025-04-08 18:09:59 -04:00

2280 lines
103 KiB
C++

// Dear ImGui: standalone example application for SDL2 + OpenGL
// (SDL is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan/Metal graphics context creation, etc.)
// Learn about Dear ImGui:
// - FAQ https://dearimgui.com/faq
// - Getting Started https://dearimgui.com/getting-started
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
// - Introduction, links and more at the top of imgui.cpp
#define IMGUI_DEFINE_MATH_OPERATORS
#include <GL/glew.h>
#include "imgui.h"
#include "imgui_impl_sdl2.h"
#include "imgui_impl_opengl3.h"
#include <stdio.h>
#include <SDL.h>
#if defined(IMGUI_IMPL_OPENGL_ES2)
#include <SDL_opengles2.h>
#else
#include <SDL_opengl.h>
#include <GL/gl.h>
#endif
// This example can also compile and run with Emscripten! See 'Makefile.emscripten' for details.
#ifdef __EMSCRIPTEN__
#include "../libs/emscripten/emscripten_mainloop_stub.h"
#endif
#include "exif.h"
#define APP_IMAGE_IMPLEMENTATION
#define IMGUI_IMAGE_VIEWER_IMPLEMENTATION
#include "app_image.h"
#include "tex_inspect_opengl.h"
#include "imgui_tex_inspect.h"
#include "shaderutils.h"
static float exposure = 0.0f;
static float contrast = 0.0f;
static float highlights = 0.0f;
static float shadows = 0.0f;
static float whites = 0.0f;
static float blacks = 0.0f;
static float temperature = 6500.0f; // Example starting point (Kelvin)
static float tint = 0.0f;
static float vibrance = 0.0f;
static float saturation = 0.0f;
static float clarity = 0.0f;
static float texture = 0.0f;
static float dehaze = 0.0f;
#include <string>
#include <vector>
#include <map>
#include <functional> // For std::function
#include <memory> // For unique_ptr
#include "imfilebrowser.h" // <<< Add this
#include <filesystem> // <<< Add for path manipulation (C++17)
struct ShaderUniform
{
std::string name;
GLint location = -1;
// Add type info if needed for different glUniform calls, or handle in setter
};
struct PipelineOperation
{
std::string name;
GLuint shaderProgram = 0;
bool enabled = true;
std::map<std::string, ShaderUniform> uniforms; // Map uniform name to its info
// Function to update uniforms based on global slider values etc.
std::function<void(GLuint /*program*/)> updateUniformsCallback;
// Store the actual slider variable pointers for direct modification in ImGui
// This avoids needing complex callbacks for simple sliders
float *exposureVal = nullptr;
float *contrastVal = nullptr;
float *highlightsVal = nullptr;
float *shadowsVal = nullptr;
float *whitesVal = nullptr;
float *blacksVal = nullptr;
float *temperatureVal = nullptr;
float *tintVal = nullptr;
float *vibranceVal = nullptr;
float *saturationVal = nullptr;
float *clarityVal = nullptr;
float *textureVal = nullptr;
float *dehazeVal = nullptr;
// ... add pointers for other controls as needed
PipelineOperation(std::string n) : name(std::move(n)) {}
void FindUniformLocations()
{
if (!shaderProgram)
return;
for (auto &pair : uniforms)
{
pair.second.location = glGetUniformLocation(shaderProgram, pair.second.name.c_str());
if (pair.second.location == -1 && name != "Passthrough" && name != "LinearToSRGB" && name != "SRGBToLinear")
{ // Ignore for simple shaders
// Don't treat missing texture samplers as errors here, they are set explicitly
if (pair.second.name != "InputTexture")
{
fprintf(stderr, "Warning: Uniform '%s' not found in shader '%s'\n", pair.second.name.c_str(), name.c_str());
}
}
}
}
};
// Enum for Color Spaces (expand later)
enum class ColorSpace
{
LINEAR_SRGB, // Linear Rec.709/sRGB primaries
SRGB // Non-linear sRGB (display)
// Add AdobeRGB, ProPhoto etc. later
};
const char *ColorSpaceToString(ColorSpace cs)
{
switch (cs)
{
case ColorSpace::LINEAR_SRGB:
return "Linear sRGB";
case ColorSpace::SRGB:
return "sRGB";
default:
return "Unknown";
}
}
bool ReadTextureToAppImage(GLuint textureId, int width, int height, AppImage &outImage)
{
if (textureId == 0 || width <= 0 || height <= 0)
{
fprintf(stderr, "ReadTextureToAppImage: Invalid parameters.\n");
return false;
}
// We assume the texture 'textureId' holds LINEAR RGBA FLOAT data (e.g., GL_RGBA16F)
// Resize AppImage to hold the data
outImage.resize(width, height, 4); // Expecting 4 channels (RGBA) from pipeline texture
outImage.m_isLinear = true; // Data we read back should be linear
outImage.m_colorSpaceName = "Linear sRGB"; // Assuming pipeline used sRGB primaries
std::vector<float> &pixelData = outImage.getPixelVector();
if (pixelData.empty())
{
fprintf(stderr, "ReadTextureToAppImage: Failed to allocate AppImage buffer.\n");
return false;
}
// Bind the texture
GLint lastTexture;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture);
glBindTexture(GL_TEXTURE_2D, textureId);
// Set alignment (good practice)
glPixelStorei(GL_PACK_ALIGNMENT, 1);
// Read the pixels
// We request GL_RGBA and GL_FLOAT as that's our assumed linear working format on GPU
glGetTexImage(GL_TEXTURE_2D,
0, // Mipmap level 0
GL_RGBA, // Request RGBA format
GL_FLOAT, // Request float data type
pixelData.data()); // Pointer to destination buffer
GLenum err = glGetError();
glBindTexture(GL_TEXTURE_2D, lastTexture); // Restore previous binding
if (err != GL_NO_ERROR)
{
fprintf(stderr, "ReadTextureToAppImage: OpenGL Error during glGetTexImage: %u\n", err);
outImage.clear_image(); // Clear invalid data
return false;
}
printf("ReadTextureToAppImage: Successfully read %dx%d texture.\n", width, height);
return true;
}
class ImageProcessingPipeline
{
private:
GLuint m_fbo[2] = {0, 0};
GLuint m_tex[2] = {0, 0}; // Ping-pong textures
GLuint m_vao = 0;
GLuint m_vbo = 0;
int m_texWidth = 0;
int m_texHeight = 0;
GLuint m_passthroughShader = 0;
GLuint m_linearToSrgbShader = 0;
GLuint m_srgbToLinearShader = 0;
void CreateFullscreenQuad()
{
// Simple quad covering -1 to 1 in x,y and 0 to 1 in u,v
float vertices[] = {
// positions // texCoords
-1.0f, 1.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, -1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
1.0f, -1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f};
printf("Matrix ready.\n");
glGenVertexArrays(1, &m_vao);
printf("Fullscreen quad VAO created.\n");
glGenBuffers(1, &m_vbo);
printf("Fullscreen quad VBO created.\n");
glBindVertexArray(m_vao);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
printf("Fullscreen quad VBO created.\n");
// Position attribute
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// Texture coordinate attribute
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)(2 * sizeof(float)));
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
printf("Fullscreen quad VAO/VBO created.\n");
}
void CreateOrResizeFBOs(int width, int height)
{
if (width == m_texWidth && height == m_texHeight && m_fbo[0] != 0)
{
return; // Already correct size
}
if (width <= 0 || height <= 0)
return; // Invalid dimensions
// Cleanup existing
DestroyFBOs();
m_texWidth = width;
m_texHeight = height;
glGenFramebuffers(2, m_fbo);
glGenTextures(2, m_tex);
GLint lastTexture;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture);
GLint lastFBO;
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &lastFBO); // Or GL_FRAMEBUFFER_BINDING
for (int i = 0; i < 2; ++i)
{
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[i]);
glBindTexture(GL_TEXTURE_2D, m_tex[i]);
// Create floating point texture
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // Use NEAREST for processing steps
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// Attach texture to FBO
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_tex[i], 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
fprintf(stderr, "ERROR::FRAMEBUFFER:: Framebuffer %d is not complete!\n", i);
DestroyFBOs(); // Clean up partial setup
glBindTexture(GL_TEXTURE_2D, lastTexture);
glBindFramebuffer(GL_FRAMEBUFFER, lastFBO);
return;
}
else
{
printf("FBO %d (Texture %d) created successfully (%dx%d).\n", m_fbo[i], m_tex[i], width, height);
}
}
glBindTexture(GL_TEXTURE_2D, lastTexture);
glBindFramebuffer(GL_FRAMEBUFFER, lastFBO);
}
void DestroyFBOs()
{
if (m_fbo[0])
glDeleteFramebuffers(2, m_fbo);
if (m_tex[0])
glDeleteTextures(2, m_tex);
m_fbo[0] = m_fbo[1] = 0;
m_tex[0] = m_tex[1] = 0;
m_texWidth = m_texHeight = 0;
printf("Destroyed FBOs and textures.\n");
}
public:
// The ordered list of operations the user has configured
std::vector<PipelineOperation> activeOperations;
ColorSpace inputColorSpace = ColorSpace::LINEAR_SRGB; // Default based on AppImage goal
ColorSpace outputColorSpace = ColorSpace::SRGB; // Default for display
ImageProcessingPipeline() = default;
~ImageProcessingPipeline()
{
DestroyFBOs();
if (m_vao)
glDeleteVertexArrays(1, &m_vao);
if (m_vbo)
glDeleteBuffers(1, &m_vbo);
// Shaders owned by PipelineOperation structs should be deleted externally or via smart pointers
if (m_passthroughShader)
glDeleteProgram(m_passthroughShader);
if (m_linearToSrgbShader)
glDeleteProgram(m_linearToSrgbShader);
if (m_srgbToLinearShader)
glDeleteProgram(m_srgbToLinearShader);
printf("ImageProcessingPipeline destroyed.\n");
}
void Init(const std::string &shaderBasePath)
{
printf("Initializing ImageProcessingPipeline...\n");
CreateFullscreenQuad();
printf("Fullscreen quad created.\n");
// Load essential shaders
std::string vsPath = shaderBasePath + "passthrough.vert";
printf("Loading shaders from: %s\n", vsPath.c_str());
m_passthroughShader = LoadShaderProgramFromFiles(vsPath, shaderBasePath + "passthrough.frag");
m_linearToSrgbShader = LoadShaderProgramFromFiles(vsPath, shaderBasePath + "linear_to_srgb.frag");
m_srgbToLinearShader = LoadShaderProgramFromFiles(vsPath, shaderBasePath + "srgb_to_linear.frag");
printf("Loaded shaders: %s, %s, %s\n", vsPath.c_str(), (shaderBasePath + "linear_to_srgb.frag").c_str(), (shaderBasePath + "srgb_to_linear.frag").c_str());
if (!m_passthroughShader || !m_linearToSrgbShader || !m_srgbToLinearShader)
{
fprintf(stderr, "Failed to load essential pipeline shaders!\n");
}
else
{
printf("Essential pipeline shaders loaded.\n");
}
}
void ResetResources()
{
printf("Pipeline: Resetting FBOs and Textures.\n");
DestroyFBOs(); // Call the existing cleanup method
}
// Call this each frame to process the image
// Returns the Texture ID of the final processed image
GLuint ProcessImage(GLuint inputTextureId, int width, int height, bool applyOutputConversion = true)
{
if (inputTextureId == 0 || width <= 0 || height <= 0)
{
return 0; // No input or invalid size
}
CreateOrResizeFBOs(width, height);
if (m_fbo[0] == 0)
{
fprintf(stderr, "FBOs not ready, cannot process image.\n");
return 0; // FBOs not ready
}
// Store original viewport and FBO to restore later
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
GLint lastFBO;
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &lastFBO);
glViewport(0, 0, m_texWidth, m_texHeight);
glBindVertexArray(m_vao); // Bind the quad VAO once
int currentSourceTexIndex = 0; // Start with texture m_tex[0] as the first *write* target
GLuint currentReadTexId = inputTextureId; // Initially read from the original image
// --- Input Color Space Conversion ---
bool inputConversionDone = false;
if (inputColorSpace == ColorSpace::SRGB)
{
printf("Pipeline: Applying sRGB -> Linear conversion.\n");
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]);
glUseProgram(m_srgbToLinearShader);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_1D, currentReadTexId);
glUniform1i(glGetUniformLocation(m_srgbToLinearShader, "InputTexture"), 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
currentReadTexId = m_tex[currentSourceTexIndex]; // Next read is from the texture we just wrote to
currentSourceTexIndex = 1 - currentSourceTexIndex; // Swap target FBO/texture
inputConversionDone = true;
}
else
{
printf("Pipeline: Input is Linear, no conversion needed.\n");
// If input is already linear, we might need to copy it to the first FBO texture
// if there are actual processing steps, otherwise the first step reads the original.
// This copy ensures the ping-pong works correctly even if the first *user* step is disabled.
// However, if NO user steps are enabled, we want to display the original (potentially with output conversion).
bool anyUserOpsEnabled = false;
for (const auto &op : activeOperations)
{
if (op.enabled && op.shaderProgram && op.name != "Passthrough")
{ // Check it's a real operation
anyUserOpsEnabled = true;
break;
}
}
if (anyUserOpsEnabled)
{
// Need to copy original linear input into the pipeline's texture space
printf("Pipeline: Copying linear input to FBO texture for processing.\n");
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]);
glUseProgram(m_passthroughShader); // Use simple passthrough
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, currentReadTexId);
glUniform1i(glGetUniformLocation(m_passthroughShader, "InputTexture"), 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
currentReadTexId = m_tex[currentSourceTexIndex];
currentSourceTexIndex = 1 - currentSourceTexIndex;
inputConversionDone = true;
}
else
{
// No user ops, keep reading directly from original inputTextureId
inputConversionDone = false; // Treat as if no initial step happened yet
printf("Pipeline: No enabled user operations, skipping initial copy.\n");
}
}
// --- Apply Editing Operations ---
int appliedOps = 0;
for (const auto &op : activeOperations)
{
if (op.enabled && op.shaderProgram)
{
printf("Pipeline: Applying operation: %s\n", op.name.c_str());
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]);
glUseProgram(op.shaderProgram);
// Set Input Texture Sampler
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, currentReadTexId);
GLint loc = glGetUniformLocation(op.shaderProgram, "InputTexture");
if (loc != -1)
glUniform1i(loc, 0);
else if (op.name != "Passthrough")
fprintf(stderr, "Warning: InputTexture uniform not found in shader %s\n", op.name.c_str());
// Set operation-specific uniforms
if (op.updateUniformsCallback)
{
op.updateUniformsCallback(op.shaderProgram);
}
else
{
// Alternative: Set uniforms directly based on stored pointers
if (op.exposureVal && op.uniforms.count("exposureValue"))
{
glUniform1f(op.uniforms.at("exposureValue").location, *op.exposureVal);
}
if (op.contrastVal && op.uniforms.count("contrastValue"))
{
glUniform1f(op.uniforms.at("contrastValue").location, *op.contrastVal);
}
if (op.clarityVal && op.uniforms.count("clarityValue"))
{
glUniform1f(op.uniforms.at("clarityValue").location, *op.clarityVal);
}
if (op.highlightsVal && op.uniforms.count("highlightsValue"))
{
glUniform1f(op.uniforms.at("highlightsValue").location, *op.highlightsVal);
}
if (op.shadowsVal && op.uniforms.count("shadowsValue"))
{
glUniform1f(op.uniforms.at("shadowsValue").location, *op.shadowsVal);
}
if (op.whitesVal && op.uniforms.count("whitesValue"))
{
glUniform1f(op.uniforms.at("whitesValue").location, *op.whitesVal);
}
if (op.blacksVal && op.uniforms.count("blacksValue"))
{
glUniform1f(op.uniforms.at("blacksValue").location, *op.blacksVal);
}
if (op.textureVal && op.uniforms.count("textureValue"))
{
glUniform1f(op.uniforms.at("textureValue").location, *op.textureVal);
}
if (op.dehazeVal && op.uniforms.count("dehazeValue"))
{
glUniform1f(op.uniforms.at("dehazeValue").location, *op.dehazeVal);
}
if (op.saturationVal && op.uniforms.count("saturationValue"))
{
glUniform1f(op.uniforms.at("saturationValue").location, *op.saturationVal);
}
if (op.vibranceVal && op.uniforms.count("vibranceValue"))
{
glUniform1f(op.uniforms.at("vibranceValue").location, *op.vibranceVal);
}
if (op.temperatureVal && op.uniforms.count("temperatureValue"))
{
glUniform1f(op.uniforms.at("temperatureValue").location, *op.temperatureVal);
}
if (op.tintVal && op.uniforms.count("tintValue"))
{
glUniform1f(op.uniforms.at("tintValue").location, *op.tintVal);
}
}
glDrawArrays(GL_TRIANGLES, 0, 6);
// Prepare for next pass
currentReadTexId = m_tex[currentSourceTexIndex]; // Next pass reads from the texture we just wrote
currentSourceTexIndex = 1 - currentSourceTexIndex; // Swap FBO target
appliedOps++;
}
}
// If no user ops were applied AND no input conversion happened,
// currentReadTexId is still the original inputTextureId.
if (appliedOps == 0 && !inputConversionDone)
{
printf("Pipeline: No operations applied, output = input (%d).\n", currentReadTexId);
// Proceed to output conversion using original inputTextureId
}
else if (appliedOps > 0 || inputConversionDone)
{
printf("Pipeline: %d operations applied, final intermediate texture ID: %d\n", appliedOps, currentReadTexId);
// currentReadTexId now holds the result of the last applied operation (or the input conversion)
}
else
{
// This case should ideally not be reached if logic above is correct
printf("Pipeline: Inconsistent state after processing loop.\n");
}
// --- Output Color Space Conversion ---
GLuint finalTextureId = currentReadTexId; // Assume this is the final one unless converted
if (applyOutputConversion)
{
if (outputColorSpace == ColorSpace::SRGB)
{
// Check if the last written data (currentReadTexId) is already sRGB.
// In this simple setup, it's always linear *unless* no ops applied and input was sRGB.
// More robustly: Track the color space through the pipeline.
// For now, assume currentReadTexId holds linear data if any op or input conversion happened.
bool needsLinearToSrgb = (appliedOps > 0 || inputConversionDone);
if (!needsLinearToSrgb && inputColorSpace == ColorSpace::SRGB)
{
printf("Pipeline: Output is sRGB, and input was sRGB with no ops, no final conversion needed.\n");
// Input was sRGB, no ops applied, output should be sRGB. currentReadTexId is original sRGB input.
finalTextureId = currentReadTexId;
}
else if (needsLinearToSrgb)
{
printf("Pipeline: Applying Linear -> sRGB conversion for output.\n");
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); // Use the *next* FBO for the final write
glUseProgram(m_linearToSrgbShader);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, currentReadTexId); // Read the last result
glUniform1i(glGetUniformLocation(m_linearToSrgbShader, "InputTexture"), 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
finalTextureId = m_tex[currentSourceTexIndex]; // The final result is in this texture
}
else
{
// Input was linear, no ops, output requires sRGB.
printf("Pipeline: Input Linear, no ops, applying Linear -> sRGB conversion for output.\n");
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]);
glUseProgram(m_linearToSrgbShader);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, currentReadTexId); // Read original linear input
glUniform1i(glGetUniformLocation(m_linearToSrgbShader, "InputTexture"), 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
finalTextureId = m_tex[currentSourceTexIndex];
}
}
else
{
printf("Pipeline: Output is Linear, no final conversion needed.\n");
// If output should be linear, finalTextureId is already correct (it's currentReadTexId)
finalTextureId = currentReadTexId;
}
}
else
{
printf("Pipeline: Skipped output conversion. Final (linear) ID: %d\n", finalTextureId);
}
// --- Cleanup ---
glBindVertexArray(0);
glBindFramebuffer(GL_FRAMEBUFFER, lastFBO); // Restore original framebuffer binding
glViewport(viewport[0], viewport[1], viewport[2], viewport[3]); // Restore viewport
glUseProgram(0); // Unbind shader program
printf("Pipeline: ProcessImage returning final texture ID: %d\n", finalTextureId);
return finalTextureId;
}
};
static ImageProcessingPipeline g_pipeline; // <<< Global pipeline manager instance
static std::vector<std::unique_ptr<PipelineOperation>> g_allOperations; // Store all possible operations
static GLuint g_processedTextureId = 0; // Texture ID after pipeline processing
static ColorSpace g_inputColorSpace = ColorSpace::LINEAR_SRGB; // Connect to pipeline's setting
static ColorSpace g_outputColorSpace = ColorSpace::SRGB; // Connect to pipeline's setting
// File Dialogs
static ImGui::FileBrowser g_openFileDialog;
// Add flags for save dialog: Allow new filename, allow creating directories
static ImGui::FileBrowser g_exportSaveFileDialog(ImGuiFileBrowserFlags_EnterNewFilename | ImGuiFileBrowserFlags_CreateNewDir);
// Export Dialog State
static bool g_showExportWindow = false;
static ImageSaveFormat g_exportFormat = ImageSaveFormat::JPEG; // Default format
static int g_exportQuality = 90; // Default JPEG quality
static std::string g_exportErrorMsg = ""; // To display errors in the export dialog
// Current loaded file path (useful for default export name)
static std::string g_currentFilePath = "";
// Crop State
static bool g_cropActive = false;
static ImVec4 g_cropRectNorm = ImVec4(0.0f, 0.0f, 1.0f, 1.0f); // (MinX, MinY, MaxX, MaxY) normalized 0-1
static ImVec4 g_cropRectNormInitial = g_cropRectNorm; // Store initial state for cancel/dragging base
static float g_cropAspectRatio = 0.0f; // 0.0f = Freeform, > 0.0f = constrained (Width / Height)
static int g_selectedAspectRatioIndex = 0; // Index for the dropdown
static GLuint g_histogramComputeShader = 0;
static GLuint g_histogramSSBO = 0;
const int NUM_HISTOGRAM_BINS = 256;
const int HISTOGRAM_BUFFER_SIZE = NUM_HISTOGRAM_BINS * 3; // R, G, B
static std::vector<unsigned int> g_histogramDataCPU(HISTOGRAM_BUFFER_SIZE, 0);
static unsigned int g_histogramMaxCount = 255; // Max count found, for scaling (init to 1 to avoid div by zero)
static bool g_histogramResourcesInitialized = false;
// Interaction state
enum class CropHandle
{
NONE,
TOP_LEFT,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM_RIGHT,
TOP,
BOTTOM,
LEFT,
RIGHT,
INSIDE
};
static CropHandle g_activeCropHandle = CropHandle::NONE;
static bool g_isDraggingCrop = false;
static ImVec2 g_dragStartMousePos = ImVec2(0, 0); // Screen coords
bool InitHistogramResources(const std::string& shaderBasePath) {
printf("Initializing Histogram Resources...\n");
// Load Compute Shader
// We need a way to load compute shaders, modify shader_utils or add here
std::string compSource = ReadFile(shaderBasePath + "histogram.comp"); // Assuming ReadFile exists
if (compSource.empty()) {
fprintf(stderr, "ERROR: Failed to read histogram.comp\n");
return false;
}
// Simple Compute Shader Compilation/Linking (add error checking!)
GLuint computeShaderObj = glCreateShader(GL_COMPUTE_SHADER);
const char* src = compSource.c_str();
glShaderSource(computeShaderObj, 1, &src, nullptr);
glCompileShader(computeShaderObj);
// --- Add GLint success; glGetShaderiv; glGetShaderInfoLog checks ---
GLint success;
glGetShaderiv(computeShaderObj, GL_COMPILE_STATUS, &success);
if (!success) {
GLint logLength;
glGetShaderiv(computeShaderObj, GL_INFO_LOG_LENGTH, &logLength);
std::vector<char> log(logLength);
glGetShaderInfoLog(computeShaderObj, logLength, nullptr, log.data());
fprintf(stderr, "ERROR::SHADER::HISTOGRAM::COMPILATION_FAILED\n%s\n", log.data());
glDeleteShader(computeShaderObj);
return false;
}
g_histogramComputeShader = glCreateProgram();
glAttachShader(g_histogramComputeShader, computeShaderObj);
glLinkProgram(g_histogramComputeShader);
// --- Add GLint success; glGetProgramiv; glGetProgramInfoLog checks ---
glGetProgramiv(g_histogramComputeShader, GL_LINK_STATUS, &success);
if (!success) {
GLint logLength;
glGetProgramiv(g_histogramComputeShader, GL_INFO_LOG_LENGTH, &logLength);
std::vector<char> log(logLength);
glGetProgramInfoLog(g_histogramComputeShader, logLength, nullptr, log.data());
fprintf(stderr, "ERROR::PROGRAM::HISTOGRAM::LINKING_FAILED\n%s\n", log.data());
glDeleteProgram(g_histogramComputeShader);
g_histogramComputeShader = 0;
glDeleteShader(computeShaderObj); // Delete shader obj even on link failure
return false;
}
glDeleteShader(computeShaderObj); // Delete shader object after linking
printf("Histogram compute shader loaded and linked successfully (Program ID: %u).\n", g_histogramComputeShader);
// Create Shader Storage Buffer Object (SSBO)
glGenBuffers(1, &g_histogramSSBO);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, g_histogramSSBO);
// Allocate buffer size: 3 channels * 256 bins * size of uint
glBufferData(GL_SHADER_STORAGE_BUFFER, HISTOGRAM_BUFFER_SIZE * sizeof(unsigned int), NULL, GL_DYNAMIC_READ); // Data will be written by GPU, read by CPU
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); // Unbind
GLenum err = glGetError();
if (err != GL_NO_ERROR || g_histogramSSBO == 0) {
fprintf(stderr, "ERROR: Failed to create histogram SSBO. OpenGL Error: %u\n", err);
if (g_histogramComputeShader) glDeleteProgram(g_histogramComputeShader);
g_histogramComputeShader = 0;
return false;
} else {
printf("Histogram SSBO created successfully (Buffer ID: %u, Size: %d bytes).\n", g_histogramSSBO, HISTOGRAM_BUFFER_SIZE * sizeof(unsigned int));
}
g_histogramResourcesInitialized = true;
return true;
}
// Aspect Ratio Options
struct AspectRatioOption
{
const char *name;
float ratio; // W/H
};
static std::vector<AspectRatioOption> g_aspectRatios = {
{"Freeform", 0.0f},
{"Original", 0.0f}, // Will be calculated dynamically
{"1:1", 1.0f},
{"16:9", 16.0f / 9.0f},
{"9:16", 9.0f / 16.0f},
{"4:3", 4.0f / 3.0f},
{"3:4", 3.0f / 4.0f},
// Add more as needed
};
void UpdateCropRect(ImVec4& rectNorm, CropHandle handle, ImVec2 deltaNorm, float aspectRatio) {
ImVec2 minXY = ImVec2(rectNorm.x, rectNorm.y);
ImVec2 maxXY = ImVec2(rectNorm.z, rectNorm.w);
// Apply delta based on handle
switch (handle) {
case CropHandle::TOP_LEFT: minXY += deltaNorm; break;
case CropHandle::TOP_RIGHT: minXY.y += deltaNorm.y; maxXY.x += deltaNorm.x; break;
case CropHandle::BOTTOM_LEFT: minXY.x += deltaNorm.x; maxXY.y += deltaNorm.y; break;
case CropHandle::BOTTOM_RIGHT: maxXY += deltaNorm; break;
case CropHandle::TOP: minXY.y += deltaNorm.y; break;
case CropHandle::BOTTOM: maxXY.y += deltaNorm.y; break;
case CropHandle::LEFT: minXY.x += deltaNorm.x; break;
case CropHandle::RIGHT: maxXY.x += deltaNorm.x; break;
case CropHandle::INSIDE: minXY += deltaNorm; maxXY += deltaNorm; break;
case CropHandle::NONE: return; // No change
}
// Ensure min < max temporarily before aspect constraint
if (minXY.x > maxXY.x) ImSwap(minXY.x, maxXY.x);
if (minXY.y > maxXY.y) ImSwap(minXY.y, maxXY.y);
// Apply Aspect Ratio Constraint (if aspectRatio > 0)
if (aspectRatio > 0.0f && handle != CropHandle::INSIDE && handle != CropHandle::NONE)
{
float currentW = maxXY.x - minXY.x;
float currentH = maxXY.y - minXY.y;
if (currentW < 1e-5f) currentW = 1e-5f; // Avoid division by zero
if (currentH < 1e-5f) currentH = 1e-5f;
float currentAspect = currentW / currentH;
float targetAspect = aspectRatio;
// Determine which dimension to adjust based on which handle was moved and aspect delta
// Simplified approach: Adjust height based on width, unless moving top/bottom handles primarily
bool adjustHeight = true;
if (handle == CropHandle::TOP || handle == CropHandle::BOTTOM) {
adjustHeight = false; // Primarily adjust width based on height change
}
if (adjustHeight) { // Adjust height based on width
float targetH = currentW / targetAspect;
float deltaH = targetH - currentH;
// Distribute height change based on handle
if (handle == CropHandle::TOP_LEFT || handle == CropHandle::TOP_RIGHT || handle == CropHandle::TOP) {
minXY.y -= deltaH; // Adjust top edge
} else {
maxXY.y += deltaH; // Adjust bottom edge (or split for side handles?)
// For LEFT/RIGHT handles, could split deltaH: minXY.y -= deltaH*0.5; maxXY.y += deltaH*0.5;
}
} else { // Adjust width based on height
float targetW = currentH * targetAspect;
float deltaW = targetW - currentW;
// Distribute width change based on handle
if (handle == CropHandle::TOP_LEFT || handle == CropHandle::BOTTOM_LEFT || handle == CropHandle::LEFT) {
minXY.x -= deltaW; // Adjust left edge
} else {
maxXY.x += deltaW; // Adjust right edge
// For TOP/BOTTOM handles, could split deltaW: minXY.x -= deltaW*0.5; maxXY.x += deltaW*0.5;
}
}
} // End aspect ratio constraint
// Update the output rectNorm
rectNorm = ImVec4(minXY.x, minXY.y, maxXY.x, maxXY.y);
}
// Helper function to crop AppImage data
bool ApplyCropToImage(AppImage& image, const ImVec4 cropRectNorm) {
if (image.isEmpty()) {
fprintf(stderr, "ApplyCropToImage: Input image is empty.\n");
return false;
}
if (cropRectNorm.x >= cropRectNorm.z || cropRectNorm.y >= cropRectNorm.w) {
fprintf(stderr, "ApplyCropToImage: Invalid crop rectangle (zero or negative size).\n");
return false; // Invalid crop rect
}
// Clamp rect just in case
ImVec4 clampedRect = cropRectNorm;
clampedRect.x = ImClamp(clampedRect.x, 0.0f, 1.0f);
clampedRect.y = ImClamp(clampedRect.y, 0.0f, 1.0f);
clampedRect.z = ImClamp(clampedRect.z, 0.0f, 1.0f);
clampedRect.w = ImClamp(clampedRect.w, 0.0f, 1.0f);
// Calculate pixel coordinates
int srcW = image.getWidth();
int srcH = image.getHeight();
int channels = image.getChannels();
int cropX_px = static_cast<int>(round(clampedRect.x * srcW));
int cropY_px = static_cast<int>(round(clampedRect.y * srcH));
int cropMaxX_px = static_cast<int>(round(clampedRect.z * srcW));
int cropMaxY_px = static_cast<int>(round(clampedRect.w * srcH));
int cropW_px = cropMaxX_px - cropX_px;
int cropH_px = cropMaxY_px - cropY_px;
if (cropW_px <= 0 || cropH_px <= 0) {
fprintf(stderr, "ApplyCropToImage: Resulting crop size is zero or negative (%dx%d).\n", cropW_px, cropH_px);
return false;
}
printf("Applying crop: Start=(%d,%d), Size=(%dx%d)\n", cropX_px, cropY_px, cropW_px, cropH_px);
// Create new image for cropped data
AppImage croppedImage(cropW_px, cropH_px, channels);
if (croppedImage.isEmpty()) {
fprintf(stderr, "ApplyCropToImage: Failed to allocate memory for cropped image.\n");
return false;
}
croppedImage.m_isLinear = image.isLinear(); // Preserve flags
croppedImage.m_colorSpaceName = image.getColorSpaceName();
// TODO: Copy metadata/ICC profile if needed? Cropping usually invalidates some metadata.
const float* srcData = image.getData();
float* dstData = croppedImage.getData();
// Copy pixel data row by row, channel by channel
for (int y_dst = 0; y_dst < cropH_px; ++y_dst) {
int y_src = cropY_px + y_dst;
// Ensure source Y is valid (should be due to clamping/checks, but be safe)
if (y_src < 0 || y_src >= srcH) continue;
// Calculate start pointers for source and destination rows
const float* srcRowStart = srcData + (static_cast<size_t>(y_src) * srcW + cropX_px) * channels;
float* dstRowStart = dstData + (static_cast<size_t>(y_dst) * cropW_px) * channels;
// Copy the entire row (width * channels floats)
std::memcpy(dstRowStart, srcRowStart, static_cast<size_t>(cropW_px) * channels * sizeof(float));
}
// Replace the original image data with the cropped data
// Use std::move if AppImage supports move assignment for efficiency
image = std::move(croppedImage);
printf("Cropped image created successfully (%dx%d).\n", image.getWidth(), image.getHeight());
return true;
}
void InitShaderOperations(const std::string &shaderBasePath)
{
// Clear existing (if any)
g_allOperations.clear();
g_pipeline.activeOperations.clear(); // Also clear the active list in the pipeline
// --- Define Operations ---
// Use unique_ptr for automatic memory management
// Match uniform names to the GLSL shaders
auto whiteBalanceOp = std::make_unique<PipelineOperation>("White Balance");
whiteBalanceOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "white_balance.frag");
if (whiteBalanceOp->shaderProgram)
{
whiteBalanceOp->uniforms["temperatureValue"] = {"temperature"};
whiteBalanceOp->uniforms["tintValue"] = {"tint"};
whiteBalanceOp->temperatureVal = &temperature;
whiteBalanceOp->tintVal = &tint;
whiteBalanceOp->FindUniformLocations();
g_allOperations.push_back(std::move(whiteBalanceOp));
printf(" + Loaded White Balance\n");
}
else
printf(" - FAILED White Balance\n");
auto exposureOp = std::make_unique<PipelineOperation>("Exposure");
exposureOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "exposure.frag");
exposureOp->uniforms["exposureValue"] = {"exposureValue"};
exposureOp->exposureVal = &exposure; // Link to global slider variable
exposureOp->FindUniformLocations();
g_allOperations.push_back(std::move(exposureOp));
auto contrastOp = std::make_unique<PipelineOperation>("Contrast");
contrastOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "contrast.frag");
if (contrastOp->shaderProgram)
{
contrastOp->uniforms["contrastValue"] = {"contrastValue"};
contrastOp->contrastVal = &contrast;
contrastOp->FindUniformLocations();
g_allOperations.push_back(std::move(contrastOp));
printf(" + Loaded Contrast\n");
}
else
printf(" - FAILED Contrast\n");
auto highlightsShadowsOp = std::make_unique<PipelineOperation>("Highlights/Shadows");
highlightsShadowsOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "highlights_shadows.frag");
if (highlightsShadowsOp->shaderProgram)
{
highlightsShadowsOp->uniforms["highlightsValue"] = {"highlightsValue"};
highlightsShadowsOp->uniforms["shadowsValue"] = {"shadowsValue"};
highlightsShadowsOp->highlightsVal = &highlights;
highlightsShadowsOp->shadowsVal = &shadows;
highlightsShadowsOp->FindUniformLocations();
g_allOperations.push_back(std::move(highlightsShadowsOp));
printf(" + Loaded Highlights/Shadows\n");
}
else
printf(" - FAILED Highlights/Shadows\n");
auto whiteBlackOp = std::make_unique<PipelineOperation>("Whites/Blacks");
whiteBlackOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "whites_blacks.frag");
if (whiteBlackOp->shaderProgram)
{
whiteBlackOp->uniforms["whitesValue"] = {"whitesValue"};
whiteBlackOp->uniforms["blacksValue"] = {"blacksValue"};
whiteBlackOp->whitesVal = &whites;
whiteBlackOp->blacksVal = &blacks;
whiteBlackOp->FindUniformLocations();
g_allOperations.push_back(std::move(whiteBlackOp));
printf(" + Loaded Whites/Blacks\n");
}
else
printf(" - FAILED Whites/Blacks\n");
auto textureOp = std::make_unique<PipelineOperation>("Texture");
textureOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "texture.frag");
if (textureOp->shaderProgram)
{
textureOp->uniforms["textureValue"] = {"textureValue"};
textureOp->textureVal = &texture;
textureOp->FindUniformLocations();
g_allOperations.push_back(std::move(textureOp));
printf(" + Loaded Texture\n");
}
else
printf(" - FAILED Texture\n");
auto clarityOp = std::make_unique<PipelineOperation>("Clarity");
clarityOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "clarity.frag");
if (clarityOp->shaderProgram)
{
clarityOp->uniforms["clarityValue"] = {"clarityValue"};
clarityOp->clarityVal = &clarity;
clarityOp->FindUniformLocations();
g_allOperations.push_back(std::move(clarityOp));
printf(" + Loaded Clarity\n");
}
else
printf(" - FAILED Clarity\n");
auto dehazeOp = std::make_unique<PipelineOperation>("Dehaze");
dehazeOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "dehaze.frag");
if (dehazeOp->shaderProgram)
{
dehazeOp->uniforms["dehazeValue"] = {"dehazeValue"};
dehazeOp->dehazeVal = &dehaze;
dehazeOp->FindUniformLocations();
g_allOperations.push_back(std::move(dehazeOp));
printf(" + Loaded Dehaze\n");
}
else
printf(" - FAILED Dehaze\n");
auto saturationOp = std::make_unique<PipelineOperation>("Saturation");
saturationOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "saturation.frag");
if (saturationOp->shaderProgram)
{
saturationOp->uniforms["saturationValue"] = {"saturationValue"};
saturationOp->saturationVal = &saturation;
saturationOp->FindUniformLocations();
g_allOperations.push_back(std::move(saturationOp));
printf(" + Loaded Saturation\n");
}
else
printf(" - FAILED Saturation\n");
auto vibranceOp = std::make_unique<PipelineOperation>("Vibrance");
vibranceOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "vibrance.frag");
if (vibranceOp->shaderProgram)
{
vibranceOp->uniforms["vibranceValue"] = {"vibranceValue"};
vibranceOp->vibranceVal = &vibrance;
vibranceOp->FindUniformLocations();
g_allOperations.push_back(std::move(vibranceOp));
printf(" + Loaded Vibrance\n");
}
else
printf(" - FAILED Vibrance\n");
g_pipeline.activeOperations.clear();
for (const auto &op_ptr : g_allOperations)
{
if (op_ptr)
{ // Make sure pointer is valid
g_pipeline.activeOperations.push_back(*op_ptr); // Add a *copy* to the active list
// Re-find locations for the copy (or ensure copy constructor handles it)
g_pipeline.activeOperations.back().FindUniformLocations();
// Copy the pointers to the actual slider variables
g_pipeline.activeOperations.back().exposureVal = op_ptr->exposureVal;
g_pipeline.activeOperations.back().contrastVal = op_ptr->contrastVal;
g_pipeline.activeOperations.back().clarityVal = op_ptr->clarityVal;
g_pipeline.activeOperations.back().highlightsVal = op_ptr->highlightsVal;
g_pipeline.activeOperations.back().shadowsVal = op_ptr->shadowsVal;
g_pipeline.activeOperations.back().whitesVal = op_ptr->whitesVal;
g_pipeline.activeOperations.back().blacksVal = op_ptr->blacksVal;
g_pipeline.activeOperations.back().textureVal = op_ptr->textureVal;
g_pipeline.activeOperations.back().dehazeVal = op_ptr->dehazeVal;
g_pipeline.activeOperations.back().saturationVal = op_ptr->saturationVal;
g_pipeline.activeOperations.back().vibranceVal = op_ptr->vibranceVal;
g_pipeline.activeOperations.back().temperatureVal = op_ptr->temperatureVal;
g_pipeline.activeOperations.back().tintVal = op_ptr->tintVal;
// Set initial enabled state if needed (e.g., all enabled by default)
g_pipeline.activeOperations.back().enabled = true;
}
}
printf("Initialized %zu possible operations. %zu added to default active pipeline.\n",
g_allOperations.size(), g_pipeline.activeOperations.size());
}
// Add this function somewhere accessible, e.g., before main()
void ComputeHistogramGPU(GLuint inputTextureID, int width, int height) {
if (!g_histogramResourcesInitialized || inputTextureID == 0 || width <= 0 || height <= 0) {
// Clear CPU data if not computed
std::fill(g_histogramDataCPU.begin(), g_histogramDataCPU.end(), 0);
g_histogramMaxCount = 1;
printf("Histogram resources not initialized or invalid input. Skipping computation.\n");
return;
}
// 1. Clear the SSBO buffer data to zeros
glBindBuffer(GL_SHADER_STORAGE_BUFFER, g_histogramSSBO);
// Using glBufferSubData might be marginally faster than glClearBufferData if driver optimizes zeroing
// static std::vector<unsigned int> zeros(HISTOGRAM_BUFFER_SIZE, 0); // Create once
// glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, HISTOGRAM_BUFFER_SIZE * sizeof(unsigned int), zeros.data());
// Or use glClearBufferData (often recommended)
GLuint zero = 0;
glClearBufferData(GL_SHADER_STORAGE_BUFFER, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, &zero);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); // Unbind
// 2. Bind resources and dispatch compute shader
glUseProgram(g_histogramComputeShader);
// Bind input texture as image unit 0 (read-only)
// IMPORTANT: Ensure the format matches the compute shader layout qualifier (e.g., rgba8)
// If textureToDisplay is RGBA16F, you'd use layout(rgba16f) in shader
glBindImageTexture(0, inputTextureID, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA16); // Assuming display texture is RGBA8
// Bind SSBO to binding point 1
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, g_histogramSSBO);
// Calculate number of work groups
GLuint workGroupSizeX = 16; // Must match layout in shader
GLuint workGroupSizeY = 16;
GLuint numGroupsX = (width + workGroupSizeX - 1) / workGroupSizeX;
GLuint numGroupsY = (height + workGroupSizeY - 1) / workGroupSizeY;
// Dispatch the compute shader
glDispatchCompute(numGroupsX, numGroupsY, 1);
// 3. Synchronization: Ensure compute shader writes finish before CPU reads buffer
// Use a memory barrier on the SSBO writes
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
// Unbind resources (optional here, but good practice)
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA16);
glUseProgram(0);
// 4. Read histogram data back from SSBO to CPU vector
glBindBuffer(GL_SHADER_STORAGE_BUFFER, g_histogramSSBO);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, HISTOGRAM_BUFFER_SIZE * sizeof(unsigned int), g_histogramDataCPU.data());
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); // Unbind
// 5. Find the maximum count for scaling the plot (optional, can be capped)
g_histogramMaxCount = 255; // Reset to 255 (prevents div by zero)
for (unsigned int count : g_histogramDataCPU) {
if (count > g_histogramMaxCount) {
g_histogramMaxCount = count;
}
}
// Optional: Cap max count to prevent extreme peaks from flattening the rest
// unsigned int capThreshold = (width * height) / 50; // e.g., cap at 2% of pixels
// g_histogramMaxCount = std::min(g_histogramMaxCount, capThreshold);
// if (g_histogramMaxCount == 0) g_histogramMaxCount = 1; // Ensure not zero after capping
GLenum err = glGetError();
if (err != GL_NO_ERROR) {
fprintf(stderr, "OpenGL Error during histogram computation/readback: %u\n", err);
// Optionally clear CPU data on error
std::fill(g_histogramDataCPU.begin(), g_histogramDataCPU.end(), 0);
g_histogramMaxCount = 1;
printf("Histogram computation failed. Data cleared.\n");
}
else {
printf("Histogram computed. Max count: %u\n", g_histogramMaxCount);
}
}
// Add this function somewhere accessible, e.g., before main()
void DrawHistogramWidget(const char* widgetId, ImVec2 graphSize) {
if (g_histogramDataCPU.empty() || g_histogramMaxCount <= 1) { // Check if data is valid
if (g_histogramDataCPU.empty()) {
ImGui::Text("Histogram data not initialized.");
} else {
ImGui::Text("Histogram data is empty or invalid.");
}
if (g_histogramMaxCount <= 1) {
ImGui::Text("Histogram max count is invalid.");
}
ImGui::Text("Histogram data not available.");
return;
}
ImGui::PushID(widgetId); // Isolate widget IDs
ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImVec2 widgetPos = ImGui::GetCursorScreenPos();
// Determine actual graph size (negative values mean use available space)
if (graphSize.x <= 0.0f) graphSize.x = ImGui::GetContentRegionAvail().x;
if (graphSize.y <= 0.0f) graphSize.y = 100.0f; // Default height
// Draw background for the histogram area (optional)
drawList->AddRectFilled(widgetPos, widgetPos + graphSize, IM_COL32(30, 30, 30, 200));
// Calculate scaling factors
float barWidth = graphSize.x / float(NUM_HISTOGRAM_BINS);
float scaleY = graphSize.y / float(g_histogramMaxCount); // Scale based on max count
// Define colors (with some transparency for overlap visibility)
const ImU32 colR = IM_COL32(255, 0, 0, 180);
const ImU32 colG = IM_COL32(0, 255, 0, 180);
const ImU32 colB = IM_COL32(0, 0, 255, 180);
// Draw the histogram bars (R, G, B)
for (int i = 0; i < NUM_HISTOGRAM_BINS; ++i) {
// Get heights (clamped to graph size)
float hR = ImMin(float(g_histogramDataCPU[i]) * scaleY, graphSize.y);
float hG = ImMin(float(g_histogramDataCPU[i + NUM_HISTOGRAM_BINS]) * scaleY, graphSize.y);
float hB = ImMin(float(g_histogramDataCPU[i + NUM_HISTOGRAM_BINS * 2]) * scaleY, graphSize.y);
// Calculate bar positions
float x0 = widgetPos.x + float(i) * barWidth;
float x1 = x0 + barWidth; // Use lines if bars are too thin, or thin rects
float yBase = widgetPos.y + graphSize.y; // Bottom of the graph
// Draw lines or thin rectangles (lines are often better for dense histograms)
// Overlap/Blend: Draw B, then G, then R so Red is most prominent? Or use alpha blending.
if (hB > 0) drawList->AddLine(ImVec2(x0 + barWidth * 0.5f, yBase), ImVec2(x0 + barWidth * 0.5f, yBase - hB), colB, 1.0f);
if (hG > 0) drawList->AddLine(ImVec2(x0 + barWidth * 0.5f, yBase), ImVec2(x0 + barWidth * 0.5f, yBase - hG), colG, 1.0f);
if (hR > 0) drawList->AddLine(ImVec2(x0 + barWidth * 0.5f, yBase), ImVec2(x0 + barWidth * 0.5f, yBase - hR), colR, 1.0f);
// --- Alternative: Rectangles (might overlap heavily) ---
// if (hB > 0) drawList->AddRectFilled(ImVec2(x0, yBase - hB), ImVec2(x1, yBase), colB);
// if (hG > 0) drawList->AddRectFilled(ImVec2(x0, yBase - hG), ImVec2(x1, yBase), colG);
// if (hR > 0) drawList->AddRectFilled(ImVec2(x0, yBase - hR), ImVec2(x1, yBase), colR);
}
// Draw border around the histogram area (optional)
drawList->AddRect(widgetPos, widgetPos + graphSize, IM_COL32(150, 150, 150, 255));
// Advance cursor past the histogram widget area
ImGui::Dummy(graphSize);
ImGui::PopID(); // Restore ID stack
}
// Main code
int main(int, char **)
{
// Setup SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0)
{
printf("Error: %s\n", SDL_GetError());
return -1;
}
// Decide GL+GLSL versions
#if defined(IMGUI_IMPL_OPENGL_ES2)
// GL ES 2.0 + GLSL 100 (WebGL 1.0)
const char *glsl_version = "#version 100";
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#elif defined(IMGUI_IMPL_OPENGL_ES3)
// GL ES 3.0 + GLSL 300 es (WebGL 2.0)
const char *glsl_version = "#version 300 es";
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#elif defined(__APPLE__)
// GL 3.2 Core + GLSL 150
const char *glsl_version = "#version 150";
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); // Always required on Mac
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
#else
// GL 3.0 + GLSL 130
const char *glsl_version = "#version 130";
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#endif
// From 2.0.18: Enable native IME.
#ifdef SDL_HINT_IME_SHOW_UI
SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
#endif
// Create window with graphics context
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
SDL_Window *window = SDL_CreateWindow("tedit", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, window_flags);
if (window == nullptr)
{
printf("Error: SDL_CreateWindow(): %s\n", SDL_GetError());
return -1;
}
SDL_GLContext gl_context = SDL_GL_CreateContext(window);
if (gl_context == nullptr)
{
printf("Error: SDL_GL_CreateContext(): %s\n", SDL_GetError());
return -1;
}
SDL_GL_MakeCurrent(window, gl_context);
SDL_GL_SetSwapInterval(1); // Enable vsync
glewExperimental = GL_TRUE; // Needed for core profile
GLenum err = glewInit();
if (err != GLEW_OK)
{
fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
return -1;
}
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO &io = ImGui::GetIO();
(void)io;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking
// io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // Enable Multi-Viewport / Platform Windows
// io.ConfigViewportsNoAutoMerge = true;
// io.ConfigViewportsNoTaskBarIcon = true;
// Setup Dear ImGui style
ImGui::StyleColorsDark();
// ImGui::StyleColorsLight();
// When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones.
ImGuiStyle &style = ImGui::GetStyle();
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
style.WindowRounding = 0.0f;
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
}
// Setup Platform/Renderer backends
ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
ImGui_ImplOpenGL3_Init(glsl_version);
// Our state
ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
g_openFileDialog.SetTitle("Open Image File");
// Add common image formats and typical RAW formats
g_openFileDialog.SetTypeFilters({
".jpg", ".jpeg", ".png", ".tif", ".tiff", // Standard formats
".arw", ".cr2", ".cr3", ".nef", ".dng", ".orf", ".raf", ".rw2", // Common RAW
".*" // Allow any file as fallback
});
g_exportSaveFileDialog.SetTitle("Export Image As");
// Type filters for saving are less critical as we force the extension later,
// but can be helpful for user navigation. Let's set a default.
g_exportSaveFileDialog.SetTypeFilters({".jpg", ".png", ".tif"});
AppImage g_loadedImage; // Your loaded image data
bool g_imageIsLoaded = false;
g_processedTextureId = 0; // Initialize processed texture ID
printf("Initializing image processing pipeline...\n");
g_pipeline.Init("shaders/"); // Assuming shaders are in shaders/ subdir
ImGuiTexInspect::ImplOpenGL3_Init(); // Or DirectX 11 equivalent (check your chosen backend header file)
ImGuiTexInspect::Init();
ImGuiTexInspect::CreateContext();
InitShaderOperations("shaders/"); // Initialize shader operations
if (!InitHistogramResources("shaders/")) {
// Handle error - maybe disable histogram feature
fprintf(stderr, "Histogram initialization failed, feature disabled.\n");
}
// Main loop
bool done = false;
#ifdef __EMSCRIPTEN__
// For an Emscripten build we are disabling file-system access, so let's not attempt to do a fopen() of the imgui.ini file.
// You may manually call LoadIniSettingsFromMemory() to load settings from your own storage.
io.IniFilename = nullptr;
EMSCRIPTEN_MAINLOOP_BEGIN
#else
while (!done)
#endif
{
// Poll and handle events (inputs, window resize, etc.)
// You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
// Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
SDL_Event event;
while (SDL_PollEvent(&event))
{
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT)
done = true;
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE && event.window.windowID == SDL_GetWindowID(window))
done = true;
}
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED)
{
SDL_Delay(10);
continue;
}
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
GLuint textureToDisplay = 0; // Use a local var for clarity
GLuint textureToSave = 0; // Texture ID holding final linear data for saving
if (g_imageIsLoaded && g_loadedImage.m_textureId != 0)
{
g_pipeline.inputColorSpace = g_inputColorSpace;
g_pipeline.outputColorSpace = g_outputColorSpace;
// Modify pipeline processing slightly to get both display and save textures
// Add a flag or method to control output conversion for saving
textureToSave = g_pipeline.ProcessImage(
g_loadedImage.m_textureId,
g_loadedImage.getWidth(),
g_loadedImage.getHeight(),
false // <-- Add argument: bool applyOutputConversion = true
);
textureToDisplay = g_pipeline.ProcessImage(
g_loadedImage.m_textureId,
g_loadedImage.getWidth(),
g_loadedImage.getHeight(),
true // Apply conversion for display
);
// If the pipeline wasn't modified, textureToSave might need extra work
}
else
{
textureToDisplay = 0;
textureToSave = 0;
}
// --- Menu Bar ---
if (ImGui::BeginMainMenuBar())
{
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("Open...", "Ctrl+O"))
{
g_openFileDialog.Open();
}
// Disable Export if no image is loaded
if (ImGui::MenuItem("Export...", "Ctrl+E", false, g_imageIsLoaded))
{
g_exportErrorMsg = ""; // Clear previous errors
g_showExportWindow = true; // <<< Set the flag to show the window
}
ImGui::Separator();
if (ImGui::MenuItem("Exit"))
{
done = true; // Simple exit for now
}
ImGui::EndMenu();
}
// ... other menus ...
ImGui::EndMainMenuBar();
}
// --- File Dialog Display & Handling ---
g_openFileDialog.Display();
g_exportSaveFileDialog.Display();
if (g_openFileDialog.HasSelected())
{
std::string selectedPath = g_openFileDialog.GetSelected().string();
g_openFileDialog.ClearSelected();
printf("Opening file: %s\n", selectedPath.c_str());
// --- Load the selected image ---
std::optional<AppImage> imgOpt = loadImage(selectedPath);
if (imgOpt)
{
// If an image was already loaded, clean up its texture first
if (g_loadedImage.m_textureId != 0)
{
glDeleteTextures(1, &g_loadedImage.m_textureId);
g_loadedImage.m_textureId = 0;
}
// Clean up pipeline resources (FBOs/Textures) before loading new texture
g_pipeline.ResetResources(); // <<< NEED TO ADD THIS METHOD
g_loadedImage = std::move(*imgOpt);
printf("Image loaded (%dx%d, %d channels, Linear:%s)\n",
g_loadedImage.getWidth(), g_loadedImage.getHeight(), g_loadedImage.getChannels(), g_loadedImage.isLinear() ? "Yes" : "No");
if (loadImageTexture(g_loadedImage))
{
g_imageIsLoaded = true;
g_currentFilePath = selectedPath; // Store path
printf("Float texture created successfully (ID: %u).\n", g_loadedImage.m_textureId);
// Maybe reset sliders/pipeline state? Optional.
}
else
{
g_imageIsLoaded = false;
g_currentFilePath = "";
fprintf(stderr, "Failed to load image into GL texture.\n");
// TODO: Show error to user (e.g., modal popup)
}
}
else
{
g_imageIsLoaded = false;
g_currentFilePath = "";
fprintf(stderr, "Failed to load image file: %s\n", selectedPath.c_str());
// TODO: Show error to user
}
}
if (g_showExportWindow) // <<< Only attempt to draw if flag is true
{
// Optional: Center the window the first time it appears
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Appearing); // Auto-height
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
// Begin a standard window. Pass &g_showExportWindow to enable the 'X' button.
if (ImGui::Begin("Export Settings", &g_showExportWindow, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("Choose Export Format and Settings:");
ImGui::Separator();
// --- Format Selection ---
ImGui::Text("Format:");
ImGui::SameLine();
// ... (Combo box logic for g_exportFormat remains the same) ...
const char *formats[] = {"JPEG", "PNG (8-bit)", "PNG (16-bit)", "TIFF (8-bit)", "TIFF (16-bit)"};
int currentFormatIndex = 0;
switch (g_exportFormat)
{ /* ... map g_exportFormat to index ... */
}
if (ImGui::Combo("##ExportFormat", &currentFormatIndex, formats, IM_ARRAYSIZE(formats)))
{
switch (currentFormatIndex)
{ /* ... map index back to g_exportFormat ... */
}
g_exportErrorMsg = "";
}
// --- Format Specific Options ---
if (g_exportFormat == ImageSaveFormat::JPEG)
{
ImGui::SliderInt("Quality", &g_exportQuality, 1, 100);
}
else
{
ImGui::Dummy(ImVec2(0.0f, ImGui::GetFrameHeightWithSpacing())); // Keep consistent height
}
ImGui::Separator();
// --- Display Error Messages ---
if (!g_exportErrorMsg.empty())
{
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f));
ImGui::TextWrapped("Error: %s", g_exportErrorMsg.c_str());
ImGui::PopStyleColor();
ImGui::Separator();
}
// --- Action Buttons ---
if (ImGui::Button("Save As...", ImVec2(120, 0)))
{
// ... (Logic to set default name/path and call g_exportSaveFileDialog.Open() remains the same) ...
std::filesystem::path currentPath(g_currentFilePath);
std::string defaultName = currentPath.stem().string() + "_edited";
g_exportSaveFileDialog.SetPwd(currentPath.parent_path());
// g_exportSaveFileDialog.SetInputName(defaultName); // If supported
g_exportSaveFileDialog.Open();
}
ImGui::SameLine();
// No need for an explicit Cancel button if the 'X' works, but can keep it:
if (ImGui::Button("Cancel", ImVec2(120, 0)))
{
g_showExportWindow = false; // Close the window by setting the flag
}
} // Matches ImGui::Begin("Export Settings",...)
ImGui::End(); // IMPORTANT: Always call End() for Begin()
} // End of if(g_showExportWindow)
// --- Handle Export Save Dialog Selection ---
if (g_exportSaveFileDialog.HasSelected())
{
// ... (Your existing logic to get path, correct extension) ...
std::filesystem::path savePathFs = g_exportSaveFileDialog.GetSelected();
g_exportSaveFileDialog.ClearSelected();
std::string savePath = savePathFs.string();
// ... (Ensure/correct extension logic) ...
// --- Get Processed Image Data & Save ---
printf("Attempting to save to: %s\n", savePath.c_str());
g_exportErrorMsg = "";
if (textureToSave != 0)
{
AppImage exportImageRGBA; // Name it clearly - it holds RGBA data
printf("Reading back texture ID %u for saving...\n", textureToSave);
if (ReadTextureToAppImage(textureToSave, g_loadedImage.getWidth(), g_loadedImage.getHeight(), exportImageRGBA))
{
printf("Texture readback successful, saving...\n");
// <<< --- ADD CONVERSION LOGIC HERE --- >>>
bool saveResult = false;
if (g_exportFormat == ImageSaveFormat::JPEG)
{
// JPEG cannot handle 4 channels, convert to 3 (RGB)
if (exportImageRGBA.getChannels() == 4)
{
printf("JPEG selected: Converting 4-channel RGBA to 3-channel RGB...\n");
AppImage exportImageRGB(exportImageRGBA.getWidth(), exportImageRGBA.getHeight(), 3);
// Check allocation success? (Should be fine if RGBA worked)
const float *rgbaData = exportImageRGBA.getData();
float *rgbData = exportImageRGB.getData();
size_t numPixels = exportImageRGBA.getWidth() * exportImageRGBA.getHeight();
for (size_t i = 0; i < numPixels; ++i)
{
// Copy R, G, B; discard A
rgbData[i * 3 + 0] = rgbaData[i * 4 + 0]; // R
rgbData[i * 3 + 1] = rgbaData[i * 4 + 1]; // G
rgbData[i * 3 + 2] = rgbaData[i * 4 + 2]; // B
}
exportImageRGB.m_isLinear = exportImageRGBA.isLinear(); // Preserve linearity flag
exportImageRGB.m_colorSpaceName = exportImageRGBA.getColorSpaceName(); // Preserve colorspace info
printf("Conversion complete, saving RGB data...\n");
saveResult = saveImage(exportImageRGB, savePath, g_exportFormat, g_exportQuality);
}
else
{
// Source wasn't 4 channels? Unexpected, but save it directly.
printf("Warning: Expected 4 channels for JPEG conversion, got %d. Saving directly...\n", exportImageRGBA.getChannels());
saveResult = saveImage(exportImageRGBA, savePath, g_exportFormat, g_exportQuality);
}
}
else
{
// Format is PNG or TIFF, which should handle 4 channels (or 1/3)
printf("Saving image with original channels (%d) for PNG/TIFF...\n", exportImageRGBA.getChannels());
saveResult = saveImage(exportImageRGBA, savePath, g_exportFormat, g_exportQuality);
}
// <<< --- END CONVERSION LOGIC --- >>>
if (saveResult)
{
printf("Image saved successfully!\n");
g_showExportWindow = false; // <<< Close the settings window on success
}
else
{
fprintf(stderr, "Failed to save image.\n");
g_exportErrorMsg = "Failed to save image data to file.";
}
}
else
{
fprintf(stderr, "Failed to read back texture data from GPU.\n");
g_exportErrorMsg = "Failed to read processed image data from GPU.";
}
}
else
{
fprintf(stderr, "Cannot save: Invalid processed texture ID.\n");
g_exportErrorMsg = "No valid processed image data available to save.";
}
}
static bool use_dockspace = true;
if (use_dockspace)
{
ImGuiViewport *viewport = ImGui::GetMainViewport();
ImGuiID dockspace_id = ImGui::GetID("MyDockSpace");
// Use DockSpaceOverViewport instead of creating a manual window
// Set the viewport size for the dockspace node. This is important.
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGui::SetNextWindowViewport(viewport->ID);
// Use PassthruCentralNode to make the central node background transparent
// so the ImGui default background shows until a window is docked there.
ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_PassthruCentralNode;
// We wrap the DockSpace call in a window that doesn't really draw anything itself,
// but is required by the DockBuilder mechanism to target the space.
// Make it borderless, no title, etc.
ImGuiWindowFlags host_window_flags = 0;
host_window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
host_window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus;
host_window_flags |= ImGuiWindowFlags_NoBackground; // Make the host window transparent
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin("DockSpaceWindowHost", nullptr, host_window_flags); // No bool* needed
ImGui::PopStyleVar(3);
// Create the actual dockspace area.
ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags);
ImGui::End(); // End the transparent host window
// --- DockBuilder setup (runs once) ---
// This logic remains the same, targeting the dockspace_id
// Use DockBuilderGetNode()->IsEmpty() as a robust check for first time setup or reset.
ImGuiDockNode *centralNode = ImGui::DockBuilderGetNode(dockspace_id);
if (centralNode == nullptr || centralNode->IsEmpty())
{
printf("DockBuilder: Setting up initial layout for DockID %u\n", dockspace_id);
ImGui::DockBuilderRemoveNode(dockspace_id); // Clear out any previous state
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size); // Set the size for the root node
ImGuiID dock_main_id = dockspace_id; // This is the ID of the node just added
ImGuiID dock_right_id, dock_left_id, dock_center_id;
// Split right first (Edit Panel)
ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Right, 0.25f, &dock_right_id, &dock_main_id);
// Then split left from the remaining main area (Exif Panel)
ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Left, 0.25f, &dock_left_id, &dock_center_id); // dock_center_id is the final remaining central node
// Dock the windows into the nodes
ImGui::DockBuilderDockWindow("Image Exif", dock_left_id);
ImGui::DockBuilderDockWindow("Edit Image", dock_right_id);
ImGui::DockBuilderDockWindow("Image View", dock_center_id); // Dock image view into the center
ImGui::DockBuilderFinish(dockspace_id);
printf("DockBuilder: Layout finished.\n");
}
// --- End DockBuilder setup ---
// --- Now Begin the actual windows that get docked ---
// These calls are now *outside* any manual container window.
// They will find their place in the dockspace based on the DockBuilder setup or user interaction.
// "Image View" window
ImGui::Begin("Image View");
// Display the texture that HAS the output conversion applied
ImVec2 imageWidgetTopLeftScreen = ImGui::GetCursorScreenPos(); // Position BEFORE the inspector panel
ImVec2 availableContentSize = ImGui::GetContentRegionAvail(); // Size available FOR the inspector panel
GLuint displayTexId = textureToDisplay; // Use the display texture ID
if (displayTexId != 0)
{
ComputeHistogramGPU(textureToDisplay, g_loadedImage.getWidth(), g_loadedImage.getHeight());
// Assume ImGuiTexInspect fills available space. This might need adjustment.
ImVec2 displaySize = availableContentSize;
float displayAspect = displaySize.x / displaySize.y;
float imageAspect = float(g_loadedImage.getWidth()) / float(g_loadedImage.getHeight());
ImVec2 imageDisplaySize; // Actual size the image occupies on screen (letterboxed/pillarboxed)
ImVec2 imageDisplayOffset = ImVec2(0, 0); // Offset within the widget area due to letterboxing
if (displayAspect > imageAspect)
{ // Display is wider than image -> letterbox (bars top/bottom)
imageDisplaySize.y = displaySize.y;
imageDisplaySize.x = imageDisplaySize.y * imageAspect;
imageDisplayOffset.x = (displaySize.x - imageDisplaySize.x) * 0.5f;
}
else
{ // Display is taller than image (or same aspect) -> pillarbox (bars left/right)
imageDisplaySize.x = displaySize.x;
imageDisplaySize.y = imageDisplaySize.x / imageAspect;
imageDisplayOffset.y = (displaySize.y - imageDisplaySize.y) * 0.5f;
}
ImVec2 imageTopLeftScreen = imageWidgetTopLeftScreen + imageDisplayOffset;
ImVec2 imageBottomRightScreen = imageTopLeftScreen + imageDisplaySize;
// Use textureToDisplay here
ImGuiTexInspect::BeginInspectorPanel("Image Inspector", (ImTextureID)(intptr_t)displayTexId,
ImVec2(g_loadedImage.m_width, g_loadedImage.m_height),
ImGuiTexInspect::InspectorFlags_NoTooltip |
ImGuiTexInspect::InspectorFlags_NoGrid |
ImGuiTexInspect::InspectorFlags_NoForceFilterNearest,
ImGuiTexInspect::SizeIncludingBorder(availableContentSize));
ImGuiTexInspect::EndInspectorPanel();
// --- Draw Crop Overlay If Active ---
if (g_cropActive && g_imageIsLoaded)
{
ImDrawList *drawList = ImGui::GetForegroundDrawList();
ImGuiIO &io = ImGui::GetIO();
ImVec2 mousePos = io.MousePos;
// Calculate screen coords of the current crop rectangle
ImVec2 cropMinScreen = imageTopLeftScreen + ImVec2(g_cropRectNorm.x, g_cropRectNorm.y) * imageDisplaySize;
ImVec2 cropMaxScreen = imageTopLeftScreen + ImVec2(g_cropRectNorm.z, g_cropRectNorm.w) * imageDisplaySize;
ImVec2 cropSizeScreen = cropMaxScreen - cropMinScreen;
// Define handle size and interaction margin
float handleScreenSize = 8.0f;
float handleInteractionMargin = handleScreenSize * 1.5f; // Larger click area
ImU32 colRect = IM_COL32(255, 255, 255, 200); // White rectangle
ImU32 colHandle = IM_COL32(255, 255, 255, 255); // Solid white handle
ImU32 colGrid = IM_COL32(200, 200, 200, 100); // Faint grid lines
ImU32 colHover = IM_COL32(255, 255, 0, 255); // Yellow highlight
// --- Define Handle Positions (screen coordinates) ---
// Corners
ImVec2 tl = cropMinScreen;
ImVec2 tr = ImVec2(cropMaxScreen.x, cropMinScreen.y);
ImVec2 bl = ImVec2(cropMinScreen.x, cropMaxScreen.y);
ImVec2 br = cropMaxScreen;
// Mid-edges
ImVec2 tm = ImVec2((tl.x + tr.x) * 0.5f, tl.y);
ImVec2 bm = ImVec2((bl.x + br.x) * 0.5f, bl.y);
ImVec2 lm = ImVec2(tl.x, (tl.y + bl.y) * 0.5f);
ImVec2 rm = ImVec2(tr.x, (tr.y + br.y) * 0.5f);
// Handle definitions for hit testing and drawing
struct HandleDef
{
CropHandle id;
ImVec2 pos;
};
HandleDef handles[] = {
{CropHandle::TOP_LEFT, tl}, {CropHandle::TOP_RIGHT, tr}, {CropHandle::BOTTOM_LEFT, bl}, {CropHandle::BOTTOM_RIGHT, br}, {CropHandle::TOP, tm}, {CropHandle::BOTTOM, bm}, {CropHandle::LEFT, lm}, {CropHandle::RIGHT, rm}};
// --- Interaction Handling ---
bool isHoveringAnyHandle = false;
CropHandle hoveredHandle = CropHandle::NONE;
// Only interact if window is hovered
if (ImGui::IsWindowHovered()) // ImGuiHoveredFlags_AllowWhenBlockedByActiveItem might also be needed
{
// Check handles first (higher priority than inside rect)
for (const auto &h : handles)
{
ImRect handleRect(h.pos - ImVec2(handleInteractionMargin, handleInteractionMargin),
h.pos + ImVec2(handleInteractionMargin, handleInteractionMargin));
if (handleRect.Contains(mousePos))
{
hoveredHandle = h.id;
isHoveringAnyHandle = true;
break;
}
}
// Check inside rect if no handle hovered
ImRect insideRect(cropMinScreen, cropMaxScreen);
if (!isHoveringAnyHandle && insideRect.Contains(mousePos))
{
hoveredHandle = CropHandle::INSIDE;
}
// Mouse Down: Start dragging
if (hoveredHandle != CropHandle::NONE && ImGui::IsMouseClicked(ImGuiMouseButton_Left))
{
g_activeCropHandle = hoveredHandle;
g_isDraggingCrop = true;
g_dragStartMousePos = mousePos;
g_cropRectNormInitial = g_cropRectNorm; // Store state at drag start
printf("Started dragging handle: %d\n", (int)g_activeCropHandle);
}
} // End IsWindowHovered check
// Mouse Drag: Update crop rectangle
if (g_isDraggingCrop && ImGui::IsMouseDragging(ImGuiMouseButton_Left))
{
ImVec2 mouseDeltaScreen = mousePos - g_dragStartMousePos;
// Convert delta to normalized image coordinates
ImVec2 mouseDeltaNorm = ImVec2(0, 0);
if (imageDisplaySize.x > 1e-3 && imageDisplaySize.y > 1e-3)
{ // Avoid division by zero
mouseDeltaNorm = mouseDeltaScreen / imageDisplaySize;
}
// Update g_cropRectNorm based on handle and delta
// Store temporary rect to apply constraints later
ImVec4 tempRect = g_cropRectNormInitial; // Work from initial state + delta
// --- Update Logic (Needs Aspect Ratio Constraint Integration) ---
// [This part is complex - Simplified version below]
UpdateCropRect(tempRect, g_activeCropHandle, mouseDeltaNorm, g_cropAspectRatio);
// Clamp final rect to 0-1 range and ensure min < max
tempRect.x = ImClamp(tempRect.x, 0.0f, 1.0f);
tempRect.y = ImClamp(tempRect.y, 0.0f, 1.0f);
tempRect.z = ImClamp(tempRect.z, 0.0f, 1.0f);
tempRect.w = ImClamp(tempRect.w, 0.0f, 1.0f);
if (tempRect.x > tempRect.z)
ImSwap(tempRect.x, tempRect.z);
if (tempRect.y > tempRect.w)
ImSwap(tempRect.y, tempRect.w);
// Prevent zero size rect? (Optional)
// float minSizeNorm = 0.01f; // e.g., 1% minimum size
// if (tempRect.z - tempRect.x < minSizeNorm) tempRect.z = tempRect.x + minSizeNorm;
// if (tempRect.w - tempRect.y < minSizeNorm) tempRect.w = tempRect.y + minSizeNorm;
g_cropRectNorm = tempRect; // Update the actual state
}
else if (g_isDraggingCrop && ImGui::IsMouseReleased(ImGuiMouseButton_Left))
{
// Mouse Release: Stop dragging
g_isDraggingCrop = false;
g_activeCropHandle = CropHandle::NONE;
printf("Stopped dragging crop.\n");
}
// --- Drawing ---
// Dimming overlay (optional) - Draw 4 rects outside the crop area
drawList->AddRectFilled(imageTopLeftScreen, ImVec2(cropMinScreen.x, imageBottomRightScreen.y), IM_COL32(0,0,0,100)); // Left
drawList->AddRectFilled(ImVec2(cropMaxScreen.x, imageTopLeftScreen.y), imageBottomRightScreen, IM_COL32(0,0,0,100)); // Right
drawList->AddRectFilled(ImVec2(cropMinScreen.x, imageTopLeftScreen.y), ImVec2(cropMaxScreen.x, cropMinScreen.y), IM_COL32(0,0,0,100)); // Top
drawList->AddRectFilled(ImVec2(cropMinScreen.x, cropMaxScreen.y), ImVec2(cropMaxScreen.x, imageBottomRightScreen.y), IM_COL32(0,0,0,100)); // Bottom
// Draw crop rectangle outline
drawList->AddRect(cropMinScreen, cropMaxScreen, colRect, 0.0f, 0, 1.5f);
// Draw grid lines (simple 3x3 grid)
float thirdW = cropSizeScreen.x / 3.0f;
float thirdH = cropSizeScreen.y / 3.0f;
drawList->AddLine(ImVec2(cropMinScreen.x + thirdW, cropMinScreen.y), ImVec2(cropMinScreen.x + thirdW, cropMaxScreen.y), colGrid, 1.0f);
drawList->AddLine(ImVec2(cropMinScreen.x + thirdW * 2, cropMinScreen.y), ImVec2(cropMinScreen.x + thirdW * 2, cropMaxScreen.y), colGrid, 1.0f);
drawList->AddLine(ImVec2(cropMinScreen.x, cropMinScreen.y + thirdH), ImVec2(cropMaxScreen.x, cropMinScreen.y + thirdH), colGrid, 1.0f);
drawList->AddLine(ImVec2(cropMinScreen.x, cropMinScreen.y + thirdH * 2), ImVec2(cropMaxScreen.x, cropMinScreen.y + thirdH * 2), colGrid, 1.0f);
// Draw handles
for (const auto &h : handles)
{
bool isHovered = (h.id == hoveredHandle);
bool isActive = (h.id == g_activeCropHandle);
drawList->AddRectFilled(h.pos - ImVec2(handleScreenSize / 2, handleScreenSize / 2),
h.pos + ImVec2(handleScreenSize / 2, handleScreenSize / 2),
(isHovered || isActive) ? colHover : colHandle);
}
} // End if(g_cropActive)
}
else
{
// Show placeholder text if no image is loaded
ImVec2 winSize = ImGui::GetWindowSize();
ImVec2 textSize = ImGui::CalcTextSize("No Image Loaded");
ImGui::SetCursorPos(ImVec2((winSize.x - textSize.x) * 0.5f, (winSize.y - textSize.y) * 0.5f));
ImGui::Text("No Image Loaded. File -> Open... to load an image");
std::fill(g_histogramDataCPU.begin(), g_histogramDataCPU.end(), 0);
g_histogramMaxCount = 1;
// Or maybe: "File -> Open... to load an image"
}
ImGui::End(); // End Image View
// "Image Exif" window
ImGui::Begin("Image Exif");
if (g_imageIsLoaded)
{
ImGui::Text("Image Width: %d", g_loadedImage.m_width);
ImGui::Text("Image Height: %d", g_loadedImage.m_height);
ImGui::Text("Image Loaded: %s", g_imageIsLoaded ? "Yes" : "No");
ImGui::Text("Image Channels: %d", g_loadedImage.m_channels);
ImGui::Text("Image Color Space: %s", g_loadedImage.m_colorSpaceName.c_str());
ImGui::Text("Image ICC Profile Size: %zu bytes", g_loadedImage.m_iccProfile.size());
ImGui::Text("Image Metadata Size: %zu bytes", g_loadedImage.m_metadata.size());
ImGui::Separator();
ImGui::Text("Image Metadata: ");
for (const auto &entry : g_loadedImage.m_metadata)
{
ImGui::Text("%s: %s", entry.first.c_str(), entry.second.c_str());
}
} // Closing the if statement for g_imageIsLoaded
ImGui::End(); // End Image Exif
// "Edit Image" window
ImGui::Begin("Edit Image");
if (ImGui::CollapsingHeader("Histogram", ImGuiTreeNodeFlags_DefaultOpen)) {
DrawHistogramWidget("ExifHistogram", ImVec2(-1, 256));
}
// --- Edit Image (Right) ---
ImGui::Begin("Edit Image");
// --- Pipeline Configuration ---
ImGui::SeparatorText("Processing Pipeline");
// Input Color Space Selector
ImGui::Text("Input Color Space:");
ImGui::SameLine();
if (ImGui::BeginCombo("##InputCS", ColorSpaceToString(g_inputColorSpace)))
{
if (ImGui::Selectable(ColorSpaceToString(ColorSpace::LINEAR_SRGB), g_inputColorSpace == ColorSpace::LINEAR_SRGB))
{
g_inputColorSpace = ColorSpace::LINEAR_SRGB;
}
if (ImGui::Selectable(ColorSpaceToString(ColorSpace::SRGB), g_inputColorSpace == ColorSpace::SRGB))
{
g_inputColorSpace = ColorSpace::SRGB;
}
// Add other spaces later
ImGui::EndCombo();
}
// Output Color Space Selector
ImGui::Text("Output Color Space:");
ImGui::SameLine();
if (ImGui::BeginCombo("##OutputCS", ColorSpaceToString(g_outputColorSpace)))
{
if (ImGui::Selectable(ColorSpaceToString(ColorSpace::LINEAR_SRGB), g_outputColorSpace == ColorSpace::LINEAR_SRGB))
{
g_outputColorSpace = ColorSpace::LINEAR_SRGB;
}
if (ImGui::Selectable(ColorSpaceToString(ColorSpace::SRGB), g_outputColorSpace == ColorSpace::SRGB))
{
g_outputColorSpace = ColorSpace::SRGB;
}
// Add other spaces later
ImGui::EndCombo();
}
ImGui::Separator();
ImGui::Text("Operation Order:");
// Drag-and-Drop Reordering List
// Store indices or pointers to allow reordering `g_pipeline.activeOperations`
int move_from = -1, move_to = -1;
for (int i = 0; i < g_pipeline.activeOperations.size(); ++i)
{
PipelineOperation &op = g_pipeline.activeOperations[i];
ImGui::PushID(i); // Ensure unique IDs for controls within the loop
// Checkbox to enable/disable
ImGui::Checkbox("", &op.enabled);
ImGui::SameLine();
// Simple Up/Down Buttons (alternative or complementary to DND)
if (ImGui::ArrowButton("##up", ImGuiDir_Up) && i > 0)
{
move_from = i;
move_to = i - 1;
}
ImGui::SameLine();
if (ImGui::ArrowButton("##down", ImGuiDir_Down) && i < g_pipeline.activeOperations.size() - 1)
{
move_from = i;
move_to = i + 1;
}
ImGui::SameLine();
// Selectable for drag/drop source/target
ImGui::Selectable(op.name.c_str(), false, 0, ImVec2(ImGui::GetContentRegionAvail().x - 30, 0)); // Leave space for buttons
// Simple Drag Drop implementation
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None))
{
ImGui::SetDragDropPayload("PIPELINE_OP_DND", &i, sizeof(int));
ImGui::Text("Move %s", op.name.c_str());
ImGui::EndDragDropSource();
}
if (ImGui::BeginDragDropTarget())
{
if (const ImGuiPayload *payload = ImGui::AcceptDragDropPayload("PIPELINE_OP_DND"))
{
IM_ASSERT(payload->DataSize == sizeof(int));
move_from = *(const int *)payload->Data;
move_to = i;
}
ImGui::EndDragDropTarget();
}
ImGui::PopID();
}
// Process move if detected
if (move_from != -1 && move_to != -1 && move_from != move_to)
{
PipelineOperation temp = g_pipeline.activeOperations[move_from];
g_pipeline.activeOperations.erase(g_pipeline.activeOperations.begin() + move_from);
g_pipeline.activeOperations.insert(g_pipeline.activeOperations.begin() + move_to, temp);
printf("Moved operation %d to %d\n", move_from, move_to);
}
ImGui::SeparatorText("Adjustments");
// --- Adjustment Controls ---
// Group sliders under collapsing headers as before
// The slider variables (exposure, contrast, etc.) are now directly
// linked to the PipelineOperation structs via pointers.
if (ImGui::CollapsingHeader("White Balance", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::SliderFloat("Temperature", &temperature, 1000.0f, 20000.0f);
ImGui::SliderFloat("Tint", &tint, -100.0f, 100.0f);
}
ImGui::Separator();
if (ImGui::CollapsingHeader("Tone", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::SliderFloat("Exposure", &exposure, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic);
ImGui::SliderFloat("Contrast", &contrast, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic);
ImGui::Separator();
ImGui::SliderFloat("Highlights", &highlights, -100.0f, 100.0f);
ImGui::SliderFloat("Shadows", &shadows, -100.0f, 100.0f);
ImGui::SliderFloat("Whites", &whites, -100.0f, 100.0f);
ImGui::SliderFloat("Blacks", &blacks, -100.0f, 100.0f);
}
ImGui::Separator();
if (ImGui::CollapsingHeader("Presence", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::SliderFloat("Texture", &texture, -100.0f, 100.0f);
ImGui::SliderFloat("Clarity", &clarity, -100.0f, 100.0f);
ImGui::SliderFloat("Dehaze", &dehaze, -100.0f, 100.0f);
ImGui::Separator();
ImGui::SliderFloat("Vibrance", &vibrance, -100.0f, 100.0f);
ImGui::SliderFloat("Saturation", &saturation, -100.0f, 100.0f);
}
ImGui::Separator();
ImGui::SeparatorText("Transform");
if (!g_cropActive)
{
if (ImGui::Button("Crop & Straighten"))
{ // Combine visually for now
g_cropActive = true;
g_cropRectNorm = ImVec4(0.0f, 0.0f, 1.0f, 1.0f); // Reset crop on activation
g_cropRectNormInitial = g_cropRectNorm; // Store initial state
g_activeCropHandle = CropHandle::NONE;
g_isDraggingCrop = false;
// Update Original aspect ratio if needed
if (g_loadedImage.getHeight() > 0)
{
for (auto &opt : g_aspectRatios)
{
if (strcmp(opt.name, "Original") == 0)
{
opt.ratio = float(g_loadedImage.getWidth()) / float(g_loadedImage.getHeight());
break;
}
}
}
// If current selection is 'Original', update g_cropAspectRatio
if (g_selectedAspectRatioIndex >= 0 && g_selectedAspectRatioIndex < g_aspectRatios.size() &&
strcmp(g_aspectRatios[g_selectedAspectRatioIndex].name, "Original") == 0)
{
g_cropAspectRatio = g_aspectRatios[g_selectedAspectRatioIndex].ratio;
}
printf("Crop tool activated.\n");
}
}
else
{
ImGui::Text("Crop Active");
// Aspect Ratio Selector
if (ImGui::BeginCombo("Aspect Ratio", g_aspectRatios[g_selectedAspectRatioIndex].name))
{
for (int i = 0; i < g_aspectRatios.size(); ++i)
{
bool is_selected = (g_selectedAspectRatioIndex == i);
if (ImGui::Selectable(g_aspectRatios[i].name, is_selected))
{
g_selectedAspectRatioIndex = i;
g_cropAspectRatio = g_aspectRatios[i].ratio;
// Optional: Reset crop rectangle slightly or adjust existing one
// to the new ratio if transitioning from freeform? Or just let user resize.
printf("Selected aspect ratio: %s (%.2f)\n", g_aspectRatios[i].name, g_cropAspectRatio);
}
if (is_selected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
// Apply/Cancel Buttons
if (ImGui::Button("Apply Crop"))
{
printf("Apply Crop button clicked.\n");
// <<< --- CALL FUNCTION TO APPLY CROP --- >>>
if (ApplyCropToImage(g_loadedImage, g_cropRectNorm))
{
printf("Crop applied successfully. Reloading texture and resetting pipeline.\n");
// Reload texture with cropped data
if (!loadImageTexture(g_loadedImage))
{
fprintf(stderr, "Error reloading texture after crop!\n");
g_imageIsLoaded = false; // Mark as not usable
}
// Reset pipeline FBOs/Textures due to size change
g_pipeline.ResetResources();
}
else
{
fprintf(stderr, "Failed to apply crop to image data.\n");
// Optionally show error to user
}
// Reset state after applying
g_cropActive = false;
g_cropRectNorm = ImVec4(0.0f, 0.0f, 1.0f, 1.0f);
g_activeCropHandle = CropHandle::NONE;
g_isDraggingCrop = false;
}
ImGui::SameLine();
if (ImGui::Button("Cancel Crop"))
{
printf("Crop cancelled.\n");
g_cropActive = false;
g_cropRectNorm = ImVec4(0.0f, 0.0f, 1.0f, 1.0f); // Reset to full image
g_activeCropHandle = CropHandle::NONE;
g_isDraggingCrop = false;
}
}
ImGui::End(); // End Edit Image
ImGui::End(); // End MainDockspaceWindow
}
else
{
// Option 2: Simple full-screen window (no docking)
ImGuiViewport *viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoBringToFrontOnFocus;
ImGui::Begin("FullImageViewer", nullptr, window_flags);
ImGui::Text("Image Viewer");
ImGuiTexInspect::BeginInspectorPanel("Image Inspector", g_loadedImage.m_textureId, ImVec2(g_loadedImage.m_width, g_loadedImage.m_height), ImGuiTexInspect::InspectorFlags_NoTooltip);
ImGuiTexInspect::EndInspectorPanel();
ImGui::End();
}
// Rendering
ImGui::Render();
glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
// Update and Render additional Platform Windows
// (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere.
// For this specific demo app we could also call SDL_GL_MakeCurrent(window, gl_context) directly)
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
SDL_Window *backup_current_window = SDL_GL_GetCurrentWindow();
SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext();
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
}
SDL_GL_SwapWindow(window);
}
#ifdef __EMSCRIPTEN__
EMSCRIPTEN_MAINLOOP_END;
#endif
// Cleanup
// --- Cleanup ---
// Destroy operations which will delete shader programs
g_allOperations.clear(); // Deletes PipelineOperation objects and their shaders
g_pipeline.activeOperations.clear(); // Clear the list in pipeline (doesn't own shaders)
// Pipeline destructor handles FBOs/VAO etc.
// Delete the originally loaded texture
if (g_loadedImage.m_textureId != 0)
{
glDeleteTextures(1, &g_loadedImage.m_textureId);
g_loadedImage.m_textureId = 0;
}
if (g_histogramResourcesInitialized) {
if (g_histogramSSBO) glDeleteBuffers(1, &g_histogramSSBO);
if (g_histogramComputeShader) glDeleteProgram(g_histogramComputeShader);
printf("Cleaned up histogram resources.\n");
}
ImGuiTexInspect::Shutdown();
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
SDL_GL_DeleteContext(gl_context);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}