Mana
Loading...
Searching...
No Matches
browserbox.cpp
Go to the documentation of this file.
1/*
2 * The Mana Client
3 * Copyright (C) 2004-2009 The Mana World Development Team
4 * Copyright (C) 2009-2012 The Mana Developers
5 * Copyright (C) 2009 Aethyra Development Team
6 *
7 * This file is part of The Mana Client.
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
24
25#include "keyboardconfig.h"
26
27#include "gui/gui.h"
28#include "gui/truetypefont.h"
30
31#include "resources/itemdb.h"
32#include "resources/iteminfo.h"
33#include "resources/theme.h"
34
35#include "utils/stringutils.h"
36
37#include <guichan/graphics.hpp>
38#include <guichan/font.hpp>
39#include <guichan/cliprectangle.hpp>
40
41#include <algorithm>
42
46static void replaceKeys(std::string &text)
47{
48 auto keyStart = text.find("###");
49
50 while (keyStart != std::string::npos)
51 {
52 const auto keyEnd = text.find(";", keyStart + 3);
53 if (keyEnd == std::string::npos)
54 break;
55
56 std::string_view key(text.data() + keyStart + 3, keyEnd - keyStart - 3);
57
58 // Remove "key" prefix
59 if (key.size() > 3 && key.substr(0, 3) == "key")
60 key.remove_prefix(3);
61
62 const auto keyName = keyboard.getKeyName(key);
63 if (!keyName.empty())
64 {
65 text.replace(keyStart, keyEnd - keyStart + 1, keyName);
66 keyStart = text.find("###", keyStart + keyName.size());
67 }
68 else
69 {
70 keyStart = text.find("###", keyEnd + 1);
71 }
72 }
73}
74
75
77{
78 LayoutContext(gcn::Font *font, const Palette &palette);
79
80 LinePart linePart(int x, std::string text);
81
82 int y = 0;
83 gcn::Font *font;
84 const int fontHeight;
85 const int minusWidth;
86 const int tildeWidth;
88 const gcn::Color textColor;
89 const std::optional<gcn::Color> textOutlineColor;
90 gcn::Color color;
91 std::optional<gcn::Color> outlineColor;
92};
93
94inline LayoutContext::LayoutContext(gcn::Font *font, const Palette &palette)
95 : font(font)
96 , fontHeight(font->getHeight())
97 , minusWidth(font->getWidth("-"))
98 , tildeWidth(font->getWidth("~"))
99 , lineHeight(fontHeight)
100 , textColor(palette.getColor(Theme::TEXT))
101 , textOutlineColor(palette.getOutlineColor(Theme::TEXT))
102 , color(textColor)
103 , outlineColor(textOutlineColor)
104{
105 if (auto *trueTypeFont = dynamic_cast<const TrueTypeFont*>(font))
106 lineHeight = trueTypeFont->getLineHeight();
107}
108
109inline LinePart LayoutContext::linePart(int x, std::string text)
110{
111 return {
112 x,
113 y,
114 color,
116 std::move(text),
117 font
118 };
119}
120
121
123 mMode(mode)
124{
125 setFocusable(true);
126 addMouseListener(this);
127}
128
129BrowserBox::~BrowserBox() = default;
130
131void BrowserBox::addRows(std::string_view rows)
132{
133 std::string_view::size_type start = 0;
134 std::string_view::size_type end = 0;
135 while (end != std::string::npos)
136 {
137 end = rows.find('\n', start);
138 addRow(rows.substr(start, end - start));
139 start = end + 1;
140 }
141}
142
143void BrowserBox::addRow(std::string_view row)
144{
145 TextRow &newRow = mTextRows.emplace_back();
146
147 // Use links and user defined colors
149 {
150 // Check for links in format "@@link|Caption@@"
151 auto linkStart = row.find("@@");
152 while (linkStart != std::string::npos)
153 {
154 const auto linkSep = row.find("|", linkStart);
155 const auto linkEnd = row.find("@@", linkSep);
156
157 if (linkSep == std::string::npos || linkEnd == std::string::npos)
158 break;
159
160 BrowserLink &link = newRow.links.emplace_back();
161 link.link = row.substr(linkStart + 2, linkSep - (linkStart + 2));
162 link.caption = row.substr(linkSep + 1, linkEnd - (linkSep + 1));
163
164 if (link.caption.empty())
165 {
166 const int id = atoi(link.link.c_str());
167 if (id)
168 link.caption = itemDb->get(id).name;
169 else
170 link.caption = link.link;
171 }
172
173 newRow.text += row.substr(0, linkStart);
174 newRow.text += "##<" + link.caption;
175
176 row = row.substr(linkEnd + 2);
177 if (!row.empty())
178 {
179 newRow.text += "##>";
180 }
181 linkStart = row.find("@@");
182 }
183
184 newRow.text += row;
185 }
186 // Don't use links and user defined colors
187 else
188 {
189 newRow.text = row;
190 }
191
192 if (mEnableKeys)
193 replaceKeys(newRow.text);
194
195 // Layout the newly added row
196 LayoutContext context(getFont(), gui->getTheme()->getPalette(mPalette));
197 context.y = getHeight();
198 layoutTextRow(newRow, context);
199
200 // Auto size mode
201 if (mMode == AUTO_SIZE && newRow.width > getWidth())
202 setWidth(newRow.width);
203
204 // Discard older rows when a row limit has been set
205 // (this might invalidate the newRow reference)
206 int removedHeight = 0;
207 while (mMaxRows > 0 && mTextRows.size() > mMaxRows)
208 {
209 removedHeight += mTextRows.front().height;
210 mTextRows.pop_front();
211 }
212 if (removedHeight > 0)
213 {
214 for (auto &row : mTextRows)
215 {
216 for (auto &part : row.parts)
217 part.y -= removedHeight;
218
219 for (auto &link : row.links)
220 link.rect.y -= removedHeight;
221 }
222 }
223
224 setHeight(context.y - removedHeight);
225}
226
228{
229 mTextRows.clear();
230 setSize(mMode == AUTO_SIZE ? 0 : getWidth(), 0);
231 mHoveredLink.reset();
233}
234
235void BrowserBox::mousePressed(gcn::MouseEvent &event)
236{
237 if (!mLinkHandler)
238 return;
239
240 updateHoveredLink(event.getX(), event.getY());
241
242 if (mHoveredLink) {
245 }
246}
247
248void BrowserBox::mouseMoved(gcn::MouseEvent &event)
249{
250 updateHoveredLink(event.getX(), event.getY());
252 event.consume(); // Suppress mouse cursor change by parent
253}
254
255void BrowserBox::mouseExited(gcn::MouseEvent &event)
256{
257 mHoveredLink.reset();
258}
259
260void BrowserBox::draw(gcn::Graphics *graphics)
261{
262 const gcn::ClipRectangle &cr = graphics->getCurrentClipArea();
263 int yStart = cr.y - cr.yOffset;
264 int yEnd = yStart + cr.height;
265 if (yStart < 0)
266 yStart = 0;
267
268 if (getWidth() != mLastLayoutWidth)
270
271 if (mHoveredLink)
272 {
273 auto &palette = gui->getTheme()->getPalette(mPalette);
274 auto &link = *mHoveredLink;
275
276 const gcn::Rectangle &rect = link.rect;
277
279 {
280 graphics->setColor(palette.getColor(Theme::HIGHLIGHT));
281 graphics->fillRectangle(rect);
282 }
283
285 {
286 graphics->setColor(palette.getColor(Theme::HYPERLINK));
287 graphics->drawLine(rect.x,
288 rect.y + rect.height,
289 rect.x + rect.width,
290 rect.y + rect.height);
291 }
292 }
293
294 auto g = static_cast<Graphics*>(graphics);
295
296 for (const auto &row : mTextRows)
297 {
298 for (const auto &part : row.parts)
299 {
300 if (part.y + 50 < yStart)
301 continue;
302 if (part.y > yEnd)
303 return;
304
305 g->drawText(part.text,
306 part.x,
307 part.y,
308 Graphics::LEFT,
309 part.color,
310 part.font,
311 part.outlineColor.has_value() || mOutline,
312 mShadows,
313 part.outlineColor);
314 }
315 }
316}
317
322{
323 LayoutContext context(getFont(), gui->getTheme()->getPalette(mPalette));
324
325 for (auto &row : mTextRows)
326 layoutTextRow(row, context);
327
328 mLastLayoutWidth = getWidth();
329 mLayoutTimer.set(33);
330 setHeight(context.y);
331}
332
338{
339 // each line starts with normal font in default color
340 context.font = getFont();
341 context.color = context.textColor;
342 context.outlineColor = context.textOutlineColor;
343
344 const int startY = context.y;
345 row.parts.clear();
346
347 unsigned linkIndex = 0;
348 bool wrapped = false;
349 int x = 0;
350
351 // Check for separator lines
352 if (startsWith(row.text, "---"))
353 {
354 for (x = 0; x < getWidth(); x += context.minusWidth - 1)
355 row.parts.push_back(context.linePart(x, "-"));
356
357 context.y += row.height;
358
359 row.width = getWidth();
360 row.height = context.y - startY;
361 return;
362 }
363
364 auto &palette = gui->getTheme()->getPalette(mPalette);
365 auto prevColor = context.color;
366 auto prevOutlineColor = context.outlineColor;
367
368 // TODO: Check if we must take texture size limits into account here
369 // TODO: Check if some of the O(n) calls can be removed
370 for (std::string::size_type start = 0, end = std::string::npos;
371 start != std::string::npos;
372 start = end, end = std::string::npos)
373 {
374 // Wrapped line continuation shall be indented
375 if (wrapped)
376 {
377 context.y += context.lineHeight;
378 x = mWrapIndent;
379 wrapped = false;
380 }
381
382 if (mUseLinksAndUserColors || start == 0)
383 {
384 // Check for color or font change in format "##x", x = [<,>,B,p,0..9]
385 while (row.text.size() > start + 2 && row.text.find("##", start) == start)
386 {
387 const char c = row.text.at(start + 2);
388 start += 3;
389
390 switch (c)
391 {
392 case '>':
393 context.color = prevColor;
394 context.outlineColor = prevOutlineColor;
395 break;
396 case '<':
397 prevColor = context.color;
398 prevOutlineColor = context.outlineColor;
399 context.color = palette.getColor(Theme::HYPERLINK);
400 context.outlineColor = palette.getOutlineColor(Theme::HYPERLINK);
401 break;
402 case 'B':
403 context.font = boldFont;
404 break;
405 case 'b':
406 context.font = getFont();
407 break;
408 default: {
409 const auto colorId = Theme::getColorIdForChar(c);
410 if (colorId)
411 {
412 context.color = palette.getColor(*colorId);
413 context.outlineColor = palette.getOutlineColor(*colorId);
414 }
415 else
416 {
417 context.color = context.textColor;
418 context.outlineColor = context.textOutlineColor;
419 }
420 break;
421 }
422 }
423
424 // Update the position of the links
425 if (c == '<' && linkIndex < row.links.size())
426 {
427 auto &link = row.links[linkIndex];
428
429 link.rect.x = x;
430 link.rect.y = context.y;
431 link.rect.width = context.font->getWidth(link.caption) + 1;
432 link.rect.height = context.fontHeight - 1;
433
434 linkIndex++;
435 }
436 }
437 }
438
439 if (start >= row.text.length())
440 break;
441
442 // "Tokenize" the string at control sequences
444 end = row.text.find("##", start + 1);
445
446 std::string::size_type len =
447 end == std::string::npos ? end : end - start;
448
449 std::string part = row.text.substr(start, len);
450 int partWidth = context.font->getWidth(part);
451
452 // Auto wrap mode
453 if (mMode == AUTO_WRAP && getWidth() > 0
454 && partWidth > 0
455 && (x + partWidth) > getWidth())
456 {
457 bool forced = false;
458
459 /* FIXME: This code layout makes it easy to crash remote
460 clients by talking garbage. Forged long utf-8 characters
461 will cause either a buffer underflow in substr or an
462 infinite loop in the main loop. */
463 do
464 {
465 if (!forced)
466 end = row.text.rfind(' ', end);
467
468 // Check if we have to (stupidly) force-wrap
469 if (end == std::string::npos || end <= start)
470 {
471 forced = true;
472 end = row.text.size();
473 x += context.tildeWidth; // Account for the wrap-notifier
474 continue;
475 }
476
477 // Skip to the start of the current character
478 while ((row.text[end] & 192) == 128)
479 end--;
480 end--; // And then to the last byte of the previous one
481
482 part = row.text.substr(start, end - start + 1);
483 partWidth = context.font->getWidth(part);
484 }
485 while (end > start && partWidth > 0
486 && (x + partWidth) > getWidth());
487
488 if (forced)
489 {
490 x -= context.tildeWidth; // Remove the wrap-notifier accounting
491 row.parts.push_back(LinePart {
492 getWidth() - context.tildeWidth,
493 context.y,
494 context.color,
495 context.outlineColor,
496 "~",
497 getFont()
498 });
499 end++; // Skip to the next character
500 }
501 else
502 {
503 end += 2; // Skip to after the space
504 }
505
506 wrapped = true;
507 }
508
509 row.parts.push_back(context.linePart(x, std::move(part)));
510 row.width = std::max(row.width, x + partWidth);
511
512 if (mMode == AUTO_WRAP && partWidth == 0)
513 break;
514
515 x += partWidth;
516 }
517
518 context.y += context.lineHeight;
519 row.height = context.y - startY;
520}
521
523{
524 mHoveredLink.reset();
525
526 for (const auto &row : mTextRows)
527 {
528 for (const auto &link : row.links)
529 {
530 if (link.contains(x, y))
531 {
532 mHoveredLink = link;
533 return;
534 }
535 }
536 }
537}
538
540{
541 // Reduce relayouting frequency when there is a lot of text
542 if (mTextRows.size() > 1000)
543 if (!mLayoutTimer.passed())
544 return;
545
546 relayoutText();
547}
void maybeRelayoutText()
void addRows(std::string_view rows)
Adds one or more text rows to the browser, separated by ' '.
void mouseMoved(gcn::MouseEvent &event) override
bool mShadows
Definition browserbox.h:182
void updateHoveredLink(int x, int y)
void mouseExited(gcn::MouseEvent &event) override
BrowserBox(Mode mode=AUTO_SIZE)
void draw(gcn::Graphics *graphics) override
Draws the browser box.
unsigned int mHighlightMode
Definition browserbox.h:180
std::optional< BrowserLink > mHoveredLink
Definition browserbox.h:186
void addRow(std::string_view row)
Adds a text row to the browser.
void layoutTextRow(TextRow &row, LayoutContext &context)
Layers out the given row of text starting at the given context position.
int mLastLayoutWidth
Definition browserbox.h:188
bool mEnableKeys
Definition browserbox.h:185
LinkHandler * mLinkHandler
Definition browserbox.h:177
~BrowserBox() override
unsigned int mMaxRows
Definition browserbox.h:187
void clearRows()
Remove all rows.
int mWrapIndent
Definition browserbox.h:181
Timer mLayoutTimer
Definition browserbox.h:189
@ AUTO_WRAP
Maybe it needs a fix or to be redone.
Definition browserbox.h:79
std::deque< TextRow > mTextRows
Definition browserbox.h:175
bool mUseLinksAndUserColors
Definition browserbox.h:184
bool mOutline
Definition browserbox.h:183
void mousePressed(gcn::MouseEvent &event) override
Handles mouse actions.
void relayoutText()
Relayouts all text rows and returns the new height of the BrowserBox.
A central point of control for graphics.
Definition graphics.h:78
void drawText(const std::string &text, int x, int y, gcn::Graphics::Alignment alignment, const gcn::Color &color, gcn::Font *font, bool outline=false, bool shadow=false, const std::optional< gcn::Color > &outlineColor={}, const std::optional< gcn::Color > &shadowColor={})
Definition graphics.cpp:176
void setColor(const gcn::Color &color) override
Definition graphics.h:255
Theme * getTheme() const
The global GUI theme.
Definition gui.h:138
void setCursorType(Cursor cursor)
Sets which cursor should be used.
Definition gui.cpp:231
const ItemInfo & get(int id) const
Definition itemdb.cpp:145
std::string name
Definition iteminfo.h:113
std::string_view getKeyName(std::string_view configName) const
Get the key name by providing the keys config name.
virtual void handleLink(const std::string &link)=0
Class controlling the game's color palette.
Definition palette.h:42
Definition theme.h:196
const Palette & getPalette(size_t index) const
Definition theme.cpp:328
static std::optional< int > getColorIdForChar(char c)
Returns the color ID associated with a character, if it exists.
Definition theme.cpp:338
@ HYPERLINK
Definition theme.h:249
@ HIGHLIGHT
Definition theme.h:231
bool passed() const
Returns whether the timer has passed.
Definition time.h:88
void set(uint32_t ms=0)
Sets the timer with an optional duration in milliseconds.
Definition time.h:69
A wrapper around SDL_ttf for allowing the use of TrueType fonts.
Graphics * graphics
Definition client.cpp:104
KeyboardConfig keyboard
Definition client.cpp:101
ItemDB * itemDb
Items info database.
Definition client.cpp:106
Gui * gui
The GUI system.
Definition gui.cpp:50
gcn::Font * boldFont
Bolded text font.
Definition gui.cpp:54
bool startsWith(std::string_view str, std::string_view prefix)
Returns whether a string starts with a given prefix.
gcn::Color color
LayoutContext(gcn::Font *font, const Palette &palette)
const gcn::Color textColor
const int fontHeight
std::optional< gcn::Color > outlineColor
const int tildeWidth
LinePart linePart(int x, std::string text)
const std::optional< gcn::Color > textOutlineColor
gcn::Font * font
const int minusWidth
std::vector< LinePart > parts
Definition browserbox.h:62
int height
Definition browserbox.h:65
std::vector< BrowserLink > links
Definition browserbox.h:63
int width
Definition browserbox.h:64
std::string text
Definition browserbox.h:61