1/*
2 nanogui/tabheader.cpp -- Widget used to control tabs.
3
4 The tab header 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/tabheader.h>
15#include <nanogui/theme.h>
16#include <nanogui/opengl.h>
17#include <numeric>
18
19NAMESPACE_BEGIN(nanogui)
20
21TabHeader::TabButton::TabButton(TabHeader &header, const std::string &label)
22 : mHeader(&header), mLabel(label) { }
23
24Vector2i TabHeader::TabButton::preferredSize(NVGcontext *ctx) const {
25 // No need to call nvg font related functions since this is done by the tab header implementation
26 float bounds[4];
27 int labelWidth = nvgTextBounds(ctx, 0, 0, mLabel.c_str(), nullptr, bounds);
28 int buttonWidth = labelWidth + 2 * mHeader->theme()->mTabButtonHorizontalPadding;
29 int buttonHeight = bounds[3] - bounds[1] + 2 * mHeader->theme()->mTabButtonVerticalPadding;
30 return Vector2i(buttonWidth, buttonHeight);
31}
32
33void TabHeader::TabButton::calculateVisibleString(NVGcontext *ctx) {
34 // The size must have been set in by the enclosing tab header.
35 NVGtextRow displayedText;
36 nvgTextBreakLines(ctx, mLabel.c_str(), nullptr, mSize.x(), &displayedText, 1);
37
38 // Check to see if the text need to be truncated.
39 if (displayedText.next[0]) {
40 auto truncatedWidth = nvgTextBounds(ctx, 0.0f, 0.0f,
41 displayedText.start, displayedText.end, nullptr);
42 auto dotsWidth = nvgTextBounds(ctx, 0.0f, 0.0f, dots, nullptr, nullptr);
43 while ((truncatedWidth + dotsWidth + mHeader->theme()->mTabButtonHorizontalPadding) > mSize.x()
44 && displayedText.end != displayedText.start) {
45 --displayedText.end;
46 truncatedWidth = nvgTextBounds(ctx, 0.0f, 0.0f,
47 displayedText.start, displayedText.end, nullptr);
48 }
49
50 // Remember the truncated width to know where to display the dots.
51 mVisibleWidth = truncatedWidth;
52 mVisibleText.last = displayedText.end;
53 } else {
54 mVisibleText.last = nullptr;
55 mVisibleWidth = 0;
56 }
57 mVisibleText.first = displayedText.start;
58}
59
60void TabHeader::TabButton::drawAtPosition(NVGcontext *ctx, const Vector2i& position, bool active) {
61 int xPos = position.x();
62 int yPos = position.y();
63 int width = mSize.x();
64 int height = mSize.y();
65 auto theme = mHeader->theme();
66
67 nvgSave(ctx);
68 nvgIntersectScissor(ctx, xPos, yPos, width+1, height);
69 if (!active) {
70 // Background gradients
71 NVGcolor gradTop = theme->mButtonGradientTopPushed;
72 NVGcolor gradBot = theme->mButtonGradientBotPushed;
73
74 // Draw the background.
75 nvgBeginPath(ctx);
76 nvgRoundedRect(ctx, xPos + 1, yPos + 1, width - 1, height + 1,
77 theme->mButtonCornerRadius);
78 NVGpaint backgroundColor = nvgLinearGradient(ctx, xPos, yPos, xPos, yPos + height,
79 gradTop, gradBot);
80 nvgFillPaint(ctx, backgroundColor);
81 nvgFill(ctx);
82 }
83
84 if (active) {
85 nvgBeginPath(ctx);
86 nvgStrokeWidth(ctx, 1.0f);
87 nvgRoundedRect(ctx, xPos + 0.5f, yPos + 1.5f, width,
88 height + 1, theme->mButtonCornerRadius);
89 nvgStrokeColor(ctx, theme->mBorderLight);
90 nvgStroke(ctx);
91
92 nvgBeginPath(ctx);
93 nvgRoundedRect(ctx, xPos + 0.5f, yPos + 0.5f, width,
94 height + 1, theme->mButtonCornerRadius);
95 nvgStrokeColor(ctx, theme->mBorderDark);
96 nvgStroke(ctx);
97 } else {
98 nvgBeginPath(ctx);
99 nvgRoundedRect(ctx, xPos + 0.5f, yPos + 1.5f, width,
100 height, theme->mButtonCornerRadius);
101 nvgStrokeColor(ctx, theme->mBorderDark);
102 nvgStroke(ctx);
103 }
104 nvgResetScissor(ctx);
105 nvgRestore(ctx);
106
107 // Draw the text with some padding
108 int textX = xPos + mHeader->theme()->mTabButtonHorizontalPadding;
109 int textY = yPos + mHeader->theme()->mTabButtonVerticalPadding;
110 NVGcolor textColor = mHeader->theme()->mTextColor;
111 nvgBeginPath(ctx);
112 nvgFillColor(ctx, textColor);
113 nvgText(ctx, textX, textY, mVisibleText.first, mVisibleText.last);
114 if (mVisibleText.last != nullptr)
115 nvgText(ctx, textX + mVisibleWidth, textY, dots, nullptr);
116}
117
118void TabHeader::TabButton::drawActiveBorderAt(NVGcontext *ctx, const Vector2i &position,
119 float offset, const Color &color) {
120 int xPos = position.x();
121 int yPos = position.y();
122 int width = mSize.x();
123 int height = mSize.y();
124 nvgBeginPath(ctx);
125 nvgLineJoin(ctx, NVG_ROUND);
126 nvgMoveTo(ctx, xPos + offset, yPos + height + offset);
127 nvgLineTo(ctx, xPos + offset, yPos + offset);
128 nvgLineTo(ctx, xPos + width - offset, yPos + offset);
129 nvgLineTo(ctx, xPos + width - offset, yPos + height + offset);
130 nvgStrokeColor(ctx, color);
131 nvgStrokeWidth(ctx, mHeader->theme()->mTabBorderWidth);
132 nvgStroke(ctx);
133}
134
135void TabHeader::TabButton::drawInactiveBorderAt(NVGcontext *ctx, const Vector2i &position,
136 float offset, const Color& color) {
137 int xPos = position.x();
138 int yPos = position.y();
139 int width = mSize.x();
140 int height = mSize.y();
141 nvgBeginPath(ctx);
142 nvgRoundedRect(ctx, xPos + offset, yPos + offset, width - offset, height - offset,
143 mHeader->theme()->mButtonCornerRadius);
144 nvgStrokeColor(ctx, color);
145 nvgStroke(ctx);
146}
147
148
149TabHeader::TabHeader(Widget* parent, const std::string& font)
150 : Widget(parent), mFont(font) { }
151
152void TabHeader::setActiveTab(int tabIndex) {
153 assert(tabIndex < tabCount());
154 mActiveTab = tabIndex;
155 if (mCallback)
156 mCallback(tabIndex);
157}
158
159int TabHeader::activeTab() const {
160 return mActiveTab;
161}
162
163bool TabHeader::isTabVisible(int index) const {
164 return index >= mVisibleStart && index < mVisibleEnd;
165}
166
167void TabHeader::addTab(const std::string & label) {
168 addTab(tabCount(), label);
169}
170
171void TabHeader::addTab(int index, const std::string &label) {
172 assert(index <= tabCount());
173 mTabButtons.insert(std::next(mTabButtons.begin(), index), TabButton(*this, label));
174 setActiveTab(index);
175}
176
177int TabHeader::removeTab(const std::string &label) {
178 auto element = std::find_if(mTabButtons.begin(), mTabButtons.end(),
179 [&](const TabButton& tb) { return label == tb.label(); });
180 int index = (int) std::distance(mTabButtons.begin(), element);
181 if (element == mTabButtons.end())
182 return -1;
183 mTabButtons.erase(element);
184 if (index == mActiveTab && index != 0)
185 setActiveTab(index - 1);
186 return index;
187}
188
189void TabHeader::removeTab(int index) {
190 assert(index < tabCount());
191 mTabButtons.erase(std::next(mTabButtons.begin(), index));
192 if (index == mActiveTab && index != 0)
193 setActiveTab(index - 1);
194}
195
196const std::string& TabHeader::tabLabelAt(int index) const {
197 assert(index < tabCount());
198 return mTabButtons[index].label();
199}
200
201int TabHeader::tabIndex(const std::string &label) {
202 auto it = std::find_if(mTabButtons.begin(), mTabButtons.end(),
203 [&](const TabButton& tb) { return label == tb.label(); });
204 if (it == mTabButtons.end())
205 return -1;
206 return (int) (it - mTabButtons.begin());
207}
208
209void TabHeader::ensureTabVisible(int index) {
210 auto visibleArea = visibleButtonArea();
211 auto visibleWidth = visibleArea.second.x() - visibleArea.first.x();
212 int allowedVisibleWidth = mSize.x() - 2 * theme()->mTabControlWidth;
213 assert(allowedVisibleWidth >= visibleWidth);
214 assert(index >= 0 && index < (int) mTabButtons.size());
215
216 auto first = visibleBegin();
217 auto last = visibleEnd();
218 auto goal = tabIterator(index);
219
220 // Reach the goal tab with the visible range.
221 if (goal < first) {
222 do {
223 --first;
224 visibleWidth += first->size().x();
225 } while (goal < first);
226 while (allowedVisibleWidth < visibleWidth) {
227 --last;
228 visibleWidth -= last->size().x();
229 }
230 }
231 else if (goal >= last) {
232 do {
233 visibleWidth += last->size().x();
234 ++last;
235 } while (goal >= last);
236 while (allowedVisibleWidth < visibleWidth) {
237 visibleWidth -= first->size().x();
238 ++first;
239 }
240 }
241
242 // Check if it is possible to expand the visible range on either side.
243 while (first != mTabButtons.begin()
244 && std::next(first, -1)->size().x() < allowedVisibleWidth - visibleWidth) {
245 --first;
246 visibleWidth += first->size().x();
247 }
248 while (last != mTabButtons.end()
249 && last->size().x() < allowedVisibleWidth - visibleWidth) {
250 visibleWidth += last->size().x();
251 ++last;
252 }
253
254 mVisibleStart = (int) std::distance(mTabButtons.begin(), first);
255 mVisibleEnd = (int) std::distance(mTabButtons.begin(), last);
256}
257
258std::pair<Vector2i, Vector2i> TabHeader::visibleButtonArea() const {
259 if (mVisibleStart == mVisibleEnd)
260 return { Vector2i::Zero(), Vector2i::Zero() };
261 auto topLeft = mPos + Vector2i(theme()->mTabControlWidth, 0);
262 auto width = std::accumulate(visibleBegin(), visibleEnd(), theme()->mTabControlWidth,
263 [](int acc, const TabButton& tb) {
264 return acc + tb.size().x();
265 });
266 auto bottomRight = mPos + Vector2i(width, mSize.y());
267 return { topLeft, bottomRight };
268}
269
270std::pair<Vector2i, Vector2i> TabHeader::activeButtonArea() const {
271 if (mVisibleStart == mVisibleEnd || mActiveTab < mVisibleStart || mActiveTab >= mVisibleEnd)
272 return { Vector2i::Zero(), Vector2i::Zero() };
273 auto width = std::accumulate(visibleBegin(), activeIterator(), theme()->mTabControlWidth,
274 [](int acc, const TabButton& tb) {
275 return acc + tb.size().x();
276 });
277 auto topLeft = mPos + Vector2i(width, 0);
278 auto bottomRight = mPos + Vector2i(width + activeIterator()->size().x(), mSize.y());
279 return { topLeft, bottomRight };
280}
281
282void TabHeader::performLayout(NVGcontext* ctx) {
283 Widget::performLayout(ctx);
284
285 Vector2i currentPosition = Vector2i::Zero();
286 // Place the tab buttons relative to the beginning of the tab header.
287 for (auto& tab : mTabButtons) {
288 auto tabPreferred = tab.preferredSize(ctx);
289 if (tabPreferred.x() < theme()->mTabMinButtonWidth)
290 tabPreferred.x() = theme()->mTabMinButtonWidth;
291 else if (tabPreferred.x() > theme()->mTabMaxButtonWidth)
292 tabPreferred.x() = theme()->mTabMaxButtonWidth;
293 tab.setSize(tabPreferred);
294 tab.calculateVisibleString(ctx);
295 currentPosition.x() += tabPreferred.x();
296 }
297 calculateVisibleEnd();
298 if (mVisibleStart != 0 || mVisibleEnd != tabCount())
299 mOverflowing = true;
300}
301
302Vector2i TabHeader::preferredSize(NVGcontext* ctx) const {
303 // Set up the nvg context for measuring the text inside the tab buttons.
304 nvgFontFace(ctx, mFont.c_str());
305 nvgFontSize(ctx, fontSize());
306 nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
307 Vector2i size = Vector2i(2*theme()->mTabControlWidth, 0);
308 for (auto& tab : mTabButtons) {
309 auto tabPreferred = tab.preferredSize(ctx);
310 if (tabPreferred.x() < theme()->mTabMinButtonWidth)
311 tabPreferred.x() = theme()->mTabMinButtonWidth;
312 else if (tabPreferred.x() > theme()->mTabMaxButtonWidth)
313 tabPreferred.x() = theme()->mTabMaxButtonWidth;
314 size.x() += tabPreferred.x();
315 size.y() = std::max(size.y(), tabPreferred.y());
316 }
317 return size;
318}
319
320bool TabHeader::mouseButtonEvent(const Vector2i &p, int button, bool down, int modifiers) {
321 Widget::mouseButtonEvent(p, button, down, modifiers);
322 if (button == GLFW_MOUSE_BUTTON_1 && down) {
323 switch (locateClick(p)) {
324 case ClickLocation::LeftControls:
325 onArrowLeft();
326 return true;
327 case ClickLocation::RightControls:
328 onArrowRight();
329 return true;
330 case ClickLocation::TabButtons:
331 auto first = visibleBegin();
332 auto last = visibleEnd();
333 int currentPosition = theme()->mTabControlWidth;
334 int endPosition = p.x();
335 auto firstInvisible = std::find_if(first, last,
336 [&currentPosition, endPosition](const TabButton& tb) {
337 currentPosition += tb.size().x();
338 return currentPosition > endPosition;
339 });
340
341 // Did not click on any of the tab buttons
342 if (firstInvisible == last)
343 return true;
344
345 // Update the active tab and invoke the callback.
346 setActiveTab((int) std::distance(mTabButtons.begin(), firstInvisible));
347 return true;
348 }
349 }
350 return false;
351}
352
353void TabHeader::draw(NVGcontext* ctx) {
354 // Draw controls.
355 Widget::draw(ctx);
356 if (mOverflowing)
357 drawControls(ctx);
358
359 // Set up common text drawing settings.
360 nvgFontFace(ctx, mFont.c_str());
361 nvgFontSize(ctx, fontSize());
362 nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
363
364 auto current = visibleBegin();
365 auto last = visibleEnd();
366 auto active = std::next(mTabButtons.begin(), mActiveTab);
367 Vector2i currentPosition = mPos + Vector2i(theme()->mTabControlWidth, 0);
368
369 // Flag to draw the active tab last. Looks a little bit better.
370 bool drawActive = false;
371 Vector2i activePosition = Vector2i::Zero();
372
373 // Draw inactive visible buttons.
374 while (current != last) {
375 if (current == active) {
376 drawActive = true;
377 activePosition = currentPosition;
378 } else {
379 current->drawAtPosition(ctx, currentPosition, false);
380 }
381 currentPosition.x() += current->size().x();
382 ++current;
383 }
384
385 // Draw active visible button.
386 if (drawActive)
387 active->drawAtPosition(ctx, activePosition, true);
388}
389
390void TabHeader::calculateVisibleEnd() {
391 auto first = visibleBegin();
392 auto last = mTabButtons.end();
393 int currentPosition = theme()->mTabControlWidth;
394 int lastPosition = mSize.x() - theme()->mTabControlWidth;
395 auto firstInvisible = std::find_if(first, last,
396 [&currentPosition, lastPosition](const TabButton& tb) {
397 currentPosition += tb.size().x();
398 return currentPosition > lastPosition;
399 });
400 mVisibleEnd = (int) std::distance(mTabButtons.begin(), firstInvisible);
401}
402
403void TabHeader::drawControls(NVGcontext* ctx) {
404 // Left button.
405 bool active = mVisibleStart != 0;
406
407 // Draw the arrow.
408 nvgBeginPath(ctx);
409 auto iconLeft = utf8(mTheme->mTabHeaderLeftIcon);
410 int fontSize = mFontSize == -1 ? mTheme->mButtonFontSize : mFontSize;
411 float ih = fontSize;
412 ih *= icon_scale();
413 nvgFontSize(ctx, ih);
414 nvgFontFace(ctx, "icons");
415 NVGcolor arrowColor;
416 if (active)
417 arrowColor = mTheme->mTextColor;
418 else
419 arrowColor = mTheme->mButtonGradientBotPushed;
420 nvgFillColor(ctx, arrowColor);
421 nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
422 float yScaleLeft = 0.5f;
423 float xScaleLeft = 0.2f;
424 Vector2f leftIconPos = mPos.cast<float>() + Vector2f(xScaleLeft*theme()->mTabControlWidth, yScaleLeft*mSize.cast<float>().y());
425 nvgText(ctx, leftIconPos.x(), leftIconPos.y() + 1, iconLeft.data(), nullptr);
426
427 // Right button.
428 active = mVisibleEnd != tabCount();
429 // Draw the arrow.
430 nvgBeginPath(ctx);
431 auto iconRight = utf8(mTheme->mTabHeaderRightIcon);
432 fontSize = mFontSize == -1 ? mTheme->mButtonFontSize : mFontSize;
433 ih = fontSize;
434 ih *= icon_scale();
435 nvgFontSize(ctx, ih);
436 nvgFontFace(ctx, "icons");
437 float rightWidth = nvgTextBounds(ctx, 0, 0, iconRight.data(), nullptr, nullptr);
438 if (active)
439 arrowColor = mTheme->mTextColor;
440 else
441 arrowColor = mTheme->mButtonGradientBotPushed;
442 nvgFillColor(ctx, arrowColor);
443 nvgTextAlign(ctx, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
444 float yScaleRight = 0.5f;
445 float xScaleRight = 1.0f - xScaleLeft - rightWidth / theme()->mTabControlWidth;
446 Vector2f rightIconPos = mPos.cast<float>() + Vector2f(mSize.cast<float>().x(), mSize.cast<float>().y()*yScaleRight) -
447 Vector2f(xScaleRight*theme()->mTabControlWidth + rightWidth, 0);
448
449 nvgText(ctx, rightIconPos.x(), rightIconPos.y() + 1, iconRight.data(), nullptr);
450}
451
452TabHeader::ClickLocation TabHeader::locateClick(const Vector2i& p) {
453 auto leftDistance = (p - mPos).array();
454 bool hitLeft = (leftDistance >= 0).all() && (leftDistance < Vector2i(theme()->mTabControlWidth, mSize.y()).array()).all();
455 if (hitLeft)
456 return ClickLocation::LeftControls;
457 auto rightDistance = (p - (mPos + Vector2i(mSize.x() - theme()->mTabControlWidth, 0))).array();
458 bool hitRight = (rightDistance >= 0).all() && (rightDistance < Vector2i(theme()->mTabControlWidth, mSize.y()).array()).all();
459 if (hitRight)
460 return ClickLocation::RightControls;
461 return ClickLocation::TabButtons;
462}
463
464void TabHeader::onArrowLeft() {
465 if (mVisibleStart == 0)
466 return;
467 --mVisibleStart;
468 calculateVisibleEnd();
469}
470
471void TabHeader::onArrowRight() {
472 if (mVisibleEnd == tabCount())
473 return;
474 ++mVisibleStart;
475 calculateVisibleEnd();
476}
477
478NAMESPACE_END(nanogui)
479