I would expect that TMPro maxVisibleLines property returns the number of lines that are currently visible but instead it always returns 99999. Is there are specific way in that a TMPro UGUI needs to be used to get a correct value from this or is this property broken?
maxVisibleCharacters, maxVisibleWords and maxVisibleLines control the number of characters, words or lines that will be visible and part of the geometry of the text object. These properties are used to do things like reveal text over time like in the example “17 - Old Computer Terminal” included with TextMesh Pro.
The initial value of these properties is set to some high value to make sure everything is visible unless the user changes these values.
If you are looking for information about the text object like number of characters, words, lines, links, etc. this information is part of the TMP_Text.textInfo which also contains several sub structures like characterInfo[ ], wordInfo[ ], lineInfo[ ], linkInfo[ ], meshInfo[ ], etc.
There are several posts about textInfo on the forum here and on the TMP user forum. See this post for example.
To help you get a better understanding of how to access this information, you can add the “TMP_TextInfoDebugTool.cs” script to any TMP text object. I created this utility script to help visualize the content of the textInfo.
I see. Is there any way by using textInfo.lineInfo to find out the number of currently visible lines in a TMPro field that is set to truncate or scrollrect?
Also, where can I find TMP_TextInfoDebugTool? It doesn’t appear in the component selector.
… Further experimenting with this I think I found a reliable approach to find out the number of visible lines:
- Fill TMPro text area with text.
- Set text area to overflow mode truncate.
- (call textArea.ForceMeshUpdate() if you want the line count right on the same frame).
- Check textInfo.lineCount.
- Set text area to any other required overflow mode.
Still not the most clean approach since the text area first need to be filled with text but a lot better than what I’ve tried so far.
@Stephan_B Could you tell me where I can find TMP_TextInfoDebugTool.cs?
Also, is there any way to figure out whether a line has line breaks in it (thus occupying multiple lines)?
So far I haven’t been successful getting this info because TMPro seems to be splitting wrapped lines that are longer then the text area with into new, concrete lines.
The TMP_TextInfoDebugTool.cs script is located in the “TextMesh Pro/Examples & Extras/Scripts/…” folder. The TMP Examples & Extras package can be imported via the “Window - TextMeshPro - TMP Examples & Extras” menu.
The is no easy way to determine if a line contains a Linefeed other than checking the string for char(10).
Word Wrapping does wrap the text that exceeds the width of the text container to new lines.
In terms of the textInfo.lineCount, it should accurately reflect the # of visible lines factoring in truncation. However, as you have uncovered, the content of the textInfo is only updated when the text object is generated / updated. This regeneration only happens once per frame in late in the update cycle in OnPreCull when properties have changed.
When changing properties of the text object where you need to force a regeneration immediately, you can use ForceMeshUpdate() for that.
@Stephan_B Thanks for the info about retrieving correct lineCount. So the value is only correct for one frame during OnPreCull and then becomes incorrect again? That doesn’t seem very useful though. I’ve checked in OnPreCull in a script attached to my camera and the lineCount value is wrong at any time later after my text area has been generated and filled with text.
The textInfo.lineCount is valid for as long as the text object hasn’t changed.
So if in Update() you change the text, this changes will not be reflected until the text is regenerated in OnPreCull(). After that and unless the text object properties are changed again, the textInfo will be valid and correct.
Here is something to test.
Add the following script to a TMP objects (either type is fine).
Set the initial text and size of RectTransform to have room for 3 lines of text.
Make sure the initial text is only two lines.
Set overflow mode to truncate.
Make sure when the new longer text shows up that it ends up with 3 lines visible with the last line truncated (ie this would be 4 lines if not using Truncate). I used “A short line of text.” with RectTransform size of 20 and height of 13. Using a normal TextMeshPro component.
using UnityEngine;
using TMPro;
public class LineInfoCheck : MonoBehaviour {
TMP_Text m_TextComponent;
private void Awake()
{
m_TextComponent = GetComponent<TMP_Text>();
m_TextComponent.text = "A longer line of text which will be truncated.";
// Unless I force an update of the text object, querying the textInfo.lineCount will be incorrect since the text object hasn't been updated yet to reflect the changes I just made.
m_TextComponent.ForceMeshUpdate();
Debug.Log("The block is text contains " + m_TextComponent.textInfo.lineCount + " lines of text.");
}
}
Without ForceMeshUpdate(), the Debug.Log indicates we only have 2 lines of text. This is correct because although we just changed the text to something that will be 3 lines (4 but the last one is truncated), the textInfo is still representative of last time the text was updated where it had two lines.
Now If I ForceMeshUpdate(), then the text object is updated right then which results in Debug.Log indicating we have 3 lines of text which is correct since the 4th line is truncated.
If I was to query the textInfo after the text object has been updated in something like OnWillRenderObject() which happens after OnPreCull() and without calling ForceMeshUpdate(), Debug.Log would also correctly reflect 3 lines of text as again the 4th line is truncated.
@Stephan_B thank you for the example but I think there’s a misunderstanding …
When I said "splits to concrete lines’ I meant exactly what you’ve just described.
Let’s say I add 5 lines of text to a TMPro text area. One of those lines is too long and is wrapped to the next line.
TMPro will now report that it has 6 concrete lines of text, instead of 5 (incl. one merely wrapped). I guess what I was describing could be called “soft-wrapping”. The text area displays 6 lines but in actual data it’s still 5 lines. That’s the behavior I was looking for.
The number of lines of a text object is simply the number of lines visible.
Essentially all word processors and text processing clients, define / represent lines as I described above. The number of lines is always affected by the width of the text container which is defined from the left to right margin.
How many lines of text is the paragraph above? Given this paragraph only contains one linefeed and if the width of the window / browser was wide enough, it could end up being a single line of text. However, given the current width, it ends up being 3 lines of text.
Why do you need to number of lines to be represented differently? What are you trying to achieve?
@Stephan_B I’m trying to create a scrollable text area with TMPro that can process text with a large number of lines. Since I don’t want to make a too large, masked text area and there’s a limit dependent on the geometry in any case I’m looking for ways to create a text area that gets feed lines from an in-memory buffer when the user scrolls up/down. This would require two TMPro text areas adjacent above/below to each other being scrolled and repositioned while the text needs to be updated accordingly in both fields.
Currently I have this object structure:
Canvas
ScrollArea (with control script and ScrollRect component)
ViewPort (with RectMask2D)
Content (a game object that wraps two TMProUIGUI objects
TMProTextArea0
TMProTextArea1
The challenge is to figure out the height of Content because both TMPro area objects need to have ContentSizeFitter so that the text area extends depending on number of lines (incl. wrapped lines). But the height isn’t even correct after I added the text and call ForceMeshUpdate(). This might be an issue with ContentSizeFitter. I’d prefer not to use ContentSizeFitters but there seems to be no way to figure out the height of the actual text content on a TMPro field that has overflow mode set to overflow and the text extends the initially set height for the TMPro field.
If a TMPro text could be made to update its height so it adapts to any overflowing text, that would be very helpful.
What would be your approach if you wanted to write a scrollable text area as described above?
@Stephan_B Hi I know you must be very busy but it would be great to hear your thoughts and ideas about how to implement a text area as described above.
Bump to resurrect this old threat since the issue still hasn’t been resolved in TMPro 1.3.0.
Once again I want to request a TMPro getter that can tell us how many logical lines are currently, exactly visible in a TMPro text area, that is, the total number of lines, ignoring if they have been wrapped, so:
This is a single line.
This is a single line that is too long and has been
wrapped by TMPro to the next line.
This is a single line.
According to my request a TMProUGUI max visible lines property should return 3. In its current implementation we can only get the concrete line count via TextMeshProUGUI.lineCount wich would return 4 in this case.
Rationale:
To be able to create a text area that can scroll a large amount of text I want feed two TMPro text areas that scroll and adjust their positions with text from a buffer. For this it’s necessary to know how many (non-wrapped) lines of text fit into the text areas so that no lines are missing when switching text between the two areas and calculating offsets.
Since several users have been asking / looking for ways to either display and / or edit large amounts of text over the past few months / year, this is something I have been thinking about while busy working on other TMP related stuff.
My current conclusion is that these needs will best be served by the creation of possibly two new TMP components. The first a plain large text viewer while the other a large text editor.
I would like to build a prototype / example of the large text viewer when time permits but given I am not certain of when that will be, I will describe how this will be structured in the hopes that it will enable current users looking for this functionality to be able to implement something similar without having to wait for me to do so.
This text component will take in the raw source text made up of lots of lines of text and build an internal list where lines / blocks of text are split / separated by a Linefeed.
This component will keep track of the width and height of the view area / scroll position and ultimately what lines are visible.
This component will manage a pool of text objects where each will be responsible for displaying a single line / block of text. Again, lines of text are delimited by Linefeed so based on the width of the view area, these could individually end up wrapping.
Once it is time to display the text, this component will start with the first line / block of text and feed it to the first text component in the pool. It will then query the first text object’s textInfo and lineInfo to determine where the 2nd text object should be positioned relative to this first one. It will then repeat this process until the view area if filled vertically.
As the text scrolls up or down, this component will only be moving these text objects as re-parsing / re-layout of these individual text objects is not necessary. This will make scrolling very efficient as all text objects will be static with the exception of a new text object becoming visible in the view area. This newly visible text object will be feed its text triggering parsing / layout of only this text object.
As text objects go out of view, they will get re-cycled.
Provided each line / block of text are independent of each other, this will provide excellent performance as although the raw text could contain 1000’s of lines at worst (when moving to a new section of the text / new page), only the number of visible lines required to fill the view vertically will need to be re-parsed and re-layout. When normally scrolling up or down then only one line / block will need re-parsing / re-layout.
For those most part this should work nicely when these lines / blocks are independent of each other. However, if rich text is used where a tag spans more than one block then it would require special handling.
This component would use a 2D RectMask to mask the text objects as they go out of view.
Something similar could be implemented using TMP_InputFields but would require a different structure of objects and special handling for navigating / selecting text across multiple individual input fields.
Hopefully this information proves useful to some of you looking to implement your own system.
@Stephan_B Are the plans for implementing such a large text viewer component still “alive”?
Such a component would come handy in our project.
Yes. I actually had a discussion about this yesterday.
The first implementation of this would be a chat system example that would be included in the TMP Examples & Extras.
This chat system would include a single line input combined with the scrollable viewer which has the ability to efficiently display a massive amount of text.
Subsequent to releasing this example, I would like to provide another variant later that would enable user to edit the text in this scrollable view just like a code editor for instance.
I do not have a firm ETA on this yet but this is something that I definitely want to provide.
Is this “massive text editor” project still alive? If so, is there an ETA on when it might be released?
i would really like to have this too! Any ETA?
I’m working on a in-game text(code) editor recently. And also met the issue with line count.
So first is initiate. The in-game text editor can load a code file saved before, and as soon as it opened and the massive text is loaded in the TMP text area, the linecount is 0. The line update is part of the writeTextEvent so until I do something in the text editor, it will keep 0 line there, which is obviously not correct.
Also, it is expected to show current line number when a new line is created, but there is also a latency that the line number will only get update after I input 2 or 3 letters.
As Stephan_B mentioned above, it need to force update mesh to get the correct line count but I’m working on a text editor so considering the performance I shouldn’t put this force update in input text event right?
Is there anything else I can do to fix this?
Side note on ForceMeshUpdate: it doesn’t work if “Awake” hasn’t been called on the Text component yet, not even when ignoreActiveState=true.
The following solution works always, by activating every parent in the tree up to the root, then restoring the active state of each.
public static int GetLineCount(this TMP_Text label)
{
var disableds = label.transform.EnumerateAllParents(true).Where(p => !p.gameObject.activeSelf).ToList();
disableds.ForEach(p => p.gameObject.SetActive(true));
label.ForceMeshUpdate(true);
if (label.textInfo == null)
{
Console.Log($"Label {label.name.Quoted()} doesn't have a 'textInfo' object. This is usually because it has not been 'Awoken' yet. activeSelf: {label.gameObject.activeSelf} activeInHierarchy: {label.gameObject.activeInHierarchy}");
// Try to salvage:
return label.text.Empty() ? 0 : (label.text.Count('\n') + 1);
}
disableds.ForEach(p => p.gameObject.SetActive(false));
return label.textInfo.lineCount;
}
Utility:
public static IEnumerable<Transform> EnumerateAllParents(this Transform go, bool includeSelf = false)
{
if (!includeSelf)
{
go = go?.parent;
}
while (go)
{
yield return go;
go = go.parent;
}
}
public static string Quoted(this string s) => s == null ? "null" : $"\"{s}\"";
public static int Count<T>(this IEnumerable<T> container, T v) { return container.Count(x => EqualityComparer<T>.Default.Equals(x, v)); }
public static void ForEach<TSource>(this IEnumerable<TSource> source, Action<TSource> actor) { foreach (var x in source) { actor(x); } }