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 | |
19 | NAMESPACE_BEGIN(nanogui) |
20 | |
21 | TabHeader::TabButton::(TabHeader &, const std::string &label) |
22 | : mHeader(&header), mLabel(label) { } |
23 | |
24 | Vector2i TabHeader::TabButton::(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 | |
33 | void TabHeader::TabButton::(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 | |
60 | void TabHeader::TabButton::(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 | |
118 | void TabHeader::TabButton::(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 | |
135 | void TabHeader::TabButton::(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 | |
149 | TabHeader::(Widget* parent, const std::string& font) |
150 | : Widget(parent), mFont(font) { } |
151 | |
152 | void TabHeader::(int tabIndex) { |
153 | assert(tabIndex < tabCount()); |
154 | mActiveTab = tabIndex; |
155 | if (mCallback) |
156 | mCallback(tabIndex); |
157 | } |
158 | |
159 | int TabHeader::() const { |
160 | return mActiveTab; |
161 | } |
162 | |
163 | bool TabHeader::(int index) const { |
164 | return index >= mVisibleStart && index < mVisibleEnd; |
165 | } |
166 | |
167 | void TabHeader::(const std::string & label) { |
168 | addTab(tabCount(), label); |
169 | } |
170 | |
171 | void TabHeader::(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 | |
177 | int TabHeader::(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 | |
189 | void TabHeader::(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 | |
196 | const std::string& TabHeader::(int index) const { |
197 | assert(index < tabCount()); |
198 | return mTabButtons[index].label(); |
199 | } |
200 | |
201 | int TabHeader::(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 | |
209 | void TabHeader::(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 | |
258 | std::pair<Vector2i, Vector2i> TabHeader::() 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 | |
270 | std::pair<Vector2i, Vector2i> TabHeader::() 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 | |
282 | void TabHeader::(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 | |
302 | Vector2i TabHeader::(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 | |
320 | bool TabHeader::(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 | [¤tPosition, 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 | |
353 | void TabHeader::(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 | |
390 | void TabHeader::() { |
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 | [¤tPosition, lastPosition](const TabButton& tb) { |
397 | currentPosition += tb.size().x(); |
398 | return currentPosition > lastPosition; |
399 | }); |
400 | mVisibleEnd = (int) std::distance(mTabButtons.begin(), firstInvisible); |
401 | } |
402 | |
403 | void TabHeader::(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 | |
452 | TabHeader::ClickLocation TabHeader::(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 | |
464 | void TabHeader::() { |
465 | if (mVisibleStart == 0) |
466 | return; |
467 | --mVisibleStart; |
468 | calculateVisibleEnd(); |
469 | } |
470 | |
471 | void TabHeader::() { |
472 | if (mVisibleEnd == tabCount()) |
473 | return; |
474 | ++mVisibleStart; |
475 | calculateVisibleEnd(); |
476 | } |
477 | |
478 | NAMESPACE_END(nanogui) |
479 | |