Skip to content

Add custom text truncation support, and improve scoreboard legibility#7105

Open
Lightningbulb2 wants to merge 12 commits into
FAForever:developfrom
Lightningbulb2:develop
Open

Add custom text truncation support, and improve scoreboard legibility#7105
Lightningbulb2 wants to merge 12 commits into
FAForever:developfrom
Lightningbulb2:develop

Conversation

@Lightningbulb2

@Lightningbulb2 Lightningbulb2 commented May 8, 2026

Copy link
Copy Markdown

Description of the proposed changes

I wanted to improve scoreboard legibility.

I started by separating game speed, and game quality into their own controls to make modding and future changes easier. I added dropshadows, tooltips, and right aligned the ratings while making sure to crop player names if necessary. This led to the-

Implementation of SetTruncateText() which allows custom trailing strings like "...", "-", or even "NOFIT". This is executed through SetClipToWidth() and only activates if a truncation string is set.

To do this cleanly, and in a reusable way, I split SetText() into SetText() and its implicitly called SetDisplayText().

SetText() - controls the source text and behaves practically the same as before

SetDisplayText() - sends text changes to the engine without changing the Text object's internal string. This allows for better mod and source code capabilities for fancier text. The first example being custom text truncation.

Testing done on the proposed changes

I loaded up private AI matches and replays to check for console and visual errors. I got screenshots:

Original for reference (plus rank cropping issue)
Original


Game start with lots of players
GameStartExample


Full scoreboard
NEWSCOREBOARD


Replay scoreboard
REPLAY


Show full player names after hovering for a moment (in-case of cropped names)
CROPPEDHOVER


Tooltips added for new players
gameQuality


gameSpeed

Checklist

Summary by CodeRabbit

  • New Features
    • Added game speed display (requested vs. actual) and game quality to the score overlay.
  • Improvements
    • Refreshed score and mini-score layouts for improved legibility, alignment, and tooltip support (including hover previews for full player names).
    • Upgraded text truncation so the displayed value auto-updates based on available width, with a customizable truncation suffix.

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7afdb877-6df4-4b5e-916a-3499ffee6a74

📥 Commits

Reviewing files that changed from the base of the PR and between 608e8d8 and 9d9b4be.

📒 Files selected for processing (4)
  • loc/US/strings_db.lua
  • lua/maui/text.lua
  • lua/ui/game/score.lua
  • lua/ui/help/tooltips.lua
💤 Files with no reviewable changes (1)
  • lua/ui/help/tooltips.lua
🚧 Files skipped from review as they are similar to previous changes (3)
  • loc/US/strings_db.lua
  • lua/ui/game/score.lua
  • lua/maui/text.lua

📝 Walkthrough

Walkthrough

Adds truncation support to the UI Text component and applies it to the scoreboard: separates name/division rendering, adds time/gameSpeed/gameQuality header controls with tooltips, positions those controls in compact layouts, and updates localization and changelog entries.

Changes

Scoreboard Text Truncation and Display Enhancement

Layer / File(s) Summary
Text truncation API
lua/maui/text.lua
Initializes truncation state fields; exposes engine SetDisplayText/GetDisplayText; wraps SetText/GetText to preserve and return raw text separately; adds SetTruncationText and SetTruncationEnabled; extends SetClipToWidth to trigger truncation; implements _applyTruncation to iteratively shorten UTF-8 text with suffix.
Score UI header controls
lua/ui/game/score.lua
CreateScoreUI adds time, gameSpeed, gameQuality, units header controls to the top background with colors and tooltip wiring.
Player lines: division and hover
lua/ui/game/score.lua
SetupPlayerLines adds dedicated division text element (right-aligned, colored) and nameHover bitmap tooltip that auto-displays full name when truncated.
Name and division rendering
lua/ui/game/score.lua
updatePlayerName sets line.division and line.name separately with truncation and anchoring instead of concatenating into a single string.
Observer and map line updates
lua/ui/game/score.lua
Observer line gains explicit drop shadow on name; CreateMapNameLine uses constructor variant for ShareConditions, Size, and MapName text fields.
Beat and focus-change updates
lua/ui/game/score.lua
_OnBeat formats and sets controls.time, controls.gameSpeed, controls.gameQuality; division color/font updates when focused army changes.
Mini layout positioning
lua/ui/game/layouts/score_mini.lua
SetLayout positions controls.gameSpeed relative to controls.time and places controls.gameQuality within controls.bgTop.
Localization and changelog
loc/US/strings_db.lua, lua/ui/help/tooltips.lua, changelog/snippets/graphics.7105.md
Adds Game Speed/Game Quality localization entries, tooltip definitions, and changelog documenting UI and Text API changes.

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble words, keep full and neat,
Trim them gently so they fit your seat,
Names and ranks now sit apart,
Speed and quality play their part,
A rabbit's hop makes the scoreboard sweet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: custom text truncation support and scoreboard legibility improvements, matching the core objectives.
Description check ✅ Passed The description covers all required template sections: clear explanation of changes with visual evidence, comprehensive testing documentation with screenshots, and a completed checklist addressing changes, changelog, and reviewers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Lightningbulb2 Lightningbulb2 marked this pull request as ready for review May 8, 2026 04:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
lua/maui/text.lua (1)

