diff --git a/src/examples/bar_component.cpp b/src/examples/bar_component.cpp index c745dcb1d..440ed9c57 100644 --- a/src/examples/bar_component.cpp +++ b/src/examples/bar_component.cpp @@ -9,6 +9,7 @@ #include #include "./bar_component.hpp" +#include "./xr_renderer.hpp" #include "./content.hpp" namespace jsar::example @@ -59,6 +60,7 @@ namespace jsar::example initGLProgram(); createGeometry(); createBarTexture(); + createButtonGeometry(); if (glGetError() != GL_NO_ERROR) cout << "OpenGL error on BarComponent init" << endl; @@ -69,8 +71,13 @@ namespace jsar::example glDeleteVertexArrays(1, &vao_); glDeleteBuffers(1, &vertexVBO_); glDeleteBuffers(1, &instanceVBO_); - glDeleteProgram(program_); glDeleteTextures(1, &barTexture_); + + glDeleteVertexArrays(1, &buttonVAO_); + glDeleteBuffers(1, &buttonVertexVBO_); + glDeleteBuffers(1, &buttonInstanceVBO_); + + glDeleteProgram(barShader_.ID); } void BarComponent::addContent(Content *content) @@ -135,56 +142,7 @@ namespace jsar::example void BarComponent::initGLProgram() { - // Create and compile shaders - GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); - glShaderSource(vertexShader, 1, &barVertSource_, NULL); - glCompileShader(vertexShader); - - GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); - glShaderSource(fragmentShader, 1, &barFragSource_, NULL); - glCompileShader(fragmentShader); - - // Check for shader compile errors - int success; - char infoLog[512]; - glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); - if (!success) - { - glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); - cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" - << infoLog << endl; - } - - glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); - if (!success) - { - glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); - cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" - << infoLog << endl; - } - - // Create shader program - program_ = glCreateProgram(); - glAttachShader(program_, vertexShader); - glAttachShader(program_, fragmentShader); - glLinkProgram(program_); - - // Check for linking errors - glGetProgramiv(program_, GL_LINK_STATUS, &success); - if (!success) - { - glGetProgramInfoLog(program_, 512, NULL, infoLog); - cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" - << infoLog << endl; - } - - glDeleteShader(vertexShader); - glDeleteShader(fragmentShader); - - // Get uniform locations - viewMatrixLoc_ = glGetUniformLocation(program_, "view"); - projectionMatrixLoc_ = glGetUniformLocation(program_, "projection"); - textureLoc_ = glGetUniformLocation(program_, "barTexture"); + barShader_ = Shader(barVertSource_, barFragSource_); } void BarComponent::createGeometry() @@ -270,6 +228,89 @@ namespace jsar::example glBindVertexArray(0); } + void BarComponent::createButtonGeometry() + { + // Generate circle vertices for close button (center + perimeter) + // Each vertex has: position (x, y, z) + texCoord (u, v) = 5 floats + buttonVertices_.clear(); + + // Center vertex + buttonVertices_.push_back(0.0f); // Center x + buttonVertices_.push_back(0.0f); // Center y + buttonVertices_.push_back(0.0f); // Center z + buttonVertices_.push_back(0.5f); // Center texCoord u (center of texture) + buttonVertices_.push_back(0.5f); // Center texCoord v (center of texture) + + // Perimeter vertices + const float radius = CLOSE_BUTTON_RADIUS; + for (int i = 0; i <= 360; i += 10) + { + float theta = glm::radians((float)i); + float x = radius * cos(theta); + float y = radius * sin(theta); + + buttonVertices_.push_back(x); // Position x + buttonVertices_.push_back(y); // Position y + buttonVertices_.push_back(0.0f); // Position z + + // Texture coordinates mapped from circle to [0,1] range + buttonVertices_.push_back(0.5f + 0.5f * cos(theta)); // texCoord u + buttonVertices_.push_back(0.5f + 0.5f * sin(theta)); // texCoord v + } + + // Create VAO and VBO for button + glGenBuffers(1, &buttonVertexVBO_); + glGenVertexArrays(1, &buttonVAO_); + glBindVertexArray(buttonVAO_); + + glBindBuffer(GL_ARRAY_BUFFER, buttonVertexVBO_); + glBufferData(GL_ARRAY_BUFFER, buttonVertices_.size() * sizeof(float), buttonVertices_.data(), GL_STATIC_DRAW); + + // Position attribute (3 floats per vertex, stride = 5 floats) + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *)0); + glEnableVertexAttribArray(0); + + // Texture coordinate attribute + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *)(3 * sizeof(float))); + glEnableVertexAttribArray(1); + + // Instance buffer (will be updated dynamically) + glGenBuffers(1, &buttonInstanceVBO_); + glBindBuffer(GL_ARRAY_BUFFER, buttonInstanceVBO_); + + struct InstanceData + { + glm::mat4 transform; + glm::vec3 color; + }; + + size_t vec4Size = sizeof(glm::vec4); + size_t instanceSize = sizeof(InstanceData); + + glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, instanceSize, (void *)offsetof(InstanceData, transform)); + glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, instanceSize, (void *)(offsetof(InstanceData, transform) + vec4Size)); + glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, instanceSize, (void *)(offsetof(InstanceData, transform) + 2 * vec4Size)); + glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, instanceSize, (void *)(offsetof(InstanceData, transform) + 3 * vec4Size)); + glEnableVertexAttribArray(2); + glEnableVertexAttribArray(3); + glEnableVertexAttribArray(4); + glEnableVertexAttribArray(5); + + // Instance color + glVertexAttribPointer(6, 3, GL_FLOAT, GL_FALSE, instanceSize, (void *)offsetof(InstanceData, color)); + glEnableVertexAttribArray(6); + + // Set instance divisors + glVertexAttribDivisor(2, 1); + glVertexAttribDivisor(3, 1); + glVertexAttribDivisor(4, 1); + glVertexAttribDivisor(5, 1); + glVertexAttribDivisor(6, 1); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + } + void BarComponent::createBarTexture() { // Create Skia surface for rendering the bar texture with Apple design @@ -347,18 +388,16 @@ namespace jsar::example if (instances_.empty()) return; - - glUseProgram(program_); + glUseProgram(barShader_.ID); glBindVertexArray(vao_); // Bind the Skia-generated bar texture glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, barTexture_); - glUniform1i(textureLoc_, 0); - + barShader_.setInt("barTexture", 0); // Set uniforms - glUniformMatrix4fv(viewMatrixLoc_, 1, GL_FALSE, &viewMatrix[0][0]); - glUniformMatrix4fv(projectionMatrixLoc_, 1, GL_FALSE, &projectionMatrix[0][0]); + barShader_.setMat4("view", viewMatrix); + barShader_.setMat4("projection", projectionMatrix); { glEnable(GL_DEPTH_TEST); @@ -368,6 +407,15 @@ namespace jsar::example glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, instances_.size()); + + // Render button + // Each button vertex has 5 floats (position + texCoord) + GLsizei vertexCount = static_cast(buttonVertices_.size() / 5); + GLsizei instanceCount = static_cast(instances_.size()); + + glBindVertexArray(buttonVAO_); + glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, vertexCount, instanceCount); + glBindVertexArray(0); } // Reset state @@ -461,6 +509,100 @@ namespace jsar::example glBindBuffer(GL_ARRAY_BUFFER, instanceVBO_); glBufferData(GL_ARRAY_BUFFER, instanceData.size() * sizeof(InstanceData), instanceData.data(), GL_DYNAMIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); + + updateButtonInstanceBuffer(); + } + + void BarComponent::updateButtonInstanceBuffer() + { + struct ButtonInstanceData + { + glm::mat4 transform; + glm::vec3 color; + }; + vector instanceData; + instanceData.reserve(instances_.size()); + + for (const auto &inst : instances_) + { + ButtonInstanceData data; + glm::vec3 buttonPos = calculateButtonWorldPos(inst.transform[3]); + data.transform = glm::translate(glm::mat4(1.0f), buttonPos); + + // Set color based on state + if (inst.buttonIsHovered) + { + data.color = glm::vec3(0.7f, 0.7f, 0.7f); // Gray when hovered + } + else + { + data.color = glm::vec3(1.0f, 1.0f, 1.0f); // White default + } + + instanceData.push_back(data); + } + + // Update instance buffer - always has data, even if dummy + glBindBuffer(GL_ARRAY_BUFFER, buttonInstanceVBO_); + glBufferData(GL_ARRAY_BUFFER, instanceData.size() * sizeof(ButtonInstanceData), instanceData.data(), GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + } + + void BarComponent::processInput(Content *content) + { + auto windowCtx = content->getWindowContext(); + auto xrRenderer = windowCtx->xrRenderer; + assert(xrRenderer != nullptr); + bool mousePressed = (glfwGetMouseButton(windowCtx->window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS); + + // Extract ray origin and direction from the main controller target ray matrix + glm::mat4 controllerMatrix = xrRenderer->getMainControllerTargetRay(); + glm::vec3 rayOrigin = glm::vec3(controllerMatrix[3]); // Translation column + glm::vec3 rayDir = -glm::vec3(controllerMatrix[2]); // Negative Z axis (forward) + + Content *contentToClose = nullptr; // Clear pending close from previous frame + + // Check all instances + for (auto &inst : instances_) + { + glm::vec3 barPos = glm::vec3(inst.transform[3]); + glm::vec3 buttonPos = calculateButtonWorldPos(barPos); + + // Check button first (higher priority) + float distToButton = rayDistanceToPoint(rayOrigin, rayDir, buttonPos); + if (distToButton <= CLOSE_BUTTON_RADIUS) + { + inst.buttonIsHovered = true; + // Check for click - delay callback execution to avoid iterator invalidation + if (mousePressed) + { + contentToClose = inst.content; // Mark for closing (defer callback) + } + } + else + { + inst.buttonIsHovered = false; + } + } + // Process close callback after all instance processing is complete + // This callback may modify instances_ via removeContent(), which is safe now + if (contentToClose != nullptr && onCloseCallback_) + { + onCloseCallback_(contentToClose); + contentToClose = nullptr; + } + } + + float BarComponent::rayDistanceToPoint(const glm::vec3 &rayOrigin, const glm::vec3 &rayDir, const glm::vec3 &point) const + { + glm::vec3 toPoint = point - rayOrigin; + float t = glm::dot(toPoint, rayDir); + + if (t < 0) + return std::numeric_limits::max(); // Point is behind ray + + glm::vec3 closestPoint = rayOrigin + t * rayDir; + return glm::distance(closestPoint, point); } glm::mat4 BarComponent::calculateBarTransform(const glm::vec3 &contentPosition) const @@ -471,4 +613,10 @@ namespace jsar::example // Create transformation matrix return glm::translate(glm::mat4(1.0f), barPosition); } -} \ No newline at end of file + + glm::vec3 BarComponent::calculateButtonWorldPos(const glm::vec3 &contentPosition) const + { + // Button is positioned at top-right corner of the bar + return contentPosition + glm::vec3(BAR_WIDTH / 2 + CLOSE_BUTTON_RADIUS + CLOSE_BUTTON_OFFSET, 0.0f, 0.0f); + } +} diff --git a/src/examples/bar_component.hpp b/src/examples/bar_component.hpp index 90fad8769..b6f4775e6 100644 --- a/src/examples/bar_component.hpp +++ b/src/examples/bar_component.hpp @@ -10,6 +10,7 @@ #include #include "./window_ctx.hpp" +#include "./shader.hpp" namespace jsar::example { @@ -61,6 +62,13 @@ namespace jsar::example */ void setContentDragging(Content *content, bool dragging); + void processInput(Content *content); + + void setOnCloseCallback(std::function callback) + { + onCloseCallback_ = callback; + } + private: struct BarInstance { @@ -68,6 +76,8 @@ namespace jsar::example glm::mat4 transform; bool isHovered; bool isDragging; + bool buttonIsHovered; + bool buttonIsPressed; BarInstance(Content *c) : content(c) @@ -84,6 +94,11 @@ namespace jsar::example void createBarTexture(); glm::mat4 calculateBarTransform(const glm::vec3 &contentPosition) const; + void createButtonGeometry(); + void updateButtonInstanceBuffer(); + glm::vec3 calculateButtonWorldPos(const glm::vec3 &contentPosition) const; + float rayDistanceToPoint(const glm::vec3 &rayOrigin, const glm::vec3 &rayDir, const glm::vec3 &point) const; + private: std::vector instances_; @@ -91,13 +106,16 @@ namespace jsar::example GLuint vao_; GLuint vertexVBO_; // Vertex data for the bar quad GLuint instanceVBO_; // Instance transformation matrices and states - GLuint program_; - GLuint barTexture_; // Skia-generated bar texture + GLuint barTexture_; // Skia-generated bar texture + + // shader + Shader barShader_; - // Shader uniforms - GLint viewMatrixLoc_; - GLint projectionMatrixLoc_; - GLint textureLoc_; + // OpenGL resources for button instanced rendering + GLuint buttonVAO_; + GLuint buttonVertexVBO_; + GLuint buttonInstanceVBO_; + std::vector buttonVertices_; // 3D bar properties static constexpr float BAR_WIDTH = 0.20f; // World space width @@ -107,6 +125,8 @@ namespace jsar::example // Texture properties static constexpr int TEXTURE_WIDTH = 256; static constexpr int TEXTURE_HEIGHT = TEXTURE_WIDTH * (BAR_HEIGHT / BAR_WIDTH); + static constexpr float CLOSE_BUTTON_RADIUS = (BAR_HEIGHT / 2); + static constexpr float CLOSE_BUTTON_OFFSET = 0.01f; // Offset from left edge // Vertex data for a quad std::vector vertices_; @@ -114,5 +134,7 @@ namespace jsar::example // Shaders for 3D rendering const char *barVertSource_; const char *barFragSource_; + + std::function onCloseCallback_; }; } \ No newline at end of file diff --git a/src/examples/content.cpp b/src/examples/content.cpp index 0f4a85820..fc7788ddc 100644 --- a/src/examples/content.cpp +++ b/src/examples/content.cpp @@ -1,4 +1,5 @@ #include "./content.hpp" +#include "./xr_renderer.hpp" #include "./bar_component.hpp" namespace jsar::example @@ -17,6 +18,7 @@ namespace jsar::example { // Note: Bar component is now shared across all content instances // This method is kept for compatibility but bar creation is handled externally + windowCtx_ = windowCtx; } Content::~Content() @@ -92,22 +94,58 @@ namespace jsar::example void Content::updateDrag(const glm::vec2 &mousePosition) { - if (!isDragging_ || !contentRuntime_) + if (!isDragging_ || !contentRuntime_ || !windowCtx_) return; - // Calculate the mouse movement delta + // Calculate the mouse movement delta in screen space (pixels) glm::vec2 mouseDelta = mousePosition - dragStartMousePos_; - // Convert screen space movement to world space movement - // For now, we'll use a simple scale factor - this could be improved - // by using proper screen-to-world coordinate transformation - float scale = 0.001f; // Adjust this value to control drag sensitivity + // Get viewport dimensions + auto viewport = windowCtx_->drawingViewport(); + float viewportHeight = static_cast(viewport.height()); - glm::vec3 worldDelta = glm::vec3(mouseDelta.x * scale, -mouseDelta.y * scale, 0.0f); + // Get camera parameters from XR renderer + auto xrRenderer = windowCtx_->xrRenderer; + if (!xrRenderer) + return; + + // Get content position in world space + glm::vec3 contentPosition = glm::vec3(dragStartContentMatrix_[3]); + + // Get camera position in world space + glm::vec3 cameraPosition = xrRenderer->viewerPosition(); + + // Calculate distance from camera to content + float distance = glm::length(contentPosition - cameraPosition); + + // Use the camera's FOV (60 degrees by default in xr_renderer.hpp) + float fov = 60.0f; // This matches the default in XRStereoscopicRenderer + float fovInRadians = glm::radians(fov); + float tanHalfFov = tan(fovInRadians / 2.0f); + + // Calculate the scale factor based on perspective projection + // This ensures 1 pixel of mouse movement = 1 unit of world movement at the content's depth + float worldUnitsPerPixel = (2.0f * tanHalfFov * distance) / viewportHeight; + + // Convert screen space delta to world space delta + // Note: Y is inverted because screen Y goes down, world Y goes up + glm::vec3 worldDelta = glm::vec3( + mouseDelta.x * worldUnitsPerPixel, + -mouseDelta.y * worldUnitsPerPixel, + 0.0f); + + // Apply camera rotation to the delta (so drag follows screen orientation) + // For now, we only apply Y-axis rotation since the camera typically doesn't roll + glm::mat4 viewerMatrix = xrRenderer->getViewerBaseMatrix(); + glm::mat3 viewerRotation = glm::mat3(viewerMatrix); + // Only use the XZ plane rotation (ignore pitch) + glm::vec3 right = glm::normalize(glm::vec3(viewerRotation[0].x, 0.0f, viewerRotation[0].z)); + glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); + glm::vec3 rotatedDelta = right * worldDelta.x + up * worldDelta.y; // Create a new matrix with the updated position glm::mat4 newMatrix = dragStartContentMatrix_; - newMatrix[3] += glm::vec4(worldDelta, 0.0f); // Update translation component + newMatrix[3] += glm::vec4(rotatedDelta, 0.0f); // Update translation component // Update the content runtime's base matrix contentRuntime_->updateLocalBaseMatrix(newMatrix); @@ -147,4 +185,12 @@ namespace jsar::example Content *hitContent = barComponent_->checkRayIntersection(rayOrigin, rayDirection); return hitContent == this; } + + void Content::processInput() + { + if (barComponent_) + { + barComponent_->processInput(this); + } + } } \ No newline at end of file diff --git a/src/examples/content.hpp b/src/examples/content.hpp index f0dfe8e44..95d6ac286 100644 --- a/src/examples/content.hpp +++ b/src/examples/content.hpp @@ -109,8 +109,16 @@ namespace jsar::example */ bool isRayInBar(const glm::vec3 &rayOrigin, const glm::vec3 &rayDirection) const; + void processInput(); + + inline WindowContext *getWindowContext() const + { + return windowCtx_; + } + private: uint32_t id_; + WindowContext *windowCtx_; std::shared_ptr contentRuntime_; std::shared_ptr barComponent_; diff --git a/src/examples/shader.hpp b/src/examples/shader.hpp new file mode 100644 index 000000000..2eff2283c --- /dev/null +++ b/src/examples/shader.hpp @@ -0,0 +1,142 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace jsar::example +{ + class Shader + { + public: + unsigned int ID; + + Shader() = default; + + // Constructor that accepts vertex shader code and fragment shader code + Shader(const char *vShaderCode, const char *fShaderCode) + { + unsigned int vertex, fragment; + // vertex shader + vertex = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vertex, 1, &vShaderCode, NULL); + glCompileShader(vertex); + checkCompileErrors(vertex, "VERTEX"); + // fragment Shader + fragment = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fragment, 1, &fShaderCode, NULL); + glCompileShader(fragment); + checkCompileErrors(fragment, "FRAGMENT"); + // shader Program + ID = glCreateProgram(); + glAttachShader(ID, vertex); + glAttachShader(ID, fragment); + glLinkProgram(ID); + checkCompileErrors(ID, "PROGRAM"); + // delete the shaders as they're linked into our program now and no longer necessary + glDeleteShader(vertex); + glDeleteShader(fragment); + } + + // activate the shader + // ------------------------------------------------------------------------ + void use() + { + glUseProgram(ID); + } + // utility uniform functions + // ------------------------------------------------------------------------ + void setBool(const std::string &name, bool value) const + { + glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); + } + // ------------------------------------------------------------------------ + void setInt(const std::string &name, int value) const + { + glUniform1i(glGetUniformLocation(ID, name.c_str()), value); + } + // ------------------------------------------------------------------------ + void setFloat(const std::string &name, float value) const + { + glUniform1f(glGetUniformLocation(ID, name.c_str()), value); + } + // ------------------------------------------------------------------------ + void setVec2(const std::string &name, const glm::vec2 &value) const + { + glUniform2fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]); + } + void setVec2(const std::string &name, float x, float y) const + { + glUniform2f(glGetUniformLocation(ID, name.c_str()), x, y); + } + // ------------------------------------------------------------------------ + void setVec3(const std::string &name, const glm::vec3 &value) const + { + glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]); + } + void setVec3(const std::string &name, float x, float y, float z) const + { + glUniform3f(glGetUniformLocation(ID, name.c_str()), x, y, z); + } + // ------------------------------------------------------------------------ + void setVec4(const std::string &name, const glm::vec4 &value) const + { + glUniform4fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]); + } + void setVec4(const std::string &name, float x, float y, float z, float w) const + { + glUniform4f(glGetUniformLocation(ID, name.c_str()), x, y, z, w); + } + // ------------------------------------------------------------------------ + void setMat2(const std::string &name, const glm::mat2 &mat) const + { + glUniformMatrix2fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]); + } + // ------------------------------------------------------------------------ + void setMat3(const std::string &name, const glm::mat3 &mat) const + { + glUniformMatrix3fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]); + } + // ------------------------------------------------------------------------ + void setMat4(const std::string &name, const glm::mat4 &mat) const + { + glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]); + } + + void deleteProgram() + { + glDeleteProgram(ID); + } + + private: + // utility function for checking shader compilation/linking errors. + // ------------------------------------------------------------------------ + void checkCompileErrors(GLuint shader, std::string type) + { + GLint success; + GLchar infoLog[1024]; + if (type != "PROGRAM") + { + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if (!success) + { + glGetShaderInfoLog(shader, 1024, NULL, infoLog); + printf("ERROR::SHADER_COMPILATION_ERROR of type: %s\n%s\n -- --------------------------------------------------- -- \n", type.c_str(), infoLog); + } + } + else + { + glGetProgramiv(shader, GL_LINK_STATUS, &success); + if (!success) + { + glGetProgramInfoLog(shader, 1024, NULL, infoLog); + printf("ERROR::PROGRAM_LINKING_ERROR of type: %s\n%s\n -- --------------------------------------------------- -- \n", type.c_str(), infoLog); + } + } + } + }; +} diff --git a/src/examples/transmute_browser.cpp b/src/examples/transmute_browser.cpp index 5d1da2281..21639e375 100644 --- a/src/examples/transmute_browser.cpp +++ b/src/examples/transmute_browser.cpp @@ -435,7 +435,13 @@ namespace jsar::example // Create contents bar component contentsBarComponent_ = make_shared(); - + // Ensure close button removes content and components + auto close_content = [this](Content *content) + { + if (content) + closeContent(content->getId()); + }; + contentsBarComponent_->setOnCloseCallback(close_content); // Setup screen renderer and GUI setupScreenRenderer(); @@ -935,6 +941,13 @@ namespace jsar::example if (embedder_) embedder_->constellation->resetContents(); } + + for (auto it = contents_.begin(); it != contents_.end(); /* no increment */) + { + auto current = it++; // Advance iterator BEFORE processing + // Now 'current' is safe to use even if the element is erased + current->second->processInput(); + } } void TransmuteBrowser::processFrameRateInput(GLFWwindow *window)