Repository navigation
Add custom text truncation support, and improve scoreboard legibility #7169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
31ad501
f002a2a
16066fb
5eb1c4d
3653d26
eb39955
7e707b6
a6c3eca
a49fc59
608e8d8
789136f
9d9b4be
c0d1c64
2471fc6
64b67c1
62b6814
972927d
ffb1b94
660cdcc
529540d
d575162
3d1fcc4
0a6bd62
74e2046
d272065
9cc31ea
633b9f9
1d3bdb4
fc7d45c
44fbcc2
e5d4f76
a3d86b1
d959b21
a2e6983
bdfbf67
715b888
51696b5
86b634c
5cde680
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| - Improve scoreboard legibility with better alignment, tooltips, dropshadows, and text cropping (#7169). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| - Add truncation for the Text control (#7169): | ||
| - Accessible with `SetTruncationEnabled()` | ||
| - Use `SetTruncationText()` to assign custom trailing characters like "..." when text is cropped | ||
| - `SetText()` is split into `SetText()` and its implicitly called `SetDisplayText()`. This allows for text preprocessing operations such as truncation. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,11 @@ local LazyVarCreate = import("/lua/lazyvar.lua").Create | |
| Text = ClassUI(moho.text_methods, Control) { | ||
|
|
||
| __init = function(self, parent, debugname) | ||
|
|
||
| self._truncationText = "..." | ||
| self._fullText = "" | ||
| self._truncationEnabled = false | ||
|
|
||
| InternalCreateText(self, parent) | ||
| if debugname then | ||
| self:SetName(debugname) | ||
|
|
@@ -33,6 +38,7 @@ Text = ClassUI(moho.text_methods, Control) { | |
| self._color.OnDirty = function(var) | ||
| self:SetNewColor(var()) | ||
| end | ||
|
|
||
| end, | ||
|
|
||
| OnInit = function(self) | ||
|
|
@@ -41,15 +47,138 @@ Text = ClassUI(moho.text_methods, Control) { | |
| self:SetClipToWidth(false) | ||
| end, | ||
|
|
||
| SetClipToWidth = function(self, clipToWidth) | ||
| --- Directly sets displayed text | ||
| ---@see Text.SetText # To set the raw text | ||
| ---@type fun(self: Text, str: string | number) | ||
| SetDisplayText = moho.text_methods.SetText, | ||
|
|
||
| --- Directly gets the current displayed text | ||
| ---@see Text.SetText # To get the raw text | ||
| ---@type fun(self: Text): string | ||
| GetDisplayText = moho.text_methods.GetText, | ||
|
|
||
| --- Sets the raw text; actual displayed text is affected by operations like truncation | ||
| ---@see Text.SetDisplayText # To directly set the displayed text | ||
| ---@param text string | number | ||
| SetText = function(self, text) | ||
| local t_text = type(text) | ||
| if t_text ~= "string" and t_text ~= "number" then | ||
| error('Expected string or number but got ' .. t_text, 2) | ||
| end | ||
| self._fullText = tostring(text) | ||
| if self._truncationEnabled then | ||
| self:_applyTruncation() | ||
| else | ||
| self:SetDisplayText(self._fullText) | ||
| end | ||
| end, | ||
|
|
||
| --- Get the raw text before operations like truncation | ||
| ---@see Text.GetDisplayText # To directly get the displayed text | ||
| ---@return string | ||
| GetText = function(self) | ||
| return self._fullText | ||
| end, | ||
|
|
||
| --- Sets custom truncation trailing characters like "..." or "-". | ||
| ---@param text string | ||
| SetTruncationText = function(self, text) | ||
| self._truncationText = tostring(text) | ||
| end, | ||
|
|
||
| ---@param enabled boolean | ||
| SetTruncationEnabled = function(self, enabled) | ||
|
|
||
| -- prevent recursive OnDirty wrappers | ||
| if self._truncationEnabled == enabled then | ||
| return | ||
| end | ||
|
|
||
| self._truncationEnabled = enabled | ||
| if enabled then | ||
| -- Preserve any existing OnDirty callback before adding _applyTruncation | ||
| if self.Width.OnDirty then | ||
| self._originalWidthOnDirty = self.Width.OnDirty | ||
| end | ||
|
|
||
| self.Width.OnDirty = function(var) | ||
|
|
||
| if self._originalWidthOnDirty then | ||
| self._originalWidthOnDirty(var) | ||
| end | ||
|
|
||
| self:_applyTruncation() | ||
| end | ||
|
Comment on lines
+99
to
+111
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically this will fail if truncation is enabled and then This is an optional change though because from what I see in the codebase,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah at least this one won't overwrite another Maybe some sort of helper version that won't overwrite an existing one like Or a more complex system that allows you to add/remove to a list of named |
||
|
|
||
| -- Apply truncation immediately so SetText() and SetTruncationEnabled() order doesn't matter | ||
| self:_applyTruncation() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This causes a circular lazyvar error because
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm I think I see what you're saying, but correct me if anything is wrong here: (I'll use "element" in the context of HTML, hopefully you've seen its syntax) The Elements like So with the current setup, it's not that Text is modifying the layout, but rather the Text element is managing its own layout while also containing the text. The way FAF does it would be like if standard HTML was like this: <!--used for alignment and formatting-->
<div width="50%" text-align="center" background-color="lightblue">
<text Cropping="true" TruncationText="..." Color="red" Width="30">
YOURWORDSHERE
</text>
<Bitmap file="path/to/file/" Width="30">
<Button="path/to/standardButton">
</div>But HTML sort of does what you're suggesting and uses the generic formatting element <div width="50%" text-align="center" background-color="lightblue">
<!-- class properties OR inline definitions ("color: red" for example)-->
<div class="beautiful-text" color="red">
YOURWORDSHERE
</div>
<Bitmap file="path/to/file/" Width="30">
<Button="path/to/standardButton">
</div>// CSS class definition for above text
.beautiful-text {
width: 250px; /* 1. Explicit width is required */
white-space: nowrap; /* 2. Prevents the text from wrapping to a new line */
overflow: hidden; /* 3. Hides the overflowing content */
text-overflow: ellipsis; /* 4. Adds the '...' at the end */
}But while HTML is generally designed as a text-first system, I see why the FAF engine kept it as its own element. Things would get cluttered and confusing if If text starts getting a lot of complex/incompatible features then you would want to split/inherit it into several text types. (Like scrolling animated text for example) HTML has So while I'm still unsure about the solution to the circular dependency problem, that's the explanation to why it's setup like this. Why does ---@param clipToWidth boolean
SetClipToWidth = function(self, clipToWidth)
if clipToWidth then
self.Width:Set(function() return self.Right() - self.Left() end)
else
self.Width:Set(function() return math.floor(self.TextAdvance()) end)
end
self:SetNewClipToWidth(clipToWidth)
end,Thanks for reviewing and helping me with this! (I had a fun time writing this! I didn't anticipate my basic HTML experience would help me understand FAF UI xD) Written without any AI
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe not perfect, but this fixes the lazyvar width() initialization --- Internal function to fit the truncation string inside the max width
_applyTruncation = function(self)
-- Avoid unnecessary truncation, and no width issues
if self._fullText == "" then return end
local initialized, maxWidth = pcall(function() return self.Width() end)
if not initialized or maxWidth <= 0 then
self:SetDisplayText(self._fullText)
return
endand that also makes me realize that we could have optional truncation as a shorthand in --- Adjusts text cropping behavior
---@param clipToWidth boolean
---@param truncationEnabled? boolean
---@param customTruncationSymbol? string
SetClipToWidth = function(self, clipToWidth, truncationEnabled, customTruncationSymbol)
if clipToWidth then
self.Width:Set(function() return self.Right() - self.Left() end)
else
self:SetTruncationEnabled(false)
self.Width:Set(function() return math.floor(self.TextAdvance()) end)
end
if truncationEnabled then
self:SetTruncationEnabled(truncationEnabled)
end
if customTruncationSymbol ~= nil then
self:SetTruncationText(customTruncationSymbol)
end
self:SetNewClipToWidth(clipToWidth)
end,
But then I wonder if it should all only be in |
||
| else | ||
| -- Restore to the original OnDirty callback if it exists, otherwise set it to nil | ||
| if self._originalWidthOnDirty then | ||
| self.Width.OnDirty = self._originalWidthOnDirty | ||
| self._originalWidthOnDirty = nil | ||
| else | ||
| self.Width.OnDirty = nil | ||
| end | ||
| self:SetDisplayText(self._fullText) | ||
| end | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| end, | ||
|
|
||
| --- Adjusts text cropping behavior | ||
| ---@param clipToWidth boolean | ||
| ---@param truncationEnabled? boolean | ||
| ---@param customTruncationSymbol? string | ||
| SetClipToWidth = function(self, clipToWidth, truncationEnabled, customTruncationSymbol) | ||
| if clipToWidth then | ||
| self.Width:Set(function() return self.Right() - self.Left() end) | ||
| else | ||
| self:SetTruncationEnabled(false) | ||
| self.Width:Set(function() return math.floor(self.TextAdvance()) end) | ||
| end | ||
|
|
||
| if truncationEnabled ~= nil then | ||
| self:SetTruncationEnabled(truncationEnabled) | ||
| end | ||
|
|
||
| if customTruncationSymbol ~= nil then | ||
| self:SetTruncationText(customTruncationSymbol) | ||
| end | ||
|
|
||
| self:SetNewClipToWidth(clipToWidth) | ||
| end, | ||
|
|
||
| --- Internal function to fit the truncation string inside the max width | ||
| _applyTruncation = function(self) | ||
|
|
||
| -- Avoid unnecessary truncation, and no width issues | ||
| if self._fullText == "" then return end | ||
|
|
||
| local initialized, maxWidth = pcall(function () return self.Width() end) | ||
| if not initialized or maxWidth <= 0 then | ||
| self:SetDisplayText(self._fullText) | ||
| return | ||
| end | ||
|
|
||
| local truncationText = 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(str) | ||
| return | ||
| end | ||
|
|
||
| --iterate until string + truncation text fit | ||
| local i = STR_Utf8Len(str) | ||
| while i > 0 and self:GetStringAdvance(str .. truncationText) > maxWidth do | ||
| str = STR_Utf8SubString(str, 1, i - 1) | ||
| i = i - 1 | ||
| end | ||
|
|
||
| self:SetDisplayText(str .. truncationText) | ||
| end, | ||
|
|
||
| -- lazy var support | ||
| SetFont = function(self, family, pointsize) | ||
| if self._font then | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.