78-80: ⚡ Quick win

SetTruncationText doesn't reapply truncation to the current display.

If called after text has already been set (e.g. to swap "..." for "-"), the display won't update until the next SetText or width change. Consider re-triggering _applyTruncation here when _fullText is already populated.

♻️ Proposed fix
 SetTruncationText = function (self, text)
     self._truncationText = tostring(text)
+    if self._fullText ~= nil and self._initialized then
+        self:_applyTruncation()
+    end
 end,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lua/maui/text.lua` around lines 78 - 80, SetTruncationText currently only
updates self._truncationText and doesn't refresh the rendered text; modify
SetTruncationText to set self._truncationText = tostring(text) and then, if
self._fullText is non-nil/non-empty, call self:_applyTruncation() so the new
truncation marker is applied immediately to the current display (same behavior
as after SetText or width changes).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lua/maui/text.lua`:
- Around line 113-117: The loop is byte-oriented and can break multi-byte UTF‑8
characters: replace uses of string.len and str:sub(1, -2) with the UTF‑8 safe
helpers used elsewhere (use STR_Utf8Len to compute i and STR_Utf8SubString to
remove the last character), and ensure the truncation loop that calls
self:GetStringAdvance(str .. ellipsis) operates on the UTF‑8 substring so
multi‑byte characters aren’t split.
- Around line 106-110: In _applyTruncation: guard against _fullText being nil by
initializing it from the current display text (e.g. if self._fullText == nil
then self._fullText = self:GetDisplayText() or ""), then use that value when
calling GetStringAdvance; if the text fits (GetStringAdvance(...) <= maxWidth)
call SetDisplayText(self._fullText) and clear any truncation flag (e.g.
self._truncated = false) to restore the full text; otherwise perform the
truncation as before, call SetDisplayText(truncatedString) and set
self._truncated = true. Ensure you continue to use _truncationText/_fullText and
methods _applyTruncation, GetStringAdvance, SetDisplayText, GetDisplayText in
these changes.

In `@lua/ui/game/score.lua`:
- Around line 657-660: The variable q is being assigned without local scope
inside the block that checks sessionInfo.Options.Quality, causing a global leak;
make q a local variable (consistent with t and s used earlier) before assigning
string.format("Q:%.2f%%", sessionInfo.Options.Quality) and then call
controls.gameQuality:SetText(q) with that local q to avoid polluting the global
namespace.
- Line 132: The color string passed to controls.gameSpeed:SetColor is missing
the alpha channel; change the hex from 'BADAFF' to include an opaque alpha
prefix (match other calls like 'ff00dbff') so use an 8-character AARRGGBB string
(e.g. 'ffBADAFF') when calling controls.gameSpeed:SetColor to ensure the
intended opaque color is applied.

---

Nitpick comments:
In `@lua/maui/text.lua`:
- Around line 78-80: SetTruncationText currently only updates
self._truncationText and doesn't refresh the rendered text; modify
SetTruncationText to set self._truncationText = tostring(text) and then, if
self._fullText is non-nil/non-empty, call self:_applyTruncation() so the new
truncation marker is applied immediately to the current display (same behavior
as after SetText or width changes).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dfbb29a2-64fd-486d-a6ff-1f7e5a1e0ec2

