Skip to content
Open
11 changes: 11 additions & 0 deletions changelog/snippets/graphics.7105.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
- (#7105) Improve scoreboard legibility and internal text functions for mods/devs

#### UI

Improve scoreboard legibility with better alignment, tooltips, dropshadows, and text cropping.

#### Modding/development

added SetTruncationText() to assign custom trailing characters like "..." when text is cropped

split SetText() into **SetText()** and its implicitly called **SetDisplayText()**. This allows for fancier text implementations like custom truncation.
4 changes: 4 additions & 0 deletions loc/US/strings_db.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5951,6 +5951,10 @@ tooltipui0734="No Vote"
tooltipui0735="Vote no to your team recalling from battle as a defeat."
tooltipui0736="[Hide/Show] Mass Fabricator Panel"
tooltipui0737="[Hide/Show] Voting Panel"
tooltipui0738="Game Speed"
tooltipui0739="[your requested speed] / [actual game speed]"
tooltipui0740="Game Quality"
tooltipui0741="Estimated game quality"

-- External Factory and Auto-Deploy tooltips
tooltipui0750 = "Mobile Factory"
Expand Down
75 changes: 75 additions & 0 deletions lua/maui/text.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,38 @@ Text = ClassUI(moho.text_methods, Control) {
self._color.OnDirty = function(var)
self:SetNewColor(var())
end
self._truncationText = ""
self._fullText = nil


--- 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.



--- 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.

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

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.


--- 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.

return self._fullText
end
Comment on lines +36 to +67

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.

end,

OnInit = function(self)
Expand All @@ -41,15 +73,58 @@ Text = ClassUI(moho.text_methods, Control) {
self:SetClipToWidth(false)
end,

--- Sets custom truncation trailing characters like "..." or "-". Set to "" to disable.
---@param text string | number
SetTruncationText = function (self, text)
self._truncationText = tostring(text)
if self._fullText ~= nil and self._initialized then
self:_applyTruncation()
end
end,

SetClipToWidth = function(self, clipToWidth)
if clipToWidth then
self.Width:Set(function() return self.Right() - self.Left() end)

self.Width.OnDirty = function()
if self._truncationText ~= nil and self._truncationText ~= "" then
self:_applyTruncation()
end
end
else
self.Width:Set(function() return math.floor(self.TextAdvance()) end)
self.Width.OnDirty = nil
end
self:SetNewClipToWidth(clipToWidth)
end,

--- Internal function to fit the truncation string inside the max width
_applyTruncation = function(self)
local maxWidth = self.Width()
if maxWidth <= 0 then
return
end

-- ellipsis is the trailing '...' on truncated text
local ellipsis = self._truncationText
local str = self._fullText
if str == nil then return end
-- restore full text if it now fits
if self:GetStringAdvance(str) <= maxWidth then
self.SetDisplayText(self, str)
return
end
Comment thread
coderabbitai[bot] marked this conversation as resolved.

--iterate until string + ellipsis fit
local i = STR_Utf8Len(str)
while i > 0 and self:GetStringAdvance(str .. ellipsis) > maxWidth do
str = STR_Utf8SubString(str, 1, i - 1)
i = i - 1
end

self.SetDisplayText(self, str .. ellipsis)
end,

-- lazy var support
SetFont = function(self, family, pointsize)
if self._font then
Expand Down
4 changes: 4 additions & 0 deletions lua/ui/game/layouts/score_mini.lua
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ function SetLayout()
controls.timeIcon:SetTexture(UIUtil.UIFile('/game/unit_view_icons/time.dds'))
LayoutHelpers.RightOf(controls.time, controls.timeIcon)

LayoutHelpers.RightOf(controls.gameSpeed, controls.time, 4)

LayoutHelpers.AtLeftTopIn(controls.gameQuality, controls.bgTop, 138, 6)

LayoutHelpers.AtRightTopIn(controls.unitIcon, controls.bgTop, 10, 6)
controls.unitIcon:SetTexture(UIUtil.UIFile('/dialogs/score-overlay/tank_bmp.dds'))
LayoutHelpers.LeftOf(controls.units, controls.unitIcon)
Expand Down
65 changes: 55 additions & 10 deletions lua/ui/game/score.lua
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,15 @@ function updatePlayerName(line)
end

if sessionInfo.Options.Divisions then
line.name:SetText(playerClan .. playerName .. playerDivision)
line.division:SetText(playerDivision)
else
line.name:SetText(playerClan .. playerName .. playerRating)
line.division:SetText(playerRating)
end

line.name:SetTruncationText("...")
line.name:SetText(playerClan .. playerName)
line.name:SetDropShadow(true)
LayoutHelpers.AnchorToLeft(line.name, line.division)
end

function armyGroupHeight()
Expand Down Expand Up @@ -118,14 +123,20 @@ function CreateScoreUI(parent)

LayoutHelpers.SetWidth(controls.bgTop, 320)

controls.time = UIUtil.CreateText(controls.bgTop, '0', 12, UIUtil.bodyFont)
controls.time = UIUtil.CreateText(controls.bgTop, '0', 12, UIUtil.bodyFont, true)
controls.time:SetColor('ff00dbff')
controls.timeIcon = Bitmap(controls.bgTop)
Tooltip.AddControlTooltip(controls.timeIcon, 'score_time')
Tooltip.AddControlTooltip(controls.time, 'score_time')
controls.gameSpeed = UIUtil.CreateText(controls.bgTop, '', 12, UIUtil.bodyFont, true)
controls.gameSpeed:SetColor('ffbadaff')
Tooltip.AddControlTooltip(controls.gameSpeed, 'score_game_speed')
controls.gameQuality = UIUtil.CreateText(controls.bgTop, '', 12, UIUtil.bodyFont, true)
controls.gameQuality:SetColor('ff00dbff')
Tooltip.AddControlTooltip(controls.gameQuality, 'score_game_quality')
controls.unitIcon = Bitmap(controls.bgTop)
Tooltip.AddControlTooltip(controls.unitIcon, 'score_units')
controls.units = UIUtil.CreateText(controls.bgTop, '0', 12, UIUtil.bodyFont)
controls.units = UIUtil.CreateText(controls.bgTop, '0', 12, UIUtil.bodyFont, true)
controls.units:SetColor('ffff9900')
Tooltip.AddControlTooltip(controls.units, 'score_units')

Expand Down Expand Up @@ -302,6 +313,12 @@ function SetupPlayerLines()
LayoutHelpers.AtVerticalCenterIn(group.name, group)
group.name:SetColor('ffffffff')

group.division = UIUtil.CreateText(group, '', 12, UIUtil.bodyFont, true)
group.division:DisableHitTest()
LayoutHelpers.AtRightIn(group.division, group, 135)
LayoutHelpers.AtVerticalCenterIn(group.division, group)
group.division:SetColor('ffffffff')

group.score = UIUtil.CreateText(group, '', 12, UIUtil.bodyFont)
group.score:DisableHitTest()
LayoutHelpers.AtRightIn(group.score, group, sw * 2 + 16)
Expand Down Expand Up @@ -372,6 +389,18 @@ function SetupPlayerLines()
Tooltip.AddControlTooltip(group.units, {text = '', body = bodyText}, 1)
end

-- hover to see full name (incase they are cut off)
group.nameHover = Bitmap(group)
group.nameHover:SetSolidColor('00000000') -- fully transparent
LayoutHelpers.AnchorToLeft(group.nameHover, group.division)
LayoutHelpers.AtLeftIn(group.nameHover, group, 12)
group.nameHover.Height:Set(group.name.Height)
LayoutHelpers.AtVerticalCenterIn(group.nameHover, group)
Tooltip.AddAutoUpdatedControlTooltip(group.nameHover,
function() return group.name:GetText() or "" end,
function() return "" end,
0.5)

group.Height:Set(group.faction.Height)
group.Width:Set(controls.armyGroup.Width)
group.armyID = armyIndex
Expand Down Expand Up @@ -416,6 +445,7 @@ function SetupPlayerLines()
observerLine:Hide()
observerLine.OnHide = blockOnHide
observerLine.name.Top:Set(observerLine.Top)
observerLine.name:SetDropShadow(true)
LayoutHelpers.SetHeight(observerLine, 15)

if SessionIsReplay() then
Expand Down Expand Up @@ -476,7 +506,7 @@ function SetupPlayerLines()
end

-- ui for share conditions
group.ShareConditions = UIUtil.CreateText(group, data.ShareConditionsTitle, 10, UIUtil.bodyFont)
group.ShareConditions = UIUtil.CreateText(group, data.ShareConditionsTitle, 10, UIUtil.bodyFont, true)
Tooltip.AddForcedControlTooltipManual(group.ShareConditions, data.ShareConditionsTitle, data.ShareConditionsDescription)
LayoutHelpers.AtLeftIn(group.ShareConditions, group)
LayoutHelpers.AtVerticalCenterIn(group.ShareConditions, group)
Expand All @@ -485,15 +515,15 @@ function SetupPlayerLines()
previous = AddDash()

-- ui for map size
group.Size = UIUtil.CreateText(group, data.SizeText, 10, UIUtil.bodyFont)
group.Size = UIUtil.CreateText(group, data.SizeText, 10, UIUtil.bodyFont, true)
LayoutHelpers.RightOf(group.Size, previous)
LayoutHelpers.AtVerticalCenterIn(group.Size, group)
group.Size:SetColor('ffffffff')
previous = group.Size
previous = AddDash()

-- ui for map name
group.MapName = UIUtil.CreateText(group, data.MapTitle, 10, UIUtil.bodyFont)
group.MapName = UIUtil.CreateText(group, data.MapTitle, 10, UIUtil.bodyFont, true)
Tooltip.AddForcedControlTooltipManual(group.MapName, data.MapTitle, data.MapDescription)
LayoutHelpers.RightOf(group.MapName, previous)
LayoutHelpers.AtVerticalCenterIn(group.MapName, group)
Expand Down Expand Up @@ -618,11 +648,17 @@ local prevPlayableWidth = sessionInfo.PlayableAreaWidth
local prevPlayableHeight = sessionInfo.PlayableAreaHeight

function _OnBeat()
local s = string.format("%s (%+d / %+d)", GetGameTime(), gameSpeed, GetSimRate())
local t = string.format("%s", GetGameTime())
controls.time:SetText(t)

local s = string.format("(%+d / %+d)",gameSpeed, GetSimRate())
controls.gameSpeed:SetText(s)

if sessionInfo.Options.Quality then
s = string.format("%s Q:%.2f%%", s, sessionInfo.Options.Quality)
local q = string.format("Q:%.2f%%", sessionInfo.Options.Quality)
controls.gameQuality:SetText(q)
end
Comment thread
coderabbitai[bot] marked this conversation as resolved.
controls.time:SetText(s)


if sessionInfo.Options.NoRushOption and sessionInfo.Options.NoRushOption ~= 'Off' then
local norush = tonumber(sessionInfo.Options.NoRushOption) * 60
Expand Down Expand Up @@ -717,26 +753,35 @@ function _OnBeat()
if line.armyID == prevArmy then
if line.OOG then
line.name:SetColor('ffa0a0a0')
line.division:SetColor('ffa0a0a0')
line.score:SetColor('ffa0a0a0')
else
line.name:SetColor('ffffffff')
line.division:SetColor('ffffffff')
line.score:SetColor('ffffffff')
end
line.name:SetFont(UIUtil.bodyFont, 12)
line.division:SetFont(UIUtil.bodyFont, 12)
line.score:SetFont(UIUtil.bodyFont, 12)
elseif line.armyID == curFA then
line.name:SetColor('ffff7f00')
line.division:SetColor('ffff7f00')
line.score:SetColor('ffff7f00')

line.name:SetFont('Arial Bold', 12)
line.division:SetFont('Arial Bold', 12)
line.score:SetFont('Arial Bold', 12)
end
end
if curFA < 1 then
observerLine.name:SetColor('ffff7f00')
observerLine.division:SetColor('ffff7f00')
observerLine.name:SetFont('Arial Bold', 12)
elseif prevArmy < 1 then
observerLine.name:SetColor('ffffffff')
observerLine.division:SetColor('ffffffff')
observerLine.name:SetFont(UIUtil.bodyFont, 12)
observerLine.division:SetFont(UIUtil.bodyFont, 12)
end
if observerLine:IsHidden() and ((curFA < 1) or (sessionInfo.Options.CheatsEnabled == 'true')) then
table.insert(controls.armyLines, table.getsize(controls.armyLines), observerLine)
Expand Down
8 changes: 8 additions & 0 deletions lua/ui/help/tooltips.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1808,6 +1808,14 @@ Tooltips = {
title = "<LOC tooltipui0473>Game Time",
description = "",
},
score_game_speed ={
title = "<LOC tooltipui0738>Game Speed",
description = "<LOC tooltipui0739>[your requested speed] / [actual game speed]",
},
score_game_quality = {
title = "<LOC tooltipui0740>Game Quality",
description = "<LOC tooltipui0741>Estimated game quality",
},
score_units = {
title = "<LOC tooltipui0474>Unit Count",
description = "<LOC tooltipui0507>Current and maximum unit counts",
Expand Down