77#include < GteVector.h>
88#include < absl/meta/type_traits.h>
99
10+ #include < QChar>
1011#include < QColor>
1112#include < QFont>
1213#include < QFontDatabase>
1819#include < algorithm>
1920#include < cfloat>
2021#include < cmath>
22+ #include < cstdint>
2123#include < string>
2224
2325#include " Introspection/Introspection.h"
@@ -29,7 +31,19 @@ namespace orbit_gl {
2931
3032namespace {
3133
32- float GetYOffsetFromAlignment (QtTextRenderer::VAlign alignment, float height) {
34+ // Qt offers a QFontMetrics::horizontalAdvance to determine the width of a rendered string. This
35+ // method is fairly slow. For rendering the text in the timers we therefore use a different method:
36+ // We compute a lookup table storing the rendered width of all the characters and sum over all the
37+ // characters in a string (compare GetCharacterWidthLookup, GetStringWidthFast below). The result is
38+ // consistently a bit shorter than the correct result provided by QFontMetrics::horizontalAdvance.
39+ // Applying the heuristic below to the result from the lookup reliably yields a fairly tight upper
40+ // bound for the true width of the rendered string. Don't try to make sense of the formula - it is
41+ // just a line fitted to example data.
42+ [[nodiscard]] int MaximumHeuristic (int width, int length, uint32_t font_size) {
43+ return 2 + (length * static_cast <int >(font_size)) / (12 * 14 ) + width;
44+ }
45+
46+ [[nodiscard]] float GetYOffsetFromAlignment (QtTextRenderer::VAlign alignment, float height) {
3347 switch (alignment) {
3448 case QtTextRenderer::VAlign::Top:
3549 return 0 .f ;
@@ -47,7 +61,7 @@ float GetYOffsetFromAlignment(QtTextRenderer::VAlign alignment, float height) {
4761 }
4862}
4963
50- float GetXOffsetFromAlignment (QtTextRenderer::HAlign alignment, float width) {
64+ [[nodiscard]] float GetXOffsetFromAlignment (QtTextRenderer::HAlign alignment, float width) {
5165 // kOffset is a hack to compensate for subtle differences in the placement of the rendered text
5266 // under Linux and Windows. Setting kOffset == 0 under Windows results in texts starting left of
5367 // the interval border for unknown reasons (also see https://github.com/google/orbit/issues/4627).
@@ -126,7 +140,7 @@ void QtTextRenderer::AddText(const char* text, float x, float y, float z, TextFo
126140 font.setPixelSize (formatting.font_size );
127141 QFontMetrics metrics (font);
128142 float y_offset = GetYOffsetFromAlignment (formatting.valign , height_entire_text);
129- const float single_line_height = GetStringHeight ( " . " , formatting.font_size );
143+ const float single_line_height = GetSingleLineStringHeight ( formatting.font_size );
130144 QStringList lines = text_as_qstring.split (" \n " );
131145 float max_line_width = 0 .f ;
132146 for (const auto & line : lines) {
@@ -156,9 +170,13 @@ void QtTextRenderer::AddText(const char* text, float x, float y, float z, TextFo
156170float QtTextRenderer::AddTextTrailingCharsPrioritized (const char * text, float x, float y, float z,
157171 TextFormatting formatting,
158172 size_t trailing_chars_length) {
159- ORBIT_SCOPE_FUNCTION;
160- QString text_as_qstring (text);
161- const size_t text_length = text_as_qstring.length ();
173+ // Early-out: If we can't fit a single char, there's no use to do all the expensive
174+ // calculations below - this is a major bottleneck in some cases
175+ if (formatting.max_size >= 0 && GetMinimumTextWidth (formatting.font_size ) > formatting.max_size ) {
176+ return 0 .f ;
177+ }
178+
179+ const size_t text_length = std::strlen (text);
162180 if (text_length == 0 ) {
163181 return 0 .f ;
164182 }
@@ -169,40 +187,44 @@ float QtTextRenderer::AddTextTrailingCharsPrioritized(const char* text, float x,
169187 text, trailing_chars_length);
170188 trailing_chars_length = text_length;
171189 }
172- // Early-out: If we can't fit a single char, there's no use to do all the expensive
173- // calculations below - this is a major bottleneck in some cases
174- if (formatting.max_size >= 0 && GetMinimumTextWidth (formatting.font_size ) > formatting.max_size ) {
175- return 0 .f ;
176- }
177190
178191 const float max_width =
179192 formatting.max_size == -1 .f ? FLT_MAX : viewport_->WorldToScreen ({formatting.max_size , 0 })[0 ];
193+ const QString text_as_qstring (text);
194+ const CharacterWidthLookup& lookup = GetCharacterWidthLookup (formatting.font_size );
180195 const QString trailing_text = text_as_qstring.right (trailing_chars_length);
181- const QString leading_text = text_as_qstring.left (text_length - trailing_chars_length);
182- QFont font = QFontDatabase::systemFont (QFontDatabase::GeneralFont);
183- font.setPixelSize (formatting.font_size );
184- QFontMetrics metrics (font);
185- const float trailing_text_width = GetStringWidth (trailing_text, formatting.font_size );
186- QString elided_text;
196+ const float trailing_text_width = GetStringWidthFast (trailing_text, lookup, formatting.font_size );
197+ // If the trailing text fits we (potentially) elide the leading text.
187198 if (trailing_text_width < max_width) {
188- elided_text = metrics.elidedText (leading_text, Qt::ElideRight,
189- static_cast <int >(max_width - trailing_text_width));
199+ const QString leading_text = text_as_qstring.left (text_length - trailing_chars_length);
200+ const QString elided_text =
201+ ElideText (leading_text, static_cast <int >(max_width - trailing_text_width), lookup,
202+ formatting.font_size );
203+ return AddFittingSingleLineText (elided_text + trailing_text, x, y, z, formatting, lookup);
190204 }
205+ // If the trailing text doesn't fit we simply elide the entire text (the trailing text is not
206+ // preserved in this case).
207+ const QString elided_text =
208+ ElideText (text_as_qstring, static_cast <int >(max_width), lookup, formatting.font_size );
191209 if (elided_text.isEmpty ()) {
192- // We can't fit any elided string with the trailing characters preserved so we elide the entire
193- // string and accept that the trailing characters are truncated.
194- elided_text = metrics.elidedText (text_as_qstring, Qt::ElideRight, static_cast <int >(max_width));
195- if (elided_text.isEmpty ()) {
196- return 0 .f ;
197- }
198- return AddFittingSingleLineText (elided_text, x, y, z, formatting);
210+ return 0 .f ;
199211 }
200- return AddFittingSingleLineText (elided_text + trailing_text , x, y, z, formatting);
212+ return AddFittingSingleLineText (elided_text, x, y, z, formatting, lookup );
201213}
202214
203215float QtTextRenderer::GetStringWidth (const char * text, uint32_t font_size) {
204216 QString text_as_qstring (text);
205- QStringList lines = text_as_qstring.split (" \n " );
217+ return GetStringWidth (text_as_qstring, font_size);
218+ }
219+
220+ float QtTextRenderer::GetStringHeight (const char * text, uint32_t font_size) {
221+ QString text_as_qstring (text);
222+ int number_of_lines = text_as_qstring.count (' \n ' ) + 1 ;
223+ return static_cast <float >(number_of_lines) * GetSingleLineStringHeight (font_size);
224+ }
225+
226+ float QtTextRenderer::GetStringWidth (const QString& text, uint32_t font_size) {
227+ QStringList lines = text.split (" \n " );
206228 float max_width = 0 .f ;
207229 QFont font = QFontDatabase::systemFont (QFontDatabase::GeneralFont);
208230 font.setPixelSize (static_cast <int >(font_size));
@@ -214,38 +236,87 @@ float QtTextRenderer::GetStringWidth(const char* text, uint32_t font_size) {
214236 return max_width;
215237}
216238
217- float QtTextRenderer::GetStringHeight (const char * text, uint32_t font_size) {
218- QString text_as_qstring (text);
219- QStringList lines = text_as_qstring.split (" \n " );
220- const float number_of_lines = lines.size ();
221- QFont font = QFontDatabase::systemFont (QFontDatabase::GeneralFont);
222- font.setPixelSize (static_cast <int >(font_size));
223- QFontMetrics metrics (font);
224- return number_of_lines * viewport_->ScreenToWorld (Vec2i (0 , metrics.height ()))[1 ];
225- }
226-
227- float QtTextRenderer::GetStringWidth (const QString& text, uint32_t font_size) {
228- std::string text_as_string = text.toStdString ();
229- return GetStringWidth (text_as_string.c_str (), font_size);
230- }
231-
232239float QtTextRenderer::GetMinimumTextWidth (uint32_t font_size) {
233240 auto minimum_string_width_it = minimum_string_width_cache_.find (font_size);
234241 if (minimum_string_width_it != minimum_string_width_cache_.end ()) {
235242 return minimum_string_width_it->second ;
236243 }
237- // Only if we can fit one wide (hence the "W") character plus the ellipsis dots we start rendering
238- // text. Otherwise we leave the space empty.
239- constexpr char const * kMinimumString = " W... " ;
244+ // Only if we can fit one wide (hence the "W") character we start rendering text. Otherwise we
245+ // leave the space empty.
246+ constexpr char const * kMinimumString = " W" ;
240247 const float width = GetStringWidth (kMinimumString , font_size);
241248 minimum_string_width_cache_[font_size] = width;
242249 return width;
243250}
244251
252+ float QtTextRenderer::GetSingleLineStringHeight (uint32_t font_size) {
253+ int metrics_height = 0 ;
254+ auto it = single_line_height_cache_.find (font_size);
255+ if (it == single_line_height_cache_.end ()) {
256+ QFont font = QFontDatabase::systemFont (QFontDatabase::GeneralFont);
257+ font.setPixelSize (static_cast <int >(font_size));
258+ QFontMetrics metrics (font);
259+ int height = metrics.height ();
260+ metrics_height = single_line_height_cache_[font_size] = height;
261+ } else {
262+ metrics_height = it->second ;
263+ }
264+ return viewport_->ScreenToWorld (Vec2i (0 , metrics_height))[1 ];
265+ }
266+
267+ const QtTextRenderer::CharacterWidthLookup& QtTextRenderer::GetCharacterWidthLookup (
268+ uint32_t font_size) {
269+ auto it = character_width_lookup_cache_.find (font_size);
270+ if (it == character_width_lookup_cache_.end ()) {
271+ QFont font = QFontDatabase::systemFont (QFontDatabase::GeneralFont);
272+ font.setPixelSize (static_cast <int >(font_size));
273+ QFontMetrics metrics (font);
274+ CharacterWidthLookup lut;
275+ for (int i = 0 ; i < 256 ; i++) {
276+ lut[i] = metrics.horizontalAdvance (QChar (i));
277+ }
278+ it = character_width_lookup_cache_.emplace (font_size, lut).first ;
279+ }
280+ return it->second ;
281+ }
282+
283+ float QtTextRenderer::GetStringWidthFast (const QString& text, const CharacterWidthLookup& lookup,
284+ uint32_t font_size) {
285+ int width = 0 ;
286+ for (const QChar& c : text) {
287+ width += lookup[c.toLatin1 ()];
288+ }
289+ const int horizontal_advance = MaximumHeuristic (width, text.length (), font_size);
290+ return viewport_->ScreenToWorld (Vec2i (horizontal_advance, 0 ))[0 ];
291+ }
292+
293+ QString QtTextRenderer::ElideText (const QString& text, int max_width,
294+ const CharacterWidthLookup& lookup, uint32_t font_size) {
295+ int width_lookup = 0 ;
296+ int characters = 0 ;
297+ while (characters < text.length ()) {
298+ const int next_char_width = lookup[text[characters].toLatin1 ()];
299+ if (MaximumHeuristic (width_lookup + next_char_width, characters, font_size) > max_width) {
300+ break ;
301+ }
302+ width_lookup += next_char_width;
303+ characters++;
304+ }
305+ if (characters == text.length ()) {
306+ return text;
307+ }
308+ QString result = text.left (characters);
309+ if (characters > 0 ) {
310+ result[characters - 1 ] = ' ' ;
311+ }
312+ return result;
313+ }
314+
245315float QtTextRenderer::AddFittingSingleLineText (const QString& text, float x, float y, float z,
246- TextFormatting formatting) {
247- const float width = GetStringWidth (text, formatting.font_size );
248- const float single_line_height = GetStringHeight (" ." , formatting.font_size );
316+ const TextFormatting& formatting,
317+ const CharacterWidthLookup& lookup) {
318+ const float width = GetStringWidthFast (text, lookup, formatting.font_size );
319+ const float single_line_height = GetSingleLineStringHeight (formatting.font_size );
249320 Vec2i pen_pos = viewport_->WorldToScreen (Vec2 (x, y));
250321 LayeredVec2 transformed = translations_.TranslateXYZAndFloorXY (
251322 {{static_cast <float >(pen_pos[0 ]), static_cast <float >(pen_pos[1 ])}, z});
0 commit comments