📥 Commits

Reviewing files that changed from the base of the PR and between 327d011 and 5eb1c4d.

📒 Files selected for processing (6)
  • changelog/snippets/graphics.7105.md
  • loc/US/strings_db.lua
  • lua/maui/text.lua
  • lua/ui/game/layouts/score_mini.lua
  • lua/ui/game/score.lua
  • lua/ui/help/tooltips.lua

Comment thread lua/maui/text.lua
Comment thread lua/maui/text.lua Outdated
Comment thread lua/ui/game/score.lua Outdated
Comment thread lua/ui/game/score.lua
@lL1l1 lL1l1 added area: ui Anything to do with the User Interface of the Game ui: scoreboard related to scoreboard ui labels Jun 14, 2026
@lL1l1 lL1l1 requested a review from 4z0t June 16, 2026 05:14
Comment thread lua/maui/text.lua Outdated
Comment on lines +36 to +67
self._truncationText = ""
self._fullText = nil


--- Direct Engine SetText() that changes what text is displayed
---@type function
---@param str string | number
self.SetDisplayText = self.SetText


--- FAF extensible SetText() that uses SetDisplayText() but can retain it's original text for fancy text display setups like truncation
---@param text string | number
self.SetText = function (self, text)
self.SetDisplayText(self,text)
self._fullText = text
if self._truncationText ~= nil and self._truncationText ~= "" then
if self._initialized then
self:_applyTruncation()
end
end
end

--- Direct Engine GetText() for getting the current displayed value
---@type function
---@return string
self.GetDisplayText = self.GetText

--- FAF extensible GetText() that retrieves raw original text that isn't modified for display
---@return string
self.GetText = function (self)
return self._fullText
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make a separate class for Truncatable text. You are polluting Text class with functionality used by scoreboard only.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are really 2 changes here. A text rendering feature (better truncation), and a text architecture change (display text) which the truncation relies on.

1. Better Text Truncation

It's only used by the scoreboard because it didn't exist to be used for other things yet.

