| 1 | /* |
| 2 | nanogui/imageview.cpp -- Widget used to display images. |
| 3 | |
| 4 | The image view widget was contributed by Stefan Ivanov. |
| 5 | |
| 6 | NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>. |
| 7 | The widget drawing code is based on the NanoVG demo application |
| 8 | by Mikko Mononen. |
| 9 | |
| 10 | All rights reserved. Use of this source code is governed by a |
| 11 | BSD-style license that can be found in the LICENSE.txt file. |
| 12 | */ |
| 13 | |
| 14 | #include <nanogui/imageview.h> |
| 15 | #include <nanogui/window.h> |
| 16 | #include <nanogui/screen.h> |
| 17 | #include <nanogui/theme.h> |
| 18 | #include <cmath> |
| 19 | |
| 20 | NAMESPACE_BEGIN(nanogui) |
| 21 | |
| 22 | namespace { |
| 23 | std::vector<std::string> tokenize(const std::string &string, |
| 24 | const std::string &delim = "\n" , |
| 25 | bool includeEmpty = false) { |
| 26 | std::string::size_type lastPos = 0, pos = string.find_first_of(delim, lastPos); |
| 27 | std::vector<std::string> tokens; |
| 28 | |
| 29 | while (lastPos != std::string::npos) { |
| 30 | std::string substr = string.substr(lastPos, pos - lastPos); |
| 31 | if (!substr.empty() || includeEmpty) |
| 32 | tokens.push_back(std::move(substr)); |
| 33 | lastPos = pos; |
| 34 | if (lastPos != std::string::npos) { |
| 35 | lastPos += 1; |
| 36 | pos = string.find_first_of(delim, lastPos); |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | return tokens; |
| 41 | } |
| 42 | |
| 43 | constexpr char const *const defaultImageViewVertexShader = |
| 44 | R"(#version 330 |
| 45 | uniform vec2 scaleFactor; |
| 46 | uniform vec2 position; |
| 47 | in vec2 vertex; |
| 48 | out vec2 uv; |
| 49 | void main() { |
| 50 | uv = vertex; |
| 51 | vec2 scaledVertex = (vertex * scaleFactor) + position; |
| 52 | gl_Position = vec4(2.0*scaledVertex.x - 1.0, |
| 53 | 1.0 - 2.0*scaledVertex.y, |
| 54 | 0.0, 1.0); |
| 55 | |
| 56 | })" ; |
| 57 | |
| 58 | constexpr char const *const defaultImageViewFragmentShader = |
| 59 | R"(#version 330 |
| 60 | uniform sampler2D image; |
| 61 | out vec4 color; |
| 62 | in vec2 uv; |
| 63 | void main() { |
| 64 | color = texture(image, uv); |
| 65 | })" ; |
| 66 | |
| 67 | } |
| 68 | |
| 69 | ImageView::ImageView(Widget* parent, GLuint imageID) |
| 70 | : Widget(parent), mImageID(imageID), mScale(1.0f), mOffset(Vector2f::Zero()), |
| 71 | mFixedScale(false), mFixedOffset(false), mPixelInfoCallback(nullptr) { |
| 72 | updateImageParameters(); |
| 73 | mShader.init("ImageViewShader" , defaultImageViewVertexShader, |
| 74 | defaultImageViewFragmentShader); |
| 75 | |
| 76 | MatrixXu indices(3, 2); |
| 77 | indices.col(0) << 0, 1, 2; |
| 78 | indices.col(1) << 2, 3, 1; |
| 79 | |
| 80 | MatrixXf vertices(2, 4); |
| 81 | vertices.col(0) << 0, 0; |
| 82 | vertices.col(1) << 1, 0; |
| 83 | vertices.col(2) << 0, 1; |
| 84 | vertices.col(3) << 1, 1; |
| 85 | |
| 86 | mShader.bind(); |
| 87 | mShader.uploadIndices(indices); |
| 88 | mShader.uploadAttrib("vertex" , vertices); |
| 89 | } |
| 90 | |
| 91 | ImageView::~ImageView() { |
| 92 | mShader.free(); |
| 93 | } |
| 94 | |
| 95 | void ImageView::bindImage(GLuint imageId) { |
| 96 | mImageID = imageId; |
| 97 | updateImageParameters(); |
| 98 | fit(); |
| 99 | } |
| 100 | |
| 101 | Vector2f ImageView::imageCoordinateAt(const Vector2f& position) const { |
| 102 | auto imagePosition = position - mOffset; |
| 103 | return imagePosition / mScale; |
| 104 | } |
| 105 | |
| 106 | Vector2f ImageView::clampedImageCoordinateAt(const Vector2f& position) const { |
| 107 | auto imageCoordinate = imageCoordinateAt(position); |
| 108 | return imageCoordinate.cwiseMax(Vector2f::Zero()).cwiseMin(imageSizeF()); |
| 109 | } |
| 110 | |
| 111 | Vector2f ImageView::positionForCoordinate(const Vector2f& imageCoordinate) const { |
| 112 | return mScale*imageCoordinate + mOffset; |
| 113 | } |
| 114 | |
| 115 | void ImageView::setImageCoordinateAt(const Vector2f& position, const Vector2f& imageCoordinate) { |
| 116 | // Calculate where the new offset must be in order to satisfy the image position equation. |
| 117 | // Round the floating point values to balance out the floating point to integer conversions. |
| 118 | mOffset = position - (imageCoordinate * mScale); |
| 119 | |
| 120 | // Clamp offset so that the image remains near the screen. |
| 121 | mOffset = mOffset.cwiseMin(sizeF()).cwiseMax(-scaledImageSizeF()); |
| 122 | } |
| 123 | |
| 124 | void ImageView::center() { |
| 125 | mOffset = (sizeF() - scaledImageSizeF()) / 2; |
| 126 | } |
| 127 | |
| 128 | void ImageView::fit() { |
| 129 | // Calculate the appropriate scaling factor. |
| 130 | mScale = (sizeF().cwiseQuotient(imageSizeF())).minCoeff(); |
| 131 | center(); |
| 132 | } |
| 133 | |
| 134 | void ImageView::setScaleCentered(float scale) { |
| 135 | auto centerPosition = sizeF() / 2; |
| 136 | auto p = imageCoordinateAt(centerPosition); |
| 137 | mScale = scale; |
| 138 | setImageCoordinateAt(centerPosition, p); |
| 139 | } |
| 140 | |
| 141 | void ImageView::moveOffset(const Vector2f& delta) { |
| 142 | // Apply the delta to the offset. |
| 143 | mOffset += delta; |
| 144 | |
| 145 | // Prevent the image from going out of bounds. |
| 146 | auto scaledSize = scaledImageSizeF(); |
| 147 | if (mOffset.x() + scaledSize.x() < 0) |
| 148 | mOffset.x() = -scaledSize.x(); |
| 149 | if (mOffset.x() > sizeF().x()) |
| 150 | mOffset.x() = sizeF().x(); |
| 151 | if (mOffset.y() + scaledSize.y() < 0) |
| 152 | mOffset.y() = -scaledSize.y(); |
| 153 | if (mOffset.y() > sizeF().y()) |
| 154 | mOffset.y() = sizeF().y(); |
| 155 | } |
| 156 | |
| 157 | void ImageView::zoom(int amount, const Vector2f& focusPosition) { |
| 158 | auto focusedCoordinate = imageCoordinateAt(focusPosition); |
| 159 | float scaleFactor = std::pow(mZoomSensitivity, amount); |
| 160 | mScale = std::max(0.01f, scaleFactor * mScale); |
| 161 | setImageCoordinateAt(focusPosition, focusedCoordinate); |
| 162 | } |
| 163 | |
| 164 | bool ImageView::mouseDragEvent(const Vector2i& p, const Vector2i& rel, int button, int /*modifiers*/) { |
| 165 | if ((button & (1 << GLFW_MOUSE_BUTTON_LEFT)) != 0 && !mFixedOffset) { |
| 166 | setImageCoordinateAt((p + rel).cast<float>(), imageCoordinateAt(p.cast<float>())); |
| 167 | return true; |
| 168 | } |
| 169 | return false; |
| 170 | } |
| 171 | |
| 172 | bool ImageView::gridVisible() const { |
| 173 | return (mGridThreshold != -1) && (mScale > mGridThreshold); |
| 174 | } |
| 175 | |
| 176 | bool ImageView::pixelInfoVisible() const { |
| 177 | return mPixelInfoCallback && (mPixelInfoThreshold != -1) && (mScale > mPixelInfoThreshold); |
| 178 | } |
| 179 | |
| 180 | bool ImageView::helpersVisible() const { |
| 181 | return gridVisible() || pixelInfoVisible(); |
| 182 | } |
| 183 | |
| 184 | bool ImageView::scrollEvent(const Vector2i& p, const Vector2f& rel) { |
| 185 | if (mFixedScale) |
| 186 | return false; |
| 187 | float v = rel.y(); |
| 188 | if (std::abs(v) < 1) |
| 189 | v = std::copysign(1.f, v); |
| 190 | zoom(v, (p - position()).cast<float>()); |
| 191 | return true; |
| 192 | } |
| 193 | |
| 194 | bool ImageView::keyboardEvent(int key, int /*scancode*/, int action, int modifiers) { |
| 195 | if (action) { |
| 196 | switch (key) { |
| 197 | case GLFW_KEY_LEFT: |
| 198 | if (!mFixedOffset) { |
| 199 | if (GLFW_MOD_CONTROL & modifiers) |
| 200 | moveOffset(Vector2f(30, 0)); |
| 201 | else |
| 202 | moveOffset(Vector2f(10, 0)); |
| 203 | return true; |
| 204 | } |
| 205 | break; |
| 206 | case GLFW_KEY_RIGHT: |
| 207 | if (!mFixedOffset) { |
| 208 | if (GLFW_MOD_CONTROL & modifiers) |
| 209 | moveOffset(Vector2f(-30, 0)); |
| 210 | else |
| 211 | moveOffset(Vector2f(-10, 0)); |
| 212 | return true; |
| 213 | } |
| 214 | break; |
| 215 | case GLFW_KEY_DOWN: |
| 216 | if (!mFixedOffset) { |
| 217 | if (GLFW_MOD_CONTROL & modifiers) |
| 218 | moveOffset(Vector2f(0, -30)); |
| 219 | else |
| 220 | moveOffset(Vector2f(0, -10)); |
| 221 | return true; |
| 222 | } |
| 223 | break; |
| 224 | case GLFW_KEY_UP: |
| 225 | if (!mFixedOffset) { |
| 226 | if (GLFW_MOD_CONTROL & modifiers) |
| 227 | moveOffset(Vector2f(0, 30)); |
| 228 | else |
| 229 | moveOffset(Vector2f(0, 10)); |
| 230 | return true; |
| 231 | } |
| 232 | break; |
| 233 | } |
| 234 | } |
| 235 | return false; |
| 236 | } |
| 237 | |
| 238 | bool ImageView::keyboardCharacterEvent(unsigned int codepoint) { |
| 239 | switch (codepoint) { |
| 240 | case '-': |
| 241 | if (!mFixedScale) { |
| 242 | zoom(-1, sizeF() / 2); |
| 243 | return true; |
| 244 | } |
| 245 | break; |
| 246 | case '+': |
| 247 | if (!mFixedScale) { |
| 248 | zoom(1, sizeF() / 2); |
| 249 | return true; |
| 250 | } |
| 251 | break; |
| 252 | case 'c': |
| 253 | if (!mFixedOffset) { |
| 254 | center(); |
| 255 | return true; |
| 256 | } |
| 257 | break; |
| 258 | case 'f': |
| 259 | if (!mFixedOffset && !mFixedScale) { |
| 260 | fit(); |
| 261 | return true; |
| 262 | } |
| 263 | break; |
| 264 | case '1': case '2': case '3': case '4': case '5': |
| 265 | case '6': case '7': case '8': case '9': |
| 266 | if (!mFixedScale) { |
| 267 | setScaleCentered(1 << (codepoint - '1')); |
| 268 | return true; |
| 269 | } |
| 270 | break; |
| 271 | default: |
| 272 | return false; |
| 273 | } |
| 274 | return false; |
| 275 | } |
| 276 | |
| 277 | Vector2i ImageView::preferredSize(NVGcontext* /*ctx*/) const { |
| 278 | return mImageSize; |
| 279 | } |
| 280 | |
| 281 | void ImageView::performLayout(NVGcontext* ctx) { |
| 282 | Widget::performLayout(ctx); |
| 283 | center(); |
| 284 | } |
| 285 | |
| 286 | void ImageView::draw(NVGcontext* ctx) { |
| 287 | Widget::draw(ctx); |
| 288 | nvgEndFrame(ctx); // Flush the NanoVG draw stack, not necessary to call nvgBeginFrame afterwards. |
| 289 | |
| 290 | drawImageBorder(ctx); |
| 291 | |
| 292 | // Calculate several variables that need to be send to OpenGL in order for the image to be |
| 293 | // properly displayed inside the widget. |
| 294 | const Screen* screen = dynamic_cast<const Screen*>(this->window()->parent()); |
| 295 | assert(screen); |
| 296 | Vector2f screenSize = screen->size().cast<float>(); |
| 297 | Vector2f scaleFactor = mScale * imageSizeF().cwiseQuotient(screenSize); |
| 298 | Vector2f positionInScreen = absolutePosition().cast<float>(); |
| 299 | Vector2f positionAfterOffset = positionInScreen + mOffset; |
| 300 | Vector2f imagePosition = positionAfterOffset.cwiseQuotient(screenSize); |
| 301 | glEnable(GL_SCISSOR_TEST); |
| 302 | float r = screen->pixelRatio(); |
| 303 | glScissor(positionInScreen.x() * r, |
| 304 | (screenSize.y() - positionInScreen.y() - size().y()) * r, |
| 305 | size().x() * r, size().y() * r); |
| 306 | mShader.bind(); |
| 307 | glActiveTexture(GL_TEXTURE0); |
| 308 | glBindTexture(GL_TEXTURE_2D, mImageID); |
| 309 | mShader.setUniform("image" , 0); |
| 310 | mShader.setUniform("scaleFactor" , scaleFactor); |
| 311 | mShader.setUniform("position" , imagePosition); |
| 312 | mShader.drawIndexed(GL_TRIANGLES, 0, 2); |
| 313 | glDisable(GL_SCISSOR_TEST); |
| 314 | |
| 315 | if (helpersVisible()) |
| 316 | drawHelpers(ctx); |
| 317 | |
| 318 | drawWidgetBorder(ctx); |
| 319 | } |
| 320 | |
| 321 | void ImageView::updateImageParameters() { |
| 322 | // Query the width of the OpenGL texture. |
| 323 | glBindTexture(GL_TEXTURE_2D, mImageID); |
| 324 | GLint w, h; |
| 325 | glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); |
| 326 | glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); |
| 327 | mImageSize = Vector2i(w, h); |
| 328 | } |
| 329 | |
| 330 | void ImageView::drawWidgetBorder(NVGcontext* ctx) const { |
| 331 | nvgBeginPath(ctx); |
| 332 | nvgStrokeWidth(ctx, 1); |
| 333 | nvgRoundedRect(ctx, mPos.x() + 0.5f, mPos.y() + 0.5f, mSize.x() - 1, |
| 334 | mSize.y() - 1, 0); |
| 335 | nvgStrokeColor(ctx, mTheme->mWindowPopup); |
| 336 | nvgStroke(ctx); |
| 337 | |
| 338 | nvgBeginPath(ctx); |
| 339 | nvgRoundedRect(ctx, mPos.x() + 0.5f, mPos.y() + 0.5f, mSize.x() - 1, |
| 340 | mSize.y() - 1, mTheme->mButtonCornerRadius); |
| 341 | nvgStrokeColor(ctx, mTheme->mBorderDark); |
| 342 | nvgStroke(ctx); |
| 343 | } |
| 344 | |
| 345 | void ImageView::drawImageBorder(NVGcontext* ctx) const { |
| 346 | nvgSave(ctx); |
| 347 | nvgBeginPath(ctx); |
| 348 | nvgScissor(ctx, mPos.x(), mPos.y(), mSize.x(), mSize.y()); |
| 349 | nvgStrokeWidth(ctx, 1.0f); |
| 350 | Vector2i borderPosition = mPos + mOffset.cast<int>(); |
| 351 | Vector2i borderSize = scaledImageSizeF().cast<int>(); |
| 352 | nvgRect(ctx, borderPosition.x() - 0.5f, borderPosition.y() - 0.5f, |
| 353 | borderSize.x() + 1, borderSize.y() + 1); |
| 354 | nvgStrokeColor(ctx, Color(1.0f, 1.0f, 1.0f, 1.0f)); |
| 355 | nvgStroke(ctx); |
| 356 | nvgResetScissor(ctx); |
| 357 | nvgRestore(ctx); |
| 358 | } |
| 359 | |
| 360 | void ImageView::drawHelpers(NVGcontext* ctx) const { |
| 361 | // We need to apply mPos after the transformation to account for the position of the widget |
| 362 | // relative to the parent. |
| 363 | Vector2f upperLeftCorner = positionForCoordinate(Vector2f::Zero()) + positionF(); |
| 364 | Vector2f lowerRightCorner = positionForCoordinate(imageSizeF()) + positionF(); |
| 365 | if (gridVisible()) |
| 366 | drawPixelGrid(ctx, upperLeftCorner, lowerRightCorner, mScale); |
| 367 | if (pixelInfoVisible()) |
| 368 | drawPixelInfo(ctx, mScale); |
| 369 | } |
| 370 | |
| 371 | void ImageView::drawPixelGrid(NVGcontext* ctx, const Vector2f& upperLeftCorner, |
| 372 | const Vector2f& lowerRightCorner, float stride) { |
| 373 | nvgBeginPath(ctx); |
| 374 | |
| 375 | // Draw the vertical grid lines |
| 376 | float currentX = upperLeftCorner.x(); |
| 377 | while (currentX <= lowerRightCorner.x()) { |
| 378 | nvgMoveTo(ctx, std::round(currentX), std::round(upperLeftCorner.y())); |
| 379 | nvgLineTo(ctx, std::round(currentX), std::round(lowerRightCorner.y())); |
| 380 | currentX += stride; |
| 381 | } |
| 382 | |
| 383 | // Draw the horizontal grid lines |
| 384 | float currentY = upperLeftCorner.y(); |
| 385 | while (currentY <= lowerRightCorner.y()) { |
| 386 | nvgMoveTo(ctx, std::round(upperLeftCorner.x()), std::round(currentY)); |
| 387 | nvgLineTo(ctx, std::round(lowerRightCorner.x()), std::round(currentY)); |
| 388 | currentY += stride; |
| 389 | } |
| 390 | |
| 391 | nvgStrokeWidth(ctx, 1.0f); |
| 392 | nvgStrokeColor(ctx, Color(1.0f, 1.0f, 1.0f, 0.2f)); |
| 393 | nvgStroke(ctx); |
| 394 | } |
| 395 | |
| 396 | void ImageView::drawPixelInfo(NVGcontext* ctx, float stride) const { |
| 397 | // Extract the image coordinates at the two corners of the widget. |
| 398 | Vector2i topLeft = clampedImageCoordinateAt(Vector2f::Zero()) |
| 399 | .unaryExpr([](float x) { return std::floor(x); }) |
| 400 | .cast<int>(); |
| 401 | |
| 402 | Vector2i bottomRight = clampedImageCoordinateAt(sizeF()) |
| 403 | .unaryExpr([](float x) { return std::ceil(x); }) |
| 404 | .cast<int>(); |
| 405 | |
| 406 | // Extract the positions for where to draw the text. |
| 407 | Vector2f currentCellPosition = |
| 408 | (positionF() + positionForCoordinate(topLeft.cast<float>())); |
| 409 | |
| 410 | float xInitialPosition = currentCellPosition.x(); |
| 411 | int xInitialIndex = topLeft.x(); |
| 412 | |
| 413 | // Properly scale the pixel information for the given stride. |
| 414 | auto fontSize = stride * mFontScaleFactor; |
| 415 | static constexpr float maxFontSize = 30.0f; |
| 416 | fontSize = fontSize > maxFontSize ? maxFontSize : fontSize; |
| 417 | nvgBeginPath(ctx); |
| 418 | nvgFontSize(ctx, fontSize); |
| 419 | nvgTextAlign(ctx, NVG_ALIGN_CENTER | NVG_ALIGN_TOP); |
| 420 | nvgFontFace(ctx, "sans" ); |
| 421 | while (topLeft.y() != bottomRight.y()) { |
| 422 | while (topLeft.x() != bottomRight.x()) { |
| 423 | writePixelInfo(ctx, currentCellPosition, topLeft, stride, fontSize); |
| 424 | currentCellPosition.x() += stride; |
| 425 | ++topLeft.x(); |
| 426 | } |
| 427 | currentCellPosition.x() = xInitialPosition; |
| 428 | currentCellPosition.y() += stride; |
| 429 | ++topLeft.y(); |
| 430 | topLeft.x() = xInitialIndex; |
| 431 | } |
| 432 | } |
| 433 | |
| 434 | void ImageView::writePixelInfo(NVGcontext* ctx, const Vector2f& cellPosition, |
| 435 | const Vector2i& pixel, float stride, float fontSize) const { |
| 436 | auto pixelData = mPixelInfoCallback(pixel); |
| 437 | auto pixelDataRows = tokenize(pixelData.first); |
| 438 | |
| 439 | // If no data is provided for this pixel then simply return. |
| 440 | if (pixelDataRows.empty()) |
| 441 | return; |
| 442 | |
| 443 | nvgFillColor(ctx, pixelData.second); |
| 444 | float yOffset = (stride - fontSize * pixelDataRows.size()) / 2; |
| 445 | for (size_t i = 0; i != pixelDataRows.size(); ++i) { |
| 446 | nvgText(ctx, cellPosition.x() + stride / 2, cellPosition.y() + yOffset, |
| 447 | pixelDataRows[i].data(), nullptr); |
| 448 | yOffset += fontSize; |
| 449 | } |
| 450 | } |
| 451 | |
| 452 | NAMESPACE_END(nanogui) |
| 453 | |