Skip to content

Commit e17cddb

Browse files
authored
feat(layouts): text truncation & proper text alignment (#120)
feat(layouts): text truncation & proper text alignment You can now set `truncate` to true in a text element, like this: ```toml wrap_width = 130.0 truncate = true ``` The text is truncated at the given wrap width instead of wrapped. While I was truncating text and just generally figuring out what these imgui functions do, I returned to the problem of correctly-aligning wrapped text. For right- and center-aligned text, this requires looping through the full text rendering it one line at a time, adjusting the position of each line to match its bounds. Further, because imgui does not export any of its word-wrapping implementation, we have to do additional work to find word boundaries. To do this, we look for the nearest space preceding the imgui-selected wrap position. If it's near enough, we break the line at that position, recalculate bounds, and draw. I broke out the text rendering cases so that simple cases do the least work possible, and the space-seeking and bounds-adjusting work is only done when required. The implementation of drawing text is somewhat verbose as a result but each case is simple by itself. Fixes [#104](#104) Fixes [[#98](#98)]
1 parent 476ecea commit e17cddb

File tree

8 files changed

+93
-28
lines changed

8 files changed

+93
-28
lines changed

docs/article-layouts-v2.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,13 @@ Each slot layout has a list of *text* elements. These describe text that should
112112

113113
Here are the fields a text element has:
114114

115-
- `offset`: Where to draw this text, relative to the center of the slot
115+
- `offset`: Where to draw this text, relative to the center of the slot. This location is the *left edge* of the text box.
116116
- `alignment`: How to justify the text. Possible values are `left`, `center`, and `right`.
117117
- `font_size`: A floating-point number for the size of the type used.
118118
- `color`: The color to use to draw the text.
119119
- `contents`: A format string describing the text to draw.
120+
- `wrap_width`: The maximum allowed width of the text. If set, text is wrapped if it would be longer.
121+
` truncate`: A boolean value (`true` or `false`) indicating if the text should be cut short at the wrap width instead of wrapped. Set this to keep text at one line max.
120122

121123
The data that can be filled into a format string is:
122124

@@ -147,6 +149,8 @@ color = { r = 255, g = 255, b = 255, a = 255 }
147149
alignment = "left"
148150
contents = "{name}"
149151
font_size = 20.0
152+
wrap_width = 130.0
153+
truncate = false
150154
```
151155

152156
Any additional text elements for the power slot would also be named `[[power.text]]`. The double square brackets tells TOML that this is an [list of items](https://toml.io/en/v1.0.0#array-of-tables). Each new element named that is added to the end of the list.

src/layouts/layout_v1.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ impl HudLayout1 {
158158
contents: "{name}".to_string(),
159159
font_size: slot.name_font_size * factor,
160160
wrap_width: slot.name_wrap_width,
161+
truncate: false,
161162
});
162163
}
163164
if slot.count_color.a > 0 {
@@ -168,6 +169,7 @@ impl HudLayout1 {
168169
contents: "{count}".to_string(),
169170
font_size: slot.count_font_size * factor,
170171
wrap_width: slot.count_wrap_width,
172+
truncate: false,
171173
});
172174
}
173175

src/layouts/layout_v2.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ impl HudLayout2 {
196196
contents: text.contents.clone(),
197197
font_size: text.font_size * scale,
198198
wrap_width: text.wrap_width,
199+
truncate: text.truncate,
199200
}
200201
}
201202

@@ -265,6 +266,8 @@ pub struct TextElement {
265266
bounds: Option<Point>,
266267
#[serde(default)]
267268
wrap_width: f32,
269+
#[serde(default)]
270+
truncate: bool,
268271
}
269272

270273
#[derive(Debug, Clone, PartialEq, Default)]

src/layouts/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ impl Layout {
115115
}
116116
}
117117

118+
/// Convert the editable human-facing layout format to the format used by
119+
/// the renderer. This process scales all sizes and translates all locations
120+
/// from relative to absolute in screen space.
118121
pub fn flatten(&self) -> LayoutFlattened {
119122
match self {
120123
// *v dereference the ref-to-box, **v unbox, &**v borrow
@@ -123,6 +126,7 @@ impl Layout {
123126
}
124127
}
125128

129+
/// Find the coordinates of the layout's location in screen space.
126130
pub fn anchor_point(&self) -> Point {
127131
match self {
128132
Layout::Version1(v) => v.anchor_point(),
@@ -131,6 +135,8 @@ impl Layout {
131135
}
132136
}
133137

138+
/// An implementation detail of the anchor point calculation, used by both
139+
/// layout formats.
134140
pub fn anchor_point(
135141
global_scale: f32,
136142
size: &Point,

src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ pub mod plugin {
167167
CircleArc,
168168
}
169169

170+
/// A text element in a form ready to use by the renderer.
170171
#[derive(Clone, Debug)]
171172
pub struct TextFlattened {
172173
anchor: Point,
@@ -175,10 +176,12 @@ pub mod plugin {
175176
contents: String,
176177
font_size: f32,
177178
wrap_width: f32,
179+
truncate: bool,
178180
}
179181

180182
/// This enum maps key presses to the desired action. More like a C/java
181-
/// enum than a Rust sum type enum.
183+
/// enum than a Rust sum type enum. It's also more like an event name than
184+
/// a key press map at this point.
182185
#[derive(Debug, Clone, Hash)]
183186
enum Action {
184187
/// We do not need to do anything, possibly because the key was not one of our hotkeys.

src/renderer/ui_renderer.cpp

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -361,26 +361,82 @@ namespace ui
361361
}
362362
}
363363

364-
void drawText(const std::string text,
365-
const ImVec2 center,
366-
const float fontSize,
367-
const soulsy::Color color,
368-
const Align align,
369-
const float wrapWidth)
364+
void drawText(const std::string text, const ImVec2 center, const TextFlattened* label)
370365
{
371-
if (!text.length() || color.a == 0) { return; }
366+
if (!text.length() || label->color.a == 0) { return; }
367+
const float wrapWidth = label->wrap_width;
368+
const auto align = label->alignment;
372369

373370
auto* font = imFont;
374371
if (!font) { font = ImGui::GetDefaultFont(); }
375-
const ImU32 text_color = IM_COL32(color.r, color.g, color.b, color.a * gHudAlpha);
376-
const ImVec2 bounds = font->CalcTextSizeA(fontSize, wrapWidth, wrapWidth, text.c_str());
377-
ImVec2 alignedCenter = ImVec2(center.x, center.y);
372+
const ImU32 textColor = IM_COL32(label->color.r, label->color.g, label->color.b, label->color.a * gHudAlpha);
373+
ImVec2 lineLeftCorner = ImVec2(center.x, center.y);
374+
const auto* cstr = text.c_str();
375+
376+
// Unrolling our cases here to try to make each pass through do the least
377+
// work it can get away with. Probably needs a re-think.
378378

379-
if (align == Align::Center) { alignedCenter.x += bounds.x * 0.5f; }
380-
else if (align == Align::Right) { alignedCenter.x -= bounds.x; }
379+
// Simple fast case first: no truncation, left alignment. Imgui wraps the
380+
// text for us if wrap width is non-zero.
381+
if (!label->truncate && align == Align::Left)
382+
{
383+
ImGui::GetWindowDrawList()->AddText(
384+
font, label->font_size, lineLeftCorner, textColor, cstr, nullptr, wrapWidth, nullptr);
385+
return;
386+
}
387+
388+
// We're aligning, but not truncating or wrapping, so we can draw one location-adjusted line.
389+
if (!label->truncate && wrapWidth == 0.0f)
390+
{
391+
const ImVec2 bounds = font->CalcTextSizeA(label->font_size, 0.0f, 0.0f, cstr);
392+
if (align == Align::Center) { lineLeftCorner.x = center.x + (wrapWidth - bounds.x) * 0.5f; }
393+
else if (align == Align::Right) { lineLeftCorner.x = center.x + (wrapWidth - bounds.x); }
394+
ImGui::GetWindowDrawList()->AddText(font, label->font_size, lineLeftCorner, textColor, cstr);
395+
return;
396+
}
381397

382-
ImGui::GetWindowDrawList()->AddText(
383-
font, fontSize, alignedCenter, text_color, text.c_str(), nullptr, wrapWidth, nullptr);
398+
// The next fastest cases are truncation cases. We find our truncation point,
399+
// then align that single line by moving the draw location.
400+
if (label->truncate && wrapWidth > 0.0f)
401+
{
402+
const char* remainder = nullptr;
403+
const auto bounds = font->CalcTextSizeA(label->font_size, wrapWidth, 0.0f, cstr, nullptr, &remainder);
404+
if (align == Align::Center) { lineLeftCorner.x = center.x + (wrapWidth - bounds.x) * 0.5f; }
405+
else if (align == Align::Right) { lineLeftCorner.x = center.x + (wrapWidth - bounds.x); }
406+
ImGui::GetWindowDrawList()->AddText(font, label->font_size, lineLeftCorner, textColor, cstr, remainder);
407+
return;
408+
}
409+
410+
// Now we must wrap, not truncate, and align each line as we discover it. We stop
411+
// when we run out of text to draw or we decide we've drawn a ridiculous number of lines.
412+
int loops = 0;
413+
const char* lineToDraw = cstr;
414+
auto lineLoc = ImVec2(center.x, center.y);
415+
const auto length = text.length();
416+
417+
do {
418+
const char* remainder = nullptr;
419+
auto bounds = font->CalcTextSizeA(label->font_size, wrapWidth, 0.0f, lineToDraw, nullptr, &remainder);
420+
if ((remainder < cstr + length) && *remainder != ' ')
421+
{
422+
int adjust = 0;
423+
// magic constant; guess about how much we can shorten without being silly
424+
int furthest = std::min(8, static_cast<int>(strlen(lineToDraw)));
425+
while (adjust < furthest && *(remainder - adjust) != ' ') { adjust++; };
426+
if (*(remainder - adjust) == ' ')
427+
{
428+
remainder -= adjust;
429+
bounds = font->CalcTextSizeA(label->font_size, wrapWidth, 0.0f, lineToDraw, remainder);
430+
}
431+
}
432+
if (*remainder == ' ') { remainder++; }
433+
434+
if (align == Align::Center) { lineLoc.x = center.x + (wrapWidth - bounds.x) * 0.5f; }
435+
else if (align == Align::Right) { lineLoc.x = center.x + (wrapWidth - bounds.x); }
436+
ImGui::GetWindowDrawList()->AddText(font, label->font_size, lineLoc, textColor, lineToDraw, remainder);
437+
lineToDraw = remainder;
438+
lineLoc.y += bounds.y; // move down one line
439+
} while (strlen(lineToDraw) > 0 && ++loops < 5);
384440
}
385441

386442
void ui_renderer::initializeAnimation(const animation_type animation_type,
@@ -582,11 +638,7 @@ namespace ui
582638
if (label.color.a == 0) { continue; }
583639
const auto textPos = ImVec2(label.anchor.x, label.anchor.y);
584640
auto entrytxt = std::string(entry->fmtstr(label.contents));
585-
// Let's try a wrap width here. This is going to be wrong, but we'll experiment.
586-
if (!entrytxt.empty())
587-
{
588-
drawText(entrytxt, textPos, label.font_size, label.color, label.alignment, label.wrap_width);
589-
}
641+
if (!entrytxt.empty()) { drawText(entrytxt, textPos, &label); }
590642
}
591643
}
592644

src/renderer/ui_renderer.h

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,7 @@ namespace ui
4848
const ImVec2 size,
4949
const float angle,
5050
const ImU32 im_color); // retaining support for animations...
51-
void drawText(const std::string text,
52-
const ImVec2 center,
53-
const float font_size,
54-
const soulsy::Color color,
55-
const Align alignment);
51+
void drawText(const std::string text, const ImVec2 center, const TextFlattened* label);
5652
void drawMeterCircleArc(float level, SlotFlattened slotLayout);
5753
void drawMeterRectangular(float level, SlotFlattened slotLayout);
5854
ImVec2 rotateVector(const ImVec2 vector, const float angle);
@@ -76,8 +72,6 @@ namespace ui
7672

7773
ui_renderer();
7874

79-
// Oxidation section.
80-
// older...
8175
static void initializeAnimation(animation_type animation_type,
8276
float a_screen_x,
8377
float a_screen_y,

src/soulsy.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace soulsy
1616
struct SlotFlattened;
1717
struct SlotLayout;
1818
struct SpellData;
19+
struct TextFlattened;
1920
}
2021

2122
using namespace soulsy;

0 commit comments

Comments
 (0)