Text can already be truncated, but this simply allows the option for setting how it truncates it. You may want a dash, dots, or warning text (for texts that shouldn't be truncated but would be hard to notice if cut off at the right spot).

It's more of a small extension to SetClipToWidth() than a new special text type.

SetTruncationText() is just setting the trailing characters, if there was any confusion there. Maybe SetTrailingCharacters() would be better?

2. SetText() and SetDisplayText()

The separation of the full text and the "displayed text" sent to the engine, is much more useful than just supporting truncation text. It allows much more elaborate text animation/visual effects for modding and development because it's no longer on you to keep track of the old state. Plus anything that tries to GetText() will get it's real value instead of the partial text effect value that may have been setup. GetDisplayText() would be used for getting what the engine sees.


Hopefully that makes sense. Thanks for the review!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I get a response by the Human being? Still i insist on creating a separate class.

@Lightningbulb2 Lightningbulb2 Jun 16, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I typed all that myself, dome to keyboard :(

The last line "Hopefully that makes sense." was about my writing, not some AI garbage. I try to include a thanks when I can because this is volunteer work, and programming feedback tends to be heavy on critique so the "thanks" is as much for you as it is for me (to not take it personally).


Aaaanyway, yeah, you're right about the function instancing. That modding override trick is great for single function rewrites, but I understand avoiding it here.

How about this solution in the body rather than inside __init? That would prevent all the instancing right?

---@class Text : moho.text_methods, Control, InternalObject
Text = ClassUI(...)

    __init()..


    --- Direct Engine SetText() that changes what text is displayed
    ---@type function
    ---@type fun(self: Text, str: string | number)
    SetDisplayText = moho.text_methods.SetText,

    --- Direct Engine GetText() for getting the current displayed value
    ---@type function
    ---@return string
    GetDisplayText = moho.text_methods.GetText,

    --- FAF extensible SetText() that uses SetDisplayText() but can retain it's original text for fancy text display setups like truncation
    ---@param text string | number
    SetText = function(self, text)
        self:SetDisplayText(text)
        self._fullText = text
        if self._truncationText ~= nil and self._truncationText ~= "" then
            if self._initialized then
                self:_applyTruncation()
            end
        end
    end,

    --- FAF extensible GetText() that retrieves raw original text that isn't modified for display
    ---@return string
    GetText = function(self)
        return self._fullText
    end,

    --- Sets custom truncation trailing characters like "..." or "-". Set to "" to disable.
    ---@param text string | number
   SetTruncationText = function(...)...

Also, should I keep it as "truncation text" or call it "trailing characters" instead, for clarity?

self._truncationText would then be self._trailingCharacters

SetTruncationText() would then be SetTrailingCharacters()


Claude.ai showed me I could use "moho.text_methods.SetText" instead of "self.SetText()"

Although it defined local variables outside the class to reference it first and then assign the Display functions in the __init(), but I found that wasn't necessary.

(Hand written reply)

@4z0t 4z0t Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I offended you. I just don't like when people drop a lot of text especially made with ai not even understanding what's it about.

As I said, you are modifying class that is used everywhere in the code and any side effects are very unwanted. Please just create a separate class that inherits from Text. Like TruncatableText and instantiate it in scoreboard. It will make it easier to control scope of the change and will be easier to maintain.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest commits remove a lot of unnecessary code. It updated fine even when I was messing with the text values in-game.

The only 2 checks that happen now are of the boolean self._truncationEnabled inside both SetText(), and the OnDirty from SetClipToWidth()

If it's not enabled, nothing else runs, so it shouldn't bother the rest of the backend text.

The additional fields are

Bool: self._truncationEnabled (default: false)
String: self._truncationText (default: "...")
String: self._fullText (default: nil)

Aside from the truncation stuff, I would still want to keep the main Text changes. So adding another class would just move the truncation stuff for not much gain, plus more inconvenience because any new text features would have to be added to both.

Ignoring all the truncation stuff, this is ALL the core changes to how Text operates

   __init = function(...)

     ...

     self._fullText = nil
  end,


    SetDisplayText = moho.text_methods.SetText,

    GetDisplayText = moho.text_methods.GetText,

    SetText = function(self, text)
        self:SetDisplayText(text)
        self._fullText = text
    end,

    GetText = function(self)
        return self._fullText
    end,

So while I understand being hesitant to make sweeping changes that might break things. I've been extremely careful to keep things compatible and only those few lines actually matter for the rest of the repository. The other changes in Text.lua are from the optional text truncation, which I've kept as unobtrusive as possible so it can stay as a standard text feature.

If you can think of any side effects of adding this, let me know, otherwise we'll just have to find out if it breaks.

Thanks for reading

Comment thread lua/maui/text.lua Outdated
--- Direct Engine SetText() that changes what text is displayed
---@type function
---@param str string | number
self.SetDisplayText = self.SetText

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each Text instance has this field, not its meta.

Comment thread lua/maui/text.lua Outdated

--- FAF extensible SetText() that uses SetDisplayText() but can retain it's original text for fancy text display setups like truncation
---@param text string | number
self.SetText = function (self, text)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each Text instance has this field, not its meta.

Comment thread lua/maui/text.lua Outdated
--- Direct Engine GetText() for getting the current displayed value
---@type function
---@return string
self.GetDisplayText = self.GetText

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each Text instance has this field, not its meta.

Comment thread lua/maui/text.lua Outdated

--- FAF extensible GetText() that retrieves raw original text that isn't modified for display
---@return string
self.GetText = function (self)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each Text instance has this field, not its meta.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lua/maui/text.lua`:
- Around line 60-63: The SetText function updates the display text and full text
but does not trigger truncation, so the custom truncation suffix will not appear
until the control's bounds change. Modify the SetText function to explicitly
call _applyTruncation() after updating the text values, ensuring the truncation
is applied immediately when text is set and truncation is enabled, rather than
waiting for a bounds change event.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a69c25ef-843e-4303-9db0-e87fff970b39

📥 Commits

Reviewing files that changed from the base of the PR and between 7e707b6 and 608e8d8.

📒 Files selected for processing (2)
  • lua/maui/text.lua
  • lua/ui/game/score.lua
🚧 Files skipped from review as they are similar to previous changes (1)
  • lua/ui/game/score.lua

Comment thread lua/maui/text.lua
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ui Anything to do with the User Interface of the Game ui: scoreboard related to scoreboard ui

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants