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
20NAMESPACE_BEGIN(nanogui)
21
22namespace {
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
69ImageView::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
91ImageView::~ImageView() {
92 mShader.free();
93}
94
95void ImageView::bindImage(GLuint imageId) {
96 mImageID = imageId;
97 updateImageParameters();
98 fit();
99}
100
101Vector2f ImageView::imageCoordinateAt(const Vector2f& position) const {
102 auto imagePosition = position - mOffset;
103 return imagePosition / mScale;
104}
105
106Vector2f ImageView::clampedImageCoordinateAt(const Vector2f& position) const {
107 auto imageCoordinate = imageCoordinateAt(position);
108 return imageCoordinate.cwiseMax(Vector2f::Zero()).cwiseMin(imageSizeF());
109}
110
111Vector2f ImageView::positionForCoordinate(const Vector2f& imageCoordinate) const {
112 return mScale*imageCoordinate + mOffset;
113}
114
115void 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
124void ImageView::center() {
125 mOffset = (sizeF() - scaledImageSizeF()) / 2;
126}
127
128void ImageView::fit() {
129 // Calculate the appropriate scaling factor.
130 mScale = (sizeF().cwiseQuotient(imageSizeF())).minCoeff();
131 center();
132}
133
134void ImageView::setScaleCentered(float scale) {
135 auto centerPosition = sizeF() / 2;
136 auto p = imageCoordinateAt(centerPosition);
137 mScale = scale;
138 setImageCoordinateAt(centerPosition, p);
139}
140
141void 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
157void 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
164bool 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
172bool ImageView::gridVisible() const {
173 return (mGridThreshold != -1) && (mScale > mGridThreshold);
174}
175
176bool ImageView::pixelInfoVisible() const {
177 return mPixelInfoCallback && (mPixelInfoThreshold != -1) && (mScale > mPixelInfoThreshold);
178}
179
180bool ImageView::helpersVisible() const {
181 return gridVisible() || pixelInfoVisible();
182}
183
184bool 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
194bool 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
238bool 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
277Vector2i ImageView::preferredSize(NVGcontext* /*ctx*/) const {
278 return mImageSize;
279}
280
281void ImageView::performLayout(NVGcontext* ctx) {
282 Widget::performLayout(ctx);
283 center();
284}
285
286void 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
321void 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
330void 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
345void 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
360void 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
371void 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
396void 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
434void 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
452NAMESPACE_END(nanogui